@elyracode/lsp-rust 0.9.19 → 0.9.20

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.20] - 2026-07-19
4
+
5
+ ### Added
6
+ - Auto-diagnostics and blast radius on edit: after an `edit`, the extension now also reports fresh Rust compiler errors for the edited file and lists call sites of the edited function/class in other files (via `textDocument/documentSymbol` + `references`), alongside the existing related-symbol summaries
7
+
3
8
  ## [0.9.19] - 2026-07-18
4
9
 
5
10
  ## [0.9.18] - 2026-07-18
@@ -176,6 +176,82 @@ function summarizeHover(result: unknown): string | undefined {
176
176
  return firstLine.length > 160 ? `${firstLine.slice(0, 160)}…` : firstLine;
177
177
  }
178
178
 
179
+ // LSP SymbolKind values that can meaningfully own a blast radius:
180
+ // Class, Method, Constructor, Enum, Interface, Function, Variable, Constant, Struct.
181
+ const BLAST_SYMBOL_KINDS = new Set([5, 6, 9, 10, 11, 12, 13, 14, 23]);
182
+
183
+ interface FlatSymbol {
184
+ name: string;
185
+ kind: number;
186
+ startLine: number;
187
+ endLine: number;
188
+ depth: number;
189
+ /** 0-based position of the symbol's name (for references lookups). */
190
+ selLine: number;
191
+ selChar: number;
192
+ }
193
+
194
+ /** Flatten a documentSymbol response (hierarchical DocumentSymbol[] or flat SymbolInformation[]). */
195
+ function flattenSymbols(result: unknown): FlatSymbol[] {
196
+ if (!Array.isArray(result)) return [];
197
+ const out: FlatSymbol[] = [];
198
+ const walk = (nodes: unknown[], depth: number): void => {
199
+ for (const n of nodes) {
200
+ if (!n || typeof n !== "object") continue;
201
+ const sym = n as {
202
+ name?: unknown;
203
+ kind?: unknown;
204
+ range?: LspRange;
205
+ selectionRange?: LspRange;
206
+ location?: { range?: LspRange };
207
+ children?: unknown[];
208
+ };
209
+ const range = sym.range ?? sym.location?.range;
210
+ if (typeof sym.name === "string" && typeof sym.kind === "number" && range) {
211
+ const sel = sym.selectionRange ?? range;
212
+ out.push({
213
+ name: sym.name,
214
+ kind: sym.kind,
215
+ startLine: range.start.line,
216
+ endLine: range.end.line,
217
+ depth,
218
+ selLine: sel.start.line,
219
+ selChar: sel.start.character,
220
+ });
221
+ }
222
+ if (Array.isArray(sym.children)) walk(sym.children, depth + 1);
223
+ }
224
+ };
225
+ walk(result, 0);
226
+ return out;
227
+ }
228
+
229
+ /** Find the innermost symbol (by line span, then nesting depth) containing a 0-based line. */
230
+ function findEnclosingSymbol(symbols: FlatSymbol[], line0: number): FlatSymbol | undefined {
231
+ let best: FlatSymbol | undefined;
232
+ for (const s of symbols) {
233
+ if (!BLAST_SYMBOL_KINDS.has(s.kind)) continue;
234
+ if (line0 < s.startLine || line0 > s.endLine) continue;
235
+ if (!best) {
236
+ best = s;
237
+ continue;
238
+ }
239
+ const bestSpan = best.endLine - best.startLine;
240
+ const span = s.endLine - s.startLine;
241
+ if (span < bestSpan || (span === bestSpan && s.depth > best.depth)) best = s;
242
+ }
243
+ return best;
244
+ }
245
+
246
+ /** True when the edit tool's own result already contains a diagnostics section. */
247
+ function hasExistingDiagnostics(content: ReadonlyArray<unknown>): boolean {
248
+ return content.some((c) => {
249
+ if (!c || typeof c !== "object") return false;
250
+ const item = c as { type?: unknown; text?: unknown };
251
+ return item.type === "text" && typeof item.text === "string" && item.text.includes("\nDiagnostics (");
252
+ });
253
+ }
254
+
179
255
  // ── Extension ───────────────────────────────────────────────────────────────
