added label syntax support and highlighting
This commit is contained in:
parent
660a80a26b
commit
26b8370b01
@ -42,7 +42,7 @@
|
||||
{ "name": "constant.numeric.decimal.lyng", "match": "(?<![A-Za-z_])(?:[0-9][0-9_]*)\\.(?:[0-9_]+)(?:[eE][+-]?[0-9_]+)?|(?<![A-Za-z_])(?:[0-9][0-9_]*)(?:[eE][+-]?[0-9_]+)?" }
|
||||
]
|
||||
},
|
||||
"annotations": { "patterns": [ { "name": "entity.name.label.at.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*:" }, { "name": "storage.modifier.annotation.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*" } ] },
|
||||
"annotations": { "patterns": [ { "name": "entity.name.label.at.lyng", "match": "@[\\p{L}_][\\p{L}\\p{N}_]*" } ] },
|
||||
"mapLiterals": {
|
||||
"patterns": [
|
||||
{
|
||||
@ -74,7 +74,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] },
|
||||
"labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*@" } ] },
|
||||
"directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] },
|
||||
"declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(fun|fn)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum|interface|object)(?:\\s+([\\p{L}_][\\p{L}\\p{N}_]*))?", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(val|var)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "variable.other.declaration.lyng" } } } ] },
|
||||
"keywords": { "patterns": [ { "name": "keyword.control.lyng", "match": "\\b(?:if|else|when|while|do|for|try|catch|finally|throw|return|break|continue)\\b" }, { "name": "keyword.declaration.lyng", "match": "\\b(?:fun|fn|class|enum|interface|val|var|import|package|constructor|property|abstract|override|open|closed|extern|private|protected|static|get|set|object|init|by)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\bnot\\s+(?:in|is)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\b(?:and|or|not|in|is|as|as\\?)\\b" } ] },
|
||||
|
||||
@ -94,6 +94,16 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
|
||||
val out = ArrayList<Span>(256)
|
||||
|
||||
fun isFollowedByParenOrBlock(rangeEnd: Int): Boolean {
|
||||
var i = rangeEnd
|
||||
while (i < text.length) {
|
||||
val ch = text[i]
|
||||
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') { i++; continue }
|
||||
return ch == '(' || ch == '{'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun putRange(start: Int, end: Int, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
|
||||
if (start in 0..end && end <= text.length && start < end) out += Span(start, end, key)
|
||||
}
|
||||
@ -207,16 +217,6 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
|
||||
fun isFollowedByParenOrBlock(rangeEnd: Int): Boolean {
|
||||
var i = rangeEnd
|
||||
while (i < text.length) {
|
||||
val ch = text[i]
|
||||
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') { i++; continue }
|
||||
return ch == '(' || ch == '{'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Build simple name -> role map for top-level vals/vars and parameters
|
||||
val nameRole = HashMap<String, com.intellij.openapi.editor.colors.TextAttributesKey>(8)
|
||||
for (d in mini.declarations) when (d) {
|
||||
@ -253,7 +253,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
if (e is com.intellij.openapi.progress.ProcessCanceledException) throw e
|
||||
}
|
||||
|
||||
// Add annotation coloring using token highlighter (treat @Label as annotation)
|
||||
// Add annotation/label coloring using token highlighter
|
||||
run {
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
for (s in tokens) if (s.kind == HighlightKind.Label) {
|
||||
@ -261,8 +261,29 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
val end = s.range.endExclusive
|
||||
if (start in 0..end && end <= text.length && start < end) {
|
||||
val lexeme = try { text.substring(start, end) } catch (_: Throwable) { null }
|
||||
if (lexeme != null && lexeme.startsWith("@")) {
|
||||
putRange(start, end, LyngHighlighterColors.ANNOTATION)
|
||||
if (lexeme != null) {
|
||||
// Heuristic: if it starts with @ and follows a control keyword, it's likely a label
|
||||
// Otherwise if it starts with @ it's an annotation.
|
||||
// If it ends with @ it's a loop label.
|
||||
when {
|
||||
lexeme.endsWith("@") -> putRange(start, end, LyngHighlighterColors.LABEL)
|
||||
lexeme.startsWith("@") -> {
|
||||
// Try to see if it's an exit label
|
||||
val prevNonWs = prevNonWs(text, start)
|
||||
val prevWord = if (prevNonWs >= 0) {
|
||||
var wEnd = prevNonWs + 1
|
||||
var wStart = prevNonWs
|
||||
while (wStart > 0 && text[wStart - 1].isLetter()) wStart--
|
||||
text.substring(wStart, wEnd)
|
||||
} else null
|
||||
|
||||
if (prevWord in setOf("return", "break", "continue") || isFollowedByParenOrBlock(end)) {
|
||||
putRange(start, end, LyngHighlighterColors.LABEL)
|
||||
} else {
|
||||
putRange(start, end, LyngHighlighterColors.ANNOTATION)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -364,6 +385,16 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
private val CACHE_KEY: Key<Result> = Key.create("LYNG_SEMANTIC_CACHE")
|
||||
}
|
||||
|
||||
private fun prevNonWs(text: String, idxExclusive: Int): Int {
|
||||
var i = idxExclusive - 1
|
||||
while (i >= 0) {
|
||||
val ch = text[i]
|
||||
if (ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r') return i
|
||||
i--
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the error highlight a bit wider than a single character so it is easier to see and click.
|
||||
* Strategy:
|
||||
|
||||
@ -43,10 +43,15 @@ class LyngColorSettingsPage : ColorSettingsPage {
|
||||
}
|
||||
|
||||
var counter = 0
|
||||
counter = counter + 1
|
||||
outer@ while (counter < 10) {
|
||||
if (counter == 5) return@outer
|
||||
counter = counter + 1
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
override fun getAdditionalHighlightingTagToDescriptorMap(): MutableMap<String, TextAttributesKey>? = null
|
||||
override fun getAdditionalHighlightingTagToDescriptorMap(): MutableMap<String, TextAttributesKey> = mutableMapOf(
|
||||
"label" to LyngHighlighterColors.LABEL
|
||||
)
|
||||
|
||||
override fun getAttributeDescriptors(): Array<AttributesDescriptor> = arrayOf(
|
||||
AttributesDescriptor("Keyword", LyngHighlighterColors.KEYWORD),
|
||||
@ -58,6 +63,7 @@ class LyngColorSettingsPage : ColorSettingsPage {
|
||||
AttributesDescriptor("Punctuation", LyngHighlighterColors.PUNCT),
|
||||
// Semantic
|
||||
AttributesDescriptor("Annotation (semantic)", LyngHighlighterColors.ANNOTATION),
|
||||
AttributesDescriptor("Label (semantic)", LyngHighlighterColors.LABEL),
|
||||
AttributesDescriptor("Variable (semantic)", LyngHighlighterColors.VARIABLE),
|
||||
AttributesDescriptor("Value (semantic)", LyngHighlighterColors.VALUE),
|
||||
AttributesDescriptor("Function (semantic)", LyngHighlighterColors.FUNCTION),
|
||||
|
||||
@ -82,4 +82,9 @@ object LyngHighlighterColors {
|
||||
val ENUM_CONSTANT: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
||||
"LYNG_ENUM_CONSTANT", DefaultLanguageHighlighterColors.STATIC_FIELD
|
||||
)
|
||||
|
||||
// Labels (label@ or @label used as exit target)
|
||||
val LABEL: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
||||
"LYNG_LABEL", DefaultLanguageHighlighterColors.LABEL
|
||||
)
|
||||
}
|
||||
|
||||
@ -133,10 +133,24 @@ class LyngLexer : LexerBase() {
|
||||
return
|
||||
}
|
||||
|
||||
// Identifier / keyword
|
||||
// Labels / Annotations: @label or label@
|
||||
if (ch == '@') {
|
||||
i++
|
||||
while (i < endOffset && (buffer[i].isIdentifierPart())) i++
|
||||
myTokenEnd = i
|
||||
myTokenType = LyngTokenTypes.LABEL
|
||||
return
|
||||
}
|
||||
|
||||
if (ch.isIdentifierStart()) {
|
||||
i++
|
||||
while (i < endOffset && buffer[i].isIdentifierPart()) i++
|
||||
if (i < endOffset && buffer[i] == '@') {
|
||||
i++
|
||||
myTokenEnd = i
|
||||
myTokenType = LyngTokenTypes.LABEL
|
||||
return
|
||||
}
|
||||
myTokenEnd = i
|
||||
val text = buffer.subSequence(myTokenStart, myTokenEnd).toString()
|
||||
myTokenType = if (text in keywords) LyngTokenTypes.KEYWORD else LyngTokenTypes.IDENTIFIER
|
||||
|
||||
@ -33,6 +33,7 @@ class LyngSyntaxHighlighter : SyntaxHighlighter {
|
||||
LyngTokenTypes.BLOCK_COMMENT -> pack(LyngHighlighterColors.BLOCK_COMMENT)
|
||||
LyngTokenTypes.PUNCT -> pack(LyngHighlighterColors.PUNCT)
|
||||
LyngTokenTypes.IDENTIFIER -> pack(LyngHighlighterColors.IDENTIFIER)
|
||||
LyngTokenTypes.LABEL -> pack(LyngHighlighterColors.LABEL)
|
||||
else -> emptyArray()
|
||||
}
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ object LyngTokenTypes {
|
||||
val NUMBER = LyngTokenType("NUMBER")
|
||||
val KEYWORD = LyngTokenType("KEYWORD")
|
||||
val IDENTIFIER = LyngTokenType("IDENTIFIER")
|
||||
val LABEL = LyngTokenType("LABEL")
|
||||
val PUNCT = LyngTokenType("PUNCT")
|
||||
val BAD_CHAR = LyngTokenType("BAD_CHAR")
|
||||
}
|
||||
|
||||
@ -477,7 +477,7 @@ private fun splitIntoParts(
|
||||
private fun applyMinimalSpacingRules(code: String): String {
|
||||
var s = code
|
||||
// Ensure space before '(' for control-flow keywords
|
||||
s = s.replace(Regex("\\b(if|for|while)\\("), "$1 (")
|
||||
s = s.replace(Regex("\\b(if|for|while|return|break|continue)\\("), "$1 (")
|
||||
// Space before '{' for control-flow headers only (avoid function declarations)
|
||||
s = s.replace(Regex("\\b(if|for|while)(\\s*\\([^)]*\\))\\s*\\{"), "$1$2 {")
|
||||
s = s.replace(Regex("\\belse\\s+if(\\s*\\([^)]*\\))\\s*\\{"), "else if$1 {")
|
||||
@ -498,6 +498,8 @@ private fun applyMinimalSpacingRules(code: String): String {
|
||||
s = s.replace(Regex("(\\bcatch\\s*\\([^)]*\\))\\s*\\{"), "$1 {")
|
||||
// Ensure space before '(' for catch parameter
|
||||
s = s.replace(Regex("\\bcatch\\("), "catch (")
|
||||
// Remove space between control keyword and label: return @label -> return@label
|
||||
s = s.replace(Regex("\\b(return|break|continue)\\s+(@[\\p{L}_][\\p{L}\\p{N}_]*)"), "$1$2")
|
||||
// Remove spaces just inside parentheses/brackets: "( a )" -> "(a)"
|
||||
s = s.replace(Regex("\\(\\s+"), "(")
|
||||
// Do not strip leading indentation before a closing bracket/paren on its own line
|
||||
|
||||
@ -21,6 +21,15 @@ import kotlin.test.assertEquals
|
||||
|
||||
class LyngFormatterTest {
|
||||
|
||||
@Test
|
||||
fun labelFormatting() {
|
||||
val src = "return @label; break @outer; continue @inner"
|
||||
val expected = "return@label; break@outer; continue@inner"
|
||||
val cfg = LyngFormatConfig(applySpacing = true)
|
||||
val out = LyngFormatter.format(src, cfg)
|
||||
assertEquals(expected, out)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reindent_simpleFunction() {
|
||||
val src = """
|
||||
|
||||
@ -25,6 +25,16 @@ class HighlightMappingTest {
|
||||
private fun spansToLabeled(text: String, spans: List<HighlightSpan>): List<Pair<String, HighlightKind>> =
|
||||
spans.map { text.substring(it.range.start, it.range.endExclusive) to it.kind }
|
||||
|
||||
@Test
|
||||
fun returnAndExits() {
|
||||
val text = "return 42; break@outer null"
|
||||
val spans = SimpleLyngHighlighter().highlight(text)
|
||||
val labeled = spansToLabeled(text, spans)
|
||||
assertTrue(labeled.any { it.first == "return" && it.second == HighlightKind.Keyword })
|
||||
assertTrue(labeled.any { it.first == "break" && it.second == HighlightKind.Keyword })
|
||||
assertTrue(labeled.any { it.first == "@outer" && it.second == HighlightKind.Label })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keywordsAndIdentifiers() {
|
||||
val text = "a and b or not c"
|
||||
|
||||
@ -77,6 +77,14 @@ fun HomePage() {
|
||||
// Create, transform, and verify — the Lyng way
|
||||
import lyng.stdlib
|
||||
|
||||
fun findFirstPositive(list) {
|
||||
list.forEach {
|
||||
if (it > 0) return@findFirstPositive it
|
||||
}
|
||||
null
|
||||
}
|
||||
assertEquals(42, findFirstPositive([-1, 42, -5]))
|
||||
|
||||
val data = 1..5 // or [1,2,3,4,5]
|
||||
val evens2 = data.filter { it % 2 == 0 }.map { it * it }
|
||||
assertEquals([4, 16], evens2)
|
||||
|
||||
@ -20,6 +20,22 @@ import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class LyngHighlightTest {
|
||||
@Test
|
||||
fun highlightsReturnAndLabels() {
|
||||
val md = """
|
||||
```lyng
|
||||
return 42
|
||||
break@outer null
|
||||
return@fn val
|
||||
```
|
||||
""".trimIndent()
|
||||
val html = renderMarkdown(md)
|
||||
|
||||
assertTrue(html.contains("hl-kw"), "Expected keyword class for 'return': $html")
|
||||
assertTrue(html.contains("hl-lbl") || html.contains("hl-ann"), "Expected label/annotation class for @outer/@fn: $html")
|
||||
assertTrue(html.contains(">>>").xor(true), "Should not contain prompt marker unless expected")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun highlightsLyngFencedBlock() {
|
||||
val md = """
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user