oh-my-opencode 4.18.0 → 4.18.2

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