@mrclrchtr/supi-tree-sitter 4.1.0 → 4.3.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.
@@ -0,0 +1,307 @@
1
+ import { nodeToRange } from "../coordinates.ts";
2
+ import type { SyntaxNodeLike } from "../syntax-node.ts";
3
+ import type { OutlineItem } from "../types.ts";
4
+
5
+ /** Extract Ruby, shell, and R declarations from their grammar-specific node shapes. */
6
+ export function extractScriptingOutlineItems(
7
+ node: SyntaxNodeLike,
8
+ source: string,
9
+ ): OutlineItem[] | undefined {
10
+ const ruby = rubyItems(node, "top-level", source);
11
+ if (ruby) return ruby;
12
+
13
+ switch (node.type) {
14
+ case "variable_assignment":
15
+ return isNestedShellDeclaration(node) || node.parent?.type === "command"
16
+ ? []
17
+ : one(shellVariable(node, "variable", source));
18
+ case "declaration_command":
19
+ return isNestedShellDeclaration(node) ? [] : shellDeclarations(node, source);
20
+ case "function_definition": {
21
+ const nameType = node.childForFieldName("name")?.type;
22
+ if (nameType === "word") {
23
+ return isNestedShellDeclaration(node) ? [] : one(named(node, "function", source));
24
+ }
25
+ return nameType === "function" || nameType === "\\" ? [] : undefined;
26
+ }
27
+ case "binary_operator":
28
+ return rAssignments(node, source);
29
+ default:
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ function rubyItems(
35
+ node: SyntaxNodeLike,
36
+ context: "top-level" | "member",
37
+ source: string,
38
+ ): OutlineItem[] | undefined {
39
+ switch (node.type) {
40
+ case "assignment":
41
+ case "operator_assignment":
42
+ return rubyAssignments(node, context, source);
43
+ case "module":
44
+ return isRubyContainer(node) ? one(rubyContainer(node, "module", source)) : undefined;
45
+ case "class":
46
+ return isRubyContainer(node) ? one(rubyContainer(node, "class", source)) : undefined;
47
+ case "method":
48
+ return one(named(node, context === "member" ? "method" : "function", source));
49
+ case "singleton_method":
50
+ return one(named(node, "method", source));
51
+ case "singleton_class":
52
+ return rubyMembers(node.childForFieldName("body"), source);
53
+ case "alias":
54
+ return one(named(node, context === "member" ? "method" : "function", source));
55
+ case "call":
56
+ return context === "member" ? rubyCallMethods(node, source) : undefined;
57
+ case "block":
58
+ return node.childForFieldName("body")?.type === "block_body" ? [] : undefined;
59
+ case "do_block":
60
+ return node.childForFieldName("body")?.type === "body_statement" ? [] : undefined;
61
+ default:
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ function isRubyContainer(node: SyntaxNodeLike): boolean {
67
+ const name = node.childForFieldName("name");
68
+ const body = node.childForFieldName("body");
69
+ return Boolean(
70
+ name && RUBY_CONTAINER_NAME_TYPES.has(name.type) && (!body || body.type === "body_statement"),
71
+ );
72
+ }
73
+
74
+ function rubyContainer(node: SyntaxNodeLike, kind: string, source: string): OutlineItem | null {
75
+ const name = node.childForFieldName("name");
76
+ if (!name) return null;
77
+ return {
78
+ name: name.text,
79
+ kind,
80
+ range: nodeToRange(node, source),
81
+ children: rubyMembers(node.childForFieldName("body"), source),
82
+ };
83
+ }
84
+
85
+ function rubyMembers(body: SyntaxNodeLike | null | undefined, source: string): OutlineItem[] {
86
+ if (!body) return [];
87
+ return body.children.flatMap((child) => rubyItems(child, "member", source) ?? []);
88
+ }
89
+
90
+ function rubyAssignments(
91
+ node: SyntaxNodeLike,
92
+ context: "top-level" | "member",
93
+ source: string,
94
+ ): OutlineItem[] {
95
+ const left = node.childForFieldName("left");
96
+ if (!left) return [];
97
+ const range = nodeToRange(node, source);
98
+ const items = rubyBindingNodes(left)
99
+ .filter((binding) => context === "top-level" || binding.type !== "identifier")
100
+ .map((binding) => ({
101
+ name: binding.text,
102
+ kind:
103
+ binding.type === "constant" || binding.type === "scope_resolution"
104
+ ? "constant"
105
+ : "variable",
106
+ range,
107
+ }));
108
+ const right = unwrapParentheses(node.childForFieldName("right"));
109
+ const chained =
110
+ right?.type === "assignment" || right?.type === "operator_assignment"
111
+ ? rubyAssignments(right, context, source)
112
+ : [];
113
+ return [...items, ...chained];
114
+ }
115
+
116
+ function rubyBindingNodes(node: SyntaxNodeLike): SyntaxNodeLike[] {
117
+ if (RUBY_BINDING_TYPES.has(node.type)) return [node];
118
+ if (!RUBY_BINDING_PATTERNS.has(node.type)) return [];
119
+ return node.children.flatMap(rubyBindingNodes);
120
+ }
121
+
122
+ function rubyCallMethods(node: SyntaxNodeLike, source: string): OutlineItem[] | undefined {
123
+ const method = node.childForFieldName("method")?.text;
124
+ const argumentsNode = node.childForFieldName("arguments");
125
+ if (!method || !argumentsNode || node.childForFieldName("receiver")) return undefined;
126
+
127
+ if (RUBY_ATTRIBUTE_METHODS.has(method)) {
128
+ const range = nodeToRange(node, source);
129
+ return rubySymbolNames(argumentsNode).flatMap((name) => {
130
+ if (method === "attr_writer") return [{ name: `${name}=`, kind: "method", range }];
131
+ if (method === "attr_accessor") {
132
+ return [
133
+ { name, kind: "method", range },
134
+ { name: `${name}=`, kind: "method", range },
135
+ ];
136
+ }
137
+ return [{ name, kind: "method", range }];
138
+ });
139
+ }
140
+ if (method === "alias_method") {
141
+ const name = rubySymbolNames(argumentsNode)[0];
142
+ return name ? [{ name, kind: "method", range: nodeToRange(node, source) }] : [];
143
+ }
144
+ if (RUBY_VISIBILITY_METHODS.has(method)) {
145
+ const declaration = argumentsNode.children.find(
146
+ (child) => child.type === "method" || child.type === "singleton_method",
147
+ );
148
+ return declaration ? rubyItems(declaration, "member", source) : [];
149
+ }
150
+ return undefined;
151
+ }
152
+
153
+ function rubySymbolNames(node: SyntaxNodeLike): string[] {
154
+ return node.children
155
+ .filter((child) => child.type === "simple_symbol")
156
+ .map((child) => child.text.replace(/^:/, ""));
157
+ }
158
+
159
+ function isNestedShellDeclaration(node: SyntaxNodeLike): boolean {
160
+ if (isBackgroundedShellNode(node)) return true;
161
+ for (let parent = node.parent; parent; parent = parent.parent) {
162
+ if (SHELL_SCOPE_BOUNDARIES.has(parent.type)) return true;
163
+ }
164
+ return false;
165
+ }
166
+
167
+ function isBackgroundedShellNode(node: SyntaxNodeLike): boolean {
168
+ for (let current = node, parent = node.parent; parent; current = parent, parent = parent.parent) {
169
+ const index = parent.children.findIndex((child) => sameNode(child, current));
170
+ if (index >= 0 && parent.children[index + 1]?.type === "&") return true;
171
+ }
172
+ return false;
173
+ }
174
+
175
+ function sameNode(left: SyntaxNodeLike, right: SyntaxNodeLike): boolean {
176
+ return (
177
+ left.type === right.type &&
178
+ left.startPosition.row === right.startPosition.row &&
179
+ left.startPosition.column === right.startPosition.column &&
180
+ left.endPosition.row === right.endPosition.row &&
181
+ left.endPosition.column === right.endPosition.column
182
+ );
183
+ }
184
+
185
+ function shellVariable(
186
+ node: SyntaxNodeLike,
187
+ kind: "constant" | "variable",
188
+ source: string,
189
+ ): OutlineItem | null {
190
+ const name = node.childForFieldName("name");
191
+ return name?.type === "variable_name"
192
+ ? { name: name.text, kind, range: nodeToRange(node, source) }
193
+ : null;
194
+ }
195
+
196
+ function shellDeclarations(node: SyntaxNodeLike, source: string): OutlineItem[] {
197
+ if (node.children.some((child) => child.type === "word" && /^-[a-z]*[pf]/i.test(child.text))) {
198
+ return [];
199
+ }
200
+ const constant =
201
+ node.children.some((child) => child.type === "readonly") ||
202
+ node.children.some((child) => child.type === "word" && /^-[a-z]*r/i.test(child.text));
203
+ const range = nodeToRange(node, source);
204
+ return node.children.flatMap((child) => {
205
+ if (child.type === "variable_assignment") {
206
+ const item = shellVariable(child, constant ? "constant" : "variable", source);
207
+ return item ? [{ ...item, range }] : [];
208
+ }
209
+ return child.type === "variable_name"
210
+ ? [{ name: child.text, kind: constant ? "constant" : "variable", range }]
211
+ : [];
212
+ });
213
+ }
214
+
215
+ function rAssignments(node: SyntaxNodeLike, source: string): OutlineItem[] | undefined {
216
+ const operator = node.children.find((child) => R_ASSIGNMENT_OPERATORS.has(child.type))?.type;
217
+ if (!operator) return undefined;
218
+
219
+ const leftward = operator === "=" || operator.endsWith("<-");
220
+ const receiver = node.childForFieldName(leftward ? "lhs" : "rhs");
221
+ const value = node.childForFieldName(leftward ? "rhs" : "lhs");
222
+ if (!receiver || !value) return [];
223
+
224
+ const item =
225
+ receiver.type === "identifier"
226
+ ? [
227
+ {
228
+ name: receiver.text,
229
+ kind: rAssignedValue(value)?.type === "function_definition" ? "function" : "variable",
230
+ range: nodeToRange(node, source),
231
+ },
232
+ ]
233
+ : [];
234
+ const nestedAssignment = unwrapParentheses(value);
235
+ const chained =
236
+ nestedAssignment?.type === "binary_operator"
237
+ ? (rAssignments(nestedAssignment, source) ?? [])
238
+ : [];
239
+ return [...item, ...chained];
240
+ }
241
+
242
+ function unwrapParentheses(node: SyntaxNodeLike | null): SyntaxNodeLike | null {
243
+ if (node?.type === "parenthesized_expression") {
244
+ return unwrapParentheses(node.childForFieldName("body"));
245
+ }
246
+ if (node?.type === "parenthesized_statements") {
247
+ return unwrapParentheses(
248
+ node.children.find(
249
+ (child) => child.type === "assignment" || child.type === "operator_assignment",
250
+ ) ?? null,
251
+ );
252
+ }
253
+ return node;
254
+ }
255
+
256
+ function rAssignedValue(node: SyntaxNodeLike): SyntaxNodeLike | null {
257
+ const assignment = unwrapParentheses(node);
258
+ if (assignment?.type !== "binary_operator") return assignment;
259
+
260
+ const operator = assignment.children.find((child) =>
261
+ R_ASSIGNMENT_OPERATORS.has(child.type),
262
+ )?.type;
263
+ if (!operator) return assignment;
264
+ const leftward = operator === "=" || operator.endsWith("<-");
265
+ const value = assignment.childForFieldName(leftward ? "rhs" : "lhs");
266
+ return value ? rAssignedValue(value) : null;
267
+ }
268
+
269
+ function one(item: OutlineItem | null): OutlineItem[] {
270
+ return item ? [item] : [];
271
+ }
272
+
273
+ function named(node: SyntaxNodeLike, kind: string, source: string): OutlineItem | null {
274
+ const name = node.childForFieldName("name");
275
+ return name ? { name: name.text, kind, range: nodeToRange(node, source) } : null;
276
+ }
277
+
278
+ const RUBY_BINDING_TYPES = new Set([
279
+ "constant",
280
+ "identifier",
281
+ "global_variable",
282
+ "class_variable",
283
+ "instance_variable",
284
+ "scope_resolution",
285
+ ]);
286
+ const RUBY_BINDING_PATTERNS = new Set([
287
+ "left_assignment_list",
288
+ "destructured_left_assignment",
289
+ "rest_assignment",
290
+ ]);
291
+ const RUBY_CONTAINER_NAME_TYPES = new Set(["constant", "scope_resolution"]);
292
+ const RUBY_ATTRIBUTE_METHODS = new Set(["attr", "attr_reader", "attr_writer", "attr_accessor"]);
293
+ const RUBY_VISIBILITY_METHODS = new Set([
294
+ "private",
295
+ "protected",
296
+ "public",
297
+ "module_function",
298
+ "private_class_method",
299
+ "public_class_method",
300
+ ]);
301
+ const SHELL_SCOPE_BOUNDARIES = new Set([
302
+ "command_substitution",
303
+ "process_substitution",
304
+ "subshell",
305
+ "pipeline",
306
+ ]);
307
+ const R_ASSIGNMENT_OPERATORS = new Set(["=", "<-", "<<-", "->", "->>"]);
@@ -3,6 +3,7 @@
3
3
  import { nodeToRange } from "../coordinates.ts";
4
4
  import type { SyntaxNodeLike } from "../syntax-node.ts";
5
5
  import type { OutlineItem } from "../types.ts";
6
+ import { extractPolyglotOutlineItems } from "./outline-polyglot.ts";
6
7
 
7
8
  /** Node types that can be extracted directly as outline items. */
8
9
  const OUTLINE_DECLARATION_NODE_TYPES = new Set([
@@ -32,12 +33,15 @@ function collectItems(node: SyntaxNodeLike, source: string): OutlineItem[] {
32
33
  const items: OutlineItem[] = [];
33
34
 
34
35
  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));
36
+ const polyglotItems = extractPolyglotOutlineItems(child, source);
37
+ if (polyglotItems) {
38
+ items.push(...polyglotItems);
39
+ continue;
40
40
  }
41
+
42
+ const item = extractItem(child, source);
43
+ if (item) items.push(item);
44
+ else items.push(...collectItems(child, source));
41
45
  }
42
46
 
43
47
  return items;
package/src/types.ts CHANGED
@@ -79,7 +79,10 @@ export interface TreeSitterService {
79
79
  canParse(file: string): Promise<TreeSitterResult<{ file: string; language: string }>>;
80
80
  /** Run a Tree-sitter query and return all captures. */
81
81
  query(file: string, queryString: string): Promise<TreeSitterResult<QueryCapture[]>>;
82
- /** Extract top-level declarations plus supported class/interface/enum members. */
82
+ /**
83
+ * Extract shallow declarations, including supported nested code members,
84
+ * HTML ids, and SQL schema members.
85
+ */
83
86
  outline(file: string): Promise<TreeSitterResult<OutlineItem[]>>;
84
87
  /** Extract static ES import declarations. */
85
88
  imports(file: string): Promise<TreeSitterResult<ImportRecord[]>>;
@@ -143,7 +146,6 @@ export type SupportedExtension =
143
146
  | ".pyi"
144
147
  | ".rs"
145
148
  | ".go"
146
- | ".mod"
147
149
  | ".c"
148
150
  | ".h"
149
151
  | ".cpp"
@@ -157,9 +159,11 @@ export type SupportedExtension =
157
159
  | ".kt"
158
160
  | ".kts"
159
161
  | ".rb"
162
+ | ".gemspec"
160
163
  | ".sh"
161
164
  | ".bash"
162
165
  | ".zsh"
166
+ | ".ksh"
163
167
  | ".html"
164
168
  | ".htm"
165
169
  | ".xhtml"