@mrclrchtr/supi-tree-sitter 10.0.0 → 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.
- package/README.md +63 -17
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/LICENSE +21 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/README.md +743 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/index.js +17 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/constants.js +184 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/parse.js +1425 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/picomatch.js +361 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/scan.js +403 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/utils.js +72 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/package.json +83 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/posix.js +3 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +4 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +11 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/file-associations.ts +190 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +22 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/package.json +3 -3
- package/src/language.ts +16 -10
- package/src/operation-support.ts +13 -6
- package/src/provider/tree-sitter-provider.ts +19 -1
- package/src/session/runtime-controller.ts +18 -4
- package/src/session/session.ts +10 -3
- package/src/session/structural-worker-client.ts +17 -2
- package/src/session/structural-worker-process.ts +22 -5
- package/src/session/structural-worker-protocol.ts +36 -1
- package/src/syntax-errors.ts +73 -0
- package/src/syntax-node.ts +4 -0
- package/src/tool/call-sites.ts +15 -9
- package/src/tool/callees.ts +24 -15
- package/src/tool/exports.ts +8 -1
- package/src/tool/imports.ts +233 -7
- package/src/tool/node-at.ts +7 -3
- package/src/tool/outline-expansion.ts +78 -0
- package/src/tool/outline-jvm.ts +25 -4
- package/src/tool/outline-nested.ts +390 -0
- package/src/tool/outline.ts +49 -28
- package/src/types.ts +16 -0
- package/src/worker/process-host.mjs +61 -24
- package/src/worker/runtime.ts +44 -10
- package/src/worker/service.ts +13 -9
- 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
|
+
]);
|
package/src/syntax-node.ts
CHANGED
|
@@ -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;
|
package/src/tool/call-sites.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
//
|
|
84
|
-
const key = `${
|
|
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({
|
|
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
|
|
95
|
+
return queryResult.kind === "partial"
|
|
96
|
+
? { ...queryResult, data: matches }
|
|
97
|
+
: { kind: "success", data: matches };
|
|
92
98
|
}
|
package/src/tool/callees.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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"
|
|
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(
|
|
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
|
}
|
package/src/tool/exports.ts
CHANGED
|
@@ -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 {
|
|
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
|
}
|
package/src/tool/imports.ts
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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",
|
package/src/tool/node-at.ts
CHANGED
|
@@ -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
|
+
}
|