180
256
 
181
257
  export default function (elyra: ExtensionAPI): void {
@@ -187,6 +263,7 @@ export default function (elyra: ExtensionAPI): void {
187
263
  let cwd = "";
188
264
  const openedFiles = new Set<string>();
189
265
  const diagnosticsByUri = new Map<string, LspDiagnostic[]>();
266
+ const diagnosticsWaiters = new Map<string, Array<() => void>>();
190
267
 
191
268
  // ── JSON-RPC Client ─────────────────────────────────────────────────
192
269
 
@@ -275,6 +352,11 @@ export default function (elyra: ExtensionAPI): void {
275
352
  if (msg.method === "textDocument/publishDiagnostics" && msg.params) {
276
353
  const p = msg.params as { uri: string; diagnostics: LspDiagnostic[] };
277
354
  diagnosticsByUri.set(p.uri, p.diagnostics);
355
+ const waiters = diagnosticsWaiters.get(p.uri);
356
+ if (waiters) {
357
+ diagnosticsWaiters.delete(p.uri);
358
+ for (const w of waiters) w();
359
+ }
278
360
  }
279
361
  }
280
362
  }
@@ -316,6 +398,28 @@ export default function (elyra: ExtensionAPI): void {
316
398
  openFile(filePath);
317
399
  }
318
400
 
401
+ /** Resolve when the server publishes diagnostics for `uri`, or after `timeoutMs`. */
402
+ function waitForDiagnostics(uri: string, timeoutMs: number): Promise<void> {
403
+ return new Promise<void>((resolveWait) => {
404
+ const waiter = () => {
405
+ clearTimeout(timer);
406
+ resolveWait();
407
+ };
408
+ const timer = setTimeout(() => {
409
+ const arr = diagnosticsWaiters.get(uri);
410
+ if (arr) {
411
+ const remaining = arr.filter((w) => w !== waiter);
412
+ if (remaining.length > 0) diagnosticsWaiters.set(uri, remaining);
413
+ else diagnosticsWaiters.delete(uri);
414
+ }
415
+ resolveWait();
416
+ }, timeoutMs);
417
+ const list = diagnosticsWaiters.get(uri) ?? [];
418
+ list.push(waiter);
419
+ diagnosticsWaiters.set(uri, list);
420
+ });
421
+ }
422
+
319
423
  function formatLocations(result: unknown, workingDir: string): string {
320
424
  if (!result) return "No results found.";
321
425
 
@@ -540,7 +644,7 @@ export default function (elyra: ExtensionAPI): void {
540
644
  },
541
645
  });
542
646
 
