diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-13 06:02:56 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-13 06:11:36 +0000 |
| commit | 9ec4fbddd559d93b3d6b072dc8f56d2a572585bd (patch) | |
| tree | ac576c0da3b936ca6198b7f84944c76727a89c6b | |
| parent | e52266d8ce8823866b2dad3e04ffc78438e08397 (diff) | |
feat(plugin): color bracket pairs and mark unmatched brackets red
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| -rw-r--r-- | plugin/package.json | 46 | ||||
| -rw-r--r-- | plugin/src/brackets.js | 25 | ||||
| -rw-r--r-- | plugin/src/extension.js | 26 | ||||
| -rw-r--r-- | plugin/src/lexical.js | 4 | ||||
| -rw-r--r-- | plugin/syntaxes/cie-pseudocode.tmLanguage.json | 4 | ||||
| -rw-r--r-- | plugin/test/brackets.test.js | 31 | ||||
| -rw-r--r-- | plugin/test/highlighting.test.js | 1 |
7 files changed, 122 insertions, 15 deletions
diff --git a/plugin/package.json b/plugin/package.json index 003a866..60cf9da 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -10,20 +10,32 @@ "url": "https://github.com/ovfor4/cie-pseudocode-compiler.git", "directory": "plugin" }, - "engines": { "vscode": "^1.85.0" }, + "engines": { + "vscode": "^1.85.0" + }, "main": "./src/extension.js", - "categories": ["Programming Languages", "Formatters"], + "categories": [ + "Programming Languages", + "Formatters" + ], "scripts": { - "check": "node --check src/extension.js && node --check src/indentation.js && node --check src/lexical.js", + "check": "node --check src/extension.js && node --check src/indentation.js && node --check src/lexical.js && node --check src/brackets.js", "test": "node --test test/*.test.js" }, "contributes": { - "languages": [{ - "id": "cie-pseudocode", - "aliases": ["CIE Pseudocode", "cie-pseudocode"], - "extensions": [".cps"], - "configuration": "./language-configuration.json" - }], + "languages": [ + { + "id": "cie-pseudocode", + "aliases": [ + "CIE Pseudocode", + "cie-pseudocode" + ], + "extensions": [ + ".cps" + ], + "configuration": "./language-configuration.json" + } + ], "configurationDefaults": { "[cie-pseudocode]": { "editor.insertSpaces": false, @@ -31,14 +43,18 @@ "editor.detectIndentation": false, "editor.autoIndent": "full", "editor.formatOnType": true, - "editor.defaultFormatter": "ovfor4.cie-pseudocode" + "editor.defaultFormatter": "ovfor4.cie-pseudocode", + "editor.bracketPairColorization.enabled": true, + "editor.bracketPairColorization.independentColorPoolPerBracketType": false } }, - "grammars": [{ - "language": "cie-pseudocode", - "scopeName": "source.cie-pseudocode", - "path": "./syntaxes/cie-pseudocode.tmLanguage.json" - }] + "grammars": [ + { + "language": "cie-pseudocode", + "scopeName": "source.cie-pseudocode", + "path": "./syntaxes/cie-pseudocode.tmLanguage.json" + } + ] }, "devDependencies": { "vscode-oniguruma": "^2.0.1", diff --git a/plugin/src/brackets.js b/plugin/src/brackets.js new file mode 100644 index 0000000..ad79ea5 --- /dev/null +++ b/plugin/src/brackets.js @@ -0,0 +1,25 @@ +'use strict'; + +const { codeOnly } = require('./lexical'); +const pairs = { ')': '(', ']': '[' }; + +function unmatchedBrackets(text) { + const code = codeOnly(text); + const stack = []; + const unmatched = []; + for (let offset = 0; offset < code.length; offset++) { + const char = code[offset]; + if (char === '(' || char === '[') { + stack.push({ char, offset }); + } else if (pairs[char]) { + const opening = stack.pop(); + if (!opening || opening.char !== pairs[char]) { + if (opening) unmatched.push(opening.offset); + unmatched.push(offset); + } + } + } + return unmatched.concat(stack.map(opening => opening.offset)).sort((a, b) => a - b); +} + +module.exports = { unmatchedBrackets }; diff --git a/plugin/src/extension.js b/plugin/src/extension.js index a7b4834..bfc51a1 100644 --- a/plugin/src/extension.js +++ b/plugin/src/extension.js @@ -3,6 +3,7 @@ const vscode = require('vscode'); const { indentationEdits } = require('./indentation'); const { codeOnly } = require('./lexical'); +const { unmatchedBrackets } = require('./brackets'); const languageId = 'cie-pseudocode'; function formatLines(document, startLine, endLine, keepBlankIndent = false) { @@ -12,7 +13,31 @@ function formatLines(document, startLine, endLine, keepBlankIndent = false) { } function activate(context) { + const invalidBracket = vscode.window.createTextEditorDecorationType({ + dark: { color: '#F44747' } + }); + function refreshBrackets(document) { + const rangesByDocument = new Map(); + for (const editor of vscode.window.visibleTextEditors) { + if (document && editor.document !== document) continue; + if (editor.document.languageId !== languageId) { + editor.setDecorations(invalidBracket, []); + continue; + } + if (!rangesByDocument.has(editor.document)) { + const ranges = unmatchedBrackets(editor.document.getText()).map(offset => + new vscode.Range(editor.document.positionAt(offset), editor.document.positionAt(offset + 1))); + rangesByDocument.set(editor.document, ranges); + } + editor.setDecorations(invalidBracket, rangesByDocument.get(editor.document)); + } + } context.subscriptions.push( + invalidBracket, + vscode.workspace.onDidChangeTextDocument(event => refreshBrackets(event.document)), + // Changing a document's language closes/reopens it without a text edit. + vscode.workspace.onDidOpenTextDocument(() => refreshBrackets()), + vscode.window.onDidChangeVisibleTextEditors(() => refreshBrackets()), vscode.languages.registerDocumentRangeFormattingEditProvider(languageId, { provideDocumentRangeFormattingEdits(document, range) { const endLine = range.end.character === 0 && range.end.line > range.start.line @@ -28,6 +53,7 @@ function activate(context) { } }, '\n', 'E', 'F', 'L', 'T', 'N') ); + refreshBrackets(); } module.exports = { activate }; diff --git a/plugin/src/lexical.js b/plugin/src/lexical.js index 3fc856a..ff01166 100644 --- a/plugin/src/lexical.js +++ b/plugin/src/lexical.js @@ -17,6 +17,10 @@ function codeOnly(text) { } 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; } 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 40a67df..cb7d401 100644 --- a/plugin/syntaxes/cie-pseudocode.tmLanguage.json +++ b/plugin/syntaxes/cie-pseudocode.tmLanguage.json @@ -81,6 +81,10 @@ "strings": { "patterns": [ { + "match": "'''", + "name": "string.quoted.single.cie-pseudocode" + }, + { "begin": "\"", "end": "\"|$", "beginCaptures": { diff --git a/plugin/test/brackets.test.js b/plugin/test/brackets.test.js new file mode 100644 index 0000000..acb18d0 --- /dev/null +++ b/plugin/test/brackets.test.js @@ -0,0 +1,31 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const { test } = require('node:test'); +const { unmatchedBrackets } = require('../src/brackets'); + +test('mixed nesting matches across lines and repaired brackets stop being errors', () => { + assert.deepEqual(unmatchedBrackets('A[(B + C)\n + D[1]]'), []); + assert.deepEqual(unmatchedBrackets('A['), [1]); + assert.deepEqual(unmatchedBrackets('A[1]'), []); + assert.deepEqual(unmatchedBrackets('(\n)'), []); +}); + +test('unpaired opening, closing and crossed bracket types are all reported', () => { + assert.deepEqual(unmatchedBrackets('(['), [0, 1]); + assert.deepEqual(unmatchedBrackets(')]'), [0, 1]); + assert.deepEqual(unmatchedBrackets('([)]'), [0, 1, 2, 3]); + assert.deepEqual(unmatchedBrackets('(]'), [0, 1]); +}); + +test('comments and quotes are ignored with correct UTF-16 offsets after Unicode and CRLF', () => { + assert.deepEqual(unmatchedBrackets('OUTPUT "[( //" // )]\nOUTPUT \'[\''), []); + const unfinished = 'OUTPUT "unfinished [\r\nA['; + assert.deepEqual(unmatchedBrackets(unfinished), [unfinished.length - 1]); + const unicode = 'OUTPUT "你好😀"\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(quoteChar), [quoteChar.length - 1]); +}); diff --git a/plugin/test/highlighting.test.js b/plugin/test/highlighting.test.js index df18f6a..89e031a 100644 --- a/plugin/test/highlighting.test.js +++ b/plugin/test/highlighting.test.js @@ -58,4 +58,5 @@ test('comments, strings, lowercase and keyword prefixes cannot become keywords', 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')); }); |
