oh-my-opencode 4.18.0 → 4.18.1

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 (192) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -0
  3. package/.agents/skills/work-with-pr/SKILL.md +16 -37
  4. package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
  5. package/.opencode/skills/work-with-pr/SKILL.md +16 -37
  6. package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
  7. package/dist/cli/index.js +457 -154
  8. package/dist/cli-node/index.js +457 -154
  9. package/dist/index.js +415 -388
  10. package/package.json +16 -16
  11. package/packages/lsp-core/package.json +4 -0
  12. package/packages/lsp-core/src/index.ts +1 -0
  13. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  14. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  15. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  16. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  17. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  18. package/packages/lsp-core/src/lsp/client.ts +262 -80
  19. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  20. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  21. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +104 -0
  22. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
  23. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  24. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  25. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  26. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  27. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  28. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  29. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  30. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  31. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  32. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  33. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  34. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  35. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  36. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  37. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  38. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  39. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  40. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  41. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  42. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  43. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  44. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  45. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  46. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  47. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  48. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  60. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  61. package/packages/lsp-core/src/mcp.ts +18 -7
  62. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  63. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  64. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  65. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  66. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  67. package/packages/lsp-core/src/request-context.test.ts +171 -0
  68. package/packages/lsp-core/src/request-context.ts +222 -9
  69. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  70. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  71. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  72. package/packages/lsp-core/src/tools/rename.ts +10 -15
  73. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  74. package/packages/lsp-core/src/tools/types.ts +2 -1
  75. package/packages/lsp-daemon/dist/cli.js +3114 -747
  76. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  77. package/packages/lsp-daemon/dist/client.js +5851 -0
  78. package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
  79. package/packages/lsp-daemon/dist/daemon-client.js +113 -30
  80. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  81. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  82. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +8 -7
  83. package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
  84. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  85. package/packages/lsp-daemon/dist/index.js +2862 -754
  86. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  87. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  88. package/packages/lsp-daemon/dist/lock.js +14 -4
  89. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  90. package/packages/lsp-daemon/dist/ownership.js +168 -0
  91. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  92. package/packages/lsp-daemon/dist/paths.js +72 -33
  93. package/packages/lsp-daemon/dist/proxy.d.ts +3 -0
  94. package/packages/lsp-daemon/dist/proxy.js +54 -3
  95. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  96. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  97. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  98. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  99. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  100. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  101. package/packages/lsp-daemon/package.json +12 -3
  102. package/packages/lsp-tools-mcp/dist/cli.js +2115 -442
  103. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  104. package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
  105. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  106. package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
  107. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  108. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  109. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  110. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  111. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  112. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  113. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  114. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  115. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  116. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  117. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  118. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
  119. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  120. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  121. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  122. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  123. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  124. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  125. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  126. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  127. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  128. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  129. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  130. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  131. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  132. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  133. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  134. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  135. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  136. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  137. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  138. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +19 -5
  139. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  140. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +1 -1
  141. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  142. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  143. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
  144. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  145. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  146. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  147. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  148. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  149. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  150. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  151. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  152. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  153. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  154. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  155. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
  156. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  157. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  158. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  159. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  161. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  162. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  163. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  164. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  165. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  166. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  167. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  168. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  169. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  170. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  171. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  172. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  173. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  174. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  175. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  176. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  177. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  178. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  179. package/packages/omo-codex/plugin/package-lock.json +26 -14
  180. package/packages/omo-codex/plugin/package.json +1 -1
  181. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  182. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  183. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
  184. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
  185. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  186. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
  187. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  188. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  189. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  190. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  191. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
  192. package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
@@ -1,33 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // ../lsp-core/src/lsp/cleanup-errors.ts
4
- function reportBestEffortCleanupError(operation, error) {
5
- if (process.env["CODEX_LSP_DEBUG_CLEANUP"] !== "1")
6
- return;
4
+ function writeCleanupError(message) {
5
+ process.stderr.write(`${message}
6
+ `);
7
+ }
8
+ function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
7
9
  const message = error instanceof Error ? error.message : String(error);
8
- console.error(`[codex-lsp] ignored ${operation} failure during cleanup: ${message}`);
10
+ logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
9
11
  }
10
12
 
11
13
  // ../lsp-core/src/lsp/client.ts
12
- import { readFileSync } from "node:fs";
13
- import { resolve } from "node:path";
14
- import { pathToFileURL as pathToFileURL2 } from "node:url";
15
-
16
- // ../lsp-core/src/request-context.ts
17
- import { AsyncLocalStorage } from "node:async_hooks";
18
- var storage = new AsyncLocalStorage;
19
- function runWithRequestContext(context, fn) {
20
- return storage.run(context, fn);
21
- }
22
- function contextCwd() {
23
- return storage.getStore()?.cwd ?? process.cwd();
24
- }
25
- function contextEnv(key) {
26
- const store = storage.getStore();
27
- if (store?.env)
28
- return store.env[key];
29
- return process.env[key];
30
- }
14
+ import { resolve as resolve5 } from "node:path";
15
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
31
16
 
32
17
  // ../lsp-core/src/lsp/connection.ts
33
18
  import { pathToFileURL } from "node:url";
@@ -91,7 +76,12 @@ class LspInvalidPathError extends Error {
91
76
  }
92
77
 
93
78
  class LspServerLookupError extends Error {
79
+ lookup;
94
80
  name = "LspServerLookupError";
81
+ constructor(message, lookup) {
82
+ super(message);
83
+ this.lookup = lookup;
84
+ }
95
85
  }
96
86
 
