@mrclrchtr/supi-lsp 11.1.0 → 11.3.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 +81 -7
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +3 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +9 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/child-web-tools.ts +110 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +11 -0
- package/node_modules/semver/LICENSE +15 -0
- package/node_modules/semver/README.md +680 -0
- package/node_modules/semver/bin/semver.js +195 -0
- package/node_modules/semver/classes/comparator.js +143 -0
- package/node_modules/semver/classes/index.js +7 -0
- package/node_modules/semver/classes/range.js +577 -0
- package/node_modules/semver/classes/semver.js +350 -0
- package/node_modules/semver/functions/clean.js +8 -0
- package/node_modules/semver/functions/cmp.js +54 -0
- package/node_modules/semver/functions/coerce.js +62 -0
- package/node_modules/semver/functions/compare-build.js +9 -0
- package/node_modules/semver/functions/compare-loose.js +5 -0
- package/node_modules/semver/functions/compare.js +7 -0
- package/node_modules/semver/functions/diff.js +60 -0
- package/node_modules/semver/functions/eq.js +5 -0
- package/node_modules/semver/functions/gt.js +5 -0
- package/node_modules/semver/functions/gte.js +5 -0
- package/node_modules/semver/functions/inc.js +21 -0
- package/node_modules/semver/functions/lt.js +5 -0
- package/node_modules/semver/functions/lte.js +5 -0
- package/node_modules/semver/functions/major.js +5 -0
- package/node_modules/semver/functions/minor.js +5 -0
- package/node_modules/semver/functions/neq.js +5 -0
- package/node_modules/semver/functions/parse.js +18 -0
- package/node_modules/semver/functions/patch.js +5 -0
- package/node_modules/semver/functions/prerelease.js +8 -0
- package/node_modules/semver/functions/rcompare.js +5 -0
- package/node_modules/semver/functions/rsort.js +5 -0
- package/node_modules/semver/functions/satisfies.js +12 -0
- package/node_modules/semver/functions/sort.js +5 -0
- package/node_modules/semver/functions/truncate.js +48 -0
- package/node_modules/semver/functions/valid.js +8 -0
- package/node_modules/semver/index.js +93 -0
- package/node_modules/semver/internal/constants.js +37 -0
- package/node_modules/semver/internal/debug.js +11 -0
- package/node_modules/semver/internal/identifiers.js +29 -0
- package/node_modules/semver/internal/lrucache.js +42 -0
- package/node_modules/semver/internal/parse-options.js +17 -0
- package/node_modules/semver/internal/re.js +223 -0
- package/node_modules/semver/package.json +78 -0
- package/node_modules/semver/preload.js +4 -0
- package/node_modules/semver/range.bnf +17 -0
- package/node_modules/semver/ranges/gtr.js +6 -0
- package/node_modules/semver/ranges/intersects.js +9 -0
- package/node_modules/semver/ranges/ltr.js +6 -0
- package/node_modules/semver/ranges/max-satisfying.js +27 -0
- package/node_modules/semver/ranges/min-satisfying.js +26 -0
- package/node_modules/semver/ranges/min-version.js +63 -0
- package/node_modules/semver/ranges/outside.js +82 -0
- package/node_modules/semver/ranges/simplify.js +49 -0
- package/node_modules/semver/ranges/subset.js +249 -0
- package/node_modules/semver/ranges/to-comparators.js +10 -0
- package/node_modules/semver/ranges/valid.js +13 -0
- package/package.json +5 -3
- package/src/client/client-diagnostic-collection.ts +11 -4
- package/src/client/client-diagnostics.ts +77 -6
- package/src/client/client-input-admission.ts +149 -0
- package/src/client/client-typescript-project.ts +40 -0
- package/src/client/client.ts +174 -49
- package/src/config/config.ts +53 -2
- package/src/config/operation-support.ts +19 -4
- package/src/config/server-config.ts +22 -0
- package/src/config/typescript-launch.ts +276 -0
- package/src/diagnostics/evidence.ts +2 -2
- package/src/manager/manager-project-info.ts +22 -10
- package/src/manager/manager-workspace-symbol.ts +157 -23
- package/src/manager/manager.ts +306 -75
- package/src/provider/refactor-planning.ts +4 -3
- package/src/session/runtime-controller.ts +2 -2
- package/src/session/runtime-registry.ts +19 -2
- package/src/session/runtime-rename.ts +1 -0
- package/src/session/scanner.ts +27 -15
- package/src/session/workspace-lsp-runtime.ts +5 -1
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
} from "./client-document-state.ts";
|
|
48
48
|
import { clearTrackedDocumentState, synchronizeTrackedDocument } from "./client-document-sync.ts";
|
|
49
49
|
import { getDiagnosticFileState } from "./client-file-state.ts";
|
|
50
|
+
import { SemanticInputAdmission } from "./client-input-admission.ts";
|
|
50
51
|
import { SemanticInputRequestRetryGuard } from "./client-request-enrollment.ts";
|
|
51
52
|
import {
|
|
52
53
|
SemanticInputBarrier,
|
|
@@ -103,6 +104,7 @@ export class ClientDiagnostics {
|
|
|
103
104
|
/** URIs closed by a lifecycle operation: versioned pushes stay fail-closed. */
|
|
104
105
|
readonly #closedVersionedBarrier = new Set<string>();
|
|
105
106
|
readonly #inputBarrier: SemanticInputBarrier;
|
|
107
|
+
#inputAdmission = new SemanticInputAdmission();
|
|
106
108
|
#evidenceRevision = 0;
|
|
107
109
|
#nextSynchronizationId = 0;
|
|
108
110
|
/** One active server refresh plus one coalesced newer demand generation. */
|
|
@@ -125,6 +127,60 @@ export class ClientDiagnostics {
|
|
|
125
127
|
});
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
/** Keep first enrollment outside a semantic query or refactor-planning attempt. */
|
|
131
|
+
withSemanticRequest<T>(
|
|
132
|
+
run: () => Promise<T>,
|
|
133
|
+
control?: CodeRequestControl,
|
|
134
|
+
uri?: string,
|
|
135
|
+
): Promise<T> {
|
|
136
|
+
return this.#inputAdmission.request(run, control, uri);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Read content only after admission; tracked-document changes never wait. */
|
|
140
|
+
async openDocument(
|
|
141
|
+
filePath: string,
|
|
142
|
+
readContent: () => string,
|
|
143
|
+
control?: CodeRequestControl,
|
|
144
|
+
protectInputs = false,
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
throwIfCodeRequestInterrupted(control);
|
|
147
|
+
const open = () => {
|
|
148
|
+
throwIfCodeRequestInterrupted(control);
|
|
149
|
+
if (!this.host.isOperational()) throw new Error("LSP client is not running.");
|
|
150
|
+
this.didOpen(filePath, readContent());
|
|
151
|
+
};
|
|
152
|
+
if (this.#openDocs.has(fileToUri(filePath))) {
|
|
153
|
+
open();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await this.#inputAdmission.enroll(fileToUri(filePath), open, control, protectInputs);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Restore only still-retained documents; a close or replacement wins over recovery. */
|
|
160
|
+
async restoreDocument(
|
|
161
|
+
filePath: string,
|
|
162
|
+
readContent: () => string,
|
|
163
|
+
isCurrent: () => boolean,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
const uri = fileToUri(filePath);
|
|
166
|
+
const admission = this.#inputAdmission;
|
|
167
|
+
const canRestore = () =>
|
|
168
|
+
isCurrent() &&
|
|
169
|
+
this.host.isOperational() &&
|
|
170
|
+
admission === this.#inputAdmission &&
|
|
171
|
+
!this.#closedVersionedBarrier.has(uri) &&
|
|
172
|
+
!this.#openDocs.has(uri);
|
|
173
|
+
if (!canRestore()) return;
|
|
174
|
+
try {
|
|
175
|
+
await this.openDocument(filePath, () => {
|
|
176
|
+
if (!canRestore()) throw new Error("Document restoration was superseded.");
|
|
177
|
+
return readContent();
|
|
178
|
+
});
|
|
179
|
+
} catch {
|
|
180
|
+
if (canRestore()) this.markFailedFile(filePath);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
128
184
|
/** Synchronize all open document inputs, rejoining once after a new enrollment. */
|
|
129
185
|
async synchronizeSemanticInputs(
|
|
130
186
|
control?: CodeRequestControl,
|
|
@@ -222,17 +278,22 @@ export class ClientDiagnostics {
|
|
|
222
278
|
this.#inputBarrier.forgetDocumentContent(uri);
|
|
223
279
|
}
|
|
224
280
|
|
|
225
|
-
#ensureExplicitDocumentContent(
|
|
281
|
+
async #ensureExplicitDocumentContent(
|
|
226
282
|
filePath: string,
|
|
227
283
|
content: string,
|
|
228
284
|
contentIsAuthoritative: boolean,
|
|
229
|
-
|
|
285
|
+
control?: CodeRequestControl,
|
|
286
|
+
): Promise<void> {
|
|
230
287
|
const uri = fileToUri(filePath);
|
|
231
288
|
const document = this.#openDocs.get(uri);
|
|
232
289
|
if (!document) {
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
this.
|
|
290
|
+
// A manager read can predate admission. Re-read disk after the wait;
|
|
291
|
+
// explicit authoritative content retains its existing override semantics.
|
|
292
|
+
await this.openDocument(
|
|
293
|
+
filePath,
|
|
294
|
+
() => (contentIsAuthoritative ? content : readFileSync(filePath, "utf-8")),
|
|
295
|
+
control,
|
|
296
|
+
);
|
|
236
297
|
return;
|
|
237
298
|
}
|
|
238
299
|
// Manager reads are not authoritative: an already tracked document can
|
|
@@ -260,6 +321,8 @@ export class ClientDiagnostics {
|
|
|
260
321
|
}
|
|
261
322
|
|
|
262
323
|
clear(options: { preserveFailedDocuments?: boolean } = {}): void {
|
|
324
|
+
this.#inputAdmission.dispose();
|
|
325
|
+
this.#inputAdmission = new SemanticInputAdmission();
|
|
263
326
|
if (options.preserveFailedDocuments) {
|
|
264
327
|
for (const uri of [...this.#openDocs.keys(), ...this.#diagnosticStore.keys()]) {
|
|
265
328
|
this.#failedUris.add(uri);
|
|
@@ -336,6 +399,7 @@ export class ClientDiagnostics {
|
|
|
336
399
|
|
|
337
400
|
/** Send a save after a disk change that the input barrier observed. */
|
|
338
401
|
#noteExternalDiskChange(filePath: string, content: string): void {
|
|
402
|
+
this.#inputAdmission.invalidateRequests();
|
|
339
403
|
if (!this.host.isOperational()) return;
|
|
340
404
|
const documentSync = this.host.documentSync();
|
|
341
405
|
if (!documentSync.save) return;
|
|
@@ -368,6 +432,7 @@ export class ClientDiagnostics {
|
|
|
368
432
|
|
|
369
433
|
didClose(filePath: string): void {
|
|
370
434
|
const uri = fileToUri(filePath);
|
|
435
|
+
this.#inputAdmission.close(uri);
|
|
371
436
|
const wasOpen = this.#openDocs.has(uri);
|
|
372
437
|
const hadState =
|
|
373
438
|
wasOpen ||
|
|
@@ -627,7 +692,12 @@ export class ClientDiagnostics {
|
|
|
627
692
|
throwIfCodeRequestInterrupted(control);
|
|
628
693
|
const uri = fileToUri(filePath);
|
|
629
694
|
const contentIsAuthoritative = options.contentIsAuthoritative !== false;
|
|
630
|
-
|
|
695
|
+
try {
|
|
696
|
+
await this.#ensureExplicitDocumentContent(filePath, content, contentIsAuthoritative, control);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
throwIfCodeRequestInterrupted(control);
|
|
699
|
+
return unavailableAfterEnrollmentRetry(error);
|
|
700
|
+
}
|
|
631
701
|
const contentOverrides = this.#getDiagnosticContentOverrides(
|
|
632
702
|
uri,
|
|
633
703
|
content,
|
|
@@ -1044,6 +1114,7 @@ export class ClientDiagnostics {
|
|
|
1044
1114
|
changeKind: SemanticInputChangeKind = "content",
|
|
1045
1115
|
): void {
|
|
1046
1116
|
this.#inputBarrier.noteInputChange(changeKind);
|
|
1117
|
+
if (changeKind !== "enrollment") this.#inputAdmission.invalidateRequests();
|
|
1047
1118
|
if (invalidateEvidence) this.#advanceEvidenceRevision(false);
|
|
1048
1119
|
if (cancelRequests) this.#cancelAllDiagnosticRequests();
|
|
1049
1120
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CodeRequestControl,
|
|
3
|
+
CodeRequestDeadlineError,
|
|
4
|
+
throwIfCodeRequestInterrupted,
|
|
5
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
6
|
+
|
|
7
|
+
const MAX_PENDING_INPUT_OPERATIONS = 32;
|
|
8
|
+
|
|
9
|
+
type InputOperationKind = "request" | "enrollment" | "request-enrollment";
|
|
10
|
+
|
|
11
|
+
interface PendingInputOperation {
|
|
12
|
+
readonly uri: string | undefined;
|
|
13
|
+
readonly kind: InputOperationKind;
|
|
14
|
+
start(): void;
|
|
15
|
+
cancel(reason: unknown): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Admit first opens between semantic query and refactor-planning attempts on one client.
|
|
20
|
+
* Consecutive readers run concurrently; FIFO admission prevents either new
|
|
21
|
+
* readers or enrollments from overtaking a waiting operation. This is not
|
|
22
|
+
* diagnostic transport ownership: an interrupted semantic attempt can release
|
|
23
|
+
* its admission even when the server has not yet acknowledged cancellation.
|
|
24
|
+
*/
|
|
25
|
+
export class SemanticInputAdmission {
|
|
26
|
+
readonly #pending: PendingInputOperation[] = [];
|
|
27
|
+
#readers = 0;
|
|
28
|
+
#enrolling = false;
|
|
29
|
+
#disposed = false;
|
|
30
|
+
|
|
31
|
+
/** Cover one attempt's synchronization, RPCs and acceptance, never its retry. */
|
|
32
|
+
request<T>(run: () => Promise<T>, control?: CodeRequestControl, uri?: string): Promise<T> {
|
|
33
|
+
return this.#run("request", uri, run, control);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Commit a first open after earlier attempts leave. Demand with original
|
|
38
|
+
* semantic/refactor parameters opts into input invalidation while waiting.
|
|
39
|
+
*/
|
|
40
|
+
enroll(
|
|
41
|
+
uri: string,
|
|
42
|
+
run: () => void,
|
|
43
|
+
control?: CodeRequestControl,
|
|
44
|
+
protectInputs = false,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
return this.#run(protectInputs ? "request-enrollment" : "enrollment", uri, run, control);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A close cancels queued opens and queries for that document, before dispatch. */
|
|
50
|
+
close(uri: string): void {
|
|
51
|
+
const cancelled = this.#pending.filter((pending) => pending.uri === uri);
|
|
52
|
+
for (const pending of cancelled) this.#pending.splice(this.#pending.indexOf(pending), 1);
|
|
53
|
+
for (const pending of cancelled)
|
|
54
|
+
pending.cancel(new Error("Document closed before input admission."));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Discard queued request parameters after a known non-enrollment input change. */
|
|
58
|
+
invalidateRequests(): void {
|
|
59
|
+
const cancelled = this.#pending.filter((pending) => pending.kind !== "enrollment");
|
|
60
|
+
for (const pending of cancelled) this.#pending.splice(this.#pending.indexOf(pending), 1);
|
|
61
|
+
for (const pending of cancelled)
|
|
62
|
+
pending.cancel(new Error("Semantic inputs changed before request admission."));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Reject queued work without confusing caller completion with transport settlement. */
|
|
66
|
+
dispose(): void {
|
|
67
|
+
this.#disposed = true;
|
|
68
|
+
for (const pending of [...this.#pending]) {
|
|
69
|
+
pending.cancel(new Error("LSP client stopped before input admission."));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#run<T>(
|
|
74
|
+
kind: InputOperationKind,
|
|
75
|
+
uri: string | undefined,
|
|
76
|
+
run: () => T | Promise<T>,
|
|
77
|
+
control: CodeRequestControl | undefined,
|
|
78
|
+
): Promise<T> {
|
|
79
|
+
throwIfCodeRequestInterrupted(control);
|
|
80
|
+
if (this.#disposed) return Promise.reject(new Error("LSP input admission is disposed."));
|
|
81
|
+
if (this.#pending.length >= MAX_PENDING_INPUT_OPERATIONS) {
|
|
82
|
+
return Promise.reject(new Error("LSP input admission queue is full."));
|
|
83
|
+
}
|
|
84
|
+
return new Promise<T>((resolve, reject) => {
|
|
85
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
86
|
+
let waiting = true;
|
|
87
|
+
const cleanup = () => {
|
|
88
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
89
|
+
control?.signal?.removeEventListener("abort", abort);
|
|
90
|
+
};
|
|
91
|
+
const operation: PendingInputOperation = {
|
|
92
|
+
uri,
|
|
93
|
+
kind,
|
|
94
|
+
cancel: (reason) => {
|
|
95
|
+
if (!waiting) return;
|
|
96
|
+
waiting = false;
|
|
97
|
+
cleanup();
|
|
98
|
+
const index = this.#pending.indexOf(operation);
|
|
99
|
+
if (index !== -1) this.#pending.splice(index, 1);
|
|
100
|
+
reject(reason);
|
|
101
|
+
this.#drain();
|
|
102
|
+
},
|
|
103
|
+
start: () => {
|
|
104
|
+
waiting = false;
|
|
105
|
+
cleanup();
|
|
106
|
+
if (kind !== "request") this.#enrolling = true;
|
|
107
|
+
else this.#readers++;
|
|
108
|
+
const finish = () => {
|
|
109
|
+
if (kind !== "request") this.#enrolling = false;
|
|
110
|
+
else this.#readers--;
|
|
111
|
+
this.#drain();
|
|
112
|
+
};
|
|
113
|
+
try {
|
|
114
|
+
throwIfCodeRequestInterrupted(control);
|
|
115
|
+
Promise.resolve(run()).then(resolve, reject).finally(finish);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
reject(error);
|
|
118
|
+
finish();
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
const abort = () => {
|
|
123
|
+
try {
|
|
124
|
+
throwIfCodeRequestInterrupted(control);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
operation.cancel(error);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
this.#pending.push(operation);
|
|
130
|
+
control?.signal?.addEventListener("abort", abort, { once: true });
|
|
131
|
+
if (control?.deadline !== undefined) {
|
|
132
|
+
timer = setTimeout(
|
|
133
|
+
() => operation.cancel(new CodeRequestDeadlineError()),
|
|
134
|
+
Math.max(0, control.deadline - Date.now()),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
this.#drain();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#drain(): void {
|
|
142
|
+
while (!this.#disposed && !this.#enrolling && this.#pending.length > 0) {
|
|
143
|
+
const next = this.#pending[0];
|
|
144
|
+
if (next.kind !== "request" && this.#readers > 0) return;
|
|
145
|
+
this.#pending.shift();
|
|
146
|
+
next.start();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether tsserver placed an open file in a configured project.
|
|
3
|
+
*
|
|
4
|
+
* - `configured`: a `tsconfig.json` / `jsconfig.json` program owns the file.
|
|
5
|
+
* - `inferred`: tsserver created an inferred project for the file alone, so
|
|
6
|
+
* project-wide requests such as `workspace/symbol` do not cover the
|
|
7
|
+
* configured program.
|
|
8
|
+
* - `unknown`: the route is not TypeScript or tsserver could not answer.
|
|
9
|
+
*/
|
|
10
|
+
export type ProjectMembership = "configured" | "inferred" | "unknown";
|
|
11
|
+
|
|
12
|
+
/** Build `workspace/executeCommand` params for tsserver `projectInfo`. */
|
|
13
|
+
export function typeScriptProjectInfoParams(uri: string): unknown {
|
|
14
|
+
return {
|
|
15
|
+
command: "typescript.tsserverRequest",
|
|
16
|
+
arguments: [
|
|
17
|
+
"projectInfo",
|
|
18
|
+
{ file: uri, needFileNameList: false },
|
|
19
|
+
// Semantic execution target; the project must be loaded to answer.
|
|
20
|
+
{ executionTarget: 0, expectsResult: true, isAsync: false },
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read project membership from a tsserver `projectInfo` response.
|
|
27
|
+
* tsserver names projects without a config file `/dev/null/<kind>Project<N>*`.
|
|
28
|
+
*/
|
|
29
|
+
export function readTypeScriptProjectMembership(response: unknown): ProjectMembership {
|
|
30
|
+
if (!isRecord(response) || response.type !== "response" || response.success !== true) {
|
|
31
|
+
return "unknown";
|
|
32
|
+
}
|
|
33
|
+
const body = response.body;
|
|
34
|
+
if (!isRecord(body) || typeof body.configFileName !== "string") return "unknown";
|
|
35
|
+
return body.configFileName.startsWith("/dev/null/") ? "inferred" : "configured";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
39
|
+
return typeof value === "object" && value !== null;
|
|
40
|
+
}
|