@mrclrchtr/supi-tree-sitter 11.4.0 → 12.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "11.4.0",
3
+ "version": "12.0.0",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,6 +60,7 @@ export type {
60
60
  ImportData,
61
61
  NodeAtData,
62
62
  OutlineData,
63
+ RefactorActionIdentity,
63
64
  RefactorOperation,
64
65
  RefactorRequest,
65
66
  RefactorResult,
@@ -193,31 +193,53 @@ export interface WorkspaceEdit {
193
193
  }
194
194
 
195
195
  /**
196
- * Supported refactor operation names for the current semantic planning path.
197
- */
198
- export type RefactorOperation =
199
- | "rename_symbol"
200
- | "extract_function"
201
- | "extract_variable"
202
- | "rename_file"
203
- | "move_file"
204
- | "update_imports"
205
- | "delete_dead_code";
206
-
207
- /**
208
- * Operation-aware refactor planning request.
196
+ * Operation-aware refactor planning request. Each operation carries only the
197
+ * scope it needs:
209
198
  *
210
- * Consumers provide a target file/position plus operation-specific options.
211
- * File/resource operations remain requestable so providers can reject them
212
- * explicitly and honestly until shared workspace-edit resource ops exist.
199
+ * - `rename_symbol`: one symbol position and the new name.
200
+ * - `code_action`: one file range and a CodeActionKind prefix under `refactor`.
201
+ * - `source_action`: one whole file and a CodeActionKind prefix under `source`.
202
+ *
203
+ * `kind` matches the requested kind and its dotted sub-kinds. `title`, when
204
+ * present, selects one action by its exact title.
213
205
  */
214
- export interface RefactorRequest {
215
- operation: RefactorOperation;
216
- file: string;
217
- position: CodePosition;
218
- range?: SourceRange;
219
- newName?: string;
220
- destination?: string;
206
+ export type RefactorRequest =
207
+ | {
208
+ operation: "rename_symbol";
209
+ file: string;
210
+ position: CodePosition;
211
+ newName: string;
212
+ }
213
+ | {
214
+ operation: "code_action";
215
+ file: string;
216
+ range: SourceRange;
217
+ kind: string;
218
+ title?: string;
219
+ }
220
+ | {
221
+ operation: "source_action";
222
+ file: string;
223
+ kind: string;
224
+ title?: string;
225
+ };
226
+
227
+ /** Supported refactor operation names. */
228
+ export type RefactorOperation = RefactorRequest["operation"];
229
+
230
+ /** The code action selected for a precise code- or source-action plan. */
231
+ export interface RefactorActionIdentity {
232
+ /** Server-reported CodeActionKind. */
233
+ kind: string;
234
+ /** Server-reported action title. */
235
+ title: string;
236
+ /**
237
+ * How the edits were obtained: `edit` from the action itself (after
238
+ * `codeAction/resolve` when needed), or `captured-command` from the single
239
+ * `workspace/applyEdit` the server pushed while running the action's command.
240
+ * Captured edits were refused, never applied.
241
+ */
242
+ provenance?: "edit" | "captured-command";
221
243
  }
222
244
 
223
245
  /**
@@ -228,6 +250,10 @@ export interface DisambiguationCandidate {
228
250
  file?: string;
229
251
  line?: number;
230
252
  character?: number;
253
+ /** Server-reported CodeActionKind, for code-action candidates. */
254
+ kind?: string;
255
+ /** Exact action title to pass back as `title`, for code-action candidates. */
256
+ title?: string;
231
257
  }
232
258
 
233
259
  /**
@@ -241,6 +267,8 @@ export type RefactorResult =
241
267
  | {
242
268
  kind: "precise";
243
269
  edits: WorkspaceEdit;
270
+ /** The selected code action, for code- and source-action plans. */
271
+ action?: RefactorActionIdentity;
244
272
  /** Provider roots authorized by the semantic route. Consumers canonicalize before storage. */
245
273
  authorizedMutationRoots: string[];
246
274
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "11.4.0",
3
+ "version": "12.0.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-tree-sitter",
3
- "version": "11.4.0",
3
+ "version": "12.0.0",
4
4
  "description": "Structural AST analysis for SuPi code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,8 +36,8 @@
36
36
  "dependencies": {
37
37
  "jiti": "^2.7.0",
38
38
  "web-tree-sitter": "^0.27.0",
39
- "@mrclrchtr/supi-core": "11.4.0",
40
- "@mrclrchtr/supi-code-runtime": "11.4.0"
39
+ "@mrclrchtr/supi-code-runtime": "12.0.0",
40
+ "@mrclrchtr/supi-core": "12.0.0"
41
41
  },
42
42
  "bundledDependencies": [
43
43
  "@mrclrchtr/supi-code-runtime",
@@ -100,7 +100,21 @@ export function validatePublicPositionBounds(
100
100
  return null;
101
101
  }
102
102
 
103
- /** Split source into logical lines without CRLF line-ending bytes. */
103
+ /**
104
+ * Split source into Tree-sitter rows, dropping the `\r` of CRLF endings.
105
+ * Tree-sitter rows break only on `\n`, so a lone `\r` is not a line break here.
106
+ * LSP treats a lone `\r` as a line break, so structural lines after one differ from LSP lines.
107
+ */
104
108
  export function splitSourceLines(source: string): string[] {
105
- return source.replace(/\r\n?/g, "\n").split("\n");
109
+ if (lastSplit?.source === source) return lastSplit.lines;
110
+ const lines = source.split(/\r?\n/);
111
+ lastSplit = { source, lines };
112
+ return lines;
106
113
  }
114
+
115
+ /**
116
+ * Single-entry cache: range conversion calls this once per node on the same
117
+ * source, so re-splitting each time made extraction quadratic in file size.
118
+ * Callers must not mutate the returned array.
119
+ */
120
+ let lastSplit: { source: string; lines: string[] } | undefined;
@@ -331,8 +331,9 @@ export class StructuralWorkerClient {
331
331
  try {
332
332
  throwIfCodeRequestInterrupted(active.control);
333
333
  } catch (error) {
334
+ // Keep the chunk handshake alive so the Worker can send its terminal message
335
+ // instead of waiting for the hard stop.
334
336
  this.#interrupt(active, error);
335
- return;
336
337
  }
337
338
  active.chunks.push(Uint8Array.from(message.payload));
338
339
  active.nextSequence += 1;
@@ -433,7 +434,7 @@ export class StructuralWorkerClient {
433
434
  return;
434
435
  }
435
436
  const worker = this.#worker;
436
- if (this.#active !== request || !worker) return;
437
+ if (this.#active !== request || !worker || request.hardStopTimer) return;
437
438
  Atomics.store(request.cancellationFlag, 0, 1);
438
439
  if (
439
440
  !this.#send(worker, {
@@ -4,7 +4,9 @@ import type { GrammarId, SourceRange } from "./types.ts";
4
4
 
5
5
  /** Return whether a parsed tree contains an explicit or recovered syntax error. */
6
6
  export function hasSyntaxErrors(root: SyntaxNodeLike): boolean {
7
+ // web-tree-sitter's hasError already covers ERROR and missing descendants.
7
8
  if (root.hasError === true) return true;
9
+ if (root.hasError === false) return false;
8
10
  if (root.type === "ERROR" || root.isMissing === true) return true;
9
11
  return (root.children ?? []).some(hasSyntaxErrors);
10
12
  }
@@ -0,0 +1,94 @@
1
+ // Callee ranges for grammars whose call queries capture the whole call node.
2
+
3
+ import { tsPointToPublic } from "../coordinates.ts";
4
+ import type { SyntaxNodeLike } from "../syntax-node.ts";
5
+ import type { GrammarId, SourceRange } from "../types.ts";
6
+
7
+ /**
8
+ * Return the callee range for a whole-call capture, so call-site ranges cover the
9
+ * callee expression in every grammar (`a.b` in `a.b(1)`), as JS/TS, Python, Go,
10
+ * and Rust queries capture directly. Returns `null` when the capture already is
11
+ * the callee or no narrower callee exists.
12
+ */
13
+ export function calleeRangeForCallNode(
14
+ node: SyntaxNodeLike,
15
+ grammarId: GrammarId,
16
+ source: string,
17
+ ): SourceRange | null {
18
+ const end = calleeEnd(node, grammarId);
19
+ if (!end) return null;
20
+ const start = tsPointToPublic(node.startPosition, source);
21
+ const publicEnd = tsPointToPublic(end, source);
22
+ return {
23
+ startLine: start.line,
24
+ startCharacter: start.character,
25
+ endLine: publicEnd.line,
26
+ endCharacter: publicEnd.character,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Find the syntax node for one query capture by walking up from the leaf at its start.
32
+ * Returns `null` when no ancestor has the capture's type and range.
33
+ */
34
+ export function findCaptureNode(
35
+ root: SyntaxNodeLike & {
36
+ descendantForPosition(point: { row: number; column: number }): SyntaxNodeLike | null;
37
+ },
38
+ capture: { nodeType: string; range: SourceRange },
39
+ ): SyntaxNodeLike | null {
40
+ const { range } = capture;
41
+ let node = root.descendantForPosition({
42
+ row: range.startLine - 1,
43
+ column: range.startCharacter - 1,
44
+ });
45
+ while (node) {
46
+ if (
47
+ node.type === capture.nodeType &&
48
+ node.startPosition.row === range.startLine - 1 &&
49
+ node.startPosition.column === range.startCharacter - 1 &&
50
+ node.endPosition.row === range.endLine - 1 &&
51
+ node.endPosition.column === range.endCharacter - 1
52
+ ) {
53
+ return node;
54
+ }
55
+ node = node.parent;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function calleeEnd(
61
+ node: SyntaxNodeLike,
62
+ grammarId: GrammarId,
63
+ ): { row: number; column: number } | null {
64
+ switch (grammarId) {
65
+ case "java":
66
+ return node.type === "method_invocation"
67
+ ? (node.childForFieldName("name")?.endPosition ?? null)
68
+ : null;
69
+ case "kotlin":
70
+ return node.type === "call_expression" ? (kotlinCallee(node)?.endPosition ?? null) : null;
71
+ case "ruby":
72
+ return node.type === "call"
73
+ ? ((node.childForFieldName("method") ?? node.childForFieldName("receiver"))?.endPosition ??
74
+ null)
75
+ : null;
76
+ default:
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Kotlin nests `f(x) { }` as an outer call (lambda-only suffix) wrapping `f(x)`.
83
+ * Treat both as one call so they share the callee `f`; `f(1)(2)` keeps `f(1)` as
84
+ * the outer callee because its suffix has value arguments.
85
+ */
86
+ function kotlinCallee(node: SyntaxNodeLike): SyntaxNodeLike | null {
87
+ const callee = node.children[0];
88
+ if (!callee) return null;
89
+ const suffix = node.children.find((child) => child.type === "call_suffix");
90
+ const lambdaOnly =
91
+ suffix !== undefined && !suffix.children.some((child) => child.type === "value_arguments");
92
+ if (callee.type === "call_expression" && lambdaOnly) return kotlinCallee(callee);
93
+ return callee;
94
+ }
@@ -1,10 +1,17 @@
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 type { CallSiteMatch, GrammarId, TreeSitterResult } from "../types.ts";
4
+ import type {
5
+ CallSiteMatch,
6
+ GrammarId,
7
+ QueryCapture,
8
+ SourceRange,
9
+ TreeSitterResult,
10
+ } from "../types.ts";
5
11
  import { isTypeOnlyImportCapture } from "../worker/call-context.ts";
6
12
  import { queryParsedFile, type TreeSitterRuntime } from "../worker/runtime.ts";
7
13
  import { normalizeCallName } from "./call-name.ts";
14
+ import { calleeRangeForCallNode, findCaptureNode } from "./call-range.ts";
8
15
 
9
16
  // ── Per-grammar call-site queries ─────────────────────────────────────
10
17
 
@@ -90,17 +97,19 @@ export async function extractCallSites(
90
97
  const name = normalizeCallName(capture.text, grammarId, capture.nodeType);
91
98
  if (name.length === 0) continue;
92
99
 
100
+ const range = callSiteRange(tree.rootNode, capture, grammarId, source);
101
+
93
102
  // Collapse duplicate captures only when they identify the same source range.
94
- const key = `${capture.range.startLine}:${capture.range.startCharacter}:${capture.range.endLine}:${capture.range.endCharacter}`;
103
+ const key = `${range.startLine}:${range.startCharacter}:${range.endLine}:${range.endCharacter}`;
95
104
  if (seen.has(key)) continue;
96
105
  seen.add(key);
97
106
 
98
107
  matches.push({
99
108
  name,
100
- startLine: capture.range.startLine,
101
- startCharacter: capture.range.startCharacter,
102
- endLine: capture.range.endLine,
103
- endCharacter: capture.range.endCharacter,
109
+ startLine: range.startLine,
110
+ startCharacter: range.startCharacter,
111
+ endLine: range.endLine,
112
+ endCharacter: range.endCharacter,
104
113
  });
105
114
  }
106
115
 
@@ -111,3 +120,14 @@ export async function extractCallSites(
111
120
  tree.delete();
112
121
  }
113
122
  }
123
+
124
+ /** Narrow whole-call captures to their callee so ranges match the other grammars. */
125
+ function callSiteRange(
126
+ root: Parameters<typeof findCaptureNode>[0],
127
+ capture: QueryCapture,
128
+ grammarId: GrammarId,
129
+ source: string,
130
+ ): SourceRange {
131
+ const node = findCaptureNode(root, capture);
132
+ return (node && calleeRangeForCallNode(node, grammarId, source)) ?? capture.range;
133
+ }
@@ -9,6 +9,7 @@ import type { GrammarId, SourceRange, TreeSitterResult } from "../types.ts";
9
9
  import { isTypeOnlyImportCapture } from "../worker/call-context.ts";
10
10
  import { queryParsedFile, type TreeSitterRuntime } from "../worker/runtime.ts";
11
11
  import { normalizeCallName } from "./call-name.ts";
12
+ import { calleeRangeForCallNode } from "./call-range.ts";
12
13
  import { createCalleeDisplayName, createSourceLineStarts } from "./callee-display-name.ts";
13
14
  import { extractScopeName } from "./scope.ts";
14
15
 
@@ -387,11 +388,13 @@ function filterCalleeCaptures(
387
388
 
388
389
  if (isTypeOnlyImportCapture(enclosingNode, capture, grammarId)) continue;
389
390
  const name = normalizeCallName(capture.text, grammarId, capture.nodeType);
390
- const rangeKey = sourceRangeKey(capture.range);
391
+ const syntaxNode = syntaxNodes.get(captureNodeKey(capture));
392
+ const range =
393
+ (syntaxNode && calleeRangeForCallNode(syntaxNode, grammarId, source)) ?? capture.range;
394
+ const rangeKey = sourceRangeKey(range);
391
395
  if (name.length === 0 || seenRanges.has(rangeKey)) continue;
392
396
  seenRanges.add(rangeKey);
393
397
 
394
- const syntaxNode = syntaxNodes.get(captureNodeKey(capture));
395
398
  const displayName = syntaxNode
396
399
  ? createCalleeDisplayName({
397
400
  node: syntaxNode,
@@ -404,7 +407,7 @@ function filterCalleeCaptures(
404
407
  callees.push({
405
408
  name,
406
409
  ...(displayName === undefined ? {} : { displayName }),
407
- range: capture.range,
410
+ range,
408
411
  });
409
412
  }
410
413
 
@@ -91,9 +91,11 @@ const DECLARATION_EXPORT_NODE_TYPES = new Set([
91
91
  "type_alias_declaration",
92
92
  "enum_declaration",
93
93
  "lexical_declaration",
94
+ "variable_declaration",
94
95
  "ambient_declaration",
95
96
  "internal_module",
96
97
  "module",
98
+ "import_alias",
97
99
  ]);
98
100
 
99
101
  function extractDeclarationExport(
@@ -101,7 +103,7 @@ function extractDeclarationExport(
101
103
  source: string,
102
104
  exports: ExportRecord[],
103
105
  ): void {
104
- if (decl.type === "lexical_declaration") {
106
+ if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
105
107
  extractLexicalDeclarationExports(decl, source, exports);
106
108
  return;
107
109
  }
@@ -111,7 +113,11 @@ function extractDeclarationExport(
111
113
  return;
112
114
  }
113
115
 
114
- const name = getDeclarationName(decl);
116
+ // `export import X = Y.Z` exports the alias `X`, which may name a value, type, or namespace.
117
+ const name =
118
+ decl.type === "import_alias"
119
+ ? (decl.children.find((child) => child.type === "identifier")?.text ?? null)
120
+ : getDeclarationName(decl);
115
121
  if (!name) return;
116
122
 
117
123
  exports.push({
@@ -143,7 +149,7 @@ function extractAmbientDeclarationExport(
143
149
  const nested = decl.children.find((child) => child.type !== "declare" && child.type !== ";");
144
150
  if (!nested) return;
145
151
 
146
- if (nested.type === "lexical_declaration") {
152
+ if (nested.type === "lexical_declaration" || nested.type === "variable_declaration") {
147
153
  extractLexicalDeclarationExports(nested, source, exports);
148
154
  return;
149
155
  }
@@ -177,6 +183,8 @@ function exportKindForDeclaration(type: string): string {
177
183
  case "internal_module":
178
184
  case "module":
179
185
  return "namespace";
186
+ case "import_alias":
187
+ return "alias";
180
188
  default:
181
189
  return type.replace(/_declaration$/, "");
182
190
  }
@@ -217,8 +225,9 @@ function extractDefaultExport(node: SyntaxNodeLike, source: string, exports: Exp
217
225
  });
218
226
  }
219
227
 
228
+ /** Return the `from "module"` specifier; a string value such as `export default "x"` is not one. */
220
229
  function findStringChild(node: SyntaxNodeLike): SyntaxNodeLike | undefined {
221
- return node.children.find((child) => child.type === "string");
230
+ return node.childForFieldName("source") ?? undefined;
222
231
  }
223
232
 
224
233
  function extractReExport(
@@ -13,11 +13,7 @@ import {
13
13
  type StructuralTimingEvent,
14
14
  startStructuralTiming,
15
15
  } from "../session/structural-timing.ts";
16
- import {
17
- collectSyntaxErrorRanges,
18
- hasSyntaxErrors,
19
- withSyntaxCompleteness,
20
- } from "../syntax-errors.ts";
16
+ import { withSyntaxCompleteness } from "../syntax-errors.ts";
21
17
  import type { GrammarId, QueryCapture, TreeSitterResult } from "../types.ts";
22
18
  import {
23
19
  ParsedFileReadError,
@@ -172,8 +168,6 @@ export class TreeSitterRuntime {
172
168
  source: string;
173
169
  resolvedPath: string;
174
170
  grammarId: GrammarId;
175
- hasSyntaxErrors: boolean;
176
- errorRanges: ReturnType<typeof collectSyntaxErrorRanges>;
177
171
  };
178
172
  }
179
173
  | Exclude<TreeSitterResult<never>, { kind: "success" | "partial" }>
@@ -249,8 +243,6 @@ export class TreeSitterRuntime {
249
243
  source: parsed.source,
250
244
  resolvedPath: parsed.resolvedPath,
251
245
  grammarId: parsed.grammarId,
252
- hasSyntaxErrors: hasSyntaxErrors(parsed.tree.rootNode),
253
- errorRanges: collectSyntaxErrorRanges(parsed.tree.rootNode, parsed.source),
254
246
  },
255
247
  };
256
248
  } catch (err: unknown) {