97
87
  class LspServerInitializingError extends Error {
@@ -157,28 +147,80 @@ class JsonRpcConnection {
157
147
  onError(handler) {
158
148
  this.errorHandlers.push(handler);
159
149
  }
160
- async sendRequest(method, params) {
150
+ async sendRequest(method, params, options = {}) {
161
151
  if (this.disposed)
162
152
  throw new Error("JSON-RPC connection is disposed");
163
153
  const id = this.nextRequestId;
164
154
  this.nextRequestId += 1;
155
+ const key = String(id);
165
156
  const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
157
+ let requestWritten = false;
158
+ let cancelAfterWrite = false;
159
+ let settled = false;
160
+ const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
166
161
  const responsePromise = new Promise((resolve, reject) => {
167
- this.pendingRequests.set(String(id), {
162
+ const cleanup = () => {
163
+ options.signal?.removeEventListener("abort", onAbort);
164
+ };
165
+ const settleCancel = () => {
166
+ if (settled)
167
+ return;
168
+ settled = true;
169
+ this.pendingRequests.delete(key);
170
+ cleanup();
171
+ const rejectCancelled = () => reject(abortError(options.signal));
172
+ if (!requestWritten) {
173
+ cancelAfterWrite = true;
174
+ rejectCancelled();
175
+ return;
176
+ }
177
+ writeCancel().then(rejectCancelled, (error) => {
178
+ this.emitError(toError(error));
179
+ rejectCancelled();
180
+ });
181
+ };
182
+ const onAbort = () => settleCancel();
183
+ this.pendingRequests.set(key, {
168
184
  resolve(result) {
185
+ settled = true;
186
+ cleanup();
169
187
  resolve(result);
170
188
  },
171
- reject
189
+ reject(error) {
190
+ settled = true;
191
+ cleanup();
192
+ reject(error);
193
+ },
194
+ cleanup
172
195
  });
196
+ if (options.signal?.aborted) {
197
+ settleCancel();
198
+ return;
199
+ }
200
+ options.signal?.addEventListener("abort", onAbort, { once: true });
173
201
  });
202
+ if (settled)
203
+ return responsePromise;
174
204
  try {
175
205
  await this.writeMessage(message);
206
+ requestWritten = true;
207
+ if (cancelAfterWrite)
208
+ await writeCancel();
176
209
  } catch (error) {
177
- this.pendingRequests.delete(String(id));
210
+ if (settled)
211
+ return responsePromise;
212
+ const pending = this.pendingRequests.get(key);
213
+ if (pending) {
214
+ pending.cleanup();
215
+ this.pendingRequests.delete(key);
216
+ }
178
217
  throw error;
179
218
  }
180
219
  return responsePromise;
181
220
  }
221
+ pendingRequestCount() {
222
+ return this.pendingRequests.size;
223
+ }
182
224
  async sendNotification(method, params) {
183
225
  if (this.disposed)
184
226
  return;
@@ -195,6 +237,7 @@ class JsonRpcConnection {
195
237
  this.reader.off("error", this.handleStreamError);
196
238
  this.writer.off("error", this.handleStreamError);
197
239
  for (const pending of this.pendingRequests.values()) {
240
+ pending.cleanup();
198
241
  pending.reject(new Error("JSON-RPC connection disposed"));
199
242
  }
200
243
  this.pendingRequests.clear();
@@ -270,6 +313,7 @@ class JsonRpcConnection {
270
313
  if (!pending)
271
314
  return;
272
315
  this.pendingRequests.delete(String(id));
316
+ pending.cleanup();
273
317
  if ("error" in message) {
274
318
  pending.reject(jsonRpcErrorToError(message["error"]));
275
319
  return;
@@ -283,7 +327,11 @@ class JsonRpcConnection {
283
327
  try {
284
328
  handler(params);
285
329
  } catch (error) {
286
- this.emitError(toError(error));
330
+ if (error instanceof Error) {
331
+ this.emitError(error);
332
+ return;
333
+ }
334
+ this.emitError(new Error(String(error)));
287
335
  }
288
336
  }
289
337
  handleRequest(message) {
@@ -324,6 +372,14 @@ ${body}`;
324
372
  }
325
373
  }
326
374
  }
375
+ function abortError(signal) {
376
+ const reason = signal?.reason;
377
+ if (reason instanceof Error)
378
+ return reason;
379
+ const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
380
+ error.name = "AbortError";
381
+ return error;
382
+ }
327
383
  function parseContentLength(headers) {
328
384
  for (const line of headers.split(`\r
329
385
  `)) {
@@ -510,7 +566,7 @@ function spawnProcess(command, options) {
510
566
  return wrap(proc);
511
567
  }
512
568
 
513
- // ../lsp-core/src/lsp/transport.ts
569
+ // ../lsp-core/src/lsp/transport-protocol.ts
514
570
  function isRecord(value) {
515
571
  return typeof value === "object" && value !== null && !Array.isArray(value);
516
572
  }
@@ -530,7 +586,32 @@ function parseDiagnosticsParams(params) {
530
586
  if (!isRecord(params) || typeof params["uri"] !== "string")
531
587
  return null;
532
588
  const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
533
- return { uri: params["uri"], diagnostics };
589
+ const version = typeof params["version"] === "number" ? params["version"] : undefined;
590
+ return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
591
+ }
592
+ function createLspSpawnEnv(_root, input) {
593
+ return { ...input };
594
+ }
595
+ function isDiagnostic(value) {
596
+ return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
597
+ }
598
+ function isRange(value) {
599
+ return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
600
+ }
601
+ function isPosition(value) {
602
+ return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
603
+ }
604
+
605
+ // ../lsp-core/src/lsp/transport.ts
606
+ class LspClientNotStartedError extends Error {
607
+ serverId;
608
+ root;
609
+ name = "LspClientNotStartedError";
610
+ constructor(serverId, root) {
611
+ super("LSP client not started");
612
+ this.serverId = serverId;
613
+ this.root = root;
614
+ }
534
615
  }
535
616
 
536
617
  class LspClientTransport {
@@ -543,6 +624,8 @@ class LspClientTransport {
543
624
  diagnosticsStore = new Map;
544
625
  requestTimeoutMs;
545
626
  initializeTimeoutMs;
627
+ workspaceApplyEditHandler = null;
628
+ diagnosticPullSupported = false;
546
629
  constructor(root, server, timeouts = {}) {
547
630
  this.root = root;
548
631
  this.server = server;
@@ -555,6 +638,21 @@ class LspClientTransport {
555
638
  command() {
556
639
  return [...this.server.command];
557
640
  }
641
+ setWorkspaceApplyEditHandler(handler) {
642
+ this.workspaceApplyEditHandler = handler;
643
+ }
644
+ hasWorkspaceApplyEditHandler() {
645
+ return this.workspaceApplyEditHandler !== null;
646
+ }
647
+ setDiagnosticPullSupported(supported) {
648
+ this.diagnosticPullSupported = supported;
649
+ }
650
+ isDiagnosticPullSupported() {
651
+ return this.diagnosticPullSupported;
652
+ }
653
+ handlePublishDiagnostics(params) {
654
+ this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
655
+ }
558
656
  async start() {
559
657
  const env = createLspSpawnEnv(this.root, {
560
658
  ...process.env,
@@ -565,7 +663,6 @@ class LspClientTransport {
565
663
  env
566
664
  });
567
665
  this.startStderrReading();
568
- await new Promise((resolve) => setTimeout(resolve, 100));
569
666
  if (this.proc.exitCode !== null) {
570
667
  const stderr = this.stderrBuffer.join(`
571
668
  `);
@@ -575,7 +672,7 @@ class LspClientTransport {
575
672
  this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
576
673
  const diagnosticsParams = parseDiagnosticsParams(params);
577
674
  if (diagnosticsParams?.uri) {
578
- this.diagnosticsStore.set(diagnosticsParams.uri, diagnosticsParams.diagnostics);
675
+ this.handlePublishDiagnostics(diagnosticsParams);
579
676
  }
580
677
  });
581
678
  this.connection.onRequest("workspace/configuration", (params) => {
@@ -588,6 +685,9 @@ class LspClientTransport {
588
685
  });
589
686
  this.connection.onRequest("client/registerCapability", () => null);
590
687
  this.connection.onRequest("window/workDoneProgress/create", () => null);
688
+ if (this.workspaceApplyEditHandler) {
689
+ this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
690
+ }
591
691
  this.connection.onClose(() => {
592
692
  this.processExited = true;
593
693
  });
@@ -616,30 +716,25 @@ class LspClientTransport {
616
716
  }
617
717
  async sendRequest(method, ...args) {
618
718
  if (!this.connection)
619
- throw new Error("LSP client not started");
719
+ throw new LspClientNotStartedError(this.server.id, this.root);
620
720
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
621
721
  const stderrTail = this.stderrBuffer.slice(-10).join(`
622
722
  `);
623
723
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
624
724
  }
625
- const timeoutMs = args[1]?.timeoutMs ?? this.requestTimeoutMs;
626
- let timeoutHandle = null;
627
- const timeoutPromise = new Promise((_, reject) => {
628
- timeoutHandle = setTimeout(() => {
629
- const stderrTail = this.stderrBuffer.slice(-5).join(`
725
+ const options = args[1];
726
+ const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
727
+ const timeoutController = new AbortController;
728
+ const timeoutHandle = setTimeout(() => {
729
+ const stderrTail = this.stderrBuffer.slice(-5).join(`
630
730
  `);
631
- reject(new LspRequestTimeoutError(method, stderrTail || undefined));
632
- }, timeoutMs);
633
- });
731
+ timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
732
+ }, timeoutMs);
733
+ const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
634
734
  try {
635
- const requestPromise = args.length === 0 ? this.connection.sendRequest(method) : this.connection.sendRequest(method, args[0]);
636
- const result = await Promise.race([requestPromise, timeoutPromise]);
637
- if (timeoutHandle !== null)
638
- clearTimeout(timeoutHandle);
735
+ const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
639
736
  return result;
640
737
  } catch (error) {
641
- if (timeoutHandle !== null)
642
- clearTimeout(timeoutHandle);
643
738
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
644
739
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
645
740
  `) || undefined);
@@ -648,6 +743,9 @@ class LspClientTransport {
648
743
  throw new LspConnectionClosedError(this.server.id, this.root, error.message);
649
744
  }
650
745
  throw error;
746
+ } finally {
747
+ clearTimeout(timeoutHandle);
748
+ combinedSignal.dispose();
651
749
  }
652
750
  }
653
751
  async sendNotification(method, ...args) {
@@ -676,17 +774,17 @@ class LspClientTransport {
676
774
  try {
677
775
  await this.sendRequest("shutdown");
678
776
  } catch (error) {
679
- reportBestEffortCleanupError("shutdown request", error);
777
+ reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
680
778
  }
681
779
  try {
682
780
  await this.sendNotification("exit");
683
781
  } catch (error) {
684
- reportBestEffortCleanupError("exit notification", error);
782
+ reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
685
783
  }
686
784
  try {
687
785
  this.connection.dispose();
688
786
  } catch (error) {
689
- reportBestEffortCleanupError("connection dispose", error);
787
+ reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
690
788
  }
691
789
  this.connection = null;
692
790
  }
@@ -717,11 +815,11 @@ class LspClientTransport {
717
815
  new Promise((resolve) => setTimeout(resolve, STOP_SIGKILL_GRACE_MS))
718
816
  ]);
719
817
  } catch (error) {
720
- reportBestEffortCleanupError("hard process kill", error);
818
+ reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
721
819
  }
722
820
  }
723
821
  } catch (error) {
724
- reportBestEffortCleanupError("process stop", error);
822
+ reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
725
823
  }
726
824
  }
727
825
  this.processExited = true;
@@ -731,26 +829,45 @@ class LspClientTransport {
731
829
  return this.diagnosticsStore.get(uri) ?? [];
732
830
  }
733
831
  }
734
- function createLspSpawnEnv(_root, input) {
735
- return { ...input };
736
- }
737
- function isDiagnostic(value) {
738
- return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
739
- }
740
- function isRange(value) {
741
- return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
742
- }
743
- function isPosition(value) {
744
- return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
832
+ function combineAbortSignals(primary, secondary) {
833
+ const controller = new AbortController;
834
+ const abortFrom = (signal) => {
835
+ if (!controller.signal.aborted)
836
+ controller.abort(signal.reason);
837
+ };
838
+ const onPrimaryAbort = () => {
839
+ if (primary)
840
+ abortFrom(primary);
841
+ };
842
+ const onSecondaryAbort = () => abortFrom(secondary);
843
+ if (primary?.aborted)
844
+ abortFrom(primary);
845
+ else
846
+ primary?.addEventListener("abort", onPrimaryAbort, { once: true });
847
+ if (secondary.aborted)
848
+ abortFrom(secondary);
849
+ else
850
+ secondary.addEventListener("abort", onSecondaryAbort, { once: true });
851
+ return {
852
+ signal: controller.signal,
853
+ dispose: () => {
854
+ primary?.removeEventListener("abort", onPrimaryAbort);
855
+ secondary.removeEventListener("abort", onSecondaryAbort);
856
+ }
857
+ };
745
858
  }
746
859
 
747
860
  // ../lsp-core/src/lsp/connection.ts
748
- var INITIALIZE_SETTLE_MS = 300;
861
+ function supportsDiagnosticPull(capabilities) {
862
+ if (capabilities === undefined)
863
+ return false;
864
+ return Object.hasOwn(capabilities, "diagnosticProvider");
865
+ }
749
866
 
750
867
  class LspClientConnection extends LspClientTransport {
751
868
  async initialize() {
752
869
  const rootUri = pathToFileURL(this.root).href;
753
- await this.sendRequest("initialize", {
870
+ const result = await this.sendRequest("initialize", {
754
871
  processId: process.pid,
755
872
  rootUri,
756
873
  rootPath: this.root,
@@ -764,8 +881,7 @@ class LspClientConnection extends LspClientTransport {
764
881
  publishDiagnostics: {},
765
882
  rename: {
766
883
  prepareSupport: true,
767
- prepareSupportDefaultBehavior: 1,
768
- honorsChangeAnnotations: true
884
+ prepareSupportDefaultBehavior: 1
769
885
  },
770
886
  codeAction: {
771
887
  codeActionLiteralSupport: {
@@ -794,22 +910,28 @@ class LspClientConnection extends LspClientTransport {
794
910
  symbol: {},
795
911
  workspaceFolders: true,
796
912
  configuration: true,
797
- applyEdit: true,
913
+ ...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
798
914
  workspaceEdit: {
799
- documentChanges: true
915
+ documentChanges: true,
916
+ resourceOperations: ["create", "rename", "delete"]
800
917
  }
801
918
  }
802
919
  },
803
920
  initializationOptions: this.server.initialization
804
921
  }, { timeoutMs: this.initializeTimeoutMs });
922
+ this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
805
923
  await this.sendNotification("initialized");
806
924
  await this.sendNotification("workspace/didChangeConfiguration", {
807
925
  settings: { json: { validate: { enable: true } } }
808
926
  });
809
- await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
810
927
  }
811
928
  }
812
929
 
930
+ // ../lsp-core/src/lsp/workspace-document-state.ts
931
+ import { readFileSync, realpathSync } from "node:fs";
932
+ import { relative, resolve } from "node:path";
933
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
934
+
813
935
  // ../lsp-core/src/lsp/effective-extension.ts
814
936
  import { basename, extname } from "node:path";
815
937
  var BASENAME_EXTENSIONS = {
@@ -992,82 +1114,1422 @@ function getLanguageId(ext) {
992
1114
  return EXT_TO_LANG[ext] ?? "plaintext";
993
1115
  }
994
1116
 
1117
+ // ../lsp-core/src/lsp/workspace-document-state.ts
1118
+ var WATCHED_FILE_BATCH_SIZE = 128;
1119
+ var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
1120
+ function canonicalPath(filePath) {
1121
+ const absolute = resolve(filePath);
1122
+ try {
1123
+ return realpathSync(absolute);
1124
+ } catch {
1125
+ return absolute;
1126
+ }
1127
+ }
1128
+ function isSameOrDescendant(candidate, parent) {
1129
+ const suffix = relative(parent, candidate);
1130
+ return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
1131
+ }
1132
+ function movedPath(candidate, oldPath, newPath) {
1133
+ const suffix = relative(oldPath, candidate);
1134
+ return suffix === "" ? newPath : resolve(newPath, suffix);
1135
+ }
1136
+
1137
+ class WorkspaceDocumentState {
1138
+ sendNotification;
1139
+ clearDiagnostics;
1140
+ openDocuments = new Map;
1141
+ openByUri = new Map;
1142
+ openPromises = new Map;
1143
+ now;
1144
+ versionlessPublishQuiescenceMs;
1145
+ constructor(sendNotification, clearDiagnostics, options = {}) {
1146
+ this.sendNotification = sendNotification;
1147
+ this.clearDiagnostics = clearDiagnostics;
1148
+ this.now = options.now ?? (() => Date.now());
1149
+ this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
1150
+ }
1151
+ async openFile(filePath) {
1152
+ const path = canonicalPath(filePath);
1153
+ const existingOpen = this.openPromises.get(path);
1154
+ if (existingOpen) {
1155
+ await existingOpen;
1156
+ return this.openFile(path);
1157
+ }
1158
+ const text = readFileSync(path, "utf-8");
1159
+ const existing = this.openDocuments.get(path);
1160
+ if (!existing)
1161
+ return this.openDocumentSingleFlight(path, text);
1162
+ if (existing.text === text)
1163
+ return;
1164
+ await this.changeDocument(existing, text);
1165
+ }
1166
+ getVersion(filePath) {
1167
+ return this.openDocuments.get(canonicalPath(filePath))?.version;
1168
+ }
1169
+ getStoredDiagnostics(uri) {
1170
+ const state = this.openByUri.get(uri);
1171
+ if (!state)
1172
+ return [];
1173
+ return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
1174
+ }
1175
+ captureDiagnosticSnapshot(filePath) {
1176
+ const state = this.openDocuments.get(canonicalPath(filePath));
1177
+ if (!state)
1178
+ return null;
1179
+ return {
1180
+ path: state.path,
1181
+ uri: state.uri,
1182
+ version: state.version,
1183
+ documentGeneration: state.generation,
1184
+ publishGeneration: state.publishGeneration
1185
+ };
1186
+ }
1187
+ isCurrentSnapshot(snapshot) {
1188
+ const state = this.openDocuments.get(snapshot.path);
1189
+ return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
1190
+ }
1191
+ getPullCache(snapshot) {
1192
+ const state = this.openByUri.get(snapshot.uri);
1193
+ if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
1194
+ return null;
1195
+ return state.pullCache;
1196
+ }
1197
+ recordPullDiagnostics(snapshot, report) {
1198
+ const state = this.openByUri.get(snapshot.uri);
1199
+ if (!state)
1200
+ return;
1201
+ state.pullCache = {
1202
+ documentVersion: snapshot.version,
1203
+ diagnostics: [...report.diagnostics],
1204
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
1205
+ };
1206
+ }
1207
+ recordPublishedDiagnostics(params) {
1208
+ const state = this.openByUri.get(params.uri);
1209
+ if (!state)
1210
+ return;
1211
+ state.publishGeneration += 1;
1212
+ state.lastPublish = {
1213
+ diagnostics: [...params.diagnostics],
1214
+ publishGeneration: state.publishGeneration,
1215
+ documentGenerationAtArrival: state.generation,
1216
+ arrivedAt: this.now(),
1217
+ ...params.version === undefined ? {} : { version: params.version }
1218
+ };
1219
+ this.notifyWaiters(state);
1220
+ }
1221
+ resolvePushDiagnostics(snapshot) {
1222
+ const state = this.openByUri.get(snapshot.uri);
1223
+ if (!state?.lastPublish)
1224
+ return { status: "missing" };
1225
+ const publish = state.lastPublish;
1226
+ if (publish.version !== undefined) {
1227
+ return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
1228
+ }
1229
+ if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
1230
+ return { status: "missing" };
1231
+ const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
1232
+ const waitMs = Math.max(0, readyAt - this.now());
1233
+ return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
1234
+ }
1235
+ waitForDiagnosticsActivity(snapshot, timeoutMs) {
1236
+ const state = this.openByUri.get(snapshot.uri);
1237
+ if (!state || timeoutMs <= 0)
1238
+ return Promise.resolve();
1239
+ return new Promise((resolveActivity) => {
1240
+ let settled = false;
1241
+ const finish = () => {
1242
+ if (settled)
1243
+ return;
1244
+ settled = true;
1245
+ clearTimeout(timer);
1246
+ state.waiters.delete(finish);
1247
+ resolveActivity();
1248
+ };
1249
+ const timer = setTimeout(finish, timeoutMs);
1250
+ if (typeof timer.unref === "function")
1251
+ timer.unref();
1252
+ state.waiters.add(finish);
1253
+ });
1254
+ }
1255
+ validateVersions(operations) {
1256
+ const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
1257
+ for (const operation of operations) {
1258
+ if (operation.kind === "text") {
1259
+ const current = versions.get(operation.path);
1260
+ if (operation.documentVersion !== null && current !== operation.documentVersion) {
1261
+ const observed = current === undefined ? "closed document" : `open document version ${current}`;
1262
+ return {
1263
+ changeIndex: operation.changeIndex,
1264
+ message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
1265
+ };
1266
+ }
1267
+ if (current !== undefined)
1268
+ versions.set(operation.path, current + 1);
1269
+ continue;
1270
+ }
1271
+ if (operation.kind === "rename") {
1272
+ const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
1273
+ for (const [path] of moved)
1274
+ versions.delete(path);
1275
+ for (const [path] of moved)
1276
+ versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
1277
+ continue;
1278
+ }
1279
+ if (operation.kind === "delete") {
1280
+ for (const path of [...versions.keys()]) {
1281
+ if (isSameOrDescendant(path, operation.path))
1282
+ versions.delete(path);
1283
+ }
1284
+ continue;
1285
+ }
1286
+ if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
1287
+ versions.set(operation.path, 1);
1288
+ }
1289
+ }
1290
+ return null;
1291
+ }
1292
+ async synchronize(delta) {
1293
+ const watched = [];
1294
+ for (const mutation of delta.operations)
1295
+ await this.synchronizeMutation(mutation, watched);
1296
+ for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
1297
+ await this.sendNotification("workspace/didChangeWatchedFiles", {
1298
+ changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
1299
+ });
1300
+ }
1301
+ }
1302
+ async synchronizeMutation(mutation, watched) {
1303
+ if (mutation.kind === "text") {
1304
+ const state = this.openDocuments.get(mutation.path);
1305
+ if (state)
1306
+ await this.changeDocument(state, mutation.afterText);
1307
+ else
1308
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
1309
+ return;
1310
+ }
1311
+ if (mutation.kind === "create") {
1312
+ const state = this.openDocuments.get(mutation.path);
1313
+ if (state) {
1314
+ await this.closeDocument(state);
1315
+ await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
1316
+ } else {
1317
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
1318
+ }
1319
+ return;
1320
+ }
1321
+ if (mutation.kind === "rename") {
1322
+ const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
1323
+ for (const state of moved)
1324
+ await this.closeDocument(state);
1325
+ for (const state of moved) {
1326
+ const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
1327
+ await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
1328
+ }
1329
+ if (moved.length === 0) {
1330
+ watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
1331
+ watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
1332
+ }
1333
+ return;
1334
+ }
1335
+ const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
1336
+ for (const state of removed)
1337
+ await this.closeDocument(state);
1338
+ if (removed.length === 0)
1339
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
1340
+ }
1341
+ async openDocumentSingleFlight(path, text) {
1342
+ const existing = this.openPromises.get(path);
1343
+ if (existing)
1344
+ return existing;
1345
+ const open = (async () => {
1346
+ const state = {
1347
+ path,
1348
+ uri: pathToFileURL2(path).href,
1349
+ languageId: getLanguageId(effectiveExtension(path)),
1350
+ text,
1351
+ version: 1,
1352
+ generation: 1,
1353
+ publishGeneration: 0,
1354
+ waiters: new Set
1355
+ };
1356
+ this.openDocuments.set(path, state);
1357
+ this.openByUri.set(state.uri, state);
1358
+ this.notifyWaiters(state);
1359
+ await this.sendNotification("textDocument/didOpen", {
1360
+ textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
1361
+ });
1362
+ })().finally(() => {
1363
+ this.openPromises.delete(path);
1364
+ });
1365
+ this.openPromises.set(path, open);
1366
+ return open;
1367
+ }
1368
+ async changeDocument(state, text) {
1369
+ state.text = text;
1370
+ state.version += 1;
1371
+ state.generation += 1;
1372
+ this.clearDiagnostics(state.uri);
1373
+ this.notifyWaiters(state);
1374
+ await this.sendNotification("textDocument/didChange", {
1375
+ textDocument: { uri: state.uri, version: state.version },
1376
+ contentChanges: [{ text }]
1377
+ });
1378
+ await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
1379
+ }
1380
+ async closeDocument(state) {
1381
+ this.openDocuments.delete(state.path);
1382
+ this.openByUri.delete(state.uri);
1383
+ this.clearDiagnostics(state.uri);
1384
+ this.notifyWaiters(state);
1385
+ await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
1386
+ }
1387
+ notifyWaiters(state) {
1388
+ for (const waiter of [...state.waiters])
1389
+ waiter();
1390
+ }
1391
+ }
1392
+
1393
+ // ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
1394
+ var CONCURRENT_FAILURE_REASON_BY_PHASE = {
1395
+ applying: "workspace/applyEdit is already in progress for this workspace mutation",
1396
+ settled: "workspace/applyEdit was already handled for this workspace mutation"
1397
+ };
1398
+ function workspaceApplyEditConcurrentFailureReason(phase) {
1399
+ return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
1400
+ }
1401
+
1402
+ // ../lsp-core/src/lsp/workspace-edit-commit.ts
1403
+ import { existsSync as existsSync3, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
1404
+
1405
+ // ../lsp-core/src/lsp/workspace-edit-path.ts
1406
+ import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync2 } from "node:fs";
1407
+ import { dirname, isAbsolute, relative as relative2, resolve as resolve2 } from "node:path";
1408
+ import { fileURLToPath } from "node:url";
1409
+
1410
+ class WorkspaceEditPathError extends Error {
1411
+ path;
1412
+ detail;
1413
+ name = "WorkspaceEditPathError";
1414
+ constructor(path, detail) {
1415
+ super(`${detail}: ${path}`);
1416
+ this.path = path;
1417
+ this.detail = detail;
1418
+ }
1419
+ }
1420
+ function isPathInsideWorkspace(filePath, workspaceRoot) {
1421
+ const relativePath = relative2(workspaceRoot, filePath);
1422
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
1423
+ }
1424
+ function canonicalizeMissingPath(filePath) {
1425
+ let ancestor = filePath;
1426
+ while (!existsSync2(ancestor)) {
1427
+ const parent = dirname(ancestor);
1428
+ if (parent === ancestor)
1429
+ throw new WorkspaceEditPathError(filePath, "no existing ancestor");
1430
+ ancestor = parent;
1431
+ }
1432
+ return resolve2(realpathSync2(ancestor), relative2(ancestor, filePath));
1433
+ }
1434
+ function canonicalWorkspaceRoot(workspaceRoot) {
1435
+ try {
1436
+ const canonical = realpathSync2(resolve2(workspaceRoot));
1437
+ if (!lstatSync(canonical).isDirectory()) {
1438
+ return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
1439
+ }
1440
+ return {
1441
+ success: true,
1442
+ path: canonical,
1443
+ requestedPath: resolve2(workspaceRoot),
1444
+ followedSymbolicLink: existsSync2(resolve2(workspaceRoot)) && lstatSync(resolve2(workspaceRoot)).isSymbolicLink()
1445
+ };
1446
+ } catch (error) {
1447
+ const detail = error instanceof Error ? error.message : String(error);
1448
+ return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
1449
+ }
1450
+ }
1451
+ function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
1452
+ let requestedPath;
1453
+ try {
1454
+ const parsed = new URL(uri);
1455
+ if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
1456
+ return { success: false, error: `non-file URI ${uri}` };
1457
+ }
1458
+ requestedPath = resolve2(fileURLToPath(parsed));
1459
+ } catch (error) {
1460
+ const detail = error instanceof Error ? error.message : String(error);
1461
+ return { success: false, error: `non-file URI ${uri}: ${detail}` };
1462
+ }
1463
+ try {
1464
+ const canonical = existsSync2(requestedPath) ? realpathSync2(requestedPath) : canonicalizeMissingPath(requestedPath);
1465
+ if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
1466
+ return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
1467
+ }
1468
+ return {
1469
+ success: true,
1470
+ path: canonical,
1471
+ requestedPath,
1472
+ followedSymbolicLink: existsSync2(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
1473
+ };
1474
+ } catch (error) {
1475
+ const detail = error instanceof Error ? error.message : String(error);
1476
+ return { success: false, error: `${requestedPath}: ${detail}` };
1477
+ }
1478
+ }
1479
+ function snapshotPath(path, includeChildren) {
1480
+ if (!existsSync2(path))
1481
+ return { kind: "missing" };
1482
+ const stats = lstatSync(path);
1483
+ if (stats.isFile())
1484
+ return { kind: "file", content: readFileSync2(path, "utf-8") };
1485
+ if (stats.isDirectory()) {
1486
+ return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
1487
+ }
1488
+ throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
1489
+ }
1490
+
1491
+ // ../lsp-core/src/lsp/workspace-edit-commit.ts
1492
+ var DEFAULT_IO = {
1493
+ writeFile(path, content) {
1494
+ writeFileSync(path, content, "utf-8");
1495
+ },
1496
+ rename(oldPath, newPath) {
1497
+ renameSync(oldPath, newPath);
1498
+ },
1499
+ remove(path, recursive) {
1500
+ rmSync(path, { recursive, force: false });
1501
+ }
1502
+ };
1503
+ function snapshotsEqual(expected, actual) {
1504
+ if (expected.kind !== actual.kind)
1505
+ return false;
1506
+ if (expected.kind === "file" && actual.kind === "file")
1507
+ return expected.content === actual.content;
1508
+ if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
1509
+ return JSON.stringify(expected.children) === JSON.stringify(actual.children);
1510
+ }
1511
+ return true;
1512
+ }
1513
+ function liveSnapshot(path, expected) {
1514
+ return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
1515
+ }
1516
+ function firstOperationIndex(plan) {
1517
+ return plan.operations[0]?.changeIndex ?? 0;
1518
+ }
1519
+ function failedCommit(plan, failure) {
1520
+ const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
1521
+ return {
1522
+ result: {
1523
+ success: false,
1524
+ filesModified,
1525
+ totalEdits,
1526
+ errors: [`change ${changeIndex}: ${message}`],
1527
+ failedChange: changeIndex,
1528
+ ...lateAbort ? { lateAbort: true } : {}
1529
+ },
1530
+ delta: mutationDelta(mutations),
1531
+ fingerprint: plan.fingerprint
1532
+ };
1533
+ }
1534
+ function verifySnapshots(plan) {
1535
+ for (const [path, expected] of plan.snapshots) {
1536
+ let actual;
1537
+ try {
1538
+ actual = liveSnapshot(path, expected);
1539
+ } catch (error) {
1540
+ const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
1541
+ const detail = error instanceof Error ? error.message : String(error);
1542
+ return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
1543
+ }
1544
+ if (!snapshotsEqual(expected, actual)) {
1545
+ const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
1546
+ return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
1547
+ }
1548
+ }
1549
+ return null;
1550
+ }
1551
+ function addModifiedPath(paths, path) {
1552
+ if (!paths.includes(path))
1553
+ paths.push(path);
1554
+ }
1555
+ function reportedPath(plan, path) {
1556
+ return plan.reportedPathByCanonical.get(path) ?? path;
1557
+ }
1558
+ function changedPathsForMutation(mutation) {
1559
+ return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
1560
+ }
1561
+ function mutationDelta(operations) {
1562
+ const changedPaths = new Set;
1563
+ for (const operation of operations) {
1564
+ for (const path of changedPathsForMutation(operation))
1565
+ changedPaths.add(path);
1566
+ }
1567
+ return { operations, changedPaths: [...changedPaths].sort() };
1568
+ }
1569
+ function resolveIo(overrides) {
1570
+ return {
1571
+ writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
1572
+ rename: overrides?.rename ?? DEFAULT_IO.rename,
1573
+ remove: overrides?.remove ?? DEFAULT_IO.remove
1574
+ };
1575
+ }
1576
+ function commitOperation(context, operation) {
1577
+ const { plan, io, accumulator } = context;
1578
+ if (operation.kind === "noop")
1579
+ return;
1580
+ if (operation.kind === "text") {
1581
+ io.writeFile(operation.path, operation.afterText);
1582
+ accumulator.mutations.push({
1583
+ kind: "text",
1584
+ path: operation.path,
1585
+ beforeText: operation.beforeText,
1586
+ afterText: operation.afterText
1587
+ });
1588
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1589
+ accumulator.totalEdits += operation.editCount;
1590
+ return;
1591
+ }
1592
+ if (operation.kind === "create") {
1593
+ io.writeFile(operation.path, "");
1594
+ accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
1595
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1596
+ return;
1597
+ }
1598
+ if (operation.kind === "rename") {
1599
+ if (operation.replaceDestination) {
1600
+ const targetKind = existsSync3(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
1601
+ io.remove(operation.newPath, targetKind === "directory");
1602
+ accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
1603
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
1604
+ }
1605
+ io.rename(operation.oldPath, operation.newPath);
1606
+ accumulator.mutations.push({
1607
+ kind: "rename",
1608
+ oldPath: operation.oldPath,
1609
+ newPath: operation.newPath,
1610
+ sourceKind: operation.sourceKind
1611
+ });
1612
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
1613
+ return;
1614
+ }
1615
+ io.remove(operation.path, operation.recursive);
1616
+ accumulator.mutations.push({
1617
+ kind: "delete",
1618
+ path: operation.path,
1619
+ targetKind: operation.targetKind
1620
+ });
1621
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1622
+ }
1623
+ function commitWorkspaceEditPlan(plan, options = {}) {
1624
+ if (options.signal?.aborted) {
1625
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
1626
+ }
1627
+ const stale = verifySnapshots(plan);
1628
+ if (stale)
1629
+ return stale;
1630
+ if (options.signal?.aborted) {
1631
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
1632
+ }
1633
+ const io = resolveIo(options.io);
1634
+ const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
1635
+ const context = { plan, io, accumulator };
1636
+ let lateAbort = false;
1637
+ for (const operation of plan.operations) {
1638
+ try {
1639
+ commitOperation(context, operation);
1640
+ } catch (error) {
1641
+ const detail = error instanceof Error ? error.message : String(error);
1642
+ return failedCommit(plan, {
1643
+ message: `I/O failure during ${operation.kind}: ${detail}`,
1644
+ changeIndex: operation.changeIndex,
1645
+ mutations: accumulator.mutations,
1646
+ filesModified: accumulator.filesModified,
1647
+ totalEdits: accumulator.totalEdits,
1648
+ lateAbort: lateAbort || options.signal?.aborted === true
1649
+ });
1650
+ }
1651
+ if (options.signal?.aborted)
1652
+ lateAbort = true;
1653
+ }
1654
+ const result = {
1655
+ success: true,
1656
+ filesModified: accumulator.filesModified,
1657
+ totalEdits: accumulator.totalEdits,
1658
+ errors: [],
1659
+ ...lateAbort ? { lateAbort: true } : {}
1660
+ };
1661
+ return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
1662
+ }
1663
+
1664
+ // ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
1665
+ import { createHash } from "node:crypto";
1666
+ function canonicalFingerprint(operations) {
1667
+ const canonical = operations.map((operation) => {
1668
+ switch (operation.kind) {
1669
+ case "text":
1670
+ return {
1671
+ kind: operation.kind,
1672
+ changeIndex: operation.changeIndex,
1673
+ path: operation.path,
1674
+ edits: operation.edits,
1675
+ version: operation.version
1676
+ };
1677
+ case "rename":
1678
+ return {
1679
+ kind: operation.kind,
1680
+ changeIndex: operation.changeIndex,
1681
+ oldPath: operation.oldPath,
1682
+ newPath: operation.newPath,
1683
+ overwrite: operation.overwrite,
1684
+ ignoreIfExists: operation.ignoreIfExists
1685
+ };
1686
+ case "create":
1687
+ return {
1688
+ kind: operation.kind,
1689
+ changeIndex: operation.changeIndex,
1690
+ path: operation.path,
1691
+ overwrite: operation.overwrite,
1692
+ ignoreIfExists: operation.ignoreIfExists
1693
+ };
1694
+ case "delete":
1695
+ return {
1696
+ kind: operation.kind,
1697
+ changeIndex: operation.changeIndex,
1698
+ path: operation.path,
1699
+ recursive: operation.recursive,
1700
+ ignoreIfNotExists: operation.ignoreIfNotExists
1701
+ };
1702
+ }
1703
+ });
1704
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
1705
+ }
1706
+
1707
+ // ../lsp-core/src/lsp/workspace-edit-types.ts
1708
+ class WorkspaceEditValidationError extends Error {
1709
+ changeIndex;
1710
+ detail;
1711
+ name = "WorkspaceEditValidationError";
1712
+ constructor(changeIndex, detail) {
1713
+ super(`change ${changeIndex}: ${detail}`);
1714
+ this.changeIndex = changeIndex;
1715
+ this.detail = detail;
1716
+ }
1717
+ }
1718
+
1719
+ // ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
1720
+ function isRecord2(value) {
1721
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1722
+ }
1723
+ function parsePosition(value) {
1724
+ if (!isRecord2(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
1725
+ return null;
1726
+ }
1727
+ return { line: value["line"], character: value["character"] };
1728
+ }
1729
+ function parseRange(value) {
1730
+ if (!isRecord2(value))
1731
+ return null;
1732
+ const start = parsePosition(value["start"]);
1733
+ const end = parsePosition(value["end"]);
1734
+ return start && end ? { start, end } : null;
1735
+ }
1736
+ function parseTextEdits(value, changeIndex) {
1737
+ if (!Array.isArray(value)) {
1738
+ throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
1739
+ }
1740
+ const edits = [];
1741
+ for (const candidate of value) {
1742
+ if (!isRecord2(candidate) || typeof candidate["newText"] !== "string") {
1743
+ throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
1744
+ }
1745
+ if ("annotationId" in candidate) {
1746
+ throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
1747
+ }
1748
+ const range = parseRange(candidate["range"]);
1749
+ if (!range)
1750
+ throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
1751
+ edits.push({ range, newText: candidate["newText"] });
1752
+ }
1753
+ return edits;
1754
+ }
1755
+ function parseBooleanOption(options, key, changeIndex) {
1756
+ const value = options[key];
1757
+ if (value === undefined)
1758
+ return false;
1759
+ if (typeof value !== "boolean") {
1760
+ throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
1761
+ }
1762
+ return value;
1763
+ }
1764
+ function parseOptions(value, allowed, changeIndex) {
1765
+ if (value === undefined)
1766
+ return {};
1767
+ if (!isRecord2(value))
1768
+ throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
1769
+ for (const key of Object.keys(value)) {
1770
+ if (!allowed.includes(key))
1771
+ throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
1772
+ }
1773
+ const parsed = {};
1774
+ for (const key of allowed)
1775
+ parsed[key] = parseBooleanOption(value, key, changeIndex);
1776
+ return parsed;
1777
+ }
1778
+
1779
+ // ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
1780
+ function parseResourceChange(input) {
1781
+ const kind = input.change["kind"];
1782
+ if (kind === "create" || kind === "delete") {
1783
+ parseSinglePathResource(input, kind);
1784
+ return;
1785
+ }
1786
+ if (kind !== "rename") {
1787
+ throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
1788
+ }
1789
+ parseRename(input);
1790
+ }
1791
+ function parseSinglePathResource(input, kind) {
1792
+ const { change, changeIndex, workspaceRoot, target } = input;
1793
+ if (typeof change["uri"] !== "string")
1794
+ throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
1795
+ const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
1796
+ if (!resolvedPath.success) {
1797
+ target.failures.push({ changeIndex, message: resolvedPath.error });
1798
+ return;
1799
+ }
1800
+ if (kind === "create") {
1801
+ const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
1802
+ target.operations.push({
1803
+ kind,
1804
+ changeIndex,
1805
+ path: resolvedPath.path,
1806
+ reportedPath: resolvedPath.requestedPath,
1807
+ overwrite: options2["overwrite"] ?? false,
1808
+ ignoreIfExists: options2["ignoreIfExists"] ?? false,
1809
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
1810
+ });
1811
+ return;
1812
+ }
1813
+ const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
1814
+ target.operations.push({
1815
+ kind,
1816
+ changeIndex,
1817
+ path: resolvedPath.path,
1818
+ reportedPath: resolvedPath.requestedPath,
1819
+ recursive: options["recursive"] ?? false,
1820
+ ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
1821
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
1822
+ });
1823
+ }
1824
+ function parseRename(input) {
1825
+ const { change, changeIndex, workspaceRoot, target } = input;
1826
+ if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
1827
+ throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
1828
+ }
1829
+ const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
1830
+ const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
1831
+ if (!oldPath.success || !newPath.success) {
1832
+ target.failures.push({
1833
+ changeIndex,
1834
+ message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
1835
+ });
1836
+ return;
1837
+ }
1838
+ const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
1839
+ target.operations.push({
1840
+ kind: "rename",
1841
+ changeIndex,
1842
+ oldPath: oldPath.path,
1843
+ newPath: newPath.path,
1844
+ reportedOldPath: oldPath.requestedPath,
1845
+ reportedNewPath: newPath.requestedPath,
1846
+ overwrite: options["overwrite"] ?? false,
1847
+ ignoreIfExists: options["ignoreIfExists"] ?? false,
1848
+ followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
1849
+ });
1850
+ }
1851
+
1852
+ // ../lsp-core/src/lsp/workspace-edit-parser.ts
1853
+ function failureResult(failures) {
1854
+ const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
1855
+ const first = sorted[0];
1856
+ return {
1857
+ success: false,
1858
+ filesModified: [],
1859
+ totalEdits: 0,
1860
+ errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
1861
+ ...first ? { failedChange: first.changeIndex } : {}
1862
+ };
1863
+ }
1864
+ function parseWorkspaceEdit(edit, workspaceRoot) {
1865
+ if (!isRecord2(edit))
1866
+ return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
1867
+ if (edit["changeAnnotations"] !== undefined) {
1868
+ return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
1869
+ }
1870
+ const hasChanges = edit["changes"] !== undefined;
1871
+ const hasDocumentChanges = edit["documentChanges"] !== undefined;
1872
+ if (hasChanges && hasDocumentChanges) {
1873
+ return {
1874
+ operations: [],
1875
+ failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
1876
+ };
1877
+ }
1878
+ const target = { operations: [], failures: [] };
1879
+ if (hasChanges)
1880
+ return parseChanges(edit["changes"], workspaceRoot, target);
1881
+ return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
1882
+ }
1883
+ function parseChanges(value, workspaceRoot, target) {
1884
+ if (!isRecord2(value))
1885
+ return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
1886
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
1887
+ for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
1888
+ const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
1889
+ if (!resolvedPath.success) {
1890
+ target.failures.push({ changeIndex, message: resolvedPath.error });
1891
+ continue;
1892
+ }
1893
+ try {
1894
+ target.operations.push({
1895
+ kind: "text",
1896
+ changeIndex,
1897
+ path: resolvedPath.path,
1898
+ reportedPath: resolvedPath.requestedPath,
1899
+ edits: parseTextEdits(rawEdits, changeIndex),
1900
+ version: null
1901
+ });
1902
+ } catch (error) {
1903
+ if (error instanceof WorkspaceEditValidationError) {
1904
+ target.failures.push({ changeIndex, message: error.detail });
1905
+ continue;
1906
+ }
1907
+ throw error;
1908
+ }
1909
+ }
1910
+ return target;
1911
+ }
1912
+ function parseDocumentChanges(value, workspaceRoot, target) {
1913
+ if (value === undefined)
1914
+ return target;
1915
+ if (!Array.isArray(value)) {
1916
+ return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
1917
+ }
1918
+ for (const [changeIndex, change] of value.entries()) {
1919
+ try {
1920
+ parseDocumentChange({ change, changeIndex, workspaceRoot, target });
1921
+ } catch (error) {
1922
+ if (error instanceof WorkspaceEditValidationError) {
1923
+ target.failures.push({ changeIndex, message: error.detail });
1924
+ continue;
1925
+ }
1926
+ throw error;
1927
+ }
1928
+ }
1929
+ return target;
1930
+ }
1931
+ function parseDocumentChange(input) {
1932
+ const { change, changeIndex, workspaceRoot, target } = input;
1933
+ if (!isRecord2(change))
1934
+ throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
1935
+ if ("annotationId" in change) {
1936
+ throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
1937
+ }
1938
+ if (typeof change["kind"] === "string") {
1939
+ parseResourceChange({ change, changeIndex, workspaceRoot, target });
1940
+ return;
1941
+ }
1942
+ const identifier = change["textDocument"];
1943
+ if (!isRecord2(identifier) || typeof identifier["uri"] !== "string") {
1944
+ throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
1945
+ }
1946
+ const version = identifier["version"];
1947
+ if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
1948
+ throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
1949
+ }
1950
+ const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
1951
+ if (!resolvedPath.success) {
1952
+ target.failures.push({ changeIndex, message: resolvedPath.error });
1953
+ return;
1954
+ }
1955
+ target.operations.push({
1956
+ kind: "text",
1957
+ changeIndex,
1958
+ path: resolvedPath.path,
1959
+ reportedPath: resolvedPath.requestedPath,
1960
+ edits: parseTextEdits(change["edits"], changeIndex),
1961
+ version
1962
+ });
1963
+ }
1964
+
1965
+ // ../lsp-core/src/lsp/workspace-edit-simulation.ts
1966
+ import { dirname as dirname2, relative as relative3, resolve as resolve3 } from "node:path";
1967
+
1968
+ // ../lsp-core/src/lsp/workspace-edit-text.ts
1969
+ function comparePosition(left, right) {
1970
+ return left.line === right.line ? left.character - right.character : left.line - right.line;
1971
+ }
1972
+ function positionsEqual(left, right) {
1973
+ return left.line === right.line && left.character === right.character;
1974
+ }
1975
+ function rangesEqual(left, right) {
1976
+ return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
1977
+ }
1978
+ function isEmptyRange(range) {
1979
+ return positionsEqual(range.start, range.end);
1980
+ }
1981
+ function formatRange(range) {
1982
+ return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
1983
+ }
1984
+ function validatePosition(position, label, context) {
1985
+ const { lines, changeIndex } = context;
1986
+ if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
1987
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
1988
+ }
1989
+ if (position.line < 0 || position.character < 0) {
1990
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
1991
+ }
1992
+ const line = lines[position.line];
1993
+ if (line === undefined) {
1994
+ throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
1995
+ }
1996
+ if (position.character > line.length) {
1997
+ throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
1998
+ }
1999
+ }
2000
+ function validateRange(range, lines, changeIndex) {
2001
+ const context = { lines, changeIndex };
2002
+ validatePosition(range.start, "start", context);
2003
+ validatePosition(range.end, "end", context);
2004
+ if (comparePosition(range.start, range.end) > 0) {
2005
+ throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
2006
+ }
2007
+ }
2008
+ function sortAndDeduplicate(edits) {
2009
+ const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
2010
+ const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
2011
+ return positionOrder === 0 ? right.index - left.index : positionOrder;
2012
+ });
2013
+ const unique = [];
2014
+ for (const entry of sorted) {
2015
+ const previous = unique.at(-1);
2016
+ if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
2017
+ continue;
2018
+ }
2019
+ unique.push(entry.edit);
2020
+ }
2021
+ return unique;
2022
+ }
2023
+ function validateNoOverlap(edits, changeIndex) {
2024
+ for (let index = 0;index < edits.length - 1; index += 1) {
2025
+ const later = edits[index];
2026
+ const earlier = edits[index + 1];
2027
+ if (later === undefined || earlier === undefined)
2028
+ continue;
2029
+ if (comparePosition(earlier.range.end, later.range.start) > 0) {
2030
+ throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
2031
+ }
2032
+ }
2033
+ }
2034
+ function applyNormalizedTextEdits(content, edits) {
2035
+ const lines = content.split(`
2036
+ `);
2037
+ for (const edit of edits) {
2038
+ const { start, end } = edit.range;
2039
+ const startLine = lines[start.line];
2040
+ const endLine = lines[end.line];
2041
+ if (startLine === undefined || endLine === undefined)
2042
+ continue;
2043
+ const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
2044
+ lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
2045
+ `));
2046
+ }
2047
+ return lines.join(`
2048
+ `);
2049
+ }
2050
+ function normalizeTextEdits(content, edits, changeIndex) {
2051
+ const lines = content.split(`
2052
+ `);
2053
+ for (const edit of edits) {
2054
+ validateRange(edit.range, lines, changeIndex);
2055
+ }
2056
+ const normalized = sortAndDeduplicate(edits);
2057
+ validateNoOverlap(normalized, changeIndex);
2058
+ return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
2059
+ }
2060
+
2061
+ // ../lsp-core/src/lsp/workspace-edit-simulation.ts
2062
+ function isSameOrDescendant2(candidate, parent) {
2063
+ const relativePath = relative3(parent, candidate);
2064
+ return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
2065
+ }
2066
+ function removeVirtualSubtree(virtual, path) {
2067
+ for (const candidate of [...virtual.keys()]) {
2068
+ if (isSameOrDescendant2(candidate, path))
2069
+ virtual.delete(candidate);
2070
+ }
2071
+ virtual.set(path, { kind: "missing" });
2072
+ }
2073
+ function moveVirtualSubtree(virtual, oldPath, newPath) {
2074
+ const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
2075
+ removeVirtualSubtree(virtual, oldPath);
2076
+ removeVirtualSubtree(virtual, newPath);
2077
+ for (const [candidate, entry] of moved) {
2078
+ const suffix = relative3(oldPath, candidate);
2079
+ virtual.set(suffix === "" ? newPath : resolve3(newPath, suffix), entry);
2080
+ }
2081
+ }
2082
+ function virtualDirectoryHasChildren(virtual, path) {
2083
+ for (const [candidate, entry] of virtual) {
2084
+ if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
2085
+ return true;
2086
+ }
2087
+ return false;
2088
+ }
2089
+ function requireVirtualParent(virtual, path, changeIndex) {
2090
+ if (virtual.get(dirname2(path))?.kind !== "directory") {
2091
+ throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
2092
+ }
2093
+ }
2094
+ function simulateOperations(parsed, snapshots) {
2095
+ const virtual = new Map(snapshots);
2096
+ const planned = [];
2097
+ const failures = [];
2098
+ for (const operation of parsed) {
2099
+ try {
2100
+ planned.push(simulateOperation(operation, virtual));
2101
+ } catch (error) {
2102
+ if (error instanceof WorkspaceEditValidationError) {
2103
+ failures.push({ changeIndex: operation.changeIndex, message: error.detail });
2104
+ continue;
2105
+ }
2106
+ throw error;
2107
+ }
2108
+ }
2109
+ return { operations: planned, failures };
2110
+ }
2111
+ function simulateOperation(operation, virtual) {
2112
+ switch (operation.kind) {
2113
+ case "text":
2114
+ return simulateText(operation, virtual);
2115
+ case "create":
2116
+ return simulateCreate(operation, virtual);
2117
+ case "rename":
2118
+ return simulateRename(operation, virtual);
2119
+ case "delete":
2120
+ return simulateDelete(operation, virtual);
2121
+ }
2122
+ }
2123
+ function rejectSymbolicLink(operation) {
2124
+ if (operation.followedSymbolicLink) {
2125
+ throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
2126
+ }
2127
+ }
2128
+ function simulateText(operation, virtual) {
2129
+ const entry = virtual.get(operation.path);
2130
+ if (entry?.kind !== "file")
2131
+ throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
2132
+ const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
2133
+ virtual.set(operation.path, { kind: "file", content: normalized.text });
2134
+ return {
2135
+ kind: "text",
2136
+ changeIndex: operation.changeIndex,
2137
+ path: operation.path,
2138
+ beforeText: entry.content,
2139
+ afterText: normalized.text,
2140
+ editCount: normalized.edits.length,
2141
+ documentVersion: operation.version
2142
+ };
2143
+ }
2144
+ function simulateCreate(operation, virtual) {
2145
+ rejectSymbolicLink(operation);
2146
+ requireVirtualParent(virtual, operation.path, operation.changeIndex);
2147
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2148
+ if (target.kind !== "missing") {
2149
+ if (operation.overwrite && target.kind === "file") {
2150
+ virtual.set(operation.path, { kind: "file", content: "" });
2151
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
2152
+ }
2153
+ if (operation.ignoreIfExists)
2154
+ return { kind: "noop", changeIndex: operation.changeIndex };
2155
+ throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
2156
+ }
2157
+ virtual.set(operation.path, { kind: "file", content: "" });
2158
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
2159
+ }
2160
+ function simulateRename(operation, virtual) {
2161
+ rejectSymbolicLink(operation);
2162
+ const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
2163
+ if (source.kind === "missing") {
2164
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
2165
+ }
2166
+ if (operation.oldPath === operation.newPath)
2167
+ return { kind: "noop", changeIndex: operation.changeIndex };
2168
+ if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
2169
+ throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
2170
+ }
2171
+ requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
2172
+ const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
2173
+ if (destination.kind !== "missing" && !operation.overwrite) {
2174
+ if (operation.ignoreIfExists)
2175
+ return { kind: "noop", changeIndex: operation.changeIndex };
2176
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
2177
+ }
2178
+ moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
2179
+ return {
2180
+ kind: "rename",
2181
+ changeIndex: operation.changeIndex,
2182
+ oldPath: operation.oldPath,
2183
+ newPath: operation.newPath,
2184
+ sourceKind: source.kind,
2185
+ replaceDestination: destination.kind !== "missing"
2186
+ };
2187
+ }
2188
+ function simulateDelete(operation, virtual) {
2189
+ rejectSymbolicLink(operation);
2190
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2191
+ if (target.kind === "missing") {
2192
+ if (operation.ignoreIfNotExists)
2193
+ return { kind: "noop", changeIndex: operation.changeIndex };
2194
+ throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
2195
+ }
2196
+ if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
2197
+ throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
2198
+ }
2199
+ removeVirtualSubtree(virtual, operation.path);
2200
+ return {
2201
+ kind: "delete",
2202
+ changeIndex: operation.changeIndex,
2203
+ path: operation.path,
2204
+ targetKind: target.kind,
2205
+ recursive: operation.recursive
2206
+ };
2207
+ }
2208
+
2209
+ // ../lsp-core/src/lsp/workspace-edit-snapshot.ts
2210
+ import { existsSync as existsSync4, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
2211
+ import { dirname as dirname3, resolve as resolve4 } from "node:path";
2212
+ class WorkspaceSnapshotBuilder {
2213
+ workspaceRoot;
2214
+ snapshots = new Map;
2215
+ constructor(workspaceRoot) {
2216
+ this.workspaceRoot = workspaceRoot;
2217
+ }
2218
+ build(operations) {
2219
+ this.add(this.workspaceRoot, false);
2220
+ for (const operation of operations) {
2221
+ switch (operation.kind) {
2222
+ case "rename":
2223
+ this.add(operation.oldPath, true);
2224
+ this.add(operation.newPath, true);
2225
+ break;
2226
+ case "delete":
2227
+ this.add(operation.path, true);
2228
+ break;
2229
+ case "text":
2230
+ case "create":
2231
+ this.add(operation.path, false);
2232
+ break;
2233
+ }
2234
+ }
2235
+ return this.snapshots;
2236
+ }
2237
+ add(path, includeChildren) {
2238
+ let candidate = path;
2239
+ while (true) {
2240
+ const existing = this.snapshots.get(candidate);
2241
+ if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
2242
+ this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
2243
+ }
2244
+ if (candidate === this.workspaceRoot)
2245
+ break;
2246
+ candidate = dirname3(candidate);
2247
+ }
2248
+ if (!includeChildren || !existsSync4(path) || !lstatSync3(path).isDirectory())
2249
+ return;
2250
+ for (const child of readdirSync2(path))
2251
+ this.add(resolve4(path, child), true);
2252
+ }
2253
+ }
2254
+ function snapshotOperations(operations, workspaceRoot) {
2255
+ return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
2256
+ }
2257
+
2258
+ // ../lsp-core/src/lsp/workspace-edit-plan.ts
2259
+ class PlanPathIndex {
2260
+ firstChangeByPath = new Map;
2261
+ reportedPathByCanonical = new Map;
2262
+ build(operations) {
2263
+ for (const operation of operations) {
2264
+ switch (operation.kind) {
2265
+ case "rename":
2266
+ this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
2267
+ this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
2268
+ break;
2269
+ case "text":
2270
+ case "create":
2271
+ case "delete":
2272
+ this.add(operation.path, operation.reportedPath, operation.changeIndex);
2273
+ break;
2274
+ }
2275
+ }
2276
+ }
2277
+ add(path, reportedPath2, changeIndex) {
2278
+ if (!this.firstChangeByPath.has(path))
2279
+ this.firstChangeByPath.set(path, changeIndex);
2280
+ if (!this.reportedPathByCanonical.has(path))
2281
+ this.reportedPathByCanonical.set(path, reportedPath2);
2282
+ }
2283
+ }
2284
+ function fingerprintWorkspaceEdit(edit, workspaceRoot) {
2285
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2286
+ if (!root.success)
2287
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2288
+ const parsed = parseWorkspaceEdit(edit, root.path);
2289
+ if (parsed.failures.length > 0)
2290
+ return { success: false, result: failureResult(parsed.failures) };
2291
+ return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
2292
+ }
2293
+ function planWorkspaceEdit(edit, workspaceRoot) {
2294
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2295
+ if (!root.success)
2296
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2297
+ const parsed = parseWorkspaceEdit(edit, root.path);
2298
+ if (parsed.failures.length > 0)
2299
+ return { success: false, result: failureResult(parsed.failures) };
2300
+ let snapshots;
2301
+ try {
2302
+ snapshots = snapshotOperations(parsed.operations, root.path);
2303
+ } catch (error) {
2304
+ return {
2305
+ success: false,
2306
+ result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
2307
+ };
2308
+ }
2309
+ const simulated = simulateOperations(parsed.operations, snapshots);
2310
+ if (simulated.failures.length > 0)
2311
+ return { success: false, result: failureResult(simulated.failures) };
2312
+ const paths = new PlanPathIndex;
2313
+ paths.build(parsed.operations);
2314
+ const plan = {
2315
+ workspaceRoot: root.path,
2316
+ operations: simulated.operations,
2317
+ snapshots,
2318
+ firstChangeByPath: paths.firstChangeByPath,
2319
+ reportedPathByCanonical: paths.reportedPathByCanonical,
2320
+ fingerprint: canonicalFingerprint(parsed.operations)
2321
+ };
2322
+ return { success: true, plan };
2323
+ }
2324
+
2325
+ // ../lsp-core/src/lsp/workspace-mutation-controller.ts
2326
+ function failure(message, failedChange, base) {
2327
+ return {
2328
+ success: false,
2329
+ filesModified: base?.filesModified ?? [],
2330
+ totalEdits: base?.totalEdits ?? 0,
2331
+ errors: [message],
2332
+ ...failedChange === undefined ? {} : { failedChange },
2333
+ ...base?.lateAbort ? { lateAbort: true } : {}
2334
+ };
2335
+ }
2336
+ function responseFor(result) {
2337
+ if (result.success)
2338
+ return { applied: true };
2339
+ return {
2340
+ applied: false,
2341
+ failureReason: result.errors[0] ?? "workspace edit failed",
2342
+ ...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
2343
+ };
2344
+ }
2345
+ function isRecord3(value) {
2346
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2347
+ }
2348
+
2349
+ class WorkspaceMutationController {
2350
+ workspaceRoot;
2351
+ documents;
2352
+ activeLease = null;
2353
+ nextLeaseId = 1;
2354
+ io;
2355
+ constructor(workspaceRoot, documents) {
2356
+ this.workspaceRoot = workspaceRoot;
2357
+ this.documents = documents;
2358
+ }
2359
+ setIo(io) {
2360
+ this.io = io;
2361
+ }
2362
+ acquire(signal) {
2363
+ if (this.activeLease)
2364
+ return { success: false, result: failure("workspace mutation is already in progress") };
2365
+ if (signal?.aborted)
2366
+ return { success: false, result: failure("cancelled before mutating request") };
2367
+ const lease = {
2368
+ id: this.nextLeaseId,
2369
+ phase: "idle",
2370
+ ...signal === undefined ? {} : { signal }
2371
+ };
2372
+ this.nextLeaseId += 1;
2373
+ this.activeLease = lease;
2374
+ return { success: true, lease };
2375
+ }
2376
+ release(lease) {
2377
+ if (this.activeLease?.id !== lease.id)
2378
+ return;
2379
+ this.activeLease.phase = "sealed";
2380
+ this.activeLease = null;
2381
+ }
2382
+ isBeforeCommit(lease) {
2383
+ return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
2384
+ }
2385
+ async handleApplyEdit(params) {
2386
+ const lease = this.activeLease;
2387
+ if (!lease)
2388
+ return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
2389
+ if (lease.phase !== "idle") {
2390
+ return {
2391
+ applied: false,
2392
+ failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
2393
+ };
2394
+ }
2395
+ lease.phase = "applying";
2396
+ lease.applyCompletion = new Promise((resolve5) => {
2397
+ lease.resolveApply = resolve5;
2398
+ });
2399
+ const edit = isRecord3(params) ? params["edit"] : undefined;
2400
+ const record = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
2401
+ lease.serverApply = record;
2402
+ lease.phase = "settled";
2403
+ lease.resolveApply?.();
2404
+ return responseFor(record.result);
2405
+ }
2406
+ async reconcileRename(leaseToken, edit) {
2407
+ const lease = this.requireActiveLease(leaseToken);
2408
+ if (!lease)
2409
+ return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
2410
+ if (lease.phase === "applying")
2411
+ await lease.applyCompletion;
2412
+ if (lease.serverApply)
2413
+ return this.reconcileServerApply(lease.serverApply, edit);
2414
+ lease.phase = "sealed";
2415
+ if (!edit)
2416
+ return { edit, apply: failure("No edit provided") };
2417
+ const applied = await this.applyEdit(edit, lease);
2418
+ return { edit, apply: applied.result };
2419
+ }
2420
+ reconcileServerApply(record, edit) {
2421
+ if (!edit)
2422
+ return { edit, apply: record.result };
2423
+ const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
2424
+ if (fingerprint.success && record.fingerprint !== null && fingerprint.fingerprint === record.fingerprint) {
2425
+ return { edit, apply: record.result };
2426
+ }
2427
+ return {
2428
+ edit,
2429
+ apply: failure("rename result conflicts with server-applied workspace edit", 0, record.result)
2430
+ };
2431
+ }
2432
+ async applyEdit(edit, lease) {
2433
+ const planned = planWorkspaceEdit(edit, this.workspaceRoot);
2434
+ if (!planned.success)
2435
+ return { fingerprint: null, result: planned.result };
2436
+ const versionFailure = this.documents.validateVersions(planned.plan.operations);
2437
+ if (versionFailure) {
2438
+ return {
2439
+ fingerprint: planned.plan.fingerprint,
2440
+ result: failure(versionFailure.message, versionFailure.changeIndex)
2441
+ };
2442
+ }
2443
+ const commit = commitWorkspaceEditPlan(planned.plan, {
2444
+ ...lease.signal === undefined ? {} : { signal: lease.signal },
2445
+ ...this.io === undefined ? {} : { io: this.io }
2446
+ });
2447
+ let result = commit.result;
2448
+ if (commit.delta.operations.length > 0) {
2449
+ try {
2450
+ await this.documents.synchronize(commit.delta);
2451
+ } catch (error) {
2452
+ const message = error instanceof Error ? error.message : String(error);
2453
+ result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
2454
+ }
2455
+ }
2456
+ if (lease.signal?.aborted && !result.lateAbort)
2457
+ result = { ...result, lateAbort: true };
2458
+ return { fingerprint: planned.plan.fingerprint, result };
2459
+ }
2460
+ requireActiveLease(lease) {
2461
+ return this.activeLease?.id === lease.id ? this.activeLease : null;
2462
+ }
2463
+ }
2464
+
995
2465
  // ../lsp-core/src/lsp/client.ts
996
- var POST_OPEN_DELAY_MS = 1000;
997
- var POST_DIAGNOSTICS_WAIT_MS = 500;
2466
+ var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
2467
+ var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
998
2468
 
999
2469
  class LspClient extends LspClientConnection {
1000
- openedFiles = new Set;
1001
- documentVersions = new Map;
1002
- lastSyncedText = new Map;
1003
2470
  diagnosticPullErrors = [];
2471
+ documents;
2472
+ workspaceMutations;
2473
+ diagnosticsFreshnessTimeoutMs;
2474
+ constructor(root, server, options = {}) {
2475
+ super(root, server, options);
2476
+ this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
2477
+ this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
2478
+ versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
2479
+ });
2480
+ this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
2481
+ this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
2482
+ }
1004
2483
  getDiagnosticPullErrors() {
1005
2484
  return this.diagnosticPullErrors;
1006
2485
  }
