From 75c0e28e3d56878dbd492cb0c6d6bf341f9e6de7 Mon Sep 17 00:00:00 2001 From: _ov4 Date: Wed, 21 Oct 2026 10:37:17 +0000 Subject: feat(plugin): highlight type names and template calls Co-Authored-By: Claude Opus 5.5 --- plugin/AGENTS/ARCH.md | 19 +- plugin/ThirdPartyNotices.md | 4 +- plugin/examples/highlighting.cps | 19 + plugin/language-configuration.json | 2 +- plugin/syntaxes/cie-pseudocode.tmLanguage.json | 673 ++++++++++++++++++++----- plugin/test/highlighting.test.js | 80 ++- 6 files changed, 649 insertions(+), 148 deletions(-) (limited to 'plugin') diff --git a/plugin/AGENTS/ARCH.md b/plugin/AGENTS/ARCH.md index 90e9614..5242d3e 100644 --- a/plugin/AGENTS/ARCH.md +++ b/plugin/AGENTS/ARCH.md @@ -13,7 +13,7 @@ there is no compiler library or cpptools integration. comments, brackets, quote pairs and identifiers. - `syntaxes/cie-pseudocode.tmLanguage.json` adapts small C++ comment, quote and comma rules and uses the same general token scope categories. CIE keyword, - builtin, number and operator patterns are locally defined. Exception keywords + function, type, number and operator patterns are locally defined. Exception keywords reuse C++ try/catch/throw scopes, with ENDTRY sharing the TRY scope. - cpptools was consulted for provider registration. Its formatter and language server are not called, imported or bundled. This extension is not a cpptools @@ -95,9 +95,20 @@ only a subset of its syntax: `TYPE Name = ...` declarations do not open a record block. The compiler accepts standard WHILE headers without DO, but the indentation engine still needs DO to activate their body. -- Highlighted builtins: LENGTH, MID, RIGHT, LEFT, LCASE, UCASE, ASC, CHR, IS_NUM, - NUM_TO_STR, STR_TO_NUM and EOF. The compiler's complete table is in - `lib/CodeGen/Builtins.h` and also includes MALLOC, FREE and REALLOC. +- Function highlighting accepts leading underscores and explicit, nested template + arguments. The generic call rule covers the compiler builtins EOF, MALLOC, FREE + and REALLOC as well as ordinary standard-library functions. Builtin definitions + live in `lib/CodeGen/Builtins.h`; string functions live in pseudocode-lib. +- Custom type names use C++ type scopes in TYPE/CLASS declarations, variable and + field declarations, parameter and return types, ARRAY OF, pointer aliases, NEW, + templates and EXTERN parameter lists. Array-bound contexts shield their colons + from type annotation rules; CASE OF does not introduce a type. Balanced template + call lookahead follows Parser::isTemplateCall, requiring a following parenthesis. + No symbol tracking is performed: custom cast names and ordinary value names + stay unclassified. Builtin casts retain their existing type scopes. +- A CLASS method context enables the C++ this scope case-insensitively via a + TextMate injection, including nested declaration expressions. Comments, strings, + fields outside methods and top-level code do not receive this scope. - Highlighting covers CASE/ENDCASE/OTHERWISE, CLASS/ENDCLASS/PUBLIC/PRIVATE, CONSTANT, TEMPLATE, OVERLOAD/OPERATOR, EXTERN declarations, INCLUDE_OBJ, #LIB and ellipses using C++ scopes. OVERLOAD borrows a declaration modifier, #LIB diff --git a/plugin/ThirdPartyNotices.md b/plugin/ThirdPartyNotices.md index 6bb976c..e24f2c8 100644 --- a/plugin/ThirdPartyNotices.md +++ b/plugin/ThirdPartyNotices.md @@ -8,7 +8,9 @@ access, constant, template, operator and external-declaration highlighting also reuse C++ scopes. OVERLOAD borrows a declaration modifier, #LIB borrows #pragma and INCLUDE_OBJ borrows #include; ellipses use the C++ variadic scope. Literal escapes reuse `constant.character.escape`; the adjacent C string suffix borrows -`keyword.other.suffix.literal.user-defined.string.cpp`. +`keyword.other.suffix.literal.user-defined.string.cpp`. Custom types, template +parameters and angle delimiters use the corresponding C++ type and template +scopes; class-method receivers reuse `variable.language.this.cpp`. https://github.com/microsoft/vscode/tree/main/extensions/cpp diff --git a/plugin/examples/highlighting.cps b/plugin/examples/highlighting.cps index f80fd8f..10d93d8 100644 --- a/plugin/examples/highlighting.cps +++ b/plugin/examples/highlighting.cps @@ -56,3 +56,22 @@ CASE OF Index 2 TO Limit : OUTPUT "In range" OTHERWISE : OUTPUT "Other" ENDCASE + +TEMPLATE +FUNCTION _echo(Value : T) RETURNS T + RETURN Value +ENDFUNCTION +OUTPUT _echo(Limit) + +CLASS Counter + PRIVATE Value : INTEGER + PUBLIC PROCEDURE Add(Amount : INTEGER) + this.Value <- this.Value + Amount + ENDPROCEDURE + PUBLIC FUNCTION Get() RETURNS INTEGER + RETURN this.Value + ENDFUNCTION +ENDCLASS +DECLARE Count : Counter +CALL Count.Add(Limit) +OUTPUT Count.Get() diff --git a/plugin/language-configuration.json b/plugin/language-configuration.json index ed3e464..36a3b08 100644 --- a/plugin/language-configuration.json +++ b/plugin/language-configuration.json @@ -9,5 +9,5 @@ { "open": "\"", "close": "\"", "notIn": ["string", "comment"] } ], "surroundingPairs": [["[", "]"], ["(", ")"], ["\"", "\""], ["'", "'"]], - "wordPattern": "[A-Za-z][A-Za-z0-9_]*" + "wordPattern": "[A-Za-z_][A-Za-z0-9_]*" } diff --git a/plugin/syntaxes/cie-pseudocode.tmLanguage.json b/plugin/syntaxes/cie-pseudocode.tmLanguage.json index cdf175a..16a4ac4 100644 --- a/plugin/syntaxes/cie-pseudocode.tmLanguage.json +++ b/plugin/syntaxes/cie-pseudocode.tmLanguage.json @@ -3,145 +3,7 @@ "scopeName": "source.cie-pseudocode", "patterns": [ { - "include": "#line_comment" - }, - { - "include": "#strings" - }, - { - "match": "^\\s*(#)(LIB)\\s*$", - "captures": { - "1": { - "name": "punctuation.definition.directive.cpp" - }, - "2": { - "name": "keyword.control.directive.pragma.cpp" - } - } - }, - { - "match": "(?|<=|>=|[+*/=&^<>-]", - "name": "keyword.operator.cie-pseudocode" - }, - { - "match": "(?", + "name": "punctuation.section.angle-brackets.end.template.call.cpp" + }, + { + "match": "\\.\\.\\.", + "name": "punctuation.vararg-ellipses.cpp" + }, + { + "match": ",", + "name": "punctuation.separator.delimiter.comma.cie-pseudocode" + } + ] + }, + "type_annotation": { + "match": "(:)\\s*((?[A-Za-z_][A-Za-z0-9_]*(?:\\s*<\\s*\\g(?:\\s*,\\s*\\g)*\\s*>)?(?:\\s*\\.\\.\\.)?))", + "captures": { + "1": { + "name": "punctuation.separator.cie-pseudocode" + }, + "2": { + "patterns": [ + { + "include": "#type_tokens" + } + ] + } + } + }, + "element_type": { + "match": "(?[A-Za-z_][A-Za-z0-9_]*(?:\\s*<\\s*\\g(?:\\s*,\\s*\\g)*\\s*>)?(?:\\s*\\.\\.\\.)?))", + "captures": { + "1": { + "name": "storage.modifier.cie-pseudocode" + }, + "2": { + "patterns": [ + { + "include": "#type_tokens" + } + ] + } + } + }, + "return_type": { + "match": "(?[A-Za-z_][A-Za-z0-9_]*(?:\\s*<\\s*\\g(?:\\s*,\\s*\\g)*\\s*>)?(?:\\s*\\.\\.\\.)?))", + "captures": { + "1": { + "name": "storage.modifier.cie-pseudocode" + }, + "2": { + "patterns": [ + { + "include": "#type_tokens" + } + ] + } + } + }, + "array_bounds": { + "begin": "\\[", + "end": "\\]", + "beginCaptures": { + "0": { + "name": "punctuation.section.brackets.cie-pseudocode" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.brackets.cie-pseudocode" + } + }, + "patterns": [ + { + "include": "#array_bounds" + }, + { + "include": "#code" + } + ], + "comment": "Keep array bound expressions outside declaration type annotations, including nested indexing." + }, + "declaration": { + "begin": "(?[A-Za-z_][A-Za-z0-9_]*(?:\\s*<\\s*\\g(?:\\s*,\\s*\\g)*\\s*>)?(?:\\s*\\.\\.\\.)?))", + "captures": { + "1": { + "name": "keyword.operator.cie-pseudocode" + }, + "2": { + "name": "keyword.operator.cie-pseudocode" + }, + "3": { + "patterns": [ + { + "include": "#type_tokens" + } + ] + } + } + }, + { + "include": "#code" + } + ] + }, + "template_declaration": { + "match": "(?[A-Za-z_][A-Za-z0-9_]*(?:\\s*<\\s*\\g(?:\\s*,\\s*\\g)*\\s*>)?(?:\\s*\\.\\.\\.)?)(?:\\s*,\\s*\\g)*\\s*>)", + "captures": { + "1": { + "name": "storage.type.template.cpp" + }, + "2": { + "patterns": [ + { + "include": "#template_parameters" + } + ] + } + } + }, + "template_call": { + "match": "(?[A-Za-z_][A-Za-z0-9_]*(?:\\s*<\\s*\\g(?:\\s*,\\s*\\g)*\\s*>)?(?:\\s*\\.\\.\\.)?)(?:\\s*,\\s*\\g)*\\s*>)(?=\\s*\\()", + "captures": { + "1": { + "name": "entity.name.function.cie-pseudocode" + }, + "2": { + "patterns": [ + { + "include": "#type_tokens" + } + ] + } + }, + "comment": "Balanced type lookahead follows Parser::isTemplateCall; a following parenthesis distinguishes calls from comparisons." + }, + "new_type": { + "match": "(?[A-Za-z_][A-Za-z0-9_]*(?:\\s*<\\s*\\g(?:\\s*,\\s*\\g)*\\s*>)?(?:\\s*\\.\\.\\.)?))", + "captures": { + "1": { + "name": "keyword.operator.new.cpp" + }, + "2": { + "patterns": [ + { + "include": "#type_tokens" + } + ] + } + } + }, + "parameters": { + "begin": "\\(", + "end": "\\)", + "beginCaptures": { + "0": { + "name": "punctuation.section.parens.cie-pseudocode" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.section.parens.cie-pseudocode" + } + }, + "patterns": [ + { + "include": "#array_bounds" + }, + { + "include": "#type_annotation" + }, + { + "include": "#element_type" + }, + { + "include": "#code" + } + ] + }, + "function_header": { + "begin": "(?[A-Za-z_][A-Za-z0-9_]*(?:\\s*<\\s*\\g(?:\\s*,\\s*\\g)*\\s*>)?(?:\\s*\\.\\.\\.)?))", + "captures": { + "2": { + "name": "punctuation.separator.cie-pseudocode" + }, + "3": { + "patterns": [ + { + "include": "#type_tokens" + } + ] + } + } + }, + { + "include": "#code" + } + ] + }, + "code": { + "patterns": [ + { + "include": "#line_comment" + }, + { + "include": "#strings" + }, + { + "include": "#class" + }, + { + "include": "#type_declaration" + }, + { + "include": "#declaration" + }, + { + "include": "#template_declaration" + }, + { + "include": "#function_header" + }, + { + "include": "#extern_parameters" + }, + { + "include": "#new_type" + }, + { + "include": "#return_type" + }, + { + "match": "^\\s*(#)(LIB)\\s*$", + "captures": { + "1": { + "name": "punctuation.definition.directive.cpp" + }, + "2": { + "name": "keyword.control.directive.pragma.cpp" + } + } + }, + { + "match": "(?|<=|>=|[+*/=&^<>-]", + "name": "keyword.operator.cie-pseudocode" + }, + { + "match": "(?", + "name": "punctuation.section.angle-brackets.end.template.definition.cpp" + }, + { + "include": "#type_tokens" + } + ] + } + }, + "injections": { + "L:meta.method.cie-pseudocode -comment -string": { + "patterns": [ + { + "match": "(? { grammar = await registry.loadGrammar('source.cie-pseudocode'); }); -function scopesAt(text, word) { +function scopesAt(text, word, state) { const offset = text.indexOf(word); - return grammar.tokenizeLine(text).tokens.find( + return grammar.tokenizeLine(text, state).tokens.find( token => token.startIndex <= offset && token.endIndex > offset).scopes; } @@ -46,11 +46,85 @@ test('C++ style scopes distinguish keywords, types, literals, operators and buil ["X <- 'A'", 'A', 'string.quoted.single'] ]) assert.ok(scopesAt(text, word).some(s => s.startsWith(scope)), text); for (const name of ['LENGTH', 'MID', 'RIGHT', 'LEFT', 'LCASE', 'UCASE', - 'ASC', 'CHR', 'IS_NUM', 'NUM_TO_STR', 'STR_TO_NUM', 'EOF']) { + 'ASC', 'CHR', 'IS_NUM', 'NUM_TO_STR', 'STR_TO_NUM', 'EOF', 'MALLOC', 'FREE', 'REALLOC']) { assert.ok(scopesAt(`${name}(X)`, name).includes('entity.name.function.cie-pseudocode')); } }); +test('function names include underscores and explicit nested template arguments', () => { + for (const text of ['CALL _print(1)', 'FUNCTION _value() RETURNS INTEGER', 'FUNCTION _value RETURNS INTEGER', + 'OUTPUT _value>>(X)', 'OUTPUT _value > (X)']) { + const name = text.includes('_print') ? '_print' : '_value'; + assert.ok(scopesAt(text, name).includes('entity.name.function.cie-pseudocode'), text); + } + const text = 'OUTPUT _value>>(X)'; + for (const name of ['Box', 'Pair']) assert.ok(scopesAt(text, name).includes('entity.name.type.cpp')); + for (const name of ['INTEGER', 'REAL']) assert.ok(scopesAt(text, name).includes('storage.type.cie-pseudocode')); + assert.ok(scopesAt(text, '<').includes('punctuation.section.angle-brackets.begin.template.call.cpp')); + assert.ok(scopesAt(text, '>').includes('punctuation.section.angle-brackets.end.template.call.cpp')); + for (const expression of ['A < B AND C > D', 'A < B + C > (D)', 'A <= B', 'A <> B']) { + assert.ok(scopesAt(expression, '<').includes('keyword.operator.cie-pseudocode'), expression); + assert.ok(!scopesAt(expression, 'B').includes('entity.name.type.cpp'), expression); + } + const configuration = JSON.parse(fs.readFileSync(path.join(__dirname, '../language-configuration.json'))); + assert.equal('_value'.match(new RegExp(configuration.wordPattern))[0], '_value'); +}); + +test('types are recognized in declarations, signatures, arrays, aliases, constructors and templates', () => { + for (const [text, names] of [ + ['TYPE Student', ['Student']], + ['CLASS Student', ['Student']], + ['TYPE Native = C_TYPE("unsigned int")', ['Native']], + ['TYPE NodePointer = ^Node', ['NodePointer', 'Node']], + ['DECLARE Value : Widget', ['Widget']], + ['DECLARE Values : ARRAY[Low:High, 1:Limit] OF Box', ['Box', 'Widget']], + ['PROCEDURE Work(BYREF Value : Widget, Rest : ARRAY OF Node)', ['Widget', 'Node']], + ['FUNCTION Work(Value : Widget) RETURNS Result', ['Widget', 'Result']], + ['FUNCTION Work() RETURNS void', ['void']], + ['EXTERN "C" EXTERN_FUNCTION print(Native, RawPointer, ...) RETURNS NativeResult', ['Native', 'RawPointer', 'NativeResult']], + ['Value <- NEW Box()', ['Box', 'Widget']], + ['TEMPLATE ', ['T', 'Rest']] + ]) for (const name of names) { + assert.ok(scopesAt(text, name).some(s => /^(entity\.name\.type|storage\.type)\./.test(s)), `${text}: ${name}`); + } + for (const [text, word] of [ + ['CASE OF Selector', 'Selector'], ['Label : OUTPUT Value', 'OUTPUT'], + ['DECLARE Values : ARRAY[Low:High, Indices[Start]:Limit] OF Widget', 'High'], + ['DECLARE Values : ARRAY[Low:High, Indices[Start]:Limit] OF Widget', 'Limit'], + ['OUTPUT (Widget)Value', 'Widget'], ['OUTPUT Value.Field', 'Field'], + ['// DECLARE Value : Widget', 'Widget'], ['OUTPUT "DECLARE Value : Widget"', 'Widget'] + ]) assert.ok(!scopesAt(text, word).includes('entity.name.type.cpp'), `${text}: ${word}`); + let state = grammar.tokenizeLine('PROCEDURE Work(').ruleStack; + assert.ok(scopesAt('Value : Widget,', 'Widget', state).includes('entity.name.type.cpp')); + state = grammar.tokenizeLine('Value : Widget,', state).ruleStack; + state = grammar.tokenizeLine('Other : ARRAY[Low:High] OF Node)', state).ruleStack; + assert.ok(!scopesAt('Label : OUTPUT Value', 'OUTPUT', state).includes('entity.name.type.cpp')); +}); + +test('class fields have type scopes and this is confined to method code', () => { + let state = grammar.tokenizeLine('CLASS Counter').ruleStack; + for (const text of ['PRIVATE Value : Widget', 'PUBLIC Other : Widget', 'Field : Widget']) { + assert.ok(scopesAt(text, 'Widget', state).includes('entity.name.type.cpp')); + state = grammar.tokenizeLine(text, state).ruleStack; + } + assert.ok(!scopesAt('Field : this', 'this', state).includes('variable.language.this.cpp')); + state = grammar.tokenizeLine('PUBLIC PROCEDURE Update(Other : Widget)', state).ruleStack; + for (const word of ['this', 'This', 'THIS']) { + assert.ok(scopesAt(`${word}.Value <- Other`, word, state).includes('variable.language.this.cpp')); + } + for (const text of ['thisValue', '_this', 'this_1', '// this', 'OUTPUT "this"']) { + assert.ok(!scopesAt(text, 'this', state).includes('variable.language.this.cpp'), text); + } + assert.ok(!scopesAt('Label : OUTPUT Value', 'OUTPUT', state).includes('entity.name.type.cpp')); + assert.ok(scopesAt('DECLARE Copy : Widget(this.Value)', 'this', state).includes('variable.language.this.cpp')); + state = grammar.tokenizeLine('ENDPROCEDURE', state).ruleStack; + assert.ok(!scopesAt('Field : this', 'this', state).includes('variable.language.this.cpp')); + state = grammar.tokenizeLine('ENDCLASS', state).ruleStack; + assert.ok(!scopesAt('this <- 1', 'this', state).includes('variable.language.this.cpp')); + state = grammar.tokenizeLine('FUNCTION Outside() RETURNS INTEGER', state).ruleStack; + assert.ok(!scopesAt('this <- 1', 'this', state).includes('variable.language.this.cpp')); +}); + test('both assignment characters share the C++ assignment scope', () => { const text = 'X <- 1'; assert.deepEqual(scopesAt(text, '<-'), scopesAt(text, '-')); -- cgit v1.3.1-4-g156e