aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-21 10:31:09 +0000
committer_ov4 <ov4@ov4.org>2026-10-21 10:31:09 +0000
commitaf6bb061ff447f860c514b60202c3192ceec952b (patch)
tree09cf21eb75b8d7f6b1771c38765341836a2b890c /plugin
parent9939ebe7f0ec25b042c9d83ed651cde0e2ea3a30 (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.md9
-rw-r--r--plugin/ThirdPartyNotices.md4
-rw-r--r--plugin/examples/highlighting.cps5
-rw-r--r--plugin/src/lexical.js11
-rw-r--r--plugin/syntaxes/cie-pseudocode.tmLanguage.json38
-rw-r--r--plugin/test/brackets.test.js9
-rw-r--r--plugin/test/highlighting.test.js25
-rw-r--r--plugin/test/indentation.test.js7
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]);
+});