@mrclrchtr/supi-lsp 3.2.0 → 4.0.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.
- package/README.md +72 -62
- package/node_modules/@mrclrchtr/supi-code-runtime/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +2 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +3 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -10
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +19 -15
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +10 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +19 -29
- package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +7 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +42 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +15 -10
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -4
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -4
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +3 -2
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -4
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -10
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +19 -15
- package/package.json +4 -5
- package/src/api.ts +9 -9
- package/src/client/client.ts +70 -41
- package/src/client/transport.ts +16 -13
- package/src/config/config.ts +2 -2
- package/src/config/lsp-settings.ts +0 -2
- package/src/index.ts +6 -6
- package/src/manager/manager-diagnostics.ts +5 -95
- package/src/manager/manager-workspace-recovery.ts +4 -3
- package/src/manager/manager-workspace-symbol.ts +44 -6
- package/src/manager/manager.ts +47 -65
- package/src/provider/lsp-semantic-provider.ts +132 -79
- package/src/provider/refactor-planning.ts +6 -14
- package/src/session/readiness.ts +38 -0
- package/src/session/runtime-controller.ts +87 -75
- package/src/session/runtime-registration.ts +5 -5
- package/src/session/{service-registry.ts → runtime-registry.ts} +163 -71
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +0 -31
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +0 -11
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/types.ts +0 -2
- package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +0 -31
- package/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +0 -11
- package/node_modules/@mrclrchtr/supi-core/src/types.ts +0 -2
- package/src/diagnostics/diagnostic-augmentation.ts +0 -82
- package/src/diagnostics/diagnostic-context.ts +0 -116
- package/src/diagnostics/diagnostic-display.ts +0 -69
- package/src/manager/capability-index.ts +0 -24
- package/src/manager/client-pool.ts +0 -33
- package/src/manager/diagnostic-store.ts +0 -51
- package/src/manager/manager-stale-resync.ts +0 -47
- package/src/manager/recovery-coordinator.ts +0 -37
- package/src/manager/workspace-router.ts +0 -44
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
// Shared session-scoped LSP service registry.
|
|
2
|
-
// Peer extensions can import `getSessionLspService` from the package root
|
|
3
|
-
// to reuse the active LSP runtime without starting duplicate servers.
|
|
1
|
+
// Shared session-scoped LSP service registry reused by peer extensions.
|
|
4
2
|
|
|
3
|
+
import {
|
|
4
|
+
type CodeQueryResult,
|
|
5
|
+
mapCodeQueryResult,
|
|
6
|
+
unavailableCodeQuery,
|
|
7
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
5
8
|
import { createSessionStateRegistry } from "@mrclrchtr/supi-core/session";
|
|
6
9
|
import type {
|
|
7
10
|
CodeAction,
|
|
8
11
|
Diagnostic,
|
|
9
12
|
DocumentSymbol,
|
|
13
|
+
FileEvent,
|
|
10
14
|
Hover,
|
|
11
15
|
Location,
|
|
12
16
|
LocationLink,
|
|
@@ -19,11 +23,16 @@ import type {
|
|
|
19
23
|
} from "../config/types.ts";
|
|
20
24
|
import type { LspManager } from "../manager/manager.ts";
|
|
21
25
|
import { resolveSessionPath } from "../utils.ts";
|
|
26
|
+
import { raceReadinessValue } from "./readiness.ts";
|
|
22
27
|
|
|
23
28
|
function isRange(value: Position | Range): value is Range {
|
|
24
29
|
return "start" in value && "end" in value;
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
function unavailableFileQuery<T>(operation: string, file: string): CodeQueryResult<T> {
|
|
33
|
+
return unavailableCodeQuery(`No routed LSP client could complete ${operation} for ${file}.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
/** Workspace diagnostic summary grouped by file. */
|
|
28
37
|
export interface WorkspaceDiagnosticSummaryEntry {
|
|
29
38
|
file: string;
|
|
@@ -43,7 +52,8 @@ export interface OutstandingDiagnosticSummaryEntry {
|
|
|
43
52
|
|
|
44
53
|
/** Result from a workspace diagnostic recovery pass. */
|
|
45
54
|
export interface RecoverDiagnosticsResult {
|
|
46
|
-
|
|
55
|
+
/** Active clients targeted by the best-effort refresh, not confirmed successful refreshes. */
|
|
56
|
+
attemptedClients: number;
|
|
47
57
|
restartedClients: number;
|
|
48
58
|
staleAssessment: {
|
|
49
59
|
suspected: boolean;
|
|
@@ -52,9 +62,9 @@ export interface RecoverDiagnosticsResult {
|
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
export type
|
|
56
|
-
| { kind: "ready";
|
|
57
|
-
| { kind: "inactive";
|
|
65
|
+
export type WorkspaceLspRuntimeState =
|
|
66
|
+
| { kind: "ready"; runtime: WorkspaceLspRuntime }
|
|
67
|
+
| { kind: "inactive"; runtime: WorkspaceLspRuntime }
|
|
58
68
|
| { kind: "pending" }
|
|
59
69
|
| { kind: "disabled" }
|
|
60
70
|
| { kind: "unavailable"; reason: string };
|
|
@@ -65,59 +75,116 @@ export type SemanticReadinessResult =
|
|
|
65
75
|
| { kind: "unavailable"; reason: string };
|
|
66
76
|
|
|
67
77
|
/**
|
|
68
|
-
*
|
|
78
|
+
* Workspace-scoped LSP interface that owns routing, readiness, semantic operations,
|
|
79
|
+
* diagnostics, and recovery without exposing clients or the mutable manager.
|
|
69
80
|
* File path inputs may be absolute or session-cwd-relative; a leading `@` is stripped
|
|
70
81
|
* to match pi's built-in path-tool convention. Position arguments use raw 0-based LSP
|
|
71
82
|
* coordinates; use `toLspPosition()` from `@mrclrchtr/supi-lsp/api` when starting from
|
|
72
83
|
* user-facing 1-based line and character values.
|
|
73
84
|
*/
|
|
74
|
-
export
|
|
85
|
+
export interface WorkspaceLspRuntime {
|
|
86
|
+
hover(filePath: string, position: Position): Promise<CodeQueryResult<Hover | null>>;
|
|
87
|
+
definition(
|
|
88
|
+
filePath: string,
|
|
89
|
+
position: Position,
|
|
90
|
+
): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>>;
|
|
91
|
+
references(filePath: string, position: Position): Promise<CodeQueryResult<Location[]>>;
|
|
92
|
+
implementation(
|
|
93
|
+
filePath: string,
|
|
94
|
+
position: Position,
|
|
95
|
+
): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>>;
|
|
96
|
+
documentSymbols(
|
|
97
|
+
filePath: string,
|
|
98
|
+
): Promise<CodeQueryResult<DocumentSymbol[] | SymbolInformation[]>>;
|
|
99
|
+
workspaceSymbol(query: string): Promise<CodeQueryResult<SymbolInformation[] | WorkspaceSymbol[]>>;
|
|
100
|
+
rename(filePath: string, position: Position, newName: string): Promise<WorkspaceEdit | null>;
|
|
101
|
+
codeActions(filePath: string, positionOrRange: Position | Range): Promise<CodeAction[] | null>;
|
|
102
|
+
/** Succeeds only when the concrete routed client exists and is query-ready. */
|
|
103
|
+
waitUntilReadyForFile(
|
|
104
|
+
filePath: string,
|
|
105
|
+
options?: { timeoutMs?: number },
|
|
106
|
+
): Promise<SemanticReadinessResult>;
|
|
107
|
+
/** Succeeds only when at least one concrete workspace client is active and query-ready. */
|
|
108
|
+
waitUntilReadyForWorkspace(options?: { timeoutMs?: number }): Promise<SemanticReadinessResult>;
|
|
109
|
+
getProjectServers(): ProjectServerInfo[];
|
|
110
|
+
isSupportedSourceFile(filePath: string): boolean;
|
|
111
|
+
trackFile(filePath: string): Promise<boolean>;
|
|
112
|
+
closeFile(filePath: string): void;
|
|
113
|
+
pruneMissingFiles(): readonly string[];
|
|
114
|
+
noteWorkspaceChanges(changes: FileEvent[]): void;
|
|
115
|
+
fileDiagnostics(filePath: string, maxSeverity?: number): Promise<CodeQueryResult<Diagnostic[]>>;
|
|
116
|
+
refreshOpenDiagnostics(options?: { maxWaitMs?: number; quietMs?: number }): Promise<void>;
|
|
117
|
+
getWorkspaceDiagnosticSummary(): WorkspaceDiagnosticSummaryEntry[];
|
|
118
|
+
getOutstandingDiagnostics(
|
|
119
|
+
maxSeverity?: number,
|
|
120
|
+
): Array<{ file: string; diagnostics: Diagnostic[] }>;
|
|
121
|
+
getOutstandingDiagnosticSummary(maxSeverity?: number): OutstandingDiagnosticSummaryEntry[];
|
|
122
|
+
recoverDiagnostics(options?: {
|
|
123
|
+
restartIfStillStale?: boolean;
|
|
124
|
+
maxWaitMs?: number;
|
|
125
|
+
quietMs?: number;
|
|
126
|
+
}): Promise<RecoverDiagnosticsResult>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
75
130
|
constructor(private readonly manager: LspManager) {}
|
|
76
131
|
|
|
132
|
+
static createOwner(manager: LspManager) {
|
|
133
|
+
const runtime = new DefaultWorkspaceLspRuntime(manager);
|
|
134
|
+
return { runtime: runtime as WorkspaceLspRuntime, shutdown: () => runtime.#shutdown() };
|
|
135
|
+
}
|
|
136
|
+
|
|
77
137
|
// ── Semantic lookups ────────────────────────────────────────────────
|
|
78
138
|
|
|
79
|
-
async hover(filePath: string, position: Position): Promise<Hover | null
|
|
139
|
+
async hover(filePath: string, position: Position): Promise<CodeQueryResult<Hover | null>> {
|
|
80
140
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
81
141
|
const client = await this.manager.ensureFileOpen(resolvedPath);
|
|
82
|
-
if (!client) return
|
|
142
|
+
if (!client) return unavailableFileQuery("hover", resolvedPath);
|
|
83
143
|
return client.hover(resolvedPath, position);
|
|
84
144
|
}
|
|
85
145
|
|
|
86
146
|
async definition(
|
|
87
147
|
filePath: string,
|
|
88
148
|
position: Position,
|
|
89
|
-
): Promise<Location | Location[] | LocationLink[] | null
|
|
149
|
+
): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
|
|
90
150
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
91
151
|
const client = await this.manager.ensureFileOpen(resolvedPath);
|
|
92
|
-
if (!client) return
|
|
152
|
+
if (!client) return unavailableFileQuery("definition", resolvedPath);
|
|
93
153
|
return client.definition(resolvedPath, position);
|
|
94
154
|
}
|
|
95
155
|
|
|
96
|
-
async references(filePath: string, position: Position): Promise<Location[]
|
|
156
|
+
async references(filePath: string, position: Position): Promise<CodeQueryResult<Location[]>> {
|
|
97
157
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
98
158
|
const client = await this.manager.ensureFileOpen(resolvedPath);
|
|
99
|
-
if (!client) return
|
|
100
|
-
return
|
|
159
|
+
if (!client) return unavailableFileQuery("references", resolvedPath);
|
|
160
|
+
return mapCodeQueryResult(
|
|
161
|
+
await client.references(resolvedPath, position),
|
|
162
|
+
(data) => data ?? [],
|
|
163
|
+
);
|
|
101
164
|
}
|
|
102
165
|
|
|
103
166
|
async implementation(
|
|
104
167
|
filePath: string,
|
|
105
168
|
position: Position,
|
|
106
|
-
): Promise<Location | Location[] | LocationLink[] | null
|
|
169
|
+
): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
|
|
107
170
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
108
171
|
const client = await this.manager.ensureFileOpen(resolvedPath);
|
|
109
|
-
if (!client) return
|
|
172
|
+
if (!client) return unavailableFileQuery("implementation", resolvedPath);
|
|
110
173
|
return client.implementation(resolvedPath, position);
|
|
111
174
|
}
|
|
112
175
|
|
|
113
|
-
async documentSymbols(
|
|
176
|
+
async documentSymbols(
|
|
177
|
+
filePath: string,
|
|
178
|
+
): Promise<CodeQueryResult<DocumentSymbol[] | SymbolInformation[]>> {
|
|
114
179
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
115
180
|
const client = await this.manager.ensureFileOpen(resolvedPath);
|
|
116
|
-
if (!client) return
|
|
117
|
-
return client.documentSymbols(resolvedPath);
|
|
181
|
+
if (!client) return unavailableFileQuery("document symbols", resolvedPath);
|
|
182
|
+
return mapCodeQueryResult(await client.documentSymbols(resolvedPath), (data) => data ?? []);
|
|
118
183
|
}
|
|
119
184
|
|
|
120
|
-
async workspaceSymbol(
|
|
185
|
+
async workspaceSymbol(
|
|
186
|
+
query: string,
|
|
187
|
+
): Promise<CodeQueryResult<SymbolInformation[] | WorkspaceSymbol[]>> {
|
|
121
188
|
return this.manager.workspaceSymbol(query);
|
|
122
189
|
}
|
|
123
190
|
|
|
@@ -167,7 +234,18 @@ export class SessionLspService {
|
|
|
167
234
|
};
|
|
168
235
|
}
|
|
169
236
|
|
|
170
|
-
|
|
237
|
+
const readiness = await raceReadinessValue(
|
|
238
|
+
this.manager.waitUntilFileReady(resolvedPath),
|
|
239
|
+
options.timeoutMs,
|
|
240
|
+
);
|
|
241
|
+
if (readiness.kind !== "resolved") return readiness;
|
|
242
|
+
if (readiness.value === null) {
|
|
243
|
+
return {
|
|
244
|
+
kind: "unavailable",
|
|
245
|
+
reason: "The routed LSP client could not be started for this file",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return { kind: "ready" };
|
|
171
249
|
}
|
|
172
250
|
|
|
173
251
|
/**
|
|
@@ -177,11 +255,20 @@ export class SessionLspService {
|
|
|
177
255
|
async waitUntilReadyForWorkspace(
|
|
178
256
|
options: { timeoutMs?: number } = {},
|
|
179
257
|
): Promise<SemanticReadinessResult> {
|
|
180
|
-
|
|
258
|
+
const readiness = await raceReadinessValue(
|
|
259
|
+
this.manager.waitUntilWorkspaceReady(),
|
|
260
|
+
options.timeoutMs,
|
|
261
|
+
);
|
|
262
|
+
if (readiness.kind !== "resolved") return readiness;
|
|
263
|
+
if (readiness.value === 0) {
|
|
264
|
+
return {
|
|
265
|
+
kind: "unavailable",
|
|
266
|
+
reason: "No active LSP clients are ready for this workspace",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { kind: "ready" };
|
|
181
270
|
}
|
|
182
271
|
|
|
183
|
-
// ── Project / runtime awareness ─────────────────────────────────────
|
|
184
|
-
|
|
185
272
|
getProjectServers(): ProjectServerInfo[] {
|
|
186
273
|
return this.manager.getKnownProjectServers([]);
|
|
187
274
|
}
|
|
@@ -191,15 +278,51 @@ export class SessionLspService {
|
|
|
191
278
|
return this.manager.canServeFile(this.resolveFilePath(filePath));
|
|
192
279
|
}
|
|
193
280
|
|
|
194
|
-
|
|
281
|
+
/** Track a file in its routed client without exposing that client. */
|
|
282
|
+
async trackFile(filePath: string): Promise<boolean> {
|
|
283
|
+
const resolvedPath = this.resolveFilePath(filePath);
|
|
284
|
+
return (await this.manager.ensureFileOpen(resolvedPath)) !== null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Stop tracking a file and clear its cached diagnostics. */
|
|
288
|
+
closeFile(filePath: string): void {
|
|
289
|
+
this.manager.closeFile(this.resolveFilePath(filePath));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Remove missing files from runtime tracking. */
|
|
293
|
+
pruneMissingFiles(): readonly string[] {
|
|
294
|
+
return this.manager.pruneMissingFiles();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Notify routed clients of workspace file changes and reset pull state. */
|
|
298
|
+
noteWorkspaceChanges(changes: FileEvent[]): void {
|
|
299
|
+
this.manager.clearAllPullResultIds();
|
|
300
|
+
this.manager.notifyWorkspaceFileChanges(changes);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async #shutdown(): Promise<void> {
|
|
304
|
+
await this.manager.shutdownAll();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Diagnostics and recovery ────────────────────────────────────────
|
|
195
308
|
|
|
196
309
|
/** Sync a file through LSP and return diagnostics up to the supplied severity threshold. */
|
|
197
|
-
async fileDiagnostics(
|
|
310
|
+
async fileDiagnostics(
|
|
311
|
+
filePath: string,
|
|
312
|
+
maxSeverity: number = 4,
|
|
313
|
+
): Promise<CodeQueryResult<Diagnostic[]>> {
|
|
198
314
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
199
|
-
if (!this.manager.canServeFile(resolvedPath))
|
|
315
|
+
if (!this.manager.canServeFile(resolvedPath)) {
|
|
316
|
+
return unavailableFileQuery("diagnostics", resolvedPath);
|
|
317
|
+
}
|
|
200
318
|
return this.manager.syncFileAndGetDiagnostics(resolvedPath, maxSeverity);
|
|
201
319
|
}
|
|
202
320
|
|
|
321
|
+
/** Re-sync every open document and wait for diagnostics to settle. */
|
|
322
|
+
async refreshOpenDiagnostics(options?: { maxWaitMs?: number; quietMs?: number }): Promise<void> {
|
|
323
|
+
await this.manager.refreshOpenDiagnostics(options);
|
|
324
|
+
}
|
|
325
|
+
|
|
203
326
|
/** Get a lightweight workspace diagnostic summary for all tracked files. */
|
|
204
327
|
getWorkspaceDiagnosticSummary(): WorkspaceDiagnosticSummaryEntry[] {
|
|
205
328
|
return this.manager.getDiagnosticSummary();
|
|
@@ -231,51 +354,20 @@ export class SessionLspService {
|
|
|
231
354
|
}
|
|
232
355
|
}
|
|
233
356
|
|
|
234
|
-
|
|
357
|
+
export function createWorkspaceLspRuntimeOwner(manager: LspManager) {
|
|
358
|
+
return DefaultWorkspaceLspRuntime.createOwner(manager);
|
|
359
|
+
}
|
|
235
360
|
|
|
236
361
|
const WAIT_INTERVAL_MS = 25;
|
|
237
|
-
const
|
|
238
|
-
const registry = createSessionStateRegistry<SessionLspServiceState>("supi-lsp/session-registry");
|
|
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
|
-
}
|
|
362
|
+
const registry = createSessionStateRegistry<WorkspaceLspRuntimeState>("supi-lsp/session-registry");
|
|
271
363
|
|
|
272
364
|
/** Publish the LSP service state for a session cwd. */
|
|
273
|
-
export function
|
|
365
|
+
export function setWorkspaceLspRuntimeState(cwd: string, state: WorkspaceLspRuntimeState): void {
|
|
274
366
|
registry.set(cwd, state);
|
|
275
367
|
}
|
|
276
368
|
|
|
277
369
|
/** Acquire the LSP service state for a session cwd. */
|
|
278
|
-
export function
|
|
370
|
+
export function getWorkspaceLspRuntime(cwd: string): WorkspaceLspRuntimeState {
|
|
279
371
|
return (
|
|
280
372
|
registry.get(cwd) ?? {
|
|
281
373
|
kind: "unavailable",
|
|
@@ -285,22 +377,22 @@ export function getSessionLspService(cwd: string): SessionLspServiceState {
|
|
|
285
377
|
}
|
|
286
378
|
|
|
287
379
|
/** Wait briefly for a pending session-scoped LSP service to become ready. */
|
|
288
|
-
export async function
|
|
380
|
+
export async function waitForWorkspaceLspRuntime(
|
|
289
381
|
cwd: string,
|
|
290
382
|
timeoutMs: number = 250,
|
|
291
|
-
): Promise<
|
|
383
|
+
): Promise<WorkspaceLspRuntimeState> {
|
|
292
384
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
293
|
-
let state =
|
|
385
|
+
let state = getWorkspaceLspRuntime(cwd);
|
|
294
386
|
|
|
295
387
|
while (state.kind === "pending" && Date.now() < deadline) {
|
|
296
388
|
await new Promise((resolve) => setTimeout(resolve, WAIT_INTERVAL_MS));
|
|
297
|
-
state =
|
|
389
|
+
state = getWorkspaceLspRuntime(cwd);
|
|
298
390
|
}
|
|
299
391
|
|
|
300
392
|
return state;
|
|
301
393
|
}
|
|
302
394
|
|
|
303
395
|
/** Remove the LSP service state for a session cwd. */
|
|
304
|
-
export function
|
|
396
|
+
export function clearWorkspaceLspRuntime(cwd: string): void {
|
|
305
397
|
registry.clear(cwd);
|
|
306
398
|
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Abort controller/signal utility helpers.
|
|
3
|
-
*
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Combine a caller-provided abort controller with a generation timeout signal.
|
|
9
|
-
*
|
|
10
|
-
* Returns the combined signal and a cleanup function that removes both
|
|
11
|
-
* event listeners. Callers must invoke cleanup in a `finally` block
|
|
12
|
-
* to avoid listener leaks.
|
|
13
|
-
*/
|
|
14
|
-
export function combineAbortSignals(
|
|
15
|
-
abort: AbortController,
|
|
16
|
-
timeoutMs: number,
|
|
17
|
-
): { signal: AbortSignal; cleanup: () => void } {
|
|
18
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
19
|
-
const combinedController = new AbortController();
|
|
20
|
-
const onAbort = () => combinedController.abort();
|
|
21
|
-
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
22
|
-
timeoutSignal.addEventListener("abort", onAbort, { once: true });
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
signal: combinedController.signal,
|
|
26
|
-
cleanup: () => {
|
|
27
|
-
abort.signal.removeEventListener("abort", onAbort);
|
|
28
|
-
timeoutSignal.removeEventListener("abort", onAbort);
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** 0-based position used by LSP and code-intelligence internally. */
|
|
2
|
-
export interface CodePosition {
|
|
3
|
-
line: number;
|
|
4
|
-
character: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/** Normalized location — flat replacement for LSP's nested Location/range shape. */
|
|
8
|
-
export interface CodeLocation {
|
|
9
|
-
uri: string;
|
|
10
|
-
range: { start: CodePosition; end: CodePosition };
|
|
11
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Abort controller/signal utility helpers.
|
|
3
|
-
*
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Combine a caller-provided abort controller with a generation timeout signal.
|
|
9
|
-
*
|
|
10
|
-
* Returns the combined signal and a cleanup function that removes both
|
|
11
|
-
* event listeners. Callers must invoke cleanup in a `finally` block
|
|
12
|
-
* to avoid listener leaks.
|
|
13
|
-
*/
|
|
14
|
-
export function combineAbortSignals(
|
|
15
|
-
abort: AbortController,
|
|
16
|
-
timeoutMs: number,
|
|
17
|
-
): { signal: AbortSignal; cleanup: () => void } {
|
|
18
|
-
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
19
|
-
const combinedController = new AbortController();
|
|
20
|
-
const onAbort = () => combinedController.abort();
|
|
21
|
-
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
22
|
-
timeoutSignal.addEventListener("abort", onAbort, { once: true });
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
signal: combinedController.signal,
|
|
26
|
-
cleanup: () => {
|
|
27
|
-
abort.signal.removeEventListener("abort", onAbort);
|
|
28
|
-
timeoutSignal.removeEventListener("abort", onAbort);
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** 0-based position used by LSP and code-intelligence internally. */
|
|
2
|
-
export interface CodePosition {
|
|
3
|
-
line: number;
|
|
4
|
-
character: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/** Normalized location — flat replacement for LSP's nested Location/range shape. */
|
|
8
|
-
export interface CodeLocation {
|
|
9
|
-
uri: string;
|
|
10
|
-
range: { start: CodePosition; end: CodePosition };
|
|
11
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { Diagnostic, Hover, MarkedString, MarkupContent } from "../config/types.ts";
|
|
2
|
-
import type { LspManager } from "../manager/manager.ts";
|
|
3
|
-
import { resolveSessionPath } from "../utils.ts";
|
|
4
|
-
|
|
5
|
-
const AUGMENT_TIMEOUT_MS = 500;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Augment diagnostics with LSP hover and code_actions at the first severity-1 error.
|
|
9
|
-
* Silently returns null if LSP is unavailable, times out, or there are no errors.
|
|
10
|
-
*/
|
|
11
|
-
export async function augmentDiagnostics(
|
|
12
|
-
filePath: string,
|
|
13
|
-
diags: Diagnostic[],
|
|
14
|
-
manager: LspManager,
|
|
15
|
-
cwd: string,
|
|
16
|
-
): Promise<string | null> {
|
|
17
|
-
const firstError = diags.find((d) => d.severity === 1);
|
|
18
|
-
if (!firstError) return null;
|
|
19
|
-
|
|
20
|
-
const resolvedPath = resolveSessionPath(cwd, filePath);
|
|
21
|
-
const client = await manager.getClientForFile(resolvedPath);
|
|
22
|
-
if (!client) return null;
|
|
23
|
-
|
|
24
|
-
const pos = firstError.range.start;
|
|
25
|
-
|
|
26
|
-
const [hoverResult, codeActionsResult] = await Promise.all([
|
|
27
|
-
withTimeout(client.hover(resolvedPath, pos), AUGMENT_TIMEOUT_MS),
|
|
28
|
-
withTimeout(
|
|
29
|
-
client.codeActions(resolvedPath, { start: pos, end: pos }, { diagnostics: [firstError] }),
|
|
30
|
-
AUGMENT_TIMEOUT_MS,
|
|
31
|
-
),
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
const parts: string[] = [];
|
|
35
|
-
|
|
36
|
-
if (hoverResult) {
|
|
37
|
-
const hoverText = formatHoverForDiagnostics(hoverResult);
|
|
38
|
-
if (hoverText) parts.push(`💡 Hover info:\n${hoverText}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (codeActionsResult && codeActionsResult.length > 0) {
|
|
42
|
-
const titles = codeActionsResult.map((a) => a.title).join(", ");
|
|
43
|
-
parts.push(`💡 Available fix: ${titles}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return parts.length > 0 ? parts.join("\n") : null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Extract raw hover text for inline diagnostic augmentation.
|
|
51
|
-
* Intentionally strips markdown code-block framing (unlike formatHover)
|
|
52
|
-
* to keep augmentation concise and readable inside diagnostic output.
|
|
53
|
-
*/
|
|
54
|
-
function formatHoverForDiagnostics(hover: Hover): string {
|
|
55
|
-
const contents = hover.contents;
|
|
56
|
-
let text = "";
|
|
57
|
-
|
|
58
|
-
if (typeof contents === "string") {
|
|
59
|
-
text = contents;
|
|
60
|
-
} else if ("value" in contents) {
|
|
61
|
-
const mc = contents as MarkupContent | { language: string; value: string };
|
|
62
|
-
if ("kind" in mc) text = mc.value;
|
|
63
|
-
else text = mc.value;
|
|
64
|
-
} else if (Array.isArray(contents)) {
|
|
65
|
-
text = (contents as MarkedString[])
|
|
66
|
-
.map((c) => (typeof c === "string" ? c : c.value))
|
|
67
|
-
.join("\n");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const lines = text.split("\n").slice(0, 3);
|
|
71
|
-
return lines.join("\n");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function withTimeout<T>(promise: Promise<T | null>, ms: number): Promise<T | null> {
|
|
75
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
76
|
-
const timeoutPromise = new Promise<null>((resolve) => {
|
|
77
|
-
timer = setTimeout(() => resolve(null), ms);
|
|
78
|
-
});
|
|
79
|
-
const result = await Promise.race([promise, timeoutPromise]);
|
|
80
|
-
if (timer) clearTimeout(timer);
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// Diagnostic context formatting for the LSP extension.
|
|
2
|
-
// Extracted from guidance.ts to keep prompt surfaces separate from formatting logic.
|
|
3
|
-
|
|
4
|
-
import type { Diagnostic } from "../config/types.ts";
|
|
5
|
-
import type { OutstandingDiagnosticSummaryEntry } from "../manager/manager-types.ts";
|
|
6
|
-
import { splitSuppressionDiagnostics } from "./suppression-diagnostics.ts";
|
|
7
|
-
|
|
8
|
-
export const MAX_DETAILED_DIAGNOSTICS = 5;
|
|
9
|
-
const MAX_DETAIL_LINES_PER_FILE = 3;
|
|
10
|
-
|
|
11
|
-
interface DetailedDiagnostics {
|
|
12
|
-
file: string;
|
|
13
|
-
diagnostics: Diagnostic[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function formatDiagnosticsContext(
|
|
17
|
-
diagnostics: OutstandingDiagnosticSummaryEntry[],
|
|
18
|
-
maxFiles: number = 3,
|
|
19
|
-
detailed?: DetailedDiagnostics[],
|
|
20
|
-
staleWarning?: string | null,
|
|
21
|
-
): string | null {
|
|
22
|
-
if (diagnostics.length === 0) return null;
|
|
23
|
-
|
|
24
|
-
const totalDiags = diagnostics.reduce((sum, d) => sum + d.total, 0);
|
|
25
|
-
const detailMap = buildDetailMap(totalDiags, detailed);
|
|
26
|
-
|
|
27
|
-
const lines: string[] = [];
|
|
28
|
-
if (staleWarning) lines.push(staleWarning);
|
|
29
|
-
const visible = diagnostics.slice(0, maxFiles);
|
|
30
|
-
|
|
31
|
-
for (const entry of visible) {
|
|
32
|
-
lines.push(`- ${entry.file}: ${formatCounts(entry)}`);
|
|
33
|
-
appendDetailLines(lines, detailMap?.get(entry.file));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const remaining = diagnostics.length - visible.length;
|
|
37
|
-
if (remaining > 0) {
|
|
38
|
-
lines.push(`- +${remaining} more file${remaining === 1 ? "" : "s"}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
appendSuppressionCleanup(
|
|
42
|
-
lines,
|
|
43
|
-
visible.map((entry) => entry.file),
|
|
44
|
-
detailMap,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
return [
|
|
48
|
-
'<extension-context source="supi-lsp">',
|
|
49
|
-
"Outstanding diagnostics — fix these before proceeding:",
|
|
50
|
-
...lines,
|
|
51
|
-
"</extension-context>",
|
|
52
|
-
].join("\n");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function buildDetailMap(
|
|
56
|
-
totalDiags: number,
|
|
57
|
-
detailed?: DetailedDiagnostics[],
|
|
58
|
-
): Map<string, Diagnostic[]> | null {
|
|
59
|
-
if (totalDiags > MAX_DETAILED_DIAGNOSTICS || !detailed || detailed.length === 0) return null;
|
|
60
|
-
return new Map(detailed.map((d) => [d.file, d.diagnostics]));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function appendDetailLines(lines: string[], details?: Diagnostic[]): void {
|
|
64
|
-
if (!details) return;
|
|
65
|
-
for (const d of details.slice(0, MAX_DETAIL_LINES_PER_FILE)) {
|
|
66
|
-
const line = d.range.start.line + 1;
|
|
67
|
-
const char = d.range.start.character + 1;
|
|
68
|
-
const source = d.source ? ` ${d.source}` : "";
|
|
69
|
-
const msg = typeof d.message === "string" ? d.message : d.message.value;
|
|
70
|
-
lines.push(` L${line} C${char}${source}: ${msg}`);
|
|
71
|
-
}
|
|
72
|
-
if (details.length > MAX_DETAIL_LINES_PER_FILE) {
|
|
73
|
-
const extra = details.length - MAX_DETAIL_LINES_PER_FILE;
|
|
74
|
-
lines.push(` +${extra} more`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function appendSuppressionCleanup(
|
|
79
|
-
lines: string[],
|
|
80
|
-
visibleFiles: string[],
|
|
81
|
-
detailMap: Map<string, Diagnostic[]> | null,
|
|
82
|
-
): void {
|
|
83
|
-
if (!detailMap) return;
|
|
84
|
-
|
|
85
|
-
const suppressionLines: string[] = [];
|
|
86
|
-
for (const file of visibleFiles) {
|
|
87
|
-
const diagnostics = detailMap.get(file);
|
|
88
|
-
if (!diagnostics) continue;
|
|
89
|
-
|
|
90
|
-
const { suppressions } = splitSuppressionDiagnostics(diagnostics, 1);
|
|
91
|
-
if (suppressions.length === 0) continue;
|
|
92
|
-
|
|
93
|
-
suppressionLines.push(`- ${file}`);
|
|
94
|
-
appendDetailLines(suppressionLines, suppressions);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (suppressionLines.length === 0) return;
|
|
98
|
-
lines.push("", "Stale suppression comments — clean these up:", ...suppressionLines);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function diagnosticsContextFingerprint(content: string | null): string | null {
|
|
102
|
-
return content;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function formatCounts(entry: OutstandingDiagnosticSummaryEntry): string {
|
|
106
|
-
const counts: string[] = [];
|
|
107
|
-
if (entry.errors > 0) counts.push(pluralize(entry.errors, "error"));
|
|
108
|
-
if (entry.warnings > 0) counts.push(pluralize(entry.warnings, "warning"));
|
|
109
|
-
if (entry.information > 0) counts.push(pluralize(entry.information, "info"));
|
|
110
|
-
if (entry.hints > 0) counts.push(pluralize(entry.hints, "hint"));
|
|
111
|
-
return counts.join(", ");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function pluralize(count: number, word: string): string {
|
|
115
|
-
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
116
|
-
}
|