@mrclrchtr/supi-lsp 4.7.0 → 4.8.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.
Files changed (72) hide show
  1. package/node_modules/@mrclrchtr/supi-code-runtime/README.md +1 -1
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -15
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +1 -6
  4. package/node_modules/@mrclrchtr/supi-core/README.md +26 -34
  5. package/node_modules/@mrclrchtr/supi-core/package.json +4 -7
  6. package/node_modules/@mrclrchtr/supi-core/src/api.ts +4 -6
  7. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +0 -20
  8. package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +7 -1
  9. package/node_modules/@mrclrchtr/supi-core/src/config.ts +0 -1
  10. package/node_modules/@mrclrchtr/supi-core/src/context.ts +1 -9
  11. package/node_modules/@mrclrchtr/supi-core/src/index.ts +4 -6
  12. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +54 -28
  13. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +91 -125
  14. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +10 -7
  15. package/package.json +3 -3
  16. package/src/client/client-diagnostics.ts +361 -0
  17. package/src/client/client.ts +46 -238
  18. package/src/manager/manager.ts +25 -33
  19. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +0 -120
  20. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +0 -75
  21. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -42
  22. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +0 -221
  23. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +0 -357
  24. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +0 -11
  25. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +0 -119
  26. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +0 -36
  27. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +0 -31
  28. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context.ts +0 -16
  29. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -287
  30. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +0 -41
  31. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +0 -57
  32. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -36
  33. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -201
  34. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +0 -134
  35. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +0 -44
  36. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path.ts +0 -2
  37. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +0 -189
  38. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +0 -170
  39. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/project.ts +0 -15
  40. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +0 -4
  41. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +0 -93
  42. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/report.ts +0 -121
  43. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +0 -71
  44. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/session.ts +0 -8
  45. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +0 -373
  46. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +0 -102
  47. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +0 -15
  48. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +0 -76
  49. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +0 -487
  50. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +0 -141
  51. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +0 -118
  52. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +0 -3
  53. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +0 -33
  54. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +0 -11
  55. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +0 -68
  56. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/terminal.ts +0 -60
  57. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +0 -192
  58. package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +0 -42
  59. package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +0 -45
  60. package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +0 -119
  61. package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +0 -189
  62. package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +0 -373
  63. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +0 -102
  64. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +0 -15
  65. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +0 -141
  66. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +0 -118
  67. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +0 -3
  68. package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +0 -192
  69. package/src/client/client-refresh.ts +0 -234
  70. package/src/diagnostics/diagnostics.ts +0 -99
  71. package/src/diagnostics/suppression-diagnostics.ts +0 -60
  72. package/src/index.ts +0 -41
