@mrclrchtr/supi-lsp 7.3.1 → 8.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.
Files changed (32) hide show
  1. package/README.md +29 -6
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +1 -1
  4. package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +22 -4
  5. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +110 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/config.ts +6 -1
  8. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +2 -1
  9. package/package.json +3 -3
  10. package/src/api.ts +25 -3
  11. package/src/client/client-diagnostic-host.ts +2 -1
  12. package/src/client/client-diagnostic-refresh.ts +5 -2
  13. package/src/client/client-diagnostics.ts +39 -6
  14. package/src/client/client-document-sync.ts +82 -12
  15. package/src/client/client-semantic-input-barrier.ts +28 -5
  16. package/src/client/client.ts +606 -30
  17. package/src/config/capabilities.ts +9 -0
  18. package/src/config/config.ts +69 -8
  19. package/src/config/operation-support.ts +196 -0
  20. package/src/config/server-config.ts +15 -0
  21. package/src/config/types.ts +9 -1
  22. package/src/manager/manager-project-info.ts +2 -0
  23. package/src/manager/manager-workspace-symbol.ts +31 -2
  24. package/src/manager/manager.ts +72 -2
  25. package/src/provider/lsp-refactor-provider.ts +9 -43
  26. package/src/provider/refactor-planning.ts +102 -50
  27. package/src/provider/semantic-edit-normalizer.ts +49 -3
  28. package/src/session/runtime-controller.ts +78 -18
  29. package/src/session/runtime-registry.ts +46 -2
  30. package/src/session/workspace-lsp-runtime.ts +14 -0
  31. package/src/workspace-path-policy.ts +30 -4
  32. package/src/config/lsp-settings.ts +0 -22
@@ -1,5 +1,8 @@
1
1
  import { readFileSync } from "node:fs";
2
- import type { VersionedTextDocumentIdentifier } from "../config/types.ts";
2
+ import {
3
+ TextDocumentSyncKind,
4
+ type VersionedTextDocumentIdentifier,
5
+ } from "vscode-languageserver-protocol";
3
6
  import type { DiagnosticSynchronization } from "./client-diagnostic-evidence.ts";
4
7
  import type { DiagnosticWaitRegistry } from "./client-diagnostic-waiters.ts";
5
8
  import { fingerprintDocumentContent, type OpenDocumentState } from "./client-document-state.ts";
@@ -7,6 +10,59 @@ import { getDiagnosticFileState } from "./client-file-state.ts";
7
10
 
8
11
  type NotificationSender = (method: string, params: unknown) => void;
9
12
 
