@mrclrchtr/supi-tree-sitter 9.1.2 → 11.0.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 (41) hide show
  1. package/README.md +63 -17
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/LICENSE +21 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/README.md +743 -0
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/index.js +17 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/constants.js +184 -0
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/parse.js +1425 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/picomatch.js +361 -0
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/scan.js +403 -0
  9. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/utils.js +72 -0
  10. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/package.json +83 -0
  11. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/posix.js +3 -0
  12. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +4 -1
  13. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +11 -0
  14. package/node_modules/@mrclrchtr/supi-code-runtime/src/file-associations.ts +190 -0
  15. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +22 -0
  16. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  17. package/package.json +3 -3
  18. package/src/language.ts +16 -10
  19. package/src/operation-support.ts +13 -6
  20. package/src/provider/tree-sitter-provider.ts +19 -1
  21. package/src/session/runtime-controller.ts +18 -4
  22. package/src/session/session.ts +10 -3
  23. package/src/session/structural-worker-client.ts +17 -2
  24. package/src/session/structural-worker-process.ts +22 -5
  25. package/src/session/structural-worker-protocol.ts +36 -1
  26. package/src/syntax-errors.ts +73 -0
  27. package/src/syntax-node.ts +4 -0
  28. package/src/tool/call-sites.ts +15 -9
  29. package/src/tool/callees.ts +24 -15
  30. package/src/tool/exports.ts +8 -1
  31. package/src/tool/imports.ts +233 -7
  32. package/src/tool/node-at.ts +7 -3
  33. package/src/tool/outline-expansion.ts +78 -0
  34. package/src/tool/outline-jvm.ts +25 -4
  35. package/src/tool/outline-nested.ts +390 -0
  36. package/src/tool/outline.ts +49 -28
  37. package/src/types.ts +16 -0
  38. package/src/worker/process-host.mjs +61 -24
  39. package/src/worker/runtime.ts +44 -10
  40. package/src/worker/service.ts +13 -9
  41. package/src/worker/worker-main.ts +15 -2