1007
2486
  async openFile(filePath) {
1008
- const absPath = resolve(contextCwd(), filePath);
1009
- const uri = pathToFileURL2(absPath).href;
1010
- const text = readFileSync(absPath, "utf-8");
1011
- if (!this.openedFiles.has(absPath)) {
1012
- const ext = effectiveExtension(absPath);
1013
- const languageId = getLanguageId(ext);
1014
- const version = 1;
1015
- await this.sendNotification("textDocument/didOpen", {
1016
- textDocument: {
1017
- uri,
1018
- languageId,
1019
- version,
1020
- text
1021
- }
1022
- });
1023
- this.openedFiles.add(absPath);
1024
- this.documentVersions.set(uri, version);
1025
- this.lastSyncedText.set(uri, text);
1026
- await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
1027
- return;
1028
- }
1029
- const prevText = this.lastSyncedText.get(uri);
1030
- if (prevText === text) {
1031
- return;
1032
- }
1033
- const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
1034
- this.documentVersions.set(uri, nextVersion);
1035
- this.lastSyncedText.set(uri, text);
1036
- await this.sendNotification("textDocument/didChange", {
1037
- textDocument: { uri, version: nextVersion },
1038
- contentChanges: [{ text }]
1039
- });
1040
- await this.sendNotification("textDocument/didSave", {
1041
- textDocument: { uri },
1042
- text
1043
- });
2487
+ const absPath = this.resolveWorkspacePath(filePath);
2488
+ await this.documents.openFile(absPath);
2489
+ }
2490
+ getOpenDocumentVersion(filePath) {
2491
+ return this.documents.getVersion(this.resolveWorkspacePath(filePath));
2492
+ }
2493
+ getStoredDiagnostics(uri) {
2494
+ return [...this.documents.getStoredDiagnostics(uri)];
1044
2495
  }