13
+ /** Normalized document lifecycle options negotiated with one LSP server. */
14
+ export interface NormalizedDocumentSync {
15
+ readonly openClose: boolean;
16
+ readonly change: TextDocumentSyncKind;
17
+ readonly save: boolean;
18
+ readonly includeText: boolean;
19
+ }
20
+
21
+ const NO_DOCUMENT_SYNC: NormalizedDocumentSync = Object.freeze({
22
+ openClose: false,
23
+ change: TextDocumentSyncKind.None,
24
+ save: false,
25
+ includeText: false,
26
+ });
27
+
28
+ /**
29
+ * Normalize structured and legacy numeric server synchronization options.
30
+ * Legacy non-none values use the historical full lifecycle defaults.
31
+ */
32
+ export function normalizeDocumentSync(value: unknown): NormalizedDocumentSync {
33
+ if (value === TextDocumentSyncKind.None) return NO_DOCUMENT_SYNC;
34
+ if (value === TextDocumentSyncKind.Full || value === TextDocumentSyncKind.Incremental) {
35
+ return Object.freeze({
36
+ openClose: true,
37
+ change: value,
38
+ save: true,
39
+ includeText: false,
40
+ });
41
+ }
42
+ if (!isRecord(value)) return NO_DOCUMENT_SYNC;
43
+
44
+ const change = isTextDocumentSyncKind(value.change) ? value.change : TextDocumentSyncKind.None;
45
+ const save = value.save === true || isRecord(value.save);
46
+ return Object.freeze({
47
+ openClose: value.openClose === true,
48
+ change,
49
+ save,
50
+ includeText: isRecord(value.save) && value.save.includeText === true,
51
+ });
52
+ }
53
+
54
+ function isTextDocumentSyncKind(value: unknown): value is TextDocumentSyncKind {
55
+ return (
56
+ value === TextDocumentSyncKind.None ||
57
+ value === TextDocumentSyncKind.Full ||
58
+ value === TextDocumentSyncKind.Incremental
59
+ );
60
+ }
61
+
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return typeof value === "object" && value !== null && !Array.isArray(value);
64
+ }
65
+
10
66
  interface SynchronizeDocumentOptions {
11
67
  uri: string;
12
68
  content: string;
@@ -14,7 +70,7 @@ interface SynchronizeDocumentOptions {
14
70
  version: number;
15
71
  synchronizationId: number;
16
72
  evidenceRevision: number;
17
- incrementalSync: boolean;
73
+ documentSync: NormalizedDocumentSync;
18
74
  waiters: DiagnosticWaitRegistry;
19
75
  sendNotification: NotificationSender;
20
76
  }
@@ -31,9 +87,9 @@ function endPosition(content: string): { line: number; character: number } {
31
87
  function contentChanges(
32
88
  previousContent: string,
33
89
  content: string,
34
- incrementalSync: boolean,
90
+ change: TextDocumentSyncKind,
35
91
  ): unknown[] {
36
- if (incrementalSync) {
92
+ if (change === TextDocumentSyncKind.Incremental) {
37
93
  return [
38
94
  {
39
95
  range: {
@@ -64,10 +120,11 @@ export function synchronizeDocument(options: SynchronizeDocumentOptions): void {
64
120
  const changes = contentChanges(
65
121
  options.document.content,
66
122
  options.content,
67
- options.incrementalSync,
123
+ options.documentSync.change,
68
124
  );
69
125
  options.document.content = options.content;
70
126
  options.document.contentFingerprint = fingerprintDocumentContent(options.content);
127
+ if (options.documentSync.change === TextDocumentSyncKind.None) return;
71
128
  options.sendNotification("textDocument/didChange", {
72
129
  textDocument: {
73
130
  uri: options.uri,
@@ -98,7 +155,7 @@ export function synchronizeTrackedDocument(options: {
98
155
  nextVersion(): number;
99
156
  nextSynchronizationId(): number;
100
157
  evidenceRevision: number;
101
- incrementalSync: boolean;
158
+ documentSync: NormalizedDocumentSync;
102
159
  waiters: DiagnosticWaitRegistry;
103
160
  sendNotification: NotificationSender;
104
161
  markUnversionedSyncMoment?(): void;
@@ -117,7 +174,7 @@ export function synchronizeTrackedDocument(options: {
117
174
  version: options.nextVersion(),
118
175
  synchronizationId: options.nextSynchronizationId(),
119
176
  evidenceRevision: options.evidenceRevision,
120
- incrementalSync: options.incrementalSync,
177
+ documentSync: options.documentSync,
121
178
  waiters: options.waiters,
122
179
  sendNotification: options.sendNotification,
123
180
  });
@@ -130,11 +187,13 @@ interface ResynchronizeDocumentsOptions {
130
187
  nextVersion(uri: string): number;
131
188
  nextSynchronizationId(): number;
132
189
  evidenceRevision: number;
190
+ documentSync: NormalizedDocumentSync;
133
191
  /** Invalidate route evidence before the first changed document is applied. */
134
192
  noteInputContentChange(): number;
135
- /** Record the verified disk content in the shared semantic barrier. */
136
- observeDiskContent(uri: string, content: string): void;
137
- incrementalSync: boolean;
193
+ /** Record the verified disk content and report whether its baseline changed. */
194
+ observeDiskContent(uri: string, content: string): boolean;
195
+ /** Send one save for an externally observed disk change. */
196
+ noteExternalDiskChange(uri: string, content: string): void;
138
197
  sendNotification: NotificationSender;
139
198
  uriToFile(uri: string): string;
140
199
  /** Disk content already read by classification; avoids a second read. */
@@ -157,6 +216,17 @@ export interface ResynchronizeDocumentsResult {
157
216
  failedFiles: string[];
158
217
  }
159
218
 
219
+ /** Send one save when a refresh observes a new disk fingerprint. */
220
+ function noteObservedDiskChange(
221
+ options: ResynchronizeDocumentsOptions,
222
+ uri: string,
223
+ content: string,
224
+ ): void {
225
+ if (options.observeDiskContent(uri, content)) {
226
+ options.noteExternalDiskChange(uri, content);
227
+ }
228
+ }
229
+
160
230
  /** Re-read and synchronize every existing open document. */
161
231
  export function resynchronizeOpenDocuments(
162
232
  options: ResynchronizeDocumentsOptions,
@@ -185,7 +255,7 @@ export function resynchronizeOpenDocuments(
185
255
  version: options.nextVersion(uri),
186
256
  synchronizationId: options.nextSynchronizationId(),
187
257
  evidenceRevision: synchronizationRevision,
188
- incrementalSync: options.incrementalSync,
258
+ documentSync: options.documentSync,
189
259
  waiters: options.waiters,
190
260
  sendNotification: options.sendNotification,
191
261
  });
@@ -200,7 +270,7 @@ export function resynchronizeOpenDocuments(
200
270
  waiters: options.waiters,
201
271
  });
202
272
  }
203
- options.observeDiskContent(uri, content);
273
+ noteObservedDiskChange(options, uri, content);
204
274
  options.clearFailedFile(uri);
205
275
  synchronizedUris.add(uri);
206
276
  if (contentChanged) resynchronizedUris.add(uri);
@@ -36,6 +36,8 @@ export interface SemanticInputBarrierHost {
36
36
  isOperational(): boolean;
37
37
  getOpenDocuments(): readonly SemanticInputDocument[];
38
38
  applyDocumentUpdates(updates: readonly SemanticInputUpdate[]): void;
39
+ /** Report disk changes that were observed during synchronization. */
40
+ noteExternalDiskChange?(filePath: string, content: string): void;
39
41
  closeMissingDocument(filePath: string): void;
40
42
  markUnreadableDocument(filePath: string): void;
41
43
  }
@@ -60,6 +62,7 @@ interface OpenDocumentRead {
60
62
 
61
63
  interface SynchronizationPlan {
62
64
  readonly updates: SemanticInputUpdate[];
65
+ readonly externalDiskChanges: Array<{ readonly filePath: string; readonly content: string }>;
63
66
  readonly observedDiskFingerprints: Map<string, string | undefined>;
64
67
  }
65
68
 
@@ -202,9 +205,13 @@ export class SemanticInputBarrier {
202
205
  this.#observedDiskFingerprints.set(uri, fingerprintDocumentContent(content));
203
206
  }
204
207
 
205
- /** Record content read from disk after a synchronization or refresh pass. */
206
- observeDiskContent(uri: string, content: string): void {
207
- this.#observedDiskFingerprints.set(uri, fingerprintDocumentContent(content));
208
+ /** Record content read from disk and report a changed known baseline. */
209
+ observeDiskContent(uri: string, content: string): boolean {
210
+ const fingerprint = fingerprintDocumentContent(content);
211
+ const previous = this.#observedDiskFingerprints.get(uri);
212
+ const changed = this.#observedDiskFingerprints.has(uri) && previous !== fingerprint;
213
+ this.#observedDiskFingerprints.set(uri, fingerprint);
214
+ return changed;
208
215
  }
209
216
 
210
217
  /** Forget a document's last observed disk content. */
@@ -322,19 +329,25 @@ export class SemanticInputBarrier {
322
329
  reads: readonly OpenDocumentRead[],
323
330
  ): SynchronizationPlan {
324
331
  const updates: SemanticInputUpdate[] = [];
332
+ const externalDiskChanges: Array<{ readonly filePath: string; readonly content: string }> = [];
325
333
  const observedDiskFingerprints = new Map<string, string | undefined>();
326
334
  for (const read of reads) {
327
335
  const planned = this.#planDocumentRead(pending, read);
328
336
  observedDiskFingerprints.set(read.document.uri, planned.observedDiskFingerprint);
329
337
  if (planned.update) updates.push(planned.update);
338
+ if (planned.externalDiskChange) externalDiskChanges.push(planned.externalDiskChange);
330
339
  }
331
- return { updates, observedDiskFingerprints };
340
+ return { updates, externalDiskChanges, observedDiskFingerprints };
332
341
  }
333
342
 
334
343
  #planDocumentRead(
335
344
  pending: PendingSynchronization,
336
345
  read: OpenDocumentRead,
337
- ): { observedDiskFingerprint: string | undefined; update?: SemanticInputUpdate } {
346
+ ): {
347
+ observedDiskFingerprint: string | undefined;
348
+ update?: SemanticInputUpdate;
349
+ externalDiskChange?: { readonly filePath: string; readonly content: string };
350
+ } {
338
351
  const override = pending.contentOverrides.get(read.document.uri);
339
352
  if (read.result.kind === "error") {
340
353
  return this.#planReadError(read.document, read.result.error, override);
@@ -343,6 +356,12 @@ export class SemanticInputBarrier {
343
356
  const diskFingerprint = fingerprintDocumentContent(read.result.content);
344
357
  const hasObservedDiskContent = this.#observedDiskFingerprints.has(read.document.uri);
345
358
  const previousDiskFingerprint = this.#observedDiskFingerprints.get(read.document.uri);
359
+ const externalDiskChange =
360
+ override === undefined &&
361
+ hasObservedDiskContent &&
362
+ previousDiskFingerprint !== diskFingerprint
363
+ ? { filePath: read.document.filePath, content: read.result.content }
364
+ : undefined;
346
365
  // An explicit override controls server content; the disk fingerprint remains the next observation.
347
366
  const content =
348
367
  override ??
@@ -352,6 +371,7 @@ export class SemanticInputBarrier {
352
371
  const contentFingerprint = fingerprintDocumentContent(content);
353
372
  return {
354
373
  observedDiskFingerprint: diskFingerprint,
374
+ ...(externalDiskChange ? { externalDiskChange } : {}),
355
375
  ...(read.document.contentFingerprint !== contentFingerprint
356
376
  ? { update: { document: read.document, content } }
357
377
  : {}),
@@ -385,6 +405,9 @@ export class SemanticInputBarrier {
385
405
  this.#advanceRevision("content");
386
406
  }
387
407
  this.#rememberObservedDiskFingerprints(plan.observedDiskFingerprints);
408
+ for (const change of plan.externalDiskChanges) {
409
+ this.#host.noteExternalDiskChange?.(change.filePath, change.content);
410
+ }
388
411
  }
389
412
 
390
413
  #advanceRevision(changeKind: SemanticInputChangeKind): void {