diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-21 10:31:09 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-21 10:31:09 +0000 |
| commit | af6bb061ff447f860c514b60202c3192ceec952b (patch) | |
| tree | 09cf21eb75b8d7f6b1771c38765341836a2b890c /plugin | |
| parent | 9939ebe7f0ec25b042c9d83ed651cde0e2ea3a30 (diff) | |
fix(plugin): recognize literal escapes and C string suffixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'plugin')
| -rw-r--r-- | plugin/AGENTS/ARCH.md | 9 | ||||
| -rw-r--r-- | plugin/ThirdPartyNotices.md | 4 | ||||
| -rw-r--r-- | plugin/examples/highlighting.cps | 5 | ||||
| -rw-r--r-- | plugin/src/lexical.js | 11 | ||||
| -rw-r--r-- | plugin/syntaxes/cie-pseudocode.tmLanguage.json | 38 | ||||
| -rw-r--r-- | plugin/test/brackets.test.js | 9 | ||||
| -rw-r--r-- | plugin/test/highlighting.test.js | 25 | ||||
| -rw-r--r-- | plugin/test/indentation.test.js | 7 |
8 files changed, 80 insertions, 28 deletions
diff --git a/plugin/AGENTS/ARCH.md b/plugin/AGENTS/ARCH.md index 31c5974..90e9614 100644 --- a/plugin/AGENTS/ARCH.md +++ b/plugin/AGENTS/ARCH.md @@ -82,10 +82,11 @@ only a subset of its syntax: ordinary parentheses. `TYPE Name = C_TYPE("...")` is a single-line alias declaration, so it does not open an indentation block. - The compiler decodes C++17 byte escapes and Unicode escapes in literals; strings - encode Unicode as UTF-8 and CHAR Unicode escapes must be ASCII. The plugin's - grammar and quote masking still ignore escapes and retain legacy `'''` handling; - the compiler now requires an escaped quote character (`'\''`). Neither supports - multiline literal continuation. + encode Unicode as UTF-8 and CHAR Unicode escapes must be ASCII. Highlighting + uses the C++ escape scope and string-suffix scope for adjacent uppercase C. + Grammar and shared quote masking honor escaped quotes and backslashes; quote + characters use `'\''`, with no legacy three-quote special case. Neither + supports multiline literal continuation. - Blocks: IF/THEN/ELSE/ENDIF, WHILE/DO/ENDWHILE, REPEAT/UNTIL, FOR/NEXT, FUNCTION/ENDFUNCTION, PROCEDURE/ENDPROCEDURE, record TYPE/ENDTYPE and TRY/CATCH/ENDTRY. CATCH aligns with TRY and starts another indented body; diff --git a/plugin/ThirdPartyNotices.md b/plugin/ThirdPartyNotices.md index 581724f..6bb976c 100644 --- a/plugin/ThirdPartyNotices.md +++ b/plugin/ThirdPartyNotices.md @@ -6,7 +6,9 @@ Microsoft VS Code’s built-in C++ extension. Exception keywords reuse its `keyword.control.throw.cpp` scopes; ENDTRY uses the TRY scope. Selection, class, 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. +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`. https://github.com/microsoft/vscode/tree/main/extensions/cpp diff --git a/plugin/examples/highlighting.cps b/plugin/examples/highlighting.cps index b5deaee..f80fd8f 100644 --- a/plugin/examples/highlighting.cps +++ b/plugin/examples/highlighting.cps @@ -17,7 +17,10 @@ DECLARE Letter : CHAR Ready <- TRUE AND NOT FALSE Total <- -3.5 + 2 * 4 / 2 Text <- "CIE // [()]" & " pseudocode" -Letter <- 'A' +Letter <- '\'' +Text <- "Quote: \"; slash: \\; byte: \x41; Unicode: \u0042" +DECLARE RawText : C_STRING +RawText <- "Native text"C FOR Index <- 1 TO 3 STEP 1 Values[Index] <- Index MOD 2 diff --git a/plugin/src/lexical.js b/plugin/src/lexical.js index ff01166..fff8091 100644 --- a/plugin/src/lexical.js +++ b/plugin/src/lexical.js @@ -1,7 +1,7 @@ 'use strict'; // Preserve UTF-16 offsets and newlines, masking CIE quotes and // comments. -// Unlike C++, CIE has no escapes or multiline literals. +// Backslash escapes protect the next character; literals end at each newline. function codeOnly(text) { let quote = null; let comment = false; @@ -15,12 +15,11 @@ function codeOnly(text) { } else if (comment) { result.push(' '); } else if (quote) { - if (char === quote) quote = null; result.push(' '); - } else if (text.slice(i, i + 3) === "'''") { - // The compiler accepts a quote character as a three-quote CHAR literal. - result.push(' ', ' ', ' '); - i += 2; + if (char === '\\' && i + 1 < text.length && !/[\r\n]/.test(text[i + 1])) { + result.push(' '); + i++; + } else if (char === quote) quote = null; } else if (char === '"' || char === "'") { quote = char; result.push(' '); diff --git a/plugin/syntaxes/cie-pseudocode.tmLanguage.json b/plugin/syntaxes/cie-pseudocode.tmLanguage.json index d0830a8..cdf175a 100644 --- a/plugin/syntaxes/cie-pseudocode.tmLanguage.json +++ b/plugin/syntaxes/cie-pseudocode.tmLanguage.json @@ -160,40 +160,58 @@ "strings": { "patterns": [ { - "match": "'''", - "name": "string.quoted.single.cie-pseudocode" - }, - { "begin": "\"", - "end": "\"|$", + "end": "(\")(C)?|$", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.cie-pseudocode" } }, "endCaptures": { - "0": { + "1": { "name": "punctuation.definition.string.end.cie-pseudocode" + }, + "2": { + "name": "keyword.other.suffix.literal.user-defined.string.cpp" } }, "name": "string.quoted.double.cie-pseudocode", - "patterns": [] + "patterns": [ + { + "include": "#escapes" + } + ] }, { "begin": "'", - "end": "'|$", + "end": "(')|$", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.cie-pseudocode" } }, "endCaptures": { - "0": { + "1": { "name": "punctuation.definition.string.end.cie-pseudocode" } }, "name": "string.quoted.single.cie-pseudocode", - "patterns": [] + "patterns": [ + { + "include": "#escapes" + } + ] + } + ] + }, + "escapes": { + "patterns": [ + { + "match": "\\\\(?:['\"?\\\\abfnrtv]|[0-7]{1,3}|x[0-9A-Fa-f]+|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})", + "name": "constant.character.escape" + }, + { + "match": "\\\\[^\\r\\n]" } ] } diff --git a/plugin/test/brackets.test.js b/plugin/test/brackets.test.js index e144335..a62e657 100644 --- a/plugin/test/brackets.test.js +++ b/plugin/test/brackets.test.js @@ -24,8 +24,11 @@ test('comments and quotes are ignored with correct UTF-16 offsets after Unicode assert.deepEqual(unmatchedBrackets(unfinished), [unfinished.length - 1]); const unicode = 'OUTPUT "Hello😀"\r\nA['; assert.deepEqual(unmatchedBrackets(unicode), [unicode.length - 1]); - // Backslashes do not escape CIE quotes. - assert.deepEqual(unmatchedBrackets('"x\\"['), [4]); - const quoteChar = "OUTPUT ''' ["; + assert.deepEqual(unmatchedBrackets(String.raw`OUTPUT "x\"[(//"`), []); + const backslash = String.raw`OUTPUT "x\\" [`; + assert.deepEqual(unmatchedBrackets(backslash), [backslash.length - 1]); + const quoteChar = String.raw`OUTPUT '\'' [`; assert.deepEqual(unmatchedBrackets(quoteChar), [quoteChar.length - 1]); + const escaped = 'OUTPUT "😀\\"["\r\nA['; + assert.deepEqual(unmatchedBrackets(escaped), [escaped.length - 1]); }); diff --git a/plugin/test/highlighting.test.js b/plugin/test/highlighting.test.js index bfdf0a2..8be3d9c 100644 --- a/plugin/test/highlighting.test.js +++ b/plugin/test/highlighting.test.js @@ -122,10 +122,29 @@ test('comments, strings, lowercase and keyword prefixes cannot become keywords', assert.ok(!scopesAt(text, text.split(' ')[0]).some( s => /^(keyword|storage)\./.test(s)), text); } - // CIE has neither backslash escapes nor multiline string literals. + // An unfinished literal must not carry its string scope onto the next line. let state = grammar.tokenizeLine('OUTPUT "unfinished').ruleStack; assert.ok(grammar.tokenizeLine('IF X THEN', state).tokens[0].scopes.includes( 'keyword.control.cie-pseudocode')); - assert.ok(scopesAt('OUTPUT "x\\" + 1', '+').includes('keyword.operator.cie-pseudocode')); - assert.ok(scopesAt("OUTPUT ''' + 1", '+').includes('keyword.operator.cie-pseudocode')); +}); + +test('literal escapes protect quotes and reuse C++ escape and suffix scopes', () => { + for (const escape of [String.raw`\'`, String.raw`\"`, String.raw`\\`, String.raw`\?`, + ...['a', 'b', 'f', 'n', 'r', 't', 'v', '0', '101', 'x4142', 'u0041', 'U0001F600'] + .map(value => `\\${value}`)]) { + const text = `OUTPUT "${escape} IF // [" + 1`; + assert.ok(scopesAt(text, escape).includes('constant.character.escape'), escape); + assert.ok(scopesAt(text, 'IF').includes('string.quoted.double.cie-pseudocode'), escape); + assert.ok(scopesAt(text, '+').includes('keyword.operator.cie-pseudocode'), escape); + } + assert.ok(scopesAt(String.raw`OUTPUT '\'' + 1`, '+').includes('keyword.operator.cie-pseudocode')); + assert.ok(scopesAt(String.raw`OUTPUT '\x41'`, String.raw`\x41`).includes('constant.character.escape')); + assert.ok(scopesAt('"text"C', 'C').includes('keyword.other.suffix.literal.user-defined.string.cpp')); + for (const text of ['"text" C', '"text"c', "'x'C"]) { + assert.ok(!scopesAt(text, text.at(-1)).includes('keyword.other.suffix.literal.user-defined.string.cpp')); + } + for (const text of ['OUTPUT "unfinished', 'OUTPUT "unfinished\\', "OUTPUT '''"]) { + const state = grammar.tokenizeLine(text).ruleStack; + assert.ok(grammar.tokenizeLine('IF Ready THEN', state).tokens[0].scopes.includes('keyword.control.cie-pseudocode')); + } }); diff --git a/plugin/test/indentation.test.js b/plugin/test/indentation.test.js index 1a9d91e..9140ed3 100644 --- a/plugin/test/indentation.test.js +++ b/plugin/test/indentation.test.js @@ -75,3 +75,10 @@ test('blank lines are empty when formatted and indented at the caret after Enter assert.deepEqual(indentationEdits(lines, line, line, true), [{ line, length: 3, text: '\t' }]); } }); + +test('escaped quotes keep block keywords inside literals masked', () => { + assert.deepEqual(lineDepths([ + String.raw`IF Text = "quoted \" THEN"`, + 'THEN', String.raw`OUTPUT "escaped \" ENDIF //"`, 'ENDIF' + ]), [0, 0, 1, 0]); +}); |
