@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.
Files changed (80) hide show
  1. package/README.md +81 -7
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +3 -1
  4. package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +9 -0
  5. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-core/src/child-web-tools.ts +110 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/session.ts +11 -0
  8. package/node_modules/semver/LICENSE +15 -0
  9. package/node_modules/semver/README.md +680 -0
  10. package/node_modules/semver/bin/semver.js +195 -0
  11. package/node_modules/semver/classes/comparator.js +143 -0
  12. package/node_modules/semver/classes/index.js +7 -0
  13. package/node_modules/semver/classes/range.js +577 -0
  14. package/node_modules/semver/classes/semver.js +350 -0
  15. package/node_modules/semver/functions/clean.js +8 -0
  16. package/node_modules/semver/functions/cmp.js +54 -0
  17. package/node_modules/semver/functions/coerce.js +62 -0
  18. package/node_modules/semver/functions/compare-build.js +9 -0
  19. package/node_modules/semver/functions/compare-loose.js +5 -0
  20. package/node_modules/semver/functions/compare.js +7 -0
  21. package/node_modules/semver/functions/diff.js +60 -0
  22. package/node_modules/semver/functions/eq.js +5 -0
  23. package/node_modules/semver/functions/gt.js +5 -0
  24. package/node_modules/semver/functions/gte.js +5 -0
  25. package/node_modules/semver/functions/inc.js +21 -0
  26. package/node_modules/semver/functions/lt.js +5 -0
  27. package/node_modules/semver/functions/lte.js +5 -0
  28. package/node_modules/semver/functions/major.js +5 -0
  29. package/node_modules/semver/functions/minor.js +5 -0
  30. package/node_modules/semver/functions/neq.js +5 -0
  31. package/node_modules/semver/functions/parse.js +18 -0
  32. package/node_modules/semver/functions/patch.js +5 -0
  33. package/node_modules/semver/functions/prerelease.js +8 -0
  34. package/node_modules/semver/functions/rcompare.js +5 -0
  35. package/node_modules/semver/functions/rsort.js +5 -0
  36. package/node_modules/semver/functions/satisfies.js +12 -0
  37. package/node_modules/semver/functions/sort.js +5 -0
  38. package/node_modules/semver/functions/truncate.js +48 -0
  39. package/node_modules/semver/functions/valid.js +8 -0
  40. package/node_modules/semver/index.js +93 -0
  41. package/node_modules/semver/internal/constants.js +37 -0
  42. package/node_modules/semver/internal/debug.js +11 -0
  43. package/node_modules/semver/internal/identifiers.js +29 -0
  44. package/node_modules/semver/internal/lrucache.js +42 -0
  45. package/node_modules/semver/internal/parse-options.js +17 -0
  46. package/node_modules/semver/internal/re.js +223 -0
  47. package/node_modules/semver/package.json +78 -0
  48. package/node_modules/semver/preload.js +4 -0
  49. package/node_modules/semver/range.bnf +17 -0
  50. package/node_modules/semver/ranges/gtr.js +6 -0
  51. package/node_modules/semver/ranges/intersects.js +9 -0
  52. package/node_modules/semver/ranges/ltr.js +6 -0
  53. package/node_modules/semver/ranges/max-satisfying.js +27 -0
  54. package/node_modules/semver/ranges/min-satisfying.js +26 -0
  55. package/node_modules/semver/ranges/min-version.js +63 -0
  56. package/node_modules/semver/ranges/outside.js +82 -0
  57. package/node_modules/semver/ranges/simplify.js +49 -0
  58. package/node_modules/semver/ranges/subset.js +249 -0
  59. package/node_modules/semver/ranges/to-comparators.js +10 -0
  60. package/node_modules/semver/ranges/valid.js +13 -0
  61. package/package.json +5 -3
  62. package/src/client/client-diagnostic-collection.ts +11 -4
  63. package/src/client/client-diagnostics.ts +77 -6
  64. package/src/client/client-input-admission.ts +149 -0
  65. package/src/client/client-typescript-project.ts +40 -0
  66. package/src/client/client.ts +174 -49
  67. package/src/config/config.ts +53 -2
  68. package/src/config/operation-support.ts +19 -4
  69. package/src/config/server-config.ts +22 -0
  70. package/src/config/typescript-launch.ts +276 -0
  71. package/src/diagnostics/evidence.ts +2 -2
  72. package/src/manager/manager-project-info.ts +22 -10
  73. package/src/manager/manager-workspace-symbol.ts +157 -23
  74. package/src/manager/manager.ts +306 -75
  75. package/src/provider/refactor-planning.ts +4 -3
  76. package/src/session/runtime-controller.ts +2 -2
  77. package/src/session/runtime-registry.ts +19 -2
  78. package/src/session/runtime-rename.ts +1 -0
  79. package/src/session/scanner.ts +27 -15
  80. 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
- ): void {
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
- // An untracked file needs an initial protocol document before its barrier
234
- // read can verify the manager's content.
235
- this.didOpen(filePath, content);
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
- this.#ensureExplicitDocumentContent(filePath, content, contentIsAuthoritative);
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
+ }