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
@@ -12,9 +12,6 @@ function errorResponse(id, code, message, data) {
12
12
  function jsonRpcId(value) {
13
13
  return typeof value === "string" || typeof value === "number" || value === null ? value : null;
14
14
  }
15
- function messageFromError(error) {
16
- return error instanceof Error ? error.message : String(error);
17
- }
18
15
  // ../mcp-stdio-core/src/transport.ts
19
16
  var HEADER_SEPARATOR = Buffer.from(`\r
20
17
  \r
@@ -201,37 +198,197 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
201
198
  closed: () => isClosed
202
199
  };
203
200
  }
204
- // ../lsp-core/src/tools/diagnostics.ts
205
- import { resolve as resolve4 } from "node:path";
206
-
207
201
  // ../lsp-core/src/lsp/client-wrapper.ts
208
- import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
209
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "node:path";
202
+ import { existsSync as existsSync9, statSync as statSync3 } from "node:fs";
203
+ import { dirname as dirname6, join as join4, resolve as resolve7 } from "node:path";
210
204
 
211
205
  // ../lsp-core/src/request-context.ts
212
206
  import { AsyncLocalStorage } from "node:async_hooks";
207
+ import { existsSync, realpathSync, statSync } from "node:fs";
208
+ import { homedir } from "node:os";
209
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
210
+
211
+ class LspRequestContextParseError extends Error {
212
+ code;
213
+ name = "LspRequestContextParseError";
214
+ constructor(code, message) {
215
+ super(message);
216
+ this.code = code;
217
+ }
218
+ }
219
+
220
+ class LspRequestContextUnavailableError extends Error {
221
+ name = "LspRequestContextUnavailableError";
222
+ constructor() {
223
+ super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
224
+ }
225
+ }
213
226
  var storage = new AsyncLocalStorage;
227
+ var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
228
+ var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
214
229
  function runWithRequestContext(context, fn) {
215
230
  return storage.run(context, fn);
216
231
  }
232
+ function lspRequestContext() {
233
+ const context = storage.getStore();
234
+ if (!context)
235
+ throw new LspRequestContextUnavailableError;
236
+ return context;
237
+ }
217
238
  function contextCwd() {
218
- return storage.getStore()?.cwd ?? process.cwd();
239
+ return lspRequestContext().cwd;
219
240
  }
220
241
  function contextEnv(key) {
221
- const store = storage.getStore();
222
- if (store?.env)
223
- return store.env[key];
224
- return process.env[key];
242
+ const context = lspRequestContext();
243
+ if (key === "LSP_TOOLS_MCP_PROJECT_CONFIG")
244
+ return context.projectConfigPaths.join(delimiter);
245
+ if (key === "LSP_TOOLS_MCP_USER_CONFIG")
246
+ return context.userConfigPath;
247
+ if (key === "LSP_TOOLS_MCP_INSTALL_DECISIONS")
248
+ return context.installDecisionsPath;
249
+ return;
250
+ }
251
+ function createStandaloneMcpRequestContext(input = {}) {
252
+ const env = input.env ?? process.env;
253
+ const cwd = canonicalCwd(input.cwd ?? process.cwd());
254
+ const home = input.homeDir ?? homedir();
255
+ const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
256
+ const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
257
+ const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
258
+ return parseLspRequestContext({
259
+ cwd,
260
+ projectConfigPaths,
261
+ userConfigPath,
262
+ installDecisionsPath,
263
+ capabilities: { installDecisionTool: true }
264
+ });
265
+ }
266
+ function parseLspRequestContext(value) {
267
+ if (!isRecord(value)) {
268
+ throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
269
+ }
270
+ rejectUnknownFields(value, CONTEXT_FIELDS, "context");
271
+ const cwd = stringField(value, "cwd");
272
+ const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
273
+ const userConfigPath = stringField(value, "userConfigPath");
274
+ const installDecisionsPath = stringField(value, "installDecisionsPath");
275
+ const capabilities = capabilitiesField(value["capabilities"]);
276
+ const canonical = canonicalCwd(cwd);
277
+ for (const path of projectConfigPaths) {
278
+ requireAbsolutePath(path, "projectConfigPaths");
279
+ const projectPath = canonicalizeExistingOrNearestAncestor(path);
280
+ if (!isPathInside(canonical, projectPath)) {
281
+ throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
282
+ }
283
+ }
284
+ requireAbsolutePath(userConfigPath, "userConfigPath");
285
+ requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
286
+ return {
287
+ cwd: canonical,
288
+ projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
289
+ userConfigPath,
290
+ installDecisionsPath,
291
+ capabilities
292
+ };
293
+ }
294
+ function translateProjectConfigEnv(value, cwd) {
295
+ if (value === undefined || value.length === 0)
296
+ return [join(cwd, ".codex", "lsp-client.json")];
297
+ return value.split(delimiter).filter((entry) => entry.length > 0).map((entry) => isAbsolute(entry) ? entry : join(cwd, entry));
298
+ }
299
+ function translateHomeConfigEnv(value, home, fallback) {
300
+ if (value === undefined || value.length === 0)
301
+ return join(home, fallback);
302
+ return isAbsolute(value) ? value : join(home, value);
303
+ }
304
+ function canonicalCwd(cwd) {
305
+ const resolved = resolve(cwd);
306
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
307
+ throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
308
+ }
309
+ return realpathSync(resolved);
310
+ }
311
+ function canonicalizeExistingOrNearestAncestor(path) {
312
+ let current = resolve(path);
313
+ const suffix = [];
314
+ while (true) {
315
+ try {
316
+ const existing = realpathSync(current);
317
+ return suffix.length === 0 ? existing : join(existing, ...suffix);
318
+ } catch (error) {
319
+ if (!isMissingPathError(error))
320
+ throw error;
321
+ const parent = dirname(current);
322
+ if (parent === current)
323
+ throw error;
324
+ suffix.unshift(basename(current));
325
+ current = parent;
326
+ }
327
+ }
328
+ }
329
+ function capabilitiesField(value) {
330
+ if (!isRecord(value)) {
331
+ throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
332
+ }
333
+ rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
334
+ const installDecisionTool = value["installDecisionTool"];
335
+ if (typeof installDecisionTool !== "boolean") {
336
+ throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
337
+ }
338
+ return { installDecisionTool };
339
+ }
340
+ function stringField(value, field) {
341
+ const fieldValue = value[field];
342
+ if (typeof fieldValue !== "string" || fieldValue.length === 0) {
343
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
344
+ }
345
+ return fieldValue;
346
+ }
347
+ function stringArrayField(value, field) {
348
+ const fieldValue = value[field];
349
+ if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
350
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
351
+ }
352
+ return fieldValue;
353
+ }
354
+ function requireAbsolutePath(path, field) {
355
+ if (!isAbsolute(path)) {
356
+ throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
357
+ }
358
+ }
359
+ function isPathInside(parent, child) {
360
+ const childPath = resolve(child);
361
+ const relativePath = relative(parent, childPath);
362
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
363
+ }
364
+ function isMissingPathError(error) {
365
+ const code = errorCode(error);
366
+ return code === "ENOENT" || code === "ENOTDIR";
367
+ }
368
+ function rejectUnknownFields(value, allowed, scope) {
369
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
370
+ if (unknown.length > 0) {
371
+ throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
372
+ }
373
+ }
374
+ function isRecord(value) {
375
+ return typeof value === "object" && value !== null && !Array.isArray(value);
376
+ }
377
+ function errorCode(error) {
378
+ if (!error || typeof error !== "object" || !("code" in error))
379
+ return;
380
+ const code = Reflect.get(error, "code");
381
+ return typeof code === "string" ? code : undefined;
225
382
  }
226
383
 
227
384
  // ../lsp-core/src/lsp/effective-extension.ts
228
- import { basename, extname } from "node:path";
385
+ import { basename as basename2, extname } from "node:path";
229
386
  var BASENAME_EXTENSIONS = {
230
387
  Dockerfile: ".dockerfile",
231
388
  Containerfile: ".dockerfile"
232
389
  };
233
390
  function effectiveExtension(filePath) {
234
- return BASENAME_EXTENSIONS[basename(filePath)] ?? extname(filePath);
391
+ return BASENAME_EXTENSIONS[basename2(filePath)] ?? extname(filePath);
235
392
  }
236
393
 
237
394
  // ../lsp-core/src/lsp/errors.ts
@@ -281,7 +438,12 @@ class LspInvalidPathError extends Error {
281
438
  }
282
439
 
283
440
  class LspServerLookupError extends Error {
441
+ lookup;
284
442
  name = "LspServerLookupError";
443
+ constructor(message, lookup) {
444
+ super(message);
445
+ this.lookup = lookup;
446
+ }
285
447
  }
286
448
 
287
449
  class LspServerInitializingError extends Error {
@@ -301,17 +463,18 @@ function isLspDeadConnectionError(err) {
301
463
  }
302
464
 
303
465
  // ../lsp-core/src/lsp/cleanup-errors.ts
304
- function reportBestEffortCleanupError(operation, error) {
305
- if (process.env["CODEX_LSP_DEBUG_CLEANUP"] !== "1")
306
- return;
466
+ function writeCleanupError(message) {
467
+ process.stderr.write(`${message}
468
+ `);
469
+ }
470
+ function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
307
471
  const message = error instanceof Error ? error.message : String(error);
308
- console.error(`[codex-lsp] ignored ${operation} failure during cleanup: ${message}`);
472
+ logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
309
473
  }
310
474
 
311
475
  // ../lsp-core/src/lsp/client.ts
312
- import { readFileSync } from "node:fs";
313
- import { resolve } from "node:path";
314
- import { pathToFileURL as pathToFileURL2 } from "node:url";
476
+ import { resolve as resolve6 } from "node:path";
477
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
315
478
 
316
479
  // ../lsp-core/src/lsp/connection.ts
317
480
  import { pathToFileURL } from "node:url";
@@ -375,28 +538,80 @@ class JsonRpcConnection {
375
538
  onError(handler) {
376
539
  this.errorHandlers.push(handler);
377
540
  }
378
- async sendRequest(method, params) {
541
+ async sendRequest(method, params, options = {}) {
379
542
  if (this.disposed)
380
543
  throw new Error("JSON-RPC connection is disposed");
381
544
  const id = this.nextRequestId;
382
545
  this.nextRequestId += 1;
546
+ const key = String(id);
383
547
  const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
384
- const responsePromise = new Promise((resolve, reject) => {
385
- this.pendingRequests.set(String(id), {
548
+ let requestWritten = false;
549
+ let cancelAfterWrite = false;
550
+ let settled = false;
551
+ const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
552
+ const responsePromise = new Promise((resolve2, reject) => {
553
+ const cleanup = () => {
554
+ options.signal?.removeEventListener("abort", onAbort);
555
+ };
556
+ const settleCancel = () => {
557
+ if (settled)
558
+ return;
559
+ settled = true;
560
+ this.pendingRequests.delete(key);
561
+ cleanup();
562
+ const rejectCancelled = () => reject(abortError(options.signal));
563
+ if (!requestWritten) {
564
+ cancelAfterWrite = true;
565
+ rejectCancelled();
566
+ return;
567
+ }
568
+ writeCancel().then(rejectCancelled, (error) => {
569
+ this.emitError(toError(error));
570
+ rejectCancelled();
571
+ });
572
+ };
573
+ const onAbort = () => settleCancel();
574
+ this.pendingRequests.set(key, {
386
575
  resolve(result) {
387
- resolve(result);
576
+ settled = true;
577
+ cleanup();
578
+ resolve2(result);
579
+ },
580
+ reject(error) {
581
+ settled = true;
582
+ cleanup();
583
+ reject(error);
388
584
  },
389
- reject
585
+ cleanup
390
586
  });
587
+ if (options.signal?.aborted) {
588
+ settleCancel();
589
+ return;
590
+ }
591
+ options.signal?.addEventListener("abort", onAbort, { once: true });
391
592
  });
593
+ if (settled)
594
+ return responsePromise;
392
595
  try {
393
596
  await this.writeMessage(message);
597
+ requestWritten = true;
598
+ if (cancelAfterWrite)
599
+ await writeCancel();
394
600
  } catch (error) {
395
- this.pendingRequests.delete(String(id));
601
+ if (settled)
602
+ return responsePromise;
603
+ const pending = this.pendingRequests.get(key);
604
+ if (pending) {
605
+ pending.cleanup();
606
+ this.pendingRequests.delete(key);
607
+ }
396
608
  throw error;
397
609
  }
398
610
  return responsePromise;
399
611
  }
612
+ pendingRequestCount() {
613
+ return this.pendingRequests.size;
614
+ }
400
615
  async sendNotification(method, params) {
401
616
  if (this.disposed)
402
617
  return;
@@ -413,6 +628,7 @@ class JsonRpcConnection {
413
628
  this.reader.off("error", this.handleStreamError);
414
629
  this.writer.off("error", this.handleStreamError);
415
630
  for (const pending of this.pendingRequests.values()) {
631
+ pending.cleanup();
416
632
  pending.reject(new Error("JSON-RPC connection disposed"));
417
633
  }
418
634
  this.pendingRequests.clear();
@@ -488,6 +704,7 @@ class JsonRpcConnection {
488
704
  if (!pending)
489
705
  return;
490
706
  this.pendingRequests.delete(String(id));
707
+ pending.cleanup();
491
708
  if ("error" in message) {
492
709
  pending.reject(jsonRpcErrorToError(message["error"]));
493
710
  return;
@@ -501,7 +718,11 @@ class JsonRpcConnection {
501
718
  try {
502
719
  handler(params);
503
720
  } catch (error) {
504
- this.emitError(toError(error));
721
+ if (error instanceof Error) {
722
+ this.emitError(error);
723
+ return;
724
+ }
725
+ this.emitError(new Error(String(error)));
505
726
  }
506
727
  }
507
728
  handleRequest(message) {
@@ -526,13 +747,13 @@ class JsonRpcConnection {
526
747
  const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
527
748
  \r
528
749
  ${body}`;
529
- return new Promise((resolve, reject) => {
750
+ return new Promise((resolve2, reject) => {
530
751
  this.writer.write(payload, (error) => {
531
752
  if (error) {
532
753
  reject(error);
533
754
  return;
534
755
  }
535
- resolve();
756
+ resolve2();
536
757
  });
537
758
  });
538
759
  }
@@ -542,6 +763,14 @@ ${body}`;
542
763
  }
543
764
  }
544
765
  }
766
+ function abortError(signal) {
767
+ const reason = signal?.reason;
768
+ if (reason instanceof Error)
769
+ return reason;
770
+ const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
771
+ error.name = "AbortError";
772
+ return error;
773
+ }
545
774
  function parseContentLength2(headers) {
546
775
  for (const line of headers.split(`\r
547
776
  `)) {
@@ -581,8 +810,8 @@ function toError(error) {
581
810
 
582
811
  // ../lsp-core/src/lsp/process.ts
583
812
  import { spawn, spawnSync } from "node:child_process";
584
- import { existsSync, statSync } from "node:fs";
585
- import { delimiter, join } from "node:path";
813
+ import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
814
+ import { delimiter as delimiter2, join as join2 } from "node:path";
586
815
  function isMissingProcessError(error) {
587
816
  if (!(error instanceof Error) || !("code" in error))
588
817
  return false;
@@ -595,10 +824,10 @@ function reportKillError(context, error) {
595
824
  }
596
825
  function validateCwd(cwd) {
597
826
  try {
598
- if (!existsSync(cwd)) {
827
+ if (!existsSync2(cwd)) {
599
828
  return { valid: false, error: `Working directory does not exist: ${cwd}` };
600
829
  }
601
- const stats = statSync(cwd);
830
+ const stats = statSync2(cwd);
602
831
  if (!stats.isDirectory()) {
603
832
  return { valid: false, error: `Path is not a directory: ${cwd}` };
604
833
  }
@@ -611,9 +840,9 @@ function validateCwd(cwd) {
611
840
  }
612
841
  }
613
842
  function wrap(proc) {
614
- const exitedPromise = new Promise((resolve) => {
615
- proc.once("close", (code) => resolve(code ?? 0));
616
- proc.once("error", () => resolve(1));
843
+ const exitedPromise = new Promise((resolve2) => {
844
+ proc.once("close", (code) => resolve2(code ?? 0));
845
+ proc.once("error", () => resolve2(1));
617
846
  });
618
847
  if (!proc.stdin || !proc.stdout || !proc.stderr) {
619
848
  throw new LspProcessSpawnError("Spawned process is missing one of stdin/stdout/stderr pipes");
@@ -667,7 +896,7 @@ function isWindowsShellShim(command) {
667
896
  return lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat");
668
897
  }
669
898
  function splitPath(pathValue, platform) {
670
- const separator = platform === "win32" ? ";" : delimiter;
899
+ const separator = platform === "win32" ? ";" : delimiter2;
671
900
  return pathValue.split(separator).filter(Boolean);
672
901
  }
673
902
  function getWindowsPathExtensions(env) {
@@ -682,8 +911,8 @@ function resolveWindowsCommand(command, env) {
682
911
  const extensions = getWindowsPathExtensions(env);
683
912
  for (const baseDirectory of baseDirectories) {
684
913
  for (const extension of extensions) {
685
- const candidate = baseDirectory ? join(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
686
- if (existsSync(candidate))
914
+ const candidate = baseDirectory ? join2(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
915
+ if (existsSync2(candidate))
687
916
  return candidate;
688
917
  }
689
918
  }
@@ -728,16 +957,16 @@ function spawnProcess(command, options) {
728
957
  return wrap(proc);
729
958
  }
730
959
 
731
- // ../lsp-core/src/lsp/transport.ts
732
- function isRecord(value) {
960
+ // ../lsp-core/src/lsp/transport-protocol.ts
961
+ function isRecord2(value) {
733
962
  return typeof value === "object" && value !== null && !Array.isArray(value);
734
963
  }
735
964
  function parseConfigurationItems(params) {
736
- if (!isRecord(params) || !Array.isArray(params["items"]))
965
+ if (!isRecord2(params) || !Array.isArray(params["items"]))
737
966
  return [];
738
967
  const items = [];
739
968
  for (const item of params["items"]) {
740
- if (!isRecord(item))
969
+ if (!isRecord2(item))
741
970
  continue;
742
971
  const section = item["section"];
743
972
  items.push(section === undefined || typeof section !== "string" ? {} : { section });
@@ -745,10 +974,35 @@ function parseConfigurationItems(params) {
745
974
  return items;
746
975
  }
747
976
  function parseDiagnosticsParams(params) {
748
- if (!isRecord(params) || typeof params["uri"] !== "string")
977
+ if (!isRecord2(params) || typeof params["uri"] !== "string")
749
978
  return null;
750
979
  const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
751
- return { uri: params["uri"], diagnostics };
980
+ const version = typeof params["version"] === "number" ? params["version"] : undefined;
981
+ return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
982
+ }
983
+ function createLspSpawnEnv(_root, input) {
984
+ return { ...input };
985
+ }
986
+ function isDiagnostic(value) {
987
+ return isRecord2(value) && isRange(value["range"]) && typeof value["message"] === "string";
988
+ }
989
+ function isRange(value) {
990
+ return isRecord2(value) && isPosition(value["start"]) && isPosition(value["end"]);
991
+ }
992
+ function isPosition(value) {
993
+ return isRecord2(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
994
+ }
995
+
996
+ // ../lsp-core/src/lsp/transport.ts
997
+ class LspClientNotStartedError extends Error {
998
+ serverId;
999
+ root;
1000
+ name = "LspClientNotStartedError";
1001
+ constructor(serverId, root) {
1002
+ super("LSP client not started");
1003
+ this.serverId = serverId;
1004
+ this.root = root;
1005
+ }
752
1006
  }
753
1007
 
754
1008
  class LspClientTransport {
@@ -761,6 +1015,8 @@ class LspClientTransport {
761
1015
  diagnosticsStore = new Map;
762
1016
  requestTimeoutMs;
763
1017
  initializeTimeoutMs;
1018
+ workspaceApplyEditHandler = null;
1019
+ diagnosticPullSupported = false;
764
1020
  constructor(root, server2, timeouts = {}) {
765
1021
  this.root = root;
766
1022
  this.server = server2;
@@ -773,6 +1029,21 @@ class LspClientTransport {
773
1029
  command() {
774
1030
  return [...this.server.command];
775
1031
  }
1032
+ setWorkspaceApplyEditHandler(handler) {
1033
+ this.workspaceApplyEditHandler = handler;
1034
+ }
1035
+ hasWorkspaceApplyEditHandler() {
1036
+ return this.workspaceApplyEditHandler !== null;
1037
+ }
1038
+ setDiagnosticPullSupported(supported) {
1039
+ this.diagnosticPullSupported = supported;
1040
+ }
1041
+ isDiagnosticPullSupported() {
1042
+ return this.diagnosticPullSupported;
1043
+ }
1044
+ handlePublishDiagnostics(params) {
1045
+ this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
1046
+ }
776
1047
  async start() {
777
1048
  const env = createLspSpawnEnv(this.root, {
778
1049
  ...process.env,
@@ -783,7 +1054,6 @@ class LspClientTransport {
783
1054
  env
784
1055
  });
785
1056
  this.startStderrReading();
786
- await new Promise((resolve) => setTimeout(resolve, 100));
787
1057
  if (this.proc.exitCode !== null) {
788
1058
  const stderr = this.stderrBuffer.join(`
789
1059
  `);
@@ -793,7 +1063,7 @@ class LspClientTransport {
793
1063
  this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
794
1064
  const diagnosticsParams = parseDiagnosticsParams(params);
795
1065
  if (diagnosticsParams?.uri) {
796
- this.diagnosticsStore.set(diagnosticsParams.uri, diagnosticsParams.diagnostics);
1066
+ this.handlePublishDiagnostics(diagnosticsParams);
797
1067
  }
798
1068
  });
799
1069
  this.connection.onRequest("workspace/configuration", (params) => {
@@ -806,6 +1076,9 @@ class LspClientTransport {
806
1076
  });
807
1077
  this.connection.onRequest("client/registerCapability", () => null);
808
1078
  this.connection.onRequest("window/workDoneProgress/create", () => null);
1079
+ if (this.workspaceApplyEditHandler) {
1080
+ this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
1081
+ }
809
1082
  this.connection.onClose(() => {
810
1083
  this.processExited = true;
811
1084
  });
@@ -834,30 +1107,25 @@ class LspClientTransport {
834
1107
  }
835
1108
  async sendRequest(method, ...args) {
836
1109
  if (!this.connection)
837
- throw new Error("LSP client not started");
1110
+ throw new LspClientNotStartedError(this.server.id, this.root);
838
1111
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
839
1112
  const stderrTail = this.stderrBuffer.slice(-10).join(`
840
1113
  `);
841
1114
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
842
1115
  }
843
- const timeoutMs = args[1]?.timeoutMs ?? this.requestTimeoutMs;
844
- let timeoutHandle = null;
845
- const timeoutPromise = new Promise((_, reject) => {
846
- timeoutHandle = setTimeout(() => {
847
- const stderrTail = this.stderrBuffer.slice(-5).join(`
1116
+ const options = args[1];
1117
+ const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
1118
+ const timeoutController = new AbortController;
1119
+ const timeoutHandle = setTimeout(() => {
1120
+ const stderrTail = this.stderrBuffer.slice(-5).join(`
848
1121
  `);
849
- reject(new LspRequestTimeoutError(method, stderrTail || undefined));
850
- }, timeoutMs);
851
- });
1122
+ timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
1123
+ }, timeoutMs);
1124
+ const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
852
1125
  try {
853
- const requestPromise = args.length === 0 ? this.connection.sendRequest(method) : this.connection.sendRequest(method, args[0]);
854
- const result = await Promise.race([requestPromise, timeoutPromise]);
855
- if (timeoutHandle !== null)
856
- clearTimeout(timeoutHandle);
1126
+ const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
857
1127
  return result;
858
1128
  } catch (error) {
859
- if (timeoutHandle !== null)
860
- clearTimeout(timeoutHandle);
861
1129
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
862
1130
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
863
1131
  `) || undefined);
@@ -866,6 +1134,9 @@ class LspClientTransport {
866
1134
  throw new LspConnectionClosedError(this.server.id, this.root, error.message);
867
1135
  }
868
1136
  throw error;
1137
+ } finally {
1138
+ clearTimeout(timeoutHandle);
1139
+ combinedSignal.dispose();
869
1140
  }
870
1141
  }
871
1142
  async sendNotification(method, ...args) {
@@ -894,17 +1165,17 @@ class LspClientTransport {
894
1165
  try {
895
1166
  await this.sendRequest("shutdown");
896
1167
  } catch (error) {
897
- reportBestEffortCleanupError("shutdown request", error);
1168
+ reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
898
1169
  }
899
1170
  try {
900
1171
  await this.sendNotification("exit");
901
1172
  } catch (error) {
902
- reportBestEffortCleanupError("exit notification", error);
1173
+ reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
903
1174
  }
904
1175
  try {
905
1176
  this.connection.dispose();
906
1177
  } catch (error) {
907
- reportBestEffortCleanupError("connection dispose", error);
1178
+ reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
908
1179
  }
909
1180
  this.connection = null;
910
1181
  }
@@ -915,8 +1186,8 @@ class LspClientTransport {
915
1186
  try {
916
1187
  proc.kill();
917
1188
  let timeoutId;
918
- const timeoutPromise = new Promise((resolve) => {
919
- timeoutId = setTimeout(resolve, STOP_HARD_KILL_TIMEOUT_MS);
1189
+ const timeoutPromise = new Promise((resolve2) => {
1190
+ timeoutId = setTimeout(resolve2, STOP_HARD_KILL_TIMEOUT_MS);
920
1191
  });
921
1192
  await Promise.race([
922
1193
  proc.exited.then(() => {
@@ -932,14 +1203,14 @@ class LspClientTransport {
932
1203
  proc.kill("SIGKILL");
933
1204
  await Promise.race([
934
1205
  proc.exited,
935
- new Promise((resolve) => setTimeout(resolve, STOP_SIGKILL_GRACE_MS))
1206
+ new Promise((resolve2) => setTimeout(resolve2, STOP_SIGKILL_GRACE_MS))
936
1207
  ]);
937
1208
  } catch (error) {
938
- reportBestEffortCleanupError("hard process kill", error);
1209
+ reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
939
1210
  }
940
1211
  }
941
1212
  } catch (error) {
942
- reportBestEffortCleanupError("process stop", error);
1213
+ reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
943
1214
  }
944
1215
  }
945
1216
  this.processExited = true;
@@ -949,26 +1220,45 @@ class LspClientTransport {
949
1220
  return this.diagnosticsStore.get(uri) ?? [];
950
1221
  }
951
1222
  }
952
- function createLspSpawnEnv(_root, input) {
953
- return { ...input };
954
- }
955
- function isDiagnostic(value) {
956
- return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
957
- }
958
- function isRange(value) {
959
- return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
960
- }
961
- function isPosition(value) {
962
- return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
1223
+ function combineAbortSignals(primary, secondary) {
1224
+ const controller = new AbortController;
1225
+ const abortFrom = (signal) => {
1226
+ if (!controller.signal.aborted)
1227
+ controller.abort(signal.reason);
1228
+ };
1229
+ const onPrimaryAbort = () => {
1230
+ if (primary)
1231
+ abortFrom(primary);
1232
+ };
1233
+ const onSecondaryAbort = () => abortFrom(secondary);
1234
+ if (primary?.aborted)
1235
+ abortFrom(primary);
1236
+ else
1237
+ primary?.addEventListener("abort", onPrimaryAbort, { once: true });
1238
+ if (secondary.aborted)
1239
+ abortFrom(secondary);
1240
+ else
1241
+ secondary.addEventListener("abort", onSecondaryAbort, { once: true });
1242
+ return {
1243
+ signal: controller.signal,
1244
+ dispose: () => {
1245
+ primary?.removeEventListener("abort", onPrimaryAbort);
1246
+ secondary.removeEventListener("abort", onSecondaryAbort);
1247
+ }
1248
+ };
963
1249
  }
964
1250
 
965
1251
  // ../lsp-core/src/lsp/connection.ts
966
- var INITIALIZE_SETTLE_MS = 300;
1252
+ function supportsDiagnosticPull(capabilities) {
1253
+ if (capabilities === undefined)
1254
+ return false;
1255
+ return Object.hasOwn(capabilities, "diagnosticProvider");
1256
+ }
967
1257
 
968
1258
  class LspClientConnection extends LspClientTransport {
969
1259
  async initialize() {
970
1260
  const rootUri = pathToFileURL(this.root).href;
971
- await this.sendRequest("initialize", {
1261
+ const result = await this.sendRequest("initialize", {
972
1262
  processId: process.pid,
973
1263
  rootUri,
974
1264
  rootPath: this.root,
@@ -982,8 +1272,7 @@ class LspClientConnection extends LspClientTransport {
982
1272
  publishDiagnostics: {},
983
1273
  rename: {
984
1274
  prepareSupport: true,
985
- prepareSupportDefaultBehavior: 1,
986
- honorsChangeAnnotations: true
1275
+ prepareSupportDefaultBehavior: 1
987
1276
  },
988
1277
  codeAction: {
989
1278
  codeActionLiteralSupport: {
@@ -1012,22 +1301,28 @@ class LspClientConnection extends LspClientTransport {
1012
1301
  symbol: {},
1013
1302
  workspaceFolders: true,
1014
1303
  configuration: true,
1015
- applyEdit: true,
1304
+ ...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
1016
1305
  workspaceEdit: {
1017
- documentChanges: true
1306
+ documentChanges: true,
1307
+ resourceOperations: ["create", "rename", "delete"]
1018
1308
  }
1019
1309
  }
1020
1310
  },
1021
1311
  initializationOptions: this.server.initialization
1022
1312
  }, { timeoutMs: this.initializeTimeoutMs });
1313
+ this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
1023
1314
  await this.sendNotification("initialized");
1024
1315
  await this.sendNotification("workspace/didChangeConfiguration", {
1025
1316
  settings: { json: { validate: { enable: true } } }
1026
1317
  });
1027
- await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
1028
1318
  }
1029
1319
  }
1030
1320
 
1321
+ // ../lsp-core/src/lsp/workspace-document-state.ts
1322
+ import { readFileSync, realpathSync as realpathSync2 } from "node:fs";
1323
+ import { relative as relative2, resolve as resolve2 } from "node:path";
1324
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
1325
+
1031
1326
  // ../lsp-core/src/lsp/language-mappings.ts
1032
1327
  var SYMBOL_KIND_MAP = {
1033
1328
  1: "File",
@@ -1200,82 +1495,1422 @@ function getLanguageId(ext) {
1200
1495
  return EXT_TO_LANG[ext] ?? "plaintext";
1201
1496
  }
1202
1497
 
1498
+ // ../lsp-core/src/lsp/workspace-document-state.ts
1499
+ var WATCHED_FILE_BATCH_SIZE = 128;
1500
+ var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
1501
+ function canonicalPath(filePath) {
1502
+ const absolute = resolve2(filePath);
1503
+ try {
1504
+ return realpathSync2(absolute);
1505
+ } catch {
1506
+ return absolute;
1507
+ }
1508
+ }
1509
+ function isSameOrDescendant(candidate, parent) {
1510
+ const suffix = relative2(parent, candidate);
1511
+ return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
1512
+ }
1513
+ function movedPath(candidate, oldPath, newPath) {
1514
+ const suffix = relative2(oldPath, candidate);
1515
+ return suffix === "" ? newPath : resolve2(newPath, suffix);
1516
+ }
1517
+
1518
+ class WorkspaceDocumentState {
1519
+ sendNotification;
1520
+ clearDiagnostics;
1521
+ openDocuments = new Map;
1522
+ openByUri = new Map;
1523
+ openPromises = new Map;
1524
+ now;
1525
+ versionlessPublishQuiescenceMs;
1526
+ constructor(sendNotification, clearDiagnostics, options = {}) {
1527
+ this.sendNotification = sendNotification;
1528
+ this.clearDiagnostics = clearDiagnostics;
1529
+ this.now = options.now ?? (() => Date.now());
1530
+ this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
1531
+ }
1532
+ async openFile(filePath) {
1533
+ const path = canonicalPath(filePath);
1534
+ const existingOpen = this.openPromises.get(path);
1535
+ if (existingOpen) {
1536
+ await existingOpen;
1537
+ return this.openFile(path);
1538
+ }
1539
+ const text = readFileSync(path, "utf-8");
1540
+ const existing = this.openDocuments.get(path);
1541
+ if (!existing)
1542
+ return this.openDocumentSingleFlight(path, text);
1543
+ if (existing.text === text)
1544
+ return;
1545
+ await this.changeDocument(existing, text);
1546
+ }
1547
+ getVersion(filePath) {
1548
+ return this.openDocuments.get(canonicalPath(filePath))?.version;
1549
+ }
1550
+ getStoredDiagnostics(uri) {
1551
+ const state = this.openByUri.get(uri);
1552
+ if (!state)
1553
+ return [];
1554
+ return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
1555
+ }
1556
+ captureDiagnosticSnapshot(filePath) {
1557
+ const state = this.openDocuments.get(canonicalPath(filePath));
1558
+ if (!state)
1559
+ return null;
1560
+ return {
1561
+ path: state.path,
1562
+ uri: state.uri,
1563
+ version: state.version,
1564
+ documentGeneration: state.generation,
1565
+ publishGeneration: state.publishGeneration
1566
+ };
1567
+ }
1568
+ isCurrentSnapshot(snapshot) {
1569
+ const state = this.openDocuments.get(snapshot.path);
1570
+ return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
1571
+ }
1572
+ getPullCache(snapshot) {
1573
+ const state = this.openByUri.get(snapshot.uri);
1574
+ if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
1575
+ return null;
1576
+ return state.pullCache;
1577
+ }
1578
+ recordPullDiagnostics(snapshot, report) {
1579
+ const state = this.openByUri.get(snapshot.uri);
1580
+ if (!state)
1581
+ return;
1582
+ state.pullCache = {
1583
+ documentVersion: snapshot.version,
1584
+ diagnostics: [...report.diagnostics],
1585
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
1586
+ };
1587
+ }
1588
+ recordPublishedDiagnostics(params) {
1589
+ const state = this.openByUri.get(params.uri);
1590
+ if (!state)
1591
+ return;
1592
+ state.publishGeneration += 1;
1593
+ state.lastPublish = {
1594
+ diagnostics: [...params.diagnostics],
1595
+ publishGeneration: state.publishGeneration,
1596
+ documentGenerationAtArrival: state.generation,
1597
+ arrivedAt: this.now(),
1598
+ ...params.version === undefined ? {} : { version: params.version }
1599
+ };
1600
+ this.notifyWaiters(state);
1601
+ }
1602
+ resolvePushDiagnostics(snapshot) {
1603
+ const state = this.openByUri.get(snapshot.uri);
1604
+ if (!state?.lastPublish)
1605
+ return { status: "missing" };
1606
+ const publish = state.lastPublish;
1607
+ if (publish.version !== undefined) {
1608
+ return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
1609
+ }
1610
+ if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
1611
+ return { status: "missing" };
1612
+ const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
1613
+ const waitMs = Math.max(0, readyAt - this.now());
1614
+ return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
1615
+ }
1616
+ waitForDiagnosticsActivity(snapshot, timeoutMs) {
1617
+ const state = this.openByUri.get(snapshot.uri);
1618
+ if (!state || timeoutMs <= 0)
1619
+ return Promise.resolve();
1620
+ return new Promise((resolveActivity) => {
1621
+ let settled = false;
1622
+ const finish = () => {
1623
+ if (settled)
1624
+ return;
1625
+ settled = true;
1626
+ clearTimeout(timer);
1627
+ state.waiters.delete(finish);
1628
+ resolveActivity();
1629
+ };
1630
+ const timer = setTimeout(finish, timeoutMs);
1631
+ if (typeof timer.unref === "function")
1632
+ timer.unref();
1633
+ state.waiters.add(finish);
1634
+ });
1635
+ }
1636
+ validateVersions(operations) {
1637
+ const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
1638
+ for (const operation of operations) {
1639
+ if (operation.kind === "text") {
1640
+ const current = versions.get(operation.path);
1641
+ if (operation.documentVersion !== null && current !== operation.documentVersion) {
1642
+ const observed = current === undefined ? "closed document" : `open document version ${current}`;
1643
+ return {
1644
+ changeIndex: operation.changeIndex,
1645
+ message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
1646
+ };
1647
+ }
1648
+ if (current !== undefined)
1649
+ versions.set(operation.path, current + 1);
1650
+ continue;
1651
+ }
1652
+ if (operation.kind === "rename") {
1653
+ const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
1654
+ for (const [path] of moved)
1655
+ versions.delete(path);
1656
+ for (const [path] of moved)
1657
+ versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
1658
+ continue;
1659
+ }
1660
+ if (operation.kind === "delete") {
1661
+ for (const path of [...versions.keys()]) {
1662
+ if (isSameOrDescendant(path, operation.path))
1663
+ versions.delete(path);
1664
+ }
1665
+ continue;
1666
+ }
1667
+ if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
1668
+ versions.set(operation.path, 1);
1669
+ }
1670
+ }
1671
+ return null;
1672
+ }
1673
+ async synchronize(delta) {
1674
+ const watched = [];
1675
+ for (const mutation of delta.operations)
1676
+ await this.synchronizeMutation(mutation, watched);
1677
+ for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
1678
+ await this.sendNotification("workspace/didChangeWatchedFiles", {
1679
+ changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
1680
+ });
1681
+ }
1682
+ }
1683
+ async synchronizeMutation(mutation, watched) {
1684
+ if (mutation.kind === "text") {
1685
+ const state = this.openDocuments.get(mutation.path);
1686
+ if (state)
1687
+ await this.changeDocument(state, mutation.afterText);
1688
+ else
1689
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
1690
+ return;
1691
+ }
1692
+ if (mutation.kind === "create") {
1693
+ const state = this.openDocuments.get(mutation.path);
1694
+ if (state) {
1695
+ await this.closeDocument(state);
1696
+ await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
1697
+ } else {
1698
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
1699
+ }
1700
+ return;
1701
+ }
1702
+ if (mutation.kind === "rename") {
1703
+ const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
1704
+ for (const state of moved)
1705
+ await this.closeDocument(state);
1706
+ for (const state of moved) {
1707
+ const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
1708
+ await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
1709
+ }
1710
+ if (moved.length === 0) {
1711
+ watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
1712
+ watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
1713
+ }
1714
+ return;
1715
+ }
1716
+ const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
1717
+ for (const state of removed)
1718
+ await this.closeDocument(state);
1719
+ if (removed.length === 0)
1720
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
1721
+ }
1722
+ async openDocumentSingleFlight(path, text) {
1723
+ const existing = this.openPromises.get(path);
1724
+ if (existing)
1725
+ return existing;
1726
+ const open = (async () => {
1727
+ const state = {
1728
+ path,
1729
+ uri: pathToFileURL2(path).href,
1730
+ languageId: getLanguageId(effectiveExtension(path)),
1731
+ text,
1732
+ version: 1,
1733
+ generation: 1,
1734
+ publishGeneration: 0,
1735
+ waiters: new Set
1736
+ };
1737
+ this.openDocuments.set(path, state);
1738
+ this.openByUri.set(state.uri, state);
1739
+ this.notifyWaiters(state);
1740
+ await this.sendNotification("textDocument/didOpen", {
1741
+ textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
1742
+ });
1743
+ })().finally(() => {
1744
+ this.openPromises.delete(path);
1745
+ });
1746
+ this.openPromises.set(path, open);
1747
+ return open;
1748
+ }
1749
+ async changeDocument(state, text) {
1750
+ state.text = text;
1751
+ state.version += 1;
1752
+ state.generation += 1;
1753
+ this.clearDiagnostics(state.uri);
1754
+ this.notifyWaiters(state);
1755
+ await this.sendNotification("textDocument/didChange", {
1756
+ textDocument: { uri: state.uri, version: state.version },
1757
+ contentChanges: [{ text }]
1758
+ });
1759
+ await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
1760
+ }
1761
+ async closeDocument(state) {
1762
+ this.openDocuments.delete(state.path);
1763
+ this.openByUri.delete(state.uri);
1764
+ this.clearDiagnostics(state.uri);
1765
+ this.notifyWaiters(state);
1766
+ await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
1767
+ }
1768
+ notifyWaiters(state) {
1769
+ for (const waiter of [...state.waiters])
1770
+ waiter();
1771
+ }
1772
+ }
1773
+
1774
+ // ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
1775
+ var CONCURRENT_FAILURE_REASON_BY_PHASE = {
1776
+ applying: "workspace/applyEdit is already in progress for this workspace mutation",
1777
+ settled: "workspace/applyEdit was already handled for this workspace mutation"
1778
+ };
1779
+ function workspaceApplyEditConcurrentFailureReason(phase) {
1780
+ return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
1781
+ }
1782
+
1783
+ // ../lsp-core/src/lsp/workspace-edit-commit.ts
1784
+ import { existsSync as existsSync4, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
1785
+
1786
+ // ../lsp-core/src/lsp/workspace-edit-path.ts
1787
+ import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync3 } from "node:fs";
1788
+ import { dirname as dirname2, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3 } from "node:path";
1789
+ import { fileURLToPath } from "node:url";
1790
+
1791
+ class WorkspaceEditPathError extends Error {
1792
+ path;
1793
+ detail;
1794
+ name = "WorkspaceEditPathError";
1795
+ constructor(path, detail) {
1796
+ super(`${detail}: ${path}`);
1797
+ this.path = path;
1798
+ this.detail = detail;
1799
+ }
1800
+ }
1801
+ function isPathInsideWorkspace(filePath, workspaceRoot) {
1802
+ const relativePath = relative3(workspaceRoot, filePath);
1803
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
1804
+ }
1805
+ function canonicalizeMissingPath(filePath) {
1806
+ let ancestor = filePath;
1807
+ while (!existsSync3(ancestor)) {
1808
+ const parent = dirname2(ancestor);
1809
+ if (parent === ancestor)
1810
+ throw new WorkspaceEditPathError(filePath, "no existing ancestor");
1811
+ ancestor = parent;
1812
+ }
1813
+ return resolve3(realpathSync3(ancestor), relative3(ancestor, filePath));
1814
+ }
1815
+ function canonicalWorkspaceRoot(workspaceRoot) {
1816
+ try {
1817
+ const canonical = realpathSync3(resolve3(workspaceRoot));
1818
+ if (!lstatSync(canonical).isDirectory()) {
1819
+ return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
1820
+ }
1821
+ return {
1822
+ success: true,
1823
+ path: canonical,
1824
+ requestedPath: resolve3(workspaceRoot),
1825
+ followedSymbolicLink: existsSync3(resolve3(workspaceRoot)) && lstatSync(resolve3(workspaceRoot)).isSymbolicLink()
1826
+ };
1827
+ } catch (error) {
1828
+ const detail = error instanceof Error ? error.message : String(error);
1829
+ return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
1830
+ }
1831
+ }
1832
+ function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
1833
+ let requestedPath;
1834
+ try {
1835
+ const parsed = new URL(uri);
1836
+ if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
1837
+ return { success: false, error: `non-file URI ${uri}` };
1838
+ }
1839
+ requestedPath = resolve3(fileURLToPath(parsed));
1840
+ } catch (error) {
1841
+ const detail = error instanceof Error ? error.message : String(error);
1842
+ return { success: false, error: `non-file URI ${uri}: ${detail}` };
1843
+ }
1844
+ try {
1845
+ const canonical = existsSync3(requestedPath) ? realpathSync3(requestedPath) : canonicalizeMissingPath(requestedPath);
1846
+ if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
1847
+ return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
1848
+ }
1849
+ return {
1850
+ success: true,
1851
+ path: canonical,
1852
+ requestedPath,
1853
+ followedSymbolicLink: existsSync3(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
1854
+ };
1855
+ } catch (error) {
1856
+ const detail = error instanceof Error ? error.message : String(error);
1857
+ return { success: false, error: `${requestedPath}: ${detail}` };
1858
+ }
1859
+ }
1860
+ function snapshotPath(path, includeChildren) {
1861
+ if (!existsSync3(path))
1862
+ return { kind: "missing" };
1863
+ const stats = lstatSync(path);
1864
+ if (stats.isFile())
1865
+ return { kind: "file", content: readFileSync2(path, "utf-8") };
1866
+ if (stats.isDirectory()) {
1867
+ return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
1868
+ }
1869
+ throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
1870
+ }
1871
+
1872
+ // ../lsp-core/src/lsp/workspace-edit-commit.ts
1873
+ var DEFAULT_IO = {
1874
+ writeFile(path, content) {
1875
+ writeFileSync(path, content, "utf-8");
1876
+ },
1877
+ rename(oldPath, newPath) {
1878
+ renameSync(oldPath, newPath);
1879
+ },
1880
+ remove(path, recursive) {
1881
+ rmSync(path, { recursive, force: false });
1882
+ }
1883
+ };
1884
+ function snapshotsEqual(expected, actual) {
1885
+ if (expected.kind !== actual.kind)
1886
+ return false;
1887
+ if (expected.kind === "file" && actual.kind === "file")
1888
+ return expected.content === actual.content;
1889
+ if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
1890
+ return JSON.stringify(expected.children) === JSON.stringify(actual.children);
1891
+ }
1892
+ return true;
1893
+ }
1894
+ function liveSnapshot(path, expected) {
1895
+ return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
1896
+ }
1897
+ function firstOperationIndex(plan) {
1898
+ return plan.operations[0]?.changeIndex ?? 0;
1899
+ }
1900
+ function failedCommit(plan, failure) {
1901
+ const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
1902
+ return {
1903
+ result: {
1904
+ success: false,
1905
+ filesModified,
1906
+ totalEdits,
1907
+ errors: [`change ${changeIndex}: ${message}`],
1908
+ failedChange: changeIndex,
1909
+ ...lateAbort ? { lateAbort: true } : {}
1910
+ },
1911
+ delta: mutationDelta(mutations),
1912
+ fingerprint: plan.fingerprint
1913
+ };
1914
+ }
1915
+ function verifySnapshots(plan) {
1916
+ for (const [path, expected] of plan.snapshots) {
1917
+ let actual;
1918
+ try {
1919
+ actual = liveSnapshot(path, expected);
1920
+ } catch (error) {
1921
+ const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
1922
+ const detail = error instanceof Error ? error.message : String(error);
1923
+ return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
1924
+ }
1925
+ if (!snapshotsEqual(expected, actual)) {
1926
+ const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
1927
+ return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
1928
+ }
1929
+ }
1930
+ return null;
1931
+ }
1932
+ function addModifiedPath(paths, path) {
1933
+ if (!paths.includes(path))
1934
+ paths.push(path);
1935
+ }
1936
+ function reportedPath(plan, path) {
1937
+ return plan.reportedPathByCanonical.get(path) ?? path;
1938
+ }
1939
+ function changedPathsForMutation(mutation) {
1940
+ return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
1941
+ }
1942
+ function mutationDelta(operations) {
1943
+ const changedPaths = new Set;
1944
+ for (const operation of operations) {
1945
+ for (const path of changedPathsForMutation(operation))
1946
+ changedPaths.add(path);
1947
+ }
1948
+ return { operations, changedPaths: [...changedPaths].sort() };
1949
+ }
1950
+ function resolveIo(overrides) {
1951
+ return {
1952
+ writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
1953
+ rename: overrides?.rename ?? DEFAULT_IO.rename,
1954
+ remove: overrides?.remove ?? DEFAULT_IO.remove
1955
+ };
1956
+ }
1957
+ function commitOperation(context, operation) {
1958
+ const { plan, io, accumulator } = context;
1959
+ if (operation.kind === "noop")
1960
+ return;
1961
+ if (operation.kind === "text") {
1962
+ io.writeFile(operation.path, operation.afterText);
1963
+ accumulator.mutations.push({
1964
+ kind: "text",
1965
+ path: operation.path,
1966
+ beforeText: operation.beforeText,
1967
+ afterText: operation.afterText
1968
+ });
1969
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1970
+ accumulator.totalEdits += operation.editCount;
1971
+ return;
1972
+ }
1973
+ if (operation.kind === "create") {
1974
+ io.writeFile(operation.path, "");
1975
+ accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
1976
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1977
+ return;
1978
+ }
1979
+ if (operation.kind === "rename") {
1980
+ if (operation.replaceDestination) {
1981
+ const targetKind = existsSync4(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
1982
+ io.remove(operation.newPath, targetKind === "directory");
1983
+ accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
1984
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
1985
+ }
1986
+ io.rename(operation.oldPath, operation.newPath);
1987
+ accumulator.mutations.push({
1988
+ kind: "rename",
1989
+ oldPath: operation.oldPath,
1990
+ newPath: operation.newPath,
1991
+ sourceKind: operation.sourceKind
1992
+ });
1993
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
1994
+ return;
1995
+ }
1996
+ io.remove(operation.path, operation.recursive);
1997
+ accumulator.mutations.push({
1998
+ kind: "delete",
1999
+ path: operation.path,
2000
+ targetKind: operation.targetKind
2001
+ });
2002
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
2003
+ }
2004
+ function commitWorkspaceEditPlan(plan, options = {}) {
2005
+ if (options.signal?.aborted) {
2006
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
2007
+ }
2008
+ const stale = verifySnapshots(plan);
2009
+ if (stale)
2010
+ return stale;
2011
+ if (options.signal?.aborted) {
2012
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
2013
+ }
2014
+ const io = resolveIo(options.io);
2015
+ const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
2016
+ const context = { plan, io, accumulator };
2017
+ let lateAbort = false;
2018
+ for (const operation of plan.operations) {
2019
+ try {
2020
+ commitOperation(context, operation);
2021
+ } catch (error) {
2022
+ const detail = error instanceof Error ? error.message : String(error);
2023
+ return failedCommit(plan, {
2024
+ message: `I/O failure during ${operation.kind}: ${detail}`,
2025
+ changeIndex: operation.changeIndex,
2026
+ mutations: accumulator.mutations,
2027
+ filesModified: accumulator.filesModified,
2028
+ totalEdits: accumulator.totalEdits,
2029
+ lateAbort: lateAbort || options.signal?.aborted === true
2030
+ });
2031
+ }
2032
+ if (options.signal?.aborted)
2033
+ lateAbort = true;
2034
+ }
2035
+ const result = {
2036
+ success: true,
2037
+ filesModified: accumulator.filesModified,
2038
+ totalEdits: accumulator.totalEdits,
2039
+ errors: [],
2040
+ ...lateAbort ? { lateAbort: true } : {}
2041
+ };
2042
+ return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
2043
+ }
2044
+
2045
+ // ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
2046
+ import { createHash } from "node:crypto";
2047
+ function canonicalFingerprint(operations) {
2048
+ const canonical = operations.map((operation) => {
2049
+ switch (operation.kind) {
2050
+ case "text":
2051
+ return {
2052
+ kind: operation.kind,
2053
+ changeIndex: operation.changeIndex,
2054
+ path: operation.path,
2055
+ edits: operation.edits,
2056
+ version: operation.version
2057
+ };
2058
+ case "rename":
2059
+ return {
2060
+ kind: operation.kind,
2061
+ changeIndex: operation.changeIndex,
2062
+ oldPath: operation.oldPath,
2063
+ newPath: operation.newPath,
2064
+ overwrite: operation.overwrite,
2065
+ ignoreIfExists: operation.ignoreIfExists
2066
+ };
2067
+ case "create":
2068
+ return {
2069
+ kind: operation.kind,
2070
+ changeIndex: operation.changeIndex,
2071
+ path: operation.path,
2072
+ overwrite: operation.overwrite,
2073
+ ignoreIfExists: operation.ignoreIfExists
2074
+ };
2075
+ case "delete":
2076
+ return {
2077
+ kind: operation.kind,
2078
+ changeIndex: operation.changeIndex,
2079
+ path: operation.path,
2080
+ recursive: operation.recursive,
2081
+ ignoreIfNotExists: operation.ignoreIfNotExists
2082
+ };
2083
+ }
2084
+ });
2085
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
2086
+ }
2087
+
2088
+ // ../lsp-core/src/lsp/workspace-edit-types.ts
2089
+ class WorkspaceEditValidationError extends Error {
2090
+ changeIndex;
2091
+ detail;
2092
+ name = "WorkspaceEditValidationError";
2093
+ constructor(changeIndex, detail) {
2094
+ super(`change ${changeIndex}: ${detail}`);
2095
+ this.changeIndex = changeIndex;
2096
+ this.detail = detail;
2097
+ }
2098
+ }
2099
+
2100
+ // ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
2101
+ function isRecord3(value) {
2102
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2103
+ }
2104
+ function parsePosition(value) {
2105
+ if (!isRecord3(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
2106
+ return null;
2107
+ }
2108
+ return { line: value["line"], character: value["character"] };
2109
+ }
2110
+ function parseRange(value) {
2111
+ if (!isRecord3(value))
2112
+ return null;
2113
+ const start = parsePosition(value["start"]);
2114
+ const end = parsePosition(value["end"]);
2115
+ return start && end ? { start, end } : null;
2116
+ }
2117
+ function parseTextEdits(value, changeIndex) {
2118
+ if (!Array.isArray(value)) {
2119
+ throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
2120
+ }
2121
+ const edits = [];
2122
+ for (const candidate of value) {
2123
+ if (!isRecord3(candidate) || typeof candidate["newText"] !== "string") {
2124
+ throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
2125
+ }
2126
+ if ("annotationId" in candidate) {
2127
+ throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
2128
+ }
2129
+ const range = parseRange(candidate["range"]);
2130
+ if (!range)
2131
+ throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
2132
+ edits.push({ range, newText: candidate["newText"] });
2133
+ }
2134
+ return edits;
2135
+ }
2136
+ function parseBooleanOption(options, key, changeIndex) {
2137
+ const value = options[key];
2138
+ if (value === undefined)
2139
+ return false;
2140
+ if (typeof value !== "boolean") {
2141
+ throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
2142
+ }
2143
+ return value;
2144
+ }
2145
+ function parseOptions(value, allowed, changeIndex) {
2146
+ if (value === undefined)
2147
+ return {};
2148
+ if (!isRecord3(value))
2149
+ throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
2150
+ for (const key of Object.keys(value)) {
2151
+ if (!allowed.includes(key))
2152
+ throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
2153
+ }
2154
+ const parsed = {};
2155
+ for (const key of allowed)
2156
+ parsed[key] = parseBooleanOption(value, key, changeIndex);
2157
+ return parsed;
2158
+ }
2159
+
2160
+ // ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
2161
+ function parseResourceChange(input) {
2162
+ const kind = input.change["kind"];
2163
+ if (kind === "create" || kind === "delete") {
2164
+ parseSinglePathResource(input, kind);
2165
+ return;
2166
+ }
2167
+ if (kind !== "rename") {
2168
+ throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
2169
+ }
2170
+ parseRename(input);
2171
+ }
2172
+ function parseSinglePathResource(input, kind) {
2173
+ const { change, changeIndex, workspaceRoot, target } = input;
2174
+ if (typeof change["uri"] !== "string")
2175
+ throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
2176
+ const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
2177
+ if (!resolvedPath.success) {
2178
+ target.failures.push({ changeIndex, message: resolvedPath.error });
2179
+ return;
2180
+ }
2181
+ if (kind === "create") {
2182
+ const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
2183
+ target.operations.push({
2184
+ kind,
2185
+ changeIndex,
2186
+ path: resolvedPath.path,
2187
+ reportedPath: resolvedPath.requestedPath,
2188
+ overwrite: options2["overwrite"] ?? false,
2189
+ ignoreIfExists: options2["ignoreIfExists"] ?? false,
2190
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
2191
+ });
2192
+ return;
2193
+ }
2194
+ const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
2195
+ target.operations.push({
2196
+ kind,
2197
+ changeIndex,
2198
+ path: resolvedPath.path,
2199
+ reportedPath: resolvedPath.requestedPath,
2200
+ recursive: options["recursive"] ?? false,
2201
+ ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
2202
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
2203
+ });
2204
+ }
2205
+ function parseRename(input) {
2206
+ const { change, changeIndex, workspaceRoot, target } = input;
2207
+ if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
2208
+ throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
2209
+ }
2210
+ const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
2211
+ const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
2212
+ if (!oldPath.success || !newPath.success) {
2213
+ target.failures.push({
2214
+ changeIndex,
2215
+ message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
2216
+ });
2217
+ return;
2218
+ }
2219
+ const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
2220
+ target.operations.push({
2221
+ kind: "rename",
2222
+ changeIndex,
2223
+ oldPath: oldPath.path,
2224
+ newPath: newPath.path,
2225
+ reportedOldPath: oldPath.requestedPath,
2226
+ reportedNewPath: newPath.requestedPath,
2227
+ overwrite: options["overwrite"] ?? false,
2228
+ ignoreIfExists: options["ignoreIfExists"] ?? false,
2229
+ followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
2230
+ });
2231
+ }
2232
+
2233
+ // ../lsp-core/src/lsp/workspace-edit-parser.ts
2234
+ function failureResult(failures) {
2235
+ const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
2236
+ const first = sorted[0];
2237
+ return {
2238
+ success: false,
2239
+ filesModified: [],
2240
+ totalEdits: 0,
2241
+ errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
2242
+ ...first ? { failedChange: first.changeIndex } : {}
2243
+ };
2244
+ }
2245
+ function parseWorkspaceEdit(edit, workspaceRoot) {
2246
+ if (!isRecord3(edit))
2247
+ return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
2248
+ if (edit["changeAnnotations"] !== undefined) {
2249
+ return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
2250
+ }
2251
+ const hasChanges = edit["changes"] !== undefined;
2252
+ const hasDocumentChanges = edit["documentChanges"] !== undefined;
2253
+ if (hasChanges && hasDocumentChanges) {
2254
+ return {
2255
+ operations: [],
2256
+ failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
2257
+ };
2258
+ }
2259
+ const target = { operations: [], failures: [] };
2260
+ if (hasChanges)
2261
+ return parseChanges(edit["changes"], workspaceRoot, target);
2262
+ return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
2263
+ }
2264
+ function parseChanges(value, workspaceRoot, target) {
2265
+ if (!isRecord3(value))
2266
+ return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
2267
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
2268
+ for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
2269
+ const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
2270
+ if (!resolvedPath.success) {
2271
+ target.failures.push({ changeIndex, message: resolvedPath.error });
2272
+ continue;
2273
+ }
2274
+ try {
2275
+ target.operations.push({
2276
+ kind: "text",
2277
+ changeIndex,
2278
+ path: resolvedPath.path,
2279
+ reportedPath: resolvedPath.requestedPath,
2280
+ edits: parseTextEdits(rawEdits, changeIndex),
2281
+ version: null
2282
+ });
2283
+ } catch (error) {
2284
+ if (error instanceof WorkspaceEditValidationError) {
2285
+ target.failures.push({ changeIndex, message: error.detail });
2286
+ continue;
2287
+ }
2288
+ throw error;
2289
+ }
2290
+ }
2291
+ return target;
2292
+ }
2293
+ function parseDocumentChanges(value, workspaceRoot, target) {
2294
+ if (value === undefined)
2295
+ return target;
2296
+ if (!Array.isArray(value)) {
2297
+ return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
2298
+ }
2299
+ for (const [changeIndex, change] of value.entries()) {
2300
+ try {
2301
+ parseDocumentChange({ change, changeIndex, workspaceRoot, target });
2302
+ } catch (error) {
2303
+ if (error instanceof WorkspaceEditValidationError) {
2304
+ target.failures.push({ changeIndex, message: error.detail });
2305
+ continue;
2306
+ }
2307
+ throw error;
2308
+ }
2309
+ }
2310
+ return target;
2311
+ }
2312
+ function parseDocumentChange(input) {
2313
+ const { change, changeIndex, workspaceRoot, target } = input;
2314
+ if (!isRecord3(change))
2315
+ throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
2316
+ if ("annotationId" in change) {
2317
+ throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
2318
+ }
2319
+ if (typeof change["kind"] === "string") {
2320
+ parseResourceChange({ change, changeIndex, workspaceRoot, target });
2321
+ return;
2322
+ }
2323
+ const identifier = change["textDocument"];
2324
+ if (!isRecord3(identifier) || typeof identifier["uri"] !== "string") {
2325
+ throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
2326
+ }
2327
+ const version = identifier["version"];
2328
+ if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
2329
+ throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
2330
+ }
2331
+ const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
2332
+ if (!resolvedPath.success) {
2333
+ target.failures.push({ changeIndex, message: resolvedPath.error });
2334
+ return;
2335
+ }
2336
+ target.operations.push({
2337
+ kind: "text",
2338
+ changeIndex,
2339
+ path: resolvedPath.path,
2340
+ reportedPath: resolvedPath.requestedPath,
2341
+ edits: parseTextEdits(change["edits"], changeIndex),
2342
+ version
2343
+ });
2344
+ }
2345
+
2346
+ // ../lsp-core/src/lsp/workspace-edit-simulation.ts
2347
+ import { dirname as dirname3, relative as relative4, resolve as resolve4 } from "node:path";
2348
+
2349
+ // ../lsp-core/src/lsp/workspace-edit-text.ts
2350
+ function comparePosition(left, right) {
2351
+ return left.line === right.line ? left.character - right.character : left.line - right.line;
2352
+ }
2353
+ function positionsEqual(left, right) {
2354
+ return left.line === right.line && left.character === right.character;
2355
+ }
2356
+ function rangesEqual(left, right) {
2357
+ return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
2358
+ }
2359
+ function isEmptyRange(range) {
2360
+ return positionsEqual(range.start, range.end);
2361
+ }
2362
+ function formatRange(range) {
2363
+ return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
2364
+ }
2365
+ function validatePosition(position, label, context) {
2366
+ const { lines, changeIndex } = context;
2367
+ if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
2368
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
2369
+ }
2370
+ if (position.line < 0 || position.character < 0) {
2371
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
2372
+ }
2373
+ const line = lines[position.line];
2374
+ if (line === undefined) {
2375
+ throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
2376
+ }
2377
+ if (position.character > line.length) {
2378
+ throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
2379
+ }
2380
+ }
2381
+ function validateRange(range, lines, changeIndex) {
2382
+ const context = { lines, changeIndex };
2383
+ validatePosition(range.start, "start", context);
2384
+ validatePosition(range.end, "end", context);
2385
+ if (comparePosition(range.start, range.end) > 0) {
2386
+ throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
2387
+ }
2388
+ }
2389
+ function sortAndDeduplicate(edits) {
2390
+ const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
2391
+ const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
2392
+ return positionOrder === 0 ? right.index - left.index : positionOrder;
2393
+ });
2394
+ const unique = [];
2395
+ for (const entry of sorted) {
2396
+ const previous = unique.at(-1);
2397
+ if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
2398
+ continue;
2399
+ }
2400
+ unique.push(entry.edit);
2401
+ }
2402
+ return unique;
2403
+ }
2404
+ function validateNoOverlap(edits, changeIndex) {
2405
+ for (let index = 0;index < edits.length - 1; index += 1) {
2406
+ const later = edits[index];
2407
+ const earlier = edits[index + 1];
2408
+ if (later === undefined || earlier === undefined)
2409
+ continue;
2410
+ if (comparePosition(earlier.range.end, later.range.start) > 0) {
2411
+ throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
2412
+ }
2413
+ }
2414
+ }
2415
+ function applyNormalizedTextEdits(content, edits) {
2416
+ const lines = content.split(`
2417
+ `);
2418
+ for (const edit of edits) {
2419
+ const { start, end } = edit.range;
2420
+ const startLine = lines[start.line];
2421
+ const endLine = lines[end.line];
2422
+ if (startLine === undefined || endLine === undefined)
2423
+ continue;
2424
+ const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
2425
+ lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
2426
+ `));
2427
+ }
2428
+ return lines.join(`
2429
+ `);
2430
+ }
2431
+ function normalizeTextEdits(content, edits, changeIndex) {
2432
+ const lines = content.split(`
2433
+ `);
2434
+ for (const edit of edits) {
2435
+ validateRange(edit.range, lines, changeIndex);
2436
+ }
2437
+ const normalized = sortAndDeduplicate(edits);
2438
+ validateNoOverlap(normalized, changeIndex);
2439
+ return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
2440
+ }
2441
+
2442
+ // ../lsp-core/src/lsp/workspace-edit-simulation.ts
2443
+ function isSameOrDescendant2(candidate, parent) {
2444
+ const relativePath = relative4(parent, candidate);
2445
+ return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
2446
+ }
2447
+ function removeVirtualSubtree(virtual, path) {
2448
+ for (const candidate of [...virtual.keys()]) {
2449
+ if (isSameOrDescendant2(candidate, path))
2450
+ virtual.delete(candidate);
2451
+ }
2452
+ virtual.set(path, { kind: "missing" });
2453
+ }
2454
+ function moveVirtualSubtree(virtual, oldPath, newPath) {
2455
+ const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
2456
+ removeVirtualSubtree(virtual, oldPath);
2457
+ removeVirtualSubtree(virtual, newPath);
2458
+ for (const [candidate, entry] of moved) {
2459
+ const suffix = relative4(oldPath, candidate);
2460
+ virtual.set(suffix === "" ? newPath : resolve4(newPath, suffix), entry);
2461
+ }
2462
+ }
2463
+ function virtualDirectoryHasChildren(virtual, path) {
2464
+ for (const [candidate, entry] of virtual) {
2465
+ if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
2466
+ return true;
2467
+ }
2468
+ return false;
2469
+ }
2470
+ function requireVirtualParent(virtual, path, changeIndex) {
2471
+ if (virtual.get(dirname3(path))?.kind !== "directory") {
2472
+ throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
2473
+ }
2474
+ }
2475
+ function simulateOperations(parsed, snapshots) {
2476
+ const virtual = new Map(snapshots);
2477
+ const planned = [];
2478
+ const failures = [];
2479
+ for (const operation of parsed) {
2480
+ try {
2481
+ planned.push(simulateOperation(operation, virtual));
2482
+ } catch (error) {
2483
+ if (error instanceof WorkspaceEditValidationError) {
2484
+ failures.push({ changeIndex: operation.changeIndex, message: error.detail });
2485
+ continue;
2486
+ }
2487
+ throw error;
2488
+ }
2489
+ }
2490
+ return { operations: planned, failures };
2491
+ }
2492
+ function simulateOperation(operation, virtual) {
2493
+ switch (operation.kind) {
2494
+ case "text":
2495
+ return simulateText(operation, virtual);
2496
+ case "create":
2497
+ return simulateCreate(operation, virtual);
2498
+ case "rename":
2499
+ return simulateRename(operation, virtual);
2500
+ case "delete":
2501
+ return simulateDelete(operation, virtual);
2502
+ }
2503
+ }
2504
+ function rejectSymbolicLink(operation) {
2505
+ if (operation.followedSymbolicLink) {
2506
+ throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
2507
+ }
2508
+ }
2509
+ function simulateText(operation, virtual) {
2510
+ const entry = virtual.get(operation.path);
2511
+ if (entry?.kind !== "file")
2512
+ throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
2513
+ const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
2514
+ virtual.set(operation.path, { kind: "file", content: normalized.text });
2515
+ return {
2516
+ kind: "text",
2517
+ changeIndex: operation.changeIndex,
2518
+ path: operation.path,
2519
+ beforeText: entry.content,
2520
+ afterText: normalized.text,
2521
+ editCount: normalized.edits.length,
2522
+ documentVersion: operation.version
2523
+ };
2524
+ }
2525
+ function simulateCreate(operation, virtual) {
2526
+ rejectSymbolicLink(operation);
2527
+ requireVirtualParent(virtual, operation.path, operation.changeIndex);
2528
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2529
+ if (target.kind !== "missing") {
2530
+ if (operation.overwrite && target.kind === "file") {
2531
+ virtual.set(operation.path, { kind: "file", content: "" });
2532
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
2533
+ }
2534
+ if (operation.ignoreIfExists)
2535
+ return { kind: "noop", changeIndex: operation.changeIndex };
2536
+ throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
2537
+ }
2538
+ virtual.set(operation.path, { kind: "file", content: "" });
2539
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
2540
+ }
2541
+ function simulateRename(operation, virtual) {
2542
+ rejectSymbolicLink(operation);
2543
+ const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
2544
+ if (source.kind === "missing") {
2545
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
2546
+ }
2547
+ if (operation.oldPath === operation.newPath)
2548
+ return { kind: "noop", changeIndex: operation.changeIndex };
2549
+ if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
2550
+ throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
2551
+ }
2552
+ requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
2553
+ const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
2554
+ if (destination.kind !== "missing" && !operation.overwrite) {
2555
+ if (operation.ignoreIfExists)
2556
+ return { kind: "noop", changeIndex: operation.changeIndex };
2557
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
2558
+ }
2559
+ moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
2560
+ return {
2561
+ kind: "rename",
2562
+ changeIndex: operation.changeIndex,
2563
+ oldPath: operation.oldPath,
2564
+ newPath: operation.newPath,
2565
+ sourceKind: source.kind,
2566
+ replaceDestination: destination.kind !== "missing"
2567
+ };
2568
+ }
2569
+ function simulateDelete(operation, virtual) {
2570
+ rejectSymbolicLink(operation);
2571
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2572
+ if (target.kind === "missing") {
2573
+ if (operation.ignoreIfNotExists)
2574
+ return { kind: "noop", changeIndex: operation.changeIndex };
2575
+ throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
2576
+ }
2577
+ if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
2578
+ throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
2579
+ }
2580
+ removeVirtualSubtree(virtual, operation.path);
2581
+ return {
2582
+ kind: "delete",
2583
+ changeIndex: operation.changeIndex,
2584
+ path: operation.path,
2585
+ targetKind: target.kind,
2586
+ recursive: operation.recursive
2587
+ };
2588
+ }
2589
+
2590
+ // ../lsp-core/src/lsp/workspace-edit-snapshot.ts
2591
+ import { existsSync as existsSync5, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
2592
+ import { dirname as dirname4, resolve as resolve5 } from "node:path";
2593
+ class WorkspaceSnapshotBuilder {
2594
+ workspaceRoot;
2595
+ snapshots = new Map;
2596
+ constructor(workspaceRoot) {
2597
+ this.workspaceRoot = workspaceRoot;
2598
+ }
2599
+ build(operations) {
2600
+ this.add(this.workspaceRoot, false);
2601
+ for (const operation of operations) {
2602
+ switch (operation.kind) {
2603
+ case "rename":
2604
+ this.add(operation.oldPath, true);
2605
+ this.add(operation.newPath, true);
2606
+ break;
2607
+ case "delete":
2608
+ this.add(operation.path, true);
2609
+ break;
2610
+ case "text":
2611
+ case "create":
2612
+ this.add(operation.path, false);
2613
+ break;
2614
+ }
2615
+ }
2616
+ return this.snapshots;
2617
+ }
2618
+ add(path, includeChildren) {
2619
+ let candidate = path;
2620
+ while (true) {
2621
+ const existing = this.snapshots.get(candidate);
2622
+ if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
2623
+ this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
2624
+ }
2625
+ if (candidate === this.workspaceRoot)
2626
+ break;
2627
+ candidate = dirname4(candidate);
2628
+ }
2629
+ if (!includeChildren || !existsSync5(path) || !lstatSync3(path).isDirectory())
2630
+ return;
2631
+ for (const child of readdirSync2(path))
2632
+ this.add(resolve5(path, child), true);
2633
+ }
2634
+ }
2635
+ function snapshotOperations(operations, workspaceRoot) {
2636
+ return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
2637
+ }
2638
+
2639
+ // ../lsp-core/src/lsp/workspace-edit-plan.ts
2640
+ class PlanPathIndex {
2641
+ firstChangeByPath = new Map;
2642
+ reportedPathByCanonical = new Map;
2643
+ build(operations) {
2644
+ for (const operation of operations) {
2645
+ switch (operation.kind) {
2646
+ case "rename":
2647
+ this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
2648
+ this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
2649
+ break;
2650
+ case "text":
2651
+ case "create":
2652
+ case "delete":
2653
+ this.add(operation.path, operation.reportedPath, operation.changeIndex);
2654
+ break;
2655
+ }
2656
+ }
2657
+ }
2658
+ add(path, reportedPath2, changeIndex) {
2659
+ if (!this.firstChangeByPath.has(path))
2660
+ this.firstChangeByPath.set(path, changeIndex);
2661
+ if (!this.reportedPathByCanonical.has(path))
2662
+ this.reportedPathByCanonical.set(path, reportedPath2);
2663
+ }
2664
+ }
2665
+ function fingerprintWorkspaceEdit(edit, workspaceRoot) {
2666
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2667
+ if (!root.success)
2668
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2669
+ const parsed = parseWorkspaceEdit(edit, root.path);
2670
+ if (parsed.failures.length > 0)
2671
+ return { success: false, result: failureResult(parsed.failures) };
2672
+ return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
2673
+ }
2674
+ function planWorkspaceEdit(edit, workspaceRoot) {
2675
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2676
+ if (!root.success)
2677
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2678
+ const parsed = parseWorkspaceEdit(edit, root.path);
2679
+ if (parsed.failures.length > 0)
2680
+ return { success: false, result: failureResult(parsed.failures) };
2681
+ let snapshots;
2682
+ try {
2683
+ snapshots = snapshotOperations(parsed.operations, root.path);
2684
+ } catch (error) {
2685
+ return {
2686
+ success: false,
2687
+ result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
2688
+ };
2689
+ }
2690
+ const simulated = simulateOperations(parsed.operations, snapshots);
2691
+ if (simulated.failures.length > 0)
2692
+ return { success: false, result: failureResult(simulated.failures) };
2693
+ const paths = new PlanPathIndex;
2694
+ paths.build(parsed.operations);
2695
+ const plan = {
2696
+ workspaceRoot: root.path,
2697
+ operations: simulated.operations,
2698
+ snapshots,
2699
+ firstChangeByPath: paths.firstChangeByPath,
2700
+ reportedPathByCanonical: paths.reportedPathByCanonical,
2701
+ fingerprint: canonicalFingerprint(parsed.operations)
2702
+ };
2703
+ return { success: true, plan };
2704
+ }
2705
+
2706
+ // ../lsp-core/src/lsp/workspace-mutation-controller.ts
2707
+ function failure(message, failedChange, base) {
2708
+ return {
2709
+ success: false,
2710
+ filesModified: base?.filesModified ?? [],
2711
+ totalEdits: base?.totalEdits ?? 0,
2712
+ errors: [message],
2713
+ ...failedChange === undefined ? {} : { failedChange },
2714
+ ...base?.lateAbort ? { lateAbort: true } : {}
2715
+ };
2716
+ }
2717
+ function responseFor(result) {
2718
+ if (result.success)
2719
+ return { applied: true };
2720
+ return {
2721
+ applied: false,
2722
+ failureReason: result.errors[0] ?? "workspace edit failed",
2723
+ ...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
2724
+ };
2725
+ }
2726
+ function isRecord4(value) {
2727
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2728
+ }
2729
+
2730
+ class WorkspaceMutationController {
2731
+ workspaceRoot;
2732
+ documents;
2733
+ activeLease = null;
2734
+ nextLeaseId = 1;
2735
+ io;
2736
+ constructor(workspaceRoot, documents) {
2737
+ this.workspaceRoot = workspaceRoot;
2738
+ this.documents = documents;
2739
+ }
2740
+ setIo(io) {
2741
+ this.io = io;
2742
+ }
2743
+ acquire(signal) {
2744
+ if (this.activeLease)
2745
+ return { success: false, result: failure("workspace mutation is already in progress") };
2746
+ if (signal?.aborted)
2747
+ return { success: false, result: failure("cancelled before mutating request") };
2748
+ const lease = {
2749
+ id: this.nextLeaseId,
2750
+ phase: "idle",
2751
+ ...signal === undefined ? {} : { signal }
2752
+ };
2753
+ this.nextLeaseId += 1;
2754
+ this.activeLease = lease;
2755
+ return { success: true, lease };
2756
+ }
2757
+ release(lease) {
2758
+ if (this.activeLease?.id !== lease.id)
2759
+ return;
2760
+ this.activeLease.phase = "sealed";
2761
+ this.activeLease = null;
2762
+ }
2763
+ isBeforeCommit(lease) {
2764
+ return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
2765
+ }
2766
+ async handleApplyEdit(params) {
2767
+ const lease = this.activeLease;
2768
+ if (!lease)
2769
+ return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
2770
+ if (lease.phase !== "idle") {
2771
+ return {
2772
+ applied: false,
2773
+ failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
2774
+ };
2775
+ }
2776
+ lease.phase = "applying";
2777
+ lease.applyCompletion = new Promise((resolve6) => {
2778
+ lease.resolveApply = resolve6;
2779
+ });
2780
+ const edit = isRecord4(params) ? params["edit"] : undefined;
2781
+ const record2 = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
2782
+ lease.serverApply = record2;
2783
+ lease.phase = "settled";
2784
+ lease.resolveApply?.();
2785
+ return responseFor(record2.result);
2786
+ }
2787
+ async reconcileRename(leaseToken, edit) {
2788
+ const lease = this.requireActiveLease(leaseToken);
2789
+ if (!lease)
2790
+ return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
2791
+ if (lease.phase === "applying")
2792
+ await lease.applyCompletion;
2793
+ if (lease.serverApply)
2794
+ return this.reconcileServerApply(lease.serverApply, edit);
2795
+ lease.phase = "sealed";
2796
+ if (!edit)
2797
+ return { edit, apply: failure("No edit provided") };
2798
+ const applied = await this.applyEdit(edit, lease);
2799
+ return { edit, apply: applied.result };
2800
+ }
2801
+ reconcileServerApply(record2, edit) {
2802
+ if (!edit)
2803
+ return { edit, apply: record2.result };
2804
+ const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
2805
+ if (fingerprint.success && record2.fingerprint !== null && fingerprint.fingerprint === record2.fingerprint) {
2806
+ return { edit, apply: record2.result };
2807
+ }
2808
+ return {
2809
+ edit,
2810
+ apply: failure("rename result conflicts with server-applied workspace edit", 0, record2.result)
2811
+ };
2812
+ }
2813
+ async applyEdit(edit, lease) {
2814
+ const planned = planWorkspaceEdit(edit, this.workspaceRoot);
2815
+ if (!planned.success)
2816
+ return { fingerprint: null, result: planned.result };
2817
+ const versionFailure = this.documents.validateVersions(planned.plan.operations);
2818
+ if (versionFailure) {
2819
+ return {
2820
+ fingerprint: planned.plan.fingerprint,
2821
+ result: failure(versionFailure.message, versionFailure.changeIndex)
2822
+ };
2823
+ }
2824
+ const commit = commitWorkspaceEditPlan(planned.plan, {
2825
+ ...lease.signal === undefined ? {} : { signal: lease.signal },
2826
+ ...this.io === undefined ? {} : { io: this.io }
2827
+ });
2828
+ let result = commit.result;
2829
+ if (commit.delta.operations.length > 0) {
2830
+ try {
2831
+ await this.documents.synchronize(commit.delta);
2832
+ } catch (error) {
2833
+ const message = error instanceof Error ? error.message : String(error);
2834
+ result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
2835
+ }
2836
+ }
2837
+ if (lease.signal?.aborted && !result.lateAbort)
2838
+ result = { ...result, lateAbort: true };
2839
+ return { fingerprint: planned.plan.fingerprint, result };
2840
+ }
2841
+ requireActiveLease(lease) {
2842
+ return this.activeLease?.id === lease.id ? this.activeLease : null;
2843
+ }
2844
+ }
2845
+
1203
2846
  // ../lsp-core/src/lsp/client.ts