543
- // ── Hook: auto-context on edit (see "Symbol-aware auto-context" above) ──
647
+ // ── Hook: auto-context, auto-diagnostics, and blast radius on edit ──
544
648
  elyra.on("tool_result", async (event) => {
545
649
  if (!initialized || !isEditToolResult(event) || event.isError) return undefined;
546
650
 
@@ -554,14 +658,14 @@ export default function (elyra: ExtensionAPI): void {
554
658
  const fileText = readFileSync(absPath, "utf-8");
555
659
  const details = event.details as EditToolDetails | undefined;
556
660
  const fromLine = details?.firstChangedLine ?? 1;
661
+ const uri = fileUri(absPath);
662
+ const sections: string[] = [];
557
663
 
664
+ // 1. Related symbols: hover summaries for referenced types.
558
665
  const candidates = new Set<string>();
559
666
  for (const edit of input.edits) {
560
667
  for (const s of extractCandidateSymbols(edit.newText, RUST_SYMBOL_STOPLIST, 4)) candidates.add(s);
561
668
  }
562
- if (candidates.size === 0) return undefined;
563
-
564
- const uri = fileUri(absPath);
565
669
  const lookups = [...candidates].slice(0, 5).map(async (symbol) => {
566
670
  const pos = findSymbolPosition(fileText, symbol, fromLine);
567
671
  if (!pos) return undefined;
@@ -577,10 +681,67 @@ export default function (elyra: ExtensionAPI): void {
577
681
  }
578
682
  });
579
683
 
580
- const lines = (await Promise.all(lookups)).filter((l): l is string => !!l);
581
- if (lines.length === 0) return undefined;
684
+ // 2. Blast radius: references to the edited symbol outside this file.
685
+ const blastRadius = (async () => {
686
+ try {
687
+ const symbolResult = await sendRequest("textDocument/documentSymbol", {
688
+ textDocument: { uri },
689
+ });
690
+ const enclosing = findEnclosingSymbol(flattenSymbols(symbolResult), fromLine - 1);
691
+ if (!enclosing) return undefined;
692
+
693
+ const refsResult = await sendRequest("textDocument/references", {
694
+ textDocument: { uri },
695
+ position: { line: enclosing.selLine, character: enclosing.selChar },
696
+ context: { includeDeclaration: false },
697
+ });
698
+ if (!Array.isArray(refsResult)) return undefined;
699
+
700
+ const external = (refsResult as LspLocation[]).filter((loc) => uriToPath(loc.uri) !== absPath);
701
+ if (external.length === 0) return undefined;
702
+
703
+ const prefix = `${cwd}/`;
704
+ const shown = external.slice(0, 8).map((loc) => {
705
+ const p = uriToPath(loc.uri);
706
+ const rel = p.startsWith(prefix) ? p.slice(prefix.length) : p;
707
+ return `${rel}:${loc.range.start.line + 1}`;
708
+ });
709
+ const more = external.length > shown.length ? ` (+${external.length - shown.length} more)` : "";
710
+ return `${enclosing.name} is referenced in ${external.length} place${external.length === 1 ? "" : "s"} outside this file:\n${shown.map((s) => `- ${s}`).join("\n")}${more}`;
711
+ } catch {
712
+ return undefined;
713
+ }
714
+ })();
715
+
716
+ // 3. Fresh diagnostics for the edited file (skip if the edit tool already reported diagnostics).
717
+ const freshDiagnostics = (async () => {
718
+ if (hasExistingDiagnostics(event.content)) return undefined;
719
+ try {
720
+ await waitForDiagnostics(uri, 2500);
721
+ const errors = (diagnosticsByUri.get(uri) ?? []).filter((d) => (d.severity ?? 1) === 1);
722
+ if (errors.length === 0) return undefined;
723
+ const shown = errors.slice(0, 8).map((d) => {
724
+ const code = d.code ? ` [${d.code}]` : "";
725
+ return `- ${input.path}:${d.range.start.line + 1}:${d.range.start.character + 1}${code}: ${d.message.split("\n")[0]}`;
726
+ });
727
+ const more = errors.length > shown.length ? ` (+${errors.length - shown.length} more)` : "";
728
+ return `${errors.length} error${errors.length === 1 ? "" : "s"} in this file after the edit:\n${shown.join("\n")}${more}`;
729
+ } catch {
730
+ return undefined;
731
+ }
732
+ })();
733
+
734
+ const [hoverLines, blast, diag] = await Promise.all([Promise.all(lookups), blastRadius, freshDiagnostics]);
735
+
736
+ const related = hoverLines.filter((l): l is string => !!l);
737
+ if (diag) sections.push(`[Diagnostics — auto-checked via LSP]\n${diag}`);
738
+ if (blast) sections.push(`[Blast radius — auto-resolved via LSP]\n${blast}`);
739
+ if (related.length > 0) {
740
+ sections.push(`[Related symbols — auto-resolved via LSP, no extra tool call needed]\n${related.join("\n")}`);
741
+ }
742
+ if (sections.length === 0) return undefined;
582
743
 
583
- const contextBlock = `\n\n[Related symbols — auto-resolved via LSP, no extra tool call needed]\n${lines.join("\n")}`;
744
+ const contextBlock = `\n\n${sections.join("\n\n")}`;
584
745
  return { content: [...event.content, { type: "text" as const, text: contextBlock }] };
585
746
  } catch {
586
747
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elyracode/lsp-rust",
3
- "version": "0.9.19",
3
+ "version": "0.9.20",
4
4
  "description": "Rust LSP integration for Elyra — semantic code navigation and diagnostics via rust-analyzer",
5
5
  "type": "module",
6
6
  "keywords": [