@mrclrchtr/supi-code-intelligence 6.1.0 → 6.2.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 (114) hide show
  1. package/README.md +1 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  6. package/node_modules/@mrclrchtr/supi-lsp/README.md +21 -3
  7. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  8. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  11. package/node_modules/@mrclrchtr/supi-lsp/package.json +4 -5
  12. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +12 -7
  13. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +0 -5
  15. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +6 -3
  16. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +2 -1
  17. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +5 -2
  18. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +2 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +17 -5
  20. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +40 -18
  21. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +164 -30
  22. package/node_modules/@mrclrchtr/supi-lsp/src/config/lsp-settings.ts +9 -87
  23. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +0 -1
  24. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +4 -5
  25. package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +0 -29
  26. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +7 -21
  27. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-severity.ts +6 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +11 -14
  29. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +0 -5
  30. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +0 -1
  31. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +29 -49
  32. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +1 -1
  33. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -8
  34. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +1 -9
  35. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +6 -3
  36. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +9 -1
  37. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +26 -41
  38. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +275 -99
  39. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +4 -1
  40. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +59 -28
  41. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +27 -19
  42. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +26 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registration.ts +0 -17
  44. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +83 -19
  45. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +5 -2
  46. package/node_modules/@mrclrchtr/supi-lsp/src/session/scanner.ts +24 -21
  47. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +29 -3
  48. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +19 -40
  49. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +13 -19
  50. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +287 -0
  51. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +6 -18
  52. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  53. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  54. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  55. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -5
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +1 -11
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +19 -0
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +5 -23
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/src/operation-support.ts +3 -21
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +5 -9
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +10 -20
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +7 -1
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +9 -5
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +134 -9
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-name.ts +96 -0
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +11 -12
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +39 -56
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +9 -7
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/js-binding-pattern.ts +26 -0
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +2 -21
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-html-sql.ts +144 -12
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline.ts +50 -37
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/scope.ts +138 -0
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +0 -7
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +1 -14
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +0 -10
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +6 -8
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +34 -10
  80. package/package.json +5 -5
  81. package/src/analysis/capability/capability-warnings.ts +4 -22
  82. package/src/analysis/search/ast-scan-timing.ts +4 -2
  83. package/src/analysis/search/ast-scan.ts +1 -1
  84. package/src/api.ts +1 -0
  85. package/src/extension.ts +4 -2
  86. package/src/session/health-refresh.ts +24 -2
  87. package/src/session/health-types.ts +19 -0
  88. package/src/session/input/health-refactor.ts +16 -14
  89. package/src/session/refactor-types.ts +14 -1
  90. package/src/session/refactor-workflow.ts +21 -16
  91. package/src/substrate/lsp/lifecycle.ts +0 -3
  92. package/src/substrate/lsp/maintenance.ts +4 -5
  93. package/src/substrate/lsp/settings.ts +6 -9
  94. package/src/substrate/lsp/state.ts +0 -2
  95. package/src/substrate/workspace-provider-host.ts +2 -2
  96. package/src/tool/code_health/file-scope-markdown.ts +43 -0
  97. package/src/tool/code_health/markdown.ts +66 -66
  98. package/src/tool/code_health/refresh-outcome.ts +46 -0
  99. package/src/tool/code_health/refresh-status.ts +166 -23
  100. package/src/tool/code_health/result.ts +1 -0
  101. package/src/tool/code_health/tui.ts +18 -5
  102. package/src/tool/code_refactor_apply/guidance.ts +3 -2
  103. package/src/tool/code_refactor_plan/guidance.ts +1 -1
  104. package/src/tool/register.ts +5 -2
  105. package/src/tool/schemas.ts +21 -11
  106. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-actions.ts +0 -59
  107. package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +0 -24
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +0 -132
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +0 -158
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +0 -167
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -42
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-registration.ts +0 -37
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/service-registry.ts +0 -30
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/structure.ts +0 -8
@@ -1,9 +1,12 @@
1
1
  // Structural callee extraction — enclosing-scope lookup with per-language queries.
2
2
 
3
3
  import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
4
+ import { validatePublicPositionBounds } from "../coordinates.ts";
4
5
  import { detectGrammar } from "../language.ts";
5
6
  import type { GrammarId, SourceRange, TreeSitterResult } from "../types.ts";