1204
- var POST_OPEN_DELAY_MS = 1000;
1205
- var POST_DIAGNOSTICS_WAIT_MS = 500;
2847
+ var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
2848
+ var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
1206
2849
 
1207
2850
  class LspClient extends LspClientConnection {
1208
- openedFiles = new Set;
1209
- documentVersions = new Map;
1210
- lastSyncedText = new Map;
1211
2851
  diagnosticPullErrors = [];
2852
+ documents;
2853
+ workspaceMutations;
2854
+ diagnosticsFreshnessTimeoutMs;
2855
+ constructor(root, server2, options = {}) {
2856
+ super(root, server2, options);
2857
+ this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
2858
+ this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
2859
+ versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
2860
+ });
2861
+ this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
2862
+ this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
2863
+ }
1212
2864
  getDiagnosticPullErrors() {
1213
2865
  return this.diagnosticPullErrors;
1214
2866
  }
1215
2867
  async openFile(filePath) {
1216
- const absPath = resolve(contextCwd(), filePath);
1217
- const uri = pathToFileURL2(absPath).href;
1218
- const text = readFileSync(absPath, "utf-8");
1219
- if (!this.openedFiles.has(absPath)) {
1220
- const ext = effectiveExtension(absPath);
1221
- const languageId = getLanguageId(ext);
1222
- const version = 1;
1223
- await this.sendNotification("textDocument/didOpen", {
1224
- textDocument: {
1225
- uri,
1226
- languageId,
1227
- version,
1228
- text
1229
- }
1230
- });
1231
- this.openedFiles.add(absPath);
1232
- this.documentVersions.set(uri, version);
1233
- this.lastSyncedText.set(uri, text);
1234
- await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
1235
- return;
1236
- }
1237
- const prevText = this.lastSyncedText.get(uri);
1238
- if (prevText === text) {
1239
- return;
1240
- }
1241
- const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
1242
- this.documentVersions.set(uri, nextVersion);
1243
- this.lastSyncedText.set(uri, text);
1244
- await this.sendNotification("textDocument/didChange", {
1245
- textDocument: { uri, version: nextVersion },
1246
- contentChanges: [{ text }]
1247
- });
1248
- await this.sendNotification("textDocument/didSave", {
1249
- textDocument: { uri },
1250
- text
1251
- });
2868
+ const absPath = this.resolveWorkspacePath(filePath);
2869
+ await this.documents.openFile(absPath);
2870
+ }
2871
+ getOpenDocumentVersion(filePath) {
2872
+ return this.documents.getVersion(this.resolveWorkspacePath(filePath));
2873
+ }
2874
+ getStoredDiagnostics(uri) {
2875
+ return [...this.documents.getStoredDiagnostics(uri)];
2876
+ }
2877
+ setWorkspaceEditIo(io) {
2878
+ this.workspaceMutations.setIo(io);
1252
2879
  }
