@mrclrchtr/supi-lsp 4.7.0 → 4.9.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/node_modules/@mrclrchtr/supi-code-runtime/README.md +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -15
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +1 -6
- package/node_modules/@mrclrchtr/supi-core/README.md +29 -35
- package/node_modules/@mrclrchtr/supi-core/package.json +4 -7
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +4 -6
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +0 -20
- package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +7 -1
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +0 -1
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +1 -9
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +107 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +4 -6
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +54 -28
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +91 -125
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +10 -7
- package/package.json +3 -3
- package/src/client/client-diagnostics.ts +361 -0
- package/src/client/client.ts +46 -238
- package/src/manager/manager.ts +25 -33
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +0 -120
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +0 -75
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -42
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +0 -221
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +0 -357
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +0 -11
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +0 -119
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +0 -36
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +0 -31
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context.ts +0 -16
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -287
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +0 -41
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +0 -57
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -36
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -201
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +0 -134
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +0 -44
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path.ts +0 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +0 -189
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +0 -170
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/project.ts +0 -15
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +0 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +0 -93
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/report.ts +0 -121
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +0 -71
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/session.ts +0 -8
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +0 -373
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +0 -102
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +0 -15
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +0 -76
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +0 -487
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +0 -141
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +0 -118
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +0 -3
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +0 -33
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +0 -11
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +0 -68
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/terminal.ts +0 -60
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +0 -192
- package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +0 -42
- package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +0 -45
- package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +0 -119
- package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +0 -189
- package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +0 -373
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +0 -102
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +0 -15
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +0 -141
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +0 -118
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +0 -3
- package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +0 -192
- package/src/client/client-refresh.ts +0 -234
- package/src/diagnostics/diagnostics.ts +0 -99
- package/src/diagnostics/suppression-diagnostics.ts +0 -60
- package/src/index.ts +0 -41
package/src/client/client.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// LSP Client — wraps a server process + JsonRpcClient.
|
|
2
2
|
// Handles initialize handshake, document sync, shutdown, and crash recovery.
|
|
3
3
|
|
|
4
|
-
// biome-ignore lint/style/noExcessiveLinesPerFile:
|
|
4
|
+
// biome-ignore lint/style/noExcessiveLinesPerFile: process lifecycle, readiness, and protocol requests stay in one client wrapper; document and diagnostic state is delegated.
|
|
5
5
|
import { type ChildProcess, execSync, spawn } from "node:child_process";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
6
|
import * as path from "node:path";
|
|
8
7
|
import {
|
|
9
8
|
type CodeQueryResult,
|
|
@@ -18,6 +17,7 @@ import type {
|
|
|
18
17
|
CodeActionContext,
|
|
19
18
|
Diagnostic,
|
|
20
19
|
DidChangeWatchedFilesParams,
|
|
20
|
+
DocumentDiagnosticReport,
|
|
21
21
|
DocumentSymbol,
|
|
22
22
|
FileEvent,
|
|
23
23
|
Hover,
|
|
@@ -30,17 +30,14 @@ import type {
|
|
|
30
30
|
ServerCapabilities,
|
|
31
31
|
ServerConfig,
|
|
32
32
|
SymbolInformation,
|
|
33
|
-
TextDocumentIdentifier,
|
|
34
|
-
TextDocumentItem,
|
|
35
|
-
VersionedTextDocumentIdentifier,
|
|
36
33
|
WorkspaceEdit,
|
|
37
34
|
WorkspaceSymbol,
|
|
38
35
|
} from "../config/types.ts";
|
|
39
|
-
import {
|
|
36
|
+
import { fileToUri } from "../utils.ts";
|
|
37
|
+
import { ClientDiagnostics, type DiagnosticEntry } from "./client-diagnostics.ts";
|
|
40
38
|
import { JsonRpcClient, JsonRpcRequestError } from "./transport.ts";
|
|
41
39
|
|
|
42
40
|
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
43
|
-
const DIAGNOSTIC_WAIT_MS = 3_000;
|
|
44
41
|
|
|
45
42
|
/** Race an operation against a timeout without retaining the timer after settlement. */
|
|
46
43
|
async function withTimeout<T>(
|
|
@@ -89,19 +86,6 @@ function killProcessTree(pid: number): void {
|
|
|
89
86
|
// ── Types ─────────────────────────────────────────────────────────────
|
|
90
87
|
export type ClientStatus = "initializing" | "running" | "error" | "shutdown";
|
|
91
88
|
|
|
92
|
-
export interface DiagnosticEntry {
|
|
93
|
-
uri: string;
|
|
94
|
-
diagnostics: Diagnostic[];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Internal metadata tracked alongside cached diagnostics. */
|
|
98
|
-
export interface DiagnosticCacheEntry {
|
|
99
|
-
diagnostics: Diagnostic[];
|
|
100
|
-
receivedAt: number;
|
|
101
|
-
version?: number;
|
|
102
|
-
resultId?: string;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
89
|
// ── LspClient ─────────────────────────────────────────────────────────
|
|
106
90
|
export class LspClient {
|
|
107
91
|
readonly name: string;
|
|
@@ -111,13 +95,7 @@ export class LspClient {
|
|
|
111
95
|
private rpc: JsonRpcClient | null = null;
|
|
112
96
|
private _status: ClientStatus = "initializing";
|
|
113
97
|
private capabilities: ServerCapabilities | null = null;
|
|
114
|
-
|
|
115
|
-
/** Open documents: uri → { version, languageId } */
|
|
116
|
-
private openDocs = new Map<string, { version: number; languageId: string }>();
|
|
117
|
-
/** Per-file diagnostics with freshness metadata */
|
|
118
|
-
private diagnosticStore = new Map<string, DiagnosticCacheEntry>();
|
|
119
|
-
/** Listeners waiting for diagnostics on a specific uri */
|
|
120
|
-
private diagnosticWaiters = new Map<string, Array<() => void>>();
|
|
98
|
+
private readonly diagnostics: ClientDiagnostics;
|
|
121
99
|
|
|
122
100
|
// ── Readiness (work-done-progress) ──────────────────────────────────
|
|
123
101
|
private trackedTokens = new Map<ProgressToken, "begin-seen" | "ended">();
|
|
@@ -135,6 +113,23 @@ export class LspClient {
|
|
|
135
113
|
) {
|
|
136
114
|
this.name = name;
|
|
137
115
|
this.root = root;
|
|
116
|
+
this.diagnostics = new ClientDiagnostics({
|
|
117
|
+
isOperational: () => this.rpc !== null && this._status === "running",
|
|
118
|
+
supportsPullDiagnostics: () => this.hasDiagnosticProvider,
|
|
119
|
+
sendNotification: (method, params) => {
|
|
120
|
+
if (this.rpc) void this.rpc.sendNotification(method, params);
|
|
121
|
+
},
|
|
122
|
+
pullDocumentDiagnostics: async (uri, previousResultId, timeoutMs) => {
|
|
123
|
+
const rpc = this.rpc;
|
|
124
|
+
if (!rpc || this._status !== "running") throw new Error("client not running");
|
|
125
|
+
await this.getReady();
|
|
126
|
+
return rpc.sendRequest(
|
|
127
|
+
"textDocument/diagnostic",
|
|
128
|
+
{ textDocument: { uri }, previousResultId },
|
|
129
|
+
{ timeoutMs },
|
|
130
|
+
) as Promise<DocumentDiagnosticReport>;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
138
133
|
}
|
|
139
134
|
|
|
140
135
|
get status(): ClientStatus {
|
|
@@ -142,7 +137,7 @@ export class LspClient {
|
|
|
142
137
|
}
|
|
143
138
|
|
|
144
139
|
get openFiles(): string[] {
|
|
145
|
-
return
|
|
140
|
+
return this.diagnostics.openFiles;
|
|
146
141
|
}
|
|
147
142
|
|
|
148
143
|
get serverCapabilities(): ServerCapabilities | null {
|
|
@@ -277,9 +272,7 @@ export class LspClient {
|
|
|
277
272
|
}
|
|
278
273
|
}
|
|
279
274
|
|
|
280
|
-
this.
|
|
281
|
-
this.diagnosticStore.clear();
|
|
282
|
-
this.releaseAllDiagnosticWaiters();
|
|
275
|
+
this.diagnostics.clear();
|
|
283
276
|
|
|
284
277
|
// Clear readiness state
|
|
285
278
|
if (this.noProgressTimer) {
|
|
@@ -291,126 +284,40 @@ export class LspClient {
|
|
|
291
284
|
this.rejectReady(new Error("Client shutdown"));
|
|
292
285
|
}
|
|
293
286
|
|
|
294
|
-
// ── Document Synchronization
|
|
295
|
-
/** Open a document
|
|
287
|
+
// ── Document Synchronization and Diagnostics ────────────────────────
|
|
288
|
+
/** Open a document, or update it when it is already open. */
|
|
296
289
|
didOpen(filePath: string, content: string): void {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const uri = fileToUri(filePath);
|
|
300
|
-
const languageId = detectLanguageId(filePath);
|
|
301
|
-
|
|
302
|
-
if (this.openDocs.has(uri)) {
|
|
303
|
-
// Already open — send didChange instead
|
|
304
|
-
this.didChange(filePath, content);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
this.openDocs.set(uri, { version: 1, languageId });
|
|
309
|
-
void this.rpc.sendNotification("textDocument/didOpen", {
|
|
310
|
-
textDocument: {
|
|
311
|
-
uri,
|
|
312
|
-
languageId,
|
|
313
|
-
version: 1,
|
|
314
|
-
text: content,
|
|
315
|
-
} satisfies TextDocumentItem,
|
|
316
|
-
});
|
|
290
|
+
this.diagnostics.didOpen(filePath, content);
|
|
317
291
|
}
|
|
318
292
|
|
|
319
|
-
/**
|
|
293
|
+
/** Update a document, or open it when it is not tracked yet. */
|
|
320
294
|
didChange(filePath: string, content: string): void {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const uri = fileToUri(filePath);
|
|
324
|
-
const doc = this.openDocs.get(uri);
|
|
325
|
-
|
|
326
|
-
if (!doc) {
|
|
327
|
-
// Not yet open — do a didOpen
|
|
328
|
-
this.didOpen(filePath, content);
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
doc.version++;
|
|
333
|
-
void this.rpc.sendNotification("textDocument/didChange", {
|
|
334
|
-
textDocument: { uri, version: doc.version } satisfies VersionedTextDocumentIdentifier,
|
|
335
|
-
contentChanges: [{ text: content }],
|
|
336
|
-
});
|
|
295
|
+
this.diagnostics.didChange(filePath, content);
|
|
337
296
|
}
|
|
338
297
|
|
|
339
|
-
/** Close a document and
|
|
298
|
+
/** Close a document and remove its cached diagnostic state. */
|
|
340
299
|
didClose(filePath: string): void {
|
|
341
|
-
|
|
342
|
-
const wasOpen = this.openDocs.has(uri);
|
|
343
|
-
|
|
344
|
-
this.clearFileState(uri);
|
|
345
|
-
|
|
346
|
-
if (!wasOpen || !this.rpc || this._status !== "running") return;
|
|
347
|
-
|
|
348
|
-
void this.rpc.sendNotification("textDocument/didClose", {
|
|
349
|
-
textDocument: { uri } satisfies TextDocumentIdentifier,
|
|
350
|
-
});
|
|
300
|
+
this.diagnostics.didClose(filePath);
|
|
351
301
|
}
|
|
352
302
|
|
|
353
|
-
/**
|
|
303
|
+
/** Remove missing document and diagnostic state, and return the removed paths. */
|
|
354
304
|
pruneMissingFiles(): string[] {
|
|
355
|
-
|
|
356
|
-
const removedFiles: string[] = [];
|
|
357
|
-
|
|
358
|
-
for (const uri of uris) {
|
|
359
|
-
const filePath = uriToFile(uri);
|
|
360
|
-
if (existsSync(filePath)) continue;
|
|
361
|
-
|
|
362
|
-
const wasOpen = this.openDocs.has(uri);
|
|
363
|
-
this.clearFileState(uri);
|
|
364
|
-
removedFiles.push(filePath);
|
|
365
|
-
|
|
366
|
-
if (wasOpen && this.rpc && this._status === "running") {
|
|
367
|
-
void this.rpc.sendNotification("textDocument/didClose", {
|
|
368
|
-
textDocument: { uri } satisfies TextDocumentIdentifier,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return removedFiles;
|
|
305
|
+
return this.diagnostics.pruneMissingFiles();
|
|
374
306
|
}
|
|
375
307
|
|
|
376
|
-
|
|
377
|
-
/** Get stored diagnostics for a file. */
|
|
308
|
+
/** Return stored diagnostics for one file. */
|
|
378
309
|
getDiagnostics(filePath: string): Diagnostic[] {
|
|
379
|
-
return this.
|
|
310
|
+
return this.diagnostics.getDiagnostics(filePath);
|
|
380
311
|
}
|
|
381
312
|
|
|
382
|
-
/**
|
|
383
|
-
* Get all stored diagnostics across all files.
|
|
384
|
-
*
|
|
385
|
-
* Filters out empty entries and — defensively — files that no longer exist
|
|
386
|
-
* on disk. The latter guards against a race where a `publishDiagnostics`
|
|
387
|
-
* notification (already in-flight) arrives *after* `pruneMissingFiles()`
|
|
388
|
-
* has removed the file from `openDocs`, recreating a stale cache entry.
|
|
389
|
-
*/
|
|
313
|
+
/** Return non-empty diagnostics for files that still exist. */
|
|
390
314
|
getAllDiagnostics(): DiagnosticEntry[] {
|
|
391
|
-
|
|
392
|
-
for (const [uri, entry] of this.diagnosticStore) {
|
|
393
|
-
if (entry.diagnostics.length === 0) continue;
|
|
394
|
-
// Defensive: drop diagnostics for files deleted after prune. Late
|
|
395
|
-
// in-flight publishDiagnostics notifications can recreate stale entries.
|
|
396
|
-
if (!existsSync(uriToFile(uri))) continue;
|
|
397
|
-
result.push({ uri, diagnostics: entry.diagnostics });
|
|
398
|
-
}
|
|
399
|
-
return result;
|
|
315
|
+
return this.diagnostics.getAllDiagnostics();
|
|
400
316
|
}
|
|
401
317
|
|
|
402
|
-
/**
|
|
403
|
-
* Clear all pull-diagnostic result IDs, forcing full (not `unchanged`)
|
|
404
|
-
* pull diagnostic responses on the next refresh cycle.
|
|
405
|
-
*
|
|
406
|
-
* Use this after file creation/write operations so that cross-file
|
|
407
|
-
* diagnostics (e.g., `Cannot find module` errors in importing files)
|
|
408
|
-
* are fully re-computed instead of returned as `unchanged`.
|
|
409
|
-
*/
|
|
318
|
+
/** Force the next pull refresh to request complete diagnostic reports. */
|
|
410
319
|
clearPullResultIds(): void {
|
|
411
|
-
|
|
412
|
-
delete entry.resultId;
|
|
413
|
-
}
|
|
320
|
+
this.diagnostics.clearPullResultIds();
|
|
414
321
|
}
|
|
415
322
|
|
|
416
323
|
/** Check if server supports pull diagnostics. */
|
|
@@ -426,46 +333,16 @@ export class LspClient {
|
|
|
426
333
|
} satisfies DidChangeWatchedFilesParams);
|
|
427
334
|
}
|
|
428
335
|
|
|
429
|
-
/**
|
|
430
|
-
* Re-read and re-sync all currently open, existing documents.
|
|
431
|
-
* Delegates to client-refresh module.
|
|
432
|
-
*/
|
|
336
|
+
/** Re-read open documents, then collect pull diagnostics or wait for push diagnostics. */
|
|
433
337
|
async refreshOpenDiagnostics(
|
|
434
338
|
options: { maxWaitMs?: number; quietMs?: number } = {},
|
|
435
339
|
): Promise<void> {
|
|
436
|
-
|
|
437
|
-
return refreshClientOpenDiagnostics(this, options);
|
|
340
|
+
return this.diagnostics.refreshOpenDiagnostics(options);
|
|
438
341
|
}
|
|
439
342
|
|
|
440
|
-
/**
|
|
441
|
-
* Sync a file and wait for diagnostics (up to timeout).
|
|
442
|
-
* Returns diagnostics for the file.
|
|
443
|
-
*/
|
|
343
|
+
/** Sync one file and return its diagnostics after pull or push collection. */
|
|
444
344
|
async syncAndWaitForDiagnostics(filePath: string, content: string): Promise<Diagnostic[]> {
|
|
445
|
-
|
|
446
|
-
const syncStart = Date.now();
|
|
447
|
-
|
|
448
|
-
// Sync the content
|
|
449
|
-
this.didChange(filePath, content);
|
|
450
|
-
|
|
451
|
-
// Prefer pull diagnostics when available, but fall back to push notifications.
|
|
452
|
-
if (this.hasDiagnosticProvider) {
|
|
453
|
-
const remaining = DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart);
|
|
454
|
-
if (remaining > 0) {
|
|
455
|
-
try {
|
|
456
|
-
const { pullDiagnosticsForUri } = await import("./client-refresh.ts");
|
|
457
|
-
const pulled = await pullDiagnosticsForUri(this, uri, remaining);
|
|
458
|
-
if (pulled) {
|
|
459
|
-
return this.getDiagnostics(filePath);
|
|
460
|
-
}
|
|
461
|
-
} catch {
|
|
462
|
-
// Pull diagnostics failed — fall back to push wait
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
await this.waitForDiagnostics(uri, Math.max(0, DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart)));
|
|
468
|
-
return this.getDiagnostics(filePath);
|
|
345
|
+
return this.diagnostics.syncAndWaitForDiagnostics(filePath, content);
|
|
469
346
|
}
|
|
470
347
|
|
|
471
348
|
// ── LSP Requests ───────────────────────────────────────────────────
|
|
@@ -611,78 +488,9 @@ export class LspClient {
|
|
|
611
488
|
return items.map(() => null);
|
|
612
489
|
}
|
|
613
490
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (params.version !== undefined && params.version !== null) {
|
|
618
|
-
const openDoc = this.openDocs.get(params.uri);
|
|
619
|
-
if (openDoc && params.version < openDoc.version) {
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
this.diagnosticStore.set(params.uri, {
|
|
625
|
-
diagnostics: params.diagnostics,
|
|
626
|
-
receivedAt: Date.now(),
|
|
627
|
-
version: params.version ?? undefined,
|
|
628
|
-
});
|
|
629
|
-
this.releaseDiagnosticWaiters(params.uri);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/** Wait for diagnostics on a URI, resolving on publication or timeout. */
|
|
633
|
-
private waitForDiagnostics(uri: string, timeoutMs: number): Promise<void> {
|
|
634
|
-
if (timeoutMs <= 0) return Promise.resolve();
|
|
635
|
-
|
|
636
|
-
return new Promise<void>((resolve) => {
|
|
637
|
-
const waiter = () => {
|
|
638
|
-
clearTimeout(timer);
|
|
639
|
-
this.removeDiagnosticWaiter(uri, waiter);
|
|
640
|
-
resolve();
|
|
641
|
-
};
|
|
642
|
-
const timer = setTimeout(() => {
|
|
643
|
-
this.removeDiagnosticWaiter(uri, waiter);
|
|
644
|
-
resolve();
|
|
645
|
-
}, timeoutMs);
|
|
646
|
-
const waiters = this.diagnosticWaiters.get(uri) ?? [];
|
|
647
|
-
waiters.push(waiter);
|
|
648
|
-
this.diagnosticWaiters.set(uri, waiters);
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/** Clear all per-file state and wake any diagnostics callers waiting on this URI. */
|
|
653
|
-
private clearFileState(uri: string): void {
|
|
654
|
-
this.openDocs.delete(uri);
|
|
655
|
-
this.diagnosticStore.delete(uri);
|
|
656
|
-
this.releaseDiagnosticWaiters(uri);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/** Remove a single pending diagnostics waiter, usually after its timeout fires. */
|
|
660
|
-
private removeDiagnosticWaiter(uri: string, waiter: () => void): void {
|
|
661
|
-
const waiters = this.diagnosticWaiters.get(uri);
|
|
662
|
-
if (!waiters) return;
|
|
663
|
-
|
|
664
|
-
const next = waiters.filter((entry) => entry !== waiter);
|
|
665
|
-
if (next.length > 0) {
|
|
666
|
-
this.diagnosticWaiters.set(uri, next);
|
|
667
|
-
} else {
|
|
668
|
-
this.diagnosticWaiters.delete(uri);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/** Wake every pending diagnostics waiter during shutdown or bulk cleanup. */
|
|
673
|
-
private releaseAllDiagnosticWaiters(): void {
|
|
674
|
-
for (const uri of Array.from(this.diagnosticWaiters.keys())) {
|
|
675
|
-
this.releaseDiagnosticWaiters(uri);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/** Wake all diagnostics waiters for a URI and remove them from the waiter map. */
|
|
680
|
-
private releaseDiagnosticWaiters(uri: string): void {
|
|
681
|
-
const waiters = this.diagnosticWaiters.get(uri);
|
|
682
|
-
if (!waiters) return;
|
|
683
|
-
|
|
684
|
-
this.diagnosticWaiters.delete(uri);
|
|
685
|
-
for (const waiter of waiters) waiter();
|
|
491
|
+
/** Apply a diagnostic publication received from the LSP transport. */
|
|
492
|
+
handlePublishDiagnostics(params: PublishDiagnosticsParams): void {
|
|
493
|
+
this.diagnostics.handlePublishDiagnostics(params);
|
|
686
494
|
}
|
|
687
495
|
|
|
688
496
|
// ── Readiness (work-done-progress) ──────────────────────────────────
|
package/src/manager/manager.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
FileEvent,
|
|
13
13
|
LspConfig,
|
|
14
14
|
ProjectServerInfo,
|
|
15
|
+
ServerConfig,
|
|
15
16
|
SymbolInformation,
|
|
16
17
|
WorkspaceSymbol,
|
|
17
18
|
} from "../config/types.ts";
|
|
@@ -65,6 +66,13 @@ import {
|
|
|
65
66
|
|
|
66
67
|
type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
|
|
67
68
|
|
|
69
|
+
type FileRoute = {
|
|
70
|
+
serverName: string;
|
|
71
|
+
serverConfig: ServerConfig;
|
|
72
|
+
root: string;
|
|
73
|
+
key: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
68
76
|
// ── LspManager ────────────────────────────────────────────────────────
|
|
69
77
|
export class LspManager {
|
|
70
78
|
/** Active clients keyed by "serverName:root" */
|
|
@@ -96,30 +104,16 @@ export class LspManager {
|
|
|
96
104
|
this.excludePatterns = patterns;
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
/** Check whether any configured language server handles the given file's extension. */
|
|
100
|
-
hasServerForExtension(filePath: string): boolean {
|
|
101
|
-
return getServerForFile(this.config, resolveSessionPath(this.cwd, filePath)) !== null;
|
|
102
|
-
}
|
|
103
107
|
// ── Public API ────────────────────────────────────────────────────
|
|
104
108
|
registerDetectedServers(detected: DetectedProjectServer[]): void {
|
|
105
109
|
this.knownRoots = projectRoots.buildKnownRootsMap(detected);
|
|
106
110
|
}
|
|
107
111
|
/** Check whether a file path has an available LSP server for explicit semantic operations. */
|
|
108
112
|
canServeFile(filePath: string): boolean {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
// Mirror getClientForFile's root resolution so the unavailable check stays
|
|
114
|
-
// root-specific. A failed startup in one workspace must not suppress
|
|
115
|
-
// activation for unrelated roots served by the same language server.
|
|
116
|
-
const root = resolveRootForFile(resolvedPath, serverName, serverConfig.rootMarkers, {
|
|
117
|
-
knownRoots: this.knownRoots,
|
|
118
|
-
cwd: this.cwd,
|
|
119
|
-
});
|
|
120
|
-
const key = clientKey(serverName, root);
|
|
121
|
-
if (this.getUnavailableReason(key, serverConfig.command)) return false;
|
|
122
|
-
return this.isServerCommandAvailable(serverConfig.command);
|
|
113
|
+
const route = this.resolveFileRoute(filePath);
|
|
114
|
+
if (!route) return false;
|
|
115
|
+
if (this.getUnavailableReason(route.key, route.serverConfig.command)) return false;
|
|
116
|
+
return this.isServerCommandAvailable(route.serverConfig.command);
|
|
123
117
|
}
|
|
124
118
|
|
|
125
119
|
/**
|
|
@@ -160,15 +154,8 @@ export class LspManager {
|
|
|
160
154
|
}
|
|
161
155
|
/** Get or create an LSP client for the given file. */
|
|
162
156
|
async getClientForFile(filePath: string): Promise<LspClient | null> {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
if (!match) return null;
|
|
166
|
-
const [serverName, serverConfig] = match;
|
|
167
|
-
const root = resolveRootForFile(resolvedPath, serverName, serverConfig.rootMarkers, {
|
|
168
|
-
knownRoots: this.knownRoots,
|
|
169
|
-
cwd: this.cwd,
|
|
170
|
-
});
|
|
171
|
-
return this.startServerForRoot(serverName, root);
|
|
157
|
+
const route = this.resolveFileRoute(filePath);
|
|
158
|
+
return route ? this.startServerForRoot(route.serverName, route.root) : null;
|
|
172
159
|
}
|
|
173
160
|
async startServerForRoot(serverName: string, root: string): Promise<LspClient | null> {
|
|
174
161
|
const serverConfig = this.config.servers[serverName];
|
|
@@ -213,7 +200,7 @@ export class LspManager {
|
|
|
213
200
|
*/
|
|
214
201
|
private async performStart(
|
|
215
202
|
serverName: string,
|
|
216
|
-
serverConfig:
|
|
203
|
+
serverConfig: ServerConfig,
|
|
217
204
|
root: string,
|
|
218
205
|
key: string,
|
|
219
206
|
): Promise<LspClient | null> {
|
|
@@ -241,8 +228,8 @@ export class LspManager {
|
|
|
241
228
|
}
|
|
242
229
|
}
|
|
243
230
|
|
|
244
|
-
/**
|
|
245
|
-
private
|
|
231
|
+
/** Resolve a file to its configured server and workspace-specific client key. */
|
|
232
|
+
private resolveFileRoute(filePath: string): FileRoute | null {
|
|
246
233
|
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
247
234
|
const match = getServerForFile(this.config, resolvedPath);
|
|
248
235
|
if (!match) return null;
|
|
@@ -251,7 +238,13 @@ export class LspManager {
|
|
|
251
238
|
knownRoots: this.knownRoots,
|
|
252
239
|
cwd: this.cwd,
|
|
253
240
|
});
|
|
254
|
-
return
|
|
241
|
+
return { serverName, serverConfig, root, key: clientKey(serverName, root) };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Find an already-started client for a file without spawning a new server. */
|
|
245
|
+
private getExistingClientForFile(filePath: string): LspClient | null {
|
|
246
|
+
const route = this.resolveFileRoute(filePath);
|
|
247
|
+
return route ? (this.clients.get(route.key) ?? null) : null;
|
|
255
248
|
}
|
|
256
249
|
|
|
257
250
|
/** Restart the clients that own the supplied file paths, if any are active. */
|
|
@@ -260,8 +253,7 @@ export class LspManager {
|
|
|
260
253
|
const seen = new Set<string>();
|
|
261
254
|
|
|
262
255
|
for (const filePath of filePaths) {
|
|
263
|
-
const
|
|
264
|
-
const client = this.getExistingClientForFile(resolvedPath);
|
|
256
|
+
const client = this.getExistingClientForFile(filePath);
|
|
265
257
|
if (!client) continue;
|
|
266
258
|
|
|
267
259
|
const key = clientKey(client.name, client.root);
|
package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<a href="https://github.com/mrclrchtr/supi/tree/main/packages/supi-core">
|
|
3
|
-
<img src="https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-core/assets/social-preview.png" alt="SuPi Core" width="100%">
|
|
4
|
-
</a>
|
|
5
|
-
</div>
|
|
6
|
-
|
|
7
|
-
# @mrclrchtr/supi-core
|
|
8
|
-
|
|
9
|
-
Shared infrastructure for SuPi extensions.
|
|
10
|
-
|
|
11
|
-
This is a **pure library** — it does not register any pi commands or tools. The `/supi-settings` command is now available through `@mrclrchtr/supi-settings`.
|
|
12
|
-
|
|
13
|
-
## Install
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
pnpm add @mrclrchtr/supi-core
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Package surfaces
|
|
20
|
-
|
|
21
|
-
- `@mrclrchtr/supi-core/api` — reusable helpers for other packages and extensions
|
|
22
|
-
- `@mrclrchtr/supi-core/report` — shared text/report rendering helpers for TUI and plain-text summaries
|
|
23
|
-
|
|
24
|
-
## What you get from the API
|
|
25
|
-
|
|
26
|
-
### Config helpers
|
|
27
|
-
|
|
28
|
-
- `loadSupiConfig()` — merged config with resolution order `defaults <- global <- project`
|
|
29
|
-
- `loadSupiConfigForScope()` — load one scope at a time for settings UIs
|
|
30
|
-
- `writeSupiConfig()` — persist values
|
|
31
|
-
- `removeSupiConfigKey()` — remove a key or override
|
|
32
|
-
|
|
33
|
-
Config file locations:
|
|
34
|
-
|
|
35
|
-
- global: `~/.pi/agent/supi/config.json`
|
|
36
|
-
- project: `.pi/supi/config.json`
|
|
37
|
-
|
|
38
|
-
### Settings helpers
|
|
39
|
-
|
|
40
|
-
- `registerDeclarativeSettings(pi, options)` — contribute a config-backed declarative settings section with source-aware scoped persistence
|
|
41
|
-
- `registerSettingsCommand(pi)` — register `/supi-settings` (used by `@mrclrchtr/supi-settings`)
|
|
42
|
-
- `openSettingsOverlay(pi, ctx)` — open the shared settings UI directly
|
|
43
|
-
- `createInputSubmenu()` — helper for simple text-entry submenus
|
|
44
|
-
- `createModelPickerSubmenu()` — helper for scoped model selection submenus, with optional host-owned choices and `disabled` control
|
|
45
|
-
|
|
46
|
-
The built-in settings UI supports:
|
|
47
|
-
|
|
48
|
-
- project/global scope toggle
|
|
49
|
-
- source badges for project, global, and default values
|
|
50
|
-
- Inherit/Reset actions that delete scoped config keys
|
|
51
|
-
- grouped extension sections
|
|
52
|
-
- searchable setting lists
|
|
53
|
-
|
|
54
|
-
### Context helpers
|
|
55
|
-
|
|
56
|
-
- `wrapExtensionContext()` — wrap injected text in SuPi's `<extension-context>` tag
|
|
57
|
-
- `findLastUserMessageIndex()`
|
|
58
|
-
- `getContextToken()`
|
|
59
|
-
- `getPromptContent()`
|
|
60
|
-
- `pruneAndReorderContextMessages()`
|
|
61
|
-
- `restorePromptContent()`
|
|
62
|
-
|
|
63
|
-
### Shared registries
|
|
64
|
-
|
|
65
|
-
- context-provider registry for `/supi-context`
|
|
66
|
-
- debug-event registry for producers that want shared debug capture
|
|
67
|
-
- settings registry used by `/supi-settings`
|
|
68
|
-
|
|
69
|
-
### Project and session helpers
|
|
70
|
-
|
|
71
|
-
- project-root detection and directory walking helpers such as `findProjectRoot()` and `walkProject()`
|
|
72
|
-
- active-branch session helper: `getActiveBranchEntries()`
|
|
73
|
-
- terminal helpers such as `formatTitle()`, `signalWaiting()`, and `signalDone()`
|
|
74
|
-
|
|
75
|
-
### Report helpers
|
|
76
|
-
|
|
77
|
-
- `clampReportWidth()` — enforce a minimum readable report width
|
|
78
|
-
- `formatReportTitle()` / `formatSectionHeader()` — shared themed headers
|
|
79
|
-
- `formatDimLine()` / `formatKeyValueLine()` — common summary rows
|
|
80
|
-
- `formatOverflowHint()` — consistent preview-overflow hints
|
|
81
|
-
- `wrapReportText()` — ANSI-aware wrapped report blocks with optional indentation
|
|
82
|
-
|
|
83
|
-
## Example
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
87
|
-
import { loadSupiConfig, registerDeclarativeSettings, wrapExtensionContext } from "@mrclrchtr/supi-core/api";
|
|
88
|
-
|
|
89
|
-
export default function myExtension(pi: ExtensionAPI) {
|
|
90
|
-
const defaults = { enabled: true };
|
|
91
|
-
const config = loadSupiConfig("my-extension", process.cwd(), defaults);
|
|
92
|
-
|
|
93
|
-
registerDeclarativeSettings(pi, {
|
|
94
|
-
id: "my-extension",
|
|
95
|
-
label: "My Extension",
|
|
96
|
-
section: "my-extension",
|
|
97
|
-
defaults,
|
|
98
|
-
fields: [
|
|
99
|
-
{
|
|
100
|
-
kind: "boolean" as const,
|
|
101
|
-
key: "enabled",
|
|
102
|
-
label: "Enabled",
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const message = wrapExtensionContext("my-extension", "hello", {
|
|
108
|
-
enabled: config.enabled,
|
|
109
|
-
});
|
|
110
|
-
void message;
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## Source
|
|
115
|
-
|
|
116
|
-
- `src/api.ts` — exported library surface
|
|
117
|
-
- `src/config.ts` — shared config loading and writing
|
|
118
|
-
- `src/config-settings.ts` — config-backed settings registration helper
|
|
119
|
-
- `src/settings-ui.ts` — shared settings overlay
|
|
120
|
-
- `src/report.ts` — shared text/report rendering helpers
|