@elyracode/lsp-php 0.9.17 → 0.9.19
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 +144 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.19] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
## [0.9.18] - 2026-07-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Proactive symbol-aware auto-context: after an `edit` tool call, referenced class/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
|
|
9
|
+
|
|
3
10
|
## [0.9.17] - 2026-07-18
|
|
4
11
|
|
|
5
12
|
## [0.9.16] - 2026-07-15
|
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,82 @@ function positionFromLineCol(line: number, col: number): LspPosition {
|
|
|
67
72
|
return { line: line - 1, character: col - 1 };
|
|
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 class, is this the shape I think it is".
|
|
80
|
+
|
|
81
|
+
/** Common built-ins that rarely need an explanation. */
|
|
82
|
+
const PHP_SYMBOL_STOPLIST = new Set([
|
|
83
|
+
"Exception",
|
|
84
|
+
"Error",
|
|
85
|
+
"Throwable",
|
|
86
|
+
"ArrayObject",
|
|
87
|
+
"ArrayAccess",
|
|
88
|
+
"Closure",
|
|
89
|
+
"Generator",
|
|
90
|
+
"Iterator",
|
|
91
|
+
"IteratorAggregate",
|
|
92
|
+
"Countable",
|
|
93
|
+
"Traversable",
|
|
94
|
+
"JsonSerializable",
|
|
95
|
+
"Stringable",
|
|
96
|
+
"DateTime",
|
|
97
|
+
"DateTimeImmutable",
|
|
98
|
+
"DateInterval",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
/** Extract candidate PascalCase class/interface identifiers from a snippet of code. */
|
|
102
|
+
function extractCandidateSymbols(text: string, stoplist: Set<string>, max: number): string[] {
|
|
103
|
+
const found = new Set<string>();
|
|
104
|
+
const re = /\b[A-Z][A-Za-z0-9_]*\b/g;
|
|
105
|
+
let m: RegExpExecArray | null = re.exec(text);
|
|
106
|
+
while (m && found.size < max * 3) {
|
|
107
|
+
if (!stoplist.has(m[0])) found.add(m[0]);
|
|
108
|
+
m = re.exec(text);
|
|
109
|
+
}
|
|
110
|
+
return [...found].slice(0, max);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Find the (1-based) line/column of the first whole-word match of `symbol`, searching from `fromLine` first. */
|
|
114
|
+
function findSymbolPosition(
|
|
115
|
+
fileText: string,
|
|
116
|
+
symbol: string,
|
|
117
|
+
fromLine: number,
|
|
118
|
+
): { line: number; column: number } | undefined {
|
|
119
|
+
const lines = fileText.split("\n");
|
|
120
|
+
const re = new RegExp(`\\b${symbol}\\b`);
|
|
121
|
+
const start = Math.max(0, fromLine - 1);
|
|
122
|
+
for (let i = start; i < lines.length; i++) {
|
|
123
|
+
const idx = lines[i].search(re);
|
|
124
|
+
if (idx >= 0) return { line: i + 1, column: idx + 1 };
|
|
125
|
+
}
|
|
126
|
+
for (let i = 0; i < start; i++) {
|
|
127
|
+
const idx = lines[i].search(re);
|
|
128
|
+
if (idx >= 0) return { line: i + 1, column: idx + 1 };
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Reduce a hover result to a short, single-line summary. */
|
|
134
|
+
function summarizeHover(result: unknown): string | undefined {
|
|
135
|
+
if (!result) return undefined;
|
|
136
|
+
const hover = result as LspHoverResult;
|
|
137
|
+
let text: string;
|
|
138
|
+
if (typeof hover.contents === "string") text = hover.contents;
|
|
139
|
+
else if (Array.isArray(hover.contents)) {
|
|
140
|
+
text = hover.contents.map((c) => (typeof c === "string" ? c : c.value)).join("\n");
|
|
141
|
+
} else text = hover.contents.value;
|
|
142
|
+
|
|
143
|
+
const firstLine = text
|
|
144
|
+
.split("\n")
|
|
145
|
+
.map((l) => l.trim())
|
|
146
|
+
.find((l) => l && !l.startsWith("```"));
|
|
147
|
+
if (!firstLine) return undefined;
|
|
148
|
+
return firstLine.length > 160 ? `${firstLine.slice(0, 160)}\u2026` : firstLine;
|
|
149
|
+
}
|
|
150
|
+
|
|
70
151
|
function whichBinary(name: string): string | undefined {
|
|
71
152
|
try {
|
|
72
153
|
const found = execSync(`which ${name}`, {
|
|
@@ -232,6 +313,17 @@ export default function (elyra: ExtensionAPI): void {
|
|
|
232
313
|
openedFiles.add(uri);
|
|
233
314
|
}
|
|
234
315
|
|
|
316
|
+
/** Force the server to see the current on-disk content (e.g. right after our own edit). */
|
|
317
|
+
function syncFile(filePath: string): void {
|
|
318
|
+
const absPath = resolve(cwd, filePath);
|
|
319
|
+
const uri = fileUri(absPath);
|
|
320
|
+
if (openedFiles.has(uri)) {
|
|
321
|
+
sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
322
|
+
openedFiles.delete(uri);
|
|
323
|
+
}
|
|
324
|
+
openFile(filePath);
|
|
325
|
+
}
|
|
326
|
+
|
|
235
327
|
function formatLocations(result: unknown, workingDir: string): string {
|
|
236
328
|
if (!result) return "No results found.";
|
|
237
329
|
|
|
@@ -452,13 +544,60 @@ export default function (elyra: ExtensionAPI): void {
|
|
|
452
544
|
const message = err instanceof Error ? err.message : String(err);
|
|
453
545
|
return { content: [{ type: "text", text: `Error: ${message}` }], details: {} };
|
|
454
546
|
}
|
|
455
|
-
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// ── Hook: auto-context on edit (see "Symbol-aware auto-context" above) ──
|
|
551
|
+
elyra.on("tool_result", async (event) => {
|
|
552
|
+
if (!initialized || !isEditToolResult(event) || event.isError) return undefined;
|
|
553
|
+
|
|
554
|
+
const input = event.input as unknown as EditToolInput;
|
|
555
|
+
if (typeof input?.path !== "string" || !Array.isArray(input.edits)) return undefined;
|
|
556
|
+
if (!input.path.endsWith(".php")) return undefined;
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
syncFile(input.path);
|
|
560
|
+
const absPath = resolve(cwd, input.path);
|
|
561
|
+
const fileText = readFileSync(absPath, "utf-8");
|
|
562
|
+
const details = event.details as EditToolDetails | undefined;
|
|
563
|
+
const fromLine = details?.firstChangedLine ?? 1;
|
|
564
|
+
|
|
565
|
+
const candidates = new Set<string>();
|
|
566
|
+
for (const edit of input.edits) {
|
|
567
|
+
for (const s of extractCandidateSymbols(edit.newText, PHP_SYMBOL_STOPLIST, 4)) candidates.add(s);
|
|
568
|
+
}
|
|
569
|
+
if (candidates.size === 0) return undefined;
|
|
570
|
+
|
|
571
|
+
const uri = fileUri(absPath);
|
|
572
|
+
const lookups = [...candidates].slice(0, 5).map(async (symbol) => {
|
|
573
|
+
const pos = findSymbolPosition(fileText, symbol, fromLine);
|
|
574
|
+
if (!pos) return undefined;
|
|
575
|
+
try {
|
|
576
|
+
const result = await sendRequest("textDocument/hover", {
|
|
577
|
+
textDocument: { uri },
|
|
578
|
+
position: positionFromLineCol(pos.line, pos.column),
|
|
579
|
+
});
|
|
580
|
+
const summary = summarizeHover(result);
|
|
581
|
+
return summary ? `- ${symbol}: ${summary}` : undefined;
|
|
582
|
+
} catch {
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const lines = (await Promise.all(lookups)).filter((l): l is string => !!l);
|
|
588
|
+
if (lines.length === 0) return undefined;
|
|
589
|
+
|
|
590
|
+
const contextBlock = `\n\n[Related symbols \u2014 auto-resolved via LSP, no extra tool call needed]\n${lines.join("\n")}`;
|
|
591
|
+
return { content: [...event.content, { type: "text" as const, text: contextBlock }] };
|
|
592
|
+
} catch {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
});
|
|
456
596
|
});
|
|
457
|
-
});
|
|
458
597
|
|
|
459
|
-
|
|
598
|
+
// ── Shutdown ────────────────────────────────────────────
|
|
460
599
|
|
|
461
|
-
|
|
600
|
+
elyra.on("session_shutdown", async () => {
|
|
462
601
|
if (lspProcess && initialized) {
|
|
463
602
|
try {
|
|
464
603
|
await sendRequest("shutdown", null);
|