1253
- async definition(filePath, line, character) {
1254
- const absPath = resolve(contextCwd(), filePath);
2880
+ handlePublishDiagnostics(params) {
2881
+ super.handlePublishDiagnostics(params);
2882
+ this.documents.recordPublishedDiagnostics(params);
2883
+ }
2884
+ async definition(filePath, line, character, signal) {
2885
+ const absPath = this.resolveWorkspacePath(filePath);
1255
2886
  await this.openFile(absPath);
2887
+ const options = signal === undefined ? {} : { signal };
1256
2888
  return this.sendRequest("textDocument/definition", {
1257
- textDocument: { uri: pathToFileURL2(absPath).href },
2889
+ textDocument: { uri: pathToFileURL3(absPath).href },
1258
2890
  position: { line: line - 1, character }
1259
- });
2891
+ }, options);
1260
2892
  }
1261
- async references(filePath, line, character, includeDeclaration = true) {
1262
- const absPath = resolve(contextCwd(), filePath);
2893
+ async references(filePath, line, character, includeDeclaration = true, signal) {
2894
+ const absPath = this.resolveWorkspacePath(filePath);
1263
2895
  await this.openFile(absPath);
2896
+ const options = signal === undefined ? {} : { signal };
1264
2897
  return this.sendRequest("textDocument/references", {
1265
- textDocument: { uri: pathToFileURL2(absPath).href },
2898
+ textDocument: { uri: pathToFileURL3(absPath).href },
1266
2899
  position: { line: line - 1, character },
1267
2900
  context: { includeDeclaration }
1268
- });
2901
+ }, options);
1269
2902
  }
