Skip to content

Commit 996306d

Browse files
committed
fix: restore syntax highlighting inside/after syntax errors, add tests, configure CI
1 parent 9d21b58 commit 996306d

3 files changed

Lines changed: 132 additions & 3 deletions

File tree

.github/workflows/test-build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ jobs:
6262
ls -la dist | head -10
6363
echo "Total files: $(find dist -type f | wc -l)"
6464
65+
- name: Run core library tests
66+
working-directory: ./anycode-base
67+
run: pnpm test -- --run
68+
6569
- name: Install Rust
6670
uses: dtolnay/rust-toolchain@stable
6771
with:
@@ -81,6 +85,10 @@ jobs:
8185
workspaces: |
8286
anycode-backend -> target
8387
88+
- name: Run Rust backend tests
89+
working-directory: ./anycode-backend
90+
run: cargo test
91+
8492
- name: Build Linux x86_64 musl binary (test)
8593
working-directory: ./anycode-backend
8694
run: cargo build --release --target x86_64-unknown-linux-musl

anycode-base/src/code.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -744,9 +744,19 @@ export class Code {
744744
};
745745

746746
for (; column < lineText.length;) {
747-
const capture = captures.find(
748-
c => c.node.startIndex <= bytesCounter && bytesCounter < c.node.endIndex
749-
);
747+
// Pick the narrowest capture range that contains current byte position.
748+
// This preserves nested/specific highlight precedence without sorting in the hot path.
749+
let capture: Parser.QueryCapture | undefined;
750+
let captureLen = 0;
751+
for (const c of captures) {
752+
if (c.node.startIndex <= bytesCounter && bytesCounter < c.node.endIndex) {
753+
const len = c.node.endIndex - c.node.startIndex;
754+
if (!capture || len < captureLen) {
755+
capture = c;
756+
captureLen = len;
757+
}
758+
}
759+
}
750760
if (capture?.name.startsWith("injection.content.")) {
751761
// --- CASE 1: Injection ---
752762
const injectionData = injectionCapturesArray.find(
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { describe, it, expect, beforeAll } from 'vitest';
2+
import { Code } from '../src/code';
3+
import { setWasmBasePath } from '../src/utils';
4+
import * as path from 'path';
5+
6+
describe('Syntax Highlighting Tests', () => {
7+
beforeAll(() => {
8+
// Set path for local wasm binaries
9+
setWasmBasePath(path.resolve(__dirname, '../wasm') + '/');
10+
});
11+
12+
describe('Basic Highlighting', () => {
13+
it('should correctly highlight basic Python structures', async () => {
14+
const pythonCode = `# This is a comment
15+
def my_func(x):
16+
return x + 42
17+
`;
18+
const code = new Code(pythonCode, 'test.py', 'python');
19+
await code.init();
20+
21+
// Line 0: comment
22+
const nodesLine0 = code.getLineNodes(0);
23+
expect(nodesLine0.some(n => n.name === 'comment' && n.text === '# This is a comment')).toBe(true);
24+
25+
// Line 1: def my_func(x):
26+
const nodesLine1 = code.getLineNodes(1);
27+
expect(nodesLine1.some(n => n.name === 'keyword' && n.text === 'def')).toBe(true);
28+
expect(nodesLine1.some(n => n.name === 'function' && n.text === 'my_func')).toBe(true);
29+
expect(nodesLine1.some(n => n.name === 'variable.parameter' && n.text === 'x')).toBe(true);
30+
31+
// Line 2: return x + 42
32+
const nodesLine2 = code.getLineNodes(2);
33+
expect(nodesLine2.some(n => n.name === 'keyword' && n.text === 'return')).toBe(true);
34+
expect(nodesLine2.some(n => n.name === 'constant' && n.text === '42')).toBe(true);
35+
});
36+
37+
it('should correctly highlight basic JavaScript structures', async () => {
38+
const jsCode = `// Comment
39+
const val = "hello";
40+
if (val === "hello") {
41+
console.log(123);
42+
}
43+
`;
44+
const code = new Code(jsCode, 'test.js', 'javascript');
45+
await code.init();
46+
47+
// Line 0: comment
48+
const nodesLine0 = code.getLineNodes(0);
49+
expect(nodesLine0.some(n => n.name === 'comment' && n.text === '// Comment')).toBe(true);
50+
51+
// Line 1: const val = "hello";
52+
const nodesLine1 = code.getLineNodes(1);
53+
expect(nodesLine1.some(n => n.name === 'keyword' && n.text === 'const')).toBe(true);
54+
expect(nodesLine1.some(n => n.name === 'string' && n.text === '"hello"')).toBe(true);
55+
56+
// Line 2: if (val === "hello") {
57+
const nodesLine2 = code.getLineNodes(2);
58+
expect(nodesLine2.some(n => n.name === 'keyword' && n.text === 'if')).toBe(true);
59+
expect(nodesLine2.some(n => n.name === 'punctuation.bracket' && n.text === '{')).toBe(true);
60+
61+
// Line 3: console.log(123);
62+
const nodesLine3 = code.getLineNodes(3);
63+
expect(nodesLine3.some(n => n.name === 'variable.builtin' && n.text === 'console')).toBe(true);
64+
expect(nodesLine3.some(n => n.name === 'number' && n.text === '123')).toBe(true);
65+
});
66+
});
67+
68+
describe('Syntax Error Recovery Highlighting', () => {
69+
it('should correctly highlight code inside and after syntax errors in Python', async () => {
70+
const pythonCode = `print("hi")
71+
print("hi"
72+
print("hi")
73+
print("hi")
74+
`;
75+
const code = new Code(pythonCode, 'test.py', 'python');
76+
await code.init();
77+
78+
// Line 0: print("hi") - normal highlight
79+
const nodesLine0 = code.getLineNodes(0);
80+
expect(nodesLine0.some(n => n.name === 'function.builtin' && n.text === 'print')).toBe(true);
81+
expect(nodesLine0.some(n => n.name === 'string' && n.text === '"hi"')).toBe(true);
82+
83+
// Line 2 (which is print("hi") after the missing paren on line 1)
84+
const nodesLine2 = code.getLineNodes(2);
85+
expect(nodesLine2.some(n => n.name === 'function.builtin' && n.text === 'print')).toBe(true);
86+
expect(nodesLine2.some(n => n.name === 'string' && n.text === '"hi"')).toBe(true);
87+
// Ensure the entire line is not grouped under a single 'error' token
88+
const errorNodes = nodesLine2.filter(n => n.name === 'error');
89+
if (errorNodes.length > 0) {
90+
expect(errorNodes.some(n => n.text === 'print("hi")')).toBe(false);
91+
}
92+
});
93+
94+
it('should correctly highlight code inside and after syntax errors in JavaScript', async () => {
95+
const jsCode = `console.log("hi");
96+
console.log("hi"
97+
console.log("hi");
98+
`;
99+
const code = new Code(jsCode, 'test.js', 'javascript');
100+
await code.init();
101+
102+
// Line 2 (console.log("hi"); after the missing paren on line 1)
103+
const nodesLine2 = code.getLineNodes(2);
104+
expect(nodesLine2.some(n => n.name === 'string' && n.text === '"hi"')).toBe(true);
105+
const errorNodes = nodesLine2.filter(n => n.name === 'error');
106+
if (errorNodes.length > 0) {
107+
expect(errorNodes.some(n => n.text.includes('console.log'))).toBe(false);
108+
}
109+
});
110+
});
111+
});

0 commit comments

Comments
 (0)