1045
- async definition(filePath, line, character) {
1046
- const absPath = resolve(contextCwd(), filePath);
2496
+ setWorkspaceEditIo(io) {
2497
+ this.workspaceMutations.setIo(io);
2498
+ }
2499
+ handlePublishDiagnostics(params) {
2500
+ super.handlePublishDiagnostics(params);
2501
+ this.documents.recordPublishedDiagnostics(params);
2502
+ }
2503
+ async definition(filePath, line, character, signal) {
2504
+ const absPath = this.resolveWorkspacePath(filePath);
1047
2505
  await this.openFile(absPath);
2506
+ const options = signal === undefined ? {} : { signal };
1048
2507
  return this.sendRequest("textDocument/definition", {
1049
- textDocument: { uri: pathToFileURL2(absPath).href },
2508
+ textDocument: { uri: pathToFileURL3(absPath).href },
1050
2509
  position: { line: line - 1, character }
1051
- });
2510
+ }, options);
1052
2511
  }
1053
- async references(filePath, line, character, includeDeclaration = true) {
1054
- const absPath = resolve(contextCwd(), filePath);
2512
+ async references(filePath, line, character, includeDeclaration = true, signal) {
2513
+ const absPath = this.resolveWorkspacePath(filePath);
1055
2514
  await this.openFile(absPath);
2515
+ const options = signal === undefined ? {} : { signal };
1056
2516
  return this.sendRequest("textDocument/references", {
1057
- textDocument: { uri: pathToFileURL2(absPath).href },
2517
+ textDocument: { uri: pathToFileURL3(absPath).href },
1058
2518
  position: { line: line - 1, character },
1059
2519
  context: { includeDeclaration }
1060
- });
2520
+ }, options);
1061
2521
  }