1270
- async documentSymbols(filePath) {
1271
- const absPath = resolve(contextCwd(), filePath);
2903
+ async documentSymbols(filePath, signal) {
2904
+ const absPath = this.resolveWorkspacePath(filePath);
1272
2905
  await this.openFile(absPath);
2906
+ const options = signal === undefined ? {} : { signal };
1273
2907
  return this.sendRequest("textDocument/documentSymbol", {
1274
- textDocument: { uri: pathToFileURL2(absPath).href }
1275
- });
2908
+ textDocument: { uri: pathToFileURL3(absPath).href }
2909
+ }, options);
1276
2910
  }
1277
- async workspaceSymbols(query) {
1278
- return this.sendRequest("workspace/symbol", { query });
2911
+ async workspaceSymbols(query, signal) {
2912
+ const options = signal === undefined ? {} : { signal };
2913
+ return this.sendRequest("workspace/symbol", { query }, options);
1279
2914
  }
1280
2915
  isUnsupportedDiagnosticPullError(error) {
1281
2916
  if (!(error instanceof Error))
@@ -1285,42 +2920,173 @@ class LspClient extends LspClientConnection {
1285
2920
  return true;
1286
2921
  return /unsupported|not supported|method not found|unknown request/i.test(error.message);
1287
2922
  }
1288
- async diagnostics(filePath) {
1289
- const absPath = resolve(contextCwd(), filePath);
1290
- const uri = pathToFileURL2(absPath).href;
1291
- await this.openFile(absPath);
1292
- await new Promise((r) => setTimeout(r, POST_DIAGNOSTICS_WAIT_MS));
1293
- try {
1294
- const result = await this.sendRequest("textDocument/diagnostic", {
1295
- textDocument: { uri }
1296
- });
1297
- if (result.items) {
1298
- return { items: result.items };
2923
+ freshnessTimeout(absPath) {
2924
+ return {
2925
+ items: [],
2926
+ transientError: {
2927
+ kind: "freshness_timeout",
2928
+ message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
1299
2929
  }
1300
- } catch (error) {
1301
- if (!this.isUnsupportedDiagnosticPullError(error)) {
1302
- this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
2930
+ };
2931
+ }
2932
+ parseDiagnosticPullReport(value) {
2933
+ if (value.kind === "unchanged") {
2934
+ return {
2935
+ type: "unchanged",
2936
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
2937
+ };
2938
+ }
2939
+ return {
2940
+ type: "full",
2941
+ diagnostics: value.items ?? [],
2942
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
2943
+ };
2944
+ }
2945
+ async diagnostics(filePath, signal) {
2946
+ signal?.throwIfAborted();
2947
+ const absPath = this.resolveWorkspacePath(filePath);
2948
+ const uri = pathToFileURL3(absPath).href;
2949
+ await this.openFile(absPath);
2950
+ const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
2951
+ for (;; ) {
2952
+ signal?.throwIfAborted();
2953
+ const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
2954
+ if (!snapshot)
2955
+ return this.freshnessTimeout(absPath);
2956
+ const push = this.documents.resolvePushDiagnostics(snapshot);
2957
+ if (push.status === "ready")
2958
+ return { items: [...push.diagnostics] };
2959
+ let pushFallbackOnly = !this.isDiagnosticPullSupported();
2960
+ if (!pushFallbackOnly) {
2961
+ const cached = this.documents.getPullCache(snapshot);
2962
+ try {
2963
+ const remainingMs2 = deadlineAt - Date.now();
2964
+ if (remainingMs2 <= 0)
2965
+ return this.freshnessTimeout(absPath);
2966
+ const result = await this.sendRequest("textDocument/diagnostic", {
2967
+ textDocument: { uri },
2968
+ ...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
2969
+ }, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
2970
+ if (!this.documents.isCurrentSnapshot(snapshot))
2971
+ continue;
2972
+ const report = this.parseDiagnosticPullReport(result);
2973
+ if (report.type === "full") {
2974
+ this.documents.recordPullDiagnostics(snapshot, {
2975
+ kind: "full",
2976
+ diagnostics: report.diagnostics,
2977
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
2978
+ });
2979
+ return { items: [...report.diagnostics] };
2980
+ }
2981
+ if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
2982
+ return { items: [...cached.diagnostics] };
2983
+ }
2984
+ } catch (error) {
2985
+ if (this.isUnsupportedDiagnosticPullError(error)) {
2986
+ this.setDiagnosticPullSupported(false);
2987
+ pushFallbackOnly = true;
2988
+ } else if (error instanceof LspRequestTimeoutError) {
2989
+ pushFallbackOnly = true;
2990
+ } else {
2991
+ this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
2992
+ throw error;
2993
+ }
2994
+ }
1303
2995
  }
2996
+ if (!pushFallbackOnly)
2997
+ continue;
2998
+ const remainingMs = deadlineAt - Date.now();
2999
+ if (remainingMs <= 0)
3000
+ return this.freshnessTimeout(absPath);
3001
+ const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
3002
+ await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
1304
3003
  }
1305
- return { items: this.getStoredDiagnostics(uri) };
1306
3004
  }
1307
- async prepareRename(filePath, line, character) {
1308
- const absPath = resolve(contextCwd(), filePath);
3005
+ async prepareRename(filePath, line, character, signal) {
3006
+ const absPath = this.resolveWorkspacePath(filePath);
1309
3007
  await this.openFile(absPath);
3008
+ const options = signal === undefined ? {} : { signal };
1310
3009
  return this.sendRequest("textDocument/prepareRename", {
1311
- textDocument: { uri: pathToFileURL2(absPath).href },
3010
+ textDocument: { uri: pathToFileURL3(absPath).href },
1312
3011
  position: { line: line - 1, character }
1313
- });
3012
+ }, options);
1314
3013
  }
1315
- async rename(filePath, line, character, newName) {
1316
- const absPath = resolve(contextCwd(), filePath);
3014
+ async rename(filePath, line, character, newName, signal) {
3015
+ const absPath = this.resolveWorkspacePath(filePath);
1317
3016
  await this.openFile(absPath);
1318
- return this.sendRequest("textDocument/rename", {
1319
- textDocument: { uri: pathToFileURL2(absPath).href },
1320
- position: { line: line - 1, character },
1321
- newName
1322
- });
3017
+ const acquired = this.workspaceMutations.acquire(signal);
3018
+ if (!acquired.success)
3019
+ return { edit: null, apply: acquired.result };
3020
+ const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
3021
+ try {
3022
+ const renameParams = {
3023
+ textDocument: { uri: pathToFileURL3(absPath).href },
3024
+ position: { line: line - 1, character },
3025
+ newName
3026
+ };
3027
+ const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
3028
+ signal: preCommitSignal.signal
3029
+ });
3030
+ return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
3031
+ } finally {
3032
+ preCommitSignal?.dispose();
3033
+ this.workspaceMutations.release(acquired.lease);
3034
+ }
1323
3035
  }
3036
+ resolveWorkspacePath(filePath) {
3037
+ return resolve6(this.root, filePath);
3038
+ }
3039
+ }
3040
+ function waitForDiagnosticsActivity(wait, signal) {
3041
+ if (!signal)
3042
+ return wait;
3043
+ if (signal.aborted)
3044
+ return Promise.reject(abortError2(signal));
3045
+ return new Promise((resolve7, reject) => {
3046
+ const onAbort = () => {
3047
+ signal.removeEventListener("abort", onAbort);
3048
+ reject(abortError2(signal));
3049
+ };
3050
+ signal.addEventListener("abort", onAbort, { once: true });
3051
+ wait.then(() => {
3052
+ signal.removeEventListener("abort", onAbort);
3053
+ resolve7();
3054
+ }, (error) => {
3055
+ signal.removeEventListener("abort", onAbort);
3056
+ reject(error);
3057
+ });
3058
+ });
3059
+ }
3060
+ function createPreCommitAbortSignal(source, isBeforeCommit) {
3061
+ if (!source)
3062
+ return;
3063
+ const controller = new AbortController;
3064
+ const onAbort = () => {
3065
+ if (isBeforeCommit() && !controller.signal.aborted)
3066
+ controller.abort(preCommitAbortReason(source));
3067
+ };
3068
+ if (source.aborted)
3069
+ onAbort();
3070
+ else
3071
+ source.addEventListener("abort", onAbort, { once: true });
3072
+ return {
3073
+ signal: controller.signal,
3074
+ dispose: () => source.removeEventListener("abort", onAbort)
3075
+ };
3076
+ }
3077
+ function preCommitAbortReason(source) {
3078
+ const reason = source.reason;
3079
+ if (reason instanceof Error && reason.name !== "AbortError")
3080
+ return reason;
3081
+ return new Error("LSP request cancelled before workspace edit commit");
3082
+ }
3083
+ function abortError2(signal) {
3084
+ const reason = signal.reason;
3085
+ if (reason instanceof Error)
3086
+ return reason;
3087
+ const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
3088
+ error.name = "AbortError";
3089
+ return error;
1324
3090
  }
1325
3091
 
1326
3092
  // ../lsp-core/src/lsp/process-signal-cleanup.ts
@@ -1352,7 +3118,7 @@ async function stopClientBestEffort(client) {
1352
3118
  function awaitWithSignal(promise, signal) {
1353
3119
  if (!signal)
1354
3120
  return promise;
1355
- return new Promise((resolve2, reject) => {
3121
+ return new Promise((resolve7, reject) => {
1356
3122
  let settled = false;
1357
3123
  const onAbort = () => {
1358
3124
  if (settled)
@@ -1370,7 +3136,7 @@ function awaitWithSignal(promise, signal) {
1370
3136
  return;
1371
3137
  settled = true;
1372
3138
  signal.removeEventListener("abort", onAbort);
1373
- resolve2(value);
3139
+ resolve7(value);
1374
3140
  }, (err) => {
1375
3141
  if (settled)
1376
3142
  return;
@@ -1624,21 +3390,17 @@ async function disposeDefaultLspManager() {
1624
3390
  }
1625
3391
 
1626
3392
  // ../lsp-core/src/lsp/server-install-state.ts
1627
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
1628
- import { homedir } from "node:os";
1629
- import { dirname, isAbsolute, join as join2 } from "node:path";
3393
+ import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
3394
+ import { dirname as dirname5 } from "node:path";
1630
3395
  function getInstallDecisionsPath() {
1631
- const override = contextEnv("LSP_TOOLS_MCP_INSTALL_DECISIONS");
1632
- if (!override)
1633
- return join2(homedir(), ".codex", "lsp-install-decisions.json");
1634
- return isAbsolute(override) ? override : join2(homedir(), override);
3396
+ return lspRequestContext().installDecisionsPath;
1635
3397
  }
1636
3398
  function loadInstallDecisions() {
1637
3399
  const path = getInstallDecisionsPath();
1638
- if (!existsSync2(path))
3400
+ if (!existsSync6(path))
1639
3401
  return {};
1640
3402
  try {
1641
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
3403
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
1642
3404
  return isInstallDecisions(parsed) ? parsed : {};
1643
3405
  } catch {
1644
3406
  return {};
@@ -1657,28 +3419,26 @@ function isInstallDecision(value) {
1657
3419
  }
1658
3420
  function writeInstallDecisions(decisions) {
1659
3421
  const path = getInstallDecisionsPath();
1660
- mkdirSync(dirname(path), { recursive: true });
3422
+ mkdirSync(dirname5(path), { recursive: true });
1661
3423
  const tmpPath = `${path}.tmp`;
1662
- writeFileSync(tmpPath, `${JSON.stringify(decisions, null, 2)}
3424
+ writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
1663
3425
  `, "utf8");
1664
- renameSync(tmpPath, path);
3426
+ renameSync2(tmpPath, path);
1665
3427
  }
1666
3428
  function isInstallDecisions(value) {
1667
- return isRecord2(value) && Object.values(value).every(isInstallDecisionRecord);
3429
+ return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
1668
3430
  }
1669
3431
  function isInstallDecisionRecord(value) {
1670
- if (!isRecord2(value))
3432
+ if (!isRecord5(value))
1671
3433
  return false;
1672
3434
  return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
1673
3435
  }
1674
- function isRecord2(value) {
3436
+ function isRecord5(value) {
1675
3437
  return typeof value === "object" && value !== null && !Array.isArray(value);
1676
3438
  }
1677
3439
 
1678
3440
  // ../lsp-core/src/lsp/config-loader.ts
1679
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
1680
- import { homedir as homedir2 } from "node:os";
1681
- import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
3441
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
1682
3442
 
1683
3443
  // ../lsp-core/src/lsp/server-definitions.ts
1684
3444
  var LSP_INSTALL_HINTS = {
@@ -1828,27 +3588,17 @@ var BUILTIN_SERVERS = {
1828
3588
  };
1829
3589
 
1830
3590
  // ../lsp-core/src/lsp/config-loader.ts
1831
- function resolveProjectConfigPath(path) {
1832
- return isAbsolute2(path) ? path : join3(contextCwd(), path);
1833
- }
1834
3591
  function getProjectConfigPaths() {
1835
- const projectOverride = contextEnv("LSP_TOOLS_MCP_PROJECT_CONFIG");
1836
- if (projectOverride) {
1837
- return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
1838
- }
1839
- return [join3(contextCwd(), ".codex", "lsp-client.json")];
3592
+ return lspRequestContext().projectConfigPaths;
1840
3593
  }
1841
3594
  function getUserConfigPath() {
1842
- const userOverride = contextEnv("LSP_TOOLS_MCP_USER_CONFIG");
1843
- if (!userOverride)
1844
- return join3(homedir2(), ".codex", "lsp-client.json");
1845
- return isAbsolute2(userOverride) ? userOverride : join3(homedir2(), userOverride);
3595
+ return lspRequestContext().userConfigPath;
1846
3596
  }
1847
3597
  function loadJsonFile(path) {
1848
- if (!existsSync3(path))
3598
+ if (!existsSync7(path))
1849
3599
  return null;
1850
3600
  try {
1851
- const parsed = JSON.parse(readFileSync3(path, "utf-8"));
3601
+ const parsed = JSON.parse(readFileSync4(path, "utf-8"));
1852
3602
  return isConfigJson(parsed) ? parsed : null;
1853
3603
  } catch {
1854
3604
  return null;
@@ -1987,16 +3737,16 @@ function applyOptionalServerFields(server2, entry) {
1987
3737
  }
1988
3738
  }
1989
3739
  function isConfigJson(value) {
1990
- if (!isRecord3(value))
3740
+ if (!isRecord6(value))
1991
3741
  return false;
1992
3742
  const lsp = value["lsp"];
1993
- return lsp === undefined || isRecord3(lsp);
3743
+ return lsp === undefined || isRecord6(lsp);
1994
3744
  }
1995
3745
  function parseLspEntry(value) {
1996
3746
  return isLspEntry(value) ? value : null;
1997
3747
  }
1998
3748
  function isLspEntry(value) {
1999
- if (!isRecord3(value))
3749
+ if (!isRecord6(value))
2000
3750
  return false;
2001
3751
  const disabled = value["disabled"];
2002
3752
  const command = value["command"];
@@ -2004,15 +3754,15 @@ function isLspEntry(value) {
2004
3754
  const priority = value["priority"];
2005
3755
  const env = value["env"];
2006
3756
  const initialization = value["initialization"];
2007
- 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));
3757
+ 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));
2008
3758
  }
2009
3759
  function isStringArray(value) {
2010
3760
  return Array.isArray(value) && value.every((item) => typeof item === "string");
2011
3761
  }
2012
3762
  function isStringRecord(value) {
2013
- return isRecord3(value) && Object.values(value).every((item) => typeof item === "string");
3763
+ return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
2014
3764
  }
2015
- function isRecord3(value) {
3765
+ function isRecord6(value) {
2016
3766
  return typeof value === "object" && value !== null && !Array.isArray(value);
2017
3767
  }
2018
3768
  function getDisabledServerIds() {
@@ -2033,8 +3783,8 @@ function getDisabledServerIds() {
2033
3783
  }
2034
3784
 
2035
3785
  // ../lsp-core/src/lsp/server-installation.ts
2036
- import { existsSync as existsSync4 } from "node:fs";
2037
- import { delimiter as delimiter3, join as join4 } from "node:path";
3786
+ import { existsSync as existsSync8 } from "node:fs";
3787
+ import { delimiter as delimiter3, join as join3 } from "node:path";
2038
3788
  function isServerInstalled(command, _workingDirectory) {
2039
3789
  if (command.length === 0)
2040
3790
  return false;
@@ -2042,7 +3792,7 @@ function isServerInstalled(command, _workingDirectory) {
2042
3792
  if (!cmd)
2043
3793
  return false;
2044
3794
  if (cmd.includes("/") || cmd.includes("\\")) {
2045
- if (existsSync4(cmd))
3795
+ if (existsSync8(cmd))
2046
3796
  return true;
2047
3797
  }
2048
3798
  const isWindows = process.platform === "win32";
@@ -2063,7 +3813,7 @@ function isServerInstalled(command, _workingDirectory) {
2063
3813
  const paths = pathEnv.split(delimiter3);
2064
3814
  for (const p of paths) {
2065
3815
  for (const suffix of exts) {
2066
- if (existsSync4(join4(p, cmd + suffix))) {
3816
+ if (existsSync8(join3(p, cmd + suffix))) {
2067
3817
  return true;
2068
3818
  }
2069
3819
  }
@@ -2162,39 +3912,50 @@ function getAllServers() {
2162
3912
  var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
2163
3913
  function isDirectoryPath(filePath) {
2164
3914
  try {
2165
- return statSync2(filePath).isDirectory();
3915
+ return statSync3(filePath).isDirectory();
2166
3916
  } catch {
2167
3917
  return false;
2168
3918
  }
2169
3919
  }
2170
3920
  function findWorkspaceRoot(filePath) {
2171
- const abs = resolve2(contextCwd(), filePath);
3921
+ const abs = resolvePathInsideContext(filePath);
2172
3922
  let dir = abs;
2173
3923
  if (!isDirectoryPath(dir)) {
2174
- dir = dirname2(dir);
3924
+ dir = dirname6(dir);
2175
3925
  }
2176
3926
  let prevDir = "";
2177
3927
  while (dir !== prevDir) {
2178
3928
  for (const marker of WORKSPACE_MARKERS) {
2179
- if (existsSync5(join5(dir, marker))) {
3929
+ if (existsSync9(join4(dir, marker))) {
2180
3930
  return dir;
2181
3931
  }
2182
3932
  }
2183
3933
  prevDir = dir;
2184
- dir = dirname2(dir);
3934
+ dir = dirname6(dir);
3935
+ }
3936
+ return dirname6(abs);
3937
+ }
3938
+ function resolvePathInsideContext(filePath) {
3939
+ const cwd = contextCwd();
3940
+ const abs = resolve7(cwd, filePath);
3941
+ const canonical = canonicalizeExistingOrNearestAncestor(abs);
3942
+ if (!isPathInside(cwd, canonical)) {
3943
+ throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
2185
3944
  }
2186
- return dirname2(abs);
3945
+ return canonical;
2187
3946
  }
2188
3947
  function formatServerLookupError(result) {
2189
3948
  if (result.status === "not_installed") {
2190
3949
  return formatNotInstalled(result);
2191
3950
  }
3951
+ const context = lspRequestContext();
3952
+ const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
2192
3953
  return [
2193
3954
  `No LSP server configured for extension: ${result.extension}`,
2194
3955
  "",
2195
3956
  `Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
2196
3957
  "",
2197
- "Configure a custom server in '.codex/lsp-client.json':",
3958
+ `Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
2198
3959
  " {",
2199
3960
  ' "lsp": {',
2200
3961
  ' "my-server": {',
@@ -2210,6 +3971,7 @@ function formatNotInstalled(result) {
2210
3971
  const { server: server2, installHint } = result;
2211
3972
  const extensions = server2.extensions.join(", ");
2212
3973
  const decision = loadInstallDecision(server2.id)?.decision;
3974
+ const context = lspRequestContext();
2213
3975
  if (decision === "declined") {
2214
3976
  return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
2215
3977
  }
@@ -2225,6 +3987,17 @@ function formatNotInstalled(result) {
2225
3987
  "The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
2226
3988
  ` ${installHint}`
2227
3989
  ].join(`
3990
+ `);
3991
+ }
3992
+ if (!context.capabilities.installDecisionTool) {
3993
+ return [
3994
+ ...header,
3995
+ "To install, run:",
3996
+ ` ${installHint}`,
3997
+ "",
3998
+ "ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
3999
+ "Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
4000
+ ].join(`
2228
4001
  `);
2229
4002
  }
2230
4003
  return [
@@ -2236,6 +4009,7 @@ function formatNotInstalled(result) {
2236
4009
  "- If the user agrees: run the install command above, then retry this tool.",
2237
4010
  "- If the user declines, OR has NOT explicitly asked for LSP installation:",
2238
4011
  ` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
4012
+ ` which writes to ${context.installDecisionsPath},`,
2239
4013
  " then ignore this message and proceed WITHOUT LSP."
2240
4014
  ].join(`
2241
4015
  `);
@@ -2249,14 +4023,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
2249
4023
  "prepareRename"
2250
4024
  ]);
2251
4025
  async function withLspClient(filePath, fn, toolName, options = {}) {
2252
- const absPath = resolve2(contextCwd(), filePath);
4026
+ const absPath = resolvePathInsideContext(filePath);
2253
4027
  if (isDirectoryPath(absPath)) {
2254
4028
  throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
2255
4029
  }
2256
4030
  const ext = effectiveExtension(absPath);
2257
4031
  const result = findServerForExtension(ext);
2258
4032
  if (result.status !== "found") {
2259
- throw new LspServerLookupError(formatServerLookupError(result));
4033
+ throw new LspServerLookupError(formatServerLookupError(result), result);
2260
4034
  }
2261
4035
  const server2 = result.server;
2262
4036
  const root = findWorkspaceRoot(absPath);
@@ -2284,11 +4058,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
2284
4058
  }
2285
4059
 
2286
4060
  // ../lsp-core/src/lsp/directory-diagnostics.ts
2287
- import { existsSync as existsSync6, lstatSync, readdirSync } from "node:fs";
2288
- import { join as join6, resolve as resolve3 } from "node:path";
4061
+ import { existsSync as existsSync10, lstatSync as lstatSync4, readdirSync as readdirSync3 } from "node:fs";
4062
+ import { join as join5, resolve as resolve8 } from "node:path";
2289
4063
 
2290
4064
  // ../lsp-core/src/lsp/formatters.ts
2291
- import { fileURLToPath } from "node:url";
4065
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2292
4066
  var DIAGNOSTIC_SEVERITY_FILTERS = {
2293
4067
  error: 1,
2294
4068
  warning: 2,
@@ -2296,7 +4070,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
2296
4070
  hint: 4
2297
4071
  };
2298
4072
  function uriToPath(uri) {
2299
- return fileURLToPath(uri);
4073
+ return fileURLToPath2(uri);
2300
4074
  }
2301
4075
  function formatLocation(loc) {
2302
4076
  if ("targetUri" in loc) {
@@ -2382,6 +4156,9 @@ function formatApplyResult(result) {
2382
4156
  for (const file of result.filesModified) {
2383
4157
  lines.push(` - ${file}`);
2384
4158
  }
4159
+ if (result.lateAbort) {
4160
+ lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
4161
+ }
2385
4162
  } else {
2386
4163
  lines.push("Failed to apply some changes:");
2387
4164
  for (const err of result.errors) {
@@ -2397,6 +4174,7 @@ function formatApplyResult(result) {
2397
4174
 
2398
4175
  // ../lsp-core/src/lsp/directory-diagnostics.ts
2399
4176
  var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
4177
+ var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
2400
4178
  function collectFilesWithExtension(dir, extension, maxFiles) {
2401
4179
  const files = [];
2402
4180
  function walk(currentDir) {
@@ -2404,17 +4182,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2404
4182
  return;
2405
4183
  let entries = [];
2406
4184
  try {
2407
- entries = readdirSync(currentDir);
4185
+ entries = readdirSync3(currentDir);
2408
4186
  } catch {
2409
4187
  return;
2410
4188
  }
2411
4189
  for (const entry of entries) {
2412
4190
  if (files.length >= maxFiles)
2413
4191
  return;
2414
- const fullPath = join6(currentDir, entry);
4192
+ const fullPath = join5(currentDir, entry);
2415
4193
  let stat;
2416
4194
  try {
2417
- stat = lstatSync(fullPath);
4195
+ stat = lstatSync4(fullPath);
2418
4196
  } catch {
2419
4197
  continue;
2420
4198
  }
@@ -2432,52 +4210,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2432
4210
  walk(dir);
2433
4211
  return files;
2434
4212
  }
2435
- async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
4213
+ async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
2436
4214
  if (!extension.startsWith(".")) {
2437
4215
  throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
2438
4216
  }
2439
- const absDir = resolve3(contextCwd(), directory);
2440
- if (!existsSync6(absDir)) {
4217
+ const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
4218
+ if (!existsSync10(absDir)) {
2441
4219
  throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
2442
4220
  }
2443
- const serverResult = findServerForExtension(extension);
4221
+ const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
2444
4222
  if (serverResult.status !== "found") {
2445
4223
  throw new LspServerLookupError(formatServerLookupError(serverResult));
2446
4224
  }
2447
4225
  const server2 = serverResult.server;
2448
- const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
4226
+ const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
2449
4227
  const wasCapped = allFiles.length > maxFiles;
2450
4228
  const filesToProcess = allFiles.slice(0, maxFiles);
2451
4229
  if (filesToProcess.length === 0) {
2452
- return [
4230
+ const output = [
2453
4231
  `Directory: ${absDir}`,
2454
4232
  `Extension: ${extension}`,
2455
4233
  "Files scanned: 0",
2456
4234
  `No files found with extension "${extension}".`
2457
4235
  ].join(`
2458
4236
  `);
4237
+ return { output, totalDiagnostics: 0, fileFailures: [] };
2459
4238
  }
2460
- const root = findWorkspaceRoot(absDir);
2461
- const manager = getLspManager();
4239
+ const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
4240
+ const manager = options.manager ?? getLspManager();
2462
4241
  const allDiagnostics = [];
2463
4242
  const fileErrors = [];
4243
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
4244
+ options.signal?.throwIfAborted();
2464
4245
  const client = await manager.getClient(root, server2);
2465
4246
  try {
2466
- for (const file of filesToProcess) {
2467
- try {
2468
- const result = await client.diagnostics(file);
2469
- const filtered = filterDiagnosticsBySeverity(result.items, severity);
2470
- allDiagnostics.push(...filtered.map((diagnostic) => ({
2471
- filePath: file,
2472
- diagnostic
2473
- })));
2474
- } catch (e) {
2475
- fileErrors.push({
2476
- file,
2477
- error: e instanceof Error ? e.message : String(e)
2478
- });
4247
+ let nextIndex = 0;
4248
+ const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
4249
+ for (;; ) {
4250
+ if (options.signal?.aborted)
4251
+ return;
4252
+ const file = filesToProcess[nextIndex];
4253
+ nextIndex += 1;
4254
+ if (file === undefined)
4255
+ return;
4256
+ try {
4257
+ const result = await client.diagnostics(file, options.signal);
4258
+ const filtered = filterDiagnosticsBySeverity(result.items, severity);
4259
+ allDiagnostics.push(...filtered.map((diagnostic) => ({
4260
+ filePath: file,
4261
+ diagnostic
4262
+ })));
4263
+ } catch (e) {
4264
+ fileErrors.push({
4265
+ file,
4266
+ error: e instanceof Error ? e.message : String(e)
4267
+ });
4268
+ }
2479
4269
  }
2480
- }
4270
+ });
4271
+ await Promise.all(workers);
2481
4272
  } finally {
2482
4273
  manager.releaseClient(root, server2.id);
2483
4274
  }
@@ -2505,13 +4296,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
2505
4296
  lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
2506
4297
  }
2507
4298
  }
2508
- return lines.join(`
2509
- `);
4299
+ return { output: lines.join(`
4300
+ `), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
2510
4301
  }
2511
4302
 
2512
4303
  // ../lsp-core/src/lsp/infer-extension.ts
2513
- import { lstatSync as lstatSync2, readdirSync as readdirSync2 } from "node:fs";
2514
- import { join as join7 } from "node:path";
4304
+ import { lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
4305
+ import { join as join6 } from "node:path";
2515
4306
  var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
2516
4307
  var MAX_SCAN_ENTRIES = 500;
2517
4308
  function inferExtensionFromDirectory(directory) {
@@ -2522,17 +4313,17 @@ function inferExtensionFromDirectory(directory) {
2522
4313
  return;
2523
4314
  let entries;
2524
4315
  try {
2525
- entries = readdirSync2(dir);
4316
+ entries = readdirSync4(dir);
2526
4317
  } catch {
2527
4318
  return;
2528
4319
  }
2529
4320
  for (const entry of entries) {
2530
4321
  if (scanned >= MAX_SCAN_ENTRIES)
2531
4322
  return;
2532
- const fullPath = join7(dir, entry);
4323
+ const fullPath = join6(dir, entry);
2533
4324
  let stat;
2534
4325
  try {
2535
- stat = lstatSync2(fullPath);
4326
+ stat = lstatSync5(fullPath);
2536
4327
  } catch {
2537
4328
  continue;
2538
4329
  }
@@ -2606,13 +4397,48 @@ function missingDependencyResult(error, details) {
2606
4397
  details: {
2607
4398
  ...details,
2608
4399
  error: message,
2609
- errorKind: "missing_dependency"
4400
+ errorKind: "missing_dependency",
4401
+ ...availabilityDetails(error)
2610
4402
  }
2611
4403
  };
2612
4404
  }
4405
+ function availabilityDetails(error) {
4406
+ const availability = missingDependencyAvailability(error);
4407
+ return availability === null ? {} : { availability };
4408
+ }
4409
+ function missingDependencyAvailability(error) {
4410
+ if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
4411
+ return null;
4412
+ const context = lspRequestContext();
4413
+ switch (error.lookup.status) {
4414
+ case "not_configured":
4415
+ return {
4416
+ kind: "not_configured",
4417
+ extension: error.lookup.extension,
4418
+ availableServers: [...error.lookup.availableServers],
4419
+ projectConfigPaths: [...context.projectConfigPaths],
4420
+ userConfigPath: context.userConfigPath,
4421
+ installDecisionTool: context.capabilities.installDecisionTool
4422
+ };
4423
+ case "not_installed":
4424
+ return {
4425
+ kind: "not_installed",
4426
+ serverId: error.lookup.server.id,
4427
+ command: [...error.lookup.server.command],
4428
+ extensions: [...error.lookup.server.extensions],
4429
+ installHint: error.lookup.installHint,
4430
+ installDecisionTool: context.capabilities.installDecisionTool,
4431
+ installDecisionsPath: context.installDecisionsPath
4432
+ };
4433
+ default: {
4434
+ const exhaustive = error.lookup;
4435
+ return exhaustive;
4436
+ }
4437
+ }
4438
+ }
2613
4439
 
2614
4440
  // ../lsp-core/src/tools/parameters.ts
2615
- function isRecord4(value) {
4441
+ function isRecord7(value) {
2616
4442
  return typeof value === "object" && value !== null && !Array.isArray(value);
2617
4443
  }
2618
4444
  function requireString(params, key) {
@@ -2669,7 +4495,7 @@ async function executeLspDiagnostics(params, signal) {
2669
4495
  const filePath = requireString(params, "filePath");
2670
4496
  const severity = severityFilter(params);
2671
4497
  try {
2672
- const absPath = resolve4(contextCwd(), filePath);
4498
+ const absPath = resolvePathInsideContext(filePath);
2673
4499
  if (isDirectoryPath(absPath)) {
2674
4500
  const extension = inferExtensionFromDirectory(absPath);
2675
4501
  if (!extension) {
@@ -2686,18 +4512,33 @@ async function executeLspDiagnostics(params, signal) {
2686
4512
  };
2687
4513
  return text(message, details3);
2688
4514
  }
2689
- const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
4515
+ const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
2690
4516
  const details2 = {
2691
4517
  filePath,
2692
4518
  severity,
2693
4519
  mode: "directory",
2694
4520
  diagnostics: [],
4521
+ totalDiagnostics: output2.totalDiagnostics,
4522
+ truncated: false,
4523
+ fileFailures: [...output2.fileFailures]
4524
+ };
4525
+ return text(output2.output, details2);
4526
+ }
4527
+ const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
4528
+ if (result.transientError) {
4529
+ const message = result.transientError.message;
4530
+ const details2 = {
4531
+ filePath,
4532
+ severity,
4533
+ mode: "file",
4534
+ diagnostics: [],
2695
4535
  totalDiagnostics: 0,
2696
- truncated: false
4536
+ truncated: false,
4537
+ error: message,
4538
+ errorKind: result.transientError.kind
2697
4539
  };
2698
- return text(output2, details2);
4540
+ return text(message, details2, true);
2699
4541
  }
2700
- const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
2701
4542
  const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
2702
4543
  const total = diagnostics.length;
2703
4544
  const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
@@ -2758,7 +4599,7 @@ async function executeLspGotoDefinition(params, signal) {
2758
4599
  const line = requireNumber(params, "line");
2759
4600
  const character = requireNumber(params, "character");
2760
4601
  try {
2761
- const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
4602
+ const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
2762
4603
  const locations = !result ? [] : Array.isArray(result) ? result : [result];
2763
4604
  const details = { filePath, line, character, locations };
2764
4605
  if (locations.length === 0)
@@ -2783,7 +4624,7 @@ async function executeLspFindReferences(params, signal) {
2783
4624
  const character = requireNumber(params, "character");
2784
4625
  const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
2785
4626
  try {
2786
- const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
4627
+ const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
2787
4628
  const references = Array.isArray(result) ? result : [];
2788
4629
  const total = references.length;
2789
4630
  const truncated = total > DEFAULT_MAX_REFERENCES;
@@ -2819,181 +4660,13 @@ async function executeLspFindReferences(params, signal) {
2819
4660
  }
2820
4661
  }
2821
4662
 
2822
- // ../lsp-core/src/lsp/workspace-edit.ts
2823
- import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2824
- import { dirname as dirname3, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
2825
- import { fileURLToPath as fileURLToPath2 } from "node:url";
2826
- function errorMessage2(error) {
2827
- return error instanceof Error ? error.message : String(error);
2828
- }
2829
- function isPathInsideWorkspace(filePath, workspaceRoot) {
2830
- const relativePath = relative(workspaceRoot, filePath);
2831
- return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
2832
- }
2833
- function realpathForValidation(filePath) {
2834
- if (existsSync7(filePath))
2835
- return realpathSync(filePath);
2836
- const parent = dirname3(filePath);
2837
- return resolve5(realpathSync(parent), relative(parent, filePath));
2838
- }
2839
- function uriToWorkspacePath(uri, workspaceRoot) {
2840
- let filePath;
2841
- try {
2842
- filePath = fileURLToPath2(uri);
2843
- } catch (error) {
2844
- return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
2845
- }
2846
- let validatedPath;
2847
- try {
2848
- validatedPath = realpathForValidation(filePath);
2849
- } catch (error) {
2850
- return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
2851
- }
2852
- if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
2853
- return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
2854
- }
2855
- return { success: true, path: filePath };
2856
- }
2857
- function applyTextEditsToFile(filePath, edits) {
2858
- try {
2859
- const content = readFileSync4(filePath, "utf-8");
2860
- const lines = content.split(`
2861
- `);
2862
- const sortedEdits = [...edits].sort((a, b) => {
2863
- if (b.range.start.line !== a.range.start.line) {
2864
- return b.range.start.line - a.range.start.line;
2865
- }
2866
- return b.range.start.character - a.range.start.character;
2867
- });
2868
- for (const edit of sortedEdits) {
2869
- const startLine = edit.range.start.line;
2870
- const startChar = edit.range.start.character;
2871
- const endLine = edit.range.end.line;
2872
- const endChar = edit.range.end.character;
2873
- if (startLine === endLine) {
2874
- const line = lines[startLine] ?? "";
2875
- lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
2876
- } else {
2877
- const firstLine = lines[startLine] ?? "";
2878
- const lastLine = lines[endLine] ?? "";
2879
- const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
2880
- lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
2881
- `));
2882
- }
2883
- }
2884
- writeFileSync2(filePath, lines.join(`
2885
- `), "utf-8");
2886
- return { success: true, editCount: edits.length };
2887
- } catch (err) {
2888
- return {
2889
- success: false,
2890
- editCount: 0,
2891
- error: err instanceof Error ? err.message : String(err)
2892
- };
2893
- }
2894
- }
2895
- function applyWorkspaceEdit(edit, options = {}) {
2896
- if (!edit) {
2897
- return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
2898
- }
2899
- const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
2900
- const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
2901
- if (edit.changes) {
2902
- for (const [uri, edits] of Object.entries(edit.changes)) {
2903
- const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
2904
- if (!validatedPath.success) {
2905
- result.success = false;
2906
- result.errors.push(validatedPath.error);
2907
- continue;
2908
- }
2909
- const applyResult = applyTextEditsToFile(validatedPath.path, edits);
2910
- if (applyResult.success) {
2911
- result.filesModified.push(validatedPath.path);
2912
- result.totalEdits += applyResult.editCount;
2913
- } else {
2914
- result.success = false;
2915
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
2916
- }
2917
- }
2918
- }
2919
- if (edit.documentChanges) {
2920
- for (const change of edit.documentChanges) {
2921
- if (!("kind" in change)) {
2922
- const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
2923
- if (!validatedPath.success) {
2924
- result.success = false;
2925
- result.errors.push(validatedPath.error);
2926
- continue;
2927
- }
2928
- const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
2929
- if (applyResult.success) {
2930
- result.filesModified.push(validatedPath.path);
2931
- result.totalEdits += applyResult.editCount;
2932
- } else {
2933
- result.success = false;
2934
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
2935
- }
2936
- continue;
2937
- }
2938
- if (change.kind === "create") {
2939
- try {
2940
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
2941
- if (!validatedPath.success) {
2942
- result.success = false;
2943
- result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
2944
- continue;
2945
- }
2946
- writeFileSync2(validatedPath.path, "", "utf-8");
2947
- result.filesModified.push(validatedPath.path);
2948
- } catch (err) {
2949
- result.success = false;
2950
- result.errors.push(`Create ${change.uri}: ${String(err)}`);
2951
- }
2952
- } else if (change.kind === "rename") {
2953
- try {
2954
- const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
2955
- const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
2956
- if (!oldPath.success || !newPath.success) {
2957
- const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
2958
- result.success = false;
2959
- result.errors.push(`Rename ${change.oldUri}: ${error}`);
2960
- continue;
2961
- }
2962
- const content = readFileSync4(oldPath.path, "utf-8");
2963
- writeFileSync2(newPath.path, content, "utf-8");
2964
- unlinkSync(oldPath.path);
2965
- result.filesModified.push(newPath.path);
2966
- } catch (err) {
2967
- result.success = false;
2968
- result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
2969
- }
2970
- } else if (change.kind === "delete") {
2971
- try {
2972
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
2973
- if (!validatedPath.success) {
2974
- result.success = false;
2975
- result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
2976
- continue;
2977
- }
2978
- unlinkSync(validatedPath.path);
2979
- result.filesModified.push(validatedPath.path);
2980
- } catch (err) {
2981
- result.success = false;
2982
- result.errors.push(`Delete ${change.uri}: ${String(err)}`);
2983
- }
2984
- }
2985
- }
2986
- }
2987
- return result;
2988
- }
2989
-
2990
4663
  // ../lsp-core/src/tools/rename.ts
2991
4664
  async function executeLspPrepareRename(params, signal) {
2992
4665
  const filePath = requireString(params, "filePath");
2993
4666
  const line = requireNumber(params, "line");
2994
4667
  const character = requireNumber(params, "character");
2995
4668
  try {
2996
- const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
4669
+ const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
2997
4670
  const details = { filePath, line, character, result };
2998
4671
  return text(formatPrepareRenameResult(result), details);
2999
4672
  } catch (error) {
@@ -3014,13 +4687,9 @@ async function executeLspRename(params, signal) {
3014
4687
  const character = requireNumber(params, "character");
3015
4688
  const newName = requireString(params, "newName");
3016
4689
  try {
3017
- const edit = await withLspClient(filePath, async (client, workspaceRoot) => ({
3018
- edit: await client.rename(filePath, line, character, newName),
3019
- workspaceRoot
3020
- }), "rename", clientOptions(signal));
3021
- const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
3022
- const details = { filePath, line, character, newName, apply, edit: edit.edit };
3023
- return text(formatApplyResult(apply), details, !apply.success);
4690
+ const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
4691
+ const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
4692
+ return text(formatApplyResult(result.apply), details, !result.apply.success);
3024
4693
  } catch (error) {
3025
4694
  const missingDependency = missingDependencyResult(error, {
3026
4695
  filePath,
@@ -3095,10 +4764,10 @@ async function executeLspSymbols(params, signal) {
3095
4764
  errorKind: "missing_query"
3096
4765
  });
3097
4766
  }
3098
- const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
4767
+ const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
3099
4768
  return formatSymbolsResult(filePath, scope, symbols2, limit, query);
3100
4769
  }
3101
- const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
4770
+ const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
3102
4771
  return formatSymbolsResult(filePath, scope, symbols, limit);
3103
4772
  } catch (error) {
3104
4773
  const query = optionalString(params, "query");
@@ -3264,12 +4933,12 @@ function matchesToolName(tool, name) {
3264
4933
  return tool.name === name || (tool.aliases?.includes(name) ?? false);
3265
4934
  }
3266
4935
  function coerceToolArguments(value) {
3267
- return isRecord4(value) ? value : {};
4936
+ return isRecord7(value) ? value : {};
3268
4937
  }
3269
4938
  // ../lsp-core/src/mcp.ts
3270
4939
  var SERVER_NAME = "lsp";
3271
4940
  var SERVER_VERSION = "0.1.0";
3272
- async function handleLspMcpRequest(input) {
4941
+ async function handleLspMcpRequest(input, options = {}) {
3273
4942
  if (!isPlainRecord(input)) {
3274
4943
  return errorResponse(null, -32600, "Invalid Request");
3275
4944
  }
@@ -3291,33 +4960,37 @@ async function handleLspMcpRequest(input) {
3291
4960
  return successResponse(id, { tools: LSP_MCP_TOOLS.map(describeTool) });
3292
4961
  }
3293
4962
  if (method === "tools/call") {
3294
- return handleToolCall(id, input["params"]);
4963
+ return handleToolCall(id, input["params"], options.signal);
3295
4964
  }
3296
4965
  return errorResponse(id, -32601, `Method not found: ${String(method)}`);
3297
4966
  }
3298
4967
  async function runMcpStdioServer(input = process.stdin, output = process.stdout) {
4968
+ const requestContext = createStandaloneMcpRequestContext();
3299
4969
  await runJsonRpcStdioServer({
3300
4970
  input,
3301
4971
  output,
3302
4972
  idleTimeoutMs: 0,
3303
- handler: handleLspMcpRequest,
4973
+ handler: (request) => runWithRequestContext(requestContext, () => handleLspMcpRequest(request)),
3304
4974
  handlerOptions: undefined
3305
4975
  });
3306
4976
  }
3307
- async function handleToolCall(id, params) {
4977
+ async function handleToolCall(id, params, signal) {
3308
4978
  if (!isPlainRecord(params) || typeof params["name"] !== "string") {
3309
4979
  return errorResponse(id, -32602, "tools/call requires params.name");
3310
4980
  }
3311
4981
  try {
3312
- const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
4982
+ const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
3313
4983
  return successResponse(id, {
3314
4984
  content: result.content,
3315
4985
  isError: result.isError ?? false,
3316
4986
  details: result.details
3317
4987
  });
3318
4988
  } catch (error) {
4989
+ if (!(error instanceof Error)) {
4990
+ throw error;
4991
+ }
3319
4992
  return successResponse(id, {
3320
- content: [{ type: "text", text: messageFromError(error) }],
4993
+ content: [{ type: "text", text: error.message }],
3321
4994
  isError: true
3322
4995
  });
3323
4996
  }