@mrclrchtr/supi-lsp 7.4.0 → 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.
- package/README.md +29 -6
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +22 -4
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +110 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +6 -1
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +2 -1
- package/package.json +3 -3
- package/src/api.ts +25 -3
- package/src/client/client-diagnostic-host.ts +2 -1
- package/src/client/client-diagnostic-refresh.ts +5 -2
- package/src/client/client-diagnostics.ts +39 -6
- package/src/client/client-document-sync.ts +82 -12
- package/src/client/client-semantic-input-barrier.ts +28 -5
- package/src/client/client.ts +606 -30
- package/src/config/capabilities.ts +9 -0
- package/src/config/config.ts +69 -8
- package/src/config/operation-support.ts +196 -0
- package/src/config/server-config.ts +15 -0
- package/src/config/types.ts +9 -1
- package/src/manager/manager-project-info.ts +2 -0
- package/src/manager/manager-workspace-symbol.ts +31 -2
- package/src/manager/manager.ts +72 -2
- package/src/provider/lsp-refactor-provider.ts +9 -43
- package/src/provider/refactor-planning.ts +102 -50
- package/src/provider/semantic-edit-normalizer.ts +49 -3
- package/src/session/runtime-controller.ts +78 -18
- package/src/session/runtime-registry.ts +46 -2
- package/src/session/workspace-lsp-runtime.ts +14 -0
- package/src/workspace-path-policy.ts +30 -4
- package/src/config/lsp-settings.ts +0 -22
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
90
|
+
change: TextDocumentSyncKind,
|
|
35
91
|
): unknown[] {
|
|
36
|
-
if (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
136
|
-
observeDiskContent(uri: string, content: string):
|
|
137
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
206
|
-
observeDiskContent(uri: string, content: string):
|
|
207
|
-
|
|
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
|
-
): {
|
|
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 {
|