@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.
Files changed (74) 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 +29 -35
  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/debug-registry.ts +8 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +107 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/index.ts +4 -6
  14. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +54 -28
  15. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +91 -125
  16. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +10 -7
  17. package/package.json +3 -3
  18. package/src/client/client-diagnostics.ts +361 -0
  19. package/src/client/client.ts +46 -238
  20. package/src/manager/manager.ts +25 -33
  21. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +0 -120
  22. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +0 -75
  23. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +0 -42
  24. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +0 -221
  25. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +0 -357
  26. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +0 -11
  27. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +0 -119
  28. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +0 -36
  29. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +0 -31
  30. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/context.ts +0 -16
  31. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -287
  32. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +0 -41
  33. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +0 -57
  34. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +0 -36
  35. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +0 -201
  36. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +0 -134
  37. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +0 -44
  38. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/path.ts +0 -2
  39. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +0 -189
  40. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +0 -170
  41. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/project.ts +0 -15
  42. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +0 -4
  43. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +0 -93
  44. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/report.ts +0 -121
  45. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +0 -71
  46. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/session.ts +0 -8
  47. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +0 -373
  48. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +0 -102
  49. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +0 -15
  50. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +0 -76
  51. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +0 -487
  52. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +0 -141
  53. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +0 -118
  54. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +0 -3
  55. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings.ts +0 -33
  56. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +0 -11
  57. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +0 -68
  58. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/terminal.ts +0 -60
  59. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +0 -192
  60. package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +0 -42
  61. package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +0 -45
  62. package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +0 -119
  63. package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +0 -189
  64. package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +0 -373
  65. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-action-menu.ts +0 -102
  66. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +0 -15
  67. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +0 -141
  68. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +0 -118
  69. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +0 -3
  70. package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +0 -192
  71. package/src/client/client-refresh.ts +0 -234
  72. package/src/diagnostics/diagnostics.ts +0 -99
  73. package/src/diagnostics/suppression-diagnostics.ts +0 -60
  74. 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
+ }