From 4aed3b341ca01ca9f8ce5713dea22bea29a638dd Mon Sep 17 00:00:00 2001 From: David de Hilster Date: Tue, 13 Oct 2026 14:36:34 -0400 Subject: [PATCH] The grammars use standard scope names, and the colours follow Resyncs analyzer-views/src/grammars/ with nlpplus-tmbundle main (655c78d; was ab5328e), which renamed every invented scope to its TextMate-convention equivalent (nlpplus-tmbundle#3, vscode-nlp#107). colors.ts is rewritten through the same old-to-new map the extension uses to migrate its own rules (vscode-nlp src/language/scopeMigration.ts), and is again identical, rule for rule, to vscode-nlp's .vscode/settings.json: 18 light rules, 16 dark. Rendered through highlight.ts before and after, light and dark, on real files: .tree and .seq are identical; in .nlp, region markers take the theme's control-keyword colour, the suggested concept the type colour, and "=" the operator colour; .dict headwords, keys and values take the tag, property and string colours; .kbb numbers take the extension's blue, as they already do in VS Code. The resync also brings the grammar fixes since ab5328e (block comments in the data formats, the base modifier and node constants). Co-Authored-By: Claude Opus 5.5 --- analyzer-views/src/colors.ts | 52 +++++++++---------- analyzer-views/src/grammars/README.md | 2 +- .../src/grammars/dict.tmLanguage.json | 36 +++++++++---- .../src/grammars/kb.tmLanguage.json | 24 +++++++-- .../src/grammars/kbb.tmLanguage.json | 32 +++++++++--- .../src/grammars/nlp.tmLanguage.json | 38 +++++++------- .../src/grammars/seq.tmLanguage.json | 4 +- .../src/grammars/tree.tmLanguage.json | 14 ++--- .../src/grammars/txxt.tmLanguage.json | 2 +- analyzer-views/src/highlight.ts | 6 +-- docs/ARCHITECTURE.md | 9 ++-- 11 files changed, 136 insertions(+), 83 deletions(-) diff --git a/analyzer-views/src/colors.ts b/analyzer-views/src/colors.ts index 68e9dae..e6e61a7 100644 --- a/analyzer-views/src/colors.ts +++ b/analyzer-views/src/colors.ts @@ -1,12 +1,12 @@ // The colours VisualText gives NLP++ tokens, as the VS Code extension sets them. // -// The extension's grammars name scopes of their own -- keyword.node.tree, -// keyword.rewrite.tree, keyword.concept.kbb2 -- which no theme knows, so VS Code themes -// would colour them all as plain keywords. The extension therefore ships +// The grammars use standard TextMate scope names (nlpplus-tmbundle#3), so any theme +// colours NLP++ reasonably. On top of that the extension ships its own // editor.tokenColorCustomizations in the .vscode/settings.json of its repository and of // the analyzer repositories (vscode-nlp/.vscode/settings.json), with one set of rules for // light themes and one for dark. Parse trees are the clearest case: node names green, -// rewrites bold, offsets blue, "fired" red. +// rewrites bold, offsets blue, "fired" red. A rule for an old scope name that covered +// several things lists all of their new names (vscode-nlp src/language/scopeMigration.ts). // // Copied from there rule for rule, so the two can be compared, and folded into the shiki // themes in highlight.ts. PURE: no DOM. @@ -18,41 +18,41 @@ export interface TokenRule { export const LIGHT_RULES: TokenRule[] = [ { scope: "entity.name.function.letter.nlp", settings: { foreground: "#dc1b1b" } }, { scope: "entity.name.function.nlp", settings: { foreground: "#000000" } }, - { scope: "variable.parameter.nlp", settings: { foreground: "#696969", fontStyle: "bold" } }, - { scope: "keyword.attribute.nlp", settings: { foreground: "#e655f3" } }, - { scope: "keyword.constants.nlp", settings: { foreground: "#0a7b83", fontStyle: "bold" } }, + { scope: ["variable.parameter.nlp", "keyword.control.kb"], settings: { foreground: "#696969", fontStyle: "bold" } }, + { scope: "entity.other.attribute-name.nlp", settings: { foreground: "#e655f3" } }, + { scope: "constant.language.nlp", settings: { foreground: "#0a7b83", fontStyle: "bold" } }, { scope: ["keyword.operator.nlp", "keyword.operator.word.nlp"], settings: { foreground: "#1f6feb" } }, - { scope: "constant.numeric.tree", settings: { foreground: "#5596f0" } }, - { scope: "keyword.fired.tree", settings: { foreground: "#dc1b1b", fontStyle: "bold" } }, - { scope: "keyword.rewrite.tree", settings: { foreground: "#000000", fontStyle: "bold" } }, - { scope: "keyword.node.tree", settings: { foreground: "#4e8e3c", fontStyle: "bold" } }, + { scope: ["constant.numeric.tree", "constant.numeric.kbb"], settings: { foreground: "#5596f0" } }, + { scope: "keyword.other.fired.tree", settings: { foreground: "#dc1b1b", fontStyle: "bold" } }, + { scope: "entity.name.type.tree", settings: { foreground: "#000000", fontStyle: "bold" } }, + { scope: "string.unquoted.token.tree", settings: { foreground: "#4e8e3c", fontStyle: "bold" } }, { scope: "source.txxt", settings: { foreground: "#a5a5a5" } }, { scope: "keyword.other.txxt", settings: { foreground: "#6e60eb", fontStyle: "bold" } }, { scope: "comment.line.txxt", settings: { foreground: "#dd2c2c", fontStyle: "bold" } }, { scope: "variable.parameter.txxt", settings: { foreground: "#535353", fontStyle: "bold" } }, - { scope: "keyword.other.kbb", settings: { foreground: "#5d00d6", fontStyle: "bold" } }, - { scope: "keyword.concept.kbb", settings: { foreground: "#7e7e7e", fontStyle: "bold" } }, - { scope: "keyword.concept.kbb1", settings: { foreground: "#2eb657", fontStyle: "bold" } }, - { scope: "keyword.concept.kbb2", settings: { foreground: "#e049d9", fontStyle: "bold" } }, + { scope: "entity.name.type.level0.kbb", settings: { foreground: "#5d00d6", fontStyle: "bold" } }, + { scope: "entity.name.type.level1.kbb", settings: { foreground: "#7e7e7e", fontStyle: "bold" } }, + { scope: "entity.name.type.level2.kbb", settings: { foreground: "#2eb657", fontStyle: "bold" } }, + { scope: "entity.name.type.level3.kbb", settings: { foreground: "#e049d9", fontStyle: "bold" } }, ]; export const DARK_RULES: TokenRule[] = [ { scope: "entity.name.function.letter.nlp", settings: { foreground: "#dc1b1b" } }, { scope: "entity.name.function.nlp", settings: { foreground: "#8f8f8f", fontStyle: "bold" } }, - { scope: "variable.parameter.nlp", settings: { foreground: "#f8fa87", fontStyle: "bold" } }, - { scope: "keyword.attribute.nlp", settings: { foreground: "#f293fb" } }, - { scope: "keyword.constants.nlp", settings: { foreground: "#4ec9b0", fontStyle: "bold" } }, + { scope: ["variable.parameter.nlp", "keyword.control.kb"], settings: { foreground: "#f8fa87", fontStyle: "bold" } }, + { scope: "entity.other.attribute-name.nlp", settings: { foreground: "#f293fb" } }, + { scope: "constant.language.nlp", settings: { foreground: "#4ec9b0", fontStyle: "bold" } }, { scope: ["keyword.operator.nlp", "keyword.operator.word.nlp"], settings: { foreground: "#8ab4f8" } }, - { scope: "constant.numeric.tree", settings: { foreground: "#93bffb" } }, - { scope: "keyword.fired.tree", settings: { foreground: "#dc1b1b", fontStyle: "bold" } }, - { scope: "keyword.rewrite.tree", settings: { foreground: "#f8fa87", fontStyle: "bold" } }, - { scope: "keyword.node.tree", settings: { foreground: "#4e8e3c", fontStyle: "bold" } }, + { scope: ["constant.numeric.tree", "constant.numeric.kbb"], settings: { foreground: "#93bffb" } }, + { scope: "keyword.other.fired.tree", settings: { foreground: "#dc1b1b", fontStyle: "bold" } }, + { scope: "entity.name.type.tree", settings: { foreground: "#f8fa87", fontStyle: "bold" } }, + { scope: "string.unquoted.token.tree", settings: { foreground: "#4e8e3c", fontStyle: "bold" } }, { scope: "source.txxt", settings: { foreground: "#7e7e7e" } }, { scope: "variable.parameter.txxt", settings: { foreground: "#f8fa87" } }, - { scope: "keyword.other.kbb", settings: { foreground: "#896eff", fontStyle: "bold" } }, - { scope: "keyword.concept.kbb", settings: { foreground: "#e3ee50", fontStyle: "bold" } }, - { scope: "keyword.concept.kbb1", settings: { foreground: "#2eb657", fontStyle: "bold" } }, - { scope: "keyword.concept.kbb2", settings: { foreground: "#e049d9", fontStyle: "bold" } }, + { scope: "entity.name.type.level0.kbb", settings: { foreground: "#896eff", fontStyle: "bold" } }, + { scope: "entity.name.type.level1.kbb", settings: { foreground: "#e3ee50", fontStyle: "bold" } }, + { scope: "entity.name.type.level2.kbb", settings: { foreground: "#2eb657", fontStyle: "bold" } }, + { scope: "entity.name.type.level3.kbb", settings: { foreground: "#e049d9", fontStyle: "bold" } }, ]; // The tree scopes the extension's rules colour, for the self test to check they arrived. diff --git a/analyzer-views/src/grammars/README.md b/analyzer-views/src/grammars/README.md index 8e60b42..bc221df 100644 --- a/analyzer-views/src/grammars/README.md +++ b/analyzer-views/src/grammars/README.md @@ -1,6 +1,6 @@ # NLP++ TextMate grammars A copy of `syntaxes/` from [VisualText/nlpplus-tmbundle](https://github.com/VisualText/nlpplus-tmbundle) -at `ab5328e`, under its MIT license (`LICENSE`, beside this file). These are the grammars the NLP++ +at `655c78d`, under its MIT license (`LICENSE`, beside this file). These are the grammars the NLP++ VS Code extension uses. Shiki tokenizes with them (`../highlight.ts`), for `` and for NLP Studio's Monaco editor. diff --git a/analyzer-views/src/grammars/dict.tmLanguage.json b/analyzer-views/src/grammars/dict.tmLanguage.json index d29cb6f..3153579 100644 --- a/analyzer-views/src/grammars/dict.tmLanguage.json +++ b/analyzer-views/src/grammars/dict.tmLanguage.json @@ -15,7 +15,10 @@ "patterns": [ { "include": "#comment-line" - }, + }, + { + "include": "#comment-block" + }, { "include": "#attribute-equals" }, @@ -37,16 +40,16 @@ "match": "^([^=]+)(\\s)([\\w\\d]+)(\\s)*(=)", "captures": { "1": { - "name": "comment.line.dict" + "name": "entity.name.tag.dict" }, "2": { - "name": "entity.nothing.dict" + "name": "meta.whitespace.dict" }, "3": { - "name": "keyword.region.dict" + "name": "support.type.property-name.dict" }, "4": { - "name": "keyword.region.dict" + "name": "meta.whitespace.dict" }, "5": { "name": "keyword.operator.dict" @@ -55,16 +58,16 @@ }, "value": { "match": "[\\w\\d]+", - "name": "entity.name.function.dict" + "name": "string.unquoted.value.dict" }, "other-attrs": { "match": "([\\w]+)(\\s)*(=)", "captures": { "1": { - "name": "keyword.region.dict" + "name": "support.type.property-name.dict" }, "2": { - "name": "keyword.region.dict" + "name": "meta.whitespace.dict" }, "3": { "name": "keyword.operator.dict" @@ -75,10 +78,25 @@ "match": "\\s*#.*$\\n?", "name": "comment.line.dict" }, + "comment-block": { + "begin": "/\\*", + "beginCaptures": { + "0": { + "name": "comment.block.begin.dict" + } + }, + "end": "\\*/", + "endCaptures": { + "0": { + "name": "comment.block.end.dict" + } + }, + "name": "comment.block.dict" + }, "qstring-double": { "begin": "\"", "end": "\\\"|(?:[^\\\\\\n]$)", - "name": "string.double.dict", + "name": "string.quoted.double.dict", "patterns": [ { "include": "#string-character-escape" diff --git a/analyzer-views/src/grammars/kb.tmLanguage.json b/analyzer-views/src/grammars/kb.tmLanguage.json index 378cc01..a699973 100644 --- a/analyzer-views/src/grammars/kb.tmLanguage.json +++ b/analyzer-views/src/grammars/kb.tmLanguage.json @@ -6,7 +6,7 @@ "name": "kb", "patterns": [ { - "name": "variable.parameter.nlp", + "name": "keyword.control.kb", "match": "\\b(?i:(add|bind|ind|quit|take))\\b" }, { @@ -28,6 +28,9 @@ { "include": "#comment-line" }, + { + "include": "#comment-block" + }, { "include": "#number" }, @@ -46,7 +49,7 @@ "qstring-double": { "begin": "\"", "end": "\\\"|(?:[^\\\\\\n]$)", - "name": "string.double.kb", + "name": "string.quoted.double.kb", "patterns": [ { "include": "#string-character-escape" @@ -63,7 +66,22 @@ }, "comment-line": { "match": "\\s*#.*$\\n?", - "name": "comment.line.dict" + "name": "comment.line.kb" + }, + "comment-block": { + "begin": "/\\*", + "beginCaptures": { + "0": { + "name": "comment.block.begin.kb" + } + }, + "end": "\\*/", + "endCaptures": { + "0": { + "name": "comment.block.end.kb" + } + }, + "name": "comment.block.kb" } } } \ No newline at end of file diff --git a/analyzer-views/src/grammars/kbb.tmLanguage.json b/analyzer-views/src/grammars/kbb.tmLanguage.json index 80bcfc7..9fce346 100644 --- a/analyzer-views/src/grammars/kbb.tmLanguage.json +++ b/analyzer-views/src/grammars/kbb.tmLanguage.json @@ -16,6 +16,9 @@ { "include": "#comment-line" }, + { + "include": "#comment-block" + }, { "include": "#top-concept" }, @@ -41,22 +44,22 @@ }, "top-concept": { "match": "^([\\w\\d]+)$", - "name": "keyword.other.kbb" + "name": "entity.name.type.level0.kbb" }, "concept-name": { - "name": "keyword.concept.kbb", + "name": "entity.name.type.level1.kbb", "match": "^[\\s][\\s][\\w\\d][^:^\\[]+[:]?" }, "concept-name1": { - "name": "keyword.concept.kbb1", + "name": "entity.name.type.level2.kbb", "match": "^[\\s][\\s][\\s][\\s][\\w\\d][^:^\\[]+[:]?" }, "concept-name2": { - "name": "keyword.concept.kbb2", + "name": "entity.name.type.level3.kbb", "match": "^[\\s][\\s][\\s][\\s][\\s][\\s][\\w\\d][^:^\\[]+[:]?" }, "attribute-name": { - "name": "constant.numeric.tree", + "name": "constant.numeric.kbb", "match": "\\b[\\w\\d]+=" }, "attributes": { @@ -75,7 +78,7 @@ "qstring-double": { "begin": "\"", "end": "\\\"|(?:[^\\\\\\n]$)", - "name": "string.double.kbb", + "name": "string.quoted.double.kbb", "patterns": [ { "include": "#string-character-escape" @@ -92,7 +95,22 @@ }, "comment-line": { "match": "\\s*#.*$\\n?", - "name": "comment.line.dict" + "name": "comment.line.kbb" + }, + "comment-block": { + "begin": "/\\*", + "beginCaptures": { + "0": { + "name": "comment.block.begin.kbb" + } + }, + "end": "\\*/", + "endCaptures": { + "0": { + "name": "comment.block.end.kbb" + } + }, + "name": "comment.block.kbb" } } } \ No newline at end of file diff --git a/analyzer-views/src/grammars/nlp.tmLanguage.json b/analyzer-views/src/grammars/nlp.tmLanguage.json index 6663ba1..116b824 100644 --- a/analyzer-views/src/grammars/nlp.tmLanguage.json +++ b/analyzer-views/src/grammars/nlp.tmLanguage.json @@ -10,11 +10,11 @@ "include": "#expression" }, { - "name": "keyword.region.nlp", + "name": "keyword.control.directive.region.nlp", "match": "(@(@)?)(CHECK|CODE|DECL|MULTI|NODES|PATH|POST|PRE|RULES)\\b" }, { - "name": "keyword.constants.nlp", + "name": "constant.language.nlp", "match": "\\b_ROOT\\b" }, { @@ -85,7 +85,7 @@ "begin": "([_\\w]+)\\s*((\\[[^\\]]*\\]))*\\s*(<-)", "beginCaptures": { "1": { - "name": "variable.rewrite.nlp" + "name": "entity.name.type.rewrite.nlp" }, "2": { "patterns": [ @@ -95,13 +95,13 @@ ] }, "3": { - "name": "variable.rewrite-symbol.nlp" + "name": "meta.attribute.rewrite.nlp" } }, "end": "@@", "endCaptures": { "0": { - "name": "keyword.region.nlp" + "name": "keyword.control.directive.region.nlp" } }, "patterns": [ @@ -118,7 +118,7 @@ "include": "#escaped-char" }, { - "include": "#comment-line" + "include": "#comment" } ] }, @@ -126,13 +126,13 @@ "begin": "\\[", "beginCaptures": { "0": { - "name": "punctuation.square.open" + "name": "punctuation.square.open.nlp" } }, "end": "\\]", "endCaptures": { "0": { - "name": "punctuation.square.close" + "name": "punctuation.square.close.nlp" } }, "patterns": [ @@ -152,7 +152,7 @@ "include": "#attribute" }, { - "include": "#comment-line" + "include": "#comment" }, { "include": "#string" @@ -167,8 +167,8 @@ "name": "meta.rules.rule.attribute.nlp" }, "constant-nodes": { - "name": "keyword.constants.nlp", - "match": "\\b_(xWILD|xNUM|xALPHA|xWHITE|xNIL|xSTART|xEND|xCTRL)\\b" + "name": "constant.language.nlp", + "match": "\\b_(xALPHA|xANY|xBLANK|xCAPLET|xCAP|xCTRL|xEMOJI|xEND|xEOF|xLET|xNIL|xNUM|xPUNCT|xSTART|xVAR|xWHITE|xWILD)\\b" }, "attribute-equals": { "begin": "([_\\w]+)\\s*(=)\\s*(\\()", @@ -181,16 +181,16 @@ ] }, "2": { - "name": "keyword.equals.nlp" + "name": "keyword.operator.assignment.nlp" }, "3": { - "name": "punctuation.paren.open" + "name": "punctuation.paren.open.nlp" } }, "end": "\\)", "endCaptures": { "0": { - "name": "punctuation.paren.open" + "name": "punctuation.paren.close.nlp" } }, "patterns": [ @@ -209,11 +209,11 @@ ] }, "attribute": { - "name": "keyword.attribute.nlp", - "match": "\\b(?i:(attr|attrs|da|deacc|deaccent|except|excepts|fail|fails|gp|group|layer|layers|look|lookahead|match|matches|max|min|nest|o|one|opt|option|optional|pass|passes|plus|recurse|ren|rename|s|singlet|star|t|tree|trig|trigger|unsealed))\\b" + "name": "entity.other.attribute-name.nlp", + "match": "\\b(?i:(attr|attrs|base|da|deacc|deaccent|except|excepts|fail|fails|gp|group|layer|layers|look|lookahead|match|matches|max|min|nest|o|one|opt|option|optional|pass|passes|plus|recurse|ren|rename|s|singlet|star|t|tree|trig|trigger|unsealed))\\b" }, "escaped-char": { - "name": "constant.character.escape", + "name": "constant.character.escape.nlp", "match": "\\\\." }, "token": { @@ -261,7 +261,7 @@ "qstring-double": { "begin": "\"", "end": "\\\"|(?:[^\\\\\\n]$)", - "name": "string.double.nlp", + "name": "string.quoted.double.nlp", "patterns": [ { "include": "#string-character-escape" @@ -273,7 +273,7 @@ "name": "constant.character.escape.nlp" }, "literal": { - "name": "literal.nlp", + "name": "meta.literal.nlp", "patterns": [ { "include": "#numeric-literal" diff --git a/analyzer-views/src/grammars/seq.tmLanguage.json b/analyzer-views/src/grammars/seq.tmLanguage.json index 0a85a8e..9070ef4 100644 --- a/analyzer-views/src/grammars/seq.tmLanguage.json +++ b/analyzer-views/src/grammars/seq.tmLanguage.json @@ -69,7 +69,7 @@ "qstring-double": { "begin": "\"", "end": "\\\"|(?:[^\\\\\\n]$)", - "name": "string.double.seq", + "name": "string.quoted.double.seq", "patterns": [ { "include": "#string-character-escape" @@ -81,7 +81,7 @@ "name": "constant.character.escape.seq" }, "literal": { - "name": "literal.seq", + "name": "meta.literal.seq", "patterns": [ { "include": "#numeric-literal" diff --git a/analyzer-views/src/grammars/tree.tmLanguage.json b/analyzer-views/src/grammars/tree.tmLanguage.json index 4d67e93..67d3e19 100644 --- a/analyzer-views/src/grammars/tree.tmLanguage.json +++ b/analyzer-views/src/grammars/tree.tmLanguage.json @@ -30,10 +30,10 @@ }, "node-name": { "match": "^([\\s]+)([^_\\s]+)", - "name": "keyword.node.tree" + "name": "string.unquoted.token.tree" }, "rewrites": { - "name": "keyword.rewrite.tree", + "name": "entity.name.type.tree", "match": "\\b_[\\w]+\\b" }, "attributes": { @@ -55,7 +55,7 @@ "qstring-double": { "begin": "\"", "end": "\\\"|(?:[^\\\\\\n]$)", - "name": "string.double.tree", + "name": "string.quoted.double.tree", "patterns": [ { "include": "#string-character-escape" @@ -67,7 +67,7 @@ "name": "constant.character.escape.tree" }, "literal": { - "name": "literal.tree", + "name": "meta.literal.tree", "patterns": [ { "include": "#numeric-literal" @@ -86,15 +86,15 @@ }, "fired": { "match": "\\b(?i:(fired))\\b", - "name": "keyword.fired.tree" + "name": "keyword.other.fired.tree" }, "types": { "match": "\\b(?i:(alpha|ctrl|emoji|node|num|punct|white))\\b", - "name": "keyword.attribute.tree" + "name": "storage.type.tree" }, "comment-line": { "match": "\\s*#.*$\\n?", - "name": "comment.line.dict" + "name": "comment.line.tree" } } } \ No newline at end of file diff --git a/analyzer-views/src/grammars/txxt.tmLanguage.json b/analyzer-views/src/grammars/txxt.tmLanguage.json index da8f440..4bec7a5 100644 --- a/analyzer-views/src/grammars/txxt.tmLanguage.json +++ b/analyzer-views/src/grammars/txxt.tmLanguage.json @@ -11,7 +11,7 @@ ], "repository": { "expression": { - "name": "meta.expression.tree", + "name": "meta.expression.txxt", "patterns": [ { "include": "#angle-match" diff --git a/analyzer-views/src/highlight.ts b/analyzer-views/src/highlight.ts index d8b1414..d3022b0 100644 --- a/analyzer-views/src/highlight.ts +++ b/analyzer-views/src/highlight.ts @@ -5,9 +5,9 @@ // loaded on first use -- the grammars and themes are chunks nobody pays for until code is // on screen. renders its tokens; NLP Studio hands the same highlighter to Monaco. // -// THE EXTENSION'S COLOURS. The grammars name scopes of their own (keyword.node.tree, -// keyword.concept.kbb2, ...) that no stock theme knows, so the extension's rules for them -// (colors.ts) go into light-plus and dark-plus first. +// THE EXTENSION'S COLOURS. The grammars use standard scope names, so light-plus and +// dark-plus colour them on their own; the extension's own rules (colors.ts) go in first +// so a parse tree is green, bold and red the way VS Code shows it. // // BOTH THEMES AT ONCE. Tokens carry their light and dark colours as --shiki-light and // --shiki-dark, and style.css picks one, so a theme change repaints without tokenising. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4c7902f..5f9f8b0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -135,13 +135,12 @@ files the analyzer wrote, problems, and the engine's log. Its parse trees open i `kbView`/`textView` give them. `analyzer-views/src/icons.ts` holds that artwork inlined from `vscode-nlp/resources` (MIT), shapes carrying `currentColor` so one copy serves both themes and a switched-off pass is the same shape in grey rather than a second drawing. -- **Tree colours are the extension's.** The grammars name scopes no stock theme knows - (`keyword.node.tree`, `keyword.rewrite.tree`, `keyword.concept.kbb2`), which is why VS Code - colours them through `editor.tokenColorCustomizations` in the `.vscode/settings.json` that - `vscode-nlp` and the analyzer repositories ship. `analyzer-views/src/colors.ts` holds those +- **Tree colours are the extension's.** The grammars use standard scope names, which any + theme colours, but VS Code also colours them through `editor.tokenColorCustomizations` in + the `.vscode/settings.json` that `vscode-nlp` and the analyzer repositories ship. `analyzer-views/src/colors.ts` holds those rules, rule for rule, for light and for dark, and `analyzer-views/src/highlight.ts` folds them into both shiki themes, for the editor and for `` alike. - Without them a tree is one flat keyword colour. + Without them a tree takes the stock theme's colours rather than the extension's. - **Pass numbers are the engine's.** A switched-off pass (`/nlp name`) keeps its number and a folder or stub has none. That was measured, not assumed, and both the page (`analyzer-views/src/sequence.ts`) and the server number `analyzer.seq` the same way.