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
@@ -0,0 +1,52 @@
1
+ import type { Diagnostic } from "./types.js";
2
+
3
+ interface ConfigurationItem {
4
+ readonly section?: string;
5
+ }
6
+
7
+ interface DiagnosticsParams {
8
+ readonly uri: string;
9
+ readonly diagnostics: Diagnostic[];
10
+ readonly version?: number;
11
+ }
12
+
13
+ function isRecord(value: unknown): value is Record<string, unknown> {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+
17
+ export function parseConfigurationItems(params: unknown): ConfigurationItem[] {
18
+ if (!isRecord(params) || !Array.isArray(params["items"])) return [];
19
+ const items: ConfigurationItem[] = [];
20
+ for (const item of params["items"]) {
21
+ if (!isRecord(item)) continue;
22
+ const section = item["section"];
23
+ items.push(section === undefined || typeof section !== "string" ? {} : { section });
24
+ }
25
+ return items;
26
+ }
27
+
28
+ export function parseDiagnosticsParams(params: unknown): DiagnosticsParams | null {
29
+ if (!isRecord(params) || typeof params["uri"] !== "string") return null;
30
+ const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
31
+ const version = typeof params["version"] === "number" ? params["version"] : undefined;
32
+ return { uri: params["uri"], diagnostics, ...(version === undefined ? {} : { version }) };
33
+ }
34
+
35
+ export function createLspSpawnEnv(
36
+ _root: string,
37
+ input: Record<string, string | undefined>,
38
+ ): Record<string, string | undefined> {
39
+ return { ...input };
40
+ }
41
+
42
+ function isDiagnostic(value: unknown): value is Diagnostic {
43
+ return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
44
+ }
45
+
46
+ function isRange(value: unknown): value is Diagnostic["range"] {
47
+ return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
48
+ }
49
+
50
+ function isPosition(value: unknown): value is Diagnostic["range"]["start"] {
51
+ return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
52
+ }
@@ -3,41 +3,28 @@ import { INIT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, STOP_HARD_KILL_TIMEOUT_MS, STOP_SI
3
3
  import { LspConnectionClosedError, LspProcessExitedError, LspRequestTimeoutError } from "./errors.js";
4
4
  import { JsonRpcConnection } from "./json-rpc-connection.js";
5
5
  import { type SpawnedProcess, spawnProcess } from "./process.js";
6
+ import { createLspSpawnEnv, parseConfigurationItems, parseDiagnosticsParams } from "./transport-protocol.js";
6
7
  import type { Diagnostic, ResolvedServer } from "./types.js";
8
+ import type { WorkspaceApplyEditResponse } from "./workspace-mutation-controller.js";
7
9
 
8
- interface ConfigurationItem {
9
- section?: string;
10
- }
11
-
12
- interface DiagnosticsParams {
13
- uri: string;
14
- diagnostics: Diagnostic[];
15
- }
10
+ export { createLspSpawnEnv } from "./transport-protocol.js";
16
11
 
17
- function isRecord(value: unknown): value is Record<string, unknown> {
18
- return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ export interface LspClientTimeoutOptions {
13
+ requestTimeoutMs?: number;
14
+ initializeTimeoutMs?: number;
19
15
  }
20
16
 
21
- function parseConfigurationItems(params: unknown): ConfigurationItem[] {
22
- if (!isRecord(params) || !Array.isArray(params["items"])) return [];
23
- const items: ConfigurationItem[] = [];
24
- for (const item of params["items"]) {
25
- if (!isRecord(item)) continue;
26
- const section = item["section"];
27
- items.push(section === undefined || typeof section !== "string" ? {} : { section });
28
- }
29
- return items;
30
- }
17
+ type WorkspaceApplyEditHandler = (params: unknown) => Promise<WorkspaceApplyEditResponse>;
31
18
 
32
- function parseDiagnosticsParams(params: unknown): DiagnosticsParams | null {
33
- if (!isRecord(params) || typeof params["uri"] !== "string") return null;
34
- const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
35
- return { uri: params["uri"], diagnostics };
36
- }
19
+ class LspClientNotStartedError extends Error {
20
+ override readonly name = "LspClientNotStartedError";
37
21
 
38
- export interface LspClientTimeoutOptions {
39
- requestTimeoutMs?: number;
40
- initializeTimeoutMs?: number;
22
+ constructor(
23
+ readonly serverId: string,
24
+ readonly root: string,
25
+ ) {
26
+ super("LSP client not started");
27
+ }
41
28
  }
42
29
 
43
30
  export class LspClientTransport {
@@ -48,6 +35,8 @@ export class LspClientTransport {
48
35
  protected readonly diagnosticsStore = new Map<string, Diagnostic[]>();
49
36
  protected readonly requestTimeoutMs: number;
50
37
  protected readonly initializeTimeoutMs: number;
38
+ private workspaceApplyEditHandler: WorkspaceApplyEditHandler | null = null;
39
+ private diagnosticPullSupported = false;
51
40
 
52
41
  constructor(
53
42
  protected readonly root: string,
@@ -66,6 +55,30 @@ export class LspClientTransport {
66
55
  return [...this.server.command];
67
56
  }
68
57
 
58
+ protected setWorkspaceApplyEditHandler(handler: WorkspaceApplyEditHandler): void {
59
+ this.workspaceApplyEditHandler = handler;
60
+ }
61
+
62
+ protected hasWorkspaceApplyEditHandler(): boolean {
63
+ return this.workspaceApplyEditHandler !== null;
64
+ }
65
+
66
+ protected setDiagnosticPullSupported(supported: boolean): void {
67
+ this.diagnosticPullSupported = supported;
68
+ }
69
+
70
+ protected isDiagnosticPullSupported(): boolean {
71
+ return this.diagnosticPullSupported;
72
+ }
73
+
74
+ protected handlePublishDiagnostics(params: {
75
+ readonly uri: string;
76
+ readonly diagnostics: readonly Diagnostic[];
77
+ readonly version?: number;
78
+ }): void {
79
+ this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
80
+ }
81
+
69
82
  async start(): Promise<void> {
70
83
  const env = createLspSpawnEnv(this.root, {
71
84
  ...process.env,
@@ -78,8 +91,6 @@ export class LspClientTransport {
78
91
  });
79
92
  this.startStderrReading();
80
93
 
81
- await new Promise<void>((resolve) => setTimeout(resolve, 100));
82
-
83
94
  if (this.proc.exitCode !== null) {
84
95
  const stderr = this.stderrBuffer.join("\n");
85
96
  throw new LspProcessExitedError(this.server.id, this.root, this.proc.exitCode, stderr.slice(-2000));
@@ -90,7 +101,7 @@ export class LspClientTransport {
90
101
  this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
91
102
  const diagnosticsParams = parseDiagnosticsParams(params);
92
103
  if (diagnosticsParams?.uri) {
93
- this.diagnosticsStore.set(diagnosticsParams.uri, diagnosticsParams.diagnostics);
104
+ this.handlePublishDiagnostics(diagnosticsParams);
94
105
  }
95
106
  });
96
107
 
@@ -104,6 +115,9 @@ export class LspClientTransport {
104
115
 
105
116
  this.connection.onRequest("client/registerCapability", () => null);
106
117
  this.connection.onRequest("window/workDoneProgress/create", () => null);
118
+ if (this.workspaceApplyEditHandler) {
119
+ this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
120
+ }
107
121
 
108
122
  this.connection.onClose(() => {
109
123
  this.processExited = true;
@@ -140,12 +154,16 @@ export class LspClientTransport {
140
154
 
141
155
  protected sendRequest<T>(method: string): Promise<T>;
142
156
  protected sendRequest<T>(method: string, params: unknown): Promise<T>;
143
- protected sendRequest<T>(method: string, params: unknown, options: { timeoutMs?: number }): Promise<T>;
157
+ protected sendRequest<T>(
158
+ method: string,
159
+ params: unknown,
160
+ options: { timeoutMs?: number; signal?: AbortSignal },
161
+ ): Promise<T>;
144
162
  protected async sendRequest<T>(
145
163
  method: string,
146
- ...args: [] | [unknown] | [unknown, { timeoutMs?: number }]
164
+ ...args: [] | [unknown] | [unknown, { timeoutMs?: number; signal?: AbortSignal }]
147
165
  ): Promise<T> {
148
- if (!this.connection) throw new Error("LSP client not started");
166
+ if (!this.connection) throw new LspClientNotStartedError(this.server.id, this.root);
149
167
 
150
168
  if (this.processExited || (this.proc && this.proc.exitCode !== null)) {
151
169
  const stderrTail = this.stderrBuffer.slice(-10).join("\n");
@@ -157,25 +175,22 @@ export class LspClientTransport {
157
175
  );
158
176
  }
159
177
 
160
- const timeoutMs = args[1]?.timeoutMs ?? this.requestTimeoutMs;
161
- let timeoutHandle: NodeJS.Timeout | null = null;
162
- const timeoutPromise = new Promise<never>((_, reject) => {
163
- timeoutHandle = setTimeout(() => {
164
- const stderrTail = this.stderrBuffer.slice(-5).join("\n");
165
- reject(new LspRequestTimeoutError(method, stderrTail || undefined));
166
- }, timeoutMs);
167
- });
178
+ const options = args[1];
179
+ const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
180
+ const timeoutController = new AbortController();
181
+ const timeoutHandle = setTimeout(() => {
182
+ const stderrTail = this.stderrBuffer.slice(-5).join("\n");
183
+ timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
184
+ }, timeoutMs);
185
+ const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
168
186
 
169
187
  try {
170
- const requestPromise =
188
+ const result =
171
189
  args.length === 0
172
- ? this.connection.sendRequest<T>(method)
173
- : this.connection.sendRequest<T>(method, args[0]);
174
- const result = await Promise.race([requestPromise, timeoutPromise]);
175
- if (timeoutHandle !== null) clearTimeout(timeoutHandle);
190
+ ? await this.connection.sendRequest<T>(method, undefined, { signal: combinedSignal.signal })
191
+ : await this.connection.sendRequest<T>(method, args[0], { signal: combinedSignal.signal });
176
192
  return result;
177
193
  } catch (error) {
178
- if (timeoutHandle !== null) clearTimeout(timeoutHandle);
179
194
  if (this.processExited || (this.proc && this.proc.exitCode !== null)) {
180
195
  throw new LspProcessExitedError(
181
196
  this.server.id,
@@ -188,6 +203,9 @@ export class LspClientTransport {
188
203
  throw new LspConnectionClosedError(this.server.id, this.root, error.message);
189
204
  }
190
205
  throw error;
206
+ } finally {
207
+ clearTimeout(timeoutHandle);
208
+ combinedSignal.dispose();
191
209
  }
192
210
  }
193
211
 
@@ -219,17 +237,17 @@ export class LspClientTransport {
219
237
  try {
220
238
  await this.sendRequest<null>("shutdown");
221
239
  } catch (error) {
222
- reportBestEffortCleanupError("shutdown request", error);
240
+ reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
223
241
  }
224
242
  try {
225
243
  await this.sendNotification("exit");
226
244
  } catch (error) {
227
- reportBestEffortCleanupError("exit notification", error);
245
+ reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
228
246
  }
229
247
  try {
230
248
  this.connection.dispose();
231
249
  } catch (error) {
232
- reportBestEffortCleanupError("connection dispose", error);
250
+ reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
233
251
  }
234
252
  this.connection = null;
235
253
  }
@@ -262,11 +280,11 @@ export class LspClientTransport {
262
280
  new Promise<void>((resolve) => setTimeout(resolve, STOP_SIGKILL_GRACE_MS)),
263
281
  ]);
264
282
  } catch (error) {
265
- reportBestEffortCleanupError("hard process kill", error);
283
+ reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
266
284
  }
267
285
  }
268
286
  } catch (error) {
269
- reportBestEffortCleanupError("process stop", error);
287
+ reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
270
288
  }
271
289
  }
272
290
 
@@ -279,21 +297,29 @@ export class LspClientTransport {
279
297
  }
280
298
  }
281
299
 
282
- export function createLspSpawnEnv(
283
- _root: string,
284
- input: Record<string, string | undefined>,
285
- ): Record<string, string | undefined> {
286
- return { ...input };
287
- }
288
-
289
- function isDiagnostic(value: unknown): value is Diagnostic {
290
- return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
291
- }
292
-
293
- function isRange(value: unknown): value is Diagnostic["range"] {
294
- return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
295
- }
296
-
297
- function isPosition(value: unknown): value is Diagnostic["range"]["start"] {
298
- return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
300
+ function combineAbortSignals(
301
+ primary: AbortSignal | undefined,
302
+ secondary: AbortSignal,
303
+ ): { readonly signal: AbortSignal; readonly dispose: () => void } {
304
+ const controller = new AbortController();
305
+ const abortFrom = (signal: AbortSignal): void => {
306
+ if (!controller.signal.aborted) controller.abort(signal.reason);
307
+ };
308
+ const onPrimaryAbort = (): void => {
309
+ if (primary) abortFrom(primary);
310
+ };
311
+ const onSecondaryAbort = (): void => abortFrom(secondary);
312
+
313
+ if (primary?.aborted) abortFrom(primary);
314
+ else primary?.addEventListener("abort", onPrimaryAbort, { once: true });
315
+ if (secondary.aborted) abortFrom(secondary);
316
+ else secondary.addEventListener("abort", onSecondaryAbort, { once: true });
317
+
318
+ return {
319
+ signal: controller.signal,
320
+ dispose: () => {
321
+ primary?.removeEventListener("abort", onPrimaryAbort);
322
+ secondary.removeEventListener("abort", onSecondaryAbort);
323
+ },
324
+ };
299
325
  }
@@ -0,0 +1,19 @@
1
+ export type WorkspaceApplyEditConcurrentPhase = "applying" | "settled";
2
+
3
+ const CONCURRENT_FAILURE_REASON_BY_PHASE: Readonly<Record<WorkspaceApplyEditConcurrentPhase, string>> = {
4
+ applying: "workspace/applyEdit is already in progress for this workspace mutation",
5
+ settled: "workspace/applyEdit was already handled for this workspace mutation",
6
+ };
7
+
8
+ export const CANONICAL_CONCURRENT_WORKSPACE_APPLY_EDIT_FAILURE_REASON =
9
+ "workspace/applyEdit concurrent for this workspace mutation";
10
+
11
+ export function workspaceApplyEditConcurrentFailureReason(phase: WorkspaceApplyEditConcurrentPhase): string {
12
+ return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
13
+ }
14
+
15
+ export function canonicalizeWorkspaceApplyEditFailureReason(reason: string): string {
16
+ return Object.values(CONCURRENT_FAILURE_REASON_BY_PHASE).includes(reason)
17
+ ? CANONICAL_CONCURRENT_WORKSPACE_APPLY_EDIT_FAILURE_REASON
18
+ : reason;
19
+ }
@@ -0,0 +1,214 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+
3
+ import { afterEach, describe, expect, it } from "bun:test";
4
+
5
+ import {
6
+ createWorkspaceEditTestHarness,
7
+ readEvents,
8
+ renameTextEdit,
9
+ waitForEventCount,
10
+ } from "./workspace-apply-edit-test-support.js";
11
+
12
+ const harness = createWorkspaceEditTestHarness();
13
+
14
+ afterEach(async () => {
15
+ await harness.cleanup();
16
+ });
17
+
18
+ describe("LspClient workspace mutation lease", () => {
19
+ it("#given no mutating request #when the server calls applyEdit #then the request is rejected as unscoped", async () => {
20
+ const context = await harness.makeClient({ unscopedApplyEdit: renameTextEdit("before", "after", { version: 1 }) });
21
+
22
+ const responses = await waitForEventCount(
23
+ context.events,
24
+ (event) => event.type === "clientResponse" && event.method === "workspace/applyEdit",
25
+ 1,
26
+ );
27
+
28
+ expect(responses[0]?.result).toEqual({
29
+ applied: false,
30
+ failureReason: expect.stringContaining("active workspace mutation"),
31
+ });
32
+ expect(readFileSync(context.source, "utf-8")).toBe("const before = 1;\n");
33
+ });
34
+
35
+ it("#given one valid server apply #when the server applies twice #then the repeated request is rejected", async () => {
36
+ const context = await harness.makeClient({
37
+ renameSteps: [
38
+ { applyEdit: renameTextEdit("before", "after", { version: 1 }), applyTwice: true, renameResult: "same" },
39
+ ],
40
+ });
41
+ await context.client.openFile(context.source);
42
+
43
+ const result = await context.client.rename(context.source, 1, 6, "after");
44
+
45
+ expect(result.apply.success).toBe(true);
46
+ const responses = readEvents(context.events).filter(
47
+ (event) => event.type === "clientResponse" && event.method === "workspace/applyEdit",
48
+ );
49
+ expect(responses).toHaveLength(2);
50
+ expect(responses[0]?.result).toEqual({ applied: true });
51
+ expect(responses[1]?.result).toEqual({
52
+ applied: false,
53
+ failureReason: expect.stringContaining("already handled"),
54
+ });
55
+ expect(readFileSync(context.source, "utf-8")).toBe("const after = 1;\n");
56
+ });
57
+
58
+ it("#given two concurrent server applies #when one enters commit #then the other is rejected without a second write", async () => {
59
+ const context = await harness.makeClient({
60
+ renameSteps: [
61
+ {
62
+ applyEdit: renameTextEdit("before", "after", { version: 1 }),
63
+ applyConcurrently: true,
64
+ renameResult: "same",
65
+ },
66
+ ],
67
+ });
68
+ let writes = 0;
69
+ context.client.setWorkspaceEditIo({
70
+ writeFile(path, content) {
71
+ writes += 1;
72
+ writeFileSync(path, content, "utf-8");
73
+ },
74
+ });
75
+ await context.client.openFile(context.source);
76
+
77
+ const result = await context.client.rename(context.source, 1, 6, "after");
78
+
79
+ expect(result.apply.success).toBe(true);
80
+ expect(writes).toBe(1);
81
+ const responses = readEvents(context.events).filter(
82
+ (event) => event.type === "clientResponse" && event.method === "workspace/applyEdit",
83
+ );
84
+ expect(responses.map((response) => response.result)).toContainEqual({ applied: true });
85
+ const rejection = responses.find(
86
+ (response) => typeof response.result === "object" && response.result !== null && "failureReason" in response.result,
87
+ );
88
+ expect(rejection?.result).toMatchObject({ applied: false });
89
+ expect(JSON.stringify(rejection?.result)).toMatch(/already (?:in progress|handled)/);
90
+ });
91
+
92
+ it("#given a rename lease is held #when another rename starts #then it is rejected without a second request", async () => {
93
+ const context = await harness.makeClient({
94
+ renameSteps: [
95
+ {
96
+ renameResult: "edit",
97
+ renameEdit: renameTextEdit("before", "after", { version: 1 }),
98
+ responseDelayMs: 150,
99
+ },
100
+ ],
101
+ });
102
+ await context.client.openFile(context.source);
103
+ const firstPromise = context.client.rename(context.source, 1, 6, "after");
104
+ await waitForEventCount(
105
+ context.events,
106
+ (event) => event.type === "clientRequest" && event.method === "textDocument/rename",
107
+ 1,
108
+ );
109
+
110
+ const concurrent = await context.client.rename(context.source, 1, 6, "other_");
111
+ const first = await firstPromise;
112
+
113
+ expect(concurrent.apply.success).toBe(false);
114
+ expect(concurrent.apply.errors.join("\n")).toContain("workspace mutation is already in progress");
115
+ expect(first.apply.success).toBe(true);
116
+ const requests = readEvents(context.events).filter(
117
+ (event) => event.type === "clientRequest" && event.method === "textDocument/rename",
118
+ );
119
+ expect(requests).toHaveLength(1);
120
+ });
121
+
122
+ it("#given no server apply #when rename directly returns an edit #then it is applied exactly once", async () => {
123
+ const context = await harness.makeClient({
124
+ renameSteps: [{ renameResult: "edit", renameEdit: renameTextEdit("before", "after", { version: 1 }) }],
125
+ });
126
+ await context.client.openFile(context.source);
127
+
128
+ const result = await context.client.rename(context.source, 1, 6, "after");
129
+
130
+ expect(result.apply.success).toBe(true);
131
+ expect(readFileSync(context.source, "utf-8")).toBe("const after = 1;\n");
132
+ expect(readEvents(context.events).filter((event) => event.method === "workspace/applyEdit")).toHaveLength(0);
133
+ });
134
+
135
+ it("#given cancellation before a delayed server apply #when the rename request is aborted #then no file is mutated", async () => {
136
+ const controller = new AbortController();
137
+ const context = await harness.makeClient({
138
+ renameSteps: [
139
+ {
140
+ applyEdit: renameTextEdit("before", "after", { version: 1 }),
141
+ applyDelayMs: 100,
142
+ renameResult: "same",
143
+ },
144
+ ],
145
+ });
146
+ await context.client.openFile(context.source);
147
+ const rename = context.client.rename(context.source, 1, 6, "after", controller.signal);
148
+ await waitForEventCount(
149
+ context.events,
150
+ (event) => event.type === "clientRequest" && event.method === "textDocument/rename",
151
+ 1,
152
+ );
153
+ controller.abort();
154
+
155
+ await expect(rename).rejects.toThrow(/cancel/i);
156
+
157
+ const [cancel] = await waitForEventCount(
158
+ context.events,
159
+ (event) => event.type === "clientNotification" && event.method === "$/cancelRequest",
160
+ 1,
161
+ );
162
+ expect(cancel?.params).toEqual({ id: 2 });
163
+ expect(readFileSync(context.source, "utf-8")).toBe("const before = 1;\n");
164
+ });
165
+
166
+ it("#given cancellation after the commit gate #when applyEdit finishes #then rename reports one completed late-abort mutation", async () => {
167
+ const controller = new AbortController();
168
+ const context = await harness.makeClient({
169
+ renameSteps: [{ applyEdit: renameTextEdit("before", "after", { version: 1 }), renameResult: "same" }],
170
+ });
171
+ let writes = 0;
172
+ context.client.setWorkspaceEditIo({
173
+ writeFile(path, content) {
174
+ writes += 1;
175
+ writeFileSync(path, content, "utf-8");
176
+ controller.abort();
177
+ },
178
+ });
179
+ await context.client.openFile(context.source);
180
+
181
+ const result = await context.client.rename(context.source, 1, 6, "after", controller.signal);
182
+
183
+ expect(result.apply).toMatchObject({ success: true, lateAbort: true, totalEdits: 1 });
184
+ expect(writes).toBe(1);
185
+ expect(readFileSync(context.source, "utf-8")).toBe("const after = 1;\n");
186
+ });
187
+
188
+ it("#given lease-backed and bare clients #when initialized #then applyEdit and annotations are advertised honestly", async () => {
189
+ const active = await harness.makeClient({});
190
+ const inactive = await harness.makeBareConnection({});
191
+
192
+ const activeCapabilities = initializeCapabilities(active.events);
193
+ const inactiveCapabilities = initializeCapabilities(inactive.events);
194
+ expect(activeCapabilities.workspace?.applyEdit).toBe(true);
195
+ expect(inactiveCapabilities.workspace?.applyEdit).toBeUndefined();
196
+ expect(activeCapabilities.textDocument?.rename?.honorsChangeAnnotations).toBeUndefined();
197
+ expect(activeCapabilities.workspace?.workspaceEdit?.changeAnnotationSupport).toBeUndefined();
198
+ });
199
+ });
200
+
201
+ interface InitializeCapabilities {
202
+ readonly workspace?: {
203
+ readonly applyEdit?: boolean;
204
+ readonly workspaceEdit?: { readonly changeAnnotationSupport?: unknown };
205
+ };
206
+ readonly textDocument?: { readonly rename?: { readonly honorsChangeAnnotations?: boolean } };
207
+ }
208
+
209
+ function initializeCapabilities(path: string): InitializeCapabilities {
210
+ const initialize = readEvents(path).find(
211
+ (event) => event.type === "clientRequest" && event.method === "initialize",
212
+ );
213
+ return (initialize?.params as { capabilities?: InitializeCapabilities } | undefined)?.capabilities ?? {};
214
+ }
@@ -0,0 +1,113 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ import { afterEach, describe, expect, it } from "bun:test";
5
+
6
+ import type { WorkspaceEdit } from "./types.js";
7
+ import {
8
+ createWorkspaceEditTestHarness,
9
+ readEvents,
10
+ renameTextEdit,
11
+ } from "./workspace-apply-edit-test-support.js";
12
+
13
+ const harness = createWorkspaceEditTestHarness();
14
+
15
+ afterEach(async () => {
16
+ await harness.cleanup();
17
+ });
18
+
19
+ describe("LspClient workspace edit synchronization", () => {
20
+ it("#given an open edited document #when applyEdit commits #then didChange precedes didSave with the next version", async () => {
21
+ const context = await harness.makeClient({
22
+ renameSteps: [{ applyEdit: renameTextEdit("before", "after", { version: 1 }), renameResult: "same" }],
23
+ });
24
+ await context.client.openFile(context.source);
25
+
26
+ await context.client.rename(context.source, 1, 6, "after");
27
+
28
+ const notifications = readEvents(context.events).filter((event) => event.type === "clientNotification");
29
+ const changeIndex = notifications.findIndex((event) => event.method === "textDocument/didChange");
30
+ const saveIndex = notifications.findIndex((event) => event.method === "textDocument/didSave");
31
+ expect(changeIndex).toBeGreaterThan(-1);
32
+ expect(saveIndex).toBeGreaterThan(changeIndex);
33
+ expect(notifications[changeIndex]?.params).toMatchObject({ textDocument: { version: 2 } });
34
+ });
35
+
36
+ it("#given an open file resource rename #when applyEdit commits #then didClose and didOpen move document state", async () => {
37
+ const edit: WorkspaceEdit = {
38
+ documentChanges: [{ kind: "rename", oldUri: "file:///placeholder", newUri: "file:///destination" }],
39
+ };
40
+ const context = await harness.makeClient({ renameSteps: [{ applyEdit: edit, renameResult: "null" }] });
41
+ await context.client.openFile(context.source);
42
+
43
+ const result = await context.client.rename(context.source, 1, 6, "after");
44
+
45
+ expect(result.apply.success).toBe(true);
46
+ expect(existsSync(context.source)).toBe(false);
47
+ expect(readFileSync(context.destination, "utf-8")).toBe("const before = 1;\n");
48
+ expect(context.client.getOpenDocumentVersion(context.source)).toBeUndefined();
49
+ expect(context.client.getOpenDocumentVersion(context.destination)).toBe(1);
50
+ const methods = readEvents(context.events).map((event) => event.method);
51
+ expect(methods.indexOf("textDocument/didClose")).toBeLessThan(methods.lastIndexOf("textDocument/didOpen"));
52
+ });
53
+
54
+ it("#given an open file deletion #when applyEdit commits #then didClose removes document state", async () => {
55
+ const edit: WorkspaceEdit = { documentChanges: [{ kind: "delete", uri: "file:///placeholder" }] };
56
+ const context = await harness.makeClient({ renameSteps: [{ applyEdit: edit, renameResult: "null" }] });
57
+ await context.client.openFile(context.source);
58
+
59
+ const result = await context.client.rename(context.source, 1, 6, "after");
60
+
61
+ expect(result.apply.success).toBe(true);
62
+ expect(existsSync(context.source)).toBe(false);
63
+ expect(context.client.getOpenDocumentVersion(context.source)).toBeUndefined();
64
+ expect(readEvents(context.events).some((event) => event.method === "textDocument/didClose")).toBe(true);
65
+ });
66
+
67
+ it("#given a closed edited file #when applyEdit commits #then one changed watched-file event is emitted", async () => {
68
+ const edit = renameTextEdit("closed", "after_", { version: null, uri: "file:///closed" });
69
+ const context = await harness.makeClient({ renameSteps: [{ applyEdit: edit, renameResult: "null" }] });
70
+ await context.client.openFile(context.source);
71
+
72
+ const result = await context.client.rename(context.source, 1, 6, "after");
73
+
74
+ expect(result.apply.success).toBe(true);
75
+ expect(readFileSync(context.closed, "utf-8")).toBe("const after_ = 1;\n");
76
+ const watched = readEvents(context.events).find(
77
+ (event) => event.type === "clientNotification" && event.method === "workspace/didChangeWatchedFiles",
78
+ );
79
+ expect(watched?.params).toEqual({ changes: [{ uri: pathToFileURL(realpathSync(context.closed)).href, type: 2 }] });
80
+ });
81
+
82
+ it("#given a new closed file #when applyEdit creates it #then one created watched-file event is emitted", async () => {
83
+ const edit: WorkspaceEdit = { documentChanges: [{ kind: "create", uri: "file:///created" }] };
84
+ const context = await harness.makeClient({ renameSteps: [{ applyEdit: edit, renameResult: "null" }] });
85
+ await context.client.openFile(context.source);
86
+
87
+ const result = await context.client.rename(context.source, 1, 6, "after");
88
+
89
+ expect(result.apply.success).toBe(true);
90
+ expect(readFileSync(context.created, "utf-8")).toBe("");
91
+ const watched = readEvents(context.events).find(
92
+ (event) => event.type === "clientNotification" && event.method === "workspace/didChangeWatchedFiles",
93
+ );
94
+ expect(watched?.params).toEqual({ changes: [{ uri: pathToFileURL(realpathSync(context.created)).href, type: 1 }] });
95
+ });
96
+
97
+ it("#given an open file overwritten by create #when applyEdit commits #then didClose and didOpen reset its state", async () => {
98
+ const edit: WorkspaceEdit = {
99
+ documentChanges: [{ kind: "create", uri: "file:///closed", options: { overwrite: true } }],
100
+ };
101
+ const context = await harness.makeClient({ renameSteps: [{ applyEdit: edit, renameResult: "null" }] });
102
+ await context.client.openFile(context.source);
103
+ await context.client.openFile(context.closed);
104
+
105
+ const result = await context.client.rename(context.source, 1, 6, "after");
106
+
107
+ expect(result.apply.success).toBe(true);
108
+ expect(readFileSync(context.closed, "utf-8")).toBe("");
109
+ expect(context.client.getOpenDocumentVersion(context.closed)).toBe(1);
110
+ const methods = readEvents(context.events).map((event) => event.method);
111
+ expect(methods.indexOf("textDocument/didClose")).toBeLessThan(methods.lastIndexOf("textDocument/didOpen"));
112
+ });
113
+ });