6
7
  import { queryParsedFile, type TreeSitterRuntime } from "../worker/runtime.ts";
8
+ import { normalizeCallName } from "./call-name.ts";
9
+ import { extractScopeName } from "./scope.ts";
7
10
 
8
11
  /** Result shape returned by lookupCalleesAt. */
9
12
  export interface CalleesAtResult {
@@ -25,13 +28,13 @@ const CALLEE_QUERIES: Partial<Record<GrammarId, string>> = {
25
28
  typescript: "(call_expression function: (_) @callee) (new_expression constructor: (_) @callee)",
26
29
  tsx: "(call_expression function: (_) @callee) (new_expression constructor: (_) @callee)",
27
30
  python: "(call function: (_) @callee)",
28
- rust: "(call_expression function: (_) @callee) (macro_invocation) @callee",
31
+ rust: "(call_expression function: (_) @callee) (macro_invocation macro: (_) @callee)",
29
32
  go: "(call_expression function: (_) @callee)",
30
33
  c: "(call_expression function: (_) @callee)",
31
34
  cpp: "(call_expression function: (_) @callee)",
32
- java: "(method_invocation name: (_) @callee) (object_creation_expression type: (_) @callee)",
33
- kotlin: "(call_expression . (_) @callee)",
34
- ruby: "(call method: (_) @callee)",
35
+ java: "(method_invocation) @callee (object_creation_expression type: (_) @callee)",
36
+ kotlin: "(call_expression) @callee",
37
+ ruby: "(call) @callee",
35
38
  bash: "(command . (_) @callee)",
36
39
  r: "(call function: (_) @callee)",
37
40
  };
@@ -57,63 +60,22 @@ const ENCLOSING_SCOPE_TYPES: Record<GrammarId, ReadonlySet<string>> = {
57
60
  "arrow_function",
58
61
  "function_expression",
59
62
  ]),
60
- python: new Set(["function_definition"]),
61
- rust: new Set(["function_item"]),
62
- go: new Set(["function_declaration"]),
63
+ python: new Set(["function_definition", "lambda"]),
64
+ rust: new Set(["function_item", "closure_expression"]),
65
+ go: new Set(["function_declaration", "method_declaration", "func_literal"]),
63
66
  c: new Set(["function_definition"]),
64
- cpp: new Set(["function_definition"]),
65
- java: new Set(["method_declaration"]),
66
- kotlin: new Set(["function_declaration"]),
67
- ruby: new Set(["method"]),
67
+ cpp: new Set(["function_definition", "lambda_expression"]),
68
+ java: new Set(["method_declaration", "constructor_declaration", "lambda_expression"]),
69
+ kotlin: new Set(["function_declaration", "lambda_literal", "anonymous_function"]),
70
+ ruby: new Set(["method", "block", "do_block", "lambda"]),
68
71
  bash: new Set(["function_definition"]),
69
72
  r: new Set(["function_definition"]),
70
73
  html: new Set(),
71
74
  sql: new Set(),
72
75
  };
73
76
 
74
- // ── Name extraction from enclosing scope text ─────────────────────────
75
-
76
- /** Extract a best-effort name from an enclosing scope node's source text. */
77
- function extractScopeName(_type: string, text: string): string {
78
- // JS/TS/Go/Rust: `function foo` or `fn foo` or `def foo`
79
- // Python: `def foo`
80
- const firstLine = text.split("\n")[0] ?? text;
81
- const match = firstLine.match(/(?:function|fn|func|def|method)\s+(\w+)/);
82
- if (match?.[1]) return match[1];
83
-
84
- // Ruby: `def foo`
85
- const rubyMatch = firstLine.match(/def\s+(\w+)/);
86
- if (rubyMatch?.[1]) return rubyMatch[1];
87
-
88
- // Bash: `foo()`
89
- const bashMatch = firstLine.match(/^(\w+)\s*\(\)/);
90
- if (bashMatch?.[1]) return bashMatch[1];
91
-
92
- // Kotlin/Java: `fun foo` or `fun Foo.bar()` — extract the last name part
93
- const kotlinMatch = firstLine.match(/fun\s+(?:[\w.]+\.)?(\w+)/);
94
- if (kotlinMatch?.[1]) return kotlinMatch[1];
95
-
96
- // C++: type name(params) { — heuristic: first word before `(`
97
- const cLikeMatch = firstLine.match(
98
- /(\w+)\s*\([^)]*\)\s*(?:const\s*)?(?:override\s*)?(?:final\s*)?[{;]/,
99
- );
100
- if (cLikeMatch?.[1]) return cLikeMatch[1];
101
-
102
- return "anonymous";
103
- }
104
-
105
77
  // ── Main entrypoint ──────────────────────────────────────────────────
