@mrclrchtr/supi-tree-sitter 0.1.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 +97 -0
- package/package.json +67 -0
- package/resources/.gitkeep +0 -0
- package/resources/grammars/bash/tree-sitter-bash.wasm +0 -0
- package/resources/grammars/bash/tree-sitter-bash.wasm.json +7 -0
- package/resources/grammars/c/tree-sitter-c.wasm +0 -0
- package/resources/grammars/c/tree-sitter-c.wasm.json +7 -0
- package/resources/grammars/cpp/tree-sitter-cpp.wasm +0 -0
- package/resources/grammars/cpp/tree-sitter-cpp.wasm.json +7 -0
- package/resources/grammars/go/tree-sitter-go.wasm +0 -0
- package/resources/grammars/go/tree-sitter-go.wasm.json +7 -0
- package/resources/grammars/html/tree-sitter-html.wasm +0 -0
- package/resources/grammars/html/tree-sitter-html.wasm.json +7 -0
- package/resources/grammars/java/tree-sitter-java.wasm +0 -0
- package/resources/grammars/java/tree-sitter-java.wasm.json +7 -0
- package/resources/grammars/javascript/tree-sitter-javascript.wasm +0 -0
- package/resources/grammars/javascript/tree-sitter-javascript.wasm.json +7 -0
- package/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
- package/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +12 -0
- package/resources/grammars/python/tree-sitter-python.wasm +0 -0
- package/resources/grammars/python/tree-sitter-python.wasm.json +7 -0
- package/resources/grammars/r/tree-sitter-r.wasm +0 -0
- package/resources/grammars/r/tree-sitter-r.wasm.json +7 -0
- package/resources/grammars/ruby/tree-sitter-ruby.wasm +0 -0
- package/resources/grammars/ruby/tree-sitter-ruby.wasm.json +7 -0
- package/resources/grammars/rust/tree-sitter-rust.wasm +0 -0
- package/resources/grammars/rust/tree-sitter-rust.wasm.json +7 -0
- package/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
- package/resources/grammars/sql/tree-sitter-sql.wasm.json +19 -0
- package/resources/grammars/tsx/tree-sitter-tsx.wasm +0 -0
- package/resources/grammars/tsx/tree-sitter-tsx.wasm.json +7 -0
- package/resources/grammars/typescript/tree-sitter-typescript.wasm +0 -0
- package/resources/grammars/typescript/tree-sitter-typescript.wasm.json +7 -0
- package/scripts/generate-kotlin-wasm.mjs +126 -0
- package/scripts/generate-sql-wasm.mjs +144 -0
- package/scripts/vendor-wasm.mjs +151 -0
- package/src/callees.ts +343 -0
- package/src/coordinates.ts +108 -0
- package/src/exports.ts +315 -0
- package/src/formatting.ts +104 -0
- package/src/imports.ts +42 -0
- package/src/index.ts +16 -0
- package/src/language.ts +116 -0
- package/src/node-at.ts +96 -0
- package/src/outline.ts +287 -0
- package/src/runtime.ts +237 -0
- package/src/session.ts +112 -0
- package/src/structure.ts +7 -0
- package/src/syntax-node.ts +13 -0
- package/src/tree-sitter.ts +306 -0
- package/src/types.ts +146 -0
package/src/outline.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Outline extraction for supported files.
|
|
2
|
+
|
|
3
|
+
import { nodeToRange } from "./coordinates.ts";
|
|
4
|
+
import type { SyntaxNodeLike } from "./syntax-node.ts";
|
|
5
|
+
import type { OutlineItem } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/** Node types that can be extracted directly as outline items. */
|
|
8
|
+
const OUTLINE_DECLARATION_NODE_TYPES = new Set([
|
|
9
|
+
"function_declaration",
|
|
10
|
+
"generator_function_declaration",
|
|
11
|
+
"class_declaration",
|
|
12
|
+
"abstract_class_declaration",
|
|
13
|
+
"class",
|
|
14
|
+
"interface_declaration",
|
|
15
|
+
"type_alias_declaration",
|
|
16
|
+
"enum_declaration",
|
|
17
|
+
"method_definition",
|
|
18
|
+
"public_field_definition",
|
|
19
|
+
"variable_declarator",
|
|
20
|
+
"ambient_declaration",
|
|
21
|
+
"internal_module",
|
|
22
|
+
"module",
|
|
23
|
+
"function_signature",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Extract a structural outline from a parsed tree. */
|
|
27
|
+
export function collectOutline(rootNode: SyntaxNodeLike, source: string): OutlineItem[] {
|
|
28
|
+
return collectItems(rootNode, source);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function collectItems(node: SyntaxNodeLike, source: string): OutlineItem[] {
|
|
32
|
+
const items: OutlineItem[] = [];
|
|
33
|
+
|
|
34
|
+
for (const child of node.children) {
|
|
35
|
+
const item = extractItem(child, source);
|
|
36
|
+
if (item) {
|
|
37
|
+
items.push(item);
|
|
38
|
+
} else {
|
|
39
|
+
items.push(...collectItems(child, source));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return items;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractItem(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
47
|
+
switch (node.type) {
|
|
48
|
+
case "export_statement":
|
|
49
|
+
return extractExportStatement(node, source);
|
|
50
|
+
case "lexical_declaration":
|
|
51
|
+
return extractLexicalDeclaration(node, source);
|
|
52
|
+
case "function_declaration":
|
|
53
|
+
case "generator_function_declaration":
|
|
54
|
+
case "function_signature":
|
|
55
|
+
return extractNamedDeclaration(node, "function", source);
|
|
56
|
+
case "class_declaration":
|
|
57
|
+
case "abstract_class_declaration":
|
|
58
|
+
case "class":
|
|
59
|
+
return extractClassDeclaration(node, source);
|
|
60
|
+
case "interface_declaration":
|
|
61
|
+
return extractInterfaceDeclaration(node, source);
|
|
62
|
+
case "type_alias_declaration":
|
|
63
|
+
return extractNamedDeclaration(node, "type", source);
|
|
64
|
+
case "enum_declaration":
|
|
65
|
+
return extractEnumDeclaration(node, source);
|
|
66
|
+
case "method_definition":
|
|
67
|
+
return extractNamedDeclaration(node, "method", source);
|
|
68
|
+
case "public_field_definition":
|
|
69
|
+
return extractFieldDefinition(node, source);
|
|
70
|
+
case "variable_declarator":
|
|
71
|
+
return extractVariableDeclarator(node, source);
|
|
72
|
+
case "ambient_declaration":
|
|
73
|
+
return extractAmbientDeclaration(node, source);
|
|
74
|
+
case "internal_module":
|
|
75
|
+
case "module":
|
|
76
|
+
return extractModuleDeclaration(node, source);
|
|
77
|
+
default:
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Extract an outline item from an export wrapper without exposing non-extractable syntax nodes. */
|
|
83
|
+
function extractExportStatement(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
84
|
+
const decl = node.children.find((child) => OUTLINE_DECLARATION_NODE_TYPES.has(child.type));
|
|
85
|
+
if (decl) {
|
|
86
|
+
const item = extractItem(decl, source);
|
|
87
|
+
if (item) return item;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (hasDefaultKeyword(node)) {
|
|
91
|
+
return {
|
|
92
|
+
name: "default",
|
|
93
|
+
kind: "export",
|
|
94
|
+
range: nodeToRange(node, source),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const exportClause = node.children.find((child) => child.type === "export_clause");
|
|
99
|
+
if (exportClause) {
|
|
100
|
+
return {
|
|
101
|
+
name: node.text.replace(/^export\s+/, "").substring(0, 60),
|
|
102
|
+
kind: "export",
|
|
103
|
+
range: nodeToRange(node, source),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractLexicalDeclaration(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
111
|
+
const declarators = node.children.filter((child) => child.type === "variable_declarator");
|
|
112
|
+
if (declarators.length !== 1) return null;
|
|
113
|
+
return extractVariableDeclarator(declarators[0] as SyntaxNodeLike, source);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractNamedDeclaration(
|
|
117
|
+
node: SyntaxNodeLike,
|
|
118
|
+
kind: string,
|
|
119
|
+
source: string,
|
|
120
|
+
): OutlineItem | null {
|
|
121
|
+
const nameNode = findNameNode(node);
|
|
122
|
+
if (!nameNode) return null;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
name: nameNode.text,
|
|
126
|
+
kind,
|
|
127
|
+
range: nodeToRange(node, source),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractClassDeclaration(node: SyntaxNodeLike, source: string): OutlineItem {
|
|
132
|
+
const nameNode = findNameNode(node);
|
|
133
|
+
return {
|
|
134
|
+
name: nameNode ? nameNode.text : "<anonymous>",
|
|
135
|
+
kind: "class",
|
|
136
|
+
range: nodeToRange(node, source),
|
|
137
|
+
children: collectClassMembers(node, source),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractInterfaceDeclaration(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
142
|
+
const item = extractNamedDeclaration(node, "interface", source);
|
|
143
|
+
if (!item) return null;
|
|
144
|
+
return { ...item, children: collectInterfaceMembers(node, source) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function extractEnumDeclaration(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
148
|
+
const item = extractNamedDeclaration(node, "enum", source);
|
|
149
|
+
if (!item) return null;
|
|
150
|
+
return { ...item, children: collectEnumMembers(node, source) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function extractVariableDeclarator(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
154
|
+
const nameNode = findNameNode(node);
|
|
155
|
+
if (!nameNode) return null;
|
|
156
|
+
return {
|
|
157
|
+
name: nameNode.text,
|
|
158
|
+
kind: detectKind(node),
|
|
159
|
+
range: nodeToRange(node, source),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function extractFieldDefinition(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
164
|
+
const nameNode = findNameNode(node);
|
|
165
|
+
if (!nameNode) return null;
|
|
166
|
+
return {
|
|
167
|
+
name: nameNode.text,
|
|
168
|
+
kind: isFunctionLike(node.childForFieldName("value")) ? "field-function" : "field",
|
|
169
|
+
range: nodeToRange(node, source),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractAmbientDeclaration(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
174
|
+
const declaration = node.children.find((child) => OUTLINE_DECLARATION_NODE_TYPES.has(child.type));
|
|
175
|
+
return declaration ? extractItem(declaration, source) : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function extractModuleDeclaration(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
179
|
+
const name = getModuleName(node);
|
|
180
|
+
if (!name) return null;
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
name,
|
|
184
|
+
kind: "namespace",
|
|
185
|
+
range: nodeToRange(node, source),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function detectKind(node: SyntaxNodeLike): string {
|
|
190
|
+
const valueNode = node.childForFieldName("value");
|
|
191
|
+
if (!valueNode) return "variable";
|
|
192
|
+
if (isFunctionLike(valueNode)) return "function";
|
|
193
|
+
if (valueNode.type === "class" || valueNode.type === "class_expression") return "class";
|
|
194
|
+
return "variable";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Collect supported class members without descending into method implementation bodies. */
|
|
198
|
+
function collectClassMembers(node: SyntaxNodeLike, source: string): OutlineItem[] {
|
|
199
|
+
const body = node.childForFieldName("body");
|
|
200
|
+
if (!body) return [];
|
|
201
|
+
|
|
202
|
+
const items: OutlineItem[] = [];
|
|
203
|
+
for (const child of body.children) {
|
|
204
|
+
if (child.type !== "method_definition" && child.type !== "public_field_definition") continue;
|
|
205
|
+
const item = extractItem(child, source);
|
|
206
|
+
if (item) items.push(item);
|
|
207
|
+
}
|
|
208
|
+
return items;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Collect interface signatures as shallow member outline items. */
|
|
212
|
+
function collectInterfaceMembers(node: SyntaxNodeLike, source: string): OutlineItem[] {
|
|
213
|
+
const body = node.childForFieldName("body");
|
|
214
|
+
if (!body) return [];
|
|
215
|
+
|
|
216
|
+
const items: OutlineItem[] = [];
|
|
217
|
+
for (const child of body.children) {
|
|
218
|
+
const item = extractInterfaceMember(child, source);
|
|
219
|
+
if (item) items.push(item);
|
|
220
|
+
}
|
|
221
|
+
return items;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function extractInterfaceMember(node: SyntaxNodeLike, source: string): OutlineItem | null {
|
|
225
|
+
if (node.type === "method_signature") return memberItem(node, "method", source);
|
|
226
|
+
if (node.type === "property_signature") return memberItem(node, "property", source);
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Collect enum members from both bare identifiers and assigned members. */
|
|
231
|
+
function collectEnumMembers(node: SyntaxNodeLike, source: string): OutlineItem[] {
|
|
232
|
+
const body = node.childForFieldName("body");
|
|
233
|
+
if (!body) return [];
|
|
234
|
+
|
|
235
|
+
const items: OutlineItem[] = [];
|
|
236
|
+
for (const child of body.children) {
|
|
237
|
+
if (child.type !== "property_identifier" && child.type !== "enum_assignment") continue;
|
|
238
|
+
const item = memberItem(child, "enum-member", source);
|
|
239
|
+
if (item) items.push(item);
|
|
240
|
+
}
|
|
241
|
+
return items;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function memberItem(node: SyntaxNodeLike, kind: string, source: string): OutlineItem | null {
|
|
245
|
+
const nameNode = findNameNode(node);
|
|
246
|
+
if (!nameNode) return null;
|
|
247
|
+
return {
|
|
248
|
+
name: nameNode.text,
|
|
249
|
+
kind,
|
|
250
|
+
range: nodeToRange(node, source),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function findNameNode(node: SyntaxNodeLike): SyntaxNodeLike | null {
|
|
255
|
+
return (
|
|
256
|
+
node.childForFieldName("name") ??
|
|
257
|
+
node.children.find((child) =>
|
|
258
|
+
[
|
|
259
|
+
"identifier",
|
|
260
|
+
"type_identifier",
|
|
261
|
+
"property_identifier",
|
|
262
|
+
"private_property_identifier",
|
|
263
|
+
].includes(child.type),
|
|
264
|
+
) ??
|
|
265
|
+
null
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function hasDefaultKeyword(node: SyntaxNodeLike): boolean {
|
|
270
|
+
return node.children.some((child) => child.type === "default");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getModuleName(node: SyntaxNodeLike): string | null {
|
|
274
|
+
const nameNode =
|
|
275
|
+
node.childForFieldName("name") ?? node.children.find((child) => child.type === "string");
|
|
276
|
+
if (!nameNode) return null;
|
|
277
|
+
return nameNode.type === "string" ? stripQuotes(nameNode.text) : nameNode.text;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function stripQuotes(text: string): string {
|
|
281
|
+
return text.replace(/^["']|["']$/g, "");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function isFunctionLike(node: SyntaxNodeLike | null): boolean {
|
|
285
|
+
if (!node) return false;
|
|
286
|
+
return ["arrow_function", "function_expression", "generator_function"].includes(node.type);
|
|
287
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Tree-sitter runtime — parser management, parse, and query services.
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import type { Language, Parser, Tree } from "web-tree-sitter";
|
|
6
|
+
import { nodeToRange } from "./coordinates.ts";
|
|
7
|
+
import { detectGrammar, resolveGrammarWasmPath } from "./language.ts";
|
|
8
|
+
import type { GrammarId, QueryCapture, TreeSitterResult } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
interface ParserEntry {
|
|
11
|
+
parser: Parser;
|
|
12
|
+
language: Language;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Session-scoped Tree-sitter runtime.
|
|
17
|
+
*
|
|
18
|
+
* A runtime owns the expensive `web-tree-sitter` initialization and parser
|
|
19
|
+
* instances for one pi working directory. Call `dispose()` when the session is
|
|
20
|
+
* torn down so WASM parser resources are released.
|
|
21
|
+
*/
|
|
22
|
+
export class TreeSitterRuntime {
|
|
23
|
+
private parserModule: typeof import("web-tree-sitter") | undefined;
|
|
24
|
+
private parsers = new Map<GrammarId, ParserEntry>();
|
|
25
|
+
private parserPromises = new Map<GrammarId, Promise<ParserEntry>>();
|
|
26
|
+
private initPromise: Promise<typeof import("web-tree-sitter")> | undefined;
|
|
27
|
+
private initializing = false;
|
|
28
|
+
private disposed = false;
|
|
29
|
+
|
|
30
|
+
/** Create a runtime that resolves relative file paths from `cwd`. */
|
|
31
|
+
constructor(private cwd: string) {}
|
|
32
|
+
|
|
33
|
+
/** Ensure web-tree-sitter Parser is initialized. */
|
|
34
|
+
private async ensureParserInit(): Promise<typeof import("web-tree-sitter")> {
|
|
35
|
+
this.assertActive();
|
|
36
|
+
if (this.parserModule) return this.parserModule;
|
|
37
|
+
if (this.initializing && this.initPromise) return this.initPromise;
|
|
38
|
+
|
|
39
|
+
this.initializing = true;
|
|
40
|
+
this.initPromise = (async () => {
|
|
41
|
+
const mod = await import("web-tree-sitter");
|
|
42
|
+
await mod.Parser.init();
|
|
43
|
+
this.assertActive();
|
|
44
|
+
this.parserModule = mod;
|
|
45
|
+
return mod;
|
|
46
|
+
})();
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
return await this.initPromise;
|
|
50
|
+
} catch (err: unknown) {
|
|
51
|
+
// Allow retry on next call
|
|
52
|
+
this.initPromise = undefined;
|
|
53
|
+
this.initializing = false;
|
|
54
|
+
throw new Error("Failed to initialize web-tree-sitter", { cause: err });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get or create a parser entry for a grammar.
|
|
60
|
+
*
|
|
61
|
+
* Concurrent first-use calls for the same grammar share one initialization
|
|
62
|
+
* promise. Failed initialization is not cached, so a later request can retry.
|
|
63
|
+
*/
|
|
64
|
+
async ensureGrammarParser(grammarId: GrammarId): Promise<ParserEntry> {
|
|
65
|
+
this.assertActive();
|
|
66
|
+
const existing = this.parsers.get(grammarId);
|
|
67
|
+
if (existing) return existing;
|
|
68
|
+
|
|
69
|
+
const pending = this.parserPromises.get(grammarId);
|
|
70
|
+
if (pending) return pending;
|
|
71
|
+
|
|
72
|
+
const promise = this.createGrammarParser(grammarId);
|
|
73
|
+
this.parserPromises.set(grammarId, promise);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
return await promise;
|
|
77
|
+
} finally {
|
|
78
|
+
if (this.parserPromises.get(grammarId) === promise) {
|
|
79
|
+
this.parserPromises.delete(grammarId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async createGrammarParser(grammarId: GrammarId): Promise<ParserEntry> {
|
|
85
|
+
const mod = await this.ensureParserInit();
|
|
86
|
+
const wasmPath = resolveGrammarWasmPath(grammarId);
|
|
87
|
+
|
|
88
|
+
const language = await mod.Language.load(wasmPath);
|
|
89
|
+
const parser = new mod.Parser();
|
|
90
|
+
try {
|
|
91
|
+
parser.setLanguage(language);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
parser.delete();
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.disposed) {
|
|
98
|
+
parser.delete();
|
|
99
|
+
throw new Error("Tree-sitter runtime has been disposed");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const entry = { parser, language };
|
|
103
|
+
this.parsers.set(grammarId, entry);
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Read and parse a file. Returns structured result. */
|
|
108
|
+
async parseFile(filePath: string): Promise<
|
|
109
|
+
TreeSitterResult<{
|
|
110
|
+
tree: Tree;
|
|
111
|
+
source: string;
|
|
112
|
+
resolvedPath: string;
|
|
113
|
+
grammarId: GrammarId;
|
|
114
|
+
}>
|
|
115
|
+
> {
|
|
116
|
+
const resolvedPath = path.resolve(this.cwd, filePath);
|
|
117
|
+
|
|
118
|
+
// Check language support first
|
|
119
|
+
const grammarId = detectGrammar(filePath);
|
|
120
|
+
if (!grammarId) {
|
|
121
|
+
return {
|
|
122
|
+
kind: "unsupported-language",
|
|
123
|
+
file: filePath,
|
|
124
|
+
message: `No Tree-sitter grammar configured for ${path.extname(filePath) || "this file type"}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Read the file
|
|
129
|
+
let source: string;
|
|
130
|
+
try {
|
|
131
|
+
source = fs.readFileSync(resolvedPath, "utf-8");
|
|
132
|
+
} catch (err: unknown) {
|
|
133
|
+
const message = err instanceof Error ? err.message : "File could not be read";
|
|
134
|
+
return {
|
|
135
|
+
kind: "file-access-error",
|
|
136
|
+
file: filePath,
|
|
137
|
+
message,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const entry = await this.ensureGrammarParser(grammarId);
|
|
143
|
+
const tree = entry.parser.parse(source);
|
|
144
|
+
return {
|
|
145
|
+
kind: "success",
|
|
146
|
+
data: { tree: tree as Tree, source, resolvedPath, grammarId },
|
|
147
|
+
};
|
|
148
|
+
} catch (err: unknown) {
|
|
149
|
+
return { kind: "runtime-error", message: formatError(err, "Parser initialization failed") };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Execute a Tree-sitter query against a file. */
|
|
154
|
+
async queryFile(
|
|
155
|
+
filePath: string,
|
|
156
|
+
queryString: string,
|
|
157
|
+
): Promise<TreeSitterResult<QueryCapture[]>> {
|
|
158
|
+
if (!queryString || queryString.trim().length === 0) {
|
|
159
|
+
return { kind: "validation-error", message: "query is required and must be non-empty" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const parseResult = await this.parseFile(filePath);
|
|
163
|
+
if (parseResult.kind !== "success") return parseResult;
|
|
164
|
+
|
|
165
|
+
const { tree, source } = parseResult.data;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const entry = await this.ensureGrammarParser(parseResult.data.grammarId);
|
|
169
|
+
const mod = await this.ensureParserInit();
|
|
170
|
+
let query: InstanceType<typeof mod.Query>;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
query = new mod.Query(entry.language, queryString);
|
|
174
|
+
} catch (err: unknown) {
|
|
175
|
+
return { kind: "validation-error", message: `Invalid query: ${formatError(err)}` };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const matches = query.matches(tree.rootNode);
|
|
180
|
+
const captures: QueryCapture[] = [];
|
|
181
|
+
for (const match of matches) {
|
|
182
|
+
for (const { name, node } of match.captures) {
|
|
183
|
+
captures.push({
|
|
184
|
+
name,
|
|
185
|
+
nodeType: node.type,
|
|
186
|
+
range: nodeToRange(node, source),
|
|
187
|
+
text: node.text,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { kind: "success", data: captures };
|
|
192
|
+
} finally {
|
|
193
|
+
query.delete();
|
|
194
|
+
}
|
|
195
|
+
} catch (err: unknown) {
|
|
196
|
+
return { kind: "runtime-error", message: formatError(err, "Query execution failed") };
|
|
197
|
+
} finally {
|
|
198
|
+
tree.delete();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Get the grammar ID for a file, or undefined if unsupported. */
|
|
203
|
+
getGrammarId(filePath: string): GrammarId | undefined {
|
|
204
|
+
return detectGrammar(filePath);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Resolve a file path from cwd. */
|
|
208
|
+
resolvePath(filePath: string): string {
|
|
209
|
+
return path.resolve(this.cwd, filePath);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Dispose all held parser resources. */
|
|
213
|
+
dispose(): void {
|
|
214
|
+
this.disposed = true;
|
|
215
|
+
for (const [, entry] of this.parsers) {
|
|
216
|
+
entry.parser.delete();
|
|
217
|
+
}
|
|
218
|
+
this.parsers.clear();
|
|
219
|
+
this.parserPromises.clear();
|
|
220
|
+
this.parserModule = undefined;
|
|
221
|
+
this.initPromise = undefined;
|
|
222
|
+
this.initializing = false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private assertActive(): void {
|
|
226
|
+
if (this.disposed) {
|
|
227
|
+
throw new Error("Tree-sitter runtime has been disposed");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Format errors with their cause chain's first message for user-facing tool output. */
|
|
233
|
+
function formatError(err: unknown, fallback = "Operation failed"): string {
|
|
234
|
+
if (!(err instanceof Error)) return String(err || fallback);
|
|
235
|
+
if (err.cause instanceof Error) return `${err.message}: ${err.cause.message}`;
|
|
236
|
+
return err.message || fallback;
|
|
237
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Session factory — creates a TreeSitterSession bound to a working directory.
|
|
2
|
+
|
|
3
|
+
import { detectGrammar, isJsTsGrammar } from "./language.ts";
|
|
4
|
+
import { TreeSitterRuntime } from "./runtime.ts";
|
|
5
|
+
import {
|
|
6
|
+
extractExports,
|
|
7
|
+
extractImports,
|
|
8
|
+
extractOutline,
|
|
9
|
+
lookupCalleesAt,
|
|
10
|
+
lookupNodeAt,
|
|
11
|
+
} from "./structure.ts";
|
|
12
|
+
import type {
|
|
13
|
+
CalleesAtResult,
|
|
14
|
+
ExportRecord,
|
|
15
|
+
ImportRecord,
|
|
16
|
+
NodeAtResult,
|
|
17
|
+
OutlineItem,
|
|
18
|
+
QueryCapture,
|
|
19
|
+
TreeSitterResult,
|
|
20
|
+
TreeSitterSession,
|
|
21
|
+
} from "./types.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a new Tree-sitter session bound to the given working directory.
|
|
25
|
+
* The session owns parser/grammar reuse and must be disposed when done.
|
|
26
|
+
*/
|
|
27
|
+
export function createTreeSitterSession(cwd: string): TreeSitterSession {
|
|
28
|
+
const runtime = new TreeSitterRuntime(cwd);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
async canParse(file: string) {
|
|
32
|
+
const result = await runtime.parseFile(file);
|
|
33
|
+
if (result.kind !== "success") return result;
|
|
34
|
+
const { resolvedPath, grammarId, tree } = result.data;
|
|
35
|
+
try {
|
|
36
|
+
return {
|
|
37
|
+
kind: "success",
|
|
38
|
+
data: { file: resolvedPath, language: grammarId },
|
|
39
|
+
};
|
|
40
|
+
} finally {
|
|
41
|
+
tree.delete();
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async query(file: string, queryString: string): Promise<TreeSitterResult<QueryCapture[]>> {
|
|
46
|
+
return runtime.queryFile(file, queryString);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async outline(file: string): Promise<TreeSitterResult<OutlineItem[]>> {
|
|
50
|
+
const parseResult = await runtime.parseFile(file);
|
|
51
|
+
if (parseResult.kind !== "success") return parseResult;
|
|
52
|
+
if (!isJsTsGrammar(parseResult.data.grammarId)) {
|
|
53
|
+
return {
|
|
54
|
+
kind: "unsupported-language",
|
|
55
|
+
file,
|
|
56
|
+
message: `outline is not supported for ${parseResult.data.grammarId} files`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const { tree, source } = parseResult.data;
|
|
60
|
+
try {
|
|
61
|
+
const items = extractOutline(tree.rootNode, source);
|
|
62
|
+
return { kind: "success", data: items };
|
|
63
|
+
} finally {
|
|
64
|
+
tree.delete();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async imports(file: string): Promise<TreeSitterResult<ImportRecord[]>> {
|
|
69
|
+
const grammarId = detectGrammar(file);
|
|
70
|
+
if (grammarId && !isJsTsGrammar(grammarId)) {
|
|
71
|
+
return {
|
|
72
|
+
kind: "unsupported-language",
|
|
73
|
+
file,
|
|
74
|
+
message: `imports is not supported for ${grammarId} files`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return extractImports(runtime, file);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
async exports(file: string): Promise<TreeSitterResult<ExportRecord[]>> {
|
|
81
|
+
const grammarId = detectGrammar(file);
|
|
82
|
+
if (grammarId && !isJsTsGrammar(grammarId)) {
|
|
83
|
+
return {
|
|
84
|
+
kind: "unsupported-language",
|
|
85
|
+
file,
|
|
86
|
+
message: `exports is not supported for ${grammarId} files`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return extractExports(runtime, file);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async nodeAt(
|
|
93
|
+
file: string,
|
|
94
|
+
line: number,
|
|
95
|
+
character: number,
|
|
96
|
+
): Promise<TreeSitterResult<NodeAtResult>> {
|
|
97
|
+
return lookupNodeAt(runtime, file, line, character);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
async calleesAt(
|
|
101
|
+
file: string,
|
|
102
|
+
line: number,
|
|
103
|
+
character: number,
|
|
104
|
+
): Promise<TreeSitterResult<CalleesAtResult>> {
|
|
105
|
+
return lookupCalleesAt(runtime, file, line, character);
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
dispose() {
|
|
109
|
+
runtime.dispose();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/structure.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Structural extraction services — re-exports from sub-modules.
|
|
2
|
+
|
|
3
|
+
export { lookupCalleesAt } from "./callees.ts";
|
|
4
|
+
export { extractExports } from "./exports.ts";
|
|
5
|
+
export { extractImports } from "./imports.ts";
|
|
6
|
+
export { lookupNodeAt } from "./node-at.ts";
|
|
7
|
+
export { collectOutline as extractOutline } from "./outline.ts";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Shared type alias for tree-sitter syntax nodes used in structure extraction.
|
|
2
|
+
// We use a minimal interface instead of importing from web-tree-sitter directly
|
|
3
|
+
// to avoid coupling to the runtime module.
|
|
4
|
+
|
|
5
|
+
export interface SyntaxNodeLike {
|
|
6
|
+
type: string;
|
|
7
|
+
text: string;
|
|
8
|
+
children: SyntaxNodeLike[];
|
|
9
|
+
parent: SyntaxNodeLike | null;
|
|
10
|
+
startPosition: { row: number; column: number };
|
|
11
|
+
endPosition: { row: number; column: number };
|
|
12
|
+
childForFieldName(name: string): SyntaxNodeLike | null;
|
|
13
|
+
}
|