pi-hashline-edit-pro 6.1.2 → 6.2.0

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.
Files changed (66) hide show
  1. package/README.md +14 -12
  2. package/index.ts +20 -12
  3. package/package.json +3 -2
  4. package/src/auto-read-all.ts +39 -21
  5. package/src/config-ui.ts +135 -55
  6. package/src/config.ts +27 -9
  7. package/src/copy-move.ts +4 -4
  8. package/src/grep.ts +3 -3
  9. package/src/insert.ts +3 -3
  10. package/src/outline/index.ts +45 -0
  11. package/src/outline/parsers.ts +42 -0
  12. package/src/outline/render.ts +166 -0
  13. package/src/outline/vendor/LICENSE +201 -0
  14. package/src/outline/vendor/README.md +17 -0
  15. package/src/outline/vendor/parsers/blade.ts +138 -0
  16. package/src/outline/vendor/parsers/index.ts +75 -0
  17. package/src/outline/vendor/parsers/java.ts +501 -0
  18. package/src/outline/vendor/parsers/manager.ts +1721 -0
  19. package/src/outline/vendor/parsers/php.ts +332 -0
  20. package/src/outline/vendor/parsers/sass.ts +416 -0
  21. package/src/outline/vendor/parsers/scss.ts +566 -0
  22. package/src/outline/vendor/types.ts +82 -0
  23. package/src/outline/vendor/wasm/LICENSES.md +37 -0
  24. package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
  25. package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
  26. package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
  27. package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
  28. package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
  29. package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
  30. package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
  31. package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
  32. package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
  33. package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
  34. package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
  35. package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
  36. package/src/read.ts +108 -36
  37. package/src/replace-match.ts +3 -3
  38. package/src/replace-undo.ts +3 -3
  39. package/src/replace.ts +3 -3
  40. package/tool-prompts/read-guidelines.md +3 -0
  41. package/{prompts → tool-prompts}/read.md +2 -0
  42. package/prompts/read-guidelines.md +0 -1
  43. package/src/auto-read-all-state.ts +0 -27
  44. /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
  45. /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
  46. /package/{prompts → tool-prompts}/copy.md +0 -0
  47. /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
  48. /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
  49. /package/{prompts → tool-prompts}/grep.md +0 -0
  50. /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
  51. /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
  52. /package/{prompts → tool-prompts}/insert.md +0 -0
  53. /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
  54. /package/{prompts → tool-prompts}/move-snippet.md +0 -0
  55. /package/{prompts → tool-prompts}/move.md +0 -0
  56. /package/{prompts → tool-prompts}/read-snippet.md +0 -0
  57. /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
  58. /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
  59. /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
  60. /package/{prompts → tool-prompts}/replace-match.md +0 -0
  61. /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
  62. /package/{prompts → tool-prompts}/replace.md +0 -0
  63. /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
  64. /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
  65. /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
  66. /package/{prompts → tool-prompts}/undo-last-change.md +0 -0