106
78
 
107
- /**
108
- * Extract direct structural callee calls for a file at the given position.
109
- *
110
- * 1. Parses the file with the Tree-sitter runtime.
111
- * 2. Finds the enclosing function/method/callback scope at the position.
112
- * 3. Runs a grammar-specific callee query.
113
- * 4. Filters to captures within that enclosing scope.
114
- * 5. Excludes nested function/method/callback scopes that do not contain the anchor.
115
- * 6. Deduplicates by name.
116
- */
117
79
  /** Validate that coordinates are usable and grammar is supported. */
118
80
  function validateCalleeInput(
119
81
  filePath: string,
@@ -168,6 +130,16 @@ function findEnclosingScope(
168
130
  return null;
169
131
  }
170
132
 
133
+ /**
134
+ * Extract direct structural callee calls for a file at the given position.
135
+ *
136
+ * 1. Parses the file with the Tree-sitter runtime.
137
+ * 2. Finds the enclosing function/method/callback scope at the position.
138
+ * 3. Runs a grammar-specific callee query.
139
+ * 4. Filters to captures within that enclosing scope.
140
+ * 5. Excludes nested function/method/callback scopes that do not contain the anchor.
141
+ * 6. Deduplicates by name.
142
+ */
171
143
  // biome-ignore lint/complexity/useMaxParams: provider function needs runtime + coordinates + depth
172
144
  export async function lookupCalleesAt(
173
145
  runtime: TreeSitterRuntime,
@@ -189,6 +161,9 @@ export async function lookupCalleesAt(
189
161
  const { tree, source } = parseResult.data;
190
162
 
191
163
  try {
164
+ const boundsError = validatePublicPositionBounds(line, character, source);
165
+ if (boundsError) return boundsError;
166
+
192
167
  const scopes = ENCLOSING_SCOPE_TYPES[grammarId];
193
168
  const tsPoint = { row: line - 1, column: character - 1 };
194
169
  const node = tree.rootNode.descendantForPosition(tsPoint);
@@ -230,10 +205,17 @@ export async function lookupCalleesAt(
230
205
  };
231
206
  }
232
207
 
233
- const callees = filterCalleeCaptures(queryResult.data, enclosingNode, scopes, tsPoint, depth);
208
+ const callees = filterCalleeCaptures(
209
+ queryResult.data,
210
+ grammarId,
211
+ enclosingNode,
212
+ scopes,
213
+ tsPoint,
214
+ depth,
215
+ );
234
216
 
235
217
  const enclosingRange = nodeToSourceRange(enclosingNode);
236
- const scopeName = extractScopeName(enclosingNode.type, enclosingNode.text);
218
+ const scopeName = extractScopeName(enclosingNode);
237
219
 
238
220
  return {
239
221
  kind: "success",
@@ -329,7 +311,8 @@ function containsPoint(
329
311
  // biome-ignore lint/complexity/useMaxParams: filtering needs captures + node context + depth
330
312
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: filtering combines scope containment, column-aware inner-scope exclusion, and dedup in one cohesive pass
331
313
  function filterCalleeCaptures(
332
- captures: Array<{ range: SourceRange; text: string }>,
314
+ captures: Array<{ nodeType: string; range: SourceRange; text: string }>,
315
+ grammarId: GrammarId,
333
316
  // biome-ignore lint/suspicious/noExplicitAny: tree-sitter SyntaxNode is complex
334
317
  enclosingNode: any,
335
318
  scopeTypes: ReadonlySet<string>,
@@ -382,7 +365,7 @@ function filterCalleeCaptures(
382
365
  if (isInInner) continue;
383
366
  }
384
367
 
385
- const name = capture.text.replace(/\s+/g, "").slice(0, 60);
368
+ const name = normalizeCallName(capture.text, grammarId, capture.nodeType);
386
369
  if (name.length === 0 || seen.has(name)) continue;
387
370
  seen.add(name);
388
371
 
@@ -5,6 +5,7 @@ import { nodeToRange } from "../coordinates.ts";
5
5
  import type { SyntaxNodeLike } from "../syntax-node.ts";
6
6
  import type { ExportRecord, TreeSitterResult } from "../types.ts";
7
7
  import type { TreeSitterRuntime } from "../worker/runtime.ts";
8
+ import { extractBindingIdentifiers } from "./js-binding-pattern.ts";
8
9
 
9
10
  /** Extract export records from a supported file. */
10
11
  export async function extractExports(
@@ -120,13 +121,9 @@ function extractLexicalDeclarationExports(
120
121
  ): void {
121
122
  for (const child of decl.children) {
122
123
  if (child.type !== "variable_declarator") continue;
123
- const nameNode = findNameNode(child);
124
- if (!nameNode) continue;
125
- exports.push({
126
- name: nameNode.text,
127
- kind: "variable",
128
- range: nodeToRange(child, source),
129
- });
124
+ const names = extractBindingIdentifiers(child.childForFieldName("name"));
125
+ const range = nodeToRange(child, source);
126
+ for (const name of names) exports.push({ name, kind: "variable", range });
130
127
  }
131
128
  }
132
129
 
@@ -139,6 +136,11 @@ function extractAmbientDeclarationExport(
139
136
  const nested = decl.children.find((child) => child.type !== "declare" && child.type !== ";");
140
137
  if (!nested) return;
141
138
 
139
+ if (nested.type === "lexical_declaration") {
140
+ extractLexicalDeclarationExports(nested, source, exports);
141
+ return;
142
+ }
143
+
142
144
  const name = getDeclarationName(nested);
143
145
  if (!name) return;
144
146
 
@@ -0,0 +1,26 @@
1
+ import type { SyntaxNodeLike } from "../syntax-node.ts";
2
+
3
+ /** Extract every identifier bound by one JavaScript or TypeScript pattern. */
4
+ export function extractBindingIdentifiers(pattern: SyntaxNodeLike | null): string[] {
5
+ if (!pattern) return [];
6
+
7
+ switch (pattern.type) {
8
+ case "identifier":
9
+ return [pattern.text];
10
+ case "shorthand_property_identifier_pattern":
11
+ return [pattern.text];
12
+ case "pair_pattern":
13
+ case "pair":
14
+ return extractBindingIdentifiers(pattern.childForFieldName("value"));
15
+ case "object_assignment_pattern":
16
+ case "assignment_pattern":
17
+ return extractBindingIdentifiers(pattern.childForFieldName("left"));
18
+ case "rest_pattern":
19
+ return pattern.children.flatMap((child) => extractBindingIdentifiers(child));
20
+ case "object_pattern":
21
+ case "array_pattern":
22
+ return pattern.children.flatMap((child) => extractBindingIdentifiers(child));
23
+ default:
24
+ return [];
25
+ }
26
+ }
@@ -1,7 +1,7 @@
1
1
  // Node-at-position lookup.
2
2
 
3
3
  import type { CodeRequestControl } from "@mrclrchtr/supi-code-runtime/api";
4
- import { nodeToRange, publicToTreeSitter, splitSourceLines } from "../coordinates.ts";
4
+ import { nodeToRange, publicToTreeSitter, validatePublicPositionBounds } from "../coordinates.ts";
5
5
  import type { NodeAtResult, SourceRange, TreeSitterResult } from "../types.ts";
6
6
  import type { TreeSitterRuntime } from "../worker/runtime.ts";
7
7
 
@@ -28,7 +28,7 @@ export async function lookupNodeAt(
28
28
  const { tree, source } = parseResult.data;
29
29
 
30
30
  try {
31
- const boundsError = validateBounds(line, character, source);
31
+ const boundsError = validatePublicPositionBounds(line, character, source);
32
32
  if (boundsError) return boundsError;
33
33
 
34
34
  const tsPoint = publicToTreeSitter(line, character, source);
@@ -52,25 +52,6 @@ export async function lookupNodeAt(
52
52
  }
53
53
  }
54
54
 
55
- /** Validate that a requested public position exists in the source text. */
56
- function validateBounds(
57
- line: number,
58
- character: number,
59
- source: string,
60
- ): TreeSitterResult<NodeAtResult> | null {
61
- const lines = splitSourceLines(source);
62
- if (line > lines.length) {
63
- return { kind: "validation-error", message: "line is beyond end of file" };
64
- }
65
-
66
- const lineText = lines[line - 1] ?? "";
67
- if (character > lineText.length + 1) {
68
- return { kind: "validation-error", message: "character is beyond end of line" };
69
- }
70
-
71
- return null;
72
- }
73
-
74
55
  function collectAncestry(
75
56
  node: {
76
57
  type: string;
@@ -80,7 +80,7 @@ function sqlType(node: SyntaxNodeLike, source: string): OutlineItem | null {
80
80
  ? enumElements.children
81
81
  .filter((child) => child.type === "literal")
82
82
  .map((child) => ({
83
- name: stripSqlQuotes(child.text),
83
+ name: sqlStringLiteralValue(child.text),
84
84
  kind: "enum-member",
85
85
  range: nodeToRange(child, source),
86
86
  }))
@@ -112,10 +112,12 @@ function sqlColumns(node: SyntaxNodeLike, source: string): OutlineItem[] {
112
112
  function sqlInlineConstraints(node: SyntaxNodeLike, source: string): OutlineItem[] {
113
113
  const marker = node.children.findIndex((child) => child.type === "keyword_constraint");
114
114
  if (marker < 0) return [];
115
- const name = node.children.slice(marker + 1).find((child) => child.type === "literal");
116
- return name
117
- ? [{ name: stripSqlQuotes(name.text), kind: "constraint", range: nodeToRange(name, source) }]
118
- : [];
115
+ const nameNode = node.children
116
+ .slice(marker + 1)
117
+ .find((child) => child.type === "literal" || child.type === "identifier");
118
+ const name = readSqlNameAfterKeyword(node.text, "CONSTRAINT");
119
+ if (!name || !nameNode) return [];
120
+ return [{ name, kind: "constraint", range: nodeToRange(nameNode, source) }];
119
121
  }
120
122
 
121
123
  function sqlSchemaChildren(node: SyntaxNodeLike, source: string): OutlineItem[] {
@@ -139,21 +141,29 @@ function sqlItem(
139
141
  }
140
142
 
141
143
  function sqlMember(node: SyntaxNodeLike, kind: string, source: string): OutlineItem | null {
144
+ const textName =
145
+ kind === "constraint"
146
+ ? readSqlNameAfterKeyword(node.text, "CONSTRAINT")
147
+ : readSqlIdentifier(node.text);
142
148
  const name = node.childForFieldName("name") ?? directChild(node, "identifier");
143
- return name ? { name: stripSqlQuotes(name.text), kind, range: nodeToRange(node, source) } : null;
149
+ const value = textName ?? (name ? normalizeSqlIdentifierLike(name.text) : null);
150
+ return value ? { name: value, kind, range: nodeToRange(node, source) } : null;
144
151
  }
145
152
 
146
153
  function sqlDeclarationName(node: SyntaxNodeLike): string | null {
147
- if (node.type === "create_index") {
148
- const indexName = node.childForFieldName("column");
149
- return indexName ? stripSqlQuotes(indexName.text) : null;
154
+ const keyword = SQL_DECLARATION_KEYWORDS[node.type];
155
+ if (keyword) {
156
+ const name = readSqlNameAfterKeyword(node.text, keyword);
157
+ if (name) return name;
150
158
  }
159
+
151
160
  const reference = directChild(node, "object_reference");
152
161
  if (reference) {
153
162
  const name = reference.childForFieldName("name") ?? lastDirectChild(reference, "identifier");
154
- if (name) return name.text;
163
+ if (name) return normalizeSqlIdentifierLike(name.text);
155
164
  }
156
- return directChild(node, "identifier")?.text ?? null;
165
+ const name = directChild(node, "identifier");
166
+ return name ? normalizeSqlIdentifierLike(name.text) : null;
157
167
  }
158
168
 
159
169
  function directChild(node: SyntaxNodeLike, type: string): SyntaxNodeLike | null {
@@ -168,12 +178,119 @@ function lastDirectChild(node: SyntaxNodeLike, type: string): SyntaxNodeLike | n
168
178
  return null;
169
179
  }
170
180
 
171
- function stripSqlQuotes(value: string): string {
181
+ function normalizeSqlIdentifierLike(value: string): string {
182
+ if (value.startsWith('"') && value.endsWith('"')) {
183
+ return value.slice(1, -1).replaceAll('""', '"');
184
+ }
185
+ if (value.startsWith("'") && value.endsWith("'")) {
186
+ return sqlStringLiteralValue(value);
187
+ }
188
+ return value;
189
+ }
190
+
191
+ function sqlStringLiteralValue(value: string): string {
172
192
  return value.startsWith("'") && value.endsWith("'")
173
193
  ? value.slice(1, -1).replaceAll("''", "'")
174
194
  : value;
175
195
  }
176
196
 
197
+ function readSqlNameAfterKeyword(text: string, keyword: string): string | null {
198
+ const marker = findSqlKeyword(text, keyword);
199
+ if (marker < 0) return null;
200
+ let position = skipSqlSpace(text, marker + keyword.length);
201
+ for (const optional of ["CONCURRENTLY", "IF", "NOT", "EXISTS"]) {
202
+ if (readSqlWord(text, position) === optional) {
203
+ position = skipSqlSpace(text, position + optional.length);
204
+ }
205
+ }
206
+ const name = readSqlQualifiedIdentifier(text, position);
207
+ return name ? normalizeSqlIdentifierLike(name) : null;
208
+ }
209
+
210
+ function readSqlIdentifier(text: string): string | null {
211
+ const name = readSqlQualifiedIdentifier(text, 0);
212
+ return name ? normalizeSqlIdentifierLike(name) : null;
213
+ }
214
+
215
+ function readSqlQualifiedIdentifier(text: string, start: number): string | null {
216
+ let position = skipSqlSpace(text, start);
217
+ let last: string | null = null;
218
+ while (position < text.length) {
219
+ const token = readSqlIdentifierToken(text, position);
220
+ if (!token) break;
221
+ last = token.value;
222
+ position = skipSqlSpace(text, token.end);
223
+ if (text[position] !== ".") break;
224
+ position = skipSqlSpace(text, position + 1);
225
+ }
226
+ return last;
227
+ }
228
+
229
+ function readSqlIdentifierToken(
230
+ text: string,
231
+ start: number,
232
+ ): { value: string; end: number } | null {
233
+ if (text[start] === '"' || text[start] === "'") {
234
+ const quote = text[start];
235
+ let position = start + 1;
236
+ while (position < text.length) {
237
+ if (text[position] !== quote) {
238
+ position += 1;
239
+ continue;
240
+ }
241
+ if (text[position + 1] === quote) {
242
+ position += 2;
243
+ continue;
244
+ }
245
+ position += 1;
246
+ return { value: text.slice(start, position), end: position };
247
+ }
248
+ return null;
249
+ }
250
+
251
+ let position = start;
252
+ while (position < text.length && !/[\s(;.,]/.test(text[position] ?? "")) position += 1;
253
+ return position > start ? { value: text.slice(start, position), end: position } : null;
254
+ }
255
+
256
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: quoted SQL names need a small lexical scan before keyword recovery
257
+ function findSqlKeyword(text: string, keyword: string): number {
258
+ const upper = text.toUpperCase();
259
+ let quote: '"' | "'" | null = null;
260
+ for (let position = 0; position < text.length; position += 1) {
261
+ const character = text[position];
262
+ if (quote !== null) {
263
+ if (character === quote && text[position + 1] === quote) {
264
+ position += 1;
265
+ } else if (character === quote) {
266
+ quote = null;
267
+ }
268
+ continue;
269
+ }
270
+ if (character === '"' || character === "'") {
271
+ quote = character;
272
+ continue;
273
+ }
274
+ if (!upper.startsWith(keyword, position)) continue;
275
+ const before = upper[position - 1] ?? " ";
276
+ const after = upper[position + keyword.length] ?? " ";
277
+ if (!/[A-Z0-9_]/.test(before) && !/[A-Z0-9_]/.test(after)) return position;
278
+ }
279
+ return -1;
280
+ }
281
+
282
+ function readSqlWord(text: string, start: number): string | null {
283
+ const end = text.slice(start).search(/\s/);
284
+ const wordEnd = end < 0 ? text.length : start + end;
285
+ return wordEnd > start ? text.slice(start, wordEnd).toUpperCase() : null;
286
+ }
287
+
288
+ function skipSqlSpace(text: string, start: number): number {
289
+ let position = start;
290
+ while (/\s/.test(text[position] ?? "")) position += 1;
291
+ return position;
292
+ }
293
+
177
294
  function one(item: OutlineItem | null): OutlineItem[] {
178
295
  return item ? [item] : [];
179
296
  }
@@ -184,3 +301,18 @@ const HTML_ELEMENT_TYPES = new Set([
184
301
  "self_closing_tag",
185
302
  "style_element",
186
303
  ]);
304
+
305
+ const SQL_DECLARATION_KEYWORDS: Record<string, string> = {
306
+ create_schema: "SCHEMA",
307
+ create_type: "TYPE",
308
+ create_table: "TABLE",
309
+ create_view: "VIEW",
310
+ create_materialized_view: "VIEW",
311
+ create_index: "INDEX",
312
+ create_sequence: "SEQUENCE",
313
+ create_function: "FUNCTION",
314
+ create_trigger: "TRIGGER",
315
+ create_database: "DATABASE",
316
+ create_role: "ROLE",
317
+ create_extension: "EXTENSION",
318
+ };
@@ -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 { extractBindingIdentifiers } from "./js-binding-pattern.ts";
6
7
  import { extractPolyglotOutlineItems } from "./outline-polyglot.ts";
7
8
 
8
9
  /** Node types that can be extracted directly as outline items. */
@@ -18,6 +19,7 @@ const OUTLINE_DECLARATION_NODE_TYPES = new Set([
18
19
  "method_definition",
19
20
  "public_field_definition",
20
21
  "variable_declarator",
22
+ "lexical_declaration",
21
23
  "ambient_declaration",
22
24
  "internal_module",
23
25
  "module",
@@ -39,20 +41,27 @@ function collectItems(node: SyntaxNodeLike, source: string): OutlineItem[] {
39
41
  continue;
40
42
  }
41
43
 
42
- const item = extractItem(child, source);
43
- if (item) items.push(item);
44
+ const extractedItems = extractItems(child, source);
45
+ if (extractedItems) items.push(...extractedItems);
44
46
  else items.push(...collectItems(child, source));
45
47
  }
46
48
 
47
49
  return items;
48
50
  }
49
51
 
52
+ function extractItems(node: SyntaxNodeLike, source: string): OutlineItem[] | null {
53
+ if (node.type === "lexical_declaration") return extractLexicalDeclarationItems(node, source);
54
+ if (node.type === "ambient_declaration") return extractAmbientDeclarationItems(node, source);
55
+ if (node.type === "export_statement") return extractExportStatement(node, source);
56
+
57
+ const item = extractItem(node, source);
58
+ return item ? [item] : null;
59
+ }
60
+
50
61
  function extractItem(node: SyntaxNodeLike, source: string): OutlineItem | null {
51
62
  switch (node.type) {
52
- case "export_statement":
53
- return extractExportStatement(node, source);
54
63
  case "lexical_declaration":
55
- return extractLexicalDeclaration(node, source);
64
+ return extractLexicalDeclarationItems(node, source)[0] ?? null;
56
65
  case "function_declaration":
57
66
  case "generator_function_declaration":
58
67
  case "function_signature":
@@ -72,7 +81,7 @@ function extractItem(node: SyntaxNodeLike, source: string): OutlineItem | null {
72
81
  case "public_field_definition":
73
82
  return extractFieldDefinition(node, source);
74
83
  case "variable_declarator":
75
- return extractVariableDeclarator(node, source);
84
+ return extractVariableDeclaratorItems(node, source)[0] ?? null;
76
85
  case "ambient_declaration":
77
86
  return extractAmbientDeclaration(node, source);
78
87
  case "internal_module":
@@ -83,38 +92,36 @@ function extractItem(node: SyntaxNodeLike, source: string): OutlineItem | null {
83
92
  }
84
93
  }
85
94
 
86
- /** Extract an outline item from an export wrapper without exposing non-extractable syntax nodes. */
87
- function extractExportStatement(node: SyntaxNodeLike, source: string): OutlineItem | null {
95
+ /** Extract outline items from an export wrapper without exposing local syntax nodes. */
96
+ function extractExportStatement(node: SyntaxNodeLike, source: string): OutlineItem[] {
88
97
  const decl = node.children.find((child) => OUTLINE_DECLARATION_NODE_TYPES.has(child.type));
89
98
  if (decl) {
99
+ if (decl.type === "lexical_declaration") return extractLexicalDeclarationItems(decl, source);
100
+ if (decl.type === "ambient_declaration") return extractAmbientDeclarationItems(decl, source);
90
101
  const item = extractItem(decl, source);
91
- if (item) return item;
102
+ if (item) return [item];
92
103
  }
93
104
 
94
105
  if (hasDefaultKeyword(node)) {
95
- return {
96
- name: "default",
97
- kind: "export",
98
- range: nodeToRange(node, source),
99
- };
106
+ return [{ name: "default", kind: "export", range: nodeToRange(node, source) }];
100
107
  }
101
108
 
102
109
  const exportClause = node.children.find((child) => child.type === "export_clause");
103
- if (exportClause) {
104
- return {
105
- name: node.text.replace(/^export\s+/, "").substring(0, 60),
106
- kind: "export",
107
- range: nodeToRange(node, source),
108
- };
109
- }
110
-
111
- return null;
110
+ return exportClause
111
+ ? [
112
+ {
113
+ name: node.text.replace(/^export\s+/, "").substring(0, 60),
114
+ kind: "export",
115
+ range: nodeToRange(node, source),
116
+ },
117
+ ]
118
+ : [];
112
119
  }
113
120
 
114
- function extractLexicalDeclaration(node: SyntaxNodeLike, source: string): OutlineItem | null {
115
- const declarators = node.children.filter((child) => child.type === "variable_declarator");
116
- if (declarators.length !== 1) return null;
117
- return extractVariableDeclarator(declarators[0] as SyntaxNodeLike, source);
121
+ function extractLexicalDeclarationItems(node: SyntaxNodeLike, source: string): OutlineItem[] {
122
+ return node.children
123
+ .filter((child) => child.type === "variable_declarator")
124
+ .flatMap((declarator) => extractVariableDeclaratorItems(declarator, source));
118
125
  }
119
126
 
120
127
  function extractNamedDeclaration(
@@ -154,14 +161,11 @@ function extractEnumDeclaration(node: SyntaxNodeLike, source: string): OutlineIt
154
161
  return { ...item, children: collectEnumMembers(node, source) };
155
162
  }
156
163
 
157
- function extractVariableDeclarator(node: SyntaxNodeLike, source: string): OutlineItem | null {
158
- const nameNode = findNameNode(node);
159
- if (!nameNode) return null;
160
- return {
161
- name: nameNode.text,
162
- kind: detectKind(node),
163
- range: nodeToRange(node, source),
164
- };
164
+ function extractVariableDeclaratorItems(node: SyntaxNodeLike, source: string): OutlineItem[] {
165
+ const names = extractBindingIdentifiers(node.childForFieldName("name"));
166
+ const kind = detectKind(node);
167
+ const range = nodeToRange(node, source);
168
+ return names.map((name) => ({ name, kind, range }));
165
169
  }
166
170
 
167
171
  function extractFieldDefinition(node: SyntaxNodeLike, source: string): OutlineItem | null {
@@ -174,9 +178,18 @@ function extractFieldDefinition(node: SyntaxNodeLike, source: string): OutlineIt
174
178
  };
175
179
  }
176
180
 
177
- function extractAmbientDeclaration(node: SyntaxNodeLike, source: string): OutlineItem | null {
181
+ function extractAmbientDeclarationItems(node: SyntaxNodeLike, source: string): OutlineItem[] {
178
182
  const declaration = node.children.find((child) => OUTLINE_DECLARATION_NODE_TYPES.has(child.type));
179
- return declaration ? extractItem(declaration, source) : null;
183
+ if (!declaration) return [];
184
+ if (declaration.type === "lexical_declaration") {
185
+ return extractLexicalDeclarationItems(declaration, source);
186
+ }
187
+ const item = extractItem(declaration, source);
188
+ return item ? [item] : [];
189
+ }
190
+
191
+ function extractAmbientDeclaration(node: SyntaxNodeLike, source: string): OutlineItem | null {
192
+ return extractAmbientDeclarationItems(node, source)[0] ?? null;
180
193
  }
181
194
 
182
195
  function extractModuleDeclaration(node: SyntaxNodeLike, source: string): OutlineItem | null {