@elyracode/lsp-typescript 0.9.16 → 0.9.18
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 +7 -0
- package/extensions/index.ts +151 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.18] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Proactive symbol-aware auto-context: after an `edit` tool call, referenced type/interface identifiers in the edited code are resolved via `textDocument/hover` and appended to the tool result, so the agent sees relevant type information without a separate lookup round trip
|
|
7
|
+
|
|
8
|
+
## [0.9.17] - 2026-07-18
|
|
9
|
+
|
|
3
10
|
## [0.9.16] - 2026-07-15
|
|
4
11
|
|
|
5
12
|
## [0.9.15] - 2026-07-12
|
package/extensions/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type EditToolDetails,
|
|
3
|
+
type EditToolInput,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
isEditToolResult,
|
|
6
|
+
} from "@elyracode/coding-agent";
|
|
2
7
|
import { type ChildProcess, execSync, spawn } from "node:child_process";
|
|
3
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
9
|
import { join, resolve } from "node:path";
|
|
@@ -67,6 +72,92 @@ function languageIdForPath(filePath: string): string {
|
|
|
67
72
|
return "typescript";
|
|
68
73
|
}
|
|
69
74
|
|
|
75
|
+
// ── Symbol-aware auto-context ────────────────────────────────────────────
|
|
76
|
+
// After an edit, proactively resolve unfamiliar-looking type/class names
|
|
77
|
+
// referenced in the new code via hover, and append a short summary to the
|
|
78
|
+
// edit's own tool result. Saves a definitions/hover round trip for the
|
|
79
|
+
// common case of "I just referenced a type, is this the shape I think it is".
|
|
80
|
+
|
|
81
|
+
/** Common built-ins that rarely need an explanation. */
|
|
82
|
+
const TS_SYMBOL_STOPLIST = new Set([
|
|
83
|
+
"String",
|
|
84
|
+
"Number",
|
|
85
|
+
"Boolean",
|
|
86
|
+
"Array",
|
|
87
|
+
"Object",
|
|
88
|
+
"Promise",
|
|
89
|
+
"Error",
|
|
90
|
+
"Map",
|
|
91
|
+
"Set",
|
|
92
|
+
"Date",
|
|
93
|
+
"RegExp",
|
|
94
|
+
"JSON",
|
|
95
|
+
"Symbol",
|
|
96
|
+
"Math",
|
|
97
|
+
"Function",
|
|
98
|
+
"Record",
|
|
99
|
+
"Partial",
|
|
100
|
+
"Readonly",
|
|
101
|
+
"Pick",
|
|
102
|
+
"Omit",
|
|
103
|
+
"Required",
|
|
104
|
+
"Awaited",
|
|
105
|
+
"Buffer",
|
|
106
|
+
"ArrayBuffer",
|
|
107
|
+
"Uint8Array",
|
|
108
|
+
"Console",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
/** Extract candidate PascalCase type/class identifiers from a snippet of code. */
|
|
112
|
+
function extractCandidateSymbols(text: string, stoplist: Set<string>, max: number): string[] {
|
|
113
|
+
const found = new Set<string>();
|
|
114
|
+
const re = /\b[A-Z][A-Za-z0-9_]*\b/g;
|
|
115
|
+
let m: RegExpExecArray | null = re.exec(text);
|
|
116
|
+
while (m && found.size < max * 3) {
|
|
117
|
+
if (!stoplist.has(m[0])) found.add(m[0]);
|
|
118
|
+
m = re.exec(text);
|
|
119
|
+
}
|
|
120
|
+
return [...found].slice(0, max);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Find the (1-based) line/column of the first whole-word match of `symbol`, searching from `fromLine` first. */
|
|
124
|
+
function findSymbolPosition(
|
|
125
|
+
fileText: string,
|
|
126
|
+
symbol: string,
|
|
127
|
+
fromLine: number,
|
|
128
|
+
): { line: number; column: number } | undefined {
|
|
129
|
+
const lines = fileText.split("\n");
|
|
130
|
+
const re = new RegExp(`\\b${symbol}\\b`);
|
|
131
|
+
const start = Math.max(0, fromLine - 1);
|
|
132
|
+
for (let i = start; i < lines.length; i++) {
|
|
133
|
+
const idx = lines[i].search(re);
|
|
134
|
+
if (idx >= 0) return { line: i + 1, column: idx + 1 };
|
|
135
|
+
}
|
|
136
|
+
for (let i = 0; i < start; i++) {
|
|
137
|
+
const idx = lines[i].search(re);
|
|
138
|
+
if (idx >= 0) return { line: i + 1, column: idx + 1 };
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Reduce a hover result to a short, single-line summary. */
|
|
144
|
+
function summarizeHover(result: unknown): string | undefined {
|
|
145
|
+
if (!result) return undefined;
|
|
146
|
+
const hover = result as LspHoverResult;
|
|
147
|
+
let text: string;
|
|
148
|
+
if (typeof hover.contents === "string") text = hover.contents;
|
|
149
|
+
else if (Array.isArray(hover.contents)) {
|
|
150
|
+
text = hover.contents.map((c) => (typeof c === "string" ? c : c.value)).join("\n");
|
|
151
|
+
} else text = hover.contents.value;
|
|
152
|
+
|
|
153
|
+
const firstLine = text
|
|
154
|
+
.split("\n")
|
|
155
|
+
.map((l) => l.trim())
|
|
156
|
+
.find((l) => l && !l.startsWith("```"));
|
|
157
|
+
if (!firstLine) return undefined;
|
|
158
|
+
return firstLine.length > 160 ? `${firstLine.slice(0, 160)}\u2026` : firstLine;
|
|
159
|
+
}
|
|
160
|
+
|
|
70
161
|
function findBinary(workingDir: string): string | undefined {
|
|
71
162
|
const local = join(workingDir, "node_modules", ".bin", "typescript-language-server");
|
|
72
163
|
if (existsSync(local)) return local;
|
|
@@ -214,6 +305,17 @@ export default function (elyra: ExtensionAPI): void {
|
|
|
214
305
|
openedFiles.add(uri);
|
|
215
306
|
}
|
|
216
307
|
|
|
308
|
+
/** Force the server to see the current on-disk content (e.g. right after our own edit). */
|
|
309
|
+
function syncFile(filePath: string): void {
|
|
310
|
+
const absPath = resolve(cwd, filePath);
|
|
311
|
+
const uri = fileUri(absPath);
|
|
312
|
+
if (openedFiles.has(uri)) {
|
|
313
|
+
sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
314
|
+
openedFiles.delete(uri);
|
|
315
|
+
}
|
|
316
|
+
openFile(filePath);
|
|
317
|
+
}
|
|
318
|
+
|
|
217
319
|
function formatLocations(result: unknown, workingDir: string): string {
|
|
218
320
|
if (!result) return "No results found.";
|
|
219
321
|
|
|
@@ -432,9 +534,56 @@ export default function (elyra: ExtensionAPI): void {
|
|
|
432
534
|
}
|
|
433
535
|
},
|
|
434
536
|
});
|
|
537
|
+
|
|
538
|
+
// ── Hook: auto-context on edit (see "Symbol-aware auto-context" above) ──
|
|
539
|
+
elyra.on("tool_result", async (event) => {
|
|
540
|
+
if (!initialized || !isEditToolResult(event) || event.isError) return undefined;
|
|
541
|
+
|
|
542
|
+
const input = event.input as unknown as EditToolInput;
|
|
543
|
+
if (typeof input?.path !== "string" || !Array.isArray(input.edits)) return undefined;
|
|
544
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(input.path)) return undefined;
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
syncFile(input.path);
|
|
548
|
+
const absPath = resolve(cwd, input.path);
|
|
549
|
+
const fileText = readFileSync(absPath, "utf-8");
|
|
550
|
+
const details = event.details as EditToolDetails | undefined;
|
|
551
|
+
const fromLine = details?.firstChangedLine ?? 1;
|
|
552
|
+
|
|
553
|
+
const candidates = new Set<string>();
|
|
554
|
+
for (const edit of input.edits) {
|
|
555
|
+
for (const s of extractCandidateSymbols(edit.newText, TS_SYMBOL_STOPLIST, 4)) candidates.add(s);
|
|
556
|
+
}
|
|
557
|
+
if (candidates.size === 0) return undefined;
|
|
558
|
+
|
|
559
|
+
const uri = fileUri(absPath);
|
|
560
|
+
const lookups = [...candidates].slice(0, 5).map(async (symbol) => {
|
|
561
|
+
const pos = findSymbolPosition(fileText, symbol, fromLine);
|
|
562
|
+
if (!pos) return undefined;
|
|
563
|
+
try {
|
|
564
|
+
const result = await sendRequest("textDocument/hover", {
|
|
565
|
+
textDocument: { uri },
|
|
566
|
+
position: positionFromLineCol(pos.line, pos.column),
|
|
567
|
+
});
|
|
568
|
+
const summary = summarizeHover(result);
|
|
569
|
+
return summary ? `- ${symbol}: ${summary}` : undefined;
|
|
570
|
+
} catch {
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const lines = (await Promise.all(lookups)).filter((l): l is string => !!l);
|
|
576
|
+
if (lines.length === 0) return undefined;
|
|
577
|
+
|
|
578
|
+
const contextBlock = `\n\n[Related symbols \u2014 auto-resolved via LSP, no extra tool call needed]\n${lines.join("\n")}`;
|
|
579
|
+
return { content: [...event.content, { type: "text" as const, text: contextBlock }] };
|
|
580
|
+
} catch {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
});
|
|
435
584
|
});
|
|
436
585
|
|
|
437
|
-
// ── Shutdown
|
|
586
|
+
// ── Shutdown ────────────────────────────────────────────
|
|
438
587
|
|
|
439
588
|
elyra.on("session_shutdown", async () => {
|
|
440
589
|
if (lspProcess && initialized) {
|