@@ -0,0 +1,361 @@
1
+ // LSP client document synchronization and diagnostic state.
2
+
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import type {
5
+ Diagnostic,
6
+ DocumentDiagnosticReport,
7
+ PublishDiagnosticsParams,
8
+ TextDocumentIdentifier,
9
+ TextDocumentItem,
10
+ VersionedTextDocumentIdentifier,
11
+ } from "../config/types.ts";
12
+ import { detectLanguageId, fileToUri, uriToFile } from "../utils.ts";
13
+
14
+ const DIAGNOSTIC_WAIT_MS = 3_000;
15
+
16
+ type OpenDocument = { version: number };
17
+
18
+ type DiagnosticCacheEntry = {
19
+ diagnostics: Diagnostic[];
20
+ receivedAt: number;
21
+ version?: number;
22
+ resultId?: string;
23
+ };
24
+
25
+ /** One file's stored diagnostics as consumed by manager-level collection. */
26
+ export interface DiagnosticEntry {
27
+ uri: string;
28
+ diagnostics: Diagnostic[];
29
+ }
30
+
31
+ interface ClientDiagnosticsHost {
32
+ isOperational(): boolean;
33
+ supportsPullDiagnostics(): boolean;
34
+ sendNotification(method: string, params: unknown): void;
35
+ pullDocumentDiagnostics(
36
+ uri: string,
37
+ previousResultId: string | undefined,
38
+ timeoutMs: number,
39
+ ): Promise<DocumentDiagnosticReport | null>;
40
+ }
41
+
42
+ /**
43
+ * Owns one LSP client's open documents, diagnostic cache, waiters, and refresh policy.
44
+ *
45
+ * The host supplies transport and readiness operations. Callers use `LspClient` as
46
+ * the external seam and do not access this state directly.
47
+ */
48
+ export class ClientDiagnostics {
49
+ readonly #openDocs = new Map<string, OpenDocument>();
50
+ readonly #diagnosticStore = new Map<string, DiagnosticCacheEntry>();
51
+ readonly #diagnosticWaiters = new Map<string, Array<() => void>>();
52
+
53
+ constructor(private readonly host: ClientDiagnosticsHost) {}
54
+
55
+ get openFiles(): string[] {
56
+ return Array.from(this.#openDocs.keys()).map(uriToFile);
57
+ }
58
+
59
+ /** Release all waiters and remove all document and diagnostic state. */
60
+ clear(): void {
61
+ this.#openDocs.clear();
62
+ this.#diagnosticStore.clear();
63
+ this.#releaseAllDiagnosticWaiters();
64
+ }
65
+
66
+ /** Open a document, or update it when it is already open. */
67
+ didOpen(filePath: string, content: string): void {
68
+ if (!this.host.isOperational()) return;
69
+
70
+ const uri = fileToUri(filePath);
71
+ if (this.#openDocs.has(uri)) {
72
+ this.didChange(filePath, content);
73
+ return;
74
+ }
75
+
76
+ const languageId = detectLanguageId(filePath);
77
+ this.#openDocs.set(uri, { version: 1 });
78
+ this.host.sendNotification("textDocument/didOpen", {
79
+ textDocument: {
80
+ uri,
81
+ languageId,
82
+ version: 1,
83
+ text: content,
84
+ } satisfies TextDocumentItem,
85
+ });
86
+ }
87
+
88
+ /** Update a document, or open it when it is not tracked yet. */
89
+ didChange(filePath: string, content: string): void {
90
+ if (!this.host.isOperational()) return;
91
+
92
+ const uri = fileToUri(filePath);
93
+ const doc = this.#openDocs.get(uri);
94
+ if (!doc) {
95
+ this.didOpen(filePath, content);
96
+ return;
97
+ }
98
+
99
+ doc.version++;
100
+ this.#sendDidChange(uri, doc.version, content);
101
+ }
102
+
103
+ /** Close a document and remove its cached diagnostic state. */
104
+ didClose(filePath: string): void {
105
+ const uri = fileToUri(filePath);
106
+ const wasOpen = this.#openDocs.has(uri);
107
+ this.#clearFileState(uri);
108
+
109
+ if (wasOpen && this.host.isOperational()) {
110
+ this.#sendDidClose(uri);
111
+ }
112
+ }
113
+
114
+ /** Remove missing open documents and diagnostics, and return their file paths. */
115
+ pruneMissingFiles(): string[] {
116
+ const uris = new Set([...this.#openDocs.keys(), ...this.#diagnosticStore.keys()]);
117
+ const removedFiles: string[] = [];
118
+
119
+ for (const uri of uris) {
120
+ const filePath = uriToFile(uri);
121
+ if (existsSync(filePath)) continue;
122
+
123
+ const wasOpen = this.#openDocs.has(uri);
124
+ this.#clearFileState(uri);
125
+ removedFiles.push(filePath);
126
+ if (wasOpen && this.host.isOperational()) this.#sendDidClose(uri);
127
+ }
128
+
129
+ return removedFiles;
130
+ }
131
+
132
+ getDiagnostics(filePath: string): Diagnostic[] {
133
+ return this.#diagnosticStore.get(fileToUri(filePath))?.diagnostics ?? [];
134
+ }
135
+
136
+ /** Return non-empty diagnostics for files that still exist. */
137
+ getAllDiagnostics(): DiagnosticEntry[] {
138
+ const result: DiagnosticEntry[] = [];
139
+ for (const [uri, entry] of this.#diagnosticStore) {
140
+ if (entry.diagnostics.length === 0 || !existsSync(uriToFile(uri))) continue;
141
+ result.push({ uri, diagnostics: entry.diagnostics });
142
+ }
143
+ return result;
144
+ }
145
+
146
+ /** Force the next pull refresh to request complete diagnostic reports. */
147
+ clearPullResultIds(): void {
148
+ for (const entry of this.#diagnosticStore.values()) delete entry.resultId;
149
+ }
150
+
151
+ /** Apply diagnostics received through `textDocument/publishDiagnostics`. */
152
+ handlePublishDiagnostics(params: PublishDiagnosticsParams): void {
153
+ if (params.version !== undefined && params.version !== null) {
154
+ const openDoc = this.#openDocs.get(params.uri);
155
+ if (openDoc && params.version < openDoc.version) return;
156
+ }
157
+
158
+ this.#diagnosticStore.set(params.uri, {
159
+ diagnostics: params.diagnostics,
160
+ receivedAt: Date.now(),
161
+ version: params.version ?? undefined,
162
+ });
163
+ this.#releaseDiagnosticWaiters(params.uri);
164
+ }
165
+
166
+ /** Re-read open documents, then collect pull diagnostics or wait for push diagnostics. */
167
+ async refreshOpenDiagnostics(
168
+ options: { maxWaitMs?: number; quietMs?: number } = {},
169
+ ): Promise<void> {
170
+ if (!this.host.isOperational()) return;
171
+
172
+ const maxWaitMs = options.maxWaitMs ?? 3_000;
173
+ const quietMs = options.quietMs ?? 200;
174
+ const syncStart = Date.now();
175
+
176
+ this.#resyncOpenDocuments();
177
+ if (this.#openDocs.size === 0) return;
178
+
179
+ if (this.host.supportsPullDiagnostics()) {
180
+ try {
181
+ await this.#pullDiagnosticsForOpenDocuments(syncStart, maxWaitMs);
182
+ return;
183
+ } catch {
184
+ // Pull diagnostics failed. Wait for push diagnostics instead.
185
+ }
186
+ }
187
+
188
+ await this.#waitForDiagnosticSettle(syncStart, maxWaitMs, quietMs);
189
+ }
190
+
191
+ /** Sync one file and return its diagnostics after pull or push collection. */
192
+ async syncAndWaitForDiagnostics(filePath: string, content: string): Promise<Diagnostic[]> {
193
+ const uri = fileToUri(filePath);
194
+ const syncStart = Date.now();
195
+ this.didChange(filePath, content);
196
+
197
+ if (this.host.supportsPullDiagnostics()) {
198
+ const remaining = DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart);
199
+ if (remaining > 0) {
200
+ try {
201
+ const pulled = await this.#pullDiagnosticsForUri(uri, remaining);
202
+ if (pulled) return this.getDiagnostics(filePath);
203
+ } catch {
204
+ // Pull diagnostics failed. Wait for push diagnostics instead.
205
+ }
206
+ }
207
+ }
208
+
209
+ await this.#waitForDiagnostics(uri, Math.max(0, DIAGNOSTIC_WAIT_MS - (Date.now() - syncStart)));
210
+ return this.getDiagnostics(filePath);
211
+ }
212
+
213
+ #resyncOpenDocuments(): void {
214
+ for (const [uri, doc] of this.#openDocs) {
215
+ const filePath = uriToFile(uri);
216
+ try {
217
+ if (!existsSync(filePath)) {
218
+ this.#clearFileState(uri);
219
+ this.#sendDidClose(uri);
220
+ continue;
221
+ }
222
+ const content = readFileSync(filePath, "utf-8");
223
+ doc.version++;
224
+ this.#sendDidChange(uri, doc.version, content);
225
+ } catch {
226
+ // Keep the document open when a transient read fails.
227
+ }
228
+ }
229
+ }
230
+
231
+ async #pullDiagnosticsForOpenDocuments(syncStart: number, maxWaitMs: number): Promise<void> {
232
+ const deadline = syncStart + maxWaitMs;
233
+ const uris = Array.from(this.#openDocs.keys());
234
+ const results = await Promise.allSettled(
235
+ uris.map(async (uri) => {
236
+ const remaining = deadline - Date.now();
237
+ if (remaining <= 0) throw new Error("pull diagnostic timeout");
238
+ return this.#pullDiagnosticsForUri(uri, remaining);
239
+ }),
240
+ );
241
+
242
+ const anySuccess = results.some((result) => result.status === "fulfilled" && result.value);
243
+ const hadFailure = results.some((result) => result.status === "rejected");
244
+ if ((hadFailure || !anySuccess) && uris.length > 0) {
245
+ throw new Error("pull diagnostics incomplete");
246
+ }
247
+ }
248
+
249
+ async #pullDiagnosticsForUri(uri: string, timeoutMs: number): Promise<boolean> {
250
+ const previousResultId = this.#diagnosticStore.get(uri)?.resultId;
251
+ const report = await this.host.pullDocumentDiagnostics(uri, previousResultId, timeoutMs);
252
+ if (!report) return false;
253
+ this.#applyPullReport(uri, report);
254
+ return true;
255
+ }
256
+
257
+ #applyPullReport(uri: string, report: DocumentDiagnosticReport): void {
258
+ if (report.kind === "full") {
259
+ this.#diagnosticStore.set(uri, {
260
+ diagnostics: report.items,
261
+ receivedAt: Date.now(),
262
+ resultId: report.resultId,
263
+ });
264
+ } else {
265
+ const current = this.#diagnosticStore.get(uri);
266
+ if (current) current.resultId = report.resultId;
267
+ }
268
+
269
+ for (const [relatedUri, relatedReport] of Object.entries(report.relatedDocuments ?? {})) {
270
+ if (relatedReport.kind !== "full") continue;
271
+ this.#diagnosticStore.set(relatedUri, {
272
+ diagnostics: relatedReport.items,
273
+ receivedAt: Date.now(),
274
+ resultId: relatedReport.resultId,
275
+ });
276
+ }
277
+ }
278
+
279
+ async #waitForDiagnosticSettle(
280
+ syncStart: number,
281
+ maxWaitMs: number,
282
+ quietMs: number,
283
+ ): Promise<void> {
284
+ const deadline = syncStart + maxWaitMs;
285
+ while (Date.now() < deadline) {
286
+ const lastReceived = this.#lastDiagnosticReceivedAfter(syncStart) || syncStart;
287
+ const elapsed = Date.now() - lastReceived;
288
+ if (elapsed >= quietMs) return;
289
+ await new Promise((resolve) =>
290
+ setTimeout(resolve, Math.min(quietMs - elapsed, deadline - Date.now(), 50)),
291
+ );
292
+ }
293
+ }
294
+
295
+ #lastDiagnosticReceivedAfter(afterTime: number): number {
296
+ let latest = 0;
297
+ for (const entry of this.#diagnosticStore.values()) {
298
+ if (entry.receivedAt > afterTime && entry.receivedAt > latest) latest = entry.receivedAt;
299
+ }
300
+ return latest;
301
+ }
302
+
303
+ #sendDidChange(uri: string, version: number, content: string): void {
304
+ this.host.sendNotification("textDocument/didChange", {
305
+ textDocument: { uri, version } satisfies VersionedTextDocumentIdentifier,
306
+ contentChanges: [{ text: content }],
307
+ });
308
+ }
309
+
310
+ #sendDidClose(uri: string): void {
311
+ this.host.sendNotification("textDocument/didClose", {
312
+ textDocument: { uri } satisfies TextDocumentIdentifier,
313
+ });
314
+ }
315
+
316
+ #clearFileState(uri: string): void {
317
+ this.#openDocs.delete(uri);
318
+ this.#diagnosticStore.delete(uri);
319
+ this.#releaseDiagnosticWaiters(uri);
320
+ }
321
+
322
+ #waitForDiagnostics(uri: string, timeoutMs: number): Promise<void> {
323
+ if (timeoutMs <= 0) return Promise.resolve();
324
+
325
+ return new Promise<void>((resolve) => {
326
+ const waiter = () => {
327
+ clearTimeout(timer);
328
+ this.#removeDiagnosticWaiter(uri, waiter);
329
+ resolve();
330
+ };
331
+ const timer = setTimeout(() => {
332
+ this.#removeDiagnosticWaiter(uri, waiter);
333
+ resolve();
334
+ }, timeoutMs);
335
+ const waiters = this.#diagnosticWaiters.get(uri) ?? [];
336
+ waiters.push(waiter);
337
+ this.#diagnosticWaiters.set(uri, waiters);
338
+ });
339
+ }
340
+
341
+ #removeDiagnosticWaiter(uri: string, waiter: () => void): void {
342
+ const waiters = this.#diagnosticWaiters.get(uri);
343
+ if (!waiters) return;
344
+ const next = waiters.filter((entry) => entry !== waiter);
345
+ if (next.length > 0) this.#diagnosticWaiters.set(uri, next);
346
+ else this.#diagnosticWaiters.delete(uri);
347
+ }
348
+
349
+ #releaseAllDiagnosticWaiters(): void {
350
+ for (const uri of Array.from(this.#diagnosticWaiters.keys())) {
351
+ this.#releaseDiagnosticWaiters(uri);
352
+ }
353
+ }
354
+
355
+ #releaseDiagnosticWaiters(uri: string): void {
356
+ const waiters = this.#diagnosticWaiters.get(uri);
357
+ if (!waiters) return;
358
+ this.#diagnosticWaiters.delete(uri);
359
+ for (const waiter of waiters) waiter();
360
+ }
361
+ }