@@ -0,0 +1,1721 @@
1
+ /**
2
+ * Tree-sitter parser manager.
3
+ *
4
+ * Lazily initializes the WASM runtime and loads language grammars on demand.
5
+ * All parsers share a single Parser instance, re-configured per language.
6
+ */
7
+
8
+ import { fileURLToPath } from "node:url";
9
+ import { Parser, Language, type Node } from "web-tree-sitter";
10
+ import type { SymbolInfo, ImportInfo } from "../types.js";
11
+ import { extractJava, extractJavaImports } from "./java.js";
12
+ import { extractPhp } from "./php.js";
13
+ import { extractBlade } from "./blade.js";
14
+ import { extractScss, extractScssImports } from "./scss.js";
15
+ import { extractSass, extractSassImports } from "./sass.js";
16
+
17
+ // ---- WASM path resolution ----
18
+
19
+
20
+ /** Known language grammars with their npm package + WASM file name. */
21
+ const GRAMMAR_REGISTRY: Record<
22
+ string,
23
+ { package: string; wasm: string; label: string }
24
+ > = {
25
+ javascript: {
26
+ package: "tree-sitter-javascript",
27
+ wasm: "tree-sitter-javascript.wasm",
28
+ label: "JavaScript",
29
+ },
30
+ typescript: {
31
+ package: "tree-sitter-typescript",
32
+ wasm: "tree-sitter-typescript.wasm",
33
+ label: "TypeScript",
34
+ },
35
+ tsx: {
36
+ package: "tree-sitter-typescript",
37
+ wasm: "tree-sitter-tsx.wasm",
38
+ label: "TSX",
39
+ },
40
+ python: {
41
+ package: "tree-sitter-python",
42
+ wasm: "tree-sitter-python.wasm",
43
+ label: "Python",
44
+ },
45
+ go: {
46
+ package: "tree-sitter-go",
47
+ wasm: "tree-sitter-go.wasm",
48
+ label: "Go",
49
+ },
50
+ rust: {
51
+ package: "tree-sitter-rust",
52
+ wasm: "tree-sitter-rust.wasm",
53
+ label: "Rust",
54
+ },
55
+ solidity: {
56
+ package: "tree-sitter-solidity",
57
+ wasm: "tree-sitter-solidity.wasm",
58
+ label: "Solidity",
59
+ },
60
+ scss: {
61
+ package: "tree-sitter-scss",
62
+ wasm: "tree-sitter-scss.wasm",
63
+ label: "SCSS",
64
+ },
65
+ sass: {
66
+ package: "tree-sitter-sass",
67
+ wasm: "tree-sitter-sass.wasm",
68
+ label: "Sass",
69
+ },
70
+ java: {
71
+ package: "tree-sitter-java",
72
+ wasm: "tree-sitter-java.wasm",
73
+ label: "Java",
74
+ },
75
+ php: {
76
+ package: "tree-sitter-php",
77
+ wasm: "tree-sitter-php.wasm",
78
+ label: "PHP",
79
+ },
80
+ blade: {
81
+ package: "tree-sitter-blade",
82
+ wasm: "tree-sitter-blade.wasm",
83
+ label: "Blade",
84
+ },
85
+ markdown: {
86
+ package: "markdown",
87
+ wasm: "markdown.wasm",
88
+ label: "Markdown",
89
+ },
90
+ };
91
+
92
+ /** Map file extensions to grammar keys. */
93
+ const EXTENSION_MAP: Record<string, string> = {
94
+ ".js": "javascript",
95
+ ".mjs": "javascript",
96
+ ".cjs": "javascript",
97
+ ".jsx": "javascript",
98
+ ".ts": "typescript",
99
+ ".mts": "typescript",
100
+ ".cts": "typescript",
101
+ ".tsx": "tsx",
102
+ ".py": "python",
103
+ ".go": "go",
104
+ ".rs": "rust",
105
+ ".sol": "solidity",
106
+ ".java": "java",
107
+ ".scss": "scss",
108
+ ".sass": "sass",
109
+ ".blade.php": "blade",
110
+ ".php": "php",
111
+ ".md": "markdown",
112
+ ".markdown": "markdown",
113
+ };
114
+
115
+ const BASENAME_MAP: Record<string, string> = {
116
+ artisan: "php",
117
+ };
118
+
119
+ // ---- Singleton state ----
120
+
121
+ let initialized = false;
122
+ const loadedLanguages = new Map<string, Language>();
123
+ let sharedParser: Parser | null = null;
124
+
125
+ /** Initialize the WASM runtime (once). */
126
+ async function ensureInit(): Promise<void> {
127
+ if (initialized) return;
128
+ await Parser.init();
129
+ initialized = true;
130
+ sharedParser = new Parser();
131
+ }
132
+
133
+ /** Load a language grammar by key (cached). */
134
+ async function loadLanguage(key: string): Promise<Language> {
135
+ const cached = loadedLanguages.get(key);
136
+ if (cached) return cached;
137
+
138
+ const entry = GRAMMAR_REGISTRY[key];
139
+ if (!entry) throw new Error(`Unknown grammar: ${key}`);
140
+
141
+ const wasmPath = fileURLToPath(
142
+ new URL("../wasm/" + entry.wasm, import.meta.url),
143
+ );
144
+
145
+ const lang = await Language.load(wasmPath);
146
+ loadedLanguages.set(key, lang);
147
+ return lang;
148
+ }
149
+
150
+ // ---- Public API ----
151
+
152
+ /** Detect grammar key from a file path's extension. */
153
+ export function detectLanguage(filePath: string): string | null {
154
+ const lower = filePath.toLowerCase();
155
+ const base = lower.split(/[/\\]/).pop() ?? lower;
156
+ const named = BASENAME_MAP[base];
157
+ if (named !== undefined) return named;
158
+ for (const [ext, key] of Object.entries(EXTENSION_MAP)) {
159
+ if (lower.endsWith(ext)) return key;
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /** Get a human-readable label for a grammar key. */
165
+ export function languageLabel(key: string): string {
166
+ return GRAMMAR_REGISTRY[key]?.label ?? key;
167
+ }
168
+
169
+ /** Parse source code with a given grammar, returning SymbolInfo[]. */
170
+ export async function parseCode(
171
+ key: string,
172
+ source: string,
173
+ ): Promise<SymbolInfo[]> {
174
+ // Markdown uses regex-based parsing, not tree-sitter
175
+ if (key === "markdown") {
176
+ return extractMarkdown(source);
177
+ }
178
+
179
+ await ensureInit();
180
+
181
+ const lang = await loadLanguage(key);
182
+ if (!sharedParser) throw new Error("Parser not initialized");
183
+
184
+ sharedParser.setLanguage(lang);
185
+ const tree = sharedParser.parse(source);
186
+ if (!tree) return [];
187
+
188
+ return extractSymbols(key, tree.rootNode, source, 0);
189
+ }
190
+
191
+ // ---- Symbol extraction dispatch ----
192
+
193
+ function extractSymbols(
194
+ key: string,
195
+ node: Node,
196
+ source: string,
197
+ depth: number,
198
+ ): SymbolInfo[] {
199
+ switch (key) {
200
+ case "javascript":
201
+ case "typescript":
202
+ case "tsx":
203
+ return extractJavaScriptLike(node, source, depth);
204
+ case "python":
205
+ return extractPython(node, source, depth);
206
+ case "go":
207
+ return extractGo(node, source, depth);
208
+ case "rust":
209
+ return extractRust(node, source, depth);
210
+ case "solidity":
211
+ return extractSolidity(node, source, depth);
212
+ case "scss":
213
+ return extractScss(node, source, depth);
214
+ case "sass":
215
+ return extractSass(node, source, depth);
216
+ case "java":
217
+ return extractJava(node, source, depth);
218
+ case "php":
219
+ return extractPhp(node, source, depth);
220
+ case "blade":
221
+ return extractBlade(node, source, depth);
222
+ case "markdown":
223
+ return extractMarkdown(source);
224
+ default:
225
+ return [];
226
+ }
227
+ }
228
+
229
+ // ---- Generic helpers ----
230
+
231
+ /** Get the text of the first named child matching a type. */
232
+ function childText(
233
+ node: Node,
234
+ childType: string,
235
+ source: string,
236
+ ): string | undefined {
237
+ for (let i = 0; i < node.namedChildCount; i++) {
238
+ const c = node.namedChild(i);
239
+ if (c && c.type === childType) {
240
+ return source.slice(c.startIndex, c.endIndex);
241
+ }
242
+ }
243
+ return undefined;
244
+ }
245
+
246
+ /** Get the first field child's text (field-name-based access). */
247
+ function fieldChildText(
248
+ node: Node,
249
+ fieldName: string,
250
+ source: string,
251
+ ): string | undefined {
252
+ const c = node.childForFieldName(fieldName);
253
+ return c ? source.slice(c.startIndex, c.endIndex) : undefined;
254
+ }
255
+
256
+ // ---- Node line ranges (0-indexed → 1-indexed) ----
257
+
258
+ function nodeRange(node: Node): [number, number] {
259
+ return [node.startPosition.row + 1, node.endPosition.row + 1];
260
+ }
261
+
262
+ // ========================================================================
263
+ // JavaScript / TypeScript / TSX
264
+ // ========================================================================
265
+
266
+ function extractJavaScriptLike(
267
+ node: Node,
268
+ source: string,
269
+ depth: number,
270
+ ): SymbolInfo[] {
271
+ const results: SymbolInfo[] = [];
272
+ const MAX_DEPTH = depth + 10; // track overall depth via recursion param
273
+
274
+ for (let i = 0; i < node.namedChildCount; i++) {
275
+ const child = node.namedChild(i);
276
+ if (!child) continue;
277
+
278
+ const symbol = extractJSChild(child, source);
279
+ if (!symbol) continue;
280
+
281
+ const declaration =
282
+ child.type === "export_statement"
283
+ ? findChildByType(child, [
284
+ "class_declaration",
285
+ "class",
286
+ "abstract_class_declaration",
287
+ "interface_declaration",
288
+ "enum_declaration",
289
+ ]) ?? child
290
+ : child;
291
+
292
+ // For class/interface/enum, look inside their bodies
293
+ if (
294
+ symbol.type === "class" ||
295
+ symbol.type === "interface" ||
296
+ symbol.type === "enum"
297
+ ) {
298
+ const bodyNode = findChildByType(declaration, [
299
+ "class_body",
300
+ "interface_body",
301
+ "enum_body",
302
+ "object_type",
303
+ ]);
304
+ if (bodyNode && depth < MAX_DEPTH) {
305
+ symbol.children = extractJavaScriptLike(bodyNode, source, depth + 1);
306
+ }
307
+ }
308
+
309
+ results.push(symbol);
310
+ }
311
+
312
+ return results;
313
+ }
314
+
315
+ const JS_SYMBOL_TYPES: Record<string, string> = {
316
+ function_declaration: "function",
317
+ function_expression: "function",
318
+ arrow_function: "arrow_function",
319
+ method_definition: "method",
320
+ class_declaration: "class",
321
+ class: "class",
322
+ interface_declaration: "interface",
323
+ enum_declaration: "enum",
324
+ type_alias_declaration: "type",
325
+ abstract_class_declaration: "class",
326
+ lexical_declaration: "variable",
327
+ variable_declaration: "variable",
328
+ };
329
+
330
+ function isFunctionNode(node: Node): boolean {
331
+ return node.type === "arrow_function" || node.type === "function_expression" || node.type === "function";
332
+ }
333
+
334
+ function extractJSChild(
335
+ child: Node,
336
+ source: string,
337
+ ): SymbolInfo | null {
338
+ const type = child.type;
339
+
340
+ // Top-level declarations: functions, arrow functions, and variables
341
+ if (
342
+ type === "lexical_declaration" ||
343
+ type === "variable_declaration"
344
+ ) {
345
+ const declarators: SymbolInfo[] = [];
346
+ for (let j = 0; j < child.namedChildCount; j++) {
347
+ const d = child.namedChild(j);
348
+ if (!d || d.type !== "variable_declarator") continue;
349
+ const name = fieldChildText(d, "name", source);
350
+ if (!name) continue;
351
+ const value = d.childForFieldName("value");
352
+ if (value && isFunctionNode(value)) {
353
+ const [sl, el] = nodeRange(d);
354
+ declarators.push({
355
+ name,
356
+ type: value.type === "arrow_function" ? "arrow_function" : "function",
357
+ startLine: sl,
358
+ endLine: el,
359
+ detail: extractFnDetail(value, source),
360
+ });
361
+ } else {
362
+ const [sl, el] = nodeRange(d);
363
+ declarators.push({
364
+ name,
365
+ type: "variable",
366
+ startLine: sl,
367
+ endLine: el,
368
+ });
369
+ }
370
+ }
371
+ if (declarators.length === 1) return declarators[0];
372
+ if (declarators.length > 1) {
373
+ return {
374
+ name: "",
375
+ type: "variable",
376
+ startLine: declarators[0].startLine,
377
+ endLine: declarators[declarators.length - 1].endLine,
378
+ children: declarators,
379
+ };
380
+ }
381
+ return null;
382
+ }
383
+
384
+ // Export statement — unwrap
385
+ if (type === "export_statement") {
386
+ const isDefault = child.text.startsWith("export default");
387
+ for (let j = 0; j < child.namedChildCount; j++) {
388
+ const inner = child.namedChild(j);
389
+ if (inner) {
390
+ const result = extractJSChild(inner, source);
391
+ if (result) {
392
+ if (isDefault && result.name === "<anonymous>") result.name = "default";
393
+ return result;
394
+ }
395
+ }
396
+ }
397
+ return null;
398
+ }
399
+
400
+ if (type === "expression_statement") return testCallSymbol(child, source);
401
+
402
+ const symbolType = JS_SYMBOL_TYPES[type];
403
+ if (!symbolType) return null;
404
+
405
+ const name =
406
+ fieldChildText(child, "name", source) ||
407
+ childText(child, "property_identifier", source) ||
408
+ childText(child, "type_identifier", source) ||
409
+ "<anonymous>";
410
+
411
+ const [sl, el] = nodeRange(child);
412
+
413
+ // For functions/methods, try to get the parameters as detail
414
+ let detail: string | undefined;
415
+ if (type === "function_declaration" || type === "function_expression" || type === "method_definition" || type === "arrow_function") {
416
+ detail = extractFnDetail(child, source);
417
+ }
418
+
419
+ return { name, type: symbolType, startLine: sl, endLine: el, detail };
420
+ }
421
+
422
+ const TEST_CALLEES = new Set(["describe", "test", "it", "suite", "context", "specify"]);
423
+
424
+ function testCallSymbol(child: Node, source: string): SymbolInfo | null {
425
+ const expr = child.namedChildCount > 0 ? child.namedChild(0) : null;
426
+ if (!expr || expr.type !== "call_expression") return null;
427
+ const fn = expr.childForFieldName("function");
428
+ const args = expr.childForFieldName("arguments");
429
+ if (!fn || !args) return null;
430
+ const callee = (source.slice(fn.startIndex, fn.endIndex).split(".")[0] ?? "").trim();
431
+ if (!TEST_CALLEES.has(callee)) return null;
432
+ const first = args.namedChildren[0];
433
+ if (!first || first.type !== "string") return null;
434
+ const name = source.slice(first.startIndex, first.endIndex).replace(/^["'`]|["'`]$/g, "");
435
+ const [sl, el] = nodeRange(expr);
436
+ const symbol: SymbolInfo = { name, type: callee, startLine: sl, endLine: el };
437
+ const children = extractTestCalls(expr, source);
438
+ if (children.length > 0) symbol.children = children;
439
+ return symbol;
440
+ }
441
+
442
+ function extractTestCalls(call: Node, source: string): SymbolInfo[] {
443
+ const results: SymbolInfo[] = [];
444
+ const args = call.childForFieldName("arguments");
445
+ for (const arg of args?.namedChildren ?? []) {
446
+ if (arg.type !== "arrow_function" && arg.type !== "function_expression" && arg.type !== "function") continue;
447
+ const body = arg.childForFieldName("body");
448
+ if (!body || body.type !== "statement_block") continue;
449
+ for (const stmt of body.namedChildren) {
450
+ if (stmt.type !== "expression_statement") continue;
451
+ const inner = testCallSymbol(stmt, source);
452
+ if (inner) results.push(inner);
453
+ }
454
+ }
455
+ return results;
456
+ }
457
+
458
+ function extractFnDetail(
459
+ node: Node,
460
+ source: string,
461
+ ): string | undefined {
462
+ const params = node.childForFieldName("parameters");
463
+ if (params) {
464
+ return source.slice(params.startIndex, params.endIndex);
465
+ }
466
+ return undefined;
467
+ }
468
+
469
+ function findChildByType(
470
+ node: Node,
471
+ types: string[],
472
+ ): Node | null {
473
+ for (let i = 0; i < node.childCount; i++) {
474
+ const c = node.child(i);
475
+ if (c && types.includes(c.type)) return c;
476
+ }
477
+ return null;
478
+ }
479
+
480
+ // ========================================================================
481
+ // Python
482
+ // ========================================================================
483
+
484
+ function extractPython(
485
+ node: Node,
486
+ source: string,
487
+ depth: number,
488
+ ): SymbolInfo[] {
489
+ const results: SymbolInfo[] = [];
490
+ const MAX_DEPTH = depth + 10;
491
+
492
+ for (let i = 0; i < node.namedChildCount; i++) {
493
+ const child = node.namedChild(i);
494
+ if (!child) continue;
495
+
496
+ let symbol: SymbolInfo | null = null;
497
+
498
+ if (child.type === "class_definition") {
499
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
500
+ const [sl, el] = nodeRange(child);
501
+ symbol = { name, type: "class", startLine: sl, endLine: el };
502
+
503
+ // Recurse into body for methods
504
+ const body = child.childForFieldName("body");
505
+ if (body && depth < MAX_DEPTH) {
506
+ symbol.children = extractPython(body, source, depth + 1);
507
+ }
508
+ } else if (child.type === "function_definition") {
509
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
510
+ const [sl, el] = nodeRange(child);
511
+ const params = child.childForFieldName("parameters");
512
+ const detail = params
513
+ ? source.slice(params.startIndex, params.endIndex)
514
+ : undefined;
515
+ symbol = { name, type: "function", startLine: sl, endLine: el, detail };
516
+ } else if (child.type === "decorated_definition") {
517
+ // Unwrap to the inner definition
518
+ const def = child.namedChild(child.namedChildCount - 1);
519
+ if (def) {
520
+ const inner = extractPythonSingle(def, source);
521
+ if (inner) {
522
+ inner.type = "decorated_" + inner.type;
523
+ symbol = inner;
524
+ }
525
+ }
526
+ }
527
+
528
+ if (symbol) results.push(symbol);
529
+ }
530
+
531
+ return results;
532
+ }
533
+
534
+ function extractPythonSingle(
535
+ child: Node,
536
+ source: string,
537
+ ): SymbolInfo | null {
538
+ if (child.type === "class_definition") {
539
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
540
+ const [sl, el] = nodeRange(child);
541
+ return { name, type: "class", startLine: sl, endLine: el };
542
+ }
543
+ if (child.type === "function_definition") {
544
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
545
+ const [sl, el] = nodeRange(child);
546
+ const params = child.childForFieldName("parameters");
547
+ const detail = params
548
+ ? source.slice(params.startIndex, params.endIndex)
549
+ : undefined;
550
+ return { name, type: "function", startLine: sl, endLine: el, detail };
551
+ }
552
+ return null;
553
+ }
554
+
555
+ // ========================================================================
556
+ // Go
557
+ // ========================================================================
558
+
559
+ function extractGo(
560
+ node: Node,
561
+ source: string,
562
+ depth: number,
563
+ ): SymbolInfo[] {
564
+ const results: SymbolInfo[] = [];
565
+ const MAX_DEPTH = depth + 10;
566
+
567
+ for (let i = 0; i < node.namedChildCount; i++) {
568
+ const child = node.namedChild(i);
569
+ if (!child) continue;
570
+
571
+ let symbol: SymbolInfo | null = null;
572
+
573
+ if (child.type === "function_declaration") {
574
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
575
+ const [sl, el] = nodeRange(child);
576
+ symbol = { name, type: "function", startLine: sl, endLine: el };
577
+ } else if (child.type === "method_declaration") {
578
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
579
+ const receiver = child.childForFieldName("receiver");
580
+ const receiverText = receiver
581
+ ? source.slice(receiver.startIndex, receiver.endIndex)
582
+ : "";
583
+ const [sl, el] = nodeRange(child);
584
+ const fullName = receiverText ? `${receiverText}).${name}` : name;
585
+ symbol = { name: fullName, type: "method", startLine: sl, endLine: el };
586
+ } else if (child.type === "type_declaration") {
587
+ // type_declaration wraps type_spec nodes
588
+ const specs: SymbolInfo[] = [];
589
+ for (let j = 0; j < child.namedChildCount; j++) {
590
+ const spec = child.namedChild(j);
591
+ if (!spec || spec.type !== "type_spec") continue;
592
+ const name = fieldChildText(spec, "name", source) || "<anonymous>";
593
+ const [sl, el] = nodeRange(spec);
594
+ const typeNode = spec.childForFieldName("type");
595
+ const kind = typeNode?.type === "struct_type"
596
+ ? "struct"
597
+ : typeNode?.type === "interface_type"
598
+ ? "interface"
599
+ : "type";
600
+ const entry: SymbolInfo = { name, type: kind, startLine: sl, endLine: el };
601
+
602
+ // Recurse into struct/interface body
603
+ if (typeNode && depth < MAX_DEPTH) {
604
+ const body = findChildByType(typeNode, [
605
+ "field_declaration_list",
606
+ ]);
607
+ if (body) {
608
+ entry.children = extractGoFields(body, source);
609
+ }
610
+ }
611
+
612
+ specs.push(entry);
613
+ }
614
+ if (specs.length === 1) {
615
+ symbol = specs[0];
616
+ } else if (specs.length > 1) {
617
+ symbol = {
618
+ name: "",
619
+ type: "type",
620
+ startLine: specs[0].startLine,
621
+ endLine: specs[specs.length - 1].endLine,
622
+ children: specs,
623
+ };
624
+ }
625
+ if (symbol) results.push(symbol);
626
+ continue;
627
+ } else if (child.type === "const_declaration") {
628
+ const [sl, el] = nodeRange(child);
629
+ const names: string[] = [];
630
+ for (let j = 0; j < child.namedChildCount; j++) {
631
+ const spec = child.namedChild(j);
632
+ if (spec && spec.type === "const_spec") {
633
+ const n = fieldChildText(spec, "name", source);
634
+ if (n) names.push(n);
635
+ }
636
+ }
637
+ symbol = {
638
+ name: names.join(", ") || "const",
639
+ type: "const",
640
+ startLine: sl,
641
+ endLine: el,
642
+ };
643
+ } else if (child.type === "var_declaration") {
644
+ const [sl, el] = nodeRange(child);
645
+ const names: string[] = [];
646
+ for (let j = 0; j < child.namedChildCount; j++) {
647
+ const spec = child.namedChild(j);
648
+ if (spec && spec.type === "var_spec") {
649
+ const n = fieldChildText(spec, "name", source);
650
+ if (n) names.push(n);
651
+ }
652
+ }
653
+ symbol = {
654
+ name: names.join(", ") || "var",
655
+ type: "var",
656
+ startLine: sl,
657
+ endLine: el,
658
+ };
659
+ }
660
+
661
+ if (symbol) results.push(symbol);
662
+ }
663
+
664
+ return results;
665
+ }
666
+
667
+ function extractGoFields(
668
+ node: Node,
669
+ source: string,
670
+ ): SymbolInfo[] {
671
+ const results: SymbolInfo[] = [];
672
+
673
+ for (let i = 0; i < node.namedChildCount; i++) {
674
+ const child = node.namedChild(i);
675
+ if (!child || child.type !== "field_declaration") continue;
676
+
677
+ const name = fieldChildText(child, "name", source);
678
+ if (!name) continue;
679
+ const [sl, el] = nodeRange(child);
680
+ const typeNode = child.childForFieldName("type");
681
+ const typeText = typeNode
682
+ ? source.slice(typeNode.startIndex, typeNode.endIndex)
683
+ : "";
684
+ results.push({
685
+ name,
686
+ type: "field",
687
+ startLine: sl,
688
+ endLine: el,
689
+ detail: typeText,
690
+ });
691
+ }
692
+
693
+ return results;
694
+ }
695
+
696
+ // ========================================================================
697
+ // Rust
698
+ // ========================================================================
699
+
700
+ function extractRust(
701
+ node: Node,
702
+ source: string,
703
+ depth: number,
704
+ ): SymbolInfo[] {
705
+ const results: SymbolInfo[] = [];
706
+ const MAX_DEPTH = depth + 10;
707
+
708
+ for (let i = 0; i < node.namedChildCount; i++) {
709
+ const child = node.namedChild(i);
710
+ if (!child) continue;
711
+
712
+ let symbol: SymbolInfo | null = null;
713
+
714
+ if (child.type === "function_item") {
715
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
716
+ const [sl, el] = nodeRange(child);
717
+ const params = child.childForFieldName("parameters");
718
+ const detail = params
719
+ ? source.slice(params.startIndex, params.endIndex)
720
+ : undefined;
721
+ symbol = { name, type: "function", startLine: sl, endLine: el, detail };
722
+ } else if (child.type === "struct_item") {
723
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
724
+ const [sl, el] = nodeRange(child);
725
+ symbol = { name, type: "struct", startLine: sl, endLine: el };
726
+ } else if (child.type === "enum_item") {
727
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
728
+ const [sl, el] = nodeRange(child);
729
+ symbol = { name, type: "enum", startLine: sl, endLine: el };
730
+ } else if (child.type === "trait_item") {
731
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
732
+ const [sl, el] = nodeRange(child);
733
+ symbol = { name, type: "trait", startLine: sl, endLine: el };
734
+ } else if (child.type === "impl_item") {
735
+ const trait = child.childForFieldName("trait");
736
+ const implType = child.childForFieldName("type");
737
+ const traitStr = trait
738
+ ? source.slice(trait.startIndex, trait.endIndex) + " for "
739
+ : "";
740
+ const typeStr = implType
741
+ ? source.slice(implType.startIndex, implType.endIndex)
742
+ : "";
743
+ const [sl, el] = nodeRange(child);
744
+ const name = `impl ${traitStr}${typeStr}`;
745
+ symbol = { name, type: "impl", startLine: sl, endLine: el };
746
+
747
+ // Recurse into body for associated functions/methods
748
+ const body = child.childForFieldName("body");
749
+ if (body && depth < MAX_DEPTH) {
750
+ symbol.children = extractRustImplBody(body, source);
751
+ }
752
+ } else if (child.type === "type_item") {
753
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
754
+ const [sl, el] = nodeRange(child);
755
+ symbol = { name, type: "type", startLine: sl, endLine: el };
756
+ } else if (child.type === "const_item") {
757
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
758
+ const [sl, el] = nodeRange(child);
759
+ symbol = { name, type: "const", startLine: sl, endLine: el };
760
+ } else if (child.type === "macro_definition") {
761
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
762
+ const [sl, el] = nodeRange(child);
763
+ symbol = { name: `${name}!`, type: "macro", startLine: sl, endLine: el };
764
+ }
765
+
766
+ if (symbol) results.push(symbol);
767
+ }
768
+
769
+ return results;
770
+ }
771
+
772
+ function extractRustImplBody(
773
+ node: Node,
774
+ source: string,
775
+ ): SymbolInfo[] {
776
+ const results: SymbolInfo[] = [];
777
+
778
+ for (let i = 0; i < node.namedChildCount; i++) {
779
+ const child = node.namedChild(i);
780
+ if (!child) continue;
781
+
782
+ let symbol: SymbolInfo | null = null;
783
+
784
+ if (child.type === "function_item") {
785
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
786
+ const [sl, el] = nodeRange(child);
787
+ const params = child.childForFieldName("parameters");
788
+ const detail = params
789
+ ? source.slice(params.startIndex, params.endIndex)
790
+ : undefined;
791
+ symbol = { name, type: "method", startLine: sl, endLine: el, detail };
792
+ } else if (child.type === "const_item") {
793
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
794
+ const [sl, el] = nodeRange(child);
795
+ symbol = { name, type: "const", startLine: sl, endLine: el };
796
+ } else if (child.type === "type_item") {
797
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
798
+ const [sl, el] = nodeRange(child);
799
+ symbol = { name, type: "type", startLine: sl, endLine: el };
800
+ }
801
+
802
+ if (symbol) results.push(symbol);
803
+ }
804
+
805
+ return results;
806
+ }
807
+
808
+ // ========================================================================
809
+ // Solidity
810
+ // ========================================================================
811
+
812
+ function extractSolidity(
813
+ node: Node,
814
+ source: string,
815
+ depth: number,
816
+ ): SymbolInfo[] {
817
+ const results: SymbolInfo[] = [];
818
+ const MAX_DEPTH = depth + 10;
819
+
820
+ for (let i = 0; i < node.namedChildCount; i++) {
821
+ const child = node.namedChild(i);
822
+ if (!child) continue;
823
+
824
+ let symbol: SymbolInfo | null = null;
825
+
826
+ if (child.type === "contract_declaration") {
827
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
828
+ const [sl, el] = nodeRange(child);
829
+
830
+ // Collect inheritance detail
831
+ const inherits: string[] = [];
832
+ for (let j = 0; j < child.namedChildCount; j++) {
833
+ const c = child.namedChild(j);
834
+ if (c && c.type === "inheritance_specifier") {
835
+ const ancestor = c.childForFieldName("ancestor");
836
+ if (ancestor) {
837
+ inherits.push(source.slice(ancestor.startIndex, ancestor.endIndex));
838
+ }
839
+ }
840
+ }
841
+ const detail = inherits.length > 0 ? `is ${inherits.join(", ")}` : undefined;
842
+
843
+ symbol = { name, type: "contract", startLine: sl, endLine: el, detail };
844
+
845
+ // Recurse into contract body
846
+ const body = child.childForFieldName("body");
847
+ if (body && depth < MAX_DEPTH) {
848
+ symbol.children = extractSolidityBody(body, source, depth + 1);
849
+ }
850
+ } else if (child.type === "interface_declaration") {
851
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
852
+ const [sl, el] = nodeRange(child);
853
+
854
+ const inherits: string[] = [];
855
+ for (let j = 0; j < child.namedChildCount; j++) {
856
+ const c = child.namedChild(j);
857
+ if (c && c.type === "inheritance_specifier") {
858
+ const ancestor = c.childForFieldName("ancestor");
859
+ if (ancestor) {
860
+ inherits.push(source.slice(ancestor.startIndex, ancestor.endIndex));
861
+ }
862
+ }
863
+ }
864
+ const detail = inherits.length > 0 ? `is ${inherits.join(", ")}` : undefined;
865
+
866
+ symbol = { name, type: "interface", startLine: sl, endLine: el, detail };
867
+
868
+ const body = child.childForFieldName("body");
869
+ if (body && depth < MAX_DEPTH) {
870
+ symbol.children = extractSolidityBody(body, source, depth + 1);
871
+ }
872
+ } else if (child.type === "library_declaration") {
873
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
874
+ const [sl, el] = nodeRange(child);
875
+ symbol = { name, type: "library", startLine: sl, endLine: el };
876
+
877
+ const body = child.childForFieldName("body");
878
+ if (body && depth < MAX_DEPTH) {
879
+ symbol.children = extractSolidityBody(body, source, depth + 1);
880
+ }
881
+ } else {
882
+ // File-level item
883
+ symbol = extractSolidityItem(child, source);
884
+ }
885
+
886
+ if (symbol) results.push(symbol);
887
+ }
888
+
889
+ return results;
890
+ }
891
+
892
+ function extractSolidityBody(
893
+ node: Node,
894
+ source: string,
895
+ depth: number,
896
+ ): SymbolInfo[] {
897
+ const results: SymbolInfo[] = [];
898
+ const MAX_DEPTH = depth + 10;
899
+
900
+ for (let i = 0; i < node.namedChildCount; i++) {
901
+ const child = node.namedChild(i);
902
+ if (!child) continue;
903
+
904
+ const symbol = extractSolidityItem(child, source);
905
+ if (!symbol) continue;
906
+
907
+ // Recurse into struct and enum bodies for nested members
908
+ if (symbol.type === "struct" && depth < MAX_DEPTH) {
909
+ const bodyNode = child.childForFieldName("body");
910
+ if (bodyNode) {
911
+ symbol.children = [];
912
+ for (let j = 0; j < bodyNode.namedChildCount; j++) {
913
+ const m = bodyNode.namedChild(j);
914
+ if (!m) continue;
915
+ if (m.type === "struct_member") {
916
+ const memberName = fieldChildText(m, "name", source) || "";
917
+ const memberType = m.childForFieldName("type");
918
+ const typeStr = memberType
919
+ ? source.slice(memberType.startIndex, memberType.endIndex)
920
+ : "";
921
+ const [msl, mel] = nodeRange(m);
922
+ symbol.children.push({
923
+ name: memberName,
924
+ type: "field",
925
+ startLine: msl,
926
+ endLine: mel,
927
+ detail: typeStr,
928
+ });
929
+ }
930
+ }
931
+ }
932
+ } else if (symbol.type === "enum" && depth < MAX_DEPTH) {
933
+ const bodyNode = child.childForFieldName("body");
934
+ if (bodyNode) {
935
+ symbol.children = [];
936
+ for (let j = 0; j < bodyNode.namedChildCount; j++) {
937
+ const m = bodyNode.namedChild(j);
938
+ if (!m || m.type !== "enum_value") continue;
939
+ const [msl, mel] = nodeRange(m);
940
+ symbol.children.push({
941
+ name: source.slice(m.startIndex, m.endIndex).trim(),
942
+ type: "enum_value",
943
+ startLine: msl,
944
+ endLine: mel,
945
+ });
946
+ }
947
+ }
948
+ }
949
+
950
+ results.push(symbol);
951
+ }
952
+
953
+ return results;
954
+ }
955
+
956
+ /** Collect all unnamed `parameter`/`event_parameter`/`error_parameter` named children and return their source ranges joined. */
957
+ function solidityParamsText(
958
+ node: Node,
959
+ source: string,
960
+ paramType: string = "parameter",
961
+ ): string | undefined {
962
+ const parts: string[] = [];
963
+ for (let j = 0; j < node.namedChildCount; j++) {
964
+ const c = node.namedChild(j);
965
+ if (c && c.type === paramType) {
966
+ parts.push(source.slice(c.startIndex, c.endIndex));
967
+ }
968
+ }
969
+ return parts.length > 0 ? `(${parts.join(", ")})` : undefined;
970
+ }
971
+
972
+ function extractSolidityItem(
973
+ child: Node,
974
+ source: string,
975
+ ): SymbolInfo | null {
976
+ const type = child.type;
977
+
978
+ if (type === "function_definition") {
979
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
980
+ const [sl, el] = nodeRange(child);
981
+ const params = solidityParamsText(child, source, "parameter");
982
+
983
+ // Check for return type
984
+ let returnType: string | undefined;
985
+ for (let j = 0; j < child.namedChildCount; j++) {
986
+ const c = child.namedChild(j);
987
+ if (c && c.type === "return_type_definition") {
988
+ returnType = source.slice(c.startIndex, c.endIndex);
989
+ break;
990
+ }
991
+ }
992
+
993
+ // Collect qualifiers: visibility, state_mutability, virtual, override, modifiers
994
+ const qualifiers: string[] = [];
995
+ for (let j = 0; j < child.namedChildCount; j++) {
996
+ const c = child.namedChild(j);
997
+ if (!c) continue;
998
+ if (c.type === "visibility" || c.type === "state_mutability") {
999
+ qualifiers.push(source.slice(c.startIndex, c.endIndex));
1000
+ } else if (c.type === "virtual") {
1001
+ qualifiers.push("virtual");
1002
+ } else if (c.type === "override_specifier") {
1003
+ qualifiers.push(source.slice(c.startIndex, c.endIndex));
1004
+ } else if (c.type === "modifier_invocation") {
1005
+ qualifiers.push(source.slice(c.startIndex, c.endIndex));
1006
+ }
1007
+ }
1008
+
1009
+ const parts: string[] = [];
1010
+ if (params) parts.push(params);
1011
+ if (qualifiers.length > 0) parts.push(qualifiers.join(" "));
1012
+ if (returnType) parts.push(returnType);
1013
+ const detail = parts.length > 0 ? parts.join(" ") : undefined;
1014
+
1015
+ return { name, type: "function", startLine: sl, endLine: el, detail };
1016
+ }
1017
+
1018
+ if (type === "constructor_definition") {
1019
+ const [sl, el] = nodeRange(child);
1020
+ const params = solidityParamsText(child, source, "parameter");
1021
+
1022
+ const qualifiers: string[] = [];
1023
+ for (let j = 0; j < child.namedChildCount; j++) {
1024
+ const c = child.namedChild(j);
1025
+ if (!c) continue;
1026
+ if (c.type === "visibility" || c.type === "state_mutability") {
1027
+ qualifiers.push(source.slice(c.startIndex, c.endIndex));
1028
+ } else if (c.type === "modifier_invocation") {
1029
+ qualifiers.push(source.slice(c.startIndex, c.endIndex));
1030
+ }
1031
+ }
1032
+
1033
+ const parts: string[] = [];
1034
+ if (params) parts.push(params);
1035
+ if (qualifiers.length > 0) parts.push(qualifiers.join(" "));
1036
+ const detail = parts.length > 0 ? parts.join(" ") : undefined;
1037
+
1038
+ return { name: "constructor", type: "constructor", startLine: sl, endLine: el, detail };
1039
+ }
1040
+
1041
+ if (type === "modifier_definition") {
1042
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
1043
+ const [sl, el] = nodeRange(child);
1044
+ const detail = solidityParamsText(child, source, "parameter");
1045
+ return { name, type: "modifier", startLine: sl, endLine: el, detail };
1046
+ }
1047
+
1048
+ if (type === "event_definition") {
1049
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
1050
+ const [sl, el] = nodeRange(child);
1051
+ return { name, type: "event", startLine: sl, endLine: el };
1052
+ }
1053
+
1054
+ if (type === "error_declaration") {
1055
+ const name = childText(child, "identifier", source) || "<anonymous>";
1056
+ const [sl, el] = nodeRange(child);
1057
+ // Collect error parameters for detail
1058
+ const params: string[] = [];
1059
+ for (let j = 0; j < child.namedChildCount; j++) {
1060
+ const c = child.namedChild(j);
1061
+ if (c && c.type === "error_parameter") {
1062
+ const typeName = c.childForFieldName("type");
1063
+ if (typeName) {
1064
+ const typeText = source.slice(typeName.startIndex, typeName.endIndex);
1065
+ const paramName = fieldChildText(c, "name", source);
1066
+ params.push(paramName ? `${typeText} ${paramName}` : typeText);
1067
+ }
1068
+ }
1069
+ }
1070
+ const detail = params.length > 0 ? `(${params.join(", ")})` : undefined;
1071
+ return { name, type: "error", startLine: sl, endLine: el, detail };
1072
+ }
1073
+
1074
+ if (type === "struct_declaration") {
1075
+ const name = childText(child, "identifier", source) || fieldChildText(child, "name", source) || "<anonymous>";
1076
+ const [sl, el] = nodeRange(child);
1077
+ return { name, type: "struct", startLine: sl, endLine: el };
1078
+ }
1079
+
1080
+ if (type === "enum_declaration") {
1081
+ const name = fieldChildText(child, "name", source) || childText(child, "identifier", source) || "<anonymous>";
1082
+ const [sl, el] = nodeRange(child);
1083
+ return { name, type: "enum", startLine: sl, endLine: el };
1084
+ }
1085
+
1086
+ if (type === "fallback_receive_definition") {
1087
+ const [sl, el] = nodeRange(child);
1088
+
1089
+ // Distinguish fallback vs receive by checking first keyword child
1090
+ let symbolType = "fallback";
1091
+ for (let j = 0; j < child.childCount; j++) {
1092
+ const c = child.child(j);
1093
+ if (c && c.type === "receive") {
1094
+ symbolType = "receive";
1095
+ break;
1096
+ }
1097
+ }
1098
+
1099
+ const qualifiers: string[] = [];
1100
+ for (let j = 0; j < child.namedChildCount; j++) {
1101
+ const c = child.namedChild(j);
1102
+ if (!c) continue;
1103
+ if (c.type === "visibility" || c.type === "state_mutability") {
1104
+ qualifiers.push(source.slice(c.startIndex, c.endIndex));
1105
+ } else if (c.type === "virtual") {
1106
+ qualifiers.push("virtual");
1107
+ } else if (c.type === "override_specifier") {
1108
+ qualifiers.push(source.slice(c.startIndex, c.endIndex));
1109
+ } else if (c.type === "modifier_invocation") {
1110
+ qualifiers.push(source.slice(c.startIndex, c.endIndex));
1111
+ }
1112
+ }
1113
+
1114
+ return {
1115
+ name: symbolType,
1116
+ type: symbolType,
1117
+ startLine: sl,
1118
+ endLine: el,
1119
+ detail: qualifiers.length > 0 ? qualifiers.join(" ") : undefined,
1120
+ };
1121
+ }
1122
+
1123
+ if (type === "state_variable_declaration") {
1124
+ const name = fieldChildText(child, "name", source) || "<anonymous>";
1125
+ const [sl, el] = nodeRange(child);
1126
+ const varType = child.childForFieldName("type");
1127
+ const detail = varType ? source.slice(varType.startIndex, varType.endIndex) : undefined;
1128
+ return { name, type: "variable", startLine: sl, endLine: el, detail };
1129
+ }
1130
+
1131
+ return null;
1132
+ }
1133
+
1134
+ // ========================================================================
1135
+ // Import extraction
1136
+ // ========================================================================
1137
+
1138
+ /**
1139
+ * Extract import statements from parsed source code.
1140
+ * Returns a list of ImportInfo for each import in the file.
1141
+ */
1142
+ export async function parseFileImports(
1143
+ key: string,
1144
+ source: string,
1145
+ ): Promise<ImportInfo[]> {
1146
+ // Markdown uses regex-based parsing, not tree-sitter
1147
+ if (key === "markdown") {
1148
+ return extractMarkdownLinks(source);
1149
+ }
1150
+
1151
+ await ensureInit();
1152
+
1153
+ const lang = await loadLanguage(key);
1154
+ if (!sharedParser) throw new Error("Parser not initialized");
1155
+
1156
+ sharedParser.setLanguage(lang);
1157
+ const tree = sharedParser.parse(source);
1158
+ if (!tree) return [];
1159
+
1160
+ return extractImports(key, tree.rootNode, source);
1161
+ }
1162
+
1163
+ function extractImports(
1164
+ key: string,
1165
+ node: Node,
1166
+ source: string,
1167
+ ): ImportInfo[] {
1168
+ switch (key) {
1169
+ case "javascript":
1170
+ case "typescript":
1171
+ case "tsx":
1172
+ return extractJSImports(node, source);
1173
+ case "python":
1174
+ return extractPythonImports(node, source);
1175
+ case "go":
1176
+ return extractGoImports(node, source);
1177
+ case "rust":
1178
+ return extractRustImports(node, source);
1179
+ case "solidity":
1180
+ return extractSolidityImports(node, source);
1181
+ case "scss":
1182
+ return extractScssImports(node, source);
1183
+ case "sass":
1184
+ return extractSassImports(node, source);
1185
+ case "java":
1186
+ return extractJavaImports(node, source);
1187
+ case "markdown":
1188
+ return extractMarkdownLinks(source);
1189
+ default:
1190
+ return [];
1191
+ }
1192
+ }
1193
+
1194
+ // ---- JavaScript / TypeScript / TSX imports ----
1195
+
1196
+ function extractJSImports(node: Node, source: string): ImportInfo[] {
1197
+ const results: ImportInfo[] = [];
1198
+
1199
+ for (let i = 0; i < node.namedChildCount; i++) {
1200
+ const child = node.namedChild(i);
1201
+ if (!child) continue;
1202
+
1203
+ // import { foo } from 'bar'; or import foo from 'bar';
1204
+ if (child.type === "import_statement") {
1205
+ const sourceNode = child.childForFieldName("source");
1206
+ if (!sourceNode) continue;
1207
+ const sourceText = extractStringLiteral(sourceNode, source);
1208
+ if (!sourceText) continue;
1209
+
1210
+ const names = extractImportNames(child, source);
1211
+ results.push({
1212
+ source: sourceText,
1213
+ names,
1214
+ lineNumber: child.startPosition.row + 1,
1215
+ });
1216
+ }
1217
+
1218
+ // export { foo } from 'bar';
1219
+ if (child.type === "export_statement") {
1220
+ const sourceNode = child.childForFieldName("source");
1221
+ if (!sourceNode) continue;
1222
+ const sourceText = extractStringLiteral(sourceNode, source);
1223
+ if (!sourceText) continue;
1224
+
1225
+ const names = extractImportNames(child, source);
1226
+
1227
+ results.push({
1228
+ source: sourceText,
1229
+ names,
1230
+ lineNumber: child.startPosition.row + 1,
1231
+ });
1232
+ }
1233
+
1234
+ // const x = require('bar');
1235
+ if (
1236
+ child.type === "lexical_declaration" ||
1237
+ child.type === "variable_declaration"
1238
+ ) {
1239
+ for (let j = 0; j < child.namedChildCount; j++) {
1240
+ const decl = child.namedChild(j);
1241
+ if (!decl || decl.type !== "variable_declarator") continue;
1242
+ const value = decl.childForFieldName("value");
1243
+ if (!value || value.type !== "call_expression") continue;
1244
+ const func = value.childForFieldName("function");
1245
+ if (!func) continue;
1246
+ const funcText = source.slice(func.startIndex, func.endIndex);
1247
+ if (funcText !== "require") continue;
1248
+
1249
+ const args = value.childForFieldName("arguments");
1250
+ if (!args || args.namedChildCount === 0) continue;
1251
+ const firstArg = args.namedChild(0);
1252
+ if (!firstArg) continue;
1253
+ const sourceText = extractStringLiteral(firstArg, source);
1254
+ if (!sourceText) continue;
1255
+
1256
+ const name = fieldChildText(decl, "name", source);
1257
+ results.push({
1258
+ source: sourceText,
1259
+ names: name ? [name] : [],
1260
+ lineNumber: child.startPosition.row + 1,
1261
+ });
1262
+ }
1263
+ }
1264
+ }
1265
+
1266
+ return results;
1267
+ }
1268
+
1269
+ function extractImportNames(node: Node, source: string): string[] {
1270
+ const names: string[] = [];
1271
+
1272
+ // import_clause is a named child, NOT a field in tree-sitter grammar
1273
+ for (let j = 0; j < node.namedChildCount; j++) {
1274
+ const c = node.namedChild(j);
1275
+ if (!c) continue;
1276
+
1277
+ if (c.type === "import_clause") {
1278
+ extractNamesFromClause(c, source, names);
1279
+ }
1280
+
1281
+ // export_statement may have export_clause with export_specifiers
1282
+ if (c.type === "export_clause") {
1283
+ for (let k = 0; k < c.namedChildCount; k++) {
1284
+ const spec = c.namedChild(k);
1285
+ if (spec && spec.type === "export_specifier") {
1286
+ const name = fieldChildText(spec, "name", source);
1287
+ if (name) names.push(name);
1288
+ }
1289
+ }
1290
+ }
1291
+ }
1292
+
1293
+ return names;
1294
+ }
1295
+
1296
+ /** Extract imported symbol names from an import_clause node. */
1297
+ function extractNamesFromClause(clause: Node, source: string, names: string[]): void {
1298
+ for (let j = 0; j < clause.namedChildCount; j++) {
1299
+ const c = clause.namedChild(j);
1300
+ if (!c) continue;
1301
+
1302
+ if (c.type === "namespace_import") {
1303
+ const alias = c.childForFieldName("alias");
1304
+ if (alias) names.push("* as " + source.slice(alias.startIndex, alias.endIndex));
1305
+ } else if (c.type === "named_imports") {
1306
+ for (let k = 0; k < c.namedChildCount; k++) {
1307
+ const spec = c.namedChild(k);
1308
+ if (spec && spec.type === "import_specifier") {
1309
+ const name = fieldChildText(spec, "name", source);
1310
+ if (name) names.push(name);
1311
+ }
1312
+ }
1313
+ } else {
1314
+ // default import (identifier)
1315
+ names.push(source.slice(c.startIndex, c.endIndex));
1316
+ }
1317
+ }
1318
+ }
1319
+
1320
+ // ---- Python imports ----
1321
+
1322
+ function extractPythonImports(node: Node, source: string): ImportInfo[] {
1323
+ const results: ImportInfo[] = [];
1324
+
1325
+ for (let i = 0; i < node.namedChildCount; i++) {
1326
+ const child = node.namedChild(i);
1327
+ if (!child) continue;
1328
+
1329
+ // import foo
1330
+ if (child.type === "import_statement") {
1331
+ for (let j = 0; j < child.namedChildCount; j++) {
1332
+ const c = child.namedChild(j);
1333
+ if (!c) continue;
1334
+ if (c.type === "dotted_name") {
1335
+ const mod = source.slice(c.startIndex, c.endIndex);
1336
+ results.push({
1337
+ source: mod,
1338
+ names: [],
1339
+ lineNumber: child.startPosition.row + 1,
1340
+ });
1341
+ } else if (c.type === "aliased_import") {
1342
+ const name = fieldChildText(c, "name", source);
1343
+ const alias = fieldChildText(c, "alias", source);
1344
+ if (name) {
1345
+ results.push({
1346
+ source: name,
1347
+ names: alias ? [alias] : [],
1348
+ lineNumber: child.startPosition.row + 1,
1349
+ });
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+
1355
+ // from foo import bar
1356
+ if (child.type === "import_from_statement") {
1357
+ const moduleNode = child.childForFieldName("module_name");
1358
+ const sourceText = moduleNode
1359
+ ? source.slice(moduleNode.startIndex, moduleNode.endIndex)
1360
+ : "";
1361
+ if (!sourceText) continue;
1362
+
1363
+ const names: string[] = [];
1364
+ for (let j = 0; j < child.namedChildCount; j++) {
1365
+ const c = child.namedChild(j);
1366
+ if (!c) continue;
1367
+ if (c.type === "dotted_name") {
1368
+ names.push(source.slice(c.startIndex, c.endIndex));
1369
+ } else if (c.type === "aliased_import") {
1370
+ const name = fieldChildText(c, "name", source);
1371
+ if (name) names.push(name);
1372
+ } else if (c.type === "wildcard_import") {
1373
+ names.push("*");
1374
+ }
1375
+ }
1376
+
1377
+ results.push({
1378
+ source: sourceText,
1379
+ names,
1380
+ lineNumber: child.startPosition.row + 1,
1381
+ });
1382
+ }
1383
+ }
1384
+
1385
+ return results;
1386
+ }
1387
+
1388
+ // ---- Go imports ----
1389
+
1390
+ function extractGoImports(node: Node, source: string): ImportInfo[] {
1391
+ const results: ImportInfo[] = [];
1392
+
1393
+ for (let i = 0; i < node.namedChildCount; i++) {
1394
+ const child = node.namedChild(i);
1395
+ if (!child || child.type !== "import_declaration") continue;
1396
+
1397
+ // Go's import_declaration wraps: single import_spec, or import_spec_list (grouped)
1398
+ for (let j = 0; j < child.namedChildCount; j++) {
1399
+ const spec = child.namedChild(j);
1400
+ if (!spec) continue;
1401
+
1402
+ // Grouped imports: import_spec_list contains import_spec children
1403
+ if (spec.type === "import_spec_list") {
1404
+ for (let k = 0; k < spec.namedChildCount; k++) {
1405
+ const innerSpec = spec.namedChild(k);
1406
+ if (!innerSpec || innerSpec.type !== "import_spec") continue;
1407
+ const result = extractGoImportSpec(innerSpec, source);
1408
+ if (result) results.push(result);
1409
+ }
1410
+ } else if (spec.type === "import_spec") {
1411
+ const result = extractGoImportSpec(spec, source);
1412
+ if (result) results.push(result);
1413
+ }
1414
+ }
1415
+ }
1416
+
1417
+ return results;
1418
+ }
1419
+
1420
+ function extractGoImportSpec(spec: Node, source: string): ImportInfo | null {
1421
+ const pathNode = spec.childForFieldName("path");
1422
+ if (!pathNode) return null;
1423
+ const sourceText = extractStringLiteral(pathNode, source);
1424
+ if (!sourceText) return null;
1425
+
1426
+ const name = fieldChildText(spec, "name", source);
1427
+ return {
1428
+ source: sourceText,
1429
+ names: name ? [name] : [],
1430
+ lineNumber: spec.startPosition.row + 1,
1431
+ };
1432
+ }
1433
+
1434
+ // ---- Rust imports ----
1435
+
1436
+ function extractRustImports(node: Node, source: string): ImportInfo[] {
1437
+ const results: ImportInfo[] = [];
1438
+
1439
+ for (let i = 0; i < node.namedChildCount; i++) {
1440
+ const child = node.namedChild(i);
1441
+ if (!child) continue;
1442
+
1443
+ // use foo::bar;
1444
+ if (child.type === "use_declaration") {
1445
+ const arg = child.childForFieldName("argument");
1446
+ if (!arg) continue;
1447
+
1448
+ const sourceText = source
1449
+ .slice(arg.startIndex, arg.endIndex)
1450
+ .replace(/\s+/g, " ");
1451
+ results.push({
1452
+ source: sourceText,
1453
+ names: [],
1454
+ lineNumber: child.startPosition.row + 1,
1455
+ });
1456
+ }
1457
+
1458
+ // extern crate foo;
1459
+ if (child.type === "extern_crate_declaration") {
1460
+ const name = fieldChildText(child, "name", source);
1461
+ if (name) {
1462
+ results.push({
1463
+ source: name,
1464
+ names: [],
1465
+ lineNumber: child.startPosition.row + 1,
1466
+ });
1467
+ }
1468
+ }
1469
+ }
1470
+
1471
+ return results;
1472
+ }
1473
+
1474
+ // ---- Solidity imports ----
1475
+
1476
+ function extractSolidityImports(node: Node, source: string): ImportInfo[] {
1477
+ const results: ImportInfo[] = [];
1478
+
1479
+ for (let i = 0; i < node.namedChildCount; i++) {
1480
+ const child = node.namedChild(i);
1481
+ if (!child || child.type !== "import_directive") continue;
1482
+
1483
+ const sourceNode = child.childForFieldName("source");
1484
+ if (!sourceNode) continue;
1485
+ const sourceText = extractStringLiteral(sourceNode, source);
1486
+ if (!sourceText) continue;
1487
+
1488
+ const names: string[] = [];
1489
+ // Check for named imports: import {A, B} from "foo.sol";
1490
+ for (let j = 0; j < child.namedChildCount; j++) {
1491
+ const c = child.namedChild(j);
1492
+ if (!c) continue;
1493
+ if (c.type === "import_clause") {
1494
+ for (let k = 0; k < c.namedChildCount; k++) {
1495
+ const sym = c.namedChild(k);
1496
+ if (sym && sym.type === "symbol_alias") {
1497
+ const name = fieldChildText(sym, "name", source);
1498
+ if (name) names.push(name);
1499
+ }
1500
+ }
1501
+ }
1502
+ }
1503
+
1504
+ results.push({
1505
+ source: sourceText,
1506
+ names,
1507
+ lineNumber: child.startPosition.row + 1,
1508
+ });
1509
+ }
1510
+
1511
+ return results;
1512
+ }
1513
+
1514
+ // ---- Markdown extraction ----
1515
+
1516
+ /**
1517
+ * Extract structural symbols from Markdown content.
1518
+ * Since Markdown doesn't use tree-sitter, we use regex-based parsing.
1519
+ * Extracts headings as symbols, with nested structure based on heading levels.
1520
+ * Code blocks are nested under their parent heading.
1521
+ */
1522
+ function extractMarkdown(source: string): SymbolInfo[] {
1523
+ const lines = source.split("\n");
1524
+ const results: SymbolInfo[] = [];
1525
+
1526
+ // Track heading hierarchy for nesting
1527
+ const stack: SymbolInfo[] = [];
1528
+
1529
+ // First pass: extract headings and build hierarchy
1530
+ for (let i = 0; i < lines.length; i++) {
1531
+ const line = lines[i];
1532
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
1533
+
1534
+ if (headingMatch) {
1535
+ const level = headingMatch[1].length;
1536
+ const title = headingMatch[2].trim();
1537
+ const startLine = i + 1; // 1-indexed
1538
+
1539
+ const symbol: SymbolInfo = {
1540
+ name: title,
1541
+ type: `heading${level}`,
1542
+ startLine,
1543
+ endLine: lines.length, // Will be updated later
1544
+ children: [],
1545
+ detail: `#`.repeat(level),
1546
+ };
1547
+
1548
+ // Find parent: pop stack until we find a heading with lower level
1549
+ while (stack.length > 0) {
1550
+ const parentLevel = parseInt(stack[stack.length - 1].type?.replace('heading', '') || '0', 10);
1551
+ if (parentLevel >= level) {
1552
+ stack.pop();
1553
+ } else {
1554
+ break;
1555
+ }
1556
+ }
1557
+
1558
+ // Add to parent or root
1559
+ if (stack.length > 0) {
1560
+ stack[stack.length - 1].children!.push(symbol);
1561
+ } else {
1562
+ results.push(symbol);
1563
+ }
1564
+
1565
+ stack.push(symbol);
1566
+ }
1567
+ }
1568
+
1569
+ // Update endLine for each heading: it ends just before the next sibling or parent's next sibling
1570
+ updateEndLines(results, lines.length);
1571
+
1572
+ // Second pass: extract code blocks and nest them under appropriate headings
1573
+ const codeBlockRegex = /^```(\w+)\s*$/gm; // Only match opening ``` with a language
1574
+ let match;
1575
+ while ((match = codeBlockRegex.exec(source)) !== null) {
1576
+ const language = match[1];
1577
+ const startLine = source.slice(0, match.index).split("\n").length;
1578
+
1579
+ // Find closing ```
1580
+ const afterMatch = source.slice(match.index + match[0].length);
1581
+ const closingIdx = afterMatch.search(/^```\s*$/m);
1582
+ let endLine = startLine + 1;
1583
+ if (closingIdx !== -1) {
1584
+ endLine = startLine + afterMatch.slice(0, closingIdx).split("\n").length + 1;
1585
+ }
1586
+
1587
+ const codeBlock: SymbolInfo = {
1588
+ name: `code_block_${language}`,
1589
+ type: "code_block",
1590
+ startLine,
1591
+ endLine,
1592
+ detail: language,
1593
+ };
1594
+
1595
+ // Find the heading that contains this code block
1596
+ const parent = findParentHeading(results, startLine);
1597
+ if (parent) {
1598
+ parent.children!.push(codeBlock);
1599
+ } else {
1600
+ results.push(codeBlock);
1601
+ }
1602
+ }
1603
+
1604
+ return results;
1605
+ }
1606
+
1607
+ /** Update endLine for each heading based on next sibling or parent's next sibling. */
1608
+ function updateEndLines(symbols: SymbolInfo[], totalLines: number): void {
1609
+ for (let i = 0; i < symbols.length; i++) {
1610
+ const symbol = symbols[i];
1611
+
1612
+ // Default: ends at file end
1613
+ symbol.endLine = totalLines;
1614
+
1615
+ // If there's a next sibling, this heading ends just before it
1616
+ if (i + 1 < symbols.length) {
1617
+ symbol.endLine = symbols[i + 1].startLine - 1;
1618
+ }
1619
+
1620
+ // Recurse into children
1621
+ if (symbol.children && symbol.children.length > 0) {
1622
+ updateEndLines(symbol.children, symbol.endLine);
1623
+ // The last child's endLine might extend to parent's endLine
1624
+ const lastChild = symbol.children[symbol.children.length - 1];
1625
+ if (lastChild.endLine < symbol.endLine) {
1626
+ lastChild.endLine = symbol.endLine;
1627
+ }
1628
+ }
1629
+ }
1630
+ }
1631
+
1632
+ /** Find the heading that contains the given line number. */
1633
+ function findParentHeading(symbols: SymbolInfo[], line: number): SymbolInfo | null {
1634
+ for (const symbol of symbols) {
1635
+ if (symbol.type?.startsWith("heading")) {
1636
+ // Check if line is within this heading's range
1637
+ if (line >= symbol.startLine && line <= symbol.endLine) {
1638
+ // Try to find a more specific child
1639
+ if (symbol.children && symbol.children.length > 0) {
1640
+ const child = findParentHeading(symbol.children, line);
1641
+ if (child) return child;
1642
+ }
1643
+ return symbol;
1644
+ }
1645
+ }
1646
+ }
1647
+ return null;
1648
+ }
1649
+
1650
+ /**
1651
+ * Extract links from Markdown content as "imports".
1652
+ * Links to local files are considered imports.
1653
+ */
1654
+ function extractMarkdownLinks(source: string): ImportInfo[] {
1655
+ const results: ImportInfo[] = [];
1656
+ const lines = source.split("\n");
1657
+
1658
+ // Match markdown links: [text](url)
1659
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
1660
+
1661
+ for (let i = 0; i < lines.length; i++) {
1662
+ const line = lines[i];
1663
+ let match;
1664
+
1665
+ while ((match = linkRegex.exec(line)) !== null) {
1666
+ const linkText = match[1];
1667
+ const url = match[2];
1668
+
1669
+ // Skip external URLs
1670
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("mailto:")) {
1671
+ continue;
1672
+ }
1673
+
1674
+ results.push({
1675
+ source: url,
1676
+ names: [linkText],
1677
+ lineNumber: i + 1,
1678
+ });
1679
+ }
1680
+ }
1681
+
1682
+ // Also extract import-like references: ![alt](image)
1683
+ const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
1684
+ for (let i = 0; i < lines.length; i++) {
1685
+ const line = lines[i];
1686
+ let match;
1687
+
1688
+ while ((match = imageRegex.exec(line)) !== null) {
1689
+ const altText = match[1] || "image";
1690
+ const url = match[2];
1691
+
1692
+ // Skip external URLs
1693
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("mailto:")) {
1694
+ continue;
1695
+ }
1696
+
1697
+ results.push({
1698
+ source: url,
1699
+ names: [altText],
1700
+ lineNumber: i + 1,
1701
+ });
1702
+ }
1703
+ }
1704
+
1705
+ return results;
1706
+ }
1707
+
1708
+ // ---- Shared helpers ----
1709
+
1710
+ /** Extract a string literal's value (strip surrounding quotes). */
1711
+ function extractStringLiteral(node: Node, source: string): string | null {
1712
+ const text = source.slice(node.startIndex, node.endIndex);
1713
+ // Handle single, double, and backtick quotes
1714
+ const match = text.match(/^['"`](.*)['"`]$/s);
1715
+ return match ? match[1] : text;
1716
+ }
1717
+
1718
+ // ---- Re-export ImportInfo for backward compatibility ----
1719
+
1720
+ export type { ImportInfo };
1721
+