@pellux/goodvibes-agent 0.1.58 → 0.1.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/docs/getting-started.md +1 -1
- package/package.json +3 -28
- package/src/cli/package-verification.ts +0 -3
- package/src/input/commands/guidance-runtime.ts +1 -2
- package/src/input/commands/health-runtime.ts +2 -36
- package/src/input/commands/shell-core.ts +1 -12
- package/src/input/commands.ts +0 -4
- package/src/input/submission-router.ts +1 -1
- package/src/panels/builtin/operations.ts +0 -15
- package/src/panels/index.ts +0 -2
- package/src/panels/provider-health-domains.ts +0 -27
- package/src/panels/provider-health-panel.ts +0 -4
- package/src/renderer/code-block.ts +2 -13
- package/src/version.ts +1 -1
- package/.goodvibes/GOODVIBES.md +0 -35
- package/scripts/check-bun.sh +0 -22
- package/src/input/commands/intelligence-runtime.ts +0 -223
- package/src/input/commands/teamwork-runtime.ts +0 -339
- package/src/panels/debug-panel.ts +0 -432
- package/src/panels/intelligence-panel.ts +0 -176
- package/src/renderer/semantic-diff.ts +0 -369
- package/src/renderer/syntax-highlighter.ts +0 -542
|
@@ -1,542 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SyntaxHighlighter — Tree-sitter-powered syntax highlighting for code blocks.
|
|
3
|
-
*
|
|
4
|
-
* Designed for the synchronous TUI render loop:
|
|
5
|
-
* - Initializes tree-sitter WASM and grammar parsers asynchronously in background
|
|
6
|
-
* - Returns cached highlight data synchronously from highlight()
|
|
7
|
-
* - Falls back to empty array (caller uses regex tokenizer) when parser not ready
|
|
8
|
-
* - Caches parsed results keyed by language + content hash to avoid re-parsing
|
|
9
|
-
*
|
|
10
|
-
* Vaporwave color theme:
|
|
11
|
-
* Keywords: #d000ff (purple)
|
|
12
|
-
* Strings: #00ff88 (green)
|
|
13
|
-
* Numbers: #ffcc00 (yellow)
|
|
14
|
-
* Comments: #666666 (dim grey)
|
|
15
|
-
* Functions/methods: #00ffff (cyan)
|
|
16
|
-
* Types/classes: #ff6b9d (pink)
|
|
17
|
-
* Operators: #ffffff (white)
|
|
18
|
-
* Properties: #87ceeb (light blue)
|
|
19
|
-
* Built-ins/special: #ff8c00 (orange)
|
|
20
|
-
* Default: 252 (light grey)
|
|
21
|
-
*/
|
|
22
|
-
import type { Node } from 'web-tree-sitter';
|
|
23
|
-
import { TreeSitterService } from '@pellux/goodvibes-sdk/platform/intelligence';
|
|
24
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
25
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
26
|
-
|
|
27
|
-
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
export interface SyntaxToken {
|
|
30
|
-
text: string;
|
|
31
|
-
fg: string;
|
|
32
|
-
bold?: boolean;
|
|
33
|
-
italic?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type HighlightedLine = SyntaxToken[];
|
|
37
|
-
|
|
38
|
-
// ─── Language Alias Map ──────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
// Maps fence tag language strings → tree-sitter language IDs
|
|
41
|
-
const FENCE_TO_LANG_ID: Record<string, string> = {
|
|
42
|
-
ts: 'typescript',
|
|
43
|
-
tsx: 'tsx',
|
|
44
|
-
typescript: 'typescript',
|
|
45
|
-
js: 'javascript',
|
|
46
|
-
jsx: 'javascript',
|
|
47
|
-
javascript: 'javascript',
|
|
48
|
-
mjs: 'javascript',
|
|
49
|
-
py: 'python',
|
|
50
|
-
python: 'python',
|
|
51
|
-
rs: 'rust',
|
|
52
|
-
rust: 'rust',
|
|
53
|
-
go: 'go',
|
|
54
|
-
golang: 'go',
|
|
55
|
-
json: 'json',
|
|
56
|
-
json5: 'json',
|
|
57
|
-
css: 'css',
|
|
58
|
-
scss: 'css',
|
|
59
|
-
sh: 'bash',
|
|
60
|
-
bash: 'bash',
|
|
61
|
-
shell: 'bash',
|
|
62
|
-
zsh: 'bash',
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// ─── Vaporwave Color Mapping ─────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
// Map tree-sitter node types to vaporwave theme colors.
|
|
68
|
-
// The node types are specific to each grammar's output.
|
|
69
|
-
const NODE_TYPE_COLORS: Record<string, { fg: string; bold?: boolean; italic?: boolean }> = {
|
|
70
|
-
// ── Keywords
|
|
71
|
-
'if': { fg: '#d000ff', bold: true },
|
|
72
|
-
'else': { fg: '#d000ff', bold: true },
|
|
73
|
-
'return': { fg: '#d000ff', bold: true },
|
|
74
|
-
'const': { fg: '#d000ff', bold: true },
|
|
75
|
-
'let': { fg: '#d000ff', bold: true },
|
|
76
|
-
'var': { fg: '#d000ff', bold: true },
|
|
77
|
-
'function': { fg: '#d000ff', bold: true },
|
|
78
|
-
'class': { fg: '#d000ff', bold: true },
|
|
79
|
-
'import': { fg: '#d000ff', bold: true },
|
|
80
|
-
'export': { fg: '#d000ff', bold: true },
|
|
81
|
-
'from': { fg: '#d000ff', bold: true },
|
|
82
|
-
'new': { fg: '#d000ff', bold: true },
|
|
83
|
-
'typeof': { fg: '#d000ff', bold: true },
|
|
84
|
-
'instanceof': { fg: '#d000ff', bold: true },
|
|
85
|
-
'in': { fg: '#d000ff', bold: true },
|
|
86
|
-
'of': { fg: '#d000ff', bold: true },
|
|
87
|
-
'for': { fg: '#d000ff', bold: true },
|
|
88
|
-
'while': { fg: '#d000ff', bold: true },
|
|
89
|
-
'do': { fg: '#d000ff', bold: true },
|
|
90
|
-
'switch': { fg: '#d000ff', bold: true },
|
|
91
|
-
'case': { fg: '#d000ff', bold: true },
|
|
92
|
-
'break': { fg: '#d000ff', bold: true },
|
|
93
|
-
'continue': { fg: '#d000ff', bold: true },
|
|
94
|
-
'throw': { fg: '#d000ff', bold: true },
|
|
95
|
-
'try': { fg: '#d000ff', bold: true },
|
|
96
|
-
'catch': { fg: '#d000ff', bold: true },
|
|
97
|
-
'finally': { fg: '#d000ff', bold: true },
|
|
98
|
-
'async': { fg: '#d000ff', bold: true },
|
|
99
|
-
'await': { fg: '#d000ff', bold: true },
|
|
100
|
-
'yield': { fg: '#d000ff', bold: true },
|
|
101
|
-
'delete': { fg: '#d000ff', bold: true },
|
|
102
|
-
'void': { fg: '#d000ff', bold: true },
|
|
103
|
-
'static': { fg: '#d000ff', bold: true },
|
|
104
|
-
'extends': { fg: '#d000ff', bold: true },
|
|
105
|
-
'implements': { fg: '#d000ff', bold: true },
|
|
106
|
-
'interface': { fg: '#d000ff', bold: true },
|
|
107
|
-
'type': { fg: '#d000ff', bold: true },
|
|
108
|
-
'enum': { fg: '#d000ff', bold: true },
|
|
109
|
-
'namespace': { fg: '#d000ff', bold: true },
|
|
110
|
-
'abstract': { fg: '#d000ff', bold: true },
|
|
111
|
-
'readonly': { fg: '#d000ff', bold: true },
|
|
112
|
-
'as': { fg: '#d000ff', bold: true },
|
|
113
|
-
'satisfies': { fg: '#d000ff', bold: true },
|
|
114
|
-
// Python keywords
|
|
115
|
-
'def': { fg: '#d000ff', bold: true },
|
|
116
|
-
'lambda': { fg: '#d000ff', bold: true },
|
|
117
|
-
'with': { fg: '#d000ff', bold: true },
|
|
118
|
-
'pass': { fg: '#d000ff', bold: true },
|
|
119
|
-
'global': { fg: '#d000ff', bold: true },
|
|
120
|
-
'nonlocal': { fg: '#d000ff', bold: true },
|
|
121
|
-
'assert': { fg: '#d000ff', bold: true },
|
|
122
|
-
'raise': { fg: '#d000ff', bold: true },
|
|
123
|
-
'except': { fg: '#d000ff', bold: true },
|
|
124
|
-
'elif': { fg: '#d000ff', bold: true },
|
|
125
|
-
'and': { fg: '#d000ff', bold: true },
|
|
126
|
-
'or': { fg: '#d000ff', bold: true },
|
|
127
|
-
'not': { fg: '#d000ff', bold: true },
|
|
128
|
-
'is': { fg: '#d000ff', bold: true },
|
|
129
|
-
// Bash keywords
|
|
130
|
-
'then': { fg: '#d000ff', bold: true },
|
|
131
|
-
'fi': { fg: '#d000ff', bold: true },
|
|
132
|
-
'done': { fg: '#d000ff', bold: true },
|
|
133
|
-
'esac': { fg: '#d000ff', bold: true },
|
|
134
|
-
|
|
135
|
-
// ── Strings
|
|
136
|
-
'string': { fg: '#00ff88' },
|
|
137
|
-
'string_fragment': { fg: '#00ff88' },
|
|
138
|
-
'template_string': { fg: '#00ff88' },
|
|
139
|
-
'escape_sequence': { fg: '#00ff88' },
|
|
140
|
-
'raw_string': { fg: '#00ff88' },
|
|
141
|
-
'concatenated_string': { fg: '#00ff88' },
|
|
142
|
-
'string_content': { fg: '#00ff88' },
|
|
143
|
-
'quoted_attribute_value': { fg: '#00ff88' },
|
|
144
|
-
'attribute_value': { fg: '#00ff88' },
|
|
145
|
-
'pair_value': { fg: '#00ff88' },
|
|
146
|
-
'plain_value': { fg: '#00ff88' },
|
|
147
|
-
|
|
148
|
-
// ── Numbers
|
|
149
|
-
'number': { fg: '#ffcc00' },
|
|
150
|
-
'integer': { fg: '#ffcc00' },
|
|
151
|
-
'float': { fg: '#ffcc00' },
|
|
152
|
-
'decimal_integer_literal': { fg: '#ffcc00' },
|
|
153
|
-
'hex_integer_literal': { fg: '#ffcc00' },
|
|
154
|
-
'octal_integer_literal': { fg: '#ffcc00' },
|
|
155
|
-
'binary_integer_literal': { fg: '#ffcc00' },
|
|
156
|
-
|
|
157
|
-
// ── Comments
|
|
158
|
-
'comment': { fg: '#666666', italic: true },
|
|
159
|
-
'line_comment': { fg: '#666666', italic: true },
|
|
160
|
-
'block_comment': { fg: '#666666', italic: true },
|
|
161
|
-
'shebang': { fg: '#666666', italic: true },
|
|
162
|
-
|
|
163
|
-
// ── Functions/methods
|
|
164
|
-
'function_declaration': { fg: '#00ffff' },
|
|
165
|
-
'method_declaration': { fg: '#00ffff' },
|
|
166
|
-
'method_definition': { fg: '#00ffff' },
|
|
167
|
-
'arrow_function': { fg: '#00ffff' },
|
|
168
|
-
'function_expression': { fg: '#00ffff' },
|
|
169
|
-
'call_expression': { fg: '#00ffff' },
|
|
170
|
-
'function_definition': { fg: '#00ffff' }, // Python
|
|
171
|
-
|
|
172
|
-
// ── Types and classes
|
|
173
|
-
'type_identifier': { fg: '#ff6b9d' },
|
|
174
|
-
'type_annotation': { fg: '#ff6b9d' },
|
|
175
|
-
'class_declaration': { fg: '#ff6b9d' },
|
|
176
|
-
'class_definition': { fg: '#ff6b9d' }, // Python
|
|
177
|
-
'interface_declaration': { fg: '#ff6b9d' },
|
|
178
|
-
'type_alias_declaration': { fg: '#ff6b9d' },
|
|
179
|
-
'predefined_type': { fg: '#ff6b9d' },
|
|
180
|
-
'builtin_type': { fg: '#ff6b9d' },
|
|
181
|
-
'tag_name': { fg: '#ff6b9d' },
|
|
182
|
-
'element': { fg: '#ff6b9d' },
|
|
183
|
-
|
|
184
|
-
// ── Operators
|
|
185
|
-
'+': { fg: '#ffffff' },
|
|
186
|
-
'-': { fg: '#ffffff' },
|
|
187
|
-
'*': { fg: '#ffffff' },
|
|
188
|
-
'/': { fg: '#ffffff' },
|
|
189
|
-
'%': { fg: '#ffffff' },
|
|
190
|
-
'=': { fg: '#ffffff' },
|
|
191
|
-
'==': { fg: '#ffffff' },
|
|
192
|
-
'===': { fg: '#ffffff' },
|
|
193
|
-
'!=': { fg: '#ffffff' },
|
|
194
|
-
'!==': { fg: '#ffffff' },
|
|
195
|
-
'<': { fg: '#ffffff' },
|
|
196
|
-
'>': { fg: '#ffffff' },
|
|
197
|
-
'<=': { fg: '#ffffff' },
|
|
198
|
-
'>=': { fg: '#ffffff' },
|
|
199
|
-
'&&': { fg: '#ffffff' },
|
|
200
|
-
'||': { fg: '#ffffff' },
|
|
201
|
-
'??': { fg: '#ffffff' },
|
|
202
|
-
'=>': { fg: '#ffffff' },
|
|
203
|
-
'!': { fg: '#ffffff' },
|
|
204
|
-
'&': { fg: '#ffffff' },
|
|
205
|
-
'|': { fg: '#ffffff' },
|
|
206
|
-
'^': { fg: '#ffffff' },
|
|
207
|
-
'~': { fg: '#ffffff' },
|
|
208
|
-
'<<': { fg: '#ffffff' },
|
|
209
|
-
'>>': { fg: '#ffffff' },
|
|
210
|
-
'>>>': { fg: '#ffffff' },
|
|
211
|
-
|
|
212
|
-
// ── Properties
|
|
213
|
-
'property_identifier': { fg: '#87ceeb' },
|
|
214
|
-
'shorthand_property_identifier': { fg: '#87ceeb' },
|
|
215
|
-
'attribute_name': { fg: '#87ceeb' },
|
|
216
|
-
'property_name': { fg: '#87ceeb' },
|
|
217
|
-
'pair_key': { fg: '#87ceeb' },
|
|
218
|
-
|
|
219
|
-
// ── Built-ins / special values
|
|
220
|
-
'true': { fg: '#ff8c00' },
|
|
221
|
-
'false': { fg: '#ff8c00' },
|
|
222
|
-
'null': { fg: '#ff8c00' },
|
|
223
|
-
'undefined': { fg: '#ff8c00' },
|
|
224
|
-
'none': { fg: '#ff8c00' },
|
|
225
|
-
'None': { fg: '#ff8c00' },
|
|
226
|
-
'True': { fg: '#ff8c00' },
|
|
227
|
-
'False': { fg: '#ff8c00' },
|
|
228
|
-
'this': { fg: '#ff8c00' },
|
|
229
|
-
'super': { fg: '#ff8c00' },
|
|
230
|
-
'self': { fg: '#ff8c00' },
|
|
231
|
-
'boolean': { fg: '#ff8c00' },
|
|
232
|
-
|
|
233
|
-
// ── JSON specific
|
|
234
|
-
'json_string': { fg: '#00ff88' },
|
|
235
|
-
'json_key': { fg: '#87ceeb' },
|
|
236
|
-
'json_number': { fg: '#ffcc00' },
|
|
237
|
-
|
|
238
|
-
// ── CSS specific
|
|
239
|
-
'class_selector': { fg: '#ff6b9d' },
|
|
240
|
-
'id_selector': { fg: '#ff6b9d' },
|
|
241
|
-
'pseudo_class_selector': { fg: '#d000ff' },
|
|
242
|
-
'pseudo_element_selector': { fg: '#d000ff' },
|
|
243
|
-
'property_name_css': { fg: '#87ceeb' },
|
|
244
|
-
'unit': { fg: '#ffcc00' },
|
|
245
|
-
'color_value': { fg: '#00ff88' },
|
|
246
|
-
'at_keyword': { fg: '#d000ff', bold: true },
|
|
247
|
-
'important': { fg: '#d000ff', bold: true },
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// Default color for unrecognized node types
|
|
251
|
-
const DEFAULT_FG = '252';
|
|
252
|
-
|
|
253
|
-
// ─── Content Hash ─────────────────────────────────────────────────────────────
|
|
254
|
-
|
|
255
|
-
/** Cheap DJB2-variant hash for cache keying. */
|
|
256
|
-
function hashString(s: string): number {
|
|
257
|
-
let h = 5381;
|
|
258
|
-
for (let i = 0; i < s.length; i++) {
|
|
259
|
-
h = ((h << 5) + h) ^ s.charCodeAt(i);
|
|
260
|
-
h = h >>> 0; // keep unsigned 32-bit
|
|
261
|
-
}
|
|
262
|
-
return h;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ─── AST Walker ──────────────────────────────────────────────────────────────
|
|
266
|
-
|
|
267
|
-
interface Span {
|
|
268
|
-
startRow: number;
|
|
269
|
-
startCol: number;
|
|
270
|
-
endRow: number;
|
|
271
|
-
endCol: number;
|
|
272
|
-
text: string;
|
|
273
|
-
fg: string;
|
|
274
|
-
bold?: boolean;
|
|
275
|
-
italic?: boolean;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Walk the AST and collect leaf nodes with their positions and colors.
|
|
280
|
-
* Returns a flat array of colored spans that covers all tokens in the code.
|
|
281
|
-
*/
|
|
282
|
-
function collectSpans(root: Node, code: string): Span[] {
|
|
283
|
-
const spans: Span[] = [];
|
|
284
|
-
|
|
285
|
-
function getStyle(node: Node): { fg: string; bold?: boolean; italic?: boolean } | null {
|
|
286
|
-
// Named nodes (keywords, identifiers, etc.)
|
|
287
|
-
const namedStyle = NODE_TYPE_COLORS[node.type];
|
|
288
|
-
if (namedStyle) return namedStyle;
|
|
289
|
-
|
|
290
|
-
// Anonymous nodes (punctuation, operators, keywords stored as literals)
|
|
291
|
-
if (!node.isNamed) {
|
|
292
|
-
const text = node.text.trim();
|
|
293
|
-
const literalStyle = NODE_TYPE_COLORS[text];
|
|
294
|
-
if (literalStyle) return literalStyle;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function visit(node: Node): void {
|
|
301
|
-
// Leaf nodes: emit a span
|
|
302
|
-
if (node.childCount === 0) {
|
|
303
|
-
const style = getStyle(node);
|
|
304
|
-
const text = node.text;
|
|
305
|
-
if (text.length === 0) return;
|
|
306
|
-
spans.push({
|
|
307
|
-
startRow: node.startPosition.row,
|
|
308
|
-
startCol: node.startPosition.column,
|
|
309
|
-
endRow: node.endPosition.row,
|
|
310
|
-
endCol: node.endPosition.column,
|
|
311
|
-
text,
|
|
312
|
-
fg: style?.fg ?? DEFAULT_FG,
|
|
313
|
-
bold: style?.bold,
|
|
314
|
-
italic: style?.italic,
|
|
315
|
-
});
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// For named nodes with a dominant style (comments, strings, etc.),
|
|
320
|
-
// emit as a single span rather than recursing into children.
|
|
321
|
-
// This prevents partial coloring of multi-char nodes.
|
|
322
|
-
const style = getStyle(node);
|
|
323
|
-
if (style && isLeafLike(node)) {
|
|
324
|
-
const text = node.text;
|
|
325
|
-
if (text.length === 0) return;
|
|
326
|
-
spans.push({
|
|
327
|
-
startRow: node.startPosition.row,
|
|
328
|
-
startCol: node.startPosition.column,
|
|
329
|
-
endRow: node.endPosition.row,
|
|
330
|
-
endCol: node.endPosition.column,
|
|
331
|
-
text,
|
|
332
|
-
fg: style.fg,
|
|
333
|
-
bold: style.bold,
|
|
334
|
-
italic: style.italic,
|
|
335
|
-
});
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Recurse into children
|
|
340
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
341
|
-
const child = node.child(i);
|
|
342
|
-
if (child) visit(child);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
visit(root);
|
|
347
|
-
|
|
348
|
-
// Sort spans by start position for proper ordering
|
|
349
|
-
spans.sort((a, b) => {
|
|
350
|
-
if (a.startRow !== b.startRow) return a.startRow - b.startRow;
|
|
351
|
-
return a.startCol - b.startCol;
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
return spans;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** Node types where we emit the whole subtree as one colored span. */
|
|
358
|
-
const LEAF_LIKE_TYPES = new Set([
|
|
359
|
-
'string', 'template_string', 'comment', 'line_comment', 'block_comment',
|
|
360
|
-
'raw_string', 'concatenated_string', 'string_content', 'shebang',
|
|
361
|
-
'attribute_value', 'quoted_attribute_value',
|
|
362
|
-
]);
|
|
363
|
-
|
|
364
|
-
function isLeafLike(node: Node): boolean {
|
|
365
|
-
return LEAF_LIKE_TYPES.has(node.type);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// ─── Span → Per-line Token Arrays ────────────────────────────────────────────
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Convert a flat list of positioned spans into per-line SyntaxToken arrays.
|
|
372
|
-
* Handles multi-line spans (e.g., block comments, template literals).
|
|
373
|
-
*/
|
|
374
|
-
function spansToLines(spans: Span[], codeLines: string[]): HighlightedLine[] {
|
|
375
|
-
// Initialize result: one entry per code line, each starting with no tokens
|
|
376
|
-
const result: HighlightedLine[] = codeLines.map(() => []);
|
|
377
|
-
|
|
378
|
-
// Track the current position to emit default-colored text for gaps
|
|
379
|
-
const linePositions: number[] = codeLines.map(() => 0);
|
|
380
|
-
|
|
381
|
-
for (const span of spans) {
|
|
382
|
-
// For single-line spans
|
|
383
|
-
if (span.startRow === span.endRow) {
|
|
384
|
-
const row = span.startRow;
|
|
385
|
-
if (row >= codeLines.length) continue;
|
|
386
|
-
|
|
387
|
-
// Skip if we've already passed this position (overlap)
|
|
388
|
-
if (linePositions[row] > span.startCol) continue;
|
|
389
|
-
|
|
390
|
-
// Emit gap text with default color
|
|
391
|
-
const currentCol = linePositions[row];
|
|
392
|
-
if (currentCol < span.startCol) {
|
|
393
|
-
const gapText = codeLines[row].slice(currentCol, span.startCol);
|
|
394
|
-
if (gapText) result[row].push({ text: gapText, fg: DEFAULT_FG });
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const tokenText = codeLines[row].slice(span.startCol, span.endCol);
|
|
398
|
-
if (tokenText) {
|
|
399
|
-
result[row].push({ text: tokenText, fg: span.fg, bold: span.bold, italic: span.italic });
|
|
400
|
-
}
|
|
401
|
-
linePositions[row] = span.endCol;
|
|
402
|
-
} else {
|
|
403
|
-
// Multi-line span: slice each line
|
|
404
|
-
for (let r = span.startRow; r <= span.endRow; r++) {
|
|
405
|
-
if (r >= codeLines.length) break;
|
|
406
|
-
|
|
407
|
-
const colStart = r === span.startRow ? span.startCol : 0;
|
|
408
|
-
const colEnd = r === span.endRow ? span.endCol : codeLines[r].length;
|
|
409
|
-
|
|
410
|
-
// Overlap guard FIRST
|
|
411
|
-
if (linePositions[r] > colStart) continue;
|
|
412
|
-
|
|
413
|
-
// Emit gap
|
|
414
|
-
const currentCol = linePositions[r];
|
|
415
|
-
if (currentCol < colStart) {
|
|
416
|
-
const gapText = codeLines[r].slice(currentCol, colStart);
|
|
417
|
-
if (gapText) result[r].push({ text: gapText, fg: DEFAULT_FG });
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const tokenText = codeLines[r].slice(colStart, colEnd);
|
|
421
|
-
if (tokenText) {
|
|
422
|
-
result[r].push({ text: tokenText, fg: span.fg, bold: span.bold, italic: span.italic });
|
|
423
|
-
}
|
|
424
|
-
linePositions[r] = colEnd;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Fill remaining text on each line with default color
|
|
430
|
-
for (let r = 0; r < codeLines.length; r++) {
|
|
431
|
-
const remaining = codeLines[r].slice(linePositions[r]);
|
|
432
|
-
if (remaining) result[r].push({ text: remaining, fg: DEFAULT_FG });
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return result;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// ─── SyntaxHighlighter Class ──────────────────────────────────────────────────
|
|
439
|
-
|
|
440
|
-
const MAX_HIGHLIGHT_CACHE = 200;
|
|
441
|
-
|
|
442
|
-
export class SyntaxHighlighter {
|
|
443
|
-
private service: TreeSitterService;
|
|
444
|
-
private cache: Map<string, HighlightedLine[]> = new Map();
|
|
445
|
-
private pending: Set<string> = new Set();
|
|
446
|
-
|
|
447
|
-
constructor() {
|
|
448
|
-
this.service = new TreeSitterService();
|
|
449
|
-
// Kick off WASM initialization in background
|
|
450
|
-
this.service.initialize().catch((err: unknown) => {
|
|
451
|
-
logger.warn('SyntaxHighlighter: background init failed', { error: summarizeError(err) });
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Map a fence tag language string to a tree-sitter language ID.
|
|
457
|
-
* Returns null if the language is not supported by tree-sitter.
|
|
458
|
-
*/
|
|
459
|
-
fenceToLangId(fenceTag: string): string | null {
|
|
460
|
-
return FENCE_TO_LANG_ID[fenceTag.toLowerCase()] ?? null;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Synchronous highlight lookup.
|
|
465
|
-
*
|
|
466
|
-
* If the highlight cache has a result for this code+language, returns it.
|
|
467
|
-
* Otherwise, schedules an async parse in background and returns null.
|
|
468
|
-
* Callers should fall back to regex-based tokenization when null is returned.
|
|
469
|
-
*/
|
|
470
|
-
highlight(code: string, fenceTag: string): HighlightedLine[] | null {
|
|
471
|
-
const langId = this.fenceToLangId(fenceTag);
|
|
472
|
-
if (!langId) return null; // unsupported language
|
|
473
|
-
|
|
474
|
-
const key = `${langId}:${hashString(code)}`;
|
|
475
|
-
const cached = this.cache.get(key);
|
|
476
|
-
if (cached) return cached;
|
|
477
|
-
|
|
478
|
-
// Schedule background parse if not already pending
|
|
479
|
-
if (!this.pending.has(key)) {
|
|
480
|
-
this.scheduleParse(code, langId, key);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return null; // not ready yet
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Schedule an async parse. Fires and forgets — result lands in cache.
|
|
488
|
-
* Callers will pick it up on the next render cycle.
|
|
489
|
-
*/
|
|
490
|
-
private scheduleParse(code: string, langId: string, key: string): void {
|
|
491
|
-
this.pending.add(key);
|
|
492
|
-
|
|
493
|
-
// Use a stable virtual path for the parser cache key
|
|
494
|
-
const virtualPath = `__highlight__.${langId}`;
|
|
495
|
-
|
|
496
|
-
Promise.resolve().then(async () => {
|
|
497
|
-
try {
|
|
498
|
-
// Ensure the grammar is loaded
|
|
499
|
-
const language = await this.service.loadLanguage(langId);
|
|
500
|
-
if (!language) {
|
|
501
|
-
logger.debug('SyntaxHighlighter: grammar not available', { langId });
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Parse the code
|
|
506
|
-
const tree = await this.service.parse(virtualPath, code, langId);
|
|
507
|
-
if (!tree) {
|
|
508
|
-
logger.debug('SyntaxHighlighter: parse returned null', { langId });
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Walk AST and build per-line token arrays
|
|
513
|
-
const codeLines = code.split('\n');
|
|
514
|
-
const spans = collectSpans(tree.rootNode, code);
|
|
515
|
-
const highlighted = spansToLines(spans, codeLines);
|
|
516
|
-
|
|
517
|
-
// Evict oldest entry if at capacity (FIFO)
|
|
518
|
-
if (this.cache.size >= MAX_HIGHLIGHT_CACHE) {
|
|
519
|
-
const firstKey = this.cache.keys().next().value;
|
|
520
|
-
if (firstKey !== undefined) this.cache.delete(firstKey);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
this.cache.set(key, highlighted);
|
|
524
|
-
logger.debug('SyntaxHighlighter: parsed and cached', { langId, lines: codeLines.length });
|
|
525
|
-
} catch (err) {
|
|
526
|
-
logger.warn('SyntaxHighlighter: parse error', { langId, error: summarizeError(err) });
|
|
527
|
-
} finally {
|
|
528
|
-
this.pending.delete(key);
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/** Clear all cached highlights (e.g., on theme change). */
|
|
534
|
-
clearCache(): void {
|
|
535
|
-
this.cache.clear();
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/** Current cache size (for diagnostics). */
|
|
539
|
-
get cacheSize(): number {
|
|
540
|
-
return this.cache.size;
|
|
541
|
-
}
|
|
542
|
-
}
|