@mrclrchtr/supi-lsp 1.16.1 → 2.0.1
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/README.md +44 -56
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +6 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +3 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +7 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +56 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/evidence-badge.ts +40 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +5 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +76 -3
- package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +37 -2
- package/node_modules/@mrclrchtr/supi-core/package.json +6 -1
- package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/package.json +4 -31
- package/src/api.ts +27 -0
- package/src/client/client-refresh.ts +5 -0
- package/src/client/client.ts +246 -1
- package/src/client/transport.ts +4 -0
- package/src/config/capabilities.ts +3 -0
- package/src/config/config.ts +4 -2
- package/src/config/lsp-settings.ts +102 -0
- package/src/config/server-actions.ts +59 -0
- package/src/config/server-config.ts +4 -0
- package/src/index.ts +11 -0
- package/src/manager/capability-index.ts +1 -1
- package/src/manager/manager-project-info.ts +2 -1
- package/src/manager/manager.ts +115 -26
- package/src/provider/lsp-semantic-provider.ts +177 -134
- package/src/provider/refactor-planning.ts +214 -0
- package/src/session/runtime-controller.ts +320 -0
- package/src/session/runtime-registration.ts +26 -5
- package/src/session/service-registry.ts +81 -4
- package/src/summary.ts +2 -0
- package/src/extension.ts +0 -1
- package/src/format.ts +0 -373
- package/src/handlers/diagnostic-injection.ts +0 -182
- package/src/handlers/session-lifecycle.ts +0 -149
- package/src/handlers/status-command.ts +0 -38
- package/src/handlers/workspace-recovery.ts +0 -92
- package/src/lsp.ts +0 -37
- package/src/session/lsp-state.ts +0 -93
- package/src/session/settings-registration.ts +0 -296
- package/src/session/tree-persist.ts +0 -75
- package/src/tool/guidance.ts +0 -69
- package/src/tool/names.ts +0 -27
- package/src/tool/overrides.ts +0 -204
- package/src/tool/register-tools.ts +0 -69
- package/src/tool/service-actions.ts +0 -399
- package/src/tool/tool-specs.ts +0 -248
- package/src/ui/renderer.ts +0 -120
- package/src/ui/ui.ts +0 -360
- package/src/workspace-change.ts +0 -40
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
LocationLink,
|
|
13
13
|
Position,
|
|
14
14
|
ProjectServerInfo,
|
|
15
|
+
Range,
|
|
15
16
|
SymbolInformation,
|
|
16
17
|
WorkspaceEdit,
|
|
17
18
|
WorkspaceSymbol,
|
|
@@ -19,6 +20,10 @@ import type {
|
|
|
19
20
|
import type { LspManager } from "../manager/manager.ts";
|
|
20
21
|
import { resolveSessionPath } from "../utils.ts";
|
|
21
22
|
|
|
23
|
+
function isRange(value: Position | Range): value is Range {
|
|
24
|
+
return "start" in value && "end" in value;
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
/** Workspace diagnostic summary grouped by file. */
|
|
23
28
|
export interface WorkspaceDiagnosticSummaryEntry {
|
|
24
29
|
file: string;
|
|
@@ -54,6 +59,11 @@ export type SessionLspServiceState =
|
|
|
54
59
|
| { kind: "disabled" }
|
|
55
60
|
| { kind: "unavailable"; reason: string };
|
|
56
61
|
|
|
62
|
+
export type SemanticReadinessResult =
|
|
63
|
+
| { kind: "ready" }
|
|
64
|
+
| { kind: "timeout" }
|
|
65
|
+
| { kind: "unavailable"; reason: string };
|
|
66
|
+
|
|
57
67
|
/**
|
|
58
68
|
* Public wrapper around {@link LspManager} that exposes stable semantic operations.
|
|
59
69
|
* File path inputs may be absolute or session-cwd-relative; a leading `@` is stripped
|
|
@@ -122,20 +132,54 @@ export class SessionLspService {
|
|
|
122
132
|
return client.rename(resolvedPath, position, newName);
|
|
123
133
|
}
|
|
124
134
|
|
|
125
|
-
async codeActions(
|
|
135
|
+
async codeActions(
|
|
136
|
+
filePath: string,
|
|
137
|
+
positionOrRange: Position | Range,
|
|
138
|
+
): Promise<CodeAction[] | null> {
|
|
126
139
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
127
140
|
const client = await this.manager.ensureFileOpen(resolvedPath);
|
|
128
141
|
if (!client) return null;
|
|
129
142
|
|
|
130
|
-
const range =
|
|
143
|
+
const range = isRange(positionOrRange)
|
|
144
|
+
? positionOrRange
|
|
145
|
+
: { start: positionOrRange, end: positionOrRange };
|
|
131
146
|
const diagnostics = client
|
|
132
147
|
.getDiagnostics(resolvedPath)
|
|
133
|
-
.filter((diagnostic) => diagnostic.range.
|
|
134
|
-
.filter((diagnostic) => diagnostic.range.
|
|
148
|
+
.filter((diagnostic) => diagnostic.range.end.line >= range.start.line)
|
|
149
|
+
.filter((diagnostic) => diagnostic.range.start.line <= range.end.line);
|
|
135
150
|
|
|
136
151
|
return client.codeActions(resolvedPath, range, { diagnostics });
|
|
137
152
|
}
|
|
138
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Wait until the LSP client that owns a file is ready for semantic queries.
|
|
156
|
+
* Performs a lightweight semantic warm-up probe before resolving.
|
|
157
|
+
*/
|
|
158
|
+
async waitUntilReadyForFile(
|
|
159
|
+
filePath: string,
|
|
160
|
+
options: { timeoutMs?: number } = {},
|
|
161
|
+
): Promise<SemanticReadinessResult> {
|
|
162
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
163
|
+
if (!this.manager.canServeFile(resolvedPath)) {
|
|
164
|
+
return {
|
|
165
|
+
kind: "unavailable",
|
|
166
|
+
reason: "No LSP client can serve this file",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return raceReadiness(this.manager.waitUntilFileReady(resolvedPath), options.timeoutMs);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Wait until all started LSP clients are ready for semantic queries.
|
|
175
|
+
* Performs one representative warm-up probe per client/root.
|
|
176
|
+
*/
|
|
177
|
+
async waitUntilReadyForWorkspace(
|
|
178
|
+
options: { timeoutMs?: number } = {},
|
|
179
|
+
): Promise<SemanticReadinessResult> {
|
|
180
|
+
return raceReadiness(this.manager.waitUntilWorkspaceReady(), options.timeoutMs);
|
|
181
|
+
}
|
|
182
|
+
|
|
139
183
|
// ── Project / runtime awareness ─────────────────────────────────────
|
|
140
184
|
|
|
141
185
|
getProjectServers(): ProjectServerInfo[] {
|
|
@@ -190,8 +234,41 @@ export class SessionLspService {
|
|
|
190
234
|
// ── Registry ──────────────────────────────────────────────────────────
|
|
191
235
|
|
|
192
236
|
const WAIT_INTERVAL_MS = 25;
|
|
237
|
+
const DEFAULT_SEMANTIC_READY_TIMEOUT_MS = 15_000;
|
|
193
238
|
const registry = createSessionStateRegistry<SessionLspServiceState>("supi-lsp/session-registry");
|
|
194
239
|
|
|
240
|
+
async function raceReadiness(
|
|
241
|
+
readiness: Promise<unknown>,
|
|
242
|
+
timeoutMs: number | undefined,
|
|
243
|
+
): Promise<SemanticReadinessResult> {
|
|
244
|
+
const effectiveTimeoutMs = timeoutMs ?? DEFAULT_SEMANTIC_READY_TIMEOUT_MS;
|
|
245
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await Promise.race([
|
|
249
|
+
readiness,
|
|
250
|
+
new Promise<never>((_, reject) => {
|
|
251
|
+
timer = setTimeout(
|
|
252
|
+
() => reject(new Error("semantic-readiness-timeout")),
|
|
253
|
+
effectiveTimeoutMs,
|
|
254
|
+
);
|
|
255
|
+
}),
|
|
256
|
+
]);
|
|
257
|
+
return { kind: "ready" };
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (timer) clearTimeout(timer);
|
|
260
|
+
if (error instanceof Error && error.message === "semantic-readiness-timeout") {
|
|
261
|
+
return { kind: "timeout" };
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
kind: "unavailable",
|
|
265
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
266
|
+
};
|
|
267
|
+
} finally {
|
|
268
|
+
if (timer) clearTimeout(timer);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
195
272
|
/** Publish the LSP service state for a session cwd. */
|
|
196
273
|
export function setSessionLspServiceState(cwd: string, state: SessionLspServiceState): void {
|
|
197
274
|
registry.set(cwd, state);
|
package/src/summary.ts
CHANGED
|
@@ -112,6 +112,8 @@ export function isProjectSource(filePath: string, cwd: string): boolean {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export function shouldIgnoreLspPath(filePath: string, cwd: string): boolean {
|
|
115
|
+
if (!isInProjectTree(filePath, cwd)) return true;
|
|
116
|
+
|
|
115
117
|
const normalized = normalizeRelevantPath(filePath);
|
|
116
118
|
if (
|
|
117
119
|
normalized === "node_modules" ||
|
package/src/extension.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default } from "./lsp.ts";
|
package/src/format.ts
DELETED
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
// LSP result formatting — converts LSP response types into readable text.
|
|
2
|
-
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import type {
|
|
5
|
-
AnnotatedTextEdit,
|
|
6
|
-
CodeAction,
|
|
7
|
-
DocumentSymbol,
|
|
8
|
-
Hover,
|
|
9
|
-
Location,
|
|
10
|
-
LocationLink,
|
|
11
|
-
MarkedString,
|
|
12
|
-
MarkupContent,
|
|
13
|
-
SnippetTextEdit,
|
|
14
|
-
SymbolInformation,
|
|
15
|
-
TextEdit,
|
|
16
|
-
WorkspaceEdit,
|
|
17
|
-
WorkspaceSymbol,
|
|
18
|
-
} from "./config/types.ts";
|
|
19
|
-
import { isProjectSource } from "./summary.ts";
|
|
20
|
-
import { uriToFile } from "./utils.ts";
|
|
21
|
-
|
|
22
|
-
// ── Hover ─────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
export function formatHover(hover: Hover): string {
|
|
25
|
-
const contents = hover.contents;
|
|
26
|
-
|
|
27
|
-
if (typeof contents === "string") return contents;
|
|
28
|
-
if ("value" in contents) {
|
|
29
|
-
const mc = contents as MarkupContent | { language: string; value: string };
|
|
30
|
-
if ("kind" in mc) return mc.value;
|
|
31
|
-
return `\`\`\`${mc.language}\n${mc.value}\n\`\`\``;
|
|
32
|
-
}
|
|
33
|
-
if (Array.isArray(contents)) {
|
|
34
|
-
return (contents as MarkedString[])
|
|
35
|
-
.map((c) => (typeof c === "string" ? c : `\`\`\`${c.language}\n${c.value}\n\`\`\``))
|
|
36
|
-
.join("\n\n");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return String(contents);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ── Locations ─────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
function formatLocationLine(loc: Location, cwd: string): string {
|
|
45
|
-
const file = relPath(uriToFile(loc.uri), cwd);
|
|
46
|
-
const line = loc.range.start.line + 1;
|
|
47
|
-
const col = loc.range.start.character + 1;
|
|
48
|
-
return `${file}:${line}:${col}`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function formatLocationList(label: string, locs: Location[], cwd: string): string {
|
|
52
|
-
const lines = [`${label} (${locs.length} locations):\n`];
|
|
53
|
-
for (const loc of locs) {
|
|
54
|
-
lines.push(`- ${formatLocationLine(loc, cwd)}`);
|
|
55
|
-
}
|
|
56
|
-
return lines.join("\n");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function formatExternalFallback(label: string, locs: Location[], cwd: string): string {
|
|
60
|
-
const maxShown = 3;
|
|
61
|
-
const shown = locs.slice(0, maxShown);
|
|
62
|
-
const hidden = locs.length - maxShown;
|
|
63
|
-
|
|
64
|
-
let result: string;
|
|
65
|
-
if (shown.length === 1) {
|
|
66
|
-
result = `${label}: ${formatLocationLine(shown[0], cwd)}`;
|
|
67
|
-
} else {
|
|
68
|
-
const lines = [`${label} (${locs.length} locations):\n`];
|
|
69
|
-
for (const loc of shown) {
|
|
70
|
-
lines.push(`- ${formatLocationLine(loc, cwd)}`);
|
|
71
|
-
}
|
|
72
|
-
result = lines.join("\n");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (hidden > 0) {
|
|
76
|
-
result += `\n+${hidden} more external ${hidden === 1 ? "location" : "locations"}`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return result;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function formatExternalSuffix(count: number): string {
|
|
83
|
-
return count === 1
|
|
84
|
-
? "+1 external location (node_modules, .pnpm, or out-of-tree)"
|
|
85
|
-
: `+${count} external locations (node_modules, .pnpm, or out-of-tree)`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function formatLocations(label: string, locations: Location[], cwd: string): string {
|
|
89
|
-
const projectLocs: Location[] = [];
|
|
90
|
-
const externalLocs: Location[] = [];
|
|
91
|
-
for (const loc of locations) {
|
|
92
|
-
if (isProjectSource(uriToFile(loc.uri), cwd)) {
|
|
93
|
-
projectLocs.push(loc);
|
|
94
|
-
} else {
|
|
95
|
-
externalLocs.push(loc);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let result: string;
|
|
100
|
-
|
|
101
|
-
if (projectLocs.length === 1) {
|
|
102
|
-
result = `${label}: ${formatLocationLine(projectLocs[0], cwd)}`;
|
|
103
|
-
} else if (projectLocs.length > 1) {
|
|
104
|
-
result = formatLocationList(label, projectLocs, cwd);
|
|
105
|
-
} else if (externalLocs.length > 0) {
|
|
106
|
-
return formatExternalFallback(label, externalLocs, cwd);
|
|
107
|
-
} else {
|
|
108
|
-
return `${label}: No locations found.`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (externalLocs.length > 0) {
|
|
112
|
-
result += `\n${formatExternalSuffix(externalLocs.length)}`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function normalizeLocations(result: Location | Location[] | LocationLink[]): Location[] {
|
|
119
|
-
if (Array.isArray(result)) {
|
|
120
|
-
return result.map((item) => {
|
|
121
|
-
if ("targetUri" in item) {
|
|
122
|
-
const link = item as LocationLink;
|
|
123
|
-
return { uri: link.targetUri, range: link.targetSelectionRange };
|
|
124
|
-
}
|
|
125
|
-
return item as Location;
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
return [result as Location];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── Symbols ───────────────────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
export function formatDocumentSymbols(symbols: DocumentSymbol[], indent: number): string {
|
|
134
|
-
const lines: string[] = [];
|
|
135
|
-
const prefix = " ".repeat(indent);
|
|
136
|
-
|
|
137
|
-
for (const sym of symbols) {
|
|
138
|
-
const kind = symbolKindName(sym.kind);
|
|
139
|
-
const line = sym.selectionRange.start.line + 1;
|
|
140
|
-
const detail = sym.detail ? ` — ${sym.detail}` : "";
|
|
141
|
-
lines.push(`${prefix}- ${kind} **${sym.name}**${detail} (line ${line})`);
|
|
142
|
-
|
|
143
|
-
if (sym.children && sym.children.length > 0) {
|
|
144
|
-
lines.push(formatDocumentSymbols(sym.children, indent + 1));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return lines.join("\n");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function formatSymbolInformation(symbols: SymbolInformation[], cwd: string): string {
|
|
152
|
-
const projectSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
|
|
153
|
-
const externalSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
|
|
154
|
-
for (const sym of symbols) {
|
|
155
|
-
if (isProjectSource(uriToFile(sym.location.uri), cwd)) {
|
|
156
|
-
projectSyms.push(sym);
|
|
157
|
-
} else {
|
|
158
|
-
externalSyms.push(sym);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const lines: string[] = [];
|
|
163
|
-
const symbolsToShow = projectSyms.length > 0 ? projectSyms : externalSyms;
|
|
164
|
-
for (const sym of symbolsToShow) {
|
|
165
|
-
const kind = symbolKindName(sym.kind);
|
|
166
|
-
const file = relPath(uriToFile(sym.location.uri), cwd);
|
|
167
|
-
const line = "range" in sym.location ? sym.location.range.start.line + 1 : "?";
|
|
168
|
-
const container = sym.containerName ? ` (in ${sym.containerName})` : "";
|
|
169
|
-
lines.push(`- ${kind} **${sym.name}**${container} — ${file}:${line}`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (projectSyms.length > 0 && externalSyms.length > 0) {
|
|
173
|
-
const suffix =
|
|
174
|
-
externalSyms.length === 1
|
|
175
|
-
? "+1 external symbol (node_modules, .pnpm, or out-of-tree)"
|
|
176
|
-
: `+${externalSyms.length} external symbols (node_modules, .pnpm, or out-of-tree)`;
|
|
177
|
-
lines.push(`- _${suffix}_`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return lines.join("\n");
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ── Workspace Edits ───────────────────────────────────────────────────
|
|
184
|
-
|
|
185
|
-
interface DisplayEdit {
|
|
186
|
-
range: { start: { line: number } };
|
|
187
|
-
newText: string;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
interface EditEntry {
|
|
191
|
-
file: string;
|
|
192
|
-
edits: DisplayEdit[];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Normalize a TextEdit/AnnotatedTextEdit/SnippetTextEdit to a display-friendly form.
|
|
197
|
-
*
|
|
198
|
-
* @since 3.18.0 — vscode-languageserver-types widened TextDocumentEdit.edits to include
|
|
199
|
-
* SnippetTextEdit (guarded by `workspace.workspaceEdit.snippetEditSupport` client capability).
|
|
200
|
-
*/
|
|
201
|
-
function editToDisplay(edit: TextEdit | AnnotatedTextEdit | SnippetTextEdit): DisplayEdit {
|
|
202
|
-
if ("snippet" in edit) {
|
|
203
|
-
return { range: edit.range, newText: edit.snippet.value };
|
|
204
|
-
}
|
|
205
|
-
return { range: edit.range, newText: edit.newText };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pre-existing — not introduced by this change
|
|
209
|
-
function partitionWorkspaceEdit(
|
|
210
|
-
edit: WorkspaceEdit,
|
|
211
|
-
cwd: string,
|
|
212
|
-
): { projectChanges: EditEntry[]; externalCount: number } {
|
|
213
|
-
const projectChanges: EditEntry[] = [];
|
|
214
|
-
let externalCount = 0;
|
|
215
|
-
|
|
216
|
-
if (edit.changes) {
|
|
217
|
-
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
218
|
-
const filePath = uriToFile(uri);
|
|
219
|
-
if (isProjectSource(filePath, cwd)) {
|
|
220
|
-
projectChanges.push({
|
|
221
|
-
file: relPath(filePath, cwd),
|
|
222
|
-
edits: edits.map(editToDisplay),
|
|
223
|
-
});
|
|
224
|
-
} else {
|
|
225
|
-
externalCount++;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (edit.documentChanges) {
|
|
231
|
-
for (const dc of edit.documentChanges) {
|
|
232
|
-
if (!("textDocument" in dc)) continue;
|
|
233
|
-
const filePath = uriToFile(dc.textDocument.uri);
|
|
234
|
-
if (isProjectSource(filePath, cwd)) {
|
|
235
|
-
projectChanges.push({
|
|
236
|
-
file: relPath(filePath, cwd),
|
|
237
|
-
edits: dc.edits.map(editToDisplay),
|
|
238
|
-
});
|
|
239
|
-
} else {
|
|
240
|
-
externalCount++;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return { projectChanges, externalCount };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function formatWorkspaceEdit(edit: WorkspaceEdit, cwd: string): string {
|
|
249
|
-
const { projectChanges, externalCount } = partitionWorkspaceEdit(edit, cwd);
|
|
250
|
-
|
|
251
|
-
const lines: string[] = ["Rename workspace edit:\n"];
|
|
252
|
-
for (const { file, edits } of projectChanges) {
|
|
253
|
-
lines.push(`**${file}** (${edits.length} change(s))`);
|
|
254
|
-
for (const e of edits) {
|
|
255
|
-
const line = e.range.start.line + 1;
|
|
256
|
-
lines.push(` Line ${line}: → "${e.newText}"`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (externalCount > 0) {
|
|
261
|
-
const suffix =
|
|
262
|
-
externalCount === 1
|
|
263
|
-
? "+1 external file (node_modules, .pnpm, or out-of-tree)"
|
|
264
|
-
: `+${externalCount} external files (node_modules, .pnpm, or out-of-tree)`;
|
|
265
|
-
lines.push(`\n_${suffix}_`);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return lines.join("\n");
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ── Code Actions ──────────────────────────────────────────────────────
|
|
272
|
-
|
|
273
|
-
export function formatCodeActions(actions: CodeAction[]): string {
|
|
274
|
-
const lines = [`Available code actions (${actions.length}):\n`];
|
|
275
|
-
for (const action of actions) {
|
|
276
|
-
const kind = action.kind ? ` [${action.kind}]` : "";
|
|
277
|
-
const preferred = action.isPreferred ? " ⭐" : "";
|
|
278
|
-
lines.push(`- **${action.title}**${kind}${preferred}`);
|
|
279
|
-
if (action.edit) {
|
|
280
|
-
const fileCount = action.edit.changes
|
|
281
|
-
? Object.keys(action.edit.changes).length
|
|
282
|
-
: (action.edit.documentChanges?.length ?? 0);
|
|
283
|
-
lines.push(` Edits ${fileCount} file(s)`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return lines.join("\n");
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// ── Workspace Symbols ─────────────────────────────────────────────────
|
|
290
|
-
|
|
291
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pre-existing — threshold exceeded by widened parameter type for WorkspaceSymbol compatibility
|
|
292
|
-
export function formatWorkspaceSymbols(
|
|
293
|
-
symbols: (SymbolInformation | WorkspaceSymbol)[],
|
|
294
|
-
cwd: string,
|
|
295
|
-
): string {
|
|
296
|
-
if (symbols.length === 0) return "No symbols found.";
|
|
297
|
-
|
|
298
|
-
const projectSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
|
|
299
|
-
const externalSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
|
|
300
|
-
for (const sym of symbols) {
|
|
301
|
-
if (isProjectSource(uriToFile(sym.location.uri), cwd)) {
|
|
302
|
-
projectSyms.push(sym);
|
|
303
|
-
} else {
|
|
304
|
-
externalSyms.push(sym);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (projectSyms.length === 0 && externalSyms.length > 0) {
|
|
309
|
-
return `Workspace symbols: No in-project symbols found.\n+${externalSyms.length} external symbol${externalSyms.length === 1 ? "" : "s"} (node_modules, .pnpm, or out-of-tree).`;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const lines = [`Workspace symbols (${projectSyms.length}):\n`];
|
|
313
|
-
for (const sym of projectSyms) {
|
|
314
|
-
const kind = symbolKindName(sym.kind);
|
|
315
|
-
const file = relPath(uriToFile(sym.location.uri), cwd);
|
|
316
|
-
const loc = "range" in sym.location ? sym.location.range : null;
|
|
317
|
-
const line = loc ? loc.start.line + 1 : "?";
|
|
318
|
-
const col = loc ? loc.start.character + 1 : "?";
|
|
319
|
-
const container = sym.containerName ? ` — ${sym.containerName}` : "";
|
|
320
|
-
lines.push(`- **${sym.name}** (${kind})${container} — ${file}:${line}:${col}`);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (externalSyms.length > 0) {
|
|
324
|
-
const suffix =
|
|
325
|
-
externalSyms.length === 1
|
|
326
|
-
? "+1 external symbol (node_modules, .pnpm, or out-of-tree)"
|
|
327
|
-
: `+${externalSyms.length} external symbols (node_modules, .pnpm, or out-of-tree)`;
|
|
328
|
-
lines.push(`\n_${suffix}_`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return lines.join("\n");
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ── Symbol Kind Names ─────────────────────────────────────────────────
|
|
335
|
-
|
|
336
|
-
const SYMBOL_KIND_NAMES: Record<number, string> = {
|
|
337
|
-
1: "File",
|
|
338
|
-
2: "Module",
|
|
339
|
-
3: "Namespace",
|
|
340
|
-
4: "Package",
|
|
341
|
-
5: "Class",
|
|
342
|
-
6: "Method",
|
|
343
|
-
7: "Property",
|
|
344
|
-
8: "Field",
|
|
345
|
-
9: "Constructor",
|
|
346
|
-
10: "Enum",
|
|
347
|
-
11: "Interface",
|
|
348
|
-
12: "Function",
|
|
349
|
-
13: "Variable",
|
|
350
|
-
14: "Constant",
|
|
351
|
-
15: "String",
|
|
352
|
-
16: "Number",
|
|
353
|
-
17: "Boolean",
|
|
354
|
-
18: "Array",
|
|
355
|
-
19: "Object",
|
|
356
|
-
20: "Key",
|
|
357
|
-
21: "Null",
|
|
358
|
-
22: "EnumMember",
|
|
359
|
-
23: "Struct",
|
|
360
|
-
24: "Event",
|
|
361
|
-
25: "Operator",
|
|
362
|
-
26: "TypeParameter",
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
function symbolKindName(kind: number): string {
|
|
366
|
-
return SYMBOL_KIND_NAMES[kind] ?? `Kind(${kind})`;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ── Helpers ───────────────────────────────────────────────────────────
|
|
370
|
-
|
|
371
|
-
function relPath(filePath: string, cwd: string): string {
|
|
372
|
-
return path.relative(cwd, filePath);
|
|
373
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
// Diagnostic injection handlers — before_agent_start and context.
|
|
2
|
-
//
|
|
3
|
-
// Extracted from lsp.ts to keep each orchestration concern in its own module.
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
BeforeAgentStartEvent,
|
|
7
|
-
BeforeAgentStartEventResult,
|
|
8
|
-
ContextEvent,
|
|
9
|
-
ExtensionAPI,
|
|
10
|
-
ExtensionContext,
|
|
11
|
-
} from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { pruneAndReorderContextMessages, restorePromptContent } from "@mrclrchtr/supi-core/context";
|
|
13
|
-
import type { Diagnostic } from "../config/types.ts";
|
|
14
|
-
import {
|
|
15
|
-
diagnosticsContextFingerprint,
|
|
16
|
-
formatDiagnosticsContext,
|
|
17
|
-
MAX_DETAILED_DIAGNOSTICS,
|
|
18
|
-
} from "../diagnostics/diagnostic-context.ts";
|
|
19
|
-
import { formatDiagnosticsDisplayContent } from "../diagnostics/diagnostic-display.ts";
|
|
20
|
-
import { assessStaleDiagnostics } from "../diagnostics/stale-diagnostics.ts";
|
|
21
|
-
// Force re-open files with module-resolution errors
|
|
22
|
-
import { forceResyncStaleModuleFiles } from "../manager/manager-stale-resync.ts";
|
|
23
|
-
import type { OutstandingDiagnosticSummaryEntry } from "../manager/manager-types.ts";
|
|
24
|
-
import {
|
|
25
|
-
ensureLspToolsActive,
|
|
26
|
-
type LspRuntimeState,
|
|
27
|
-
refreshProjectServers,
|
|
28
|
-
removeLspTools,
|
|
29
|
-
} from "../session/lsp-state.ts";
|
|
30
|
-
import { persistLspInactiveState } from "../session/tree-persist.ts";
|
|
31
|
-
import { updateLspUi } from "../ui/ui.ts";
|
|
32
|
-
import { refreshWorkspaceSentinels } from "../workspace-change.ts";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Register handlers for before_agent_start (diagnostic injection) and context
|
|
36
|
-
* (lsp-context message pruning / prompt-content restoration).
|
|
37
|
-
*/
|
|
38
|
-
export function registerDiagnosticInjectionHandlers(
|
|
39
|
-
pi: ExtensionAPI,
|
|
40
|
-
state: LspRuntimeState,
|
|
41
|
-
): void {
|
|
42
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: before_agent_start coordinates sentinel recovery, pruning, refresh, and diagnostic injection.
|
|
43
|
-
pi.on("before_agent_start", async (_event: BeforeAgentStartEvent, ctx: ExtensionContext) => {
|
|
44
|
-
if (!state.manager || !state.lspActive) {
|
|
45
|
-
removeLspTools(pi);
|
|
46
|
-
if (!state.manager && state.lspActive) {
|
|
47
|
-
persistLspInactiveState(pi, state);
|
|
48
|
-
}
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
ensureLspToolsActive(pi);
|
|
53
|
-
|
|
54
|
-
refreshWorkspaceSentinels(state, ctx.cwd);
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Two-pass prune/refresh pattern:
|
|
58
|
-
*
|
|
59
|
-
* 1. Prune files deleted since the last turn and send didClose.
|
|
60
|
-
* 2. Re-sync remaining open docs and wait for diagnostics to settle.
|
|
61
|
-
* 3. Prune *again* — late publishDiagnostics notifications (already
|
|
62
|
-
* in-flight when step 1 ran) may have re-created stale entries for
|
|
63
|
-
* files that no longer exist. `getAllDiagnostics()` also filters
|
|
64
|
-
* by existence, so this second pass is belt-and-suspenders.
|
|
65
|
-
*/
|
|
66
|
-
state.manager.pruneMissingFiles();
|
|
67
|
-
try {
|
|
68
|
-
await state.manager.refreshOpenDiagnostics();
|
|
69
|
-
} catch {
|
|
70
|
-
// Refresh failures must not prevent agent startup
|
|
71
|
-
}
|
|
72
|
-
state.manager.pruneMissingFiles();
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await forceResyncStaleModuleFiles(state.manager, ctx.cwd);
|
|
76
|
-
} catch {
|
|
77
|
-
// Best-effort: don't fail the agent turn
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
refreshProjectServers(state);
|
|
81
|
-
updateLspUi(ctx, state.manager, state.inlineSeverity, state.projectServers);
|
|
82
|
-
|
|
83
|
-
const diagnostics = state.manager.getOutstandingDiagnosticSummary(state.inlineSeverity);
|
|
84
|
-
const totalDiags = diagnostics.reduce((sum, d) => sum + d.total, 0);
|
|
85
|
-
const detailed =
|
|
86
|
-
totalDiags <= MAX_DETAILED_DIAGNOSTICS
|
|
87
|
-
? state.manager.getOutstandingDiagnostics(state.inlineSeverity)
|
|
88
|
-
: undefined;
|
|
89
|
-
const staleAssessment = state.staleSuspected
|
|
90
|
-
? assessStaleDiagnostics(state.manager.getOutstandingDiagnostics(4))
|
|
91
|
-
: { suspected: false, matchedFiles: [], warning: null };
|
|
92
|
-
state.staleSuspected = staleAssessment.suspected;
|
|
93
|
-
|
|
94
|
-
const staleWarning = staleAssessment.suspected ? staleAssessment.warning : null;
|
|
95
|
-
const content = formatDiagnosticsContext(diagnostics, 3, detailed, staleWarning);
|
|
96
|
-
const fingerprint = diagnosticsContextFingerprint(content);
|
|
97
|
-
|
|
98
|
-
if (!content) {
|
|
99
|
-
state.lastDiagnosticsFingerprint = null;
|
|
100
|
-
state.currentContextToken = null;
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (fingerprint === state.lastDiagnosticsFingerprint) {
|
|
105
|
-
state.currentContextToken = null;
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
state.lastDiagnosticsFingerprint = fingerprint;
|
|
110
|
-
state.currentContextToken = `lsp-context-${++state.contextCounter}`;
|
|
111
|
-
|
|
112
|
-
return buildDiagnosticResult(
|
|
113
|
-
diagnostics,
|
|
114
|
-
detailed,
|
|
115
|
-
state.inlineSeverity,
|
|
116
|
-
state.currentContextToken,
|
|
117
|
-
staleWarning,
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
pi.on("context", (event: ContextEvent) => {
|
|
122
|
-
const messages = pruneAndReorderContextMessages(
|
|
123
|
-
event.messages as Array<{
|
|
124
|
-
role?: string;
|
|
125
|
-
customType?: string;
|
|
126
|
-
content?: unknown;
|
|
127
|
-
details?: unknown;
|
|
128
|
-
}>,
|
|
129
|
-
"lsp-context",
|
|
130
|
-
state.currentContextToken,
|
|
131
|
-
);
|
|
132
|
-
const contextMessages = restorePromptContent(
|
|
133
|
-
messages,
|
|
134
|
-
"lsp-context",
|
|
135
|
-
state.currentContextToken,
|
|
136
|
-
) as typeof event.messages;
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
contextMessages.length === event.messages.length &&
|
|
140
|
-
contextMessages.every((message, index) => message === event.messages[index])
|
|
141
|
-
) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
return { messages: contextMessages };
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Build the `lsp-context` custom message used to surface outstanding diagnostics. */
|
|
149
|
-
// biome-ignore lint/complexity/useMaxParams: wrapper groups the prompt payload fields in one place.
|
|
150
|
-
function buildDiagnosticResult(
|
|
151
|
-
diagnostics: OutstandingDiagnosticSummaryEntry[],
|
|
152
|
-
detailed:
|
|
153
|
-
| {
|
|
154
|
-
file: string;
|
|
155
|
-
diagnostics: Diagnostic[];
|
|
156
|
-
}[]
|
|
157
|
-
| undefined,
|
|
158
|
-
severity: number,
|
|
159
|
-
token: string,
|
|
160
|
-
staleWarning?: string | null,
|
|
161
|
-
): BeforeAgentStartEventResult {
|
|
162
|
-
return {
|
|
163
|
-
message: {
|
|
164
|
-
customType: "lsp-context",
|
|
165
|
-
content: formatDiagnosticsDisplayContent(diagnostics, detailed),
|
|
166
|
-
display: true,
|
|
167
|
-
details: {
|
|
168
|
-
contextToken: token,
|
|
169
|
-
promptContent: formatDiagnosticsContext(diagnostics, 3, detailed, staleWarning),
|
|
170
|
-
inlineSeverity: severity,
|
|
171
|
-
...(staleWarning ? { staleWarning } : {}),
|
|
172
|
-
diagnostics: diagnostics.map((d) => ({
|
|
173
|
-
file: d.file,
|
|
174
|
-
errors: d.errors,
|
|
175
|
-
warnings: d.warnings,
|
|
176
|
-
information: d.information,
|
|
177
|
-
hints: d.hints,
|
|
178
|
-
})),
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
}
|