@@ -0,0 +1,73 @@
1
+ import { nodeToRange } from "./coordinates.ts";
2
+ import type { SyntaxNodeLike } from "./syntax-node.ts";
3
+ import type { GrammarId, SourceRange } from "./types.ts";
4
+
5
+ /** Return whether a parsed tree contains an explicit or recovered syntax error. */
6
+ export function hasSyntaxErrors(root: SyntaxNodeLike): boolean {
7
+ if (root.hasError === true) return true;
8
+ if (root.type === "ERROR" || root.isMissing === true) return true;
9
+ return (root.children ?? []).some(hasSyntaxErrors);
10
+ }
11
+
12
+ /** Collect explicit ERROR and inserted-node ranges from one parser tree. */
13
+ export function collectSyntaxErrorRanges(root: SyntaxNodeLike, source: string): SourceRange[] {
14
+ const ranges: SourceRange[] = [];
15
+ const seen = new Set<string>();
16
+ const visit = (node: SyntaxNodeLike): void => {
17
+ if (node.type === "ERROR" || node.isMissing === true) {
18
+ const range = nodeToRange(node, source);
19
+ const key = `${range.startLine}:${range.startCharacter}:${range.endLine}:${range.endCharacter}`;
20
+ if (!seen.has(key)) {
21
+ seen.add(key);
22
+ ranges.push(range);
23
+ }
24
+ return;
25
+ }
26
+ for (const child of node.children ?? []) visit(child);
27
+ };
28
+ visit(root);
29
+ return ranges;
30
+ }
31
+
32
+ interface SyntaxCompletenessInput<T> {
33
+ readonly data: T;
34
+ readonly file: string;
35
+ readonly root: SyntaxNodeLike;
36
+ readonly source: string;
37
+ readonly grammarId: GrammarId;
38
+ }
39
+
40
+ /** Attach explicit recovery metadata when a parsed tree contains syntax errors. */
41
+ export function withSyntaxCompleteness<T>(input: SyntaxCompletenessInput<T>):
42
+ | { kind: "success"; data: T }
43
+ | {
44
+ kind: "partial";
45
+ data: T;
46
+ file: string;
47
+ reason: string;
48
+ errorRanges: SourceRange[];
49
+ } {
50
+ const { data, file, root, source, grammarId } = input;
51
+ if (!PARTIAL_EVIDENCE_GRAMMARS.has(grammarId) || !hasSyntaxErrors(root)) {
52
+ return { kind: "success", data };
53
+ }
54
+ return {
55
+ kind: "partial",
56
+ data,
57
+ file,
58
+ reason: "Tree-sitter recovered syntax; structural evidence may be incomplete.",
59
+ errorRanges: collectSyntaxErrorRanges(root, source),
60
+ };
61
+ }
62
+
63
+ const PARTIAL_EVIDENCE_GRAMMARS: ReadonlySet<GrammarId> = new Set([
64
+ "javascript",
65
+ "typescript",
66
+ "tsx",
67
+ "python",
68
+ "java",
69
+ "go",
70
+ "rust",
71
+ "c",
72
+ "cpp",
73
+ ]);
@@ -7,6 +7,10 @@ export interface SyntaxNodeLike {
7
7
  text: string;
8
8
  children: SyntaxNodeLike[];
9
9
  parent: SyntaxNodeLike | null;
10
+ /** True when this syntax node is an inserted recovery node. */
11
+ isMissing?: boolean;
12
+ /** True when this node contains a syntax error or inserted node. */
13
+ hasError?: boolean;
10
14
  startPosition: { row: number; column: number };
11
15
  endPosition: { row: number; column: number };
12
16
  childForFieldName(name: string): SyntaxNodeLike | null;
@@ -1,7 +1,6 @@
1
1
  /** File-wide call-site extraction using per-grammar tree-sitter queries. */
2
2
 
3
3
  import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
4
- import { detectGrammar } from "../language.ts";
5
4
  import type { CallSiteMatch, GrammarId, TreeSitterResult } from "../types.ts";
6
5
  import type { TreeSitterRuntime } from "../worker/runtime.ts";
7
6
  import { normalizeCallName } from "./call-name.ts";
@@ -42,15 +41,14 @@ export function supportsCallSitesGrammar(grammarId: GrammarId): boolean {
42
41
  /**
43
42
  * Extract all call-site callee expressions from a file.
44
43
  *
45
- * Returns a deduplicated list of { name, startLine } for every call-site
46
- * captured by the grammar-specific query.
44
+ * Returns each written call-site expression once, keyed by its exact source range.
47
45
  */
48
46
  export async function extractCallSites(
49
47
  runtime: TreeSitterRuntime,
50
48
  filePath: string,
51
49
  control?: CodeRequestControl,
52
50
  ): Promise<TreeSitterResult<CallSiteMatch[]>> {
53
- const grammarId = detectGrammar(filePath);
51
+ const grammarId = runtime.getGrammarForFile(filePath);
54
52
  if (!grammarId) {
55
53
  return {
56
54
  kind: "unsupported-language",
@@ -69,7 +67,7 @@ export async function extractCallSites(
69
67
  }
70
68
 
71
69
  const queryResult = await runtime.queryFile(filePath, queryStr, control);
72
- if (queryResult.kind !== "success") return queryResult;
70
+ if (queryResult.kind !== "success" && queryResult.kind !== "partial") return queryResult;
73
71
 
74
72
  const captures = queryResult.data;
75
73
  const seen = new Set<string>();
@@ -80,13 +78,21 @@ export async function extractCallSites(
80
78
  const name = normalizeCallName(capture.text, grammarId, capture.nodeType);
81
79
  if (name.length === 0) continue;
82
80
 
83
- // Deduplicate by name + line
84
- const key = `${name}:${capture.range.startLine}`;
81
+ // Collapse duplicate captures only when they identify the same source range.
82
+ const key = `${capture.range.startLine}:${capture.range.startCharacter}:${capture.range.endLine}:${capture.range.endCharacter}`;
85
83
  if (seen.has(key)) continue;
86
84
  seen.add(key);
87
85
 
88
- matches.push({ name, startLine: capture.range.startLine });
86
+ matches.push({
87
+ name,
88
+ startLine: capture.range.startLine,
89
+ startCharacter: capture.range.startCharacter,
90
+ endLine: capture.range.endLine,
91
+ endCharacter: capture.range.endCharacter,
92
+ });
89
93
  }
90
94
 
91
- return { kind: "success", data: matches };
95
+ return queryResult.kind === "partial"
96
+ ? { ...queryResult, data: matches }
97
+ : { kind: "success", data: matches };
92
98
  }
@@ -3,7 +3,7 @@
3
3
 
4
4
  import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
5
5
  import { validatePublicPositionBounds } from "../coordinates.ts";
6
- import { detectGrammar } from "../language.ts";
6
+ import { withSyntaxCompleteness } from "../syntax-errors.ts";
7
7
  import type { SyntaxNodeLike } from "../syntax-node.ts";
8
8
  import type { GrammarId, SourceRange, TreeSitterResult } from "../types.ts";
9
9
  import { queryParsedFile, type TreeSitterRuntime } from "../worker/runtime.ts";
@@ -82,11 +82,13 @@ const ENCLOSING_SCOPE_TYPES: Record<GrammarId, ReadonlySet<string>> = {
82
82
  // ── Main entrypoint ──────────────────────────────────────────────────
83
83
 
84
84
  /** Validate that coordinates are usable and grammar is supported. */
85
- function validateCalleeInput(
86
- filePath: string,
87
- line: number,
88
- character: number,
89
- ): { kind: "ok"; grammarId: GrammarId } | TreeSitterResult<CalleesAtResult> {
85
+ function validateCalleeInput(input: {
86
+ filePath: string;
87
+ line: number;
88
+ character: number;
89
+ grammarId: GrammarId | undefined;
90
+ }): { kind: "ok" } | TreeSitterResult<CalleesAtResult> {
91
+ const { filePath, line, character, grammarId } = input;
90
92
  if (!Number.isInteger(line) || line < 1) {
91
93
  return {
92
94
  kind: "validation-error" as const,
@@ -100,7 +102,6 @@ function validateCalleeInput(
100
102
  };
101
103
  }
102
104
 
103
- const grammarId = detectGrammar(filePath);
104
105
  if (!grammarId) {
105
106
  return {
106
107
  kind: "unsupported-language" as const,
@@ -117,7 +118,7 @@ function validateCalleeInput(
117
118
  };
118
119
  }
119
120
 
120
- return { kind: "ok", grammarId };
121
+ return { kind: "ok" };
121
122
  }
122
123
 
123
124
  /** Find the enclosing function/method node at a position in the tree. */
@@ -155,15 +156,19 @@ export async function lookupCalleesAt(
155
156
  control?: CodeRequestControl,
156
157
  ): Promise<TreeSitterResult<CalleesAtResult>> {
157
158
  // Validate coordinates and grammar
158
- const validation = validateCalleeInput(filePath, line, character);
159
+ const validation = validateCalleeInput({
160
+ filePath,
161
+ line,
162
+ character,
163
+ grammarId: runtime.getGrammarForFile(filePath),
164
+ });
159
165
  if (validation.kind !== "ok") return validation;
160
- const { grammarId } = validation;
161
166
 
162
167
  // Parse the file
163
168
  const parseResult = await runtime.parseFile(filePath, control);
164
169
  if (parseResult.kind !== "success") return parseResult;
165
170
 
166
- const { tree, source } = parseResult.data;
171
+ const { tree, source, grammarId } = parseResult.data;
167
172
 
168
173
  try {
169
174
  const boundsError = validatePublicPositionBounds(line, character, source);
@@ -197,13 +202,14 @@ export async function lookupCalleesAt(
197
202
  }
198
203
 
199
204
  const queryResult = await queryParsedFile(runtime, {
205
+ file: filePath,
200
206
  grammarId,
201
207
  tree,
202
208
  source,
203
209
  queryString: queryStr,
204
210
  control,
205
211
  });
206
- if (queryResult.kind !== "success") {
212
+ if (queryResult.kind !== "success" && queryResult.kind !== "partial") {
207
213
  return {
208
214
  kind: "runtime-error",
209
215
  message: "Callee query failed",
@@ -223,8 +229,7 @@ export async function lookupCalleesAt(
223
229
  const enclosingRange = nodeToSourceRange(enclosingNode);
224
230
  const scopeName = extractScopeName(enclosingNode);
225
231
 
226
- return {
227
- kind: "success",
232
+ return withSyntaxCompleteness({
228
233
  data: {
229
234
  enclosingScope: {
230
235
  name: scopeName,
@@ -233,7 +238,11 @@ export async function lookupCalleesAt(
233
238
  callees,
234
239
  depth,
235
240
  },
236
- };
241
+ file: filePath,
242
+ root: tree.rootNode,
243
+ source,
244
+ grammarId,
245
+ });
237
246
  } finally {
238
247
  tree.delete();
239
248
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
4
4
  import { nodeToRange } from "../coordinates.ts";
5
+ import { withSyntaxCompleteness } from "../syntax-errors.ts";
5
6
  import type { SyntaxNodeLike } from "../syntax-node.ts";
6
7
  import type { ExportRecord, TreeSitterResult } from "../types.ts";
7
8
  import type { TreeSitterRuntime } from "../worker/runtime.ts";
@@ -21,7 +22,13 @@ export async function extractExports(
21
22
 
22
23
  try {
23
24
  collectTopLevelExports(tree.rootNode, source, exports);
24
- return { kind: "success", data: exports };
25
+ return withSyntaxCompleteness({
26
+ data: exports,
27
+ file: filePath,
28
+ root: tree.rootNode,
29
+ source,
30
+ grammarId: parseResult.data.grammarId,
31
+ });
25
32
  } finally {
26
33
  tree.delete();
27
34
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
4
4
  import { nodeToRange } from "../coordinates.ts";
5
+ import { withSyntaxCompleteness } from "../syntax-errors.ts";
5
6
  import type { SyntaxNodeLike } from "../syntax-node.ts";
6
7
  import type { ImportRecord, TreeSitterResult } from "../types.ts";
7
8
  import type { TreeSitterRuntime } from "../worker/runtime.ts";
@@ -19,12 +20,36 @@ export async function extractImports(
19
20
  const imports: ImportRecord[] = [];
20
21
 
21
22
  try {
22
- if (parseResult.data.grammarId === "latex") {
23
- walkForLatexImports(tree.rootNode, source, imports);
24
- } else {
25
- walkForImports(tree.rootNode, source, imports);
23
+ switch (parseResult.data.grammarId) {
24
+ case "latex":
25
+ walkForLatexImports(tree.rootNode, source, imports);
26
+ break;
27
+ case "python":
28
+ walkForPythonImports(tree.rootNode, source, imports);
29
+ break;
30
+ case "java":
31
+ walkForJavaImports(tree.rootNode, source, imports);
32
+ break;
33
+ case "go":
34
+ walkForGoImports(tree.rootNode, source, imports);
35
+ break;
36
+ case "rust":
37
+ walkForRustImports(tree.rootNode, source, imports);
38
+ break;
39
+ case "c":
40
+ case "cpp":
41
+ walkForCIncludes(tree.rootNode, source, imports);
42
+ break;
43
+ default:
44
+ walkForImports(tree.rootNode, source, imports);
26
45
  }
27
- return { kind: "success", data: imports };
46
+ return withSyntaxCompleteness({
47
+ data: imports,
48
+ file: filePath,
49
+ root: tree.rootNode,
50
+ source,
51
+ grammarId: parseResult.data.grammarId,
52
+ });
28
53
  } finally {
29
54
  tree.delete();
30
55
  }
@@ -35,18 +60,219 @@ function walkForImports(node: SyntaxNodeLike, source: string, imports: ImportRec
35
60
  const sourceNode = node.childForFieldName("source");
36
61
  if (sourceNode) {
37
62
  const specifier = sourceNode.text.replace(/^["']|["']$/g, "");
63
+ imports.push({ moduleSpecifier: specifier, range: nodeToRange(node, source) });
64
+ }
65
+ return;
66
+ }
67
+ for (const child of node.children) walkForImports(child, source, imports);
68
+ }
69
+
70
+ function walkForPythonImports(node: SyntaxNodeLike, source: string, imports: ImportRecord[]): void {
71
+ if (node.type === "import_statement") {
72
+ for (const child of node.children) {
73
+ const path = pythonImportPath(child);
74
+ if (path) imports.push({ moduleSpecifier: path, range: nodeToRange(node, source) });
75
+ }
76
+ return;
77
+ }
78
+ if (node.type === "import_from_statement") {
79
+ const module = node.children.find(
80
+ (child) => child.type === "relative_import" || child.type === "dotted_name",
81
+ );
82
+ if (module) imports.push({ moduleSpecifier: module.text, range: nodeToRange(node, source) });
83
+ return;
84
+ }
85
+ for (const child of node.children) walkForPythonImports(child, source, imports);
86
+ }
87
+
88
+ function pythonImportPath(node: SyntaxNodeLike): string | null {
89
+ if (node.type === "aliased_import") {
90
+ const path = node.children.find(
91
+ (child) => child.type === "dotted_name" || child.type === "identifier",
92
+ );
93
+ return path?.text ?? null;
94
+ }
95
+ return node.type === "dotted_name" || node.type === "identifier" ? node.text : null;
96
+ }
97
+
98
+ function walkForJavaImports(node: SyntaxNodeLike, source: string, imports: ImportRecord[]): void {
99
+ if (node.type === "import_declaration") {
100
+ const path = node.children.find(
101
+ (child) => child.type === "scoped_identifier" || child.type === "identifier",
102
+ );
103
+ if (path) {
104
+ const wildcard = node.children.some(
105
+ (child) => child.type === "*" || child.type === "asterisk",
106
+ );
38
107
  imports.push({
39
- moduleSpecifier: specifier,
108
+ moduleSpecifier: `${path.text}${wildcard ? ".*" : ""}`,
40
109
  range: nodeToRange(node, source),
41
110
  });
42
111
  }
43
112
  return;
44
113
  }
114
+ for (const child of node.children) walkForJavaImports(child, source, imports);
115
+ }
116
+
117
+ function walkForGoImports(node: SyntaxNodeLike, source: string, imports: ImportRecord[]): void {
118
+ if (node.type === "import_spec") {
119
+ const literal = node.children.find(
120
+ (child) => child.type === "interpreted_string_literal" || child.type === "raw_string_literal",
121
+ );
122
+ if (literal) {
123
+ imports.push({
124
+ moduleSpecifier: literalContent(literal.text),
125
+ range: nodeToRange(node, source),
126
+ });
127
+ }
128
+ return;
129
+ }
130
+ for (const child of node.children) walkForGoImports(child, source, imports);
131
+ }
132
+
133
+ function walkForRustImports(node: SyntaxNodeLike, source: string, imports: ImportRecord[]): void {
134
+ if (node.type === "use_declaration") {
135
+ const argument = node.childForFieldName("argument");
136
+ if (argument) flattenRustUseTree(argument, "", source, imports);
137
+ return;
138
+ }
139
+ if (node.type === "extern_crate_declaration") {
140
+ const crate = node.children.find((child) => child.type === "identifier");
141
+ if (crate) imports.push({ moduleSpecifier: crate.text, range: nodeToRange(node, source) });
142
+ return;
143
+ }
144
+ if (node.type === "mod_item") {
145
+ const body = node.childForFieldName("body");
146
+ if (body) {
147
+ walkForRustImports(body, source, imports);
148
+ } else {
149
+ const name = node.childForFieldName("name");
150
+ if (name) imports.push({ moduleSpecifier: name.text, range: nodeToRange(node, source) });
151
+ }
152
+ return;
153
+ }
154
+ for (const child of node.children) walkForRustImports(child, source, imports);
155
+ }
156
+
157
+ function flattenRustUseTree(
158
+ node: SyntaxNodeLike,
159
+ prefix: string,
160
+ source: string,
161
+ imports: ImportRecord[],
162
+ ): void {
163
+ switch (node.type) {
164
+ case "scoped_use_list":
165
+ flattenRustScopedUseList(node, prefix, source, imports);
166
+ return;
167
+ case "use_list":
168
+ flattenRustUseList(node, prefix, source, imports);
169
+ return;
170
+ case "use_as_clause":
171
+ flattenRustUseAlias(node, prefix, source, imports);
172
+ return;
173
+ case "use_wildcard":
174
+ flattenRustUseWildcard(node, prefix, source, imports);
175
+ return;
176
+ case "self":
177
+ imports.push({ moduleSpecifier: prefix, range: nodeToRange(node, source) });
178
+ return;
179
+ default:
180
+ if (RUST_USE_PATH_TYPES.has(node.type)) {
181
+ imports.push({
182
+ moduleSpecifier: appendRustPath(prefix, node.text),
183
+ range: nodeToRange(node, source),
184
+ });
185
+ return;
186
+ }
187
+ for (const child of node.children) flattenRustUseTree(child, prefix, source, imports);
188
+ }
189
+ }
190
+
191
+ function flattenRustScopedUseList(
192
+ node: SyntaxNodeLike,
193
+ prefix: string,
194
+ source: string,
195
+ imports: ImportRecord[],
196
+ ): void {
197
+ const group = node.children.find((child) => child.type === "use_list");
198
+ const base = node.children.find((child) => child !== group && child.type !== "::");
199
+ if (group) flattenRustUseTree(group, appendRustPath(prefix, base?.text ?? ""), source, imports);
200
+ }
201
+
202
+ function flattenRustUseList(
203
+ node: SyntaxNodeLike,
204
+ prefix: string,
205
+ source: string,
206
+ imports: ImportRecord[],
207
+ ): void {
45
208
  for (const child of node.children) {
46
- walkForImports(child, source, imports);
209
+ if (["{", "}", ","].includes(child.type)) continue;
210
+ if (child.type === "self") {
211
+ imports.push({ moduleSpecifier: prefix, range: nodeToRange(child, source) });
212
+ } else {
213
+ flattenRustUseTree(child, prefix, source, imports);
214
+ }
215
+ }
216
+ }
217
+
218
+ function flattenRustUseAlias(
219
+ node: SyntaxNodeLike,
220
+ prefix: string,
221
+ source: string,
222
+ imports: ImportRecord[],
223
+ ): void {
224
+ const aliasIndex = node.children.findIndex((child) => child.type === "as");
225
+ const pathChildren = aliasIndex < 0 ? node.children : node.children.slice(0, aliasIndex);
226
+ const path = pathChildren.find((child) => child.type !== "::");
227
+ if (path) flattenRustUseTree(path, prefix, source, imports);
228
+ }
229
+
230
+ function flattenRustUseWildcard(
231
+ node: SyntaxNodeLike,
232
+ prefix: string,
233
+ source: string,
234
+ imports: ImportRecord[],
235
+ ): void {
236
+ const path = node.children.find((child) => child.type !== "::" && child.type !== "*");
237
+ const target = appendRustPath(prefix, path?.text ?? "");
238
+ imports.push({ moduleSpecifier: `${target}::*`, range: nodeToRange(node, source) });
239
+ }
240
+
241
+ function appendRustPath(prefix: string, suffix: string): string {
242
+ if (!prefix) return suffix;
243
+ if (!suffix) return prefix;
244
+ return `${prefix}::${suffix}`;
245
+ }
246
+
247
+ function walkForCIncludes(node: SyntaxNodeLike, source: string, imports: ImportRecord[]): void {
248
+ if (node.type === "preproc_include") {
249
+ const literal = node.children.find(
250
+ (child) => child.type === "system_lib_string" || child.type === "string_literal",
251
+ );
252
+ if (literal) {
253
+ imports.push({
254
+ moduleSpecifier: literalContent(literal.text),
255
+ range: nodeToRange(node, source),
256
+ });
257
+ }
258
+ return;
47
259
  }
260
+ for (const child of node.children) walkForCIncludes(child, source, imports);
48
261
  }
49
262
 
263
+ function literalContent(value: string): string {
264
+ return value.length >= 2 ? value.slice(1, -1) : "";
265
+ }
266
+
267
+ const RUST_USE_PATH_TYPES = new Set([
268
+ "identifier",
269
+ "crate",
270
+ "self",
271
+ "super",
272
+ "scoped_identifier",
273
+ "scoped_type_identifier",
274
+ ]);
275
+
50
276
  const LATEX_IMPORT_FIELDS: Readonly<Record<string, string>> = {
51
277
  package_include: "paths",
52
278
  class_include: "path",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
4
4
  import { nodeToRange, publicToTreeSitter, validatePublicPositionBounds } from "../coordinates.ts";
5
+ import { withSyntaxCompleteness } from "../syntax-errors.ts";
5
6
  import type { NodeAtResult, SourceRange, TreeSitterResult } from "../types.ts";
6
7
  import type { TreeSitterRuntime } from "../worker/runtime.ts";
7
8
 
@@ -38,15 +39,18 @@ export async function lookupNodeAt(
38
39
  return { kind: "runtime-error", message: "No node found at the given position" };
39
40
  }
40
41
 
41
- return {
42
- kind: "success",
42
+ return withSyntaxCompleteness({
43
43
  data: {
44
44
  type: node.type,
45
45
  range: nodeToRange(node, source),
46
46
  text: node.text,
47
47
  ancestry: collectAncestry(node, source),
48
48
  },
49
- };
49
+ file: filePath,
50
+ root: tree.rootNode,
51
+ source,
52
+ grammarId: parseResult.data.grammarId,
53
+ });
50
54
  } finally {
51
55
  tree.delete();
52
56
  }
@@ -0,0 +1,78 @@
1
+ // Declaration wrapper handling for nested outline expansion.
2
+
3
+ import { nodeToRange } from "../coordinates.ts";
4
+ import type { SyntaxNodeLike } from "../syntax-node.ts";
5
+ import type { GrammarId, OutlineItem } from "../types.ts";
6
+ import { expandNestedItems } from "./outline-nested.ts";
7
+
8
+ /** Node types that can be extracted directly as outline items. */
9
+ export const OUTLINE_DECLARATION_NODE_TYPES = new Set([
10
+ "function_declaration",
11
+ "generator_function_declaration",
12
+ "class_declaration",
13
+ "abstract_class_declaration",
14
+ "class",
15
+ "interface_declaration",
16
+ "type_alias_declaration",
17
+ "enum_declaration",
18
+ "method_definition",
19
+ "public_field_definition",
20
+ "variable_declarator",
21
+ "lexical_declaration",
22
+ "ambient_declaration",
23
+ "internal_module",
24
+ "module",
25
+ "function_signature",
26
+ ]);
27
+
28
+ /** Expand extracted declarations using the syntax node that owns their scope. */
29
+ export function expandExtractedItems(options: {
30
+ readonly items: OutlineItem[];
31
+ readonly node: SyntaxNodeLike;
32
+ readonly source: string;
33
+ readonly lineStarts: readonly number[];
34
+ readonly grammarId?: GrammarId;
35
+ readonly extractItemsForNode: (node: SyntaxNodeLike) => OutlineItem[] | null;
36
+ }): OutlineItem[] {
37
+ const { items, node, source } = options;
38
+ if (node.type === "export_statement" || node.type === "ambient_declaration") {
39
+ const declaration = node.children.find((child) =>
40
+ OUTLINE_DECLARATION_NODE_TYPES.has(child.type),
41
+ );
42
+ if (declaration) return expandExtractedItems({ ...options, node: declaration });
43
+
44
+ if (node.type === "export_statement") {
45
+ const expression = node.children.find((child) =>
46
+ [
47
+ "arrow_function",
48
+ "function_expression",
49
+ "generator_function",
50
+ "class_expression",
51
+ ].includes(child.type),
52
+ );
53
+ if (expression) return expandExtractedItems({ ...options, node: expression });
54
+ }
55
+ }
56
+
57
+ if (node.type === "lexical_declaration") {
58
+ const declarators = node.children.filter((child) => child.type === "variable_declarator");
59
+ if (declarators.length > 0) {
60
+ return declarators.flatMap((declarator) => {
61
+ const range = nodeToRange(declarator, source);
62
+ const declarationItems = items.filter((item) => sameRange(item.range, range));
63
+ return expandExtractedItems({ ...options, items: declarationItems, node: declarator });
64
+ });
65
+ }
66
+ }
67
+
68
+ return expandNestedItems(options);
69
+ }
70
+
71
+ function sameRange(left: OutlineItem["range"], right: OutlineItem["range"]): boolean {
72
+ return (
73
+ left.startLine === right.startLine &&
74
+ left.startCharacter === right.startCharacter &&
75
+ left.endLine === right.endLine &&
76
+ left.endCharacter === right.endCharacter
77
+ );
78
+ }