1062
- async documentSymbols(filePath) {
1063
- const absPath = resolve(contextCwd(), filePath);
2522
+ async documentSymbols(filePath, signal) {
2523
+ const absPath = this.resolveWorkspacePath(filePath);
1064
2524
  await this.openFile(absPath);
2525
+ const options = signal === undefined ? {} : { signal };
1065
2526
  return this.sendRequest("textDocument/documentSymbol", {
1066
- textDocument: { uri: pathToFileURL2(absPath).href }
1067
- });
2527
+ textDocument: { uri: pathToFileURL3(absPath).href }
2528
+ }, options);
1068
2529
  }
1069
- async workspaceSymbols(query) {
1070
- return this.sendRequest("workspace/symbol", { query });
2530
+ async workspaceSymbols(query, signal) {
2531
+ const options = signal === undefined ? {} : { signal };
2532
+ return this.sendRequest("workspace/symbol", { query }, options);
1071
2533
  }
1072
2534
  isUnsupportedDiagnosticPullError(error) {
1073
2535
  if (!(error instanceof Error))
@@ -1077,43 +2539,174 @@ class LspClient extends LspClientConnection {
1077
2539
  return true;
1078
2540
  return /unsupported|not supported|method not found|unknown request/i.test(error.message);
1079
2541
  }
1080
- async diagnostics(filePath) {
1081
- const absPath = resolve(contextCwd(), filePath);
1082
- const uri = pathToFileURL2(absPath).href;
1083
- await this.openFile(absPath);
1084
- await new Promise((r) => setTimeout(r, POST_DIAGNOSTICS_WAIT_MS));
1085
- try {
1086
- const result = await this.sendRequest("textDocument/diagnostic", {
1087
- textDocument: { uri }
1088
- });
1089
- if (result.items) {
1090
- return { items: result.items };
2542
+ freshnessTimeout(absPath) {
2543
+ return {
2544
+ items: [],
2545
+ transientError: {
2546
+ kind: "freshness_timeout",
2547
+ message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
1091
2548
  }
1092
- } catch (error) {
1093
- if (!this.isUnsupportedDiagnosticPullError(error)) {
1094
- this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
2549
+ };
2550
+ }
2551
+ parseDiagnosticPullReport(value) {
2552
+ if (value.kind === "unchanged") {
2553
+ return {
2554
+ type: "unchanged",
2555
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
2556
+ };
2557
+ }
2558
+ return {
2559
+ type: "full",
2560
+ diagnostics: value.items ?? [],
2561
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
2562
+ };
2563
+ }
2564
+ async diagnostics(filePath, signal) {
2565
+ signal?.throwIfAborted();
2566
+ const absPath = this.resolveWorkspacePath(filePath);
2567
+ const uri = pathToFileURL3(absPath).href;
2568
+ await this.openFile(absPath);
2569
+ const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
2570
+ for (;; ) {
2571
+ signal?.throwIfAborted();
2572
+ const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
2573
+ if (!snapshot)
2574
+ return this.freshnessTimeout(absPath);
2575
+ const push = this.documents.resolvePushDiagnostics(snapshot);
2576
+ if (push.status === "ready")
2577
+ return { items: [...push.diagnostics] };
2578
+ let pushFallbackOnly = !this.isDiagnosticPullSupported();
2579
+ if (!pushFallbackOnly) {
2580
+ const cached = this.documents.getPullCache(snapshot);
2581
+ try {
2582
+ const remainingMs2 = deadlineAt - Date.now();
2583
+ if (remainingMs2 <= 0)
2584
+ return this.freshnessTimeout(absPath);
2585
+ const result = await this.sendRequest("textDocument/diagnostic", {
2586
+ textDocument: { uri },
2587
+ ...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
2588
+ }, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
2589
+ if (!this.documents.isCurrentSnapshot(snapshot))
2590
+ continue;
2591
+ const report = this.parseDiagnosticPullReport(result);
2592
+ if (report.type === "full") {
2593
+ this.documents.recordPullDiagnostics(snapshot, {
2594
+ kind: "full",
2595
+ diagnostics: report.diagnostics,
2596
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
2597
+ });
2598
+ return { items: [...report.diagnostics] };
2599
+ }
2600
+ if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
2601
+ return { items: [...cached.diagnostics] };
2602
+ }
2603
+ } catch (error) {
2604
+ if (this.isUnsupportedDiagnosticPullError(error)) {
2605
+ this.setDiagnosticPullSupported(false);
2606
+ pushFallbackOnly = true;
2607
+ } else if (error instanceof LspRequestTimeoutError) {
2608
+ pushFallbackOnly = true;
2609
+ } else {
2610
+ this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
2611
+ throw error;
2612
+ }
2613
+ }
1095
2614
  }
2615
+ if (!pushFallbackOnly)
2616
+ continue;
2617
+ const remainingMs = deadlineAt - Date.now();
2618
+ if (remainingMs <= 0)
2619
+ return this.freshnessTimeout(absPath);
2620
+ const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
2621
+ await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
1096
2622
  }
1097
- return { items: this.getStoredDiagnostics(uri) };
1098
2623
  }
1099
- async prepareRename(filePath, line, character) {
1100
- const absPath = resolve(contextCwd(), filePath);
2624
+ async prepareRename(filePath, line, character, signal) {
2625
+ const absPath = this.resolveWorkspacePath(filePath);
1101
2626
  await this.openFile(absPath);
2627
+ const options = signal === undefined ? {} : { signal };
1102
2628
  return this.sendRequest("textDocument/prepareRename", {
1103
- textDocument: { uri: pathToFileURL2(absPath).href },
2629
+ textDocument: { uri: pathToFileURL3(absPath).href },
1104
2630
  position: { line: line - 1, character }
1105
- });
2631
+ }, options);
1106
2632
  }
1107
- async rename(filePath, line, character, newName) {
1108
- const absPath = resolve(contextCwd(), filePath);
2633
+ async rename(filePath, line, character, newName, signal) {
2634
+ const absPath = this.resolveWorkspacePath(filePath);
1109
2635
  await this.openFile(absPath);
1110
- return this.sendRequest("textDocument/rename", {
1111
- textDocument: { uri: pathToFileURL2(absPath).href },
1112
- position: { line: line - 1, character },
1113
- newName
1114
- });
2636
+ const acquired = this.workspaceMutations.acquire(signal);
2637
+ if (!acquired.success)
2638
+ return { edit: null, apply: acquired.result };
2639
+ const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
2640
+ try {
2641
+ const renameParams = {
2642
+ textDocument: { uri: pathToFileURL3(absPath).href },
2643
+ position: { line: line - 1, character },
2644
+ newName
2645
+ };
2646
+ const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
2647
+ signal: preCommitSignal.signal
2648
+ });
2649
+ return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
2650
+ } finally {
2651
+ preCommitSignal?.dispose();
2652
+ this.workspaceMutations.release(acquired.lease);
2653
+ }
2654
+ }
2655
+ resolveWorkspacePath(filePath) {
2656
+ return resolve5(this.root, filePath);
1115
2657
  }
1116
2658
  }
2659
+ function waitForDiagnosticsActivity(wait, signal) {
2660
+ if (!signal)
2661
+ return wait;
2662
+ if (signal.aborted)
2663
+ return Promise.reject(abortError2(signal));
2664
+ return new Promise((resolve6, reject) => {
2665
+ const onAbort = () => {
2666
+ signal.removeEventListener("abort", onAbort);
2667
+ reject(abortError2(signal));
2668
+ };
2669
+ signal.addEventListener("abort", onAbort, { once: true });
2670
+ wait.then(() => {
2671
+ signal.removeEventListener("abort", onAbort);
2672
+ resolve6();
2673
+ }, (error) => {
2674
+ signal.removeEventListener("abort", onAbort);
2675
+ reject(error);
2676
+ });
2677
+ });
2678
+ }
2679
+ function createPreCommitAbortSignal(source, isBeforeCommit) {
2680
+ if (!source)
2681
+ return;
2682
+ const controller = new AbortController;
2683
+ const onAbort = () => {
2684
+ if (isBeforeCommit() && !controller.signal.aborted)
2685
+ controller.abort(preCommitAbortReason(source));
2686
+ };
2687
+ if (source.aborted)
2688
+ onAbort();
2689
+ else
2690
+ source.addEventListener("abort", onAbort, { once: true });
2691
+ return {
2692
+ signal: controller.signal,
2693
+ dispose: () => source.removeEventListener("abort", onAbort)
2694
+ };
2695
+ }
2696
+ function preCommitAbortReason(source) {
2697
+ const reason = source.reason;
2698
+ if (reason instanceof Error && reason.name !== "AbortError")
2699
+ return reason;
2700
+ return new Error("LSP request cancelled before workspace edit commit");
2701
+ }
2702
+ function abortError2(signal) {
2703
+ const reason = signal.reason;
2704
+ if (reason instanceof Error)
2705
+ return reason;
2706
+ const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
2707
+ error.name = "AbortError";
2708
+ return error;
2709
+ }
1117
2710
 
1118
2711
  // ../lsp-core/src/lsp/process-signal-cleanup.ts
1119
2712
  function installProcessSignalCleanup(cleanup) {
@@ -1144,7 +2737,7 @@ async function stopClientBestEffort(client) {
1144
2737
  function awaitWithSignal(promise, signal) {
1145
2738
  if (!signal)
1146
2739
  return promise;
1147
- return new Promise((resolve2, reject) => {
2740
+ return new Promise((resolve6, reject) => {
1148
2741
  let settled = false;
1149
2742
  const onAbort = () => {
1150
2743
  if (settled)
@@ -1162,7 +2755,7 @@ function awaitWithSignal(promise, signal) {
1162
2755
  return;
1163
2756
  settled = true;
1164
2757
  signal.removeEventListener("abort", onAbort);
1165
- resolve2(value);
2758
+ resolve6(value);
1166
2759
  }, (err) => {
1167
2760
  if (settled)
1168
2761
  return;
@@ -1428,9 +3021,6 @@ function errorResponse(id, code, message, data) {
1428
3021
  function jsonRpcId(value) {
1429
3022
  return typeof value === "string" || typeof value === "number" || value === null ? value : null;
1430
3023
  }
1431
- function messageFromError(error) {
1432
- return error instanceof Error ? error.message : String(error);
1433
- }
1434
3024
  // ../mcp-stdio-core/src/transport.ts
1435
3025
  var HEADER_SEPARATOR2 = Buffer.from(`\r
1436
3026
  \r
@@ -1617,29 +3207,201 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
1617
3207
  closed: () => isClosed
1618
3208
  };
1619
3209
  }
1620
- // ../lsp-core/src/tools/diagnostics.ts
1621
- import { resolve as resolve4 } from "node:path";
1622
-
1623
3210
  // ../lsp-core/src/lsp/client-wrapper.ts
1624
- import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
1625
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "node:path";
3211
+ import { existsSync as existsSync9, statSync as statSync3 } from "node:fs";
3212
+ import { dirname as dirname6, join as join4, resolve as resolve7 } from "node:path";
1626
3213
 
1627
- // ../lsp-core/src/lsp/server-install-state.ts
1628
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
3214
+ // ../lsp-core/src/request-context.ts
3215
+ import { AsyncLocalStorage } from "node:async_hooks";
3216
+ import { existsSync as existsSync5, realpathSync as realpathSync3, statSync as statSync2 } from "node:fs";
1629
3217
  import { homedir } from "node:os";
1630
- import { dirname, isAbsolute, join as join2 } from "node:path";
3218
+ import { basename as basename2, delimiter as delimiter2, dirname as dirname4, isAbsolute as isAbsolute2, join as join2, relative as relative4, resolve as resolve6 } from "node:path";
3219
+
3220
+ class LspRequestContextParseError extends Error {
3221
+ code;
3222
+ name = "LspRequestContextParseError";
3223
+ constructor(code, message) {
3224
+ super(message);
3225
+ this.code = code;
3226
+ }
3227
+ }
3228
+
3229
+ class LspRequestContextUnavailableError extends Error {
3230
+ name = "LspRequestContextUnavailableError";
3231
+ constructor() {
3232
+ super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
3233
+ }
3234
+ }
3235
+ var storage = new AsyncLocalStorage;
3236
+ var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
3237
+ var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
3238
+ function runWithRequestContext(context, fn) {
3239
+ return storage.run(context, fn);
3240
+ }
3241
+ function lspRequestContext() {
3242
+ const context = storage.getStore();
3243
+ if (!context)
3244
+ throw new LspRequestContextUnavailableError;
3245
+ return context;
3246
+ }
3247
+ function contextCwd() {
3248
+ return lspRequestContext().cwd;
3249
+ }
3250
+ function contextEnv(key) {
3251
+ const context = lspRequestContext();
3252
+ if (key === "LSP_TOOLS_MCP_PROJECT_CONFIG")
3253
+ return context.projectConfigPaths.join(delimiter2);
3254
+ if (key === "LSP_TOOLS_MCP_USER_CONFIG")
3255
+ return context.userConfigPath;
3256
+ if (key === "LSP_TOOLS_MCP_INSTALL_DECISIONS")
3257
+ return context.installDecisionsPath;
3258
+ return;
3259
+ }
3260
+ function createStandaloneMcpRequestContext(input = {}) {
3261
+ const env = input.env ?? process.env;
3262
+ const cwd = canonicalCwd(input.cwd ?? process.cwd());
3263
+ const home = input.homeDir ?? homedir();
3264
+ const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
3265
+ const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
3266
+ const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
3267
+ return parseLspRequestContext({
3268
+ cwd,
3269
+ projectConfigPaths,
3270
+ userConfigPath,
3271
+ installDecisionsPath,
3272
+ capabilities: { installDecisionTool: true }
3273
+ });
3274
+ }
3275
+ function parseLspRequestContext(value) {
3276
+ if (!isRecord4(value)) {
3277
+ throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
3278
+ }
3279
+ rejectUnknownFields(value, CONTEXT_FIELDS, "context");
3280
+ const cwd = stringField(value, "cwd");
3281
+ const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
3282
+ const userConfigPath = stringField(value, "userConfigPath");
3283
+ const installDecisionsPath = stringField(value, "installDecisionsPath");
3284
+ const capabilities = capabilitiesField(value["capabilities"]);
3285
+ const canonical = canonicalCwd(cwd);
3286
+ for (const path of projectConfigPaths) {
3287
+ requireAbsolutePath(path, "projectConfigPaths");
3288
+ const projectPath = canonicalizeExistingOrNearestAncestor(path);
3289
+ if (!isPathInside(canonical, projectPath)) {
3290
+ throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
3291
+ }
3292
+ }
3293
+ requireAbsolutePath(userConfigPath, "userConfigPath");
3294
+ requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
3295
+ return {
3296
+ cwd: canonical,
3297
+ projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
3298
+ userConfigPath,
3299
+ installDecisionsPath,
3300
+ capabilities
3301
+ };
3302
+ }
3303
+ function translateProjectConfigEnv(value, cwd) {
3304
+ if (value === undefined || value.length === 0)
3305
+ return [join2(cwd, ".codex", "lsp-client.json")];
3306
+ return value.split(delimiter2).filter((entry) => entry.length > 0).map((entry) => isAbsolute2(entry) ? entry : join2(cwd, entry));
3307
+ }
3308
+ function translateHomeConfigEnv(value, home, fallback) {
3309
+ if (value === undefined || value.length === 0)
3310
+ return join2(home, fallback);
3311
+ return isAbsolute2(value) ? value : join2(home, value);
3312
+ }
3313
+ function canonicalCwd(cwd) {
3314
+ const resolved = resolve6(cwd);
3315
+ if (!existsSync5(resolved) || !statSync2(resolved).isDirectory()) {
3316
+ throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
3317
+ }
3318
+ return realpathSync3(resolved);
3319
+ }
3320
+ function canonicalizeExistingOrNearestAncestor(path) {
3321
+ let current = resolve6(path);
3322
+ const suffix = [];
3323
+ while (true) {
3324
+ try {
3325
+ const existing = realpathSync3(current);
3326
+ return suffix.length === 0 ? existing : join2(existing, ...suffix);
3327
+ } catch (error) {
3328
+ if (!isMissingPathError(error))
3329
+ throw error;
3330
+ const parent = dirname4(current);
3331
+ if (parent === current)
3332
+ throw error;
3333
+ suffix.unshift(basename2(current));
3334
+ current = parent;
3335
+ }
3336
+ }
3337
+ }
3338
+ function capabilitiesField(value) {
3339
+ if (!isRecord4(value)) {
3340
+ throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
3341
+ }
3342
+ rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
3343
+ const installDecisionTool = value["installDecisionTool"];
3344
+ if (typeof installDecisionTool !== "boolean") {
3345
+ throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
3346
+ }
3347
+ return { installDecisionTool };
3348
+ }
3349
+ function stringField(value, field) {
3350
+ const fieldValue = value[field];
3351
+ if (typeof fieldValue !== "string" || fieldValue.length === 0) {
3352
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
3353
+ }
3354
+ return fieldValue;
3355
+ }
3356
+ function stringArrayField(value, field) {
3357
+ const fieldValue = value[field];
3358
+ if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
3359
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
3360
+ }
3361
+ return fieldValue;
3362
+ }
3363
+ function requireAbsolutePath(path, field) {
3364
+ if (!isAbsolute2(path)) {
3365
+ throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
3366
+ }
3367
+ }
3368
+ function isPathInside(parent, child) {
3369
+ const childPath = resolve6(child);
3370
+ const relativePath = relative4(parent, childPath);
3371
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
3372
+ }
3373
+ function isMissingPathError(error) {
3374
+ const code = errorCode(error);
3375
+ return code === "ENOENT" || code === "ENOTDIR";
3376
+ }
3377
+ function rejectUnknownFields(value, allowed, scope) {
3378
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
3379
+ if (unknown.length > 0) {
3380
+ throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
3381
+ }
3382
+ }
3383
+ function isRecord4(value) {
3384
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3385
+ }
3386
+ function errorCode(error) {
3387
+ if (!error || typeof error !== "object" || !("code" in error))
3388
+ return;
3389
+ const code = Reflect.get(error, "code");
3390
+ return typeof code === "string" ? code : undefined;
3391
+ }
3392
+
3393
+ // ../lsp-core/src/lsp/server-install-state.ts
3394
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
3395
+ import { dirname as dirname5 } from "node:path";
1631
3396
  function getInstallDecisionsPath() {
1632
- const override = contextEnv("LSP_TOOLS_MCP_INSTALL_DECISIONS");
1633
- if (!override)
1634
- return join2(homedir(), ".codex", "lsp-install-decisions.json");
1635
- return isAbsolute(override) ? override : join2(homedir(), override);
3397
+ return lspRequestContext().installDecisionsPath;
1636
3398
  }
1637
3399
  function loadInstallDecisions() {
1638
3400
  const path = getInstallDecisionsPath();
1639
- if (!existsSync2(path))
3401
+ if (!existsSync6(path))
1640
3402
  return {};
1641
3403
  try {
1642
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
3404
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
1643
3405
  return isInstallDecisions(parsed) ? parsed : {};
1644
3406
  } catch {
1645
3407
  return {};
@@ -1658,28 +3420,26 @@ function isInstallDecision(value) {
1658
3420
  }
1659
3421
  function writeInstallDecisions(decisions) {
1660
3422
  const path = getInstallDecisionsPath();
1661
- mkdirSync(dirname(path), { recursive: true });
3423
+ mkdirSync(dirname5(path), { recursive: true });
1662
3424
  const tmpPath = `${path}.tmp`;
1663
- writeFileSync(tmpPath, `${JSON.stringify(decisions, null, 2)}
3425
+ writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
1664
3426
  `, "utf8");
1665
- renameSync(tmpPath, path);
3427
+ renameSync2(tmpPath, path);
1666
3428
  }
1667
3429
  function isInstallDecisions(value) {
1668
- return isRecord2(value) && Object.values(value).every(isInstallDecisionRecord);
3430
+ return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
1669
3431
  }
1670
3432
  function isInstallDecisionRecord(value) {
1671
- if (!isRecord2(value))
3433
+ if (!isRecord5(value))
1672
3434
  return false;
1673
3435
  return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
1674
3436
  }
1675
- function isRecord2(value) {
3437
+ function isRecord5(value) {
1676
3438
  return typeof value === "object" && value !== null && !Array.isArray(value);
1677
3439
  }
1678
3440
 
1679
3441
  // ../lsp-core/src/lsp/config-loader.ts
1680
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1681
- import { homedir as homedir2 } from "node:os";
1682
- import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
3442
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
1683
3443
 
1684
3444
  // ../lsp-core/src/lsp/server-definitions.ts
1685
3445
  var LSP_INSTALL_HINTS = {
@@ -1829,27 +3589,17 @@ var BUILTIN_SERVERS = {
1829
3589
  };
1830
3590
 
1831
3591
  // ../lsp-core/src/lsp/config-loader.ts
1832
- function resolveProjectConfigPath(path) {
1833
- return isAbsolute2(path) ? path : join3(contextCwd(), path);
1834
- }
1835
3592
  function getProjectConfigPaths() {
1836
- const projectOverride = contextEnv("LSP_TOOLS_MCP_PROJECT_CONFIG");
1837
- if (projectOverride) {
1838
- return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
1839
- }
1840
- return [join3(contextCwd(), ".codex", "lsp-client.json")];
3593
+ return lspRequestContext().projectConfigPaths;
1841
3594
  }
1842
3595
  function getUserConfigPath() {
1843
- const userOverride = contextEnv("LSP_TOOLS_MCP_USER_CONFIG");
1844
- if (!userOverride)
1845
- return join3(homedir2(), ".codex", "lsp-client.json");
1846
- return isAbsolute2(userOverride) ? userOverride : join3(homedir2(), userOverride);
3596
+ return lspRequestContext().userConfigPath;
1847
3597
  }
1848
3598
  function loadJsonFile(path) {
1849
- if (!existsSync3(path))
3599
+ if (!existsSync7(path))
1850
3600
  return null;
1851
3601
  try {
1852
- const parsed = JSON.parse(readFileSync3(path, "utf-8"));
3602
+ const parsed = JSON.parse(readFileSync4(path, "utf-8"));
1853
3603
  return isConfigJson(parsed) ? parsed : null;
1854
3604
  } catch {
1855
3605
  return null;
@@ -1988,16 +3738,16 @@ function applyOptionalServerFields(server2, entry) {
1988
3738
  }
1989
3739
  }
1990
3740
  function isConfigJson(value) {
1991
- if (!isRecord3(value))
3741
+ if (!isRecord6(value))
1992
3742
  return false;
1993
3743
  const lsp = value["lsp"];
1994
- return lsp === undefined || isRecord3(lsp);
3744
+ return lsp === undefined || isRecord6(lsp);
1995
3745
  }
1996
3746
  function parseLspEntry(value) {
1997
3747
  return isLspEntry(value) ? value : null;
1998
3748
  }
1999
3749
  function isLspEntry(value) {
2000
- if (!isRecord3(value))
3750
+ if (!isRecord6(value))
2001
3751
  return false;
2002
3752
  const disabled = value["disabled"];
2003
3753
  const command = value["command"];
@@ -2005,15 +3755,15 @@ function isLspEntry(value) {
2005
3755
  const priority = value["priority"];
2006
3756
  const env = value["env"];
2007
3757
  const initialization = value["initialization"];
2008
- return (disabled === undefined || typeof disabled === "boolean") && (command === undefined || isStringArray(command)) && (extensions === undefined || isStringArray(extensions)) && (priority === undefined || typeof priority === "number") && (env === undefined || isStringRecord(env)) && (initialization === undefined || isRecord3(initialization));
3758
+ return (disabled === undefined || typeof disabled === "boolean") && (command === undefined || isStringArray(command)) && (extensions === undefined || isStringArray(extensions)) && (priority === undefined || typeof priority === "number") && (env === undefined || isStringRecord(env)) && (initialization === undefined || isRecord6(initialization));
2009
3759
  }
2010
3760
  function isStringArray(value) {
2011
3761
  return Array.isArray(value) && value.every((item) => typeof item === "string");
2012
3762
  }
2013
3763
  function isStringRecord(value) {
2014
- return isRecord3(value) && Object.values(value).every((item) => typeof item === "string");
3764
+ return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
2015
3765
  }
2016
- function isRecord3(value) {
3766
+ function isRecord6(value) {
2017
3767
  return typeof value === "object" && value !== null && !Array.isArray(value);
2018
3768
  }
2019
3769
  function getDisabledServerIds() {
@@ -2034,8 +3784,8 @@ function getDisabledServerIds() {
2034
3784
  }
2035
3785
 
2036
3786
  // ../lsp-core/src/lsp/server-installation.ts
2037
- import { existsSync as existsSync4 } from "node:fs";
2038
- import { delimiter as delimiter3, join as join4 } from "node:path";
3787
+ import { existsSync as existsSync8 } from "node:fs";
3788
+ import { delimiter as delimiter3, join as join3 } from "node:path";
2039
3789
  function isServerInstalled(command, _workingDirectory) {
2040
3790
  if (command.length === 0)
2041
3791
  return false;
@@ -2043,7 +3793,7 @@ function isServerInstalled(command, _workingDirectory) {
2043
3793
  if (!cmd)
2044
3794
  return false;
2045
3795
  if (cmd.includes("/") || cmd.includes("\\")) {
2046
- if (existsSync4(cmd))
3796
+ if (existsSync8(cmd))
2047
3797
  return true;
2048
3798
  }
2049
3799
  const isWindows = process.platform === "win32";
@@ -2064,7 +3814,7 @@ function isServerInstalled(command, _workingDirectory) {
2064
3814
  const paths = pathEnv.split(delimiter3);
2065
3815
  for (const p of paths) {
2066
3816
  for (const suffix of exts) {
2067
- if (existsSync4(join4(p, cmd + suffix))) {
3817
+ if (existsSync8(join3(p, cmd + suffix))) {
2068
3818
  return true;
2069
3819
  }
2070
3820
  }
@@ -2163,39 +3913,50 @@ function getAllServers() {
2163
3913
  var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
2164
3914
  function isDirectoryPath(filePath) {
2165
3915
  try {
2166
- return statSync2(filePath).isDirectory();
3916
+ return statSync3(filePath).isDirectory();
2167
3917
  } catch {
2168
3918
  return false;
2169
3919
  }
2170
3920
  }
2171
3921
  function findWorkspaceRoot(filePath) {
2172
- const abs = resolve2(contextCwd(), filePath);
3922
+ const abs = resolvePathInsideContext(filePath);
2173
3923
  let dir = abs;
2174
3924
  if (!isDirectoryPath(dir)) {
2175
- dir = dirname2(dir);
3925
+ dir = dirname6(dir);
2176
3926
  }
2177
3927
  let prevDir = "";
2178
3928
  while (dir !== prevDir) {
2179
3929
  for (const marker of WORKSPACE_MARKERS) {
2180
- if (existsSync5(join5(dir, marker))) {
3930
+ if (existsSync9(join4(dir, marker))) {
2181
3931
  return dir;
2182
3932
  }
2183
3933
  }
2184
3934
  prevDir = dir;
2185
- dir = dirname2(dir);
3935
+ dir = dirname6(dir);
3936
+ }
3937
+ return dirname6(abs);
3938
+ }
3939
+ function resolvePathInsideContext(filePath) {
3940
+ const cwd = contextCwd();
3941
+ const abs = resolve7(cwd, filePath);
3942
+ const canonical = canonicalizeExistingOrNearestAncestor(abs);
3943
+ if (!isPathInside(cwd, canonical)) {
3944
+ throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
2186
3945
  }
2187
- return dirname2(abs);
3946
+ return canonical;
2188
3947
  }
2189
3948
  function formatServerLookupError(result) {
2190
3949
  if (result.status === "not_installed") {
2191
3950
  return formatNotInstalled(result);
2192
3951
  }
3952
+ const context = lspRequestContext();
3953
+ const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
2193
3954
  return [
2194
3955
  `No LSP server configured for extension: ${result.extension}`,
2195
3956
  "",
2196
3957
  `Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
2197
3958
  "",
2198
- "Configure a custom server in '.codex/lsp-client.json':",
3959
+ `Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
2199
3960
  " {",
2200
3961
  ' "lsp": {',
2201
3962
  ' "my-server": {',
@@ -2211,6 +3972,7 @@ function formatNotInstalled(result) {
2211
3972
  const { server: server2, installHint } = result;
2212
3973
  const extensions = server2.extensions.join(", ");
2213
3974
  const decision = loadInstallDecision(server2.id)?.decision;
3975
+ const context = lspRequestContext();
2214
3976
  if (decision === "declined") {
2215
3977
  return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
2216
3978
  }
@@ -2226,6 +3988,17 @@ function formatNotInstalled(result) {
2226
3988
  "The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
2227
3989
  ` ${installHint}`
2228
3990
  ].join(`
3991
+ `);
3992
+ }
3993
+ if (!context.capabilities.installDecisionTool) {
3994
+ return [
3995
+ ...header,
3996
+ "To install, run:",
3997
+ ` ${installHint}`,
3998
+ "",
3999
+ "ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
4000
+ "Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
4001
+ ].join(`
2229
4002
  `);
2230
4003
  }
2231
4004
  return [
@@ -2237,6 +4010,7 @@ function formatNotInstalled(result) {
2237
4010
  "- If the user agrees: run the install command above, then retry this tool.",
2238
4011
  "- If the user declines, OR has NOT explicitly asked for LSP installation:",
2239
4012
  ` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
4013
+ ` which writes to ${context.installDecisionsPath},`,
2240
4014
  " then ignore this message and proceed WITHOUT LSP."
2241
4015
  ].join(`
2242
4016
  `);
@@ -2250,14 +4024,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
2250
4024
  "prepareRename"
2251
4025
  ]);
2252
4026
  async function withLspClient(filePath, fn, toolName, options = {}) {
2253
- const absPath = resolve2(contextCwd(), filePath);
4027
+ const absPath = resolvePathInsideContext(filePath);
2254
4028
  if (isDirectoryPath(absPath)) {
2255
4029
  throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
2256
4030
  }
2257
4031
  const ext = effectiveExtension(absPath);
2258
4032
  const result = findServerForExtension(ext);
2259
4033
  if (result.status !== "found") {
2260
- throw new LspServerLookupError(formatServerLookupError(result));
4034
+ throw new LspServerLookupError(formatServerLookupError(result), result);
2261
4035
  }
2262
4036
  const server2 = result.server;
2263
4037
  const root = findWorkspaceRoot(absPath);
@@ -2285,11 +4059,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
2285
4059
  }
2286
4060
 
2287
4061
  // ../lsp-core/src/lsp/directory-diagnostics.ts
2288
- import { existsSync as existsSync6, lstatSync, readdirSync } from "node:fs";
2289
- import { join as join6, resolve as resolve3 } from "node:path";
4062
+ import { existsSync as existsSync10, lstatSync as lstatSync4, readdirSync as readdirSync3 } from "node:fs";
4063
+ import { join as join5, resolve as resolve8 } from "node:path";
2290
4064
 
2291
4065
  // ../lsp-core/src/lsp/formatters.ts
2292
- import { fileURLToPath } from "node:url";
4066
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2293
4067
  var DIAGNOSTIC_SEVERITY_FILTERS = {
2294
4068
  error: 1,
2295
4069
  warning: 2,
@@ -2297,7 +4071,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
2297
4071
  hint: 4
2298
4072
  };
2299
4073
  function uriToPath(uri) {
2300
- return fileURLToPath(uri);
4074
+ return fileURLToPath2(uri);
2301
4075
  }
2302
4076
  function formatLocation(loc) {
2303
4077
  if ("targetUri" in loc) {
@@ -2383,6 +4157,9 @@ function formatApplyResult(result) {
2383
4157
  for (const file of result.filesModified) {
2384
4158
  lines.push(` - ${file}`);
2385
4159
  }
4160
+ if (result.lateAbort) {
4161
+ lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
4162
+ }
2386
4163
  } else {
2387
4164
  lines.push("Failed to apply some changes:");
2388
4165
  for (const err of result.errors) {
@@ -2398,6 +4175,7 @@ function formatApplyResult(result) {
2398
4175
 
2399
4176
  // ../lsp-core/src/lsp/directory-diagnostics.ts
2400
4177
  var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
4178
+ var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
2401
4179
  function collectFilesWithExtension(dir, extension, maxFiles) {
2402
4180
  const files = [];
2403
4181
  function walk(currentDir) {
@@ -2405,17 +4183,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2405
4183
  return;
2406
4184
  let entries = [];
2407
4185
  try {
2408
- entries = readdirSync(currentDir);
4186
+ entries = readdirSync3(currentDir);
2409
4187
  } catch {
2410
4188
  return;
2411
4189
  }
2412
4190
  for (const entry of entries) {
2413
4191
  if (files.length >= maxFiles)
2414
4192
  return;
2415
- const fullPath = join6(currentDir, entry);
4193
+ const fullPath = join5(currentDir, entry);
2416
4194
  let stat;
2417
4195
  try {
2418
- stat = lstatSync(fullPath);
4196
+ stat = lstatSync4(fullPath);
2419
4197
  } catch {
2420
4198
  continue;
2421
4199
  }
@@ -2433,52 +4211,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2433
4211
  walk(dir);
2434
4212
  return files;
2435
4213
  }
2436
- async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
4214
+ async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
2437
4215
  if (!extension.startsWith(".")) {
2438
4216
  throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
2439
4217
  }
2440
- const absDir = resolve3(contextCwd(), directory);
2441
- if (!existsSync6(absDir)) {
4218
+ const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
4219
+ if (!existsSync10(absDir)) {
2442
4220
  throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
2443
4221
  }
2444
- const serverResult = findServerForExtension(extension);
4222
+ const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
2445
4223
  if (serverResult.status !== "found") {
2446
4224
  throw new LspServerLookupError(formatServerLookupError(serverResult));
2447
4225
  }
2448
4226
  const server2 = serverResult.server;
2449
- const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
4227
+ const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
2450
4228
  const wasCapped = allFiles.length > maxFiles;
2451
4229
  const filesToProcess = allFiles.slice(0, maxFiles);
2452
4230
  if (filesToProcess.length === 0) {
2453
- return [
4231
+ const output = [
2454
4232
  `Directory: ${absDir}`,
2455
4233
  `Extension: ${extension}`,
2456
4234
  "Files scanned: 0",
2457
4235
  `No files found with extension "${extension}".`
2458
4236
  ].join(`
2459
4237
  `);
4238
+ return { output, totalDiagnostics: 0, fileFailures: [] };
2460
4239
  }
2461
- const root = findWorkspaceRoot(absDir);
2462
- const manager2 = getLspManager();
4240
+ const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
4241
+ const manager2 = options.manager ?? getLspManager();
2463
4242
  const allDiagnostics = [];
2464
4243
  const fileErrors = [];
4244
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
4245
+ options.signal?.throwIfAborted();
2465
4246
  const client = await manager2.getClient(root, server2);
2466
4247
  try {
2467
- for (const file of filesToProcess) {
2468
- try {
2469
- const result = await client.diagnostics(file);
2470
- const filtered = filterDiagnosticsBySeverity(result.items, severity);
2471
- allDiagnostics.push(...filtered.map((diagnostic) => ({
2472
- filePath: file,
2473
- diagnostic
2474
- })));
2475
- } catch (e) {
2476
- fileErrors.push({
2477
- file,
2478
- error: e instanceof Error ? e.message : String(e)
2479
- });
4248
+ let nextIndex = 0;
4249
+ const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
4250
+ for (;; ) {
4251
+ if (options.signal?.aborted)
4252
+ return;
4253
+ const file = filesToProcess[nextIndex];
4254
+ nextIndex += 1;
4255
+ if (file === undefined)
4256
+ return;
4257
+ try {
4258
+ const result = await client.diagnostics(file, options.signal);
4259
+ const filtered = filterDiagnosticsBySeverity(result.items, severity);
4260
+ allDiagnostics.push(...filtered.map((diagnostic) => ({
4261
+ filePath: file,
4262
+ diagnostic
4263
+ })));
4264
+ } catch (e) {
4265
+ fileErrors.push({
4266
+ file,
4267
+ error: e instanceof Error ? e.message : String(e)
4268
+ });
4269
+ }
2480
4270
  }
2481
- }
4271
+ });
4272
+ await Promise.all(workers);
2482
4273
  } finally {
2483
4274
  manager2.releaseClient(root, server2.id);
2484
4275
  }
@@ -2506,13 +4297,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
2506
4297
  lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
2507
4298
  }
2508
4299
  }
2509
- return lines.join(`
2510
- `);
4300
+ return { output: lines.join(`
4301
+ `), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
2511
4302
  }
2512
4303
 
2513
4304
  // ../lsp-core/src/lsp/infer-extension.ts
2514
- import { lstatSync as lstatSync2, readdirSync as readdirSync2 } from "node:fs";
2515
- import { join as join7 } from "node:path";
4305
+ import { lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
4306
+ import { join as join6 } from "node:path";
2516
4307
  var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
2517
4308
  var MAX_SCAN_ENTRIES = 500;
2518
4309
  function inferExtensionFromDirectory(directory) {
@@ -2523,17 +4314,17 @@ function inferExtensionFromDirectory(directory) {
2523
4314
  return;
2524
4315
  let entries;
2525
4316
  try {
2526
- entries = readdirSync2(dir);
4317
+ entries = readdirSync4(dir);
2527
4318
  } catch {
2528
4319
  return;
2529
4320
  }
2530
4321
  for (const entry of entries) {
2531
4322
  if (scanned >= MAX_SCAN_ENTRIES)
2532
4323
  return;
2533
- const fullPath = join7(dir, entry);
4324
+ const fullPath = join6(dir, entry);
2534
4325
  let stat;
2535
4326
  try {
2536
- stat = lstatSync2(fullPath);
4327
+ stat = lstatSync5(fullPath);
2537
4328
  } catch {
2538
4329
  continue;
2539
4330
  }
@@ -2607,13 +4398,48 @@ function missingDependencyResult(error, details) {
2607
4398
  details: {
2608
4399
  ...details,
2609
4400
  error: message,
2610
- errorKind: "missing_dependency"
4401
+ errorKind: "missing_dependency",
4402
+ ...availabilityDetails(error)
2611
4403
  }
2612
4404
  };
2613
4405
  }
4406
+ function availabilityDetails(error) {
4407
+ const availability = missingDependencyAvailability(error);
4408
+ return availability === null ? {} : { availability };
4409
+ }
4410
+ function missingDependencyAvailability(error) {
4411
+ if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
4412
+ return null;
4413
+ const context = lspRequestContext();
4414
+ switch (error.lookup.status) {
4415
+ case "not_configured":
4416
+ return {
4417
+ kind: "not_configured",
4418
+ extension: error.lookup.extension,
4419
+ availableServers: [...error.lookup.availableServers],
4420
+ projectConfigPaths: [...context.projectConfigPaths],
4421
+ userConfigPath: context.userConfigPath,
4422
+ installDecisionTool: context.capabilities.installDecisionTool
4423
+ };
4424
+ case "not_installed":
4425
+ return {
4426
+ kind: "not_installed",
4427
+ serverId: error.lookup.server.id,
4428
+ command: [...error.lookup.server.command],
4429
+ extensions: [...error.lookup.server.extensions],
4430
+ installHint: error.lookup.installHint,
4431
+ installDecisionTool: context.capabilities.installDecisionTool,
4432
+ installDecisionsPath: context.installDecisionsPath
4433
+ };
4434
+ default: {
4435
+ const exhaustive = error.lookup;
4436
+ return exhaustive;
4437
+ }
4438
+ }
4439
+ }
2614
4440
 
2615
4441
  // ../lsp-core/src/tools/parameters.ts
2616
- function isRecord4(value) {
4442
+ function isRecord7(value) {
2617
4443
  return typeof value === "object" && value !== null && !Array.isArray(value);
2618
4444
  }
2619
4445
  function requireString(params, key) {
@@ -2670,7 +4496,7 @@ async function executeLspDiagnostics(params, signal) {
2670
4496
  const filePath = requireString(params, "filePath");
2671
4497
  const severity = severityFilter(params);
2672
4498
  try {
2673
- const absPath = resolve4(contextCwd(), filePath);
4499
+ const absPath = resolvePathInsideContext(filePath);
2674
4500
  if (isDirectoryPath(absPath)) {
2675
4501
  const extension = inferExtensionFromDirectory(absPath);
2676
4502
  if (!extension) {
@@ -2687,18 +4513,33 @@ async function executeLspDiagnostics(params, signal) {
2687
4513
  };
2688
4514
  return text(message, details3);
2689
4515
  }
2690
- const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
4516
+ const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
2691
4517
  const details2 = {
2692
4518
  filePath,
2693
4519
  severity,
2694
4520
  mode: "directory",
2695
4521
  diagnostics: [],
4522
+ totalDiagnostics: output2.totalDiagnostics,
4523
+ truncated: false,
4524
+ fileFailures: [...output2.fileFailures]
4525
+ };
4526
+ return text(output2.output, details2);
4527
+ }
4528
+ const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
4529
+ if (result.transientError) {
4530
+ const message = result.transientError.message;
4531
+ const details2 = {
4532
+ filePath,
4533
+ severity,
4534
+ mode: "file",
4535
+ diagnostics: [],
2696
4536
  totalDiagnostics: 0,
2697
- truncated: false
4537
+ truncated: false,
4538
+ error: message,
4539
+ errorKind: result.transientError.kind
2698
4540
  };
2699
- return text(output2, details2);
4541
+ return text(message, details2, true);
2700
4542
  }
2701
- const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
2702
4543
  const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
2703
4544
  const total = diagnostics.length;
2704
4545
  const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
@@ -2759,7 +4600,7 @@ async function executeLspGotoDefinition(params, signal) {
2759
4600
  const line = requireNumber(params, "line");
2760
4601
  const character = requireNumber(params, "character");
2761
4602
  try {
2762
- const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
4603
+ const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
2763
4604
  const locations = !result ? [] : Array.isArray(result) ? result : [result];
2764
4605
  const details = { filePath, line, character, locations };
2765
4606
  if (locations.length === 0)
@@ -2784,7 +4625,7 @@ async function executeLspFindReferences(params, signal) {
2784
4625
  const character = requireNumber(params, "character");
2785
4626
  const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
2786
4627
  try {
2787
- const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
4628
+ const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
2788
4629
  const references = Array.isArray(result) ? result : [];
2789
4630
  const total = references.length;
2790
4631
  const truncated = total > DEFAULT_MAX_REFERENCES;
@@ -2820,181 +4661,13 @@ async function executeLspFindReferences(params, signal) {
2820
4661
  }
2821
4662
  }
2822
4663
 
2823
- // ../lsp-core/src/lsp/workspace-edit.ts
2824
- import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2825
- import { dirname as dirname3, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
2826
- import { fileURLToPath as fileURLToPath2 } from "node:url";
2827
- function errorMessage2(error) {
2828
- return error instanceof Error ? error.message : String(error);
2829
- }
2830
- function isPathInsideWorkspace(filePath, workspaceRoot) {
2831
- const relativePath = relative(workspaceRoot, filePath);
2832
- return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
2833
- }
2834
- function realpathForValidation(filePath) {
2835
- if (existsSync7(filePath))
2836
- return realpathSync(filePath);
2837
- const parent = dirname3(filePath);
2838
- return resolve5(realpathSync(parent), relative(parent, filePath));
2839
- }
2840
- function uriToWorkspacePath(uri, workspaceRoot) {
2841
- let filePath;
2842
- try {
2843
- filePath = fileURLToPath2(uri);
2844
- } catch (error) {
2845
- return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
2846
- }
2847
- let validatedPath;
2848
- try {
2849
- validatedPath = realpathForValidation(filePath);
2850
- } catch (error) {
2851
- return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
2852
- }
2853
- if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
2854
- return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
2855
- }
2856
- return { success: true, path: filePath };
2857
- }
2858
- function applyTextEditsToFile(filePath, edits) {
2859
- try {
2860
- const content = readFileSync4(filePath, "utf-8");
2861
- const lines = content.split(`
2862
- `);
2863
- const sortedEdits = [...edits].sort((a, b) => {
2864
- if (b.range.start.line !== a.range.start.line) {
2865
- return b.range.start.line - a.range.start.line;
2866
- }
2867
- return b.range.start.character - a.range.start.character;
2868
- });
2869
- for (const edit of sortedEdits) {
2870
- const startLine = edit.range.start.line;
2871
- const startChar = edit.range.start.character;
2872
- const endLine = edit.range.end.line;
2873
- const endChar = edit.range.end.character;
2874
- if (startLine === endLine) {
2875
- const line = lines[startLine] ?? "";
2876
- lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
2877
- } else {
2878
- const firstLine = lines[startLine] ?? "";
2879
- const lastLine = lines[endLine] ?? "";
2880
- const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
2881
- lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
2882
- `));
2883
- }
2884
- }
2885
- writeFileSync2(filePath, lines.join(`
2886
- `), "utf-8");
2887
- return { success: true, editCount: edits.length };
2888
- } catch (err) {
2889
- return {
2890
- success: false,
2891
- editCount: 0,
2892
- error: err instanceof Error ? err.message : String(err)
2893
- };
2894
- }
2895
- }
2896
- function applyWorkspaceEdit(edit, options = {}) {
2897
- if (!edit) {
2898
- return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
2899
- }
2900
- const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
2901
- const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
2902
- if (edit.changes) {
2903
- for (const [uri, edits] of Object.entries(edit.changes)) {
2904
- const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
2905
- if (!validatedPath.success) {
2906
- result.success = false;
2907
- result.errors.push(validatedPath.error);
2908
- continue;
2909
- }
2910
- const applyResult = applyTextEditsToFile(validatedPath.path, edits);
2911
- if (applyResult.success) {
2912
- result.filesModified.push(validatedPath.path);
2913
- result.totalEdits += applyResult.editCount;
2914
- } else {
2915
- result.success = false;
2916
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
2917
- }
2918
- }
2919
- }
2920
- if (edit.documentChanges) {
2921
- for (const change of edit.documentChanges) {
2922
- if (!("kind" in change)) {
2923
- const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
2924
- if (!validatedPath.success) {
2925
- result.success = false;
2926
- result.errors.push(validatedPath.error);
2927
- continue;
2928
- }
2929
- const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
2930
- if (applyResult.success) {
2931
- result.filesModified.push(validatedPath.path);
2932
- result.totalEdits += applyResult.editCount;
2933
- } else {
2934
- result.success = false;
2935
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
2936
- }
2937
- continue;
2938
- }
2939
- if (change.kind === "create") {
2940
- try {
2941
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
2942
- if (!validatedPath.success) {
2943
- result.success = false;
2944
- result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
2945
- continue;
2946
- }
2947
- writeFileSync2(validatedPath.path, "", "utf-8");
2948
- result.filesModified.push(validatedPath.path);
2949
- } catch (err) {
2950
- result.success = false;
2951
- result.errors.push(`Create ${change.uri}: ${String(err)}`);
2952
- }
2953
- } else if (change.kind === "rename") {
2954
- try {
2955
- const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
2956
- const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
2957
- if (!oldPath.success || !newPath.success) {
2958
- const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
2959
- result.success = false;
2960
- result.errors.push(`Rename ${change.oldUri}: ${error}`);
2961
- continue;
2962
- }
2963
- const content = readFileSync4(oldPath.path, "utf-8");
2964
- writeFileSync2(newPath.path, content, "utf-8");
2965
- unlinkSync(oldPath.path);
2966
- result.filesModified.push(newPath.path);
2967
- } catch (err) {
2968
- result.success = false;
2969
- result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
2970
- }
2971
- } else if (change.kind === "delete") {
2972
- try {
2973
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
2974
- if (!validatedPath.success) {
2975
- result.success = false;
2976
- result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
2977
- continue;
2978
- }
2979
- unlinkSync(validatedPath.path);
2980
- result.filesModified.push(validatedPath.path);
2981
- } catch (err) {
2982
- result.success = false;
2983
- result.errors.push(`Delete ${change.uri}: ${String(err)}`);
2984
- }
2985
- }
2986
- }
2987
- }
2988
- return result;
2989
- }
2990
-
2991
4664
  // ../lsp-core/src/tools/rename.ts
2992
4665
  async function executeLspPrepareRename(params, signal) {
2993
4666
  const filePath = requireString(params, "filePath");
2994
4667
  const line = requireNumber(params, "line");
2995
4668
  const character = requireNumber(params, "character");
2996
4669
  try {
2997
- const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
4670
+ const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
2998
4671
  const details = { filePath, line, character, result };
2999
4672
  return text(formatPrepareRenameResult(result), details);
3000
4673
  } catch (error) {
@@ -3015,13 +4688,9 @@ async function executeLspRename(params, signal) {
3015
4688
  const character = requireNumber(params, "character");
3016
4689
  const newName = requireString(params, "newName");
3017
4690
  try {
3018
- const edit = await withLspClient(filePath, async (client, workspaceRoot) => ({
3019
- edit: await client.rename(filePath, line, character, newName),
3020
- workspaceRoot
3021
- }), "rename", clientOptions(signal));
3022
- const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
3023
- const details = { filePath, line, character, newName, apply, edit: edit.edit };
3024
- return text(formatApplyResult(apply), details, !apply.success);
4691
+ const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
4692
+ const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
4693
+ return text(formatApplyResult(result.apply), details, !result.apply.success);
3025
4694
  } catch (error) {
3026
4695
  const missingDependency = missingDependencyResult(error, {
3027
4696
  filePath,
@@ -3096,10 +4765,10 @@ async function executeLspSymbols(params, signal) {
3096
4765
  errorKind: "missing_query"
3097
4766
  });
3098
4767
  }
3099
- const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
4768
+ const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
3100
4769
  return formatSymbolsResult(filePath, scope, symbols2, limit, query);
3101
4770
  }
3102
- const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
4771
+ const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
3103
4772
  return formatSymbolsResult(filePath, scope, symbols, limit);
3104
4773
  } catch (error) {
3105
4774
  const query = optionalString(params, "query");
@@ -3265,12 +4934,12 @@ function matchesToolName(tool, name) {
3265
4934
  return tool.name === name || (tool.aliases?.includes(name) ?? false);
3266
4935
  }
3267
4936
  function coerceToolArguments(value) {
3268
- return isRecord4(value) ? value : {};
4937
+ return isRecord7(value) ? value : {};
3269
4938
  }
3270
4939
  // ../lsp-core/src/mcp.ts
3271
4940
  var SERVER_NAME = "lsp";
3272
4941
  var SERVER_VERSION = "0.1.0";
3273
- async function handleLspMcpRequest(input) {
4942
+ async function handleLspMcpRequest(input, options = {}) {
3274
4943
  if (!isPlainRecord(input)) {
3275
4944
  return errorResponse(null, -32600, "Invalid Request");
3276
4945
  }
@@ -3292,33 +4961,37 @@ async function handleLspMcpRequest(input) {
3292
4961
  return successResponse(id, { tools: LSP_MCP_TOOLS.map(describeTool) });
3293
4962
  }
3294
4963
  if (method === "tools/call") {
3295
- return handleToolCall(id, input["params"]);
4964
+ return handleToolCall(id, input["params"], options.signal);
3296
4965
  }
3297
4966
  return errorResponse(id, -32601, `Method not found: ${String(method)}`);
3298
4967
  }
3299
4968
  async function runMcpStdioServer(input = process.stdin, output = process.stdout) {
4969
+ const requestContext = createStandaloneMcpRequestContext();
3300
4970
  await runJsonRpcStdioServer({
3301
4971
  input,
3302
4972
  output,
3303
4973
  idleTimeoutMs: 0,
3304
- handler: handleLspMcpRequest,
4974
+ handler: (request) => runWithRequestContext(requestContext, () => handleLspMcpRequest(request)),
3305
4975
  handlerOptions: undefined
3306
4976
  });
3307
4977
  }
3308
- async function handleToolCall(id, params) {
4978
+ async function handleToolCall(id, params, signal) {
3309
4979
  if (!isPlainRecord(params) || typeof params["name"] !== "string") {
3310
4980
  return errorResponse(id, -32602, "tools/call requires params.name");
3311
4981
  }
3312
4982
  try {
3313
- const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
4983
+ const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
3314
4984
  return successResponse(id, {
3315
4985
  content: result.content,
3316
4986
  isError: result.isError ?? false,
3317
4987
  details: result.details
3318
4988
  });
3319
4989
  } catch (error) {
4990
+ if (!(error instanceof Error)) {
4991
+ throw error;
4992
+ }
3320
4993
  return successResponse(id, {
3321
- content: [{ type: "text", text: messageFromError(error) }],
4994
+ content: [{ type: "text", text: error.message }],
3322
4995
  isError: true
3323
4996
  });
3324
4997
  }