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.
- package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3677 -0
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3154 -0
- package/.agents/skills/work-with-pr/SKILL.md +16 -37
- package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/.opencode/skills/work-with-pr/SKILL.md +16 -37
- package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/bin/AGENTS.md +33 -0
- package/dist/cli/index.js +500 -158
- package/dist/cli-node/index.js +500 -158
- package/dist/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.d.ts +6 -0
- package/dist/hooks/category-skill-reminder/hook.d.ts +9 -1
- package/dist/hooks/comment-checker/hook.d.ts +8 -1
- package/dist/hooks/todo-continuation-enforcer/types.d.ts +3 -0
- package/dist/index.js +956 -702
- package/dist/plugin/messages-transform.d.ts +1 -0
- package/dist/plugin-handlers/prometheus-agent-config-builder.d.ts +2 -0
- package/dist/tui.js +43 -4
- package/package.json +16 -16
- package/packages/git-bash-mcp/dist/cli.js +81 -19
- package/packages/lsp-core/package.json +4 -0
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
- package/packages/lsp-core/src/lsp/client.ts +262 -80
- package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
- package/packages/lsp-core/src/lsp/connection.ts +12 -6
- package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +221 -0
- package/packages/lsp-core/src/lsp/directory-diagnostics.ts +61 -28
- package/packages/lsp-core/src/lsp/errors.ts +11 -0
- package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
- package/packages/lsp-core/src/lsp/formatters.ts +3 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
- package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
- package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
- package/packages/lsp-core/src/lsp/transport.ts +96 -70
- package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
- package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
- package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
- package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
- package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
- package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
- package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
- package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
- package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
- package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
- package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
- package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
- package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
- package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
- package/packages/lsp-core/src/mcp.ts +18 -7
- package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
- package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
- package/packages/lsp-core/src/post-edit/index.ts +1 -0
- package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
- package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
- package/packages/lsp-core/src/request-context.test.ts +171 -0
- package/packages/lsp-core/src/request-context.ts +222 -9
- package/packages/lsp-core/src/tool-surface.test.ts +4 -1
- package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
- package/packages/lsp-core/src/tools/navigation.ts +12 -12
- package/packages/lsp-core/src/tools/rename.ts +10 -15
- package/packages/lsp-core/src/tools/symbols.ts +11 -11
- package/packages/lsp-core/src/tools/types.ts +2 -1
- package/packages/lsp-daemon/dist/cli.js +3330 -764
- package/packages/lsp-daemon/dist/client.d.ts +105 -0
- package/packages/lsp-daemon/dist/client.js +5995 -0
- package/packages/lsp-daemon/dist/daemon-client.d.ts +12 -7
- package/packages/lsp-daemon/dist/daemon-client.js +139 -32
- package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-server.js +40 -15
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +10 -8
- package/packages/lsp-daemon/dist/ensure-daemon.js +135 -51
- package/packages/lsp-daemon/dist/index.d.ts +2 -2
- package/packages/lsp-daemon/dist/index.js +3093 -786
- package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
- package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
- package/packages/lsp-daemon/dist/lock.js +14 -4
- package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
- package/packages/lsp-daemon/dist/ownership.js +168 -0
- package/packages/lsp-daemon/dist/paths.d.ts +33 -9
- package/packages/lsp-daemon/dist/paths.js +72 -33
- package/packages/lsp-daemon/dist/proxy.d.ts +5 -0
- package/packages/lsp-daemon/dist/proxy.js +123 -16
- package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
- package/packages/lsp-daemon/dist/request-routing.js +71 -22
- package/packages/lsp-daemon/dist/run-daemon.js +9 -2
- package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
- package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
- package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
- package/packages/lsp-daemon/package.json +12 -3
- package/packages/lsp-tools-mcp/dist/cli.js +2189 -454
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
- package/packages/lsp-tools-mcp/dist/mcp.js +2206 -471
- package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
- package/packages/lsp-tools-mcp/dist/tools.js +2119 -447
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/.mcp.json +2 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +100 -28
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +100 -28
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +21 -9
- package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +35 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +69 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +57 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/.mcp.json +2 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +3033 -936
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +20 -5
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -3
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/README.md +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
- package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +15 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +20 -0
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +50 -33
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +50 -33
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +50 -33
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +3 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +5 -6
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +5 -6
- package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +2 -2
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +26 -14
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
- package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +9 -1
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -0
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +50 -33
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
- package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
- package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +7 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +34 -3
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
|
@@ -1,80 +1,603 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// src/cli.ts
|
|
4
4
|
import { spawn as spawn3 } from "node:child_process";
|
|
5
5
|
import { argv, execPath as execPath2, stderr } from "node:process";
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// src/codex-hook-cli.ts
|
|
8
8
|
import { stdin as processStdin } from "node:process";
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
import { readFileSync } from "node:fs";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
17
|
-
import { existsSync, statSync } from "node:fs";
|
|
18
|
-
import { delimiter, join } from "node:path";
|
|
19
|
-
import { basename, extname } from "node:path";
|
|
20
|
-
import { connect as connect2 } from "node:net";
|
|
21
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
22
|
-
import { closeSync as closeSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync as openSync2 } from "node:fs";
|
|
10
|
+
// src/codex-hook.ts
|
|
11
|
+
import { readFileSync as readFileSync9, realpathSync as realpathSync6 } from "node:fs";
|
|
12
|
+
import { homedir as homedir3 } from "node:os";
|
|
13
|
+
import { join as join11, resolve as resolve11 } from "node:path";
|
|
14
|
+
|
|
15
|
+
// ../../../../lsp-daemon/dist/client.js
|
|
23
16
|
import { connect } from "node:net";
|
|
24
|
-
import {
|
|
17
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
19
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
21
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2 } from "node:fs";
|
|
22
|
+
import { Socket } from "node:net";
|
|
23
|
+
import { dirname as dirname3 } from "node:path";
|
|
25
24
|
import { execPath } from "node:process";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
import {
|
|
26
|
+
chmodSync,
|
|
27
|
+
closeSync,
|
|
28
|
+
constants,
|
|
29
|
+
fchmodSync,
|
|
30
|
+
fstatSync,
|
|
31
|
+
lstatSync,
|
|
32
|
+
mkdirSync,
|
|
33
|
+
openSync,
|
|
34
|
+
readFileSync,
|
|
35
|
+
unlinkSync,
|
|
36
|
+
writeSync
|
|
37
|
+
} from "node:fs";
|
|
29
38
|
import { createHash } from "node:crypto";
|
|
30
39
|
import { createRequire } from "node:module";
|
|
31
|
-
import { homedir, tmpdir } from "node:os";
|
|
32
|
-
import
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import { existsSync as
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
40
|
+
import { homedir as homedir2, tmpdir, userInfo } from "node:os";
|
|
41
|
+
import * as path from "node:path";
|
|
42
|
+
import { fileURLToPath } from "node:url";
|
|
43
|
+
import { statSync as statSync2 } from "node:fs";
|
|
44
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
45
|
+
import { existsSync as existsSync9, statSync as statSync4 } from "node:fs";
|
|
46
|
+
import { dirname as dirname8, join as join4, resolve as resolve7 } from "node:path";
|
|
47
|
+
import { basename as basename2, extname } from "node:path";
|
|
48
|
+
import { resolve as resolve6 } from "node:path";
|
|
49
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
50
|
+
import { pathToFileURL } from "node:url";
|
|
51
|
+
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
52
|
+
import { existsSync as existsSync2, statSync as statSync3 } from "node:fs";
|
|
53
|
+
import { delimiter as delimiter2, join as join2 } from "node:path";
|
|
54
|
+
import { readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
55
|
+
import { relative as relative2, resolve as resolve2 } from "node:path";
|
|
56
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
57
|
+
import { existsSync as existsSync4, lstatSync as lstatSync3, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
58
|
+
import { existsSync as existsSync3, lstatSync as lstatSync2, readFileSync as readFileSync3, readdirSync, realpathSync as realpathSync3 } from "node:fs";
|
|
59
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
|
|
46
60
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import { existsSync as
|
|
50
|
-
import { dirname as
|
|
61
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
62
|
+
import { dirname as dirname5, relative as relative4, resolve as resolve4 } from "node:path";
|
|
63
|
+
import { existsSync as existsSync5, lstatSync as lstatSync4, readdirSync as readdirSync2 } from "node:fs";
|
|
64
|
+
import { dirname as dirname6, resolve as resolve5 } from "node:path";
|
|
65
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
66
|
+
import { dirname as dirname7 } from "node:path";
|
|
67
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
|
|
68
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
69
|
+
import { delimiter as delimiter3, join as join3 } from "node:path";
|
|
70
|
+
import { existsSync as existsSync10, lstatSync as lstatSync5, readdirSync as readdirSync3 } from "node:fs";
|
|
71
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
51
72
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
import { lstatSync as lstatSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
74
|
+
import { join as join6 } from "node:path";
|
|
75
|
+
import { statSync as statSync5 } from "node:fs";
|
|
76
|
+
import { isAbsolute as isAbsolute4 } from "node:path";
|
|
77
|
+
|
|
78
|
+
class LspRequestContextParseError extends Error {
|
|
79
|
+
code;
|
|
80
|
+
name = "LspRequestContextParseError";
|
|
81
|
+
constructor(code, message) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.code = code;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class LspRequestContextUnavailableError extends Error {
|
|
88
|
+
name = "LspRequestContextUnavailableError";
|
|
89
|
+
constructor() {
|
|
90
|
+
super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
|
|
91
|
+
}
|
|
57
92
|
}
|
|
58
93
|
var storage = new AsyncLocalStorage;
|
|
94
|
+
var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
95
|
+
var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
|
|
96
|
+
function lspRequestContext() {
|
|
97
|
+
const context = storage.getStore();
|
|
98
|
+
if (!context)
|
|
99
|
+
throw new LspRequestContextUnavailableError;
|
|
100
|
+
return context;
|
|
101
|
+
}
|
|
59
102
|
function contextCwd() {
|
|
60
|
-
return
|
|
103
|
+
return lspRequestContext().cwd;
|
|
104
|
+
}
|
|
105
|
+
function parseLspRequestContext(value) {
|
|
106
|
+
if (!isRecord(value)) {
|
|
107
|
+
throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
|
|
108
|
+
}
|
|
109
|
+
rejectUnknownFields(value, CONTEXT_FIELDS, "context");
|
|
110
|
+
const cwd = stringField(value, "cwd");
|
|
111
|
+
const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
|
|
112
|
+
const userConfigPath = stringField(value, "userConfigPath");
|
|
113
|
+
const installDecisionsPath = stringField(value, "installDecisionsPath");
|
|
114
|
+
const capabilities = capabilitiesField(value["capabilities"]);
|
|
115
|
+
const canonical = canonicalCwd(cwd);
|
|
116
|
+
for (const path2 of projectConfigPaths) {
|
|
117
|
+
requireAbsolutePath(path2, "projectConfigPaths");
|
|
118
|
+
const projectPath = canonicalizeExistingOrNearestAncestor(path2);
|
|
119
|
+
if (!isPathInside(canonical, projectPath)) {
|
|
120
|
+
throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path2}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
requireAbsolutePath(userConfigPath, "userConfigPath");
|
|
124
|
+
requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
|
|
125
|
+
return {
|
|
126
|
+
cwd: canonical,
|
|
127
|
+
projectConfigPaths: projectConfigPaths.map((path2) => canonicalizeExistingOrNearestAncestor(path2)),
|
|
128
|
+
userConfigPath,
|
|
129
|
+
installDecisionsPath,
|
|
130
|
+
capabilities
|
|
131
|
+
};
|
|
61
132
|
}
|
|
62
|
-
function
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
133
|
+
function canonicalCwd(cwd) {
|
|
134
|
+
const resolved = resolve(cwd);
|
|
135
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
136
|
+
throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
137
|
+
}
|
|
138
|
+
return realpathSync(resolved);
|
|
139
|
+
}
|
|
140
|
+
function canonicalizeExistingOrNearestAncestor(path2) {
|
|
141
|
+
let current = resolve(path2);
|
|
142
|
+
const suffix = [];
|
|
143
|
+
while (true) {
|
|
144
|
+
try {
|
|
145
|
+
const existing = realpathSync(current);
|
|
146
|
+
return suffix.length === 0 ? existing : join(existing, ...suffix);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (!isMissingPathError(error))
|
|
149
|
+
throw error;
|
|
150
|
+
const parent = dirname(current);
|
|
151
|
+
if (parent === current)
|
|
152
|
+
throw error;
|
|
153
|
+
suffix.unshift(basename(current));
|
|
154
|
+
current = parent;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function capabilitiesField(value) {
|
|
159
|
+
if (!isRecord(value)) {
|
|
160
|
+
throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
161
|
+
}
|
|
162
|
+
rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
|
|
163
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
164
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
165
|
+
throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
|
|
166
|
+
}
|
|
167
|
+
return { installDecisionTool };
|
|
168
|
+
}
|
|
169
|
+
function stringField(value, field) {
|
|
170
|
+
const fieldValue = value[field];
|
|
171
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
172
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
173
|
+
}
|
|
174
|
+
return fieldValue;
|
|
175
|
+
}
|
|
176
|
+
function stringArrayField(value, field) {
|
|
177
|
+
const fieldValue = value[field];
|
|
178
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
179
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
|
|
180
|
+
}
|
|
181
|
+
return fieldValue;
|
|
182
|
+
}
|
|
183
|
+
function requireAbsolutePath(path2, field) {
|
|
184
|
+
if (!isAbsolute(path2)) {
|
|
185
|
+
throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path2}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function isPathInside(parent, child) {
|
|
189
|
+
const childPath = resolve(child);
|
|
190
|
+
const relativePath = relative(parent, childPath);
|
|
191
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
192
|
+
}
|
|
193
|
+
function isMissingPathError(error) {
|
|
194
|
+
const code = errorCode(error);
|
|
195
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
196
|
+
}
|
|
197
|
+
function rejectUnknownFields(value, allowed, scope) {
|
|
198
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
199
|
+
if (unknown.length > 0) {
|
|
200
|
+
throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function isRecord(value) {
|
|
204
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
205
|
+
}
|
|
206
|
+
function errorCode(error) {
|
|
207
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
208
|
+
return;
|
|
209
|
+
const code = Reflect.get(error, "code");
|
|
210
|
+
return typeof code === "string" ? code : undefined;
|
|
211
|
+
}
|
|
212
|
+
function isPlainRecord(value) {
|
|
213
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
214
|
+
}
|
|
215
|
+
var OMO_DAEMON_PROTOCOL_VERSION = 1;
|
|
216
|
+
var AUTH_ERROR_CODE = -32001;
|
|
217
|
+
function authEnvelope(token) {
|
|
218
|
+
return { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, token };
|
|
219
|
+
}
|
|
220
|
+
function readAuthToken(paths) {
|
|
221
|
+
try {
|
|
222
|
+
const token = readFileSync(paths.auth, "utf8").trim();
|
|
223
|
+
return token.length > 0 ? token : null;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error instanceof Error)
|
|
226
|
+
return null;
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function isAuthErrorResponse(message) {
|
|
231
|
+
if (!isPlainRecord(message))
|
|
232
|
+
return false;
|
|
233
|
+
const error = message["error"];
|
|
234
|
+
if (!isPlainRecord(error))
|
|
235
|
+
return false;
|
|
236
|
+
const data = error["data"];
|
|
237
|
+
return error["code"] === AUTH_ERROR_CODE && isPlainRecord(data) && data["code"] === "daemon_authentication_failed";
|
|
238
|
+
}
|
|
239
|
+
var OMO_LSP_DAEMON_DIR = "OMO_LSP_DAEMON_DIR";
|
|
240
|
+
var OMO_LSP_DAEMON_CLI = "OMO_LSP_DAEMON_CLI";
|
|
241
|
+
var OMO_LSP_DAEMON_VERSION = "OMO_LSP_DAEMON_VERSION";
|
|
242
|
+
var DAEMON_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
243
|
+
|
|
244
|
+
class InvalidRuntimeOverrideError extends Error {
|
|
245
|
+
code = "invalid_runtime_override";
|
|
246
|
+
reason;
|
|
247
|
+
constructor(reason, message) {
|
|
248
|
+
super(message);
|
|
249
|
+
this.name = "InvalidRuntimeOverrideError";
|
|
250
|
+
this.reason = reason;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
class InvalidDaemonVersionError extends Error {
|
|
255
|
+
code = "invalid_daemon_version";
|
|
256
|
+
version;
|
|
257
|
+
constructor(version) {
|
|
258
|
+
super("LSP daemon version must match [A-Za-z0-9][A-Za-z0-9._+-]{0,127}");
|
|
259
|
+
this.name = "InvalidDaemonVersionError";
|
|
260
|
+
this.version = version;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function validateDaemonVersion(version) {
|
|
264
|
+
if (!DAEMON_VERSION_PATTERN.test(version))
|
|
265
|
+
throw new InvalidDaemonVersionError(version);
|
|
266
|
+
return version;
|
|
267
|
+
}
|
|
268
|
+
function resolveDaemonRuntime(env, defaults) {
|
|
269
|
+
const cliOverride = env[OMO_LSP_DAEMON_CLI];
|
|
270
|
+
const versionOverride = env[OMO_LSP_DAEMON_VERSION];
|
|
271
|
+
const hasCliOverride = cliOverride !== undefined;
|
|
272
|
+
const hasVersionOverride = versionOverride !== undefined;
|
|
273
|
+
if (hasCliOverride !== hasVersionOverride) {
|
|
274
|
+
throw new InvalidRuntimeOverrideError("paired_values_required", `${OMO_LSP_DAEMON_CLI} and ${OMO_LSP_DAEMON_VERSION} must be set together`);
|
|
275
|
+
}
|
|
276
|
+
if (!hasCliOverride || !hasVersionOverride) {
|
|
277
|
+
if (!isAbsolute2(defaults.cliPath)) {
|
|
278
|
+
throw new InvalidRuntimeOverrideError("packaged_cli_must_be_absolute", "Packaged LSP daemon CLI path must be absolute");
|
|
279
|
+
}
|
|
280
|
+
return { cliPath: defaults.cliPath, version: validateDaemonVersion(defaults.version) };
|
|
281
|
+
}
|
|
282
|
+
if (!isAbsolute2(cliOverride)) {
|
|
283
|
+
throw new InvalidRuntimeOverrideError("cli_must_be_absolute", `${OMO_LSP_DAEMON_CLI} must be an absolute path to an existing regular file`);
|
|
284
|
+
}
|
|
285
|
+
let cliStats;
|
|
286
|
+
try {
|
|
287
|
+
cliStats = statSync2(cliOverride);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!(error instanceof Error))
|
|
290
|
+
throw error;
|
|
291
|
+
throw new InvalidRuntimeOverrideError("cli_not_found", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
|
|
292
|
+
}
|
|
293
|
+
if (!cliStats.isFile()) {
|
|
294
|
+
throw new InvalidRuntimeOverrideError("cli_not_file", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
|
|
295
|
+
}
|
|
296
|
+
return { cliPath: cliOverride, version: validateDaemonVersion(versionOverride) };
|
|
297
|
+
}
|
|
298
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
299
|
+
var MAX_SOCKET_PATH_LENGTH = 100;
|
|
300
|
+
|
|
301
|
+
class InvalidDaemonDirectoryError extends Error {
|
|
302
|
+
code = "invalid_daemon_directory";
|
|
303
|
+
directory;
|
|
304
|
+
constructor(directory) {
|
|
305
|
+
super(`${OMO_LSP_DAEMON_DIR} must be an absolute path`);
|
|
306
|
+
this.name = "InvalidDaemonDirectoryError";
|
|
307
|
+
this.directory = directory;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function resolveDaemonVersion(requireFn = requireFromHere) {
|
|
311
|
+
for (const candidate of ["./package.json", "../package.json"]) {
|
|
312
|
+
let loaded;
|
|
313
|
+
try {
|
|
314
|
+
loaded = requireFn(candidate);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (!(error instanceof Error))
|
|
317
|
+
throw error;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (typeof loaded === "object" && loaded !== null && "version" in loaded) {
|
|
321
|
+
const version = Reflect.get(loaded, "version");
|
|
322
|
+
if (typeof version === "string")
|
|
323
|
+
return validateDaemonVersion(version);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return "0";
|
|
327
|
+
}
|
|
328
|
+
function packagedRuntimeDefaults() {
|
|
329
|
+
return {
|
|
330
|
+
cliPath: fileURLToPath(new URL("./cli.js", import.meta.url)),
|
|
331
|
+
version: resolveDaemonVersion()
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function daemonBaseDir(env = process.env, platform = defaultDaemonPlatform()) {
|
|
335
|
+
const override = env[OMO_LSP_DAEMON_DIR];
|
|
336
|
+
if (override !== undefined) {
|
|
337
|
+
if (!platform.path.isAbsolute(override))
|
|
338
|
+
throw new InvalidDaemonDirectoryError(override);
|
|
339
|
+
return platform.path.resolve(override);
|
|
340
|
+
}
|
|
341
|
+
return platform.path.resolve(platform.path.join(platform.homedir(), ".omo", "lsp-daemon"));
|
|
342
|
+
}
|
|
343
|
+
function daemonPaths(env = process.env, runtimeDefaults = packagedRuntimeDefaults(), platform = defaultDaemonPlatform()) {
|
|
344
|
+
const runtime = resolveDaemonRuntime(env, runtimeDefaults);
|
|
345
|
+
const baseDir = daemonBaseDir(env, platform);
|
|
346
|
+
const dir = platform.path.resolve(platform.path.join(baseDir, `v${runtime.version}`));
|
|
347
|
+
return {
|
|
348
|
+
version: runtime.version,
|
|
349
|
+
cliPath: runtime.cliPath,
|
|
350
|
+
dir,
|
|
351
|
+
socket: resolveSocketPath(dir, runtime.version, platform),
|
|
352
|
+
lock: platform.path.join(dir, "daemon.lock"),
|
|
353
|
+
pid: platform.path.join(dir, "daemon.pid"),
|
|
354
|
+
auth: platform.path.join(dir, "daemon.auth"),
|
|
355
|
+
endpoint: platform.path.join(dir, "daemon.endpoint"),
|
|
356
|
+
owner: platform.path.join(dir, "daemon.owner"),
|
|
357
|
+
log: platform.path.join(dir, "daemon.log")
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function defaultDaemonPlatform() {
|
|
361
|
+
return {
|
|
362
|
+
platform: process.platform,
|
|
363
|
+
homedir: homedir2,
|
|
364
|
+
tmpdir,
|
|
365
|
+
getuid: () => typeof process.getuid === "function" ? process.getuid() : undefined,
|
|
366
|
+
username: () => userInfo().username,
|
|
367
|
+
path
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function resolveSocketPath(dir, version, platform) {
|
|
371
|
+
const canonicalVersionDir = platform.path.resolve(dir);
|
|
372
|
+
if (platform.platform === "win32") {
|
|
373
|
+
const currentUserDiscriminator = `${platform.getuid() ?? "win"}:${platform.username()}:${platform.path.resolve(platform.homedir())}`;
|
|
374
|
+
const digest = shortDigest(`${canonicalVersionDir}\x00${currentUserDiscriminator}`);
|
|
375
|
+
return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
|
|
376
|
+
}
|
|
377
|
+
const natural = platform.path.join(canonicalVersionDir, "daemon.sock");
|
|
378
|
+
if (natural.length < MAX_SOCKET_PATH_LENGTH)
|
|
379
|
+
return natural;
|
|
380
|
+
return platform.path.join(platform.tmpdir(), `omo-lsp-${version}-${shortDigest(canonicalVersionDir)}`, "daemon.sock");
|
|
381
|
+
}
|
|
382
|
+
function shortDigest(value) {
|
|
383
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
384
|
+
}
|
|
385
|
+
function encodeJsonLine(message) {
|
|
386
|
+
return `${JSON.stringify(message)}
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
389
|
+
function createLineDecoder(onMessage, onParseError) {
|
|
390
|
+
let buffer = "";
|
|
391
|
+
return {
|
|
392
|
+
push(chunk) {
|
|
393
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
394
|
+
let index = buffer.indexOf(`
|
|
395
|
+
`);
|
|
396
|
+
while (index !== -1) {
|
|
397
|
+
const raw = buffer.slice(0, index).trim();
|
|
398
|
+
buffer = buffer.slice(index + 1);
|
|
399
|
+
if (raw.length > 0) {
|
|
400
|
+
try {
|
|
401
|
+
onMessage(JSON.parse(raw));
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error instanceof Error) {
|
|
404
|
+
onParseError?.(raw, error);
|
|
405
|
+
} else {
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
index = buffer.indexOf(`
|
|
411
|
+
`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
var PROBE_TIMEOUT_MS = 500;
|
|
417
|
+
var DEFAULT_READY_TIMEOUT_MS = 5000;
|
|
418
|
+
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
419
|
+
|
|
420
|
+
class DaemonUnreachableError extends Error {
|
|
421
|
+
constructor(socketPath) {
|
|
422
|
+
super(`LSP daemon did not become reachable at ${socketPath}`);
|
|
423
|
+
this.name = "DaemonUnreachableError";
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async function ensureDaemonRunning(paths, deps = defaultEnsureDaemonDeps(), options = {}) {
|
|
427
|
+
const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
428
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
429
|
+
const signal = options.signal;
|
|
430
|
+
throwIfAborted(signal);
|
|
431
|
+
if (await awaitWithSignal(deps.probe(paths, signal), signal))
|
|
432
|
+
return;
|
|
433
|
+
throwIfAborted(signal);
|
|
434
|
+
deps.spawnDaemon(paths);
|
|
435
|
+
await waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs, signal);
|
|
436
|
+
}
|
|
437
|
+
async function waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs, signal) {
|
|
438
|
+
const deadline = deps.now() + readyTimeoutMs;
|
|
439
|
+
for (;; ) {
|
|
440
|
+
throwIfAborted(signal);
|
|
441
|
+
if (await awaitWithSignal(deps.probe(paths, signal), signal))
|
|
442
|
+
return;
|
|
443
|
+
if (deps.now() >= deadline)
|
|
444
|
+
throw new DaemonUnreachableError(paths.socket);
|
|
445
|
+
await awaitWithSignal(deps.sleep(pollIntervalMs, signal), signal);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function probeDaemon(paths, timeoutMs = PROBE_TIMEOUT_MS, signal) {
|
|
449
|
+
const token = readAuthToken(paths);
|
|
450
|
+
if (!token)
|
|
451
|
+
return false;
|
|
452
|
+
return await pingDaemon(paths, token, timeoutMs, signal) !== null;
|
|
453
|
+
}
|
|
454
|
+
function pingDaemon(paths, token, timeoutMs = PROBE_TIMEOUT_MS, signal) {
|
|
455
|
+
return new Promise((resolve22) => {
|
|
456
|
+
const socket = new Socket;
|
|
457
|
+
let settled = false;
|
|
458
|
+
let timer;
|
|
459
|
+
const finish = (value) => {
|
|
460
|
+
if (settled)
|
|
461
|
+
return;
|
|
462
|
+
settled = true;
|
|
463
|
+
if (timer !== undefined)
|
|
464
|
+
clearTimeout(timer);
|
|
465
|
+
signal?.removeEventListener("abort", onAbort);
|
|
466
|
+
socket.destroy();
|
|
467
|
+
resolve22(value);
|
|
468
|
+
};
|
|
469
|
+
const onAbort = () => finish(null);
|
|
470
|
+
const decoder = createLineDecoder((message) => {
|
|
471
|
+
finish(parsePingResponse(message));
|
|
472
|
+
});
|
|
473
|
+
socket.once("connect", () => {
|
|
474
|
+
socket.write(encodeJsonLine({ jsonrpc: "2.0", id: 1, method: "omo/ping", params: { _omo: authEnvelope(token) } }));
|
|
475
|
+
});
|
|
476
|
+
socket.on("data", (chunk) => decoder.push(chunk));
|
|
477
|
+
socket.once("error", () => {
|
|
478
|
+
finish(null);
|
|
479
|
+
});
|
|
480
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
481
|
+
timer.unref?.();
|
|
482
|
+
if (signal?.aborted) {
|
|
483
|
+
onAbort();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
487
|
+
socket.connect(paths.socket);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function spawnDaemonProcess(paths) {
|
|
491
|
+
mkdirSync2(dirname3(paths.log), { recursive: true });
|
|
492
|
+
const logFd = openSync2(paths.log, "a");
|
|
493
|
+
try {
|
|
494
|
+
const child = spawn(execPath, [paths.cliPath, "daemon"], {
|
|
495
|
+
detached: true,
|
|
496
|
+
stdio: ["ignore", logFd, logFd]
|
|
497
|
+
});
|
|
498
|
+
child.unref();
|
|
499
|
+
} finally {
|
|
500
|
+
closeSync2(logFd);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function defaultEnsureDaemonDeps() {
|
|
504
|
+
return {
|
|
505
|
+
probe: (paths, signal) => probeDaemon(paths, PROBE_TIMEOUT_MS, signal),
|
|
506
|
+
spawnDaemon: (paths) => spawnDaemonProcess(paths),
|
|
507
|
+
sleep: (ms, signal) => sleepWithSignal(ms, signal),
|
|
508
|
+
now: () => Date.now()
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function sleepWithSignal(ms, signal) {
|
|
512
|
+
return new Promise((resolve22) => {
|
|
513
|
+
let settled = false;
|
|
514
|
+
const finish = () => {
|
|
515
|
+
if (settled)
|
|
516
|
+
return;
|
|
517
|
+
settled = true;
|
|
518
|
+
clearTimeout(timer);
|
|
519
|
+
signal?.removeEventListener("abort", finish);
|
|
520
|
+
resolve22();
|
|
521
|
+
};
|
|
522
|
+
const timer = setTimeout(finish, ms);
|
|
523
|
+
if (signal?.aborted) {
|
|
524
|
+
finish();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function awaitWithSignal(promise, signal) {
|
|
531
|
+
if (!signal)
|
|
532
|
+
return promise;
|
|
533
|
+
if (signal.aborted)
|
|
534
|
+
return Promise.reject(abortError(signal));
|
|
535
|
+
return new Promise((resolve22, reject) => {
|
|
536
|
+
let settled = false;
|
|
537
|
+
const finish = (run) => {
|
|
538
|
+
if (settled)
|
|
539
|
+
return;
|
|
540
|
+
settled = true;
|
|
541
|
+
signal.removeEventListener("abort", onAbort);
|
|
542
|
+
run();
|
|
543
|
+
};
|
|
544
|
+
const onAbort = () => finish(() => reject(abortError(signal)));
|
|
545
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
546
|
+
promise.then((value) => finish(() => resolve22(value)), (error) => finish(() => reject(error)));
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function throwIfAborted(signal) {
|
|
550
|
+
if (signal?.aborted)
|
|
551
|
+
throw abortError(signal);
|
|
552
|
+
}
|
|
553
|
+
function abortError(signal) {
|
|
554
|
+
const reason = signal.reason;
|
|
555
|
+
if (reason instanceof Error)
|
|
556
|
+
return reason;
|
|
557
|
+
const error = new Error(typeof reason === "string" ? reason : "daemon startup cancelled");
|
|
558
|
+
error.name = "AbortError";
|
|
559
|
+
return error;
|
|
560
|
+
}
|
|
561
|
+
function parsePingResponse(message) {
|
|
562
|
+
if (!message || typeof message !== "object" || Array.isArray(message))
|
|
563
|
+
return null;
|
|
564
|
+
const result = Reflect.get(message, "result");
|
|
565
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
566
|
+
return null;
|
|
567
|
+
const pid = Reflect.get(result, "pid");
|
|
568
|
+
const nonce = Reflect.get(result, "nonce");
|
|
569
|
+
const startedAt = Reflect.get(result, "startedAt");
|
|
570
|
+
const endpoint = Reflect.get(result, "endpoint");
|
|
571
|
+
if (typeof pid !== "number" || typeof nonce !== "string" || typeof startedAt !== "string")
|
|
572
|
+
return null;
|
|
573
|
+
if (!endpoint || typeof endpoint !== "object" || Array.isArray(endpoint))
|
|
574
|
+
return null;
|
|
575
|
+
const path2 = Reflect.get(endpoint, "path");
|
|
576
|
+
const kind = Reflect.get(endpoint, "kind");
|
|
577
|
+
if (typeof path2 !== "string")
|
|
578
|
+
return null;
|
|
579
|
+
if (kind === "windows")
|
|
580
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
|
|
581
|
+
if (kind === "missing")
|
|
582
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
|
|
583
|
+
const dev = Reflect.get(endpoint, "dev");
|
|
584
|
+
const ino = Reflect.get(endpoint, "ino");
|
|
585
|
+
if (kind === "unix" && typeof dev === "number" && typeof ino === "number") {
|
|
586
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2, dev, ino } };
|
|
587
|
+
}
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
var HEADER_SEPARATOR = Buffer.from(`\r
|
|
591
|
+
\r
|
|
592
|
+
`);
|
|
593
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60000;
|
|
594
|
+
var BASENAME_EXTENSIONS = {
|
|
595
|
+
Dockerfile: ".dockerfile",
|
|
596
|
+
Containerfile: ".dockerfile"
|
|
597
|
+
};
|
|
598
|
+
function effectiveExtension(filePath) {
|
|
599
|
+
return BASENAME_EXTENSIONS[basename2(filePath)] ?? extname(filePath);
|
|
67
600
|
}
|
|
68
|
-
var DEFAULT_MAX_REFERENCES = 200;
|
|
69
|
-
var DEFAULT_MAX_SYMBOLS = 200;
|
|
70
|
-
var DEFAULT_MAX_DIAGNOSTICS = 200;
|
|
71
|
-
var DEFAULT_MAX_DIRECTORY_FILES = 50;
|
|
72
|
-
var REQUEST_TIMEOUT_MS = 15000;
|
|
73
|
-
var INIT_TIMEOUT_MS = 60000;
|
|
74
|
-
var IDLE_TIMEOUT_MS = 5 * 60000;
|
|
75
|
-
var REAPER_INTERVAL_MS = 60000;
|
|
76
|
-
var STOP_HARD_KILL_TIMEOUT_MS = 5000;
|
|
77
|
-
var STOP_SIGKILL_GRACE_MS = 1000;
|
|
78
601
|
|
|
79
602
|
class LspConnectionClosedError extends Error {
|
|
80
603
|
serverId;
|
|
@@ -122,7 +645,12 @@ class LspInvalidPathError extends Error {
|
|
|
122
645
|
}
|
|
123
646
|
|
|
124
647
|
class LspServerLookupError extends Error {
|
|
648
|
+
lookup;
|
|
125
649
|
name = "LspServerLookupError";
|
|
650
|
+
constructor(message, lookup) {
|
|
651
|
+
super(message);
|
|
652
|
+
this.lookup = lookup;
|
|
653
|
+
}
|
|
126
654
|
}
|
|
127
655
|
|
|
128
656
|
class LspServerInitializingError extends Error {
|
|
@@ -140,7 +668,25 @@ class LspProcessSpawnError extends Error {
|
|
|
140
668
|
function isLspDeadConnectionError(err) {
|
|
141
669
|
return err instanceof LspConnectionClosedError || err instanceof LspProcessExitedError;
|
|
142
670
|
}
|
|
143
|
-
|
|
671
|
+
function writeCleanupError(message) {
|
|
672
|
+
process.stderr.write(`${message}
|
|
673
|
+
`);
|
|
674
|
+
}
|
|
675
|
+
function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
|
|
676
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
677
|
+
logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
|
|
678
|
+
}
|
|
679
|
+
var DEFAULT_MAX_REFERENCES = 200;
|
|
680
|
+
var DEFAULT_MAX_SYMBOLS = 200;
|
|
681
|
+
var DEFAULT_MAX_DIAGNOSTICS = 200;
|
|
682
|
+
var DEFAULT_MAX_DIRECTORY_FILES = 50;
|
|
683
|
+
var REQUEST_TIMEOUT_MS = 15000;
|
|
684
|
+
var INIT_TIMEOUT_MS = 60000;
|
|
685
|
+
var IDLE_TIMEOUT_MS = 5 * 60000;
|
|
686
|
+
var REAPER_INTERVAL_MS = 60000;
|
|
687
|
+
var STOP_HARD_KILL_TIMEOUT_MS = 5000;
|
|
688
|
+
var STOP_SIGKILL_GRACE_MS = 1000;
|
|
689
|
+
var HEADER_SEPARATOR2 = `\r
|
|
144
690
|
\r
|
|
145
691
|
`;
|
|
146
692
|
var PARSE_ERROR = -32700;
|
|
@@ -186,28 +732,80 @@ class JsonRpcConnection {
|
|
|
186
732
|
onError(handler) {
|
|
187
733
|
this.errorHandlers.push(handler);
|
|
188
734
|
}
|
|
189
|
-
async sendRequest(method, params) {
|
|
735
|
+
async sendRequest(method, params, options = {}) {
|
|
190
736
|
if (this.disposed)
|
|
191
737
|
throw new Error("JSON-RPC connection is disposed");
|
|
192
738
|
const id = this.nextRequestId;
|
|
193
739
|
this.nextRequestId += 1;
|
|
740
|
+
const key = String(id);
|
|
194
741
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
195
|
-
|
|
196
|
-
|
|
742
|
+
let requestWritten = false;
|
|
743
|
+
let cancelAfterWrite = false;
|
|
744
|
+
let settled = false;
|
|
745
|
+
const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
746
|
+
const responsePromise = new Promise((resolve22, reject) => {
|
|
747
|
+
const cleanup = () => {
|
|
748
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
749
|
+
};
|
|
750
|
+
const settleCancel = () => {
|
|
751
|
+
if (settled)
|
|
752
|
+
return;
|
|
753
|
+
settled = true;
|
|
754
|
+
this.pendingRequests.delete(key);
|
|
755
|
+
cleanup();
|
|
756
|
+
const rejectCancelled = () => reject(abortError2(options.signal));
|
|
757
|
+
if (!requestWritten) {
|
|
758
|
+
cancelAfterWrite = true;
|
|
759
|
+
rejectCancelled();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
writeCancel().then(rejectCancelled, (error) => {
|
|
763
|
+
this.emitError(toError(error));
|
|
764
|
+
rejectCancelled();
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
const onAbort = () => settleCancel();
|
|
768
|
+
this.pendingRequests.set(key, {
|
|
197
769
|
resolve(result) {
|
|
198
|
-
|
|
770
|
+
settled = true;
|
|
771
|
+
cleanup();
|
|
772
|
+
resolve22(result);
|
|
773
|
+
},
|
|
774
|
+
reject(error) {
|
|
775
|
+
settled = true;
|
|
776
|
+
cleanup();
|
|
777
|
+
reject(error);
|
|
199
778
|
},
|
|
200
|
-
|
|
779
|
+
cleanup
|
|
201
780
|
});
|
|
781
|
+
if (options.signal?.aborted) {
|
|
782
|
+
settleCancel();
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
202
786
|
});
|
|
787
|
+
if (settled)
|
|
788
|
+
return responsePromise;
|
|
203
789
|
try {
|
|
204
790
|
await this.writeMessage(message);
|
|
791
|
+
requestWritten = true;
|
|
792
|
+
if (cancelAfterWrite)
|
|
793
|
+
await writeCancel();
|
|
205
794
|
} catch (error) {
|
|
206
|
-
|
|
795
|
+
if (settled)
|
|
796
|
+
return responsePromise;
|
|
797
|
+
const pending = this.pendingRequests.get(key);
|
|
798
|
+
if (pending) {
|
|
799
|
+
pending.cleanup();
|
|
800
|
+
this.pendingRequests.delete(key);
|
|
801
|
+
}
|
|
207
802
|
throw error;
|
|
208
803
|
}
|
|
209
804
|
return responsePromise;
|
|
210
805
|
}
|
|
806
|
+
pendingRequestCount() {
|
|
807
|
+
return this.pendingRequests.size;
|
|
808
|
+
}
|
|
211
809
|
async sendNotification(method, params) {
|
|
212
810
|
if (this.disposed)
|
|
213
811
|
return;
|
|
@@ -224,6 +822,7 @@ class JsonRpcConnection {
|
|
|
224
822
|
this.reader.off("error", this.handleStreamError);
|
|
225
823
|
this.writer.off("error", this.handleStreamError);
|
|
226
824
|
for (const pending of this.pendingRequests.values()) {
|
|
825
|
+
pending.cleanup();
|
|
227
826
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
228
827
|
}
|
|
229
828
|
this.pendingRequests.clear();
|
|
@@ -245,17 +844,17 @@ class JsonRpcConnection {
|
|
|
245
844
|
};
|
|
246
845
|
drainInputBuffer() {
|
|
247
846
|
while (true) {
|
|
248
|
-
const headerEnd = this.inputBuffer.indexOf(
|
|
847
|
+
const headerEnd = this.inputBuffer.indexOf(HEADER_SEPARATOR2);
|
|
249
848
|
if (headerEnd === -1)
|
|
250
849
|
return;
|
|
251
850
|
const headers = this.inputBuffer.subarray(0, headerEnd).toString("ascii");
|
|
252
|
-
const contentLength =
|
|
851
|
+
const contentLength = parseContentLength2(headers);
|
|
253
852
|
if (contentLength === null) {
|
|
254
853
|
this.inputBuffer = Buffer.alloc(0);
|
|
255
854
|
this.emitError(new Error("JSON-RPC message is missing Content-Length header"));
|
|
256
855
|
return;
|
|
257
856
|
}
|
|
258
|
-
const bodyStart = headerEnd + Buffer.byteLength(
|
|
857
|
+
const bodyStart = headerEnd + Buffer.byteLength(HEADER_SEPARATOR2);
|
|
259
858
|
const bodyEnd = bodyStart + contentLength;
|
|
260
859
|
if (this.inputBuffer.length < bodyEnd)
|
|
261
860
|
return;
|
|
@@ -299,6 +898,7 @@ class JsonRpcConnection {
|
|
|
299
898
|
if (!pending)
|
|
300
899
|
return;
|
|
301
900
|
this.pendingRequests.delete(String(id));
|
|
901
|
+
pending.cleanup();
|
|
302
902
|
if ("error" in message) {
|
|
303
903
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
304
904
|
return;
|
|
@@ -312,7 +912,11 @@ class JsonRpcConnection {
|
|
|
312
912
|
try {
|
|
313
913
|
handler(params);
|
|
314
914
|
} catch (error) {
|
|
315
|
-
|
|
915
|
+
if (error instanceof Error) {
|
|
916
|
+
this.emitError(error);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
this.emitError(new Error(String(error)));
|
|
316
920
|
}
|
|
317
921
|
}
|
|
318
922
|
handleRequest(message) {
|
|
@@ -337,13 +941,13 @@ class JsonRpcConnection {
|
|
|
337
941
|
const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
338
942
|
\r
|
|
339
943
|
${body}`;
|
|
340
|
-
return new Promise((
|
|
944
|
+
return new Promise((resolve22, reject) => {
|
|
341
945
|
this.writer.write(payload, (error) => {
|
|
342
946
|
if (error) {
|
|
343
947
|
reject(error);
|
|
344
948
|
return;
|
|
345
949
|
}
|
|
346
|
-
|
|
950
|
+
resolve22();
|
|
347
951
|
});
|
|
348
952
|
});
|
|
349
953
|
}
|
|
@@ -353,7 +957,15 @@ ${body}`;
|
|
|
353
957
|
}
|
|
354
958
|
}
|
|
355
959
|
}
|
|
356
|
-
function
|
|
960
|
+
function abortError2(signal) {
|
|
961
|
+
const reason = signal?.reason;
|
|
962
|
+
if (reason instanceof Error)
|
|
963
|
+
return reason;
|
|
964
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
965
|
+
error.name = "AbortError";
|
|
966
|
+
return error;
|
|
967
|
+
}
|
|
968
|
+
function parseContentLength2(headers) {
|
|
357
969
|
for (const line of headers.split(`\r
|
|
358
970
|
`)) {
|
|
359
971
|
const separatorIndex = line.indexOf(":");
|
|
@@ -401,10 +1013,10 @@ function reportKillError(context, error) {
|
|
|
401
1013
|
}
|
|
402
1014
|
function validateCwd(cwd) {
|
|
403
1015
|
try {
|
|
404
|
-
if (!
|
|
1016
|
+
if (!existsSync2(cwd)) {
|
|
405
1017
|
return { valid: false, error: `Working directory does not exist: ${cwd}` };
|
|
406
1018
|
}
|
|
407
|
-
const stats =
|
|
1019
|
+
const stats = statSync3(cwd);
|
|
408
1020
|
if (!stats.isDirectory()) {
|
|
409
1021
|
return { valid: false, error: `Path is not a directory: ${cwd}` };
|
|
410
1022
|
}
|
|
@@ -417,9 +1029,9 @@ function validateCwd(cwd) {
|
|
|
417
1029
|
}
|
|
418
1030
|
}
|
|
419
1031
|
function wrap(proc) {
|
|
420
|
-
const exitedPromise = new Promise((
|
|
421
|
-
proc.once("close", (code) =>
|
|
422
|
-
proc.once("error", () =>
|
|
1032
|
+
const exitedPromise = new Promise((resolve22) => {
|
|
1033
|
+
proc.once("close", (code) => resolve22(code ?? 0));
|
|
1034
|
+
proc.once("error", () => resolve22(1));
|
|
423
1035
|
});
|
|
424
1036
|
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
425
1037
|
throw new LspProcessSpawnError("Spawned process is missing one of stdin/stdout/stderr pipes");
|
|
@@ -473,7 +1085,7 @@ function isWindowsShellShim(command) {
|
|
|
473
1085
|
return lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat");
|
|
474
1086
|
}
|
|
475
1087
|
function splitPath(pathValue, platform) {
|
|
476
|
-
const separator = platform === "win32" ? ";" :
|
|
1088
|
+
const separator = platform === "win32" ? ";" : delimiter2;
|
|
477
1089
|
return pathValue.split(separator).filter(Boolean);
|
|
478
1090
|
}
|
|
479
1091
|
function getWindowsPathExtensions(env) {
|
|
@@ -488,8 +1100,8 @@ function resolveWindowsCommand(command, env) {
|
|
|
488
1100
|
const extensions = getWindowsPathExtensions(env);
|
|
489
1101
|
for (const baseDirectory of baseDirectories) {
|
|
490
1102
|
for (const extension of extensions) {
|
|
491
|
-
const candidate = baseDirectory ?
|
|
492
|
-
if (
|
|
1103
|
+
const candidate = baseDirectory ? join2(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
|
|
1104
|
+
if (existsSync2(candidate))
|
|
493
1105
|
return candidate;
|
|
494
1106
|
}
|
|
495
1107
|
}
|
|
@@ -523,7 +1135,7 @@ function spawnProcess(command, options) {
|
|
|
523
1135
|
throw new LspProcessSpawnError("[lsp] empty command");
|
|
524
1136
|
}
|
|
525
1137
|
const preparedCommand = createSpawnCommand(command, process.platform, process.env["ComSpec"] ?? "cmd.exe", options.env);
|
|
526
|
-
const proc =
|
|
1138
|
+
const proc = spawn2(preparedCommand.command, preparedCommand.args, {
|
|
527
1139
|
cwd: options.cwd,
|
|
528
1140
|
env: options.env,
|
|
529
1141
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -533,15 +1145,15 @@ function spawnProcess(command, options) {
|
|
|
533
1145
|
});
|
|
534
1146
|
return wrap(proc);
|
|
535
1147
|
}
|
|
536
|
-
function
|
|
1148
|
+
function isRecord2(value) {
|
|
537
1149
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
538
1150
|
}
|
|
539
1151
|
function parseConfigurationItems(params) {
|
|
540
|
-
if (!
|
|
1152
|
+
if (!isRecord2(params) || !Array.isArray(params["items"]))
|
|
541
1153
|
return [];
|
|
542
1154
|
const items = [];
|
|
543
1155
|
for (const item of params["items"]) {
|
|
544
|
-
if (!
|
|
1156
|
+
if (!isRecord2(item))
|
|
545
1157
|
continue;
|
|
546
1158
|
const section = item["section"];
|
|
547
1159
|
items.push(section === undefined || typeof section !== "string" ? {} : { section });
|
|
@@ -549,10 +1161,34 @@ function parseConfigurationItems(params) {
|
|
|
549
1161
|
return items;
|
|
550
1162
|
}
|
|
551
1163
|
function parseDiagnosticsParams(params) {
|
|
552
|
-
if (!
|
|
1164
|
+
if (!isRecord2(params) || typeof params["uri"] !== "string")
|
|
553
1165
|
return null;
|
|
554
1166
|
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
555
|
-
|
|
1167
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
1168
|
+
return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
|
|
1169
|
+
}
|
|
1170
|
+
function createLspSpawnEnv(_root, input) {
|
|
1171
|
+
return { ...input };
|
|
1172
|
+
}
|
|
1173
|
+
function isDiagnostic(value) {
|
|
1174
|
+
return isRecord2(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
1175
|
+
}
|
|
1176
|
+
function isRange(value) {
|
|
1177
|
+
return isRecord2(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
1178
|
+
}
|
|
1179
|
+
function isPosition(value) {
|
|
1180
|
+
return isRecord2(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
class LspClientNotStartedError extends Error {
|
|
1184
|
+
serverId;
|
|
1185
|
+
root;
|
|
1186
|
+
name = "LspClientNotStartedError";
|
|
1187
|
+
constructor(serverId, root) {
|
|
1188
|
+
super("LSP client not started");
|
|
1189
|
+
this.serverId = serverId;
|
|
1190
|
+
this.root = root;
|
|
1191
|
+
}
|
|
556
1192
|
}
|
|
557
1193
|
|
|
558
1194
|
class LspClientTransport {
|
|
@@ -565,9 +1201,11 @@ class LspClientTransport {
|
|
|
565
1201
|
diagnosticsStore = new Map;
|
|
566
1202
|
requestTimeoutMs;
|
|
567
1203
|
initializeTimeoutMs;
|
|
568
|
-
|
|
1204
|
+
workspaceApplyEditHandler = null;
|
|
1205
|
+
diagnosticPullSupported = false;
|
|
1206
|
+
constructor(root, server2, timeouts = {}) {
|
|
569
1207
|
this.root = root;
|
|
570
|
-
this.server =
|
|
1208
|
+
this.server = server2;
|
|
571
1209
|
this.requestTimeoutMs = timeouts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
572
1210
|
this.initializeTimeoutMs = timeouts.initializeTimeoutMs ?? INIT_TIMEOUT_MS;
|
|
573
1211
|
}
|
|
@@ -577,6 +1215,21 @@ class LspClientTransport {
|
|
|
577
1215
|
command() {
|
|
578
1216
|
return [...this.server.command];
|
|
579
1217
|
}
|
|
1218
|
+
setWorkspaceApplyEditHandler(handler) {
|
|
1219
|
+
this.workspaceApplyEditHandler = handler;
|
|
1220
|
+
}
|
|
1221
|
+
hasWorkspaceApplyEditHandler() {
|
|
1222
|
+
return this.workspaceApplyEditHandler !== null;
|
|
1223
|
+
}
|
|
1224
|
+
setDiagnosticPullSupported(supported) {
|
|
1225
|
+
this.diagnosticPullSupported = supported;
|
|
1226
|
+
}
|
|
1227
|
+
isDiagnosticPullSupported() {
|
|
1228
|
+
return this.diagnosticPullSupported;
|
|
1229
|
+
}
|
|
1230
|
+
handlePublishDiagnostics(params) {
|
|
1231
|
+
this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
|
|
1232
|
+
}
|
|
580
1233
|
async start() {
|
|
581
1234
|
const env = createLspSpawnEnv(this.root, {
|
|
582
1235
|
...process.env,
|
|
@@ -587,7 +1240,6 @@ class LspClientTransport {
|
|
|
587
1240
|
env
|
|
588
1241
|
});
|
|
589
1242
|
this.startStderrReading();
|
|
590
|
-
await new Promise((resolve6) => setTimeout(resolve6, 100));
|
|
591
1243
|
if (this.proc.exitCode !== null) {
|
|
592
1244
|
const stderr = this.stderrBuffer.join(`
|
|
593
1245
|
`);
|
|
@@ -597,7 +1249,7 @@ class LspClientTransport {
|
|
|
597
1249
|
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
598
1250
|
const diagnosticsParams = parseDiagnosticsParams(params);
|
|
599
1251
|
if (diagnosticsParams?.uri) {
|
|
600
|
-
this.
|
|
1252
|
+
this.handlePublishDiagnostics(diagnosticsParams);
|
|
601
1253
|
}
|
|
602
1254
|
});
|
|
603
1255
|
this.connection.onRequest("workspace/configuration", (params) => {
|
|
@@ -610,6 +1262,9 @@ class LspClientTransport {
|
|
|
610
1262
|
});
|
|
611
1263
|
this.connection.onRequest("client/registerCapability", () => null);
|
|
612
1264
|
this.connection.onRequest("window/workDoneProgress/create", () => null);
|
|
1265
|
+
if (this.workspaceApplyEditHandler) {
|
|
1266
|
+
this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
|
|
1267
|
+
}
|
|
613
1268
|
this.connection.onClose(() => {
|
|
614
1269
|
this.processExited = true;
|
|
615
1270
|
});
|
|
@@ -638,30 +1293,25 @@ class LspClientTransport {
|
|
|
638
1293
|
}
|
|
639
1294
|
async sendRequest(method, ...args) {
|
|
640
1295
|
if (!this.connection)
|
|
641
|
-
throw new
|
|
1296
|
+
throw new LspClientNotStartedError(this.server.id, this.root);
|
|
642
1297
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
643
1298
|
const stderrTail = this.stderrBuffer.slice(-10).join(`
|
|
644
1299
|
`);
|
|
645
1300
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
|
|
646
1301
|
}
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
1302
|
+
const options = args[1];
|
|
1303
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
1304
|
+
const timeoutController = new AbortController;
|
|
1305
|
+
const timeoutHandle = setTimeout(() => {
|
|
1306
|
+
const stderrTail = this.stderrBuffer.slice(-5).join(`
|
|
652
1307
|
`);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1308
|
+
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
1309
|
+
}, timeoutMs);
|
|
1310
|
+
const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
|
|
656
1311
|
try {
|
|
657
|
-
const
|
|
658
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
659
|
-
if (timeoutHandle !== null)
|
|
660
|
-
clearTimeout(timeoutHandle);
|
|
1312
|
+
const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
|
|
661
1313
|
return result;
|
|
662
1314
|
} catch (error) {
|
|
663
|
-
if (timeoutHandle !== null)
|
|
664
|
-
clearTimeout(timeoutHandle);
|
|
665
1315
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
666
1316
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
|
|
667
1317
|
`) || undefined);
|
|
@@ -670,6 +1320,9 @@ class LspClientTransport {
|
|
|
670
1320
|
throw new LspConnectionClosedError(this.server.id, this.root, error.message);
|
|
671
1321
|
}
|
|
672
1322
|
throw error;
|
|
1323
|
+
} finally {
|
|
1324
|
+
clearTimeout(timeoutHandle);
|
|
1325
|
+
combinedSignal.dispose();
|
|
673
1326
|
}
|
|
674
1327
|
}
|
|
675
1328
|
async sendNotification(method, ...args) {
|
|
@@ -698,17 +1351,17 @@ class LspClientTransport {
|
|
|
698
1351
|
try {
|
|
699
1352
|
await this.sendRequest("shutdown");
|
|
700
1353
|
} catch (error) {
|
|
701
|
-
reportBestEffortCleanupError("shutdown request", error);
|
|
1354
|
+
reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
|
|
702
1355
|
}
|
|
703
1356
|
try {
|
|
704
1357
|
await this.sendNotification("exit");
|
|
705
1358
|
} catch (error) {
|
|
706
|
-
reportBestEffortCleanupError("exit notification", error);
|
|
1359
|
+
reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
|
|
707
1360
|
}
|
|
708
1361
|
try {
|
|
709
1362
|
this.connection.dispose();
|
|
710
1363
|
} catch (error) {
|
|
711
|
-
reportBestEffortCleanupError("connection dispose", error);
|
|
1364
|
+
reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
|
|
712
1365
|
}
|
|
713
1366
|
this.connection = null;
|
|
714
1367
|
}
|
|
@@ -719,8 +1372,8 @@ class LspClientTransport {
|
|
|
719
1372
|
try {
|
|
720
1373
|
proc.kill();
|
|
721
1374
|
let timeoutId;
|
|
722
|
-
const timeoutPromise = new Promise((
|
|
723
|
-
timeoutId = setTimeout(
|
|
1375
|
+
const timeoutPromise = new Promise((resolve22) => {
|
|
1376
|
+
timeoutId = setTimeout(resolve22, STOP_HARD_KILL_TIMEOUT_MS);
|
|
724
1377
|
});
|
|
725
1378
|
await Promise.race([
|
|
726
1379
|
proc.exited.then(() => {
|
|
@@ -736,14 +1389,14 @@ class LspClientTransport {
|
|
|
736
1389
|
proc.kill("SIGKILL");
|
|
737
1390
|
await Promise.race([
|
|
738
1391
|
proc.exited,
|
|
739
|
-
new Promise((
|
|
1392
|
+
new Promise((resolve22) => setTimeout(resolve22, STOP_SIGKILL_GRACE_MS))
|
|
740
1393
|
]);
|
|
741
1394
|
} catch (error) {
|
|
742
|
-
reportBestEffortCleanupError("hard process kill", error);
|
|
1395
|
+
reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
|
|
743
1396
|
}
|
|
744
1397
|
}
|
|
745
1398
|
} catch (error) {
|
|
746
|
-
reportBestEffortCleanupError("process stop", error);
|
|
1399
|
+
reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
|
|
747
1400
|
}
|
|
748
1401
|
}
|
|
749
1402
|
this.processExited = true;
|
|
@@ -753,24 +1406,43 @@ class LspClientTransport {
|
|
|
753
1406
|
return this.diagnosticsStore.get(uri) ?? [];
|
|
754
1407
|
}
|
|
755
1408
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
1409
|
+
function combineAbortSignals(primary, secondary) {
|
|
1410
|
+
const controller = new AbortController;
|
|
1411
|
+
const abortFrom = (signal) => {
|
|
1412
|
+
if (!controller.signal.aborted)
|
|
1413
|
+
controller.abort(signal.reason);
|
|
1414
|
+
};
|
|
1415
|
+
const onPrimaryAbort = () => {
|
|
1416
|
+
if (primary)
|
|
1417
|
+
abortFrom(primary);
|
|
1418
|
+
};
|
|
1419
|
+
const onSecondaryAbort = () => abortFrom(secondary);
|
|
1420
|
+
if (primary?.aborted)
|
|
1421
|
+
abortFrom(primary);
|
|
1422
|
+
else
|
|
1423
|
+
primary?.addEventListener("abort", onPrimaryAbort, { once: true });
|
|
1424
|
+
if (secondary.aborted)
|
|
1425
|
+
abortFrom(secondary);
|
|
1426
|
+
else
|
|
1427
|
+
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
|
|
1428
|
+
return {
|
|
1429
|
+
signal: controller.signal,
|
|
1430
|
+
dispose: () => {
|
|
1431
|
+
primary?.removeEventListener("abort", onPrimaryAbort);
|
|
1432
|
+
secondary.removeEventListener("abort", onSecondaryAbort);
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
764
1435
|
}
|
|
765
|
-
function
|
|
766
|
-
|
|
1436
|
+
function supportsDiagnosticPull(capabilities) {
|
|
1437
|
+
if (capabilities === undefined)
|
|
1438
|
+
return false;
|
|
1439
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
767
1440
|
}
|
|
768
|
-
var INITIALIZE_SETTLE_MS = 300;
|
|
769
1441
|
|
|
770
1442
|
class LspClientConnection extends LspClientTransport {
|
|
771
1443
|
async initialize() {
|
|
772
1444
|
const rootUri = pathToFileURL(this.root).href;
|
|
773
|
-
await this.sendRequest("initialize", {
|
|
1445
|
+
const result = await this.sendRequest("initialize", {
|
|
774
1446
|
processId: process.pid,
|
|
775
1447
|
rootUri,
|
|
776
1448
|
rootPath: this.root,
|
|
@@ -784,8 +1456,7 @@ class LspClientConnection extends LspClientTransport {
|
|
|
784
1456
|
publishDiagnostics: {},
|
|
785
1457
|
rename: {
|
|
786
1458
|
prepareSupport: true,
|
|
787
|
-
prepareSupportDefaultBehavior: 1
|
|
788
|
-
honorsChangeAnnotations: true
|
|
1459
|
+
prepareSupportDefaultBehavior: 1
|
|
789
1460
|
},
|
|
790
1461
|
codeAction: {
|
|
791
1462
|
codeActionLiteralSupport: {
|
|
@@ -814,28 +1485,22 @@ class LspClientConnection extends LspClientTransport {
|
|
|
814
1485
|
symbol: {},
|
|
815
1486
|
workspaceFolders: true,
|
|
816
1487
|
configuration: true,
|
|
817
|
-
applyEdit: true,
|
|
1488
|
+
...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
|
|
818
1489
|
workspaceEdit: {
|
|
819
|
-
documentChanges: true
|
|
1490
|
+
documentChanges: true,
|
|
1491
|
+
resourceOperations: ["create", "rename", "delete"]
|
|
820
1492
|
}
|
|
821
1493
|
}
|
|
822
1494
|
},
|
|
823
1495
|
initializationOptions: this.server.initialization
|
|
824
1496
|
}, { timeoutMs: this.initializeTimeoutMs });
|
|
1497
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
825
1498
|
await this.sendNotification("initialized");
|
|
826
1499
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
827
1500
|
settings: { json: { validate: { enable: true } } }
|
|
828
1501
|
});
|
|
829
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
830
1502
|
}
|
|
831
1503
|
}
|
|
832
|
-
var BASENAME_EXTENSIONS = {
|
|
833
|
-
Dockerfile: ".dockerfile",
|
|
834
|
-
Containerfile: ".dockerfile"
|
|
835
|
-
};
|
|
836
|
-
function effectiveExtension(filePath) {
|
|
837
|
-
return BASENAME_EXTENSIONS[basename(filePath)] ?? extname(filePath);
|
|
838
|
-
}
|
|
839
1504
|
var SYMBOL_KIND_MAP = {
|
|
840
1505
|
1: "File",
|
|
841
1506
|
2: "Module",
|
|
@@ -1006,81 +1671,1384 @@ var EXT_TO_LANG = {
|
|
|
1006
1671
|
function getLanguageId(ext) {
|
|
1007
1672
|
return EXT_TO_LANG[ext] ?? "plaintext";
|
|
1008
1673
|
}
|
|
1009
|
-
var
|
|
1010
|
-
var
|
|
1674
|
+
var WATCHED_FILE_BATCH_SIZE = 128;
|
|
1675
|
+
var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1676
|
+
function canonicalPath(filePath) {
|
|
1677
|
+
const absolute = resolve2(filePath);
|
|
1678
|
+
try {
|
|
1679
|
+
return realpathSync2(absolute);
|
|
1680
|
+
} catch {
|
|
1681
|
+
return absolute;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
function isSameOrDescendant(candidate, parent) {
|
|
1685
|
+
const suffix = relative2(parent, candidate);
|
|
1686
|
+
return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
|
|
1687
|
+
}
|
|
1688
|
+
function movedPath(candidate, oldPath, newPath) {
|
|
1689
|
+
const suffix = relative2(oldPath, candidate);
|
|
1690
|
+
return suffix === "" ? newPath : resolve2(newPath, suffix);
|
|
1691
|
+
}
|
|
1011
1692
|
|
|
1012
|
-
class
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1693
|
+
class WorkspaceDocumentState {
|
|
1694
|
+
sendNotification;
|
|
1695
|
+
clearDiagnostics;
|
|
1696
|
+
openDocuments = new Map;
|
|
1697
|
+
openByUri = new Map;
|
|
1698
|
+
openPromises = new Map;
|
|
1699
|
+
now;
|
|
1700
|
+
versionlessPublishQuiescenceMs;
|
|
1701
|
+
constructor(sendNotification, clearDiagnostics, options = {}) {
|
|
1702
|
+
this.sendNotification = sendNotification;
|
|
1703
|
+
this.clearDiagnostics = clearDiagnostics;
|
|
1704
|
+
this.now = options.now ?? (() => Date.now());
|
|
1705
|
+
this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
1019
1706
|
}
|
|
1020
1707
|
async openFile(filePath) {
|
|
1021
|
-
const
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1708
|
+
const path2 = canonicalPath(filePath);
|
|
1709
|
+
const existingOpen = this.openPromises.get(path2);
|
|
1710
|
+
if (existingOpen) {
|
|
1711
|
+
await existingOpen;
|
|
1712
|
+
return this.openFile(path2);
|
|
1713
|
+
}
|
|
1714
|
+
const text = readFileSync2(path2, "utf-8");
|
|
1715
|
+
const existing = this.openDocuments.get(path2);
|
|
1716
|
+
if (!existing)
|
|
1717
|
+
return this.openDocumentSingleFlight(path2, text);
|
|
1718
|
+
if (existing.text === text)
|
|
1719
|
+
return;
|
|
1720
|
+
await this.changeDocument(existing, text);
|
|
1721
|
+
}
|
|
1722
|
+
getVersion(filePath) {
|
|
1723
|
+
return this.openDocuments.get(canonicalPath(filePath))?.version;
|
|
1724
|
+
}
|
|
1725
|
+
getStoredDiagnostics(uri) {
|
|
1726
|
+
const state = this.openByUri.get(uri);
|
|
1727
|
+
if (!state)
|
|
1728
|
+
return [];
|
|
1729
|
+
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
1730
|
+
}
|
|
1731
|
+
captureDiagnosticSnapshot(filePath) {
|
|
1732
|
+
const state = this.openDocuments.get(canonicalPath(filePath));
|
|
1733
|
+
if (!state)
|
|
1734
|
+
return null;
|
|
1735
|
+
return {
|
|
1736
|
+
path: state.path,
|
|
1737
|
+
uri: state.uri,
|
|
1738
|
+
version: state.version,
|
|
1739
|
+
documentGeneration: state.generation,
|
|
1740
|
+
publishGeneration: state.publishGeneration
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
isCurrentSnapshot(snapshot) {
|
|
1744
|
+
const state = this.openDocuments.get(snapshot.path);
|
|
1745
|
+
return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
|
|
1746
|
+
}
|
|
1747
|
+
getPullCache(snapshot) {
|
|
1748
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1749
|
+
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
|
|
1750
|
+
return null;
|
|
1751
|
+
return state.pullCache;
|
|
1752
|
+
}
|
|
1753
|
+
recordPullDiagnostics(snapshot, report) {
|
|
1754
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1755
|
+
if (!state)
|
|
1756
|
+
return;
|
|
1757
|
+
state.pullCache = {
|
|
1758
|
+
documentVersion: snapshot.version,
|
|
1759
|
+
diagnostics: [...report.diagnostics],
|
|
1760
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
recordPublishedDiagnostics(params) {
|
|
1764
|
+
const state = this.openByUri.get(params.uri);
|
|
1765
|
+
if (!state)
|
|
1766
|
+
return;
|
|
1767
|
+
state.publishGeneration += 1;
|
|
1768
|
+
state.lastPublish = {
|
|
1769
|
+
diagnostics: [...params.diagnostics],
|
|
1770
|
+
publishGeneration: state.publishGeneration,
|
|
1771
|
+
documentGenerationAtArrival: state.generation,
|
|
1772
|
+
arrivedAt: this.now(),
|
|
1773
|
+
...params.version === undefined ? {} : { version: params.version }
|
|
1774
|
+
};
|
|
1775
|
+
this.notifyWaiters(state);
|
|
1776
|
+
}
|
|
1777
|
+
resolvePushDiagnostics(snapshot) {
|
|
1778
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1779
|
+
if (!state?.lastPublish)
|
|
1780
|
+
return { status: "missing" };
|
|
1781
|
+
const publish = state.lastPublish;
|
|
1782
|
+
if (publish.version !== undefined) {
|
|
1783
|
+
return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
|
|
1784
|
+
}
|
|
1785
|
+
if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
|
|
1786
|
+
return { status: "missing" };
|
|
1787
|
+
const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
|
|
1788
|
+
const waitMs = Math.max(0, readyAt - this.now());
|
|
1789
|
+
return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
|
|
1790
|
+
}
|
|
1791
|
+
waitForDiagnosticsActivity(snapshot, timeoutMs) {
|
|
1792
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1793
|
+
if (!state || timeoutMs <= 0)
|
|
1794
|
+
return Promise.resolve();
|
|
1795
|
+
return new Promise((resolveActivity) => {
|
|
1796
|
+
let settled = false;
|
|
1797
|
+
const finish = () => {
|
|
1798
|
+
if (settled)
|
|
1799
|
+
return;
|
|
1800
|
+
settled = true;
|
|
1801
|
+
clearTimeout(timer);
|
|
1802
|
+
state.waiters.delete(finish);
|
|
1803
|
+
resolveActivity();
|
|
1804
|
+
};
|
|
1805
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1806
|
+
if (typeof timer.unref === "function")
|
|
1807
|
+
timer.unref();
|
|
1808
|
+
state.waiters.add(finish);
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
validateVersions(operations) {
|
|
1812
|
+
const versions = new Map([...this.openDocuments].map(([path2, state]) => [path2, state.version]));
|
|
1813
|
+
for (const operation of operations) {
|
|
1814
|
+
if (operation.kind === "text") {
|
|
1815
|
+
const current = versions.get(operation.path);
|
|
1816
|
+
if (operation.documentVersion !== null && current !== operation.documentVersion) {
|
|
1817
|
+
const observed = current === undefined ? "closed document" : `open document version ${current}`;
|
|
1818
|
+
return {
|
|
1819
|
+
changeIndex: operation.changeIndex,
|
|
1820
|
+
message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
if (current !== undefined)
|
|
1824
|
+
versions.set(operation.path, current + 1);
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
if (operation.kind === "rename") {
|
|
1828
|
+
const moved = [...versions].filter(([path2]) => isSameOrDescendant(path2, operation.oldPath));
|
|
1829
|
+
for (const [path2] of moved)
|
|
1830
|
+
versions.delete(path2);
|
|
1831
|
+
for (const [path2] of moved)
|
|
1832
|
+
versions.set(movedPath(path2, operation.oldPath, operation.newPath), 1);
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
if (operation.kind === "delete") {
|
|
1836
|
+
for (const path2 of [...versions.keys()]) {
|
|
1837
|
+
if (isSameOrDescendant(path2, operation.path))
|
|
1838
|
+
versions.delete(path2);
|
|
1034
1839
|
}
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
|
|
1843
|
+
versions.set(operation.path, 1);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
async synchronize(delta) {
|
|
1849
|
+
const watched = [];
|
|
1850
|
+
for (const mutation of delta.operations)
|
|
1851
|
+
await this.synchronizeMutation(mutation, watched);
|
|
1852
|
+
for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
|
|
1853
|
+
await this.sendNotification("workspace/didChangeWatchedFiles", {
|
|
1854
|
+
changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
|
|
1035
1855
|
});
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
async synchronizeMutation(mutation, watched) {
|
|
1859
|
+
if (mutation.kind === "text") {
|
|
1860
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1861
|
+
if (state)
|
|
1862
|
+
await this.changeDocument(state, mutation.afterText);
|
|
1863
|
+
else
|
|
1864
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
if (mutation.kind === "create") {
|
|
1868
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1869
|
+
if (state) {
|
|
1870
|
+
await this.closeDocument(state);
|
|
1871
|
+
await this.openDocumentSingleFlight(mutation.path, readFileSync2(mutation.path, "utf-8"));
|
|
1872
|
+
} else {
|
|
1873
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
|
|
1874
|
+
}
|
|
1040
1875
|
return;
|
|
1041
1876
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1877
|
+
if (mutation.kind === "rename") {
|
|
1878
|
+
const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
|
|
1879
|
+
for (const state of moved)
|
|
1880
|
+
await this.closeDocument(state);
|
|
1881
|
+
for (const state of moved) {
|
|
1882
|
+
const path2 = movedPath(state.path, mutation.oldPath, mutation.newPath);
|
|
1883
|
+
await this.openDocumentSingleFlight(path2, readFileSync2(path2, "utf-8"));
|
|
1884
|
+
}
|
|
1885
|
+
if (moved.length === 0) {
|
|
1886
|
+
watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
|
|
1887
|
+
watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
|
|
1888
|
+
}
|
|
1044
1889
|
return;
|
|
1045
1890
|
}
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1891
|
+
const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
|
|
1892
|
+
for (const state of removed)
|
|
1893
|
+
await this.closeDocument(state);
|
|
1894
|
+
if (removed.length === 0)
|
|
1895
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
|
|
1896
|
+
}
|
|
1897
|
+
async openDocumentSingleFlight(path2, text) {
|
|
1898
|
+
const existing = this.openPromises.get(path2);
|
|
1899
|
+
if (existing)
|
|
1900
|
+
return existing;
|
|
1901
|
+
const open = (async () => {
|
|
1902
|
+
const state = {
|
|
1903
|
+
path: path2,
|
|
1904
|
+
uri: pathToFileURL2(path2).href,
|
|
1905
|
+
languageId: getLanguageId(effectiveExtension(path2)),
|
|
1906
|
+
text,
|
|
1907
|
+
version: 1,
|
|
1908
|
+
generation: 1,
|
|
1909
|
+
publishGeneration: 0,
|
|
1910
|
+
waiters: new Set
|
|
1911
|
+
};
|
|
1912
|
+
this.openDocuments.set(path2, state);
|
|
1913
|
+
this.openByUri.set(state.uri, state);
|
|
1914
|
+
this.notifyWaiters(state);
|
|
1915
|
+
await this.sendNotification("textDocument/didOpen", {
|
|
1916
|
+
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
|
|
1917
|
+
});
|
|
1918
|
+
})().finally(() => {
|
|
1919
|
+
this.openPromises.delete(path2);
|
|
1920
|
+
});
|
|
1921
|
+
this.openPromises.set(path2, open);
|
|
1922
|
+
return open;
|
|
1923
|
+
}
|
|
1924
|
+
async changeDocument(state, text) {
|
|
1925
|
+
state.text = text;
|
|
1926
|
+
state.version += 1;
|
|
1927
|
+
state.generation += 1;
|
|
1928
|
+
this.clearDiagnostics(state.uri);
|
|
1929
|
+
this.notifyWaiters(state);
|
|
1049
1930
|
await this.sendNotification("textDocument/didChange", {
|
|
1050
|
-
textDocument: { uri, version:
|
|
1931
|
+
textDocument: { uri: state.uri, version: state.version },
|
|
1051
1932
|
contentChanges: [{ text }]
|
|
1052
1933
|
});
|
|
1053
|
-
await this.sendNotification("textDocument/didSave", {
|
|
1054
|
-
|
|
1055
|
-
|
|
1934
|
+
await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
|
|
1935
|
+
}
|
|
1936
|
+
async closeDocument(state) {
|
|
1937
|
+
this.openDocuments.delete(state.path);
|
|
1938
|
+
this.openByUri.delete(state.uri);
|
|
1939
|
+
this.clearDiagnostics(state.uri);
|
|
1940
|
+
this.notifyWaiters(state);
|
|
1941
|
+
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
1942
|
+
}
|
|
1943
|
+
notifyWaiters(state) {
|
|
1944
|
+
for (const waiter of [...state.waiters])
|
|
1945
|
+
waiter();
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
var CONCURRENT_FAILURE_REASON_BY_PHASE = {
|
|
1949
|
+
applying: "workspace/applyEdit is already in progress for this workspace mutation",
|
|
1950
|
+
settled: "workspace/applyEdit was already handled for this workspace mutation"
|
|
1951
|
+
};
|
|
1952
|
+
function workspaceApplyEditConcurrentFailureReason(phase) {
|
|
1953
|
+
return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
class WorkspaceEditPathError extends Error {
|
|
1957
|
+
path;
|
|
1958
|
+
detail;
|
|
1959
|
+
name = "WorkspaceEditPathError";
|
|
1960
|
+
constructor(path2, detail) {
|
|
1961
|
+
super(`${detail}: ${path2}`);
|
|
1962
|
+
this.path = path2;
|
|
1963
|
+
this.detail = detail;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
1967
|
+
const relativePath = relative3(workspaceRoot, filePath);
|
|
1968
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
|
|
1969
|
+
}
|
|
1970
|
+
function canonicalizeMissingPath(filePath) {
|
|
1971
|
+
let ancestor = filePath;
|
|
1972
|
+
while (!existsSync3(ancestor)) {
|
|
1973
|
+
const parent = dirname4(ancestor);
|
|
1974
|
+
if (parent === ancestor)
|
|
1975
|
+
throw new WorkspaceEditPathError(filePath, "no existing ancestor");
|
|
1976
|
+
ancestor = parent;
|
|
1977
|
+
}
|
|
1978
|
+
return resolve3(realpathSync3(ancestor), relative3(ancestor, filePath));
|
|
1979
|
+
}
|
|
1980
|
+
function canonicalWorkspaceRoot(workspaceRoot) {
|
|
1981
|
+
try {
|
|
1982
|
+
const canonical = realpathSync3(resolve3(workspaceRoot));
|
|
1983
|
+
if (!lstatSync2(canonical).isDirectory()) {
|
|
1984
|
+
return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
|
|
1985
|
+
}
|
|
1986
|
+
return {
|
|
1987
|
+
success: true,
|
|
1988
|
+
path: canonical,
|
|
1989
|
+
requestedPath: resolve3(workspaceRoot),
|
|
1990
|
+
followedSymbolicLink: existsSync3(resolve3(workspaceRoot)) && lstatSync2(resolve3(workspaceRoot)).isSymbolicLink()
|
|
1991
|
+
};
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1994
|
+
return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
|
|
1998
|
+
let requestedPath;
|
|
1999
|
+
try {
|
|
2000
|
+
const parsed = new URL(uri);
|
|
2001
|
+
if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
|
|
2002
|
+
return { success: false, error: `non-file URI ${uri}` };
|
|
2003
|
+
}
|
|
2004
|
+
requestedPath = resolve3(fileURLToPath2(parsed));
|
|
2005
|
+
} catch (error) {
|
|
2006
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2007
|
+
return { success: false, error: `non-file URI ${uri}: ${detail}` };
|
|
2008
|
+
}
|
|
2009
|
+
try {
|
|
2010
|
+
const canonical = existsSync3(requestedPath) ? realpathSync3(requestedPath) : canonicalizeMissingPath(requestedPath);
|
|
2011
|
+
if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
|
|
2012
|
+
return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
|
|
2013
|
+
}
|
|
2014
|
+
return {
|
|
2015
|
+
success: true,
|
|
2016
|
+
path: canonical,
|
|
2017
|
+
requestedPath,
|
|
2018
|
+
followedSymbolicLink: existsSync3(requestedPath) && lstatSync2(requestedPath).isSymbolicLink()
|
|
2019
|
+
};
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2022
|
+
return { success: false, error: `${requestedPath}: ${detail}` };
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
function snapshotPath(path2, includeChildren) {
|
|
2026
|
+
if (!existsSync3(path2))
|
|
2027
|
+
return { kind: "missing" };
|
|
2028
|
+
const stats = lstatSync2(path2);
|
|
2029
|
+
if (stats.isFile())
|
|
2030
|
+
return { kind: "file", content: readFileSync3(path2, "utf-8") };
|
|
2031
|
+
if (stats.isDirectory()) {
|
|
2032
|
+
return includeChildren ? { kind: "directory", children: readdirSync(path2).sort() } : { kind: "directory" };
|
|
2033
|
+
}
|
|
2034
|
+
throw new WorkspaceEditPathError(path2, "unsupported filesystem entry");
|
|
2035
|
+
}
|
|
2036
|
+
var DEFAULT_IO = {
|
|
2037
|
+
writeFile(path2, content) {
|
|
2038
|
+
writeFileSync(path2, content, "utf-8");
|
|
2039
|
+
},
|
|
2040
|
+
rename(oldPath, newPath) {
|
|
2041
|
+
renameSync(oldPath, newPath);
|
|
2042
|
+
},
|
|
2043
|
+
remove(path2, recursive) {
|
|
2044
|
+
rmSync(path2, { recursive, force: false });
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
function snapshotsEqual(expected, actual) {
|
|
2048
|
+
if (expected.kind !== actual.kind)
|
|
2049
|
+
return false;
|
|
2050
|
+
if (expected.kind === "file" && actual.kind === "file")
|
|
2051
|
+
return expected.content === actual.content;
|
|
2052
|
+
if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
|
|
2053
|
+
return JSON.stringify(expected.children) === JSON.stringify(actual.children);
|
|
2054
|
+
}
|
|
2055
|
+
return true;
|
|
2056
|
+
}
|
|
2057
|
+
function liveSnapshot(path2, expected) {
|
|
2058
|
+
return snapshotPath(path2, expected.kind === "directory" && expected.children !== undefined);
|
|
2059
|
+
}
|
|
2060
|
+
function firstOperationIndex(plan) {
|
|
2061
|
+
return plan.operations[0]?.changeIndex ?? 0;
|
|
2062
|
+
}
|
|
2063
|
+
function failedCommit(plan, failure) {
|
|
2064
|
+
const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
|
|
2065
|
+
return {
|
|
2066
|
+
result: {
|
|
2067
|
+
success: false,
|
|
2068
|
+
filesModified,
|
|
2069
|
+
totalEdits,
|
|
2070
|
+
errors: [`change ${changeIndex}: ${message}`],
|
|
2071
|
+
failedChange: changeIndex,
|
|
2072
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
2073
|
+
},
|
|
2074
|
+
delta: mutationDelta(mutations),
|
|
2075
|
+
fingerprint: plan.fingerprint
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
function verifySnapshots(plan) {
|
|
2079
|
+
for (const [path2, expected] of plan.snapshots) {
|
|
2080
|
+
let actual;
|
|
2081
|
+
try {
|
|
2082
|
+
actual = liveSnapshot(path2, expected);
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
const changeIndex = plan.firstChangeByPath.get(path2) ?? firstOperationIndex(plan);
|
|
2085
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2086
|
+
return failedCommit(plan, { message: `cannot verify snapshot for ${path2}: ${detail}`, changeIndex });
|
|
2087
|
+
}
|
|
2088
|
+
if (!snapshotsEqual(expected, actual)) {
|
|
2089
|
+
const changeIndex = plan.firstChangeByPath.get(path2) ?? firstOperationIndex(plan);
|
|
2090
|
+
return failedCommit(plan, { message: `workspace state changed before commit: ${path2}`, changeIndex });
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
function addModifiedPath(paths, path2) {
|
|
2096
|
+
if (!paths.includes(path2))
|
|
2097
|
+
paths.push(path2);
|
|
2098
|
+
}
|
|
2099
|
+
function reportedPath(plan, path2) {
|
|
2100
|
+
return plan.reportedPathByCanonical.get(path2) ?? path2;
|
|
2101
|
+
}
|
|
2102
|
+
function changedPathsForMutation(mutation) {
|
|
2103
|
+
return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
|
|
2104
|
+
}
|
|
2105
|
+
function mutationDelta(operations) {
|
|
2106
|
+
const changedPaths = new Set;
|
|
2107
|
+
for (const operation of operations) {
|
|
2108
|
+
for (const path2 of changedPathsForMutation(operation))
|
|
2109
|
+
changedPaths.add(path2);
|
|
2110
|
+
}
|
|
2111
|
+
return { operations, changedPaths: [...changedPaths].sort() };
|
|
2112
|
+
}
|
|
2113
|
+
function resolveIo(overrides) {
|
|
2114
|
+
return {
|
|
2115
|
+
writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
|
|
2116
|
+
rename: overrides?.rename ?? DEFAULT_IO.rename,
|
|
2117
|
+
remove: overrides?.remove ?? DEFAULT_IO.remove
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
function commitOperation(context, operation) {
|
|
2121
|
+
const { plan, io, accumulator } = context;
|
|
2122
|
+
if (operation.kind === "noop")
|
|
2123
|
+
return;
|
|
2124
|
+
if (operation.kind === "text") {
|
|
2125
|
+
io.writeFile(operation.path, operation.afterText);
|
|
2126
|
+
accumulator.mutations.push({
|
|
2127
|
+
kind: "text",
|
|
2128
|
+
path: operation.path,
|
|
2129
|
+
beforeText: operation.beforeText,
|
|
2130
|
+
afterText: operation.afterText
|
|
2131
|
+
});
|
|
2132
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2133
|
+
accumulator.totalEdits += operation.editCount;
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
if (operation.kind === "create") {
|
|
2137
|
+
io.writeFile(operation.path, "");
|
|
2138
|
+
accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
|
|
2139
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
if (operation.kind === "rename") {
|
|
2143
|
+
if (operation.replaceDestination) {
|
|
2144
|
+
const targetKind = existsSync4(operation.newPath) && lstatSync3(operation.newPath).isDirectory() ? "directory" : "file";
|
|
2145
|
+
io.remove(operation.newPath, targetKind === "directory");
|
|
2146
|
+
accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
|
|
2147
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
2148
|
+
}
|
|
2149
|
+
io.rename(operation.oldPath, operation.newPath);
|
|
2150
|
+
accumulator.mutations.push({
|
|
2151
|
+
kind: "rename",
|
|
2152
|
+
oldPath: operation.oldPath,
|
|
2153
|
+
newPath: operation.newPath,
|
|
2154
|
+
sourceKind: operation.sourceKind
|
|
2155
|
+
});
|
|
2156
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
io.remove(operation.path, operation.recursive);
|
|
2160
|
+
accumulator.mutations.push({
|
|
2161
|
+
kind: "delete",
|
|
2162
|
+
path: operation.path,
|
|
2163
|
+
targetKind: operation.targetKind
|
|
2164
|
+
});
|
|
2165
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2166
|
+
}
|
|
2167
|
+
function commitWorkspaceEditPlan(plan, options = {}) {
|
|
2168
|
+
if (options.signal?.aborted) {
|
|
2169
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2170
|
+
}
|
|
2171
|
+
const stale = verifySnapshots(plan);
|
|
2172
|
+
if (stale)
|
|
2173
|
+
return stale;
|
|
2174
|
+
if (options.signal?.aborted) {
|
|
2175
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2176
|
+
}
|
|
2177
|
+
const io = resolveIo(options.io);
|
|
2178
|
+
const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
|
|
2179
|
+
const context = { plan, io, accumulator };
|
|
2180
|
+
let lateAbort = false;
|
|
2181
|
+
for (const operation of plan.operations) {
|
|
2182
|
+
try {
|
|
2183
|
+
commitOperation(context, operation);
|
|
2184
|
+
} catch (error) {
|
|
2185
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2186
|
+
return failedCommit(plan, {
|
|
2187
|
+
message: `I/O failure during ${operation.kind}: ${detail}`,
|
|
2188
|
+
changeIndex: operation.changeIndex,
|
|
2189
|
+
mutations: accumulator.mutations,
|
|
2190
|
+
filesModified: accumulator.filesModified,
|
|
2191
|
+
totalEdits: accumulator.totalEdits,
|
|
2192
|
+
lateAbort: lateAbort || options.signal?.aborted === true
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
if (options.signal?.aborted)
|
|
2196
|
+
lateAbort = true;
|
|
2197
|
+
}
|
|
2198
|
+
const result = {
|
|
2199
|
+
success: true,
|
|
2200
|
+
filesModified: accumulator.filesModified,
|
|
2201
|
+
totalEdits: accumulator.totalEdits,
|
|
2202
|
+
errors: [],
|
|
2203
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
2204
|
+
};
|
|
2205
|
+
return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
|
|
2206
|
+
}
|
|
2207
|
+
function canonicalFingerprint(operations) {
|
|
2208
|
+
const canonical = operations.map((operation) => {
|
|
2209
|
+
switch (operation.kind) {
|
|
2210
|
+
case "text":
|
|
2211
|
+
return {
|
|
2212
|
+
kind: operation.kind,
|
|
2213
|
+
changeIndex: operation.changeIndex,
|
|
2214
|
+
path: operation.path,
|
|
2215
|
+
edits: operation.edits,
|
|
2216
|
+
version: operation.version
|
|
2217
|
+
};
|
|
2218
|
+
case "rename":
|
|
2219
|
+
return {
|
|
2220
|
+
kind: operation.kind,
|
|
2221
|
+
changeIndex: operation.changeIndex,
|
|
2222
|
+
oldPath: operation.oldPath,
|
|
2223
|
+
newPath: operation.newPath,
|
|
2224
|
+
overwrite: operation.overwrite,
|
|
2225
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2226
|
+
};
|
|
2227
|
+
case "create":
|
|
2228
|
+
return {
|
|
2229
|
+
kind: operation.kind,
|
|
2230
|
+
changeIndex: operation.changeIndex,
|
|
2231
|
+
path: operation.path,
|
|
2232
|
+
overwrite: operation.overwrite,
|
|
2233
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2234
|
+
};
|
|
2235
|
+
case "delete":
|
|
2236
|
+
return {
|
|
2237
|
+
kind: operation.kind,
|
|
2238
|
+
changeIndex: operation.changeIndex,
|
|
2239
|
+
path: operation.path,
|
|
2240
|
+
recursive: operation.recursive,
|
|
2241
|
+
ignoreIfNotExists: operation.ignoreIfNotExists
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
});
|
|
2245
|
+
return createHash2("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
class WorkspaceEditValidationError extends Error {
|
|
2249
|
+
changeIndex;
|
|
2250
|
+
detail;
|
|
2251
|
+
name = "WorkspaceEditValidationError";
|
|
2252
|
+
constructor(changeIndex, detail) {
|
|
2253
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
2254
|
+
this.changeIndex = changeIndex;
|
|
2255
|
+
this.detail = detail;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
function isRecord3(value) {
|
|
2259
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2260
|
+
}
|
|
2261
|
+
function parsePosition(value) {
|
|
2262
|
+
if (!isRecord3(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
|
|
2263
|
+
return null;
|
|
2264
|
+
}
|
|
2265
|
+
return { line: value["line"], character: value["character"] };
|
|
2266
|
+
}
|
|
2267
|
+
function parseRange(value) {
|
|
2268
|
+
if (!isRecord3(value))
|
|
2269
|
+
return null;
|
|
2270
|
+
const start = parsePosition(value["start"]);
|
|
2271
|
+
const end = parsePosition(value["end"]);
|
|
2272
|
+
return start && end ? { start, end } : null;
|
|
2273
|
+
}
|
|
2274
|
+
function parseTextEdits(value, changeIndex) {
|
|
2275
|
+
if (!Array.isArray(value)) {
|
|
2276
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
|
|
2277
|
+
}
|
|
2278
|
+
const edits = [];
|
|
2279
|
+
for (const candidate of value) {
|
|
2280
|
+
if (!isRecord3(candidate) || typeof candidate["newText"] !== "string") {
|
|
2281
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
|
|
2282
|
+
}
|
|
2283
|
+
if ("annotationId" in candidate) {
|
|
2284
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
|
|
2285
|
+
}
|
|
2286
|
+
const range = parseRange(candidate["range"]);
|
|
2287
|
+
if (!range)
|
|
2288
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
|
|
2289
|
+
edits.push({ range, newText: candidate["newText"] });
|
|
2290
|
+
}
|
|
2291
|
+
return edits;
|
|
2292
|
+
}
|
|
2293
|
+
function parseBooleanOption(options, key, changeIndex) {
|
|
2294
|
+
const value = options[key];
|
|
2295
|
+
if (value === undefined)
|
|
2296
|
+
return false;
|
|
2297
|
+
if (typeof value !== "boolean") {
|
|
2298
|
+
throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
|
|
2299
|
+
}
|
|
2300
|
+
return value;
|
|
2301
|
+
}
|
|
2302
|
+
function parseOptions(value, allowed, changeIndex) {
|
|
2303
|
+
if (value === undefined)
|
|
2304
|
+
return {};
|
|
2305
|
+
if (!isRecord3(value))
|
|
2306
|
+
throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
|
|
2307
|
+
for (const key of Object.keys(value)) {
|
|
2308
|
+
if (!allowed.includes(key))
|
|
2309
|
+
throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
|
|
2310
|
+
}
|
|
2311
|
+
const parsed = {};
|
|
2312
|
+
for (const key of allowed)
|
|
2313
|
+
parsed[key] = parseBooleanOption(value, key, changeIndex);
|
|
2314
|
+
return parsed;
|
|
2315
|
+
}
|
|
2316
|
+
function parseResourceChange(input) {
|
|
2317
|
+
const kind = input.change["kind"];
|
|
2318
|
+
if (kind === "create" || kind === "delete") {
|
|
2319
|
+
parseSinglePathResource(input, kind);
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
if (kind !== "rename") {
|
|
2323
|
+
throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
|
|
2324
|
+
}
|
|
2325
|
+
parseRename(input);
|
|
2326
|
+
}
|
|
2327
|
+
function parseSinglePathResource(input, kind) {
|
|
2328
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2329
|
+
if (typeof change["uri"] !== "string")
|
|
2330
|
+
throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
|
|
2331
|
+
const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
|
|
2332
|
+
if (!resolvedPath.success) {
|
|
2333
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
if (kind === "create") {
|
|
2337
|
+
const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2338
|
+
target.operations.push({
|
|
2339
|
+
kind,
|
|
2340
|
+
changeIndex,
|
|
2341
|
+
path: resolvedPath.path,
|
|
2342
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2343
|
+
overwrite: options2["overwrite"] ?? false,
|
|
2344
|
+
ignoreIfExists: options2["ignoreIfExists"] ?? false,
|
|
2345
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2346
|
+
});
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
|
|
2350
|
+
target.operations.push({
|
|
2351
|
+
kind,
|
|
2352
|
+
changeIndex,
|
|
2353
|
+
path: resolvedPath.path,
|
|
2354
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2355
|
+
recursive: options["recursive"] ?? false,
|
|
2356
|
+
ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
|
|
2357
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
function parseRename(input) {
|
|
2361
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2362
|
+
if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
|
|
2363
|
+
throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
|
|
2364
|
+
}
|
|
2365
|
+
const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
|
|
2366
|
+
const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
|
|
2367
|
+
if (!oldPath.success || !newPath.success) {
|
|
2368
|
+
target.failures.push({
|
|
2369
|
+
changeIndex,
|
|
2370
|
+
message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
|
|
2371
|
+
});
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2375
|
+
target.operations.push({
|
|
2376
|
+
kind: "rename",
|
|
2377
|
+
changeIndex,
|
|
2378
|
+
oldPath: oldPath.path,
|
|
2379
|
+
newPath: newPath.path,
|
|
2380
|
+
reportedOldPath: oldPath.requestedPath,
|
|
2381
|
+
reportedNewPath: newPath.requestedPath,
|
|
2382
|
+
overwrite: options["overwrite"] ?? false,
|
|
2383
|
+
ignoreIfExists: options["ignoreIfExists"] ?? false,
|
|
2384
|
+
followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
function failureResult(failures) {
|
|
2388
|
+
const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
|
|
2389
|
+
const first = sorted[0];
|
|
2390
|
+
return {
|
|
2391
|
+
success: false,
|
|
2392
|
+
filesModified: [],
|
|
2393
|
+
totalEdits: 0,
|
|
2394
|
+
errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
|
|
2395
|
+
...first ? { failedChange: first.changeIndex } : {}
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
function parseWorkspaceEdit(edit, workspaceRoot) {
|
|
2399
|
+
if (!isRecord3(edit))
|
|
2400
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
|
|
2401
|
+
if (edit["changeAnnotations"] !== undefined) {
|
|
2402
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
|
|
2403
|
+
}
|
|
2404
|
+
const hasChanges = edit["changes"] !== undefined;
|
|
2405
|
+
const hasDocumentChanges = edit["documentChanges"] !== undefined;
|
|
2406
|
+
if (hasChanges && hasDocumentChanges) {
|
|
2407
|
+
return {
|
|
2408
|
+
operations: [],
|
|
2409
|
+
failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
const target = { operations: [], failures: [] };
|
|
2413
|
+
if (hasChanges)
|
|
2414
|
+
return parseChanges(edit["changes"], workspaceRoot, target);
|
|
2415
|
+
return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
|
|
2416
|
+
}
|
|
2417
|
+
function parseChanges(value, workspaceRoot, target) {
|
|
2418
|
+
if (!isRecord3(value))
|
|
2419
|
+
return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
|
|
2420
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
2421
|
+
for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
|
|
2422
|
+
const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
|
|
2423
|
+
if (!resolvedPath.success) {
|
|
2424
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
try {
|
|
2428
|
+
target.operations.push({
|
|
2429
|
+
kind: "text",
|
|
2430
|
+
changeIndex,
|
|
2431
|
+
path: resolvedPath.path,
|
|
2432
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2433
|
+
edits: parseTextEdits(rawEdits, changeIndex),
|
|
2434
|
+
version: null
|
|
2435
|
+
});
|
|
2436
|
+
} catch (error) {
|
|
2437
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2438
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
throw error;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
return target;
|
|
2445
|
+
}
|
|
2446
|
+
function parseDocumentChanges(value, workspaceRoot, target) {
|
|
2447
|
+
if (value === undefined)
|
|
2448
|
+
return target;
|
|
2449
|
+
if (!Array.isArray(value)) {
|
|
2450
|
+
return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
|
|
2451
|
+
}
|
|
2452
|
+
for (const [changeIndex, change] of value.entries()) {
|
|
2453
|
+
try {
|
|
2454
|
+
parseDocumentChange({ change, changeIndex, workspaceRoot, target });
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2457
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
throw error;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
return target;
|
|
2464
|
+
}
|
|
2465
|
+
function parseDocumentChange(input) {
|
|
2466
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2467
|
+
if (!isRecord3(change))
|
|
2468
|
+
throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
|
|
2469
|
+
if ("annotationId" in change) {
|
|
2470
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
|
|
2471
|
+
}
|
|
2472
|
+
if (typeof change["kind"] === "string") {
|
|
2473
|
+
parseResourceChange({ change, changeIndex, workspaceRoot, target });
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
const identifier = change["textDocument"];
|
|
2477
|
+
if (!isRecord3(identifier) || typeof identifier["uri"] !== "string") {
|
|
2478
|
+
throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
|
|
2479
|
+
}
|
|
2480
|
+
const version = identifier["version"];
|
|
2481
|
+
if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
|
|
2482
|
+
throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
|
|
2483
|
+
}
|
|
2484
|
+
const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
|
|
2485
|
+
if (!resolvedPath.success) {
|
|
2486
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
target.operations.push({
|
|
2490
|
+
kind: "text",
|
|
2491
|
+
changeIndex,
|
|
2492
|
+
path: resolvedPath.path,
|
|
2493
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2494
|
+
edits: parseTextEdits(change["edits"], changeIndex),
|
|
2495
|
+
version
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
function comparePosition(left, right) {
|
|
2499
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
2500
|
+
}
|
|
2501
|
+
function positionsEqual(left, right) {
|
|
2502
|
+
return left.line === right.line && left.character === right.character;
|
|
2503
|
+
}
|
|
2504
|
+
function rangesEqual(left, right) {
|
|
2505
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
2506
|
+
}
|
|
2507
|
+
function isEmptyRange(range) {
|
|
2508
|
+
return positionsEqual(range.start, range.end);
|
|
2509
|
+
}
|
|
2510
|
+
function formatRange(range) {
|
|
2511
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
2512
|
+
}
|
|
2513
|
+
function validatePosition(position, label, context) {
|
|
2514
|
+
const { lines, changeIndex } = context;
|
|
2515
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
2516
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
2517
|
+
}
|
|
2518
|
+
if (position.line < 0 || position.character < 0) {
|
|
2519
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
2520
|
+
}
|
|
2521
|
+
const line = lines[position.line];
|
|
2522
|
+
if (line === undefined) {
|
|
2523
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
2524
|
+
}
|
|
2525
|
+
if (position.character > line.length) {
|
|
2526
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
function validateRange(range, lines, changeIndex) {
|
|
2530
|
+
const context = { lines, changeIndex };
|
|
2531
|
+
validatePosition(range.start, "start", context);
|
|
2532
|
+
validatePosition(range.end, "end", context);
|
|
2533
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
2534
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
function sortAndDeduplicate(edits) {
|
|
2538
|
+
const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
|
|
2539
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
2540
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
2541
|
+
});
|
|
2542
|
+
const unique = [];
|
|
2543
|
+
for (const entry of sorted) {
|
|
2544
|
+
const previous = unique.at(-1);
|
|
2545
|
+
if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
2548
|
+
unique.push(entry.edit);
|
|
2549
|
+
}
|
|
2550
|
+
return unique;
|
|
2551
|
+
}
|
|
2552
|
+
function validateNoOverlap(edits, changeIndex) {
|
|
2553
|
+
for (let index = 0;index < edits.length - 1; index += 1) {
|
|
2554
|
+
const later = edits[index];
|
|
2555
|
+
const earlier = edits[index + 1];
|
|
2556
|
+
if (later === undefined || earlier === undefined)
|
|
2557
|
+
continue;
|
|
2558
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
2559
|
+
throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
function applyNormalizedTextEdits(content, edits) {
|
|
2564
|
+
const lines = content.split(`
|
|
2565
|
+
`);
|
|
2566
|
+
for (const edit of edits) {
|
|
2567
|
+
const { start, end } = edit.range;
|
|
2568
|
+
const startLine = lines[start.line];
|
|
2569
|
+
const endLine = lines[end.line];
|
|
2570
|
+
if (startLine === undefined || endLine === undefined)
|
|
2571
|
+
continue;
|
|
2572
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
2573
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
|
|
2574
|
+
`));
|
|
2575
|
+
}
|
|
2576
|
+
return lines.join(`
|
|
2577
|
+
`);
|
|
2578
|
+
}
|
|
2579
|
+
function normalizeTextEdits(content, edits, changeIndex) {
|
|
2580
|
+
const lines = content.split(`
|
|
2581
|
+
`);
|
|
2582
|
+
for (const edit of edits) {
|
|
2583
|
+
validateRange(edit.range, lines, changeIndex);
|
|
2584
|
+
}
|
|
2585
|
+
const normalized = sortAndDeduplicate(edits);
|
|
2586
|
+
validateNoOverlap(normalized, changeIndex);
|
|
2587
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
2588
|
+
}
|
|
2589
|
+
function isSameOrDescendant2(candidate, parent) {
|
|
2590
|
+
const relativePath = relative4(parent, candidate);
|
|
2591
|
+
return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
|
|
2592
|
+
}
|
|
2593
|
+
function removeVirtualSubtree(virtual, path2) {
|
|
2594
|
+
for (const candidate of [...virtual.keys()]) {
|
|
2595
|
+
if (isSameOrDescendant2(candidate, path2))
|
|
2596
|
+
virtual.delete(candidate);
|
|
2597
|
+
}
|
|
2598
|
+
virtual.set(path2, { kind: "missing" });
|
|
2599
|
+
}
|
|
2600
|
+
function moveVirtualSubtree(virtual, oldPath, newPath) {
|
|
2601
|
+
const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
|
|
2602
|
+
removeVirtualSubtree(virtual, oldPath);
|
|
2603
|
+
removeVirtualSubtree(virtual, newPath);
|
|
2604
|
+
for (const [candidate, entry] of moved) {
|
|
2605
|
+
const suffix = relative4(oldPath, candidate);
|
|
2606
|
+
virtual.set(suffix === "" ? newPath : resolve4(newPath, suffix), entry);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
function virtualDirectoryHasChildren(virtual, path2) {
|
|
2610
|
+
for (const [candidate, entry] of virtual) {
|
|
2611
|
+
if (candidate !== path2 && entry.kind !== "missing" && isSameOrDescendant2(candidate, path2))
|
|
2612
|
+
return true;
|
|
2613
|
+
}
|
|
2614
|
+
return false;
|
|
2615
|
+
}
|
|
2616
|
+
function requireVirtualParent(virtual, path2, changeIndex) {
|
|
2617
|
+
if (virtual.get(dirname5(path2))?.kind !== "directory") {
|
|
2618
|
+
throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path2}`);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
function simulateOperations(parsed, snapshots) {
|
|
2622
|
+
const virtual = new Map(snapshots);
|
|
2623
|
+
const planned = [];
|
|
2624
|
+
const failures = [];
|
|
2625
|
+
for (const operation of parsed) {
|
|
2626
|
+
try {
|
|
2627
|
+
planned.push(simulateOperation(operation, virtual));
|
|
2628
|
+
} catch (error) {
|
|
2629
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2630
|
+
failures.push({ changeIndex: operation.changeIndex, message: error.detail });
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
throw error;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
return { operations: planned, failures };
|
|
2637
|
+
}
|
|
2638
|
+
function simulateOperation(operation, virtual) {
|
|
2639
|
+
switch (operation.kind) {
|
|
2640
|
+
case "text":
|
|
2641
|
+
return simulateText(operation, virtual);
|
|
2642
|
+
case "create":
|
|
2643
|
+
return simulateCreate(operation, virtual);
|
|
2644
|
+
case "rename":
|
|
2645
|
+
return simulateRename(operation, virtual);
|
|
2646
|
+
case "delete":
|
|
2647
|
+
return simulateDelete(operation, virtual);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
function rejectSymbolicLink(operation) {
|
|
2651
|
+
if (operation.followedSymbolicLink) {
|
|
2652
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
function simulateText(operation, virtual) {
|
|
2656
|
+
const entry = virtual.get(operation.path);
|
|
2657
|
+
if (entry?.kind !== "file")
|
|
2658
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
|
|
2659
|
+
const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
|
|
2660
|
+
virtual.set(operation.path, { kind: "file", content: normalized.text });
|
|
2661
|
+
return {
|
|
2662
|
+
kind: "text",
|
|
2663
|
+
changeIndex: operation.changeIndex,
|
|
2664
|
+
path: operation.path,
|
|
2665
|
+
beforeText: entry.content,
|
|
2666
|
+
afterText: normalized.text,
|
|
2667
|
+
editCount: normalized.edits.length,
|
|
2668
|
+
documentVersion: operation.version
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
function simulateCreate(operation, virtual) {
|
|
2672
|
+
rejectSymbolicLink(operation);
|
|
2673
|
+
requireVirtualParent(virtual, operation.path, operation.changeIndex);
|
|
2674
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2675
|
+
if (target.kind !== "missing") {
|
|
2676
|
+
if (operation.overwrite && target.kind === "file") {
|
|
2677
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2678
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
|
|
2679
|
+
}
|
|
2680
|
+
if (operation.ignoreIfExists)
|
|
2681
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2682
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
|
|
2683
|
+
}
|
|
2684
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2685
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
|
|
2686
|
+
}
|
|
2687
|
+
function simulateRename(operation, virtual) {
|
|
2688
|
+
rejectSymbolicLink(operation);
|
|
2689
|
+
const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
|
|
2690
|
+
if (source.kind === "missing") {
|
|
2691
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
|
|
2692
|
+
}
|
|
2693
|
+
if (operation.oldPath === operation.newPath)
|
|
2694
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2695
|
+
if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
|
|
2696
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
|
|
2697
|
+
}
|
|
2698
|
+
requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
|
|
2699
|
+
const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
|
|
2700
|
+
if (destination.kind !== "missing" && !operation.overwrite) {
|
|
2701
|
+
if (operation.ignoreIfExists)
|
|
2702
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2703
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
|
|
2704
|
+
}
|
|
2705
|
+
moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
|
|
2706
|
+
return {
|
|
2707
|
+
kind: "rename",
|
|
2708
|
+
changeIndex: operation.changeIndex,
|
|
2709
|
+
oldPath: operation.oldPath,
|
|
2710
|
+
newPath: operation.newPath,
|
|
2711
|
+
sourceKind: source.kind,
|
|
2712
|
+
replaceDestination: destination.kind !== "missing"
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
function simulateDelete(operation, virtual) {
|
|
2716
|
+
rejectSymbolicLink(operation);
|
|
2717
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2718
|
+
if (target.kind === "missing") {
|
|
2719
|
+
if (operation.ignoreIfNotExists)
|
|
2720
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2721
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
|
|
2722
|
+
}
|
|
2723
|
+
if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
|
|
2724
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
|
|
2725
|
+
}
|
|
2726
|
+
removeVirtualSubtree(virtual, operation.path);
|
|
2727
|
+
return {
|
|
2728
|
+
kind: "delete",
|
|
2729
|
+
changeIndex: operation.changeIndex,
|
|
2730
|
+
path: operation.path,
|
|
2731
|
+
targetKind: target.kind,
|
|
2732
|
+
recursive: operation.recursive
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
class WorkspaceSnapshotBuilder {
|
|
2737
|
+
workspaceRoot;
|
|
2738
|
+
snapshots = new Map;
|
|
2739
|
+
constructor(workspaceRoot) {
|
|
2740
|
+
this.workspaceRoot = workspaceRoot;
|
|
2741
|
+
}
|
|
2742
|
+
build(operations) {
|
|
2743
|
+
this.add(this.workspaceRoot, false);
|
|
2744
|
+
for (const operation of operations) {
|
|
2745
|
+
switch (operation.kind) {
|
|
2746
|
+
case "rename":
|
|
2747
|
+
this.add(operation.oldPath, true);
|
|
2748
|
+
this.add(operation.newPath, true);
|
|
2749
|
+
break;
|
|
2750
|
+
case "delete":
|
|
2751
|
+
this.add(operation.path, true);
|
|
2752
|
+
break;
|
|
2753
|
+
case "text":
|
|
2754
|
+
case "create":
|
|
2755
|
+
this.add(operation.path, false);
|
|
2756
|
+
break;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
return this.snapshots;
|
|
2760
|
+
}
|
|
2761
|
+
add(path2, includeChildren) {
|
|
2762
|
+
let candidate = path2;
|
|
2763
|
+
while (true) {
|
|
2764
|
+
const existing = this.snapshots.get(candidate);
|
|
2765
|
+
if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
|
|
2766
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path2));
|
|
2767
|
+
}
|
|
2768
|
+
if (candidate === this.workspaceRoot)
|
|
2769
|
+
break;
|
|
2770
|
+
candidate = dirname6(candidate);
|
|
2771
|
+
}
|
|
2772
|
+
if (!includeChildren || !existsSync5(path2) || !lstatSync4(path2).isDirectory())
|
|
2773
|
+
return;
|
|
2774
|
+
for (const child of readdirSync2(path2))
|
|
2775
|
+
this.add(resolve5(path2, child), true);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
function snapshotOperations(operations, workspaceRoot) {
|
|
2779
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
class PlanPathIndex {
|
|
2783
|
+
firstChangeByPath = new Map;
|
|
2784
|
+
reportedPathByCanonical = new Map;
|
|
2785
|
+
build(operations) {
|
|
2786
|
+
for (const operation of operations) {
|
|
2787
|
+
switch (operation.kind) {
|
|
2788
|
+
case "rename":
|
|
2789
|
+
this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
|
|
2790
|
+
this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
|
|
2791
|
+
break;
|
|
2792
|
+
case "text":
|
|
2793
|
+
case "create":
|
|
2794
|
+
case "delete":
|
|
2795
|
+
this.add(operation.path, operation.reportedPath, operation.changeIndex);
|
|
2796
|
+
break;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
add(path2, reportedPath2, changeIndex) {
|
|
2801
|
+
if (!this.firstChangeByPath.has(path2))
|
|
2802
|
+
this.firstChangeByPath.set(path2, changeIndex);
|
|
2803
|
+
if (!this.reportedPathByCanonical.has(path2))
|
|
2804
|
+
this.reportedPathByCanonical.set(path2, reportedPath2);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
function fingerprintWorkspaceEdit(edit, workspaceRoot) {
|
|
2808
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2809
|
+
if (!root.success)
|
|
2810
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2811
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2812
|
+
if (parsed.failures.length > 0)
|
|
2813
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2814
|
+
return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
|
|
2815
|
+
}
|
|
2816
|
+
function planWorkspaceEdit(edit, workspaceRoot) {
|
|
2817
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2818
|
+
if (!root.success)
|
|
2819
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2820
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2821
|
+
if (parsed.failures.length > 0)
|
|
2822
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2823
|
+
let snapshots;
|
|
2824
|
+
try {
|
|
2825
|
+
snapshots = snapshotOperations(parsed.operations, root.path);
|
|
2826
|
+
} catch (error) {
|
|
2827
|
+
return {
|
|
2828
|
+
success: false,
|
|
2829
|
+
result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
const simulated = simulateOperations(parsed.operations, snapshots);
|
|
2833
|
+
if (simulated.failures.length > 0)
|
|
2834
|
+
return { success: false, result: failureResult(simulated.failures) };
|
|
2835
|
+
const paths = new PlanPathIndex;
|
|
2836
|
+
paths.build(parsed.operations);
|
|
2837
|
+
const plan = {
|
|
2838
|
+
workspaceRoot: root.path,
|
|
2839
|
+
operations: simulated.operations,
|
|
2840
|
+
snapshots,
|
|
2841
|
+
firstChangeByPath: paths.firstChangeByPath,
|
|
2842
|
+
reportedPathByCanonical: paths.reportedPathByCanonical,
|
|
2843
|
+
fingerprint: canonicalFingerprint(parsed.operations)
|
|
2844
|
+
};
|
|
2845
|
+
return { success: true, plan };
|
|
2846
|
+
}
|
|
2847
|
+
function failure(message, failedChange, base) {
|
|
2848
|
+
return {
|
|
2849
|
+
success: false,
|
|
2850
|
+
filesModified: base?.filesModified ?? [],
|
|
2851
|
+
totalEdits: base?.totalEdits ?? 0,
|
|
2852
|
+
errors: [message],
|
|
2853
|
+
...failedChange === undefined ? {} : { failedChange },
|
|
2854
|
+
...base?.lateAbort ? { lateAbort: true } : {}
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
function responseFor(result) {
|
|
2858
|
+
if (result.success)
|
|
2859
|
+
return { applied: true };
|
|
2860
|
+
return {
|
|
2861
|
+
applied: false,
|
|
2862
|
+
failureReason: result.errors[0] ?? "workspace edit failed",
|
|
2863
|
+
...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
|
|
2864
|
+
};
|
|
2865
|
+
}
|
|
2866
|
+
function isRecord4(value) {
|
|
2867
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
class WorkspaceMutationController {
|
|
2871
|
+
workspaceRoot;
|
|
2872
|
+
documents;
|
|
2873
|
+
activeLease = null;
|
|
2874
|
+
nextLeaseId = 1;
|
|
2875
|
+
io;
|
|
2876
|
+
constructor(workspaceRoot, documents) {
|
|
2877
|
+
this.workspaceRoot = workspaceRoot;
|
|
2878
|
+
this.documents = documents;
|
|
2879
|
+
}
|
|
2880
|
+
setIo(io) {
|
|
2881
|
+
this.io = io;
|
|
2882
|
+
}
|
|
2883
|
+
acquire(signal) {
|
|
2884
|
+
if (this.activeLease)
|
|
2885
|
+
return { success: false, result: failure("workspace mutation is already in progress") };
|
|
2886
|
+
if (signal?.aborted)
|
|
2887
|
+
return { success: false, result: failure("cancelled before mutating request") };
|
|
2888
|
+
const lease = {
|
|
2889
|
+
id: this.nextLeaseId,
|
|
2890
|
+
phase: "idle",
|
|
2891
|
+
...signal === undefined ? {} : { signal }
|
|
2892
|
+
};
|
|
2893
|
+
this.nextLeaseId += 1;
|
|
2894
|
+
this.activeLease = lease;
|
|
2895
|
+
return { success: true, lease };
|
|
2896
|
+
}
|
|
2897
|
+
release(lease) {
|
|
2898
|
+
if (this.activeLease?.id !== lease.id)
|
|
2899
|
+
return;
|
|
2900
|
+
this.activeLease.phase = "sealed";
|
|
2901
|
+
this.activeLease = null;
|
|
2902
|
+
}
|
|
2903
|
+
isBeforeCommit(lease) {
|
|
2904
|
+
return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
|
|
2905
|
+
}
|
|
2906
|
+
async handleApplyEdit(params) {
|
|
2907
|
+
const lease = this.activeLease;
|
|
2908
|
+
if (!lease)
|
|
2909
|
+
return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
|
|
2910
|
+
if (lease.phase !== "idle") {
|
|
2911
|
+
return {
|
|
2912
|
+
applied: false,
|
|
2913
|
+
failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
lease.phase = "applying";
|
|
2917
|
+
lease.applyCompletion = new Promise((resolve62) => {
|
|
2918
|
+
lease.resolveApply = resolve62;
|
|
2919
|
+
});
|
|
2920
|
+
const edit = isRecord4(params) ? params["edit"] : undefined;
|
|
2921
|
+
const record2 = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
|
|
2922
|
+
lease.serverApply = record2;
|
|
2923
|
+
lease.phase = "settled";
|
|
2924
|
+
lease.resolveApply?.();
|
|
2925
|
+
return responseFor(record2.result);
|
|
2926
|
+
}
|
|
2927
|
+
async reconcileRename(leaseToken, edit) {
|
|
2928
|
+
const lease = this.requireActiveLease(leaseToken);
|
|
2929
|
+
if (!lease)
|
|
2930
|
+
return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
|
|
2931
|
+
if (lease.phase === "applying")
|
|
2932
|
+
await lease.applyCompletion;
|
|
2933
|
+
if (lease.serverApply)
|
|
2934
|
+
return this.reconcileServerApply(lease.serverApply, edit);
|
|
2935
|
+
lease.phase = "sealed";
|
|
2936
|
+
if (!edit)
|
|
2937
|
+
return { edit, apply: failure("No edit provided") };
|
|
2938
|
+
const applied = await this.applyEdit(edit, lease);
|
|
2939
|
+
return { edit, apply: applied.result };
|
|
2940
|
+
}
|
|
2941
|
+
reconcileServerApply(record2, edit) {
|
|
2942
|
+
if (!edit)
|
|
2943
|
+
return { edit, apply: record2.result };
|
|
2944
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
|
|
2945
|
+
if (fingerprint.success && record2.fingerprint !== null && fingerprint.fingerprint === record2.fingerprint) {
|
|
2946
|
+
return { edit, apply: record2.result };
|
|
2947
|
+
}
|
|
2948
|
+
return {
|
|
2949
|
+
edit,
|
|
2950
|
+
apply: failure("rename result conflicts with server-applied workspace edit", 0, record2.result)
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
async applyEdit(edit, lease) {
|
|
2954
|
+
const planned = planWorkspaceEdit(edit, this.workspaceRoot);
|
|
2955
|
+
if (!planned.success)
|
|
2956
|
+
return { fingerprint: null, result: planned.result };
|
|
2957
|
+
const versionFailure = this.documents.validateVersions(planned.plan.operations);
|
|
2958
|
+
if (versionFailure) {
|
|
2959
|
+
return {
|
|
2960
|
+
fingerprint: planned.plan.fingerprint,
|
|
2961
|
+
result: failure(versionFailure.message, versionFailure.changeIndex)
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
const commit = commitWorkspaceEditPlan(planned.plan, {
|
|
2965
|
+
...lease.signal === undefined ? {} : { signal: lease.signal },
|
|
2966
|
+
...this.io === undefined ? {} : { io: this.io }
|
|
2967
|
+
});
|
|
2968
|
+
let result = commit.result;
|
|
2969
|
+
if (commit.delta.operations.length > 0) {
|
|
2970
|
+
try {
|
|
2971
|
+
await this.documents.synchronize(commit.delta);
|
|
2972
|
+
} catch (error) {
|
|
2973
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2974
|
+
result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
if (lease.signal?.aborted && !result.lateAbort)
|
|
2978
|
+
result = { ...result, lateAbort: true };
|
|
2979
|
+
return { fingerprint: planned.plan.fingerprint, result };
|
|
2980
|
+
}
|
|
2981
|
+
requireActiveLease(lease) {
|
|
2982
|
+
return this.activeLease?.id === lease.id ? this.activeLease : null;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
|
|
2986
|
+
var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
2987
|
+
|
|
2988
|
+
class LspClient extends LspClientConnection {
|
|
2989
|
+
diagnosticPullErrors = [];
|
|
2990
|
+
documents;
|
|
2991
|
+
workspaceMutations;
|
|
2992
|
+
diagnosticsFreshnessTimeoutMs;
|
|
2993
|
+
constructor(root, server2, options = {}) {
|
|
2994
|
+
super(root, server2, options);
|
|
2995
|
+
this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
|
|
2996
|
+
this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
|
|
2997
|
+
versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
|
|
1056
2998
|
});
|
|
2999
|
+
this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
|
|
3000
|
+
this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
|
|
1057
3001
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
3002
|
+
getDiagnosticPullErrors() {
|
|
3003
|
+
return this.diagnosticPullErrors;
|
|
3004
|
+
}
|
|
3005
|
+
async openFile(filePath) {
|
|
3006
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
3007
|
+
await this.documents.openFile(absPath);
|
|
3008
|
+
}
|
|
3009
|
+
getOpenDocumentVersion(filePath) {
|
|
3010
|
+
return this.documents.getVersion(this.resolveWorkspacePath(filePath));
|
|
3011
|
+
}
|
|
3012
|
+
getStoredDiagnostics(uri) {
|
|
3013
|
+
return [...this.documents.getStoredDiagnostics(uri)];
|
|
3014
|
+
}
|
|
3015
|
+
setWorkspaceEditIo(io) {
|
|
3016
|
+
this.workspaceMutations.setIo(io);
|
|
3017
|
+
}
|
|
3018
|
+
handlePublishDiagnostics(params) {
|
|
3019
|
+
super.handlePublishDiagnostics(params);
|
|
3020
|
+
this.documents.recordPublishedDiagnostics(params);
|
|
3021
|
+
}
|
|
3022
|
+
async definition(filePath, line, character, signal) {
|
|
3023
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1060
3024
|
await this.openFile(absPath);
|
|
3025
|
+
const options = signal === undefined ? {} : { signal };
|
|
1061
3026
|
return this.sendRequest("textDocument/definition", {
|
|
1062
|
-
textDocument: { uri:
|
|
3027
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1063
3028
|
position: { line: line - 1, character }
|
|
1064
|
-
});
|
|
3029
|
+
}, options);
|
|
1065
3030
|
}
|
|
1066
|
-
async references(filePath, line, character, includeDeclaration = true) {
|
|
1067
|
-
const absPath =
|
|
3031
|
+
async references(filePath, line, character, includeDeclaration = true, signal) {
|
|
3032
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1068
3033
|
await this.openFile(absPath);
|
|
3034
|
+
const options = signal === undefined ? {} : { signal };
|
|
1069
3035
|
return this.sendRequest("textDocument/references", {
|
|
1070
|
-
textDocument: { uri:
|
|
3036
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1071
3037
|
position: { line: line - 1, character },
|
|
1072
3038
|
context: { includeDeclaration }
|
|
1073
|
-
});
|
|
3039
|
+
}, options);
|
|
1074
3040
|
}
|
|
1075
|
-
async documentSymbols(filePath) {
|
|
1076
|
-
const absPath =
|
|
3041
|
+
async documentSymbols(filePath, signal) {
|
|
3042
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1077
3043
|
await this.openFile(absPath);
|
|
3044
|
+
const options = signal === undefined ? {} : { signal };
|
|
1078
3045
|
return this.sendRequest("textDocument/documentSymbol", {
|
|
1079
|
-
textDocument: { uri:
|
|
1080
|
-
});
|
|
3046
|
+
textDocument: { uri: pathToFileURL3(absPath).href }
|
|
3047
|
+
}, options);
|
|
1081
3048
|
}
|
|
1082
|
-
async workspaceSymbols(query) {
|
|
1083
|
-
|
|
3049
|
+
async workspaceSymbols(query, signal) {
|
|
3050
|
+
const options = signal === undefined ? {} : { signal };
|
|
3051
|
+
return this.sendRequest("workspace/symbol", { query }, options);
|
|
1084
3052
|
}
|
|
1085
3053
|
isUnsupportedDiagnosticPullError(error) {
|
|
1086
3054
|
if (!(error instanceof Error))
|
|
@@ -1090,42 +3058,173 @@ class LspClient extends LspClientConnection {
|
|
|
1090
3058
|
return true;
|
|
1091
3059
|
return /unsupported|not supported|method not found|unknown request/i.test(error.message);
|
|
1092
3060
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
1100
|
-
textDocument: { uri }
|
|
1101
|
-
});
|
|
1102
|
-
if (result.items) {
|
|
1103
|
-
return { items: result.items };
|
|
3061
|
+
freshnessTimeout(absPath) {
|
|
3062
|
+
return {
|
|
3063
|
+
items: [],
|
|
3064
|
+
transientError: {
|
|
3065
|
+
kind: "freshness_timeout",
|
|
3066
|
+
message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
|
|
1104
3067
|
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
parseDiagnosticPullReport(value) {
|
|
3071
|
+
if (value.kind === "unchanged") {
|
|
3072
|
+
return {
|
|
3073
|
+
type: "unchanged",
|
|
3074
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
return {
|
|
3078
|
+
type: "full",
|
|
3079
|
+
diagnostics: value.items ?? [],
|
|
3080
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
async diagnostics(filePath, signal) {
|
|
3084
|
+
signal?.throwIfAborted();
|
|
3085
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
3086
|
+
const uri = pathToFileURL3(absPath).href;
|
|
3087
|
+
await this.openFile(absPath);
|
|
3088
|
+
const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
|
|
3089
|
+
for (;; ) {
|
|
3090
|
+
signal?.throwIfAborted();
|
|
3091
|
+
const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
|
|
3092
|
+
if (!snapshot)
|
|
3093
|
+
return this.freshnessTimeout(absPath);
|
|
3094
|
+
const push = this.documents.resolvePushDiagnostics(snapshot);
|
|
3095
|
+
if (push.status === "ready")
|
|
3096
|
+
return { items: [...push.diagnostics] };
|
|
3097
|
+
let pushFallbackOnly = !this.isDiagnosticPullSupported();
|
|
3098
|
+
if (!pushFallbackOnly) {
|
|
3099
|
+
const cached = this.documents.getPullCache(snapshot);
|
|
3100
|
+
try {
|
|
3101
|
+
const remainingMs2 = deadlineAt - Date.now();
|
|
3102
|
+
if (remainingMs2 <= 0)
|
|
3103
|
+
return this.freshnessTimeout(absPath);
|
|
3104
|
+
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
3105
|
+
textDocument: { uri },
|
|
3106
|
+
...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
|
|
3107
|
+
}, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
|
|
3108
|
+
if (!this.documents.isCurrentSnapshot(snapshot))
|
|
3109
|
+
continue;
|
|
3110
|
+
const report = this.parseDiagnosticPullReport(result);
|
|
3111
|
+
if (report.type === "full") {
|
|
3112
|
+
this.documents.recordPullDiagnostics(snapshot, {
|
|
3113
|
+
kind: "full",
|
|
3114
|
+
diagnostics: report.diagnostics,
|
|
3115
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
3116
|
+
});
|
|
3117
|
+
return { items: [...report.diagnostics] };
|
|
3118
|
+
}
|
|
3119
|
+
if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
|
|
3120
|
+
return { items: [...cached.diagnostics] };
|
|
3121
|
+
}
|
|
3122
|
+
} catch (error) {
|
|
3123
|
+
if (this.isUnsupportedDiagnosticPullError(error)) {
|
|
3124
|
+
this.setDiagnosticPullSupported(false);
|
|
3125
|
+
pushFallbackOnly = true;
|
|
3126
|
+
} else if (error instanceof LspRequestTimeoutError) {
|
|
3127
|
+
pushFallbackOnly = true;
|
|
3128
|
+
} else {
|
|
3129
|
+
this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
3130
|
+
throw error;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
1108
3133
|
}
|
|
3134
|
+
if (!pushFallbackOnly)
|
|
3135
|
+
continue;
|
|
3136
|
+
const remainingMs = deadlineAt - Date.now();
|
|
3137
|
+
if (remainingMs <= 0)
|
|
3138
|
+
return this.freshnessTimeout(absPath);
|
|
3139
|
+
const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
|
|
3140
|
+
await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
|
|
1109
3141
|
}
|
|
1110
|
-
return { items: this.getStoredDiagnostics(uri) };
|
|
1111
3142
|
}
|
|
1112
|
-
async prepareRename(filePath, line, character) {
|
|
1113
|
-
const absPath =
|
|
3143
|
+
async prepareRename(filePath, line, character, signal) {
|
|
3144
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1114
3145
|
await this.openFile(absPath);
|
|
3146
|
+
const options = signal === undefined ? {} : { signal };
|
|
1115
3147
|
return this.sendRequest("textDocument/prepareRename", {
|
|
1116
|
-
textDocument: { uri:
|
|
3148
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1117
3149
|
position: { line: line - 1, character }
|
|
1118
|
-
});
|
|
3150
|
+
}, options);
|
|
3151
|
+
}
|
|
3152
|
+
async rename(filePath, line, character, newName, signal) {
|
|
3153
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
3154
|
+
await this.openFile(absPath);
|
|
3155
|
+
const acquired = this.workspaceMutations.acquire(signal);
|
|
3156
|
+
if (!acquired.success)
|
|
3157
|
+
return { edit: null, apply: acquired.result };
|
|
3158
|
+
const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
|
|
3159
|
+
try {
|
|
3160
|
+
const renameParams = {
|
|
3161
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
3162
|
+
position: { line: line - 1, character },
|
|
3163
|
+
newName
|
|
3164
|
+
};
|
|
3165
|
+
const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
|
|
3166
|
+
signal: preCommitSignal.signal
|
|
3167
|
+
});
|
|
3168
|
+
return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
|
|
3169
|
+
} finally {
|
|
3170
|
+
preCommitSignal?.dispose();
|
|
3171
|
+
this.workspaceMutations.release(acquired.lease);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
resolveWorkspacePath(filePath) {
|
|
3175
|
+
return resolve6(this.root, filePath);
|
|
1119
3176
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
return
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
3177
|
+
}
|
|
3178
|
+
function waitForDiagnosticsActivity(wait, signal) {
|
|
3179
|
+
if (!signal)
|
|
3180
|
+
return wait;
|
|
3181
|
+
if (signal.aborted)
|
|
3182
|
+
return Promise.reject(abortError3(signal));
|
|
3183
|
+
return new Promise((resolve72, reject) => {
|
|
3184
|
+
const onAbort = () => {
|
|
3185
|
+
signal.removeEventListener("abort", onAbort);
|
|
3186
|
+
reject(abortError3(signal));
|
|
3187
|
+
};
|
|
3188
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3189
|
+
wait.then(() => {
|
|
3190
|
+
signal.removeEventListener("abort", onAbort);
|
|
3191
|
+
resolve72();
|
|
3192
|
+
}, (error) => {
|
|
3193
|
+
signal.removeEventListener("abort", onAbort);
|
|
3194
|
+
reject(error);
|
|
1127
3195
|
});
|
|
1128
|
-
}
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
function createPreCommitAbortSignal(source, isBeforeCommit) {
|
|
3199
|
+
if (!source)
|
|
3200
|
+
return;
|
|
3201
|
+
const controller = new AbortController;
|
|
3202
|
+
const onAbort = () => {
|
|
3203
|
+
if (isBeforeCommit() && !controller.signal.aborted)
|
|
3204
|
+
controller.abort(preCommitAbortReason(source));
|
|
3205
|
+
};
|
|
3206
|
+
if (source.aborted)
|
|
3207
|
+
onAbort();
|
|
3208
|
+
else
|
|
3209
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
3210
|
+
return {
|
|
3211
|
+
signal: controller.signal,
|
|
3212
|
+
dispose: () => source.removeEventListener("abort", onAbort)
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
function preCommitAbortReason(source) {
|
|
3216
|
+
const reason = source.reason;
|
|
3217
|
+
if (reason instanceof Error && reason.name !== "AbortError")
|
|
3218
|
+
return reason;
|
|
3219
|
+
return new Error("LSP request cancelled before workspace edit commit");
|
|
3220
|
+
}
|
|
3221
|
+
function abortError3(signal) {
|
|
3222
|
+
const reason = signal.reason;
|
|
3223
|
+
if (reason instanceof Error)
|
|
3224
|
+
return reason;
|
|
3225
|
+
const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
|
|
3226
|
+
error.name = "AbortError";
|
|
3227
|
+
return error;
|
|
1129
3228
|
}
|
|
1130
3229
|
function installProcessSignalCleanup(cleanup) {
|
|
1131
3230
|
const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM"];
|
|
@@ -1150,10 +3249,10 @@ async function stopClientBestEffort(client) {
|
|
|
1150
3249
|
reportBestEffortCleanupError("client stop", error);
|
|
1151
3250
|
}
|
|
1152
3251
|
}
|
|
1153
|
-
function
|
|
3252
|
+
function awaitWithSignal2(promise, signal) {
|
|
1154
3253
|
if (!signal)
|
|
1155
3254
|
return promise;
|
|
1156
|
-
return new Promise((
|
|
3255
|
+
return new Promise((resolve72, reject) => {
|
|
1157
3256
|
let settled = false;
|
|
1158
3257
|
const onAbort = () => {
|
|
1159
3258
|
if (settled)
|
|
@@ -1171,7 +3270,7 @@ function awaitWithSignal(promise, signal) {
|
|
|
1171
3270
|
return;
|
|
1172
3271
|
settled = true;
|
|
1173
3272
|
signal.removeEventListener("abort", onAbort);
|
|
1174
|
-
|
|
3273
|
+
resolve72(value);
|
|
1175
3274
|
}, (err) => {
|
|
1176
3275
|
if (settled)
|
|
1177
3276
|
return;
|
|
@@ -1196,7 +3295,7 @@ class LspManager {
|
|
|
1196
3295
|
this.idleTimeoutMs = options.idleTimeoutMs ?? IDLE_TIMEOUT_MS;
|
|
1197
3296
|
this.initTimeoutMs = options.initTimeoutMs ?? INIT_TIMEOUT_MS;
|
|
1198
3297
|
this.reaperIntervalMs = options.reaperIntervalMs ?? REAPER_INTERVAL_MS;
|
|
1199
|
-
this.clientFactory = options.clientFactory ?? ((root,
|
|
3298
|
+
this.clientFactory = options.clientFactory ?? ((root, server2) => new LspClient(root, server2));
|
|
1200
3299
|
this.now = options.now ?? (() => Date.now());
|
|
1201
3300
|
this.startReaper();
|
|
1202
3301
|
this.signalDisposer = installProcessSignalCleanup(() => this.stopAll());
|
|
@@ -1234,12 +3333,12 @@ class LspManager {
|
|
|
1234
3333
|
await stopClientBestEffort(managed.client);
|
|
1235
3334
|
}
|
|
1236
3335
|
}
|
|
1237
|
-
async getClient(root,
|
|
3336
|
+
async getClient(root, server2, signal) {
|
|
1238
3337
|
if (this.disposed) {
|
|
1239
3338
|
throw new Error("LspManager has been disposed");
|
|
1240
3339
|
}
|
|
1241
3340
|
signal?.throwIfAborted();
|
|
1242
|
-
const key = this.getKey(root,
|
|
3341
|
+
const key = this.getKey(root, server2.id);
|
|
1243
3342
|
let managed = this.clients.get(key);
|
|
1244
3343
|
if (managed) {
|
|
1245
3344
|
const t = this.now();
|
|
@@ -1253,7 +3352,7 @@ class LspManager {
|
|
|
1253
3352
|
if (managed.initPromise) {
|
|
1254
3353
|
managed.pendingWaiters++;
|
|
1255
3354
|
try {
|
|
1256
|
-
await
|
|
3355
|
+
await awaitWithSignal2(managed.initPromise, signal);
|
|
1257
3356
|
} catch (err) {
|
|
1258
3357
|
managed.pendingWaiters--;
|
|
1259
3358
|
await this.tryDeleteIfOrphaned(key, managed);
|
|
@@ -1268,13 +3367,13 @@ class LspManager {
|
|
|
1268
3367
|
if (!managed.client.isAlive()) {
|
|
1269
3368
|
await stopClientBestEffort(managed.client);
|
|
1270
3369
|
this.clients.delete(key);
|
|
1271
|
-
return this.getClient(root,
|
|
3370
|
+
return this.getClient(root, server2, signal);
|
|
1272
3371
|
}
|
|
1273
3372
|
managed.refCount++;
|
|
1274
3373
|
managed.lastUsedAt = this.now();
|
|
1275
3374
|
return managed.client;
|
|
1276
3375
|
}
|
|
1277
|
-
const client = this.clientFactory(root,
|
|
3376
|
+
const client = this.clientFactory(root, server2);
|
|
1278
3377
|
const initStartedAt = this.now();
|
|
1279
3378
|
const initPromise = (async () => {
|
|
1280
3379
|
await client.start();
|
|
@@ -1291,7 +3390,7 @@ class LspManager {
|
|
|
1291
3390
|
};
|
|
1292
3391
|
this.clients.set(key, newManaged);
|
|
1293
3392
|
try {
|
|
1294
|
-
await
|
|
3393
|
+
await awaitWithSignal2(initPromise, signal);
|
|
1295
3394
|
} catch (err) {
|
|
1296
3395
|
newManaged.pendingWaiters--;
|
|
1297
3396
|
if (this.clients.get(key) === newManaged) {
|
|
@@ -1330,13 +3429,13 @@ class LspManager {
|
|
|
1330
3429
|
this.clients.delete(key);
|
|
1331
3430
|
stopClientBestEffort(managed.client);
|
|
1332
3431
|
}
|
|
1333
|
-
warmupClient(root,
|
|
3432
|
+
warmupClient(root, server2) {
|
|
1334
3433
|
if (this.disposed)
|
|
1335
3434
|
return;
|
|
1336
|
-
const key = this.getKey(root,
|
|
3435
|
+
const key = this.getKey(root, server2.id);
|
|
1337
3436
|
if (this.clients.has(key))
|
|
1338
3437
|
return;
|
|
1339
|
-
const client = this.clientFactory(root,
|
|
3438
|
+
const client = this.clientFactory(root, server2);
|
|
1340
3439
|
const initStartedAt = this.now();
|
|
1341
3440
|
const initPromise = (async () => {
|
|
1342
3441
|
await client.start();
|
|
@@ -1416,230 +3515,15 @@ function getLspManager() {
|
|
|
1416
3515
|
}
|
|
1417
3516
|
return _defaultInstance;
|
|
1418
3517
|
}
|
|
1419
|
-
async function disposeDefaultLspManager() {
|
|
1420
|
-
if (_defaultInstance) {
|
|
1421
|
-
const m = _defaultInstance;
|
|
1422
|
-
_defaultInstance = null;
|
|
1423
|
-
await m.stopAll();
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
function isPlainRecord(value) {
|
|
1427
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1428
|
-
}
|
|
1429
|
-
function isProcessAlive(pid) {
|
|
1430
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
1431
|
-
return false;
|
|
1432
|
-
try {
|
|
1433
|
-
process.kill(pid, 0);
|
|
1434
|
-
return true;
|
|
1435
|
-
} catch (error) {
|
|
1436
|
-
return error.code === "EPERM";
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
function readLockPid(lockPath) {
|
|
1440
|
-
try {
|
|
1441
|
-
const pid = Number.parseInt(readFileSync2(lockPath, "utf8").trim(), 10);
|
|
1442
|
-
return Number.isInteger(pid) ? pid : null;
|
|
1443
|
-
} catch {
|
|
1444
|
-
return null;
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
function tryAcquireLock(lockPath, ownerPid = process.pid) {
|
|
1448
|
-
mkdirSync(dirname(lockPath), { recursive: true });
|
|
1449
|
-
for (let attempt = 0;attempt < 2; attempt += 1) {
|
|
1450
|
-
const handle = writeLockFile(lockPath, ownerPid);
|
|
1451
|
-
if (handle)
|
|
1452
|
-
return handle;
|
|
1453
|
-
if (!reapStaleLock(lockPath))
|
|
1454
|
-
return null;
|
|
1455
|
-
}
|
|
1456
|
-
return null;
|
|
1457
|
-
}
|
|
1458
|
-
function writeLockFile(lockPath, ownerPid) {
|
|
1459
|
-
try {
|
|
1460
|
-
const fd = openSync(lockPath, "wx");
|
|
1461
|
-
writeSync(fd, `${ownerPid}
|
|
1462
|
-
`);
|
|
1463
|
-
closeSync(fd);
|
|
1464
|
-
return { release: () => unlinkQuietly(lockPath) };
|
|
1465
|
-
} catch (error) {
|
|
1466
|
-
if (error.code === "EEXIST")
|
|
1467
|
-
return null;
|
|
1468
|
-
throw error;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
function reapStaleLock(lockPath) {
|
|
1472
|
-
const pid = readLockPid(lockPath);
|
|
1473
|
-
if (pid !== null && isProcessAlive(pid))
|
|
1474
|
-
return false;
|
|
1475
|
-
unlinkQuietly(lockPath);
|
|
1476
|
-
return true;
|
|
1477
|
-
}
|
|
1478
|
-
function unlinkQuietly(path) {
|
|
1479
|
-
try {
|
|
1480
|
-
unlinkSync(path);
|
|
1481
|
-
} catch (error) {}
|
|
1482
|
-
}
|
|
1483
|
-
var PROBE_TIMEOUT_MS = 500;
|
|
1484
|
-
var DEFAULT_READY_TIMEOUT_MS = 5000;
|
|
1485
|
-
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
1486
|
-
var CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
|
|
1487
|
-
|
|
1488
|
-
class DaemonUnreachableError extends Error {
|
|
1489
|
-
constructor(socketPath) {
|
|
1490
|
-
super(`LSP daemon did not become reachable at ${socketPath}`);
|
|
1491
|
-
this.name = "DaemonUnreachableError";
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
async function ensureDaemonRunning(paths, deps = defaultEnsureDaemonDeps(), options = {}) {
|
|
1495
|
-
const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
1496
|
-
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1497
|
-
if (await deps.probe(paths.socket))
|
|
1498
|
-
return;
|
|
1499
|
-
const lock = deps.acquireLock(paths.lock);
|
|
1500
|
-
if (!lock) {
|
|
1501
|
-
await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1504
|
-
try {
|
|
1505
|
-
if (await deps.probe(paths.socket))
|
|
1506
|
-
return;
|
|
1507
|
-
deps.cleanupStaleSocket(paths.socket);
|
|
1508
|
-
deps.spawnDaemon(paths);
|
|
1509
|
-
await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
|
|
1510
|
-
} finally {
|
|
1511
|
-
lock.release();
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
async function waitUntilReachable(socketPath, deps, readyTimeoutMs, pollIntervalMs) {
|
|
1515
|
-
const deadline = deps.now() + readyTimeoutMs;
|
|
1516
|
-
for (;; ) {
|
|
1517
|
-
if (await deps.probe(socketPath))
|
|
1518
|
-
return;
|
|
1519
|
-
if (deps.now() >= deadline)
|
|
1520
|
-
throw new DaemonUnreachableError(socketPath);
|
|
1521
|
-
await deps.sleep(pollIntervalMs);
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
function probeSocket(socketPath, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
1525
|
-
return new Promise((resolve22) => {
|
|
1526
|
-
const socket = connect(socketPath);
|
|
1527
|
-
const finish = (ok) => {
|
|
1528
|
-
socket.destroy();
|
|
1529
|
-
resolve22(ok);
|
|
1530
|
-
};
|
|
1531
|
-
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
1532
|
-
timer.unref?.();
|
|
1533
|
-
socket.once("connect", () => {
|
|
1534
|
-
clearTimeout(timer);
|
|
1535
|
-
finish(true);
|
|
1536
|
-
});
|
|
1537
|
-
socket.once("error", () => {
|
|
1538
|
-
clearTimeout(timer);
|
|
1539
|
-
finish(false);
|
|
1540
|
-
});
|
|
1541
|
-
});
|
|
1542
|
-
}
|
|
1543
|
-
function spawnDaemonProcess(paths) {
|
|
1544
|
-
mkdirSync2(dirname2(paths.log), { recursive: true });
|
|
1545
|
-
const logFd = openSync2(paths.log, "a");
|
|
1546
|
-
try {
|
|
1547
|
-
const cliPath = resolveDaemonCliPath();
|
|
1548
|
-
const child = spawn2(execPath, [cliPath, "daemon"], {
|
|
1549
|
-
detached: true,
|
|
1550
|
-
stdio: ["ignore", logFd, logFd]
|
|
1551
|
-
});
|
|
1552
|
-
child.unref();
|
|
1553
|
-
} finally {
|
|
1554
|
-
closeSync2(logFd);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
function resolveDaemonCliPath(env = process.env) {
|
|
1558
|
-
const override = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
|
|
1559
|
-
if (override)
|
|
1560
|
-
return override;
|
|
1561
|
-
return fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
1562
|
-
}
|
|
1563
|
-
function defaultEnsureDaemonDeps() {
|
|
1564
|
-
return {
|
|
1565
|
-
probe: (socketPath) => probeSocket(socketPath),
|
|
1566
|
-
acquireLock: (lockPath) => tryAcquireLock(lockPath),
|
|
1567
|
-
cleanupStaleSocket: (socketPath) => {
|
|
1568
|
-
if (existsSync2(socketPath))
|
|
1569
|
-
unlinkQuietly(socketPath);
|
|
1570
|
-
},
|
|
1571
|
-
spawnDaemon: (paths) => spawnDaemonProcess(paths),
|
|
1572
|
-
sleep: (ms) => new Promise((resolve22) => {
|
|
1573
|
-
setTimeout(resolve22, ms);
|
|
1574
|
-
}),
|
|
1575
|
-
now: () => Date.now()
|
|
1576
|
-
};
|
|
1577
|
-
}
|
|
1578
|
-
var requireFromHere = createRequire(import.meta.url);
|
|
1579
|
-
var MAX_SOCKET_PATH_LENGTH = 100;
|
|
1580
|
-
var CODEX_LSP_DAEMON_VERSION_ENV = "CODEX_LSP_DAEMON_VERSION";
|
|
1581
|
-
function resolveDaemonVersion(requireFn = requireFromHere) {
|
|
1582
|
-
for (const candidate of ["./package.json", "../package.json"]) {
|
|
1583
|
-
try {
|
|
1584
|
-
const pkg = requireFn(candidate);
|
|
1585
|
-
if (typeof pkg.version === "string" && pkg.version.length > 0)
|
|
1586
|
-
return pkg.version;
|
|
1587
|
-
} catch {}
|
|
1588
|
-
}
|
|
1589
|
-
return "0";
|
|
1590
|
-
}
|
|
1591
|
-
function daemonBaseDir(env = process.env) {
|
|
1592
|
-
const explicit = env["CODEX_LSP_DAEMON_DIR"]?.trim();
|
|
1593
|
-
if (explicit)
|
|
1594
|
-
return explicit;
|
|
1595
|
-
const pluginData = env["PLUGIN_DATA"]?.trim();
|
|
1596
|
-
if (pluginData)
|
|
1597
|
-
return join2(pluginData, "daemon");
|
|
1598
|
-
const codexHome = env["CODEX_HOME"]?.trim();
|
|
1599
|
-
const home = codexHome && codexHome.length > 0 ? codexHome : join2(homedir(), ".codex");
|
|
1600
|
-
return join2(home, "codex-lsp", "daemon");
|
|
1601
|
-
}
|
|
1602
|
-
function daemonPaths(env = process.env, version = resolveDaemonVersionFromEnv(env) ?? resolveDaemonVersion()) {
|
|
1603
|
-
const dir = join2(daemonBaseDir(env), `v${version}`);
|
|
1604
|
-
return {
|
|
1605
|
-
version,
|
|
1606
|
-
dir,
|
|
1607
|
-
socket: resolveSocketPath(dir, version),
|
|
1608
|
-
lock: join2(dir, "daemon.lock"),
|
|
1609
|
-
pid: join2(dir, "daemon.pid"),
|
|
1610
|
-
log: join2(dir, "daemon.log")
|
|
1611
|
-
};
|
|
1612
|
-
}
|
|
1613
|
-
function resolveDaemonVersionFromEnv(env = process.env) {
|
|
1614
|
-
const version = env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim();
|
|
1615
|
-
return version && version.length > 0 ? version : null;
|
|
1616
|
-
}
|
|
1617
|
-
function resolveSocketPath(dir, version) {
|
|
1618
|
-
const digest = createHash("sha256").update(dir).digest("hex").slice(0, 16);
|
|
1619
|
-
if (process.platform === "win32") {
|
|
1620
|
-
return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
|
|
1621
|
-
}
|
|
1622
|
-
const natural = join2(dir, "daemon.sock");
|
|
1623
|
-
if (natural.length < MAX_SOCKET_PATH_LENGTH)
|
|
1624
|
-
return natural;
|
|
1625
|
-
return join2(tmpdir(), `omo-lsp-${version}-${digest}.sock`);
|
|
1626
|
-
}
|
|
1627
|
-
var HEADER_SEPARATOR2 = Buffer.from(`\r
|
|
1628
|
-
\r
|
|
1629
|
-
`);
|
|
1630
|
-
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60000;
|
|
1631
3518
|
function getInstallDecisionsPath() {
|
|
1632
|
-
|
|
1633
|
-
if (!override)
|
|
1634
|
-
return join3(homedir2(), ".codex", "lsp-install-decisions.json");
|
|
1635
|
-
return isAbsolute(override) ? override : join3(homedir2(), override);
|
|
3519
|
+
return lspRequestContext().installDecisionsPath;
|
|
1636
3520
|
}
|
|
1637
3521
|
function loadInstallDecisions() {
|
|
1638
|
-
const
|
|
1639
|
-
if (!
|
|
3522
|
+
const path2 = getInstallDecisionsPath();
|
|
3523
|
+
if (!existsSync6(path2))
|
|
1640
3524
|
return {};
|
|
1641
3525
|
try {
|
|
1642
|
-
const parsed = JSON.parse(
|
|
3526
|
+
const parsed = JSON.parse(readFileSync4(path2, "utf8"));
|
|
1643
3527
|
return isInstallDecisions(parsed) ? parsed : {};
|
|
1644
3528
|
} catch {
|
|
1645
3529
|
return {};
|
|
@@ -1657,22 +3541,22 @@ function isInstallDecision(value) {
|
|
|
1657
3541
|
return value === "declined" || value === "allowed";
|
|
1658
3542
|
}
|
|
1659
3543
|
function writeInstallDecisions(decisions) {
|
|
1660
|
-
const
|
|
1661
|
-
mkdirSync3(
|
|
1662
|
-
const tmpPath = `${
|
|
1663
|
-
|
|
3544
|
+
const path2 = getInstallDecisionsPath();
|
|
3545
|
+
mkdirSync3(dirname7(path2), { recursive: true });
|
|
3546
|
+
const tmpPath = `${path2}.tmp`;
|
|
3547
|
+
writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
|
|
1664
3548
|
`, "utf8");
|
|
1665
|
-
|
|
3549
|
+
renameSync2(tmpPath, path2);
|
|
1666
3550
|
}
|
|
1667
3551
|
function isInstallDecisions(value) {
|
|
1668
|
-
return
|
|
3552
|
+
return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
|
|
1669
3553
|
}
|
|
1670
3554
|
function isInstallDecisionRecord(value) {
|
|
1671
|
-
if (!
|
|
3555
|
+
if (!isRecord5(value))
|
|
1672
3556
|
return false;
|
|
1673
3557
|
return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
|
|
1674
3558
|
}
|
|
1675
|
-
function
|
|
3559
|
+
function isRecord5(value) {
|
|
1676
3560
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1677
3561
|
}
|
|
1678
3562
|
var LSP_INSTALL_HINTS = {
|
|
@@ -1820,27 +3704,17 @@ var BUILTIN_SERVERS = {
|
|
|
1820
3704
|
extensions: [".razor", ".cshtml"]
|
|
1821
3705
|
}
|
|
1822
3706
|
};
|
|
1823
|
-
function resolveProjectConfigPath(path) {
|
|
1824
|
-
return isAbsolute2(path) ? path : join4(contextCwd(), path);
|
|
1825
|
-
}
|
|
1826
3707
|
function getProjectConfigPaths() {
|
|
1827
|
-
|
|
1828
|
-
if (projectOverride) {
|
|
1829
|
-
return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
|
|
1830
|
-
}
|
|
1831
|
-
return [join4(contextCwd(), ".codex", "lsp-client.json")];
|
|
3708
|
+
return lspRequestContext().projectConfigPaths;
|
|
1832
3709
|
}
|
|
1833
3710
|
function getUserConfigPath() {
|
|
1834
|
-
|
|
1835
|
-
if (!userOverride)
|
|
1836
|
-
return join4(homedir3(), ".codex", "lsp-client.json");
|
|
1837
|
-
return isAbsolute2(userOverride) ? userOverride : join4(homedir3(), userOverride);
|
|
3711
|
+
return lspRequestContext().userConfigPath;
|
|
1838
3712
|
}
|
|
1839
|
-
function loadJsonFile(
|
|
1840
|
-
if (!
|
|
3713
|
+
function loadJsonFile(path2) {
|
|
3714
|
+
if (!existsSync7(path2))
|
|
1841
3715
|
return null;
|
|
1842
3716
|
try {
|
|
1843
|
-
const parsed = JSON.parse(
|
|
3717
|
+
const parsed = JSON.parse(readFileSync5(path2, "utf-8"));
|
|
1844
3718
|
return isConfigJson(parsed) ? parsed : null;
|
|
1845
3719
|
} catch {
|
|
1846
3720
|
return null;
|
|
@@ -1857,8 +3731,8 @@ function loadAllConfigs() {
|
|
|
1857
3731
|
return configs;
|
|
1858
3732
|
}
|
|
1859
3733
|
function loadFirstJsonFile(paths) {
|
|
1860
|
-
for (const
|
|
1861
|
-
const config = loadJsonFile(
|
|
3734
|
+
for (const path2 of paths) {
|
|
3735
|
+
const config = loadJsonFile(path2);
|
|
1862
3736
|
if (config)
|
|
1863
3737
|
return config;
|
|
1864
3738
|
}
|
|
@@ -1979,16 +3853,16 @@ function applyOptionalServerFields(server2, entry) {
|
|
|
1979
3853
|
}
|
|
1980
3854
|
}
|
|
1981
3855
|
function isConfigJson(value) {
|
|
1982
|
-
if (!
|
|
3856
|
+
if (!isRecord6(value))
|
|
1983
3857
|
return false;
|
|
1984
3858
|
const lsp = value["lsp"];
|
|
1985
|
-
return lsp === undefined ||
|
|
3859
|
+
return lsp === undefined || isRecord6(lsp);
|
|
1986
3860
|
}
|
|
1987
3861
|
function parseLspEntry(value) {
|
|
1988
3862
|
return isLspEntry(value) ? value : null;
|
|
1989
3863
|
}
|
|
1990
3864
|
function isLspEntry(value) {
|
|
1991
|
-
if (!
|
|
3865
|
+
if (!isRecord6(value))
|
|
1992
3866
|
return false;
|
|
1993
3867
|
const disabled = value["disabled"];
|
|
1994
3868
|
const command = value["command"];
|
|
@@ -1996,15 +3870,15 @@ function isLspEntry(value) {
|
|
|
1996
3870
|
const priority = value["priority"];
|
|
1997
3871
|
const env = value["env"];
|
|
1998
3872
|
const initialization = value["initialization"];
|
|
1999
|
-
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 ||
|
|
3873
|
+
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));
|
|
2000
3874
|
}
|
|
2001
3875
|
function isStringArray(value) {
|
|
2002
3876
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2003
3877
|
}
|
|
2004
3878
|
function isStringRecord(value) {
|
|
2005
|
-
return
|
|
3879
|
+
return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
|
|
2006
3880
|
}
|
|
2007
|
-
function
|
|
3881
|
+
function isRecord6(value) {
|
|
2008
3882
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2009
3883
|
}
|
|
2010
3884
|
function getDisabledServerIds() {
|
|
@@ -2030,7 +3904,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2030
3904
|
if (!cmd)
|
|
2031
3905
|
return false;
|
|
2032
3906
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
2033
|
-
if (
|
|
3907
|
+
if (existsSync8(cmd))
|
|
2034
3908
|
return true;
|
|
2035
3909
|
}
|
|
2036
3910
|
const isWindows = process.platform === "win32";
|
|
@@ -2051,7 +3925,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2051
3925
|
const paths = pathEnv.split(delimiter3);
|
|
2052
3926
|
for (const p of paths) {
|
|
2053
3927
|
for (const suffix of exts) {
|
|
2054
|
-
if (
|
|
3928
|
+
if (existsSync8(join3(p, cmd + suffix))) {
|
|
2055
3929
|
return true;
|
|
2056
3930
|
}
|
|
2057
3931
|
}
|
|
@@ -2146,39 +4020,50 @@ function getAllServers() {
|
|
|
2146
4020
|
var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
2147
4021
|
function isDirectoryPath(filePath) {
|
|
2148
4022
|
try {
|
|
2149
|
-
return
|
|
4023
|
+
return statSync4(filePath).isDirectory();
|
|
2150
4024
|
} catch {
|
|
2151
4025
|
return false;
|
|
2152
4026
|
}
|
|
2153
4027
|
}
|
|
2154
4028
|
function findWorkspaceRoot(filePath) {
|
|
2155
|
-
const abs =
|
|
4029
|
+
const abs = resolvePathInsideContext(filePath);
|
|
2156
4030
|
let dir = abs;
|
|
2157
4031
|
if (!isDirectoryPath(dir)) {
|
|
2158
|
-
dir =
|
|
4032
|
+
dir = dirname8(dir);
|
|
2159
4033
|
}
|
|
2160
4034
|
let prevDir = "";
|
|
2161
4035
|
while (dir !== prevDir) {
|
|
2162
4036
|
for (const marker of WORKSPACE_MARKERS) {
|
|
2163
|
-
if (
|
|
4037
|
+
if (existsSync9(join4(dir, marker))) {
|
|
2164
4038
|
return dir;
|
|
2165
4039
|
}
|
|
2166
4040
|
}
|
|
2167
4041
|
prevDir = dir;
|
|
2168
|
-
dir =
|
|
4042
|
+
dir = dirname8(dir);
|
|
2169
4043
|
}
|
|
2170
|
-
return
|
|
4044
|
+
return dirname8(abs);
|
|
4045
|
+
}
|
|
4046
|
+
function resolvePathInsideContext(filePath) {
|
|
4047
|
+
const cwd = contextCwd();
|
|
4048
|
+
const abs = resolve7(cwd, filePath);
|
|
4049
|
+
const canonical = canonicalizeExistingOrNearestAncestor(abs);
|
|
4050
|
+
if (!isPathInside(cwd, canonical)) {
|
|
4051
|
+
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
4052
|
+
}
|
|
4053
|
+
return canonical;
|
|
2171
4054
|
}
|
|
2172
4055
|
function formatServerLookupError(result) {
|
|
2173
4056
|
if (result.status === "not_installed") {
|
|
2174
4057
|
return formatNotInstalled(result);
|
|
2175
4058
|
}
|
|
4059
|
+
const context = lspRequestContext();
|
|
4060
|
+
const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
|
|
2176
4061
|
return [
|
|
2177
4062
|
`No LSP server configured for extension: ${result.extension}`,
|
|
2178
4063
|
"",
|
|
2179
4064
|
`Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
|
|
2180
4065
|
"",
|
|
2181
|
-
|
|
4066
|
+
`Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
|
|
2182
4067
|
" {",
|
|
2183
4068
|
' "lsp": {',
|
|
2184
4069
|
' "my-server": {',
|
|
@@ -2194,6 +4079,7 @@ function formatNotInstalled(result) {
|
|
|
2194
4079
|
const { server: server2, installHint } = result;
|
|
2195
4080
|
const extensions = server2.extensions.join(", ");
|
|
2196
4081
|
const decision = loadInstallDecision(server2.id)?.decision;
|
|
4082
|
+
const context = lspRequestContext();
|
|
2197
4083
|
if (decision === "declined") {
|
|
2198
4084
|
return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
|
|
2199
4085
|
}
|
|
@@ -2209,6 +4095,17 @@ function formatNotInstalled(result) {
|
|
|
2209
4095
|
"The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
|
|
2210
4096
|
` ${installHint}`
|
|
2211
4097
|
].join(`
|
|
4098
|
+
`);
|
|
4099
|
+
}
|
|
4100
|
+
if (!context.capabilities.installDecisionTool) {
|
|
4101
|
+
return [
|
|
4102
|
+
...header,
|
|
4103
|
+
"To install, run:",
|
|
4104
|
+
` ${installHint}`,
|
|
4105
|
+
"",
|
|
4106
|
+
"ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
|
|
4107
|
+
"Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
|
|
4108
|
+
].join(`
|
|
2212
4109
|
`);
|
|
2213
4110
|
}
|
|
2214
4111
|
return [
|
|
@@ -2220,6 +4117,7 @@ function formatNotInstalled(result) {
|
|
|
2220
4117
|
"- If the user agrees: run the install command above, then retry this tool.",
|
|
2221
4118
|
"- If the user declines, OR has NOT explicitly asked for LSP installation:",
|
|
2222
4119
|
` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
|
|
4120
|
+
` which writes to ${context.installDecisionsPath},`,
|
|
2223
4121
|
" then ignore this message and proceed WITHOUT LSP."
|
|
2224
4122
|
].join(`
|
|
2225
4123
|
`);
|
|
@@ -2233,14 +4131,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
|
|
|
2233
4131
|
"prepareRename"
|
|
2234
4132
|
]);
|
|
2235
4133
|
async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
2236
|
-
const absPath =
|
|
4134
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2237
4135
|
if (isDirectoryPath(absPath)) {
|
|
2238
4136
|
throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
|
|
2239
4137
|
}
|
|
2240
4138
|
const ext = effectiveExtension(absPath);
|
|
2241
4139
|
const result = findServerForExtension(ext);
|
|
2242
4140
|
if (result.status !== "found") {
|
|
2243
|
-
throw new LspServerLookupError(formatServerLookupError(result));
|
|
4141
|
+
throw new LspServerLookupError(formatServerLookupError(result), result);
|
|
2244
4142
|
}
|
|
2245
4143
|
const server2 = result.server;
|
|
2246
4144
|
const root = findWorkspaceRoot(absPath);
|
|
@@ -2273,7 +4171,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
|
2273
4171
|
hint: 4
|
|
2274
4172
|
};
|
|
2275
4173
|
function uriToPath(uri) {
|
|
2276
|
-
return
|
|
4174
|
+
return fileURLToPath3(uri);
|
|
2277
4175
|
}
|
|
2278
4176
|
function formatLocation(loc) {
|
|
2279
4177
|
if ("targetUri" in loc) {
|
|
@@ -2359,6 +4257,9 @@ function formatApplyResult(result) {
|
|
|
2359
4257
|
for (const file of result.filesModified) {
|
|
2360
4258
|
lines.push(` - ${file}`);
|
|
2361
4259
|
}
|
|
4260
|
+
if (result.lateAbort) {
|
|
4261
|
+
lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
|
|
4262
|
+
}
|
|
2362
4263
|
} else {
|
|
2363
4264
|
lines.push("Failed to apply some changes:");
|
|
2364
4265
|
for (const err of result.errors) {
|
|
@@ -2372,6 +4273,7 @@ function formatApplyResult(result) {
|
|
|
2372
4273
|
`);
|
|
2373
4274
|
}
|
|
2374
4275
|
var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
4276
|
+
var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
|
|
2375
4277
|
function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
2376
4278
|
const files = [];
|
|
2377
4279
|
function walk(currentDir) {
|
|
@@ -2379,17 +4281,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2379
4281
|
return;
|
|
2380
4282
|
let entries = [];
|
|
2381
4283
|
try {
|
|
2382
|
-
entries =
|
|
4284
|
+
entries = readdirSync3(currentDir);
|
|
2383
4285
|
} catch {
|
|
2384
4286
|
return;
|
|
2385
4287
|
}
|
|
2386
4288
|
for (const entry of entries) {
|
|
2387
4289
|
if (files.length >= maxFiles)
|
|
2388
4290
|
return;
|
|
2389
|
-
const fullPath =
|
|
4291
|
+
const fullPath = join5(currentDir, entry);
|
|
2390
4292
|
let stat;
|
|
2391
4293
|
try {
|
|
2392
|
-
stat =
|
|
4294
|
+
stat = lstatSync5(fullPath);
|
|
2393
4295
|
} catch {
|
|
2394
4296
|
continue;
|
|
2395
4297
|
}
|
|
@@ -2407,52 +4309,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2407
4309
|
walk(dir);
|
|
2408
4310
|
return files;
|
|
2409
4311
|
}
|
|
2410
|
-
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
|
|
4312
|
+
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
|
|
2411
4313
|
if (!extension.startsWith(".")) {
|
|
2412
4314
|
throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
|
|
2413
4315
|
}
|
|
2414
|
-
const absDir =
|
|
2415
|
-
if (!
|
|
4316
|
+
const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
|
|
4317
|
+
if (!existsSync10(absDir)) {
|
|
2416
4318
|
throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
|
|
2417
4319
|
}
|
|
2418
|
-
const serverResult = findServerForExtension(extension);
|
|
4320
|
+
const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
|
|
2419
4321
|
if (serverResult.status !== "found") {
|
|
2420
4322
|
throw new LspServerLookupError(formatServerLookupError(serverResult));
|
|
2421
4323
|
}
|
|
2422
4324
|
const server2 = serverResult.server;
|
|
2423
|
-
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
|
|
4325
|
+
const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
|
|
2424
4326
|
const wasCapped = allFiles.length > maxFiles;
|
|
2425
4327
|
const filesToProcess = allFiles.slice(0, maxFiles);
|
|
2426
4328
|
if (filesToProcess.length === 0) {
|
|
2427
|
-
|
|
4329
|
+
const output = [
|
|
2428
4330
|
`Directory: ${absDir}`,
|
|
2429
4331
|
`Extension: ${extension}`,
|
|
2430
4332
|
"Files scanned: 0",
|
|
2431
4333
|
`No files found with extension "${extension}".`
|
|
2432
4334
|
].join(`
|
|
2433
4335
|
`);
|
|
4336
|
+
return { output, totalDiagnostics: 0, fileFailures: [] };
|
|
2434
4337
|
}
|
|
2435
|
-
const root = findWorkspaceRoot(absDir);
|
|
2436
|
-
const manager = getLspManager();
|
|
4338
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
|
|
4339
|
+
const manager = options.manager ?? getLspManager();
|
|
2437
4340
|
const allDiagnostics = [];
|
|
2438
4341
|
const fileErrors = [];
|
|
2439
|
-
const
|
|
4342
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
|
|
4343
|
+
options.signal?.throwIfAborted();
|
|
4344
|
+
const client = await manager.getClient(root, server2, options.signal);
|
|
2440
4345
|
try {
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
file,
|
|
2452
|
-
|
|
2453
|
-
|
|
4346
|
+
let nextIndex = 0;
|
|
4347
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
|
|
4348
|
+
for (;; ) {
|
|
4349
|
+
if (options.signal?.aborted)
|
|
4350
|
+
return;
|
|
4351
|
+
const file = filesToProcess[nextIndex];
|
|
4352
|
+
nextIndex += 1;
|
|
4353
|
+
if (file === undefined)
|
|
4354
|
+
return;
|
|
4355
|
+
try {
|
|
4356
|
+
const result = await client.diagnostics(file, options.signal);
|
|
4357
|
+
const filtered = filterDiagnosticsBySeverity(result.items, severity);
|
|
4358
|
+
allDiagnostics.push(...filtered.map((diagnostic) => ({
|
|
4359
|
+
filePath: file,
|
|
4360
|
+
diagnostic
|
|
4361
|
+
})));
|
|
4362
|
+
} catch (e) {
|
|
4363
|
+
fileErrors.push({
|
|
4364
|
+
file,
|
|
4365
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
2454
4368
|
}
|
|
2455
|
-
}
|
|
4369
|
+
});
|
|
4370
|
+
await Promise.all(workers);
|
|
2456
4371
|
} finally {
|
|
2457
4372
|
manager.releaseClient(root, server2.id);
|
|
2458
4373
|
}
|
|
@@ -2480,8 +4395,8 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
|
|
|
2480
4395
|
lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
2481
4396
|
}
|
|
2482
4397
|
}
|
|
2483
|
-
return lines.join(`
|
|
2484
|
-
`);
|
|
4398
|
+
return { output: lines.join(`
|
|
4399
|
+
`), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
|
|
2485
4400
|
}
|
|
2486
4401
|
var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
2487
4402
|
var MAX_SCAN_ENTRIES = 500;
|
|
@@ -2493,17 +4408,17 @@ function inferExtensionFromDirectory(directory) {
|
|
|
2493
4408
|
return;
|
|
2494
4409
|
let entries;
|
|
2495
4410
|
try {
|
|
2496
|
-
entries =
|
|
4411
|
+
entries = readdirSync4(dir);
|
|
2497
4412
|
} catch {
|
|
2498
4413
|
return;
|
|
2499
4414
|
}
|
|
2500
4415
|
for (const entry of entries) {
|
|
2501
4416
|
if (scanned >= MAX_SCAN_ENTRIES)
|
|
2502
4417
|
return;
|
|
2503
|
-
const fullPath =
|
|
4418
|
+
const fullPath = join6(dir, entry);
|
|
2504
4419
|
let stat;
|
|
2505
4420
|
try {
|
|
2506
|
-
stat =
|
|
4421
|
+
stat = lstatSync6(fullPath);
|
|
2507
4422
|
} catch {
|
|
2508
4423
|
continue;
|
|
2509
4424
|
}
|
|
@@ -2574,10 +4489,45 @@ function missingDependencyResult(error, details) {
|
|
|
2574
4489
|
details: {
|
|
2575
4490
|
...details,
|
|
2576
4491
|
error: message,
|
|
2577
|
-
errorKind: "missing_dependency"
|
|
4492
|
+
errorKind: "missing_dependency",
|
|
4493
|
+
...availabilityDetails(error)
|
|
2578
4494
|
}
|
|
2579
4495
|
};
|
|
2580
4496
|
}
|
|
4497
|
+
function availabilityDetails(error) {
|
|
4498
|
+
const availability = missingDependencyAvailability(error);
|
|
4499
|
+
return availability === null ? {} : { availability };
|
|
4500
|
+
}
|
|
4501
|
+
function missingDependencyAvailability(error) {
|
|
4502
|
+
if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
|
|
4503
|
+
return null;
|
|
4504
|
+
const context = lspRequestContext();
|
|
4505
|
+
switch (error.lookup.status) {
|
|
4506
|
+
case "not_configured":
|
|
4507
|
+
return {
|
|
4508
|
+
kind: "not_configured",
|
|
4509
|
+
extension: error.lookup.extension,
|
|
4510
|
+
availableServers: [...error.lookup.availableServers],
|
|
4511
|
+
projectConfigPaths: [...context.projectConfigPaths],
|
|
4512
|
+
userConfigPath: context.userConfigPath,
|
|
4513
|
+
installDecisionTool: context.capabilities.installDecisionTool
|
|
4514
|
+
};
|
|
4515
|
+
case "not_installed":
|
|
4516
|
+
return {
|
|
4517
|
+
kind: "not_installed",
|
|
4518
|
+
serverId: error.lookup.server.id,
|
|
4519
|
+
command: [...error.lookup.server.command],
|
|
4520
|
+
extensions: [...error.lookup.server.extensions],
|
|
4521
|
+
installHint: error.lookup.installHint,
|
|
4522
|
+
installDecisionTool: context.capabilities.installDecisionTool,
|
|
4523
|
+
installDecisionsPath: context.installDecisionsPath
|
|
4524
|
+
};
|
|
4525
|
+
default: {
|
|
4526
|
+
const exhaustive = error.lookup;
|
|
4527
|
+
return exhaustive;
|
|
4528
|
+
}
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
2581
4531
|
function requireString(params, key) {
|
|
2582
4532
|
const value = params[key];
|
|
2583
4533
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -2628,7 +4578,7 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2628
4578
|
const filePath = requireString(params, "filePath");
|
|
2629
4579
|
const severity = severityFilter(params);
|
|
2630
4580
|
try {
|
|
2631
|
-
const absPath =
|
|
4581
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2632
4582
|
if (isDirectoryPath(absPath)) {
|
|
2633
4583
|
const extension = inferExtensionFromDirectory(absPath);
|
|
2634
4584
|
if (!extension) {
|
|
@@ -2645,18 +4595,33 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2645
4595
|
};
|
|
2646
4596
|
return text(message, details3);
|
|
2647
4597
|
}
|
|
2648
|
-
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
|
|
4598
|
+
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
|
|
2649
4599
|
const details2 = {
|
|
2650
4600
|
filePath,
|
|
2651
4601
|
severity,
|
|
2652
4602
|
mode: "directory",
|
|
2653
4603
|
diagnostics: [],
|
|
4604
|
+
totalDiagnostics: output2.totalDiagnostics,
|
|
4605
|
+
truncated: false,
|
|
4606
|
+
fileFailures: [...output2.fileFailures]
|
|
4607
|
+
};
|
|
4608
|
+
return text(output2.output, details2);
|
|
4609
|
+
}
|
|
4610
|
+
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
|
|
4611
|
+
if (result.transientError) {
|
|
4612
|
+
const message = result.transientError.message;
|
|
4613
|
+
const details2 = {
|
|
4614
|
+
filePath,
|
|
4615
|
+
severity,
|
|
4616
|
+
mode: "file",
|
|
4617
|
+
diagnostics: [],
|
|
2654
4618
|
totalDiagnostics: 0,
|
|
2655
|
-
truncated: false
|
|
4619
|
+
truncated: false,
|
|
4620
|
+
error: message,
|
|
4621
|
+
errorKind: result.transientError.kind
|
|
2656
4622
|
};
|
|
2657
|
-
return text(
|
|
4623
|
+
return text(message, details2, true);
|
|
2658
4624
|
}
|
|
2659
|
-
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
|
|
2660
4625
|
const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
|
|
2661
4626
|
const total = diagnostics.length;
|
|
2662
4627
|
const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
|
|
@@ -2714,7 +4679,7 @@ async function executeLspGotoDefinition(params, signal) {
|
|
|
2714
4679
|
const line = requireNumber(params, "line");
|
|
2715
4680
|
const character = requireNumber(params, "character");
|
|
2716
4681
|
try {
|
|
2717
|
-
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
|
|
4682
|
+
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
|
|
2718
4683
|
const locations = !result ? [] : Array.isArray(result) ? result : [result];
|
|
2719
4684
|
const details = { filePath, line, character, locations };
|
|
2720
4685
|
if (locations.length === 0)
|
|
@@ -2739,7 +4704,7 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2739
4704
|
const character = requireNumber(params, "character");
|
|
2740
4705
|
const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
|
|
2741
4706
|
try {
|
|
2742
|
-
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
|
|
4707
|
+
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
|
|
2743
4708
|
const references = Array.isArray(result) ? result : [];
|
|
2744
4709
|
const total = references.length;
|
|
2745
4710
|
const truncated = total > DEFAULT_MAX_REFERENCES;
|
|
@@ -2750,199 +4715,36 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2750
4715
|
character,
|
|
2751
4716
|
references,
|
|
2752
4717
|
totalReferences: total,
|
|
2753
|
-
truncated
|
|
2754
|
-
};
|
|
2755
|
-
if (total === 0)
|
|
2756
|
-
return text("No references found", details);
|
|
2757
|
-
const output = [
|
|
2758
|
-
...truncated ? [`Found ${total} references (showing first ${DEFAULT_MAX_REFERENCES}):`] : [],
|
|
2759
|
-
...limited.map(formatLocation)
|
|
2760
|
-
].join(`
|
|
2761
|
-
`);
|
|
2762
|
-
return text(output, details);
|
|
2763
|
-
} catch (error) {
|
|
2764
|
-
const missingDependency = missingDependencyResult(error, {
|
|
2765
|
-
filePath,
|
|
2766
|
-
line,
|
|
2767
|
-
character,
|
|
2768
|
-
references: [],
|
|
2769
|
-
totalReferences: 0,
|
|
2770
|
-
truncated: false
|
|
2771
|
-
});
|
|
2772
|
-
if (missingDependency)
|
|
2773
|
-
return missingDependency;
|
|
2774
|
-
throw error;
|
|
2775
|
-
}
|
|
2776
|
-
}
|
|
2777
|
-
function errorMessage2(error) {
|
|
2778
|
-
return error instanceof Error ? error.message : String(error);
|
|
2779
|
-
}
|
|
2780
|
-
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
2781
|
-
const relativePath = relative(workspaceRoot, filePath);
|
|
2782
|
-
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
|
|
2783
|
-
}
|
|
2784
|
-
function realpathForValidation(filePath) {
|
|
2785
|
-
if (existsSync8(filePath))
|
|
2786
|
-
return realpathSync(filePath);
|
|
2787
|
-
const parent = dirname5(filePath);
|
|
2788
|
-
return resolve5(realpathSync(parent), relative(parent, filePath));
|
|
2789
|
-
}
|
|
2790
|
-
function uriToWorkspacePath(uri, workspaceRoot) {
|
|
2791
|
-
let filePath;
|
|
2792
|
-
try {
|
|
2793
|
-
filePath = fileURLToPath3(uri);
|
|
2794
|
-
} catch (error) {
|
|
2795
|
-
return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
|
|
2796
|
-
}
|
|
2797
|
-
let validatedPath;
|
|
2798
|
-
try {
|
|
2799
|
-
validatedPath = realpathForValidation(filePath);
|
|
2800
|
-
} catch (error) {
|
|
2801
|
-
return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
|
|
2802
|
-
}
|
|
2803
|
-
if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
|
|
2804
|
-
return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
|
|
2805
|
-
}
|
|
2806
|
-
return { success: true, path: filePath };
|
|
2807
|
-
}
|
|
2808
|
-
function applyTextEditsToFile(filePath, edits) {
|
|
2809
|
-
try {
|
|
2810
|
-
const content = readFileSync5(filePath, "utf-8");
|
|
2811
|
-
const lines = content.split(`
|
|
2812
|
-
`);
|
|
2813
|
-
const sortedEdits = [...edits].sort((a, b) => {
|
|
2814
|
-
if (b.range.start.line !== a.range.start.line) {
|
|
2815
|
-
return b.range.start.line - a.range.start.line;
|
|
2816
|
-
}
|
|
2817
|
-
return b.range.start.character - a.range.start.character;
|
|
2818
|
-
});
|
|
2819
|
-
for (const edit of sortedEdits) {
|
|
2820
|
-
const startLine = edit.range.start.line;
|
|
2821
|
-
const startChar = edit.range.start.character;
|
|
2822
|
-
const endLine = edit.range.end.line;
|
|
2823
|
-
const endChar = edit.range.end.character;
|
|
2824
|
-
if (startLine === endLine) {
|
|
2825
|
-
const line = lines[startLine] ?? "";
|
|
2826
|
-
lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
|
|
2827
|
-
} else {
|
|
2828
|
-
const firstLine = lines[startLine] ?? "";
|
|
2829
|
-
const lastLine = lines[endLine] ?? "";
|
|
2830
|
-
const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
|
|
2831
|
-
lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
|
|
2832
|
-
`));
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
writeFileSync2(filePath, lines.join(`
|
|
2836
|
-
`), "utf-8");
|
|
2837
|
-
return { success: true, editCount: edits.length };
|
|
2838
|
-
} catch (err) {
|
|
2839
|
-
return {
|
|
2840
|
-
success: false,
|
|
2841
|
-
editCount: 0,
|
|
2842
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2843
|
-
};
|
|
2844
|
-
}
|
|
2845
|
-
}
|
|
2846
|
-
function applyWorkspaceEdit(edit, options = {}) {
|
|
2847
|
-
if (!edit) {
|
|
2848
|
-
return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
|
|
2849
|
-
}
|
|
2850
|
-
const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
|
|
2851
|
-
const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
|
|
2852
|
-
if (edit.changes) {
|
|
2853
|
-
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
2854
|
-
const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
|
|
2855
|
-
if (!validatedPath.success) {
|
|
2856
|
-
result.success = false;
|
|
2857
|
-
result.errors.push(validatedPath.error);
|
|
2858
|
-
continue;
|
|
2859
|
-
}
|
|
2860
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, edits);
|
|
2861
|
-
if (applyResult.success) {
|
|
2862
|
-
result.filesModified.push(validatedPath.path);
|
|
2863
|
-
result.totalEdits += applyResult.editCount;
|
|
2864
|
-
} else {
|
|
2865
|
-
result.success = false;
|
|
2866
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2867
|
-
}
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2870
|
-
if (edit.documentChanges) {
|
|
2871
|
-
for (const change of edit.documentChanges) {
|
|
2872
|
-
if (!("kind" in change)) {
|
|
2873
|
-
const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
|
|
2874
|
-
if (!validatedPath.success) {
|
|
2875
|
-
result.success = false;
|
|
2876
|
-
result.errors.push(validatedPath.error);
|
|
2877
|
-
continue;
|
|
2878
|
-
}
|
|
2879
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
|
|
2880
|
-
if (applyResult.success) {
|
|
2881
|
-
result.filesModified.push(validatedPath.path);
|
|
2882
|
-
result.totalEdits += applyResult.editCount;
|
|
2883
|
-
} else {
|
|
2884
|
-
result.success = false;
|
|
2885
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2886
|
-
}
|
|
2887
|
-
continue;
|
|
2888
|
-
}
|
|
2889
|
-
if (change.kind === "create") {
|
|
2890
|
-
try {
|
|
2891
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2892
|
-
if (!validatedPath.success) {
|
|
2893
|
-
result.success = false;
|
|
2894
|
-
result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
|
|
2895
|
-
continue;
|
|
2896
|
-
}
|
|
2897
|
-
writeFileSync2(validatedPath.path, "", "utf-8");
|
|
2898
|
-
result.filesModified.push(validatedPath.path);
|
|
2899
|
-
} catch (err) {
|
|
2900
|
-
result.success = false;
|
|
2901
|
-
result.errors.push(`Create ${change.uri}: ${String(err)}`);
|
|
2902
|
-
}
|
|
2903
|
-
} else if (change.kind === "rename") {
|
|
2904
|
-
try {
|
|
2905
|
-
const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
|
|
2906
|
-
const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
|
|
2907
|
-
if (!oldPath.success || !newPath.success) {
|
|
2908
|
-
const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
|
|
2909
|
-
result.success = false;
|
|
2910
|
-
result.errors.push(`Rename ${change.oldUri}: ${error}`);
|
|
2911
|
-
continue;
|
|
2912
|
-
}
|
|
2913
|
-
const content = readFileSync5(oldPath.path, "utf-8");
|
|
2914
|
-
writeFileSync2(newPath.path, content, "utf-8");
|
|
2915
|
-
unlinkSync2(oldPath.path);
|
|
2916
|
-
result.filesModified.push(newPath.path);
|
|
2917
|
-
} catch (err) {
|
|
2918
|
-
result.success = false;
|
|
2919
|
-
result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
|
|
2920
|
-
}
|
|
2921
|
-
} else if (change.kind === "delete") {
|
|
2922
|
-
try {
|
|
2923
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2924
|
-
if (!validatedPath.success) {
|
|
2925
|
-
result.success = false;
|
|
2926
|
-
result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
|
|
2927
|
-
continue;
|
|
2928
|
-
}
|
|
2929
|
-
unlinkSync2(validatedPath.path);
|
|
2930
|
-
result.filesModified.push(validatedPath.path);
|
|
2931
|
-
} catch (err) {
|
|
2932
|
-
result.success = false;
|
|
2933
|
-
result.errors.push(`Delete ${change.uri}: ${String(err)}`);
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
4718
|
+
truncated
|
|
4719
|
+
};
|
|
4720
|
+
if (total === 0)
|
|
4721
|
+
return text("No references found", details);
|
|
4722
|
+
const output = [
|
|
4723
|
+
...truncated ? [`Found ${total} references (showing first ${DEFAULT_MAX_REFERENCES}):`] : [],
|
|
4724
|
+
...limited.map(formatLocation)
|
|
4725
|
+
].join(`
|
|
4726
|
+
`);
|
|
4727
|
+
return text(output, details);
|
|
4728
|
+
} catch (error) {
|
|
4729
|
+
const missingDependency = missingDependencyResult(error, {
|
|
4730
|
+
filePath,
|
|
4731
|
+
line,
|
|
4732
|
+
character,
|
|
4733
|
+
references: [],
|
|
4734
|
+
totalReferences: 0,
|
|
4735
|
+
truncated: false
|
|
4736
|
+
});
|
|
4737
|
+
if (missingDependency)
|
|
4738
|
+
return missingDependency;
|
|
4739
|
+
throw error;
|
|
2937
4740
|
}
|
|
2938
|
-
return result;
|
|
2939
4741
|
}
|
|
2940
4742
|
async function executeLspPrepareRename(params, signal) {
|
|
2941
4743
|
const filePath = requireString(params, "filePath");
|
|
2942
4744
|
const line = requireNumber(params, "line");
|
|
2943
4745
|
const character = requireNumber(params, "character");
|
|
2944
4746
|
try {
|
|
2945
|
-
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
|
|
4747
|
+
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
|
|
2946
4748
|
const details = { filePath, line, character, result };
|
|
2947
4749
|
return text(formatPrepareRenameResult(result), details);
|
|
2948
4750
|
} catch (error) {
|
|
@@ -2963,13 +4765,9 @@ async function executeLspRename(params, signal) {
|
|
|
2963
4765
|
const character = requireNumber(params, "character");
|
|
2964
4766
|
const newName = requireString(params, "newName");
|
|
2965
4767
|
try {
|
|
2966
|
-
const
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
}), "rename", clientOptions(signal));
|
|
2970
|
-
const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
|
|
2971
|
-
const details = { filePath, line, character, newName, apply, edit: edit.edit };
|
|
2972
|
-
return text(formatApplyResult(apply), details, !apply.success);
|
|
4768
|
+
const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
|
|
4769
|
+
const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
|
|
4770
|
+
return text(formatApplyResult(result.apply), details, !result.apply.success);
|
|
2973
4771
|
} catch (error) {
|
|
2974
4772
|
const missingDependency = missingDependencyResult(error, {
|
|
2975
4773
|
filePath,
|
|
@@ -3038,10 +4836,10 @@ async function executeLspSymbols(params, signal) {
|
|
|
3038
4836
|
errorKind: "missing_query"
|
|
3039
4837
|
});
|
|
3040
4838
|
}
|
|
3041
|
-
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
|
|
4839
|
+
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
|
|
3042
4840
|
return formatSymbolsResult(filePath, scope, symbols2, limit, query);
|
|
3043
4841
|
}
|
|
3044
|
-
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
|
|
4842
|
+
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
|
|
3045
4843
|
return formatSymbolsResult(filePath, scope, symbols, limit);
|
|
3046
4844
|
} catch (error) {
|
|
3047
4845
|
const query = optionalString(params, "query");
|
|
@@ -3195,35 +4993,8 @@ var LSP_MCP_TOOLS = [
|
|
|
3195
4993
|
}
|
|
3196
4994
|
];
|
|
3197
4995
|
var CONTEXT_KEY = "_context";
|
|
3198
|
-
function encodeJsonLine(message) {
|
|
3199
|
-
return `${JSON.stringify(message)}
|
|
3200
|
-
`;
|
|
3201
|
-
}
|
|
3202
|
-
function createLineDecoder(onMessage, onParseError) {
|
|
3203
|
-
let buffer = "";
|
|
3204
|
-
return {
|
|
3205
|
-
push(chunk) {
|
|
3206
|
-
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
3207
|
-
let index = buffer.indexOf(`
|
|
3208
|
-
`);
|
|
3209
|
-
while (index !== -1) {
|
|
3210
|
-
const raw = buffer.slice(0, index).trim();
|
|
3211
|
-
buffer = buffer.slice(index + 1);
|
|
3212
|
-
if (raw.length > 0) {
|
|
3213
|
-
try {
|
|
3214
|
-
onMessage(JSON.parse(raw));
|
|
3215
|
-
} catch (error) {
|
|
3216
|
-
onParseError?.(raw, error);
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
index = buffer.indexOf(`
|
|
3220
|
-
`);
|
|
3221
|
-
}
|
|
3222
|
-
}
|
|
3223
|
-
};
|
|
3224
|
-
}
|
|
3225
4996
|
var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
3226
|
-
var
|
|
4997
|
+
var nextProxyRequestId = 1;
|
|
3227
4998
|
|
|
3228
4999
|
class DaemonRequestError extends Error {
|
|
3229
5000
|
requestWritten;
|
|
@@ -3233,46 +5004,82 @@ class DaemonRequestError extends Error {
|
|
|
3233
5004
|
this.requestWritten = requestWritten;
|
|
3234
5005
|
}
|
|
3235
5006
|
}
|
|
3236
|
-
|
|
5007
|
+
|
|
5008
|
+
class DaemonAuthenticationRejectedError extends DaemonRequestError {
|
|
5009
|
+
constructor() {
|
|
5010
|
+
super("daemon authentication failed before dispatch", true);
|
|
5011
|
+
this.name = "DaemonAuthenticationRejectedError";
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
|
|
5015
|
+
class DaemonRequestCancelledError extends DaemonRequestError {
|
|
5016
|
+
constructor(requestWritten) {
|
|
5017
|
+
super("daemon request cancelled", requestWritten);
|
|
5018
|
+
this.name = "DaemonRequestCancelledError";
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
async function callToolViaDaemon(name, args, options) {
|
|
5022
|
+
const context = requireContext(options.context);
|
|
3237
5023
|
const paths = options.paths ?? daemonPaths();
|
|
3238
|
-
const ensure = options.ensure ?? ensureDaemonRunning;
|
|
5024
|
+
const ensure = options.ensure ?? ((ensurePaths, signal) => ensureDaemonRunning(ensurePaths, undefined, signal === undefined ? {} : { signal }));
|
|
3239
5025
|
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
3240
|
-
const requestArgs = withContext(args,
|
|
5026
|
+
const requestArgs = withContext(args, context);
|
|
3241
5027
|
let lastError;
|
|
3242
|
-
|
|
5028
|
+
let authRefreshUsed = false;
|
|
5029
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
3243
5030
|
try {
|
|
3244
|
-
await
|
|
3245
|
-
|
|
5031
|
+
await ensureDaemonAvailable(paths, ensure, options.signal);
|
|
5032
|
+
const token = readAuthToken(paths);
|
|
5033
|
+
if (!token)
|
|
5034
|
+
throw new DaemonRequestError("daemon auth token missing", false);
|
|
5035
|
+
const sendOptions = options.signal === undefined ? { timeoutMs } : { timeoutMs, signal: options.signal };
|
|
5036
|
+
return await sendToolCall(paths, token, name, requestArgs, sendOptions);
|
|
3246
5037
|
} catch (error) {
|
|
3247
5038
|
lastError = error;
|
|
3248
|
-
if (error instanceof
|
|
5039
|
+
if (error instanceof DaemonAuthenticationRejectedError && !authRefreshUsed) {
|
|
5040
|
+
authRefreshUsed = true;
|
|
5041
|
+
continue;
|
|
5042
|
+
}
|
|
5043
|
+
if (error instanceof DaemonRequestCancelledError)
|
|
5044
|
+
break;
|
|
5045
|
+
if (error instanceof DaemonRequestError && (error.requestWritten || !isRetryableTool(name)))
|
|
3249
5046
|
break;
|
|
3250
5047
|
}
|
|
3251
5048
|
}
|
|
3252
5049
|
return daemonUnreachableResult(paths, lastError);
|
|
3253
5050
|
}
|
|
3254
|
-
function callDiagnosticsViaDaemon(filePath, options
|
|
5051
|
+
function callDiagnosticsViaDaemon(filePath, options) {
|
|
3255
5052
|
return callToolViaDaemon("diagnostics", { filePath, severity: "error" }, options);
|
|
3256
5053
|
}
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
];
|
|
3262
|
-
function currentRequestContext(env = process.env) {
|
|
3263
|
-
const forwarded = {};
|
|
3264
|
-
for (const key of FORWARDED_ENV_KEYS) {
|
|
3265
|
-
const value = env[key];
|
|
3266
|
-
if (value !== undefined)
|
|
3267
|
-
forwarded[key] = value;
|
|
3268
|
-
}
|
|
3269
|
-
return { cwd: process.cwd(), env: forwarded };
|
|
5054
|
+
function requireContext(context) {
|
|
5055
|
+
if (!context)
|
|
5056
|
+
throw new DaemonRequestError("daemon tool context is required", false);
|
|
5057
|
+
return parseLspRequestContext(context);
|
|
3270
5058
|
}
|
|
3271
5059
|
function withContext(args, context) {
|
|
3272
|
-
if (!context || context.cwd === undefined && context.env === undefined)
|
|
3273
|
-
return args;
|
|
3274
5060
|
return { ...args, [CONTEXT_KEY]: context };
|
|
3275
5061
|
}
|
|
5062
|
+
function ensureDaemonAvailable(paths, ensure, signal) {
|
|
5063
|
+
if (!signal)
|
|
5064
|
+
return ensure(paths);
|
|
5065
|
+
return new Promise((resolve9, reject) => {
|
|
5066
|
+
let settled = false;
|
|
5067
|
+
const finish = (run) => {
|
|
5068
|
+
if (settled)
|
|
5069
|
+
return;
|
|
5070
|
+
settled = true;
|
|
5071
|
+
signal.removeEventListener("abort", onAbort);
|
|
5072
|
+
run();
|
|
5073
|
+
};
|
|
5074
|
+
const onAbort = () => finish(() => reject(new DaemonRequestCancelledError(false)));
|
|
5075
|
+
if (signal.aborted) {
|
|
5076
|
+
onAbort();
|
|
5077
|
+
return;
|
|
5078
|
+
}
|
|
5079
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5080
|
+
Promise.resolve().then(() => ensure(paths, signal)).then(() => finish(() => resolve9()), (error) => finish(() => reject(signal.aborted ? new DaemonRequestCancelledError(false) : error)));
|
|
5081
|
+
});
|
|
5082
|
+
}
|
|
3276
5083
|
function daemonUnreachableResult(paths, error) {
|
|
3277
5084
|
const text2 = [
|
|
3278
5085
|
`LSP daemon unreachable: ${errorText(error)}.`,
|
|
@@ -3284,39 +5091,84 @@ function daemonUnreachableResult(paths, error) {
|
|
|
3284
5091
|
`);
|
|
3285
5092
|
return { content: [{ type: "text", text: text2 }], isError: true };
|
|
3286
5093
|
}
|
|
3287
|
-
function sendToolCall(
|
|
3288
|
-
return new Promise((
|
|
3289
|
-
const socket =
|
|
5094
|
+
function sendToolCall(paths, token, name, args, options) {
|
|
5095
|
+
return new Promise((resolve9, reject) => {
|
|
5096
|
+
const socket = connect(paths.socket);
|
|
5097
|
+
const requestId = allocateProxyRequestId();
|
|
3290
5098
|
let settled = false;
|
|
3291
5099
|
let requestWritten = false;
|
|
5100
|
+
let cancelAfterWrite = false;
|
|
5101
|
+
const cancelPayload = () => encodeJsonLine({
|
|
5102
|
+
jsonrpc: "2.0",
|
|
5103
|
+
method: "$/cancelRequest",
|
|
5104
|
+
params: { _omo: authEnvelope(token), id: requestId }
|
|
5105
|
+
});
|
|
3292
5106
|
const finish = (run) => {
|
|
3293
5107
|
if (settled)
|
|
3294
5108
|
return;
|
|
3295
5109
|
settled = true;
|
|
3296
5110
|
clearTimeout(timer);
|
|
3297
|
-
|
|
5111
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
5112
|
+
destroyAfterCancel();
|
|
3298
5113
|
run();
|
|
3299
5114
|
};
|
|
3300
|
-
const
|
|
5115
|
+
const sendCancel = () => {
|
|
5116
|
+
if (!requestWritten) {
|
|
5117
|
+
cancelAfterWrite = true;
|
|
5118
|
+
return;
|
|
5119
|
+
}
|
|
5120
|
+
if (!socket.writable)
|
|
5121
|
+
return;
|
|
5122
|
+
socket.write(cancelPayload());
|
|
5123
|
+
};
|
|
5124
|
+
const destroyAfterCancel = () => {
|
|
5125
|
+
socket.destroy();
|
|
5126
|
+
};
|
|
5127
|
+
const onAbort = () => {
|
|
5128
|
+
sendCancel();
|
|
5129
|
+
finish(() => reject(new DaemonRequestCancelledError(requestWritten)));
|
|
5130
|
+
};
|
|
5131
|
+
const timer = setTimeout(() => {
|
|
5132
|
+
sendCancel();
|
|
5133
|
+
finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten)));
|
|
5134
|
+
}, options.timeoutMs);
|
|
3301
5135
|
timer.unref();
|
|
5136
|
+
if (options.signal?.aborted) {
|
|
5137
|
+
onAbort();
|
|
5138
|
+
return;
|
|
5139
|
+
}
|
|
5140
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3302
5141
|
const decoder = createLineDecoder((message) => {
|
|
3303
|
-
|
|
5142
|
+
if (isAuthErrorResponse(message)) {
|
|
5143
|
+
finish(() => reject(new DaemonAuthenticationRejectedError));
|
|
5144
|
+
return;
|
|
5145
|
+
}
|
|
5146
|
+
const result = toToolResult(message, requestId);
|
|
3304
5147
|
if (result)
|
|
3305
|
-
finish(() =>
|
|
5148
|
+
finish(() => resolve9(result));
|
|
3306
5149
|
else
|
|
3307
5150
|
finish(() => reject(new DaemonRequestError("invalid daemon response", requestWritten)));
|
|
3308
5151
|
});
|
|
3309
5152
|
socket.once("connect", () => {
|
|
3310
|
-
|
|
3311
|
-
|
|
5153
|
+
const payload = encodeJsonLine({
|
|
5154
|
+
jsonrpc: "2.0",
|
|
5155
|
+
id: requestId,
|
|
5156
|
+
method: "tools/call",
|
|
5157
|
+
params: { _omo: authEnvelope(token), name, arguments: args }
|
|
5158
|
+
});
|
|
5159
|
+
socket.write(payload, () => {
|
|
5160
|
+
requestWritten = true;
|
|
5161
|
+
if (cancelAfterWrite && socket.writable)
|
|
5162
|
+
socket.write(cancelPayload());
|
|
5163
|
+
});
|
|
3312
5164
|
});
|
|
3313
5165
|
socket.on("data", (chunk) => decoder.push(chunk));
|
|
3314
5166
|
socket.once("error", (error) => finish(() => reject(new DaemonRequestError(error.message, requestWritten))));
|
|
3315
5167
|
socket.once("close", () => finish(() => reject(new DaemonRequestError("daemon connection closed", requestWritten))));
|
|
3316
5168
|
});
|
|
3317
5169
|
}
|
|
3318
|
-
function toToolResult(message) {
|
|
3319
|
-
if (!isPlainRecord(message) || message["id"] !==
|
|
5170
|
+
function toToolResult(message, requestId) {
|
|
5171
|
+
if (!isPlainRecord(message) || message["id"] !== requestId)
|
|
3320
5172
|
return null;
|
|
3321
5173
|
const result = message["result"];
|
|
3322
5174
|
if (!isPlainRecord(result) || !Array.isArray(result["content"]))
|
|
@@ -3327,167 +5179,436 @@ function toToolResult(message) {
|
|
|
3327
5179
|
details: result["details"]
|
|
3328
5180
|
};
|
|
3329
5181
|
}
|
|
5182
|
+
function allocateProxyRequestId() {
|
|
5183
|
+
const id = nextProxyRequestId;
|
|
5184
|
+
nextProxyRequestId += 1;
|
|
5185
|
+
if (nextProxyRequestId > Number.MAX_SAFE_INTEGER)
|
|
5186
|
+
nextProxyRequestId = 1;
|
|
5187
|
+
return id;
|
|
5188
|
+
}
|
|
5189
|
+
function isRetryableTool(name) {
|
|
5190
|
+
return name !== "rename" && name !== "lsp_rename";
|
|
5191
|
+
}
|
|
3330
5192
|
function errorText(error) {
|
|
3331
5193
|
return error instanceof Error ? error.message : String(error);
|
|
3332
5194
|
}
|
|
5195
|
+
var OMO_LSP_DAEMON_CLI2 = "OMO_LSP_DAEMON_CLI";
|
|
5196
|
+
var OMO_LSP_DAEMON_VERSION2 = "OMO_LSP_DAEMON_VERSION";
|
|
5197
|
+
var DAEMON_VERSION_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
5198
|
+
|
|
5199
|
+
class InvalidRuntimeOverrideError2 extends Error {
|
|
5200
|
+
code = "invalid_runtime_override";
|
|
5201
|
+
reason;
|
|
5202
|
+
constructor(reason, message) {
|
|
5203
|
+
super(message);
|
|
5204
|
+
this.name = "InvalidRuntimeOverrideError";
|
|
5205
|
+
this.reason = reason;
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
function validateDaemonVersion2(version) {
|
|
5209
|
+
if (!DAEMON_VERSION_PATTERN2.test(version)) {
|
|
5210
|
+
throw new Error("LSP daemon version must match [A-Za-z0-9][A-Za-z0-9._+-]{0,127}");
|
|
5211
|
+
}
|
|
5212
|
+
return version;
|
|
5213
|
+
}
|
|
5214
|
+
function resolveDaemonRuntime2(env, defaults) {
|
|
5215
|
+
const cliOverride = env[OMO_LSP_DAEMON_CLI2];
|
|
5216
|
+
const versionOverride = env[OMO_LSP_DAEMON_VERSION2];
|
|
5217
|
+
const hasCliOverride = cliOverride !== undefined;
|
|
5218
|
+
const hasVersionOverride = versionOverride !== undefined;
|
|
5219
|
+
if (hasCliOverride !== hasVersionOverride) {
|
|
5220
|
+
throw new InvalidRuntimeOverrideError2("paired_values_required", `${OMO_LSP_DAEMON_CLI2} and ${OMO_LSP_DAEMON_VERSION2} must be set together`);
|
|
5221
|
+
}
|
|
5222
|
+
if (!hasCliOverride || !hasVersionOverride) {
|
|
5223
|
+
if (!isAbsolute4(defaults.cliPath)) {
|
|
5224
|
+
throw new InvalidRuntimeOverrideError2("packaged_cli_must_be_absolute", "Packaged LSP daemon CLI path must be absolute");
|
|
5225
|
+
}
|
|
5226
|
+
return { cliPath: defaults.cliPath, version: validateDaemonVersion2(defaults.version) };
|
|
5227
|
+
}
|
|
5228
|
+
if (!isAbsolute4(cliOverride)) {
|
|
5229
|
+
throw new InvalidRuntimeOverrideError2("cli_must_be_absolute", `${OMO_LSP_DAEMON_CLI2} must be an absolute path to an existing regular file`);
|
|
5230
|
+
}
|
|
5231
|
+
const stat = statSync5(cliOverride, { throwIfNoEntry: false });
|
|
5232
|
+
if (!stat) {
|
|
5233
|
+
throw new InvalidRuntimeOverrideError2("cli_not_found", `${OMO_LSP_DAEMON_CLI2} points to a missing file`);
|
|
5234
|
+
}
|
|
5235
|
+
if (!stat.isFile()) {
|
|
5236
|
+
throw new InvalidRuntimeOverrideError2("cli_not_file", `${OMO_LSP_DAEMON_CLI2} must point to a regular file`);
|
|
5237
|
+
}
|
|
5238
|
+
return { cliPath: cliOverride, version: validateDaemonVersion2(versionOverride) };
|
|
5239
|
+
}
|
|
5240
|
+
function callDiagnosticsViaDaemon2(filePath, options) {
|
|
5241
|
+
return callDiagnosticsViaDaemon(filePath, options);
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
// ../../../../lsp-core/src/post-edit/orchestration.ts
|
|
5245
|
+
import { extname as extname2 } from "node:path";
|
|
5246
|
+
var DEFAULT_MAX_CONCURRENCY = 4;
|
|
5247
|
+
var CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
|
|
5248
|
+
function createPostEditNotConfiguredCache() {
|
|
5249
|
+
return { notConfiguredExtensions: new Set };
|
|
5250
|
+
}
|
|
5251
|
+
async function collectPostEditDiagnostics(input) {
|
|
5252
|
+
const cache = input.cache === undefined ? createPostEditNotConfiguredCache() : input.cache;
|
|
5253
|
+
const filePaths = firstSeenDiagnosticTargets(input.filePaths, cache);
|
|
5254
|
+
const results = await runBoundedDiagnostics(filePaths, input.runDiagnostics, input.maxConcurrency);
|
|
5255
|
+
const blocks = [];
|
|
5256
|
+
const observations = [];
|
|
5257
|
+
for (const result of results) {
|
|
5258
|
+
const classification = classifyDiagnostics(result.outcome);
|
|
5259
|
+
switch (classification.kind) {
|
|
5260
|
+
case "clean":
|
|
5261
|
+
observations.push({ filePath: result.filePath, kind: "clean" });
|
|
5262
|
+
break;
|
|
5263
|
+
case "not_configured":
|
|
5264
|
+
cache.notConfiguredExtensions.add(classification.extension);
|
|
5265
|
+
observations.push({ filePath: result.filePath, kind: "not_configured" });
|
|
5266
|
+
break;
|
|
5267
|
+
case "block":
|
|
5268
|
+
blocks.push({ filePath: result.filePath, diagnostics: classification.diagnostics });
|
|
5269
|
+
observations.push({ filePath: result.filePath, kind: "block" });
|
|
5270
|
+
break;
|
|
5271
|
+
default: {
|
|
5272
|
+
const exhaustive = classification;
|
|
5273
|
+
return exhaustive;
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
return { blocks, observations };
|
|
5278
|
+
}
|
|
5279
|
+
function firstSeenDiagnosticTargets(filePaths, cache) {
|
|
5280
|
+
const seen = new Set;
|
|
5281
|
+
const targets = [];
|
|
5282
|
+
for (const filePath of filePaths) {
|
|
5283
|
+
if (filePath.length === 0 || seen.has(filePath))
|
|
5284
|
+
continue;
|
|
5285
|
+
seen.add(filePath);
|
|
5286
|
+
const extension = extensionKey(filePath);
|
|
5287
|
+
if (extension !== undefined && cache.notConfiguredExtensions.has(extension))
|
|
5288
|
+
continue;
|
|
5289
|
+
targets.push({ index: targets.length, filePath });
|
|
5290
|
+
}
|
|
5291
|
+
return targets;
|
|
5292
|
+
}
|
|
5293
|
+
async function runBoundedDiagnostics(filePaths, runDiagnostics, maxConcurrency) {
|
|
5294
|
+
const results = [];
|
|
5295
|
+
const workerCount = Math.min(Math.max(1, maxConcurrency ?? DEFAULT_MAX_CONCURRENCY), filePaths.length);
|
|
5296
|
+
let nextIndex = 0;
|
|
5297
|
+
async function worker() {
|
|
5298
|
+
for (;; ) {
|
|
5299
|
+
const target = filePaths[nextIndex];
|
|
5300
|
+
nextIndex += 1;
|
|
5301
|
+
if (target === undefined)
|
|
5302
|
+
return;
|
|
5303
|
+
results[target.index] = {
|
|
5304
|
+
index: target.index,
|
|
5305
|
+
filePath: target.filePath,
|
|
5306
|
+
outcome: await collectFileDiagnostics(target.filePath, runDiagnostics)
|
|
5307
|
+
};
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
5311
|
+
return results.filter((result) => result !== undefined);
|
|
5312
|
+
}
|
|
5313
|
+
async function collectFileDiagnostics(filePath, runDiagnostics) {
|
|
5314
|
+
try {
|
|
5315
|
+
return normalizeDiagnosticsOutcome(await runDiagnostics(filePath));
|
|
5316
|
+
} catch (error) {
|
|
5317
|
+
if (error instanceof Error)
|
|
5318
|
+
return formatDiagnosticsError(error);
|
|
5319
|
+
return normalizeDiagnosticsText(String(error));
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
function normalizeDiagnosticsOutcome(outcome) {
|
|
5323
|
+
if (typeof outcome === "string")
|
|
5324
|
+
return normalizeDiagnosticsText(outcome);
|
|
5325
|
+
return outcome;
|
|
5326
|
+
}
|
|
5327
|
+
function normalizeDiagnosticsText(text2) {
|
|
5328
|
+
return text2.trim();
|
|
5329
|
+
}
|
|
5330
|
+
function formatDiagnosticsError(error) {
|
|
5331
|
+
const message = normalizeDiagnosticsText(error.message);
|
|
5332
|
+
return message.length > 0 ? message : normalizeDiagnosticsText(String(error));
|
|
5333
|
+
}
|
|
5334
|
+
function classifyDiagnostics(outcome) {
|
|
5335
|
+
if (typeof outcome !== "string") {
|
|
5336
|
+
return { kind: "not_configured", extension: outcome.extension };
|
|
5337
|
+
}
|
|
5338
|
+
const diagnostics = outcome;
|
|
5339
|
+
if (diagnostics.length === 0 || diagnostics === CLEAN_DIAGNOSTICS_TEXT)
|
|
5340
|
+
return { kind: "clean" };
|
|
5341
|
+
return { kind: "block", diagnostics };
|
|
5342
|
+
}
|
|
5343
|
+
function extensionKey(filePath) {
|
|
5344
|
+
const extension = extname2(filePath).toLowerCase();
|
|
5345
|
+
return extension.length === 0 ? undefined : extension;
|
|
5346
|
+
}
|
|
5347
|
+
// ../../../../lsp-core/src/request-context.ts
|
|
5348
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
5349
|
+
import { existsSync as existsSync11, realpathSync as realpathSync4, statSync as statSync6 } from "node:fs";
|
|
5350
|
+
import { basename as basename3, delimiter as delimiter4, dirname as dirname2, isAbsolute as isAbsolute5, join as join7, relative as relative5, resolve as resolve9 } from "node:path";
|
|
3333
5351
|
|
|
3334
|
-
|
|
3335
|
-
|
|
5352
|
+
class LspRequestContextParseError2 extends Error {
|
|
5353
|
+
code;
|
|
5354
|
+
name = "LspRequestContextParseError";
|
|
5355
|
+
constructor(code, message) {
|
|
5356
|
+
super(message);
|
|
5357
|
+
this.code = code;
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
var storage2 = new AsyncLocalStorage2;
|
|
5361
|
+
var CONTEXT_FIELDS2 = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
5362
|
+
var CAPABILITY_FIELDS2 = new Set(["installDecisionTool"]);
|
|
5363
|
+
function parseLspRequestContext2(value) {
|
|
5364
|
+
if (!isRecord7(value)) {
|
|
5365
|
+
throw new LspRequestContextParseError2("invalid_context", "LSP request context must be an object.");
|
|
5366
|
+
}
|
|
5367
|
+
rejectUnknownFields2(value, CONTEXT_FIELDS2, "context");
|
|
5368
|
+
const cwd = stringField2(value, "cwd");
|
|
5369
|
+
const projectConfigPaths = stringArrayField2(value, "projectConfigPaths");
|
|
5370
|
+
const userConfigPath = stringField2(value, "userConfigPath");
|
|
5371
|
+
const installDecisionsPath = stringField2(value, "installDecisionsPath");
|
|
5372
|
+
const capabilities = capabilitiesField2(value["capabilities"]);
|
|
5373
|
+
const canonical = canonicalCwd2(cwd);
|
|
5374
|
+
for (const path2 of projectConfigPaths) {
|
|
5375
|
+
requireAbsolutePath2(path2, "projectConfigPaths");
|
|
5376
|
+
const projectPath = canonicalizeExistingOrNearestAncestor2(path2);
|
|
5377
|
+
if (!isPathInside2(canonical, projectPath)) {
|
|
5378
|
+
throw new LspRequestContextParseError2("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path2}`);
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
requireAbsolutePath2(userConfigPath, "userConfigPath");
|
|
5382
|
+
requireAbsolutePath2(installDecisionsPath, "installDecisionsPath");
|
|
5383
|
+
return {
|
|
5384
|
+
cwd: canonical,
|
|
5385
|
+
projectConfigPaths: projectConfigPaths.map((path2) => canonicalizeExistingOrNearestAncestor2(path2)),
|
|
5386
|
+
userConfigPath,
|
|
5387
|
+
installDecisionsPath,
|
|
5388
|
+
capabilities
|
|
5389
|
+
};
|
|
5390
|
+
}
|
|
5391
|
+
function canonicalCwd2(cwd) {
|
|
5392
|
+
const resolved = resolve9(cwd);
|
|
5393
|
+
if (!existsSync11(resolved) || !statSync6(resolved).isDirectory()) {
|
|
5394
|
+
throw new LspRequestContextParseError2("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
5395
|
+
}
|
|
5396
|
+
return realpathSync4(resolved);
|
|
5397
|
+
}
|
|
5398
|
+
function canonicalizeExistingOrNearestAncestor2(path2) {
|
|
5399
|
+
let current = resolve9(path2);
|
|
5400
|
+
const suffix = [];
|
|
5401
|
+
while (true) {
|
|
5402
|
+
try {
|
|
5403
|
+
const existing = realpathSync4(current);
|
|
5404
|
+
return suffix.length === 0 ? existing : join7(existing, ...suffix);
|
|
5405
|
+
} catch (error) {
|
|
5406
|
+
if (!isMissingPathError2(error))
|
|
5407
|
+
throw error;
|
|
5408
|
+
const parent = dirname2(current);
|
|
5409
|
+
if (parent === current)
|
|
5410
|
+
throw error;
|
|
5411
|
+
suffix.unshift(basename3(current));
|
|
5412
|
+
current = parent;
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
function capabilitiesField2(value) {
|
|
5417
|
+
if (!isRecord7(value)) {
|
|
5418
|
+
throw new LspRequestContextParseError2("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
5419
|
+
}
|
|
5420
|
+
rejectUnknownFields2(value, CAPABILITY_FIELDS2, "capabilities");
|
|
5421
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
5422
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
5423
|
+
throw new LspRequestContextParseError2("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
|
|
5424
|
+
}
|
|
5425
|
+
return { installDecisionTool };
|
|
5426
|
+
}
|
|
5427
|
+
function stringField2(value, field) {
|
|
5428
|
+
const fieldValue = value[field];
|
|
5429
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
5430
|
+
throw new LspRequestContextParseError2("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
5431
|
+
}
|
|
5432
|
+
return fieldValue;
|
|
5433
|
+
}
|
|
5434
|
+
function stringArrayField2(value, field) {
|
|
5435
|
+
const fieldValue = value[field];
|
|
5436
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
5437
|
+
throw new LspRequestContextParseError2("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
|
|
5438
|
+
}
|
|
5439
|
+
return fieldValue;
|
|
5440
|
+
}
|
|
5441
|
+
function requireAbsolutePath2(path2, field) {
|
|
5442
|
+
if (!isAbsolute5(path2)) {
|
|
5443
|
+
throw new LspRequestContextParseError2("relative_path", `LSP request context.${field} must be absolute: ${path2}`);
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
function isPathInside2(parent, child) {
|
|
5447
|
+
const childPath = resolve9(child);
|
|
5448
|
+
const relativePath = relative5(parent, childPath);
|
|
5449
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute5(relativePath);
|
|
5450
|
+
}
|
|
5451
|
+
function isMissingPathError2(error) {
|
|
5452
|
+
const code = errorCode2(error);
|
|
5453
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
5454
|
+
}
|
|
5455
|
+
function rejectUnknownFields2(value, allowed, scope) {
|
|
5456
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
5457
|
+
if (unknown.length > 0) {
|
|
5458
|
+
throw new LspRequestContextParseError2("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
function isRecord7(value) {
|
|
5462
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5463
|
+
}
|
|
5464
|
+
function errorCode2(error) {
|
|
5465
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
5466
|
+
return;
|
|
5467
|
+
const code = Reflect.get(error, "code");
|
|
5468
|
+
return typeof code === "string" ? code : undefined;
|
|
5469
|
+
}
|
|
3336
5470
|
|
|
3337
|
-
//
|
|
3338
|
-
import { existsSync as
|
|
5471
|
+
// src/daemon-cli-path.ts
|
|
5472
|
+
import { existsSync as existsSync13, readFileSync as readFileSync6 } from "node:fs";
|
|
3339
5473
|
import { createRequire as createRequire2 } from "node:module";
|
|
3340
|
-
import { dirname as
|
|
5474
|
+
import { dirname as dirname9, join as join9 } from "node:path";
|
|
3341
5475
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3342
5476
|
var requireFromHere2 = createRequire2(import.meta.url);
|
|
3343
|
-
var PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/
|
|
3344
|
-
var CODEX_LSP_DAEMON_CLI_ENV2 = "CODEX_LSP_DAEMON_CLI";
|
|
3345
|
-
var CODEX_LSP_DAEMON_VERSION_ENV2 = "CODEX_LSP_DAEMON_VERSION";
|
|
5477
|
+
var PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/cli";
|
|
3346
5478
|
function ensureLspDaemonCliEnv(env = process.env) {
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
5479
|
+
const runtime = resolveDaemonRuntime2(env, resolveLspDaemonCli());
|
|
5480
|
+
env[OMO_LSP_DAEMON_CLI2] = runtime.cliPath;
|
|
5481
|
+
env[OMO_LSP_DAEMON_VERSION2] = runtime.version;
|
|
5482
|
+
}
|
|
5483
|
+
function resolveLspDaemonCliPath(env = process.env) {
|
|
5484
|
+
const runtime = resolveDaemonRuntime2(env, resolveLspDaemonCli());
|
|
5485
|
+
if (env[OMO_LSP_DAEMON_CLI2] === undefined)
|
|
5486
|
+
env[OMO_LSP_DAEMON_CLI2] = runtime.cliPath;
|
|
5487
|
+
if (env[OMO_LSP_DAEMON_VERSION2] === undefined)
|
|
5488
|
+
env[OMO_LSP_DAEMON_VERSION2] = runtime.version;
|
|
5489
|
+
return runtime.cliPath;
|
|
3357
5490
|
}
|
|
3358
5491
|
function resolveLspDaemonCli() {
|
|
3359
5492
|
const packageCli = resolvePackageLspDaemonCliPath();
|
|
3360
5493
|
if (packageCli !== null)
|
|
3361
5494
|
return resolveConfiguredLspDaemonCli(packageCli);
|
|
3362
5495
|
const bundledCli = fileURLToPath4(new URL("../../lsp-daemon/dist/cli.js", import.meta.url));
|
|
3363
|
-
if (
|
|
5496
|
+
if (existsSync13(bundledCli))
|
|
3364
5497
|
return resolveConfiguredLspDaemonCli(bundledCli);
|
|
3365
5498
|
return resolveConfiguredLspDaemonCli(bundledCli);
|
|
3366
5499
|
}
|
|
3367
5500
|
function resolvePackageLspDaemonCliPath() {
|
|
3368
5501
|
try {
|
|
3369
5502
|
return requireFromHere2.resolve(PACKAGE_LSP_DAEMON_CLI);
|
|
3370
|
-
} catch {
|
|
5503
|
+
} catch (error) {
|
|
5504
|
+
if (!(error instanceof Error))
|
|
5505
|
+
throw error;
|
|
3371
5506
|
return null;
|
|
3372
5507
|
}
|
|
3373
5508
|
}
|
|
3374
5509
|
function resolveConfiguredLspDaemonCli(cliPath) {
|
|
5510
|
+
const version = readDaemonPackageVersion(cliPath);
|
|
5511
|
+
if (version === null) {
|
|
5512
|
+
throw new Error(`Unable to determine packaged LSP daemon version beside ${cliPath}`);
|
|
5513
|
+
}
|
|
3375
5514
|
return {
|
|
3376
5515
|
cliPath,
|
|
3377
|
-
version
|
|
5516
|
+
version
|
|
3378
5517
|
};
|
|
3379
5518
|
}
|
|
3380
5519
|
function readDaemonPackageVersion(cliPath) {
|
|
3381
5520
|
try {
|
|
3382
|
-
const parsed = JSON.parse(readFileSync6(join9(
|
|
3383
|
-
if (
|
|
5521
|
+
const parsed = JSON.parse(readFileSync6(join9(dirname9(cliPath), "package.json"), "utf8"));
|
|
5522
|
+
if (isRecord9(parsed) && typeof parsed["version"] === "string" && parsed["version"].length > 0) {
|
|
3384
5523
|
return parsed["version"];
|
|
3385
5524
|
}
|
|
3386
|
-
} catch {
|
|
5525
|
+
} catch (error) {
|
|
5526
|
+
if (!(error instanceof Error))
|
|
5527
|
+
throw error;
|
|
5528
|
+
}
|
|
3387
5529
|
return null;
|
|
3388
5530
|
}
|
|
3389
|
-
function
|
|
5531
|
+
function isRecord9(value) {
|
|
3390
5532
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3391
5533
|
}
|
|
3392
5534
|
|
|
3393
|
-
//
|
|
5535
|
+
// src/lsp-session-state.ts
|
|
3394
5536
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3395
|
-
import { homedir
|
|
3396
|
-
import { dirname as
|
|
5537
|
+
import { homedir } from "node:os";
|
|
5538
|
+
import { dirname as dirname11, join as join10 } from "node:path";
|
|
3397
5539
|
function sessionIdFrom(input) {
|
|
3398
5540
|
return typeof input.session_id === "string" && input.session_id.length > 0 ? input.session_id : undefined;
|
|
3399
5541
|
}
|
|
3400
|
-
function
|
|
5542
|
+
function readLspPostEditCache(sessionId) {
|
|
3401
5543
|
if (sessionId === undefined)
|
|
3402
|
-
return
|
|
3403
|
-
const state = readSessionState(sessionStatePath(sessionId));
|
|
3404
|
-
const extension = extensionKey(filePath);
|
|
3405
|
-
return extension !== undefined && state.postCompactProbePending !== true && state.unavailableExtensions.includes(extension);
|
|
3406
|
-
}
|
|
3407
|
-
function recordLspDiagnosticsObservations(sessionId, observations) {
|
|
3408
|
-
if (sessionId === undefined || observations.length === 0)
|
|
3409
|
-
return;
|
|
5544
|
+
return { notConfiguredExtensions: new Set };
|
|
3410
5545
|
const state = readSessionState(sessionStatePath(sessionId));
|
|
3411
|
-
|
|
3412
|
-
for (const observation of observations) {
|
|
3413
|
-
const extension = extensionKey(observation.filePath);
|
|
3414
|
-
if (extension === undefined)
|
|
3415
|
-
continue;
|
|
3416
|
-
if (observation.unavailable) {
|
|
3417
|
-
unavailableExtensions.add(extension);
|
|
3418
|
-
} else {
|
|
3419
|
-
unavailableExtensions.delete(extension);
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
writeSessionState(sessionStatePath(sessionId), { unavailableExtensions: [...unavailableExtensions].sort() });
|
|
5546
|
+
return { notConfiguredExtensions: new Set(state.notConfiguredExtensions) };
|
|
3423
5547
|
}
|
|
3424
|
-
function
|
|
5548
|
+
function writeLspPostEditCache(sessionId, cache) {
|
|
3425
5549
|
if (sessionId === undefined)
|
|
3426
5550
|
return;
|
|
3427
|
-
const state = readSessionState(sessionStatePath(sessionId));
|
|
3428
|
-
if (state.unavailableExtensions.length === 0)
|
|
3429
|
-
return;
|
|
3430
5551
|
writeSessionState(sessionStatePath(sessionId), {
|
|
3431
|
-
|
|
3432
|
-
postCompactProbePending: true
|
|
5552
|
+
notConfiguredExtensions: [...cache.notConfiguredExtensions].sort()
|
|
3433
5553
|
});
|
|
3434
5554
|
}
|
|
3435
|
-
function
|
|
3436
|
-
|
|
3437
|
-
|
|
5555
|
+
function markLspSessionCompacted(sessionId) {
|
|
5556
|
+
if (sessionId === undefined)
|
|
5557
|
+
return;
|
|
5558
|
+
writeSessionState(sessionStatePath(sessionId), emptyState());
|
|
3438
5559
|
}
|
|
3439
5560
|
function isLspDaemonUnreachableDiagnostics(diagnostics) {
|
|
3440
5561
|
return diagnostics.includes("LSP daemon unreachable");
|
|
3441
5562
|
}
|
|
3442
5563
|
function sessionStatePath(sessionId) {
|
|
3443
|
-
const root = process.env["PLUGIN_DATA"] ??
|
|
3444
|
-
return
|
|
5564
|
+
const root = process.env["PLUGIN_DATA"] ?? join10(homedir(), ".codex", "codex-lsp");
|
|
5565
|
+
return join10(root, "sessions", `${safePathSegment(sessionId)}.json`);
|
|
3445
5566
|
}
|
|
3446
|
-
function readSessionState(
|
|
5567
|
+
function readSessionState(path2) {
|
|
3447
5568
|
try {
|
|
3448
|
-
const parsed = JSON.parse(readFileSync8(
|
|
3449
|
-
if (isLspSessionState(parsed))
|
|
3450
|
-
return parsed;
|
|
5569
|
+
const parsed = JSON.parse(readFileSync8(path2, "utf8"));
|
|
5570
|
+
if (isRecord10(parsed) && isLspSessionState(parsed))
|
|
5571
|
+
return normalizeSessionState(parsed);
|
|
3451
5572
|
return emptyState();
|
|
3452
5573
|
} catch (error) {
|
|
3453
|
-
if (error instanceof SyntaxError ||
|
|
5574
|
+
if (error instanceof SyntaxError || isRecord10(error) && error["code"] === "ENOENT")
|
|
3454
5575
|
return emptyState();
|
|
3455
5576
|
throw error;
|
|
3456
5577
|
}
|
|
3457
5578
|
}
|
|
3458
|
-
function writeSessionState(
|
|
3459
|
-
mkdirSync4(
|
|
3460
|
-
writeFileSync3(
|
|
5579
|
+
function writeSessionState(path2, state) {
|
|
5580
|
+
mkdirSync4(dirname11(path2), { recursive: true });
|
|
5581
|
+
writeFileSync3(path2, `${JSON.stringify(state)}
|
|
3461
5582
|
`);
|
|
3462
5583
|
}
|
|
3463
5584
|
function emptyState() {
|
|
3464
|
-
return {
|
|
3465
|
-
}
|
|
3466
|
-
function extensionKey(filePath) {
|
|
3467
|
-
const extension = extname2(filePath).toLowerCase();
|
|
3468
|
-
return extension.length === 0 ? undefined : extension;
|
|
5585
|
+
return { notConfiguredExtensions: [] };
|
|
3469
5586
|
}
|
|
3470
5587
|
function safePathSegment(value) {
|
|
3471
5588
|
return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown-session";
|
|
3472
5589
|
}
|
|
3473
5590
|
function isLspSessionState(value) {
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
const postCompactProbePending = value["postCompactProbePending"];
|
|
3477
|
-
return value["unavailableExtensions"].every((item) => typeof item === "string") && (postCompactProbePending === undefined || typeof postCompactProbePending === "boolean");
|
|
5591
|
+
const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
|
|
5592
|
+
return Array.isArray(notConfiguredExtensions) && notConfiguredExtensions.every((item) => typeof item === "string");
|
|
3478
5593
|
}
|
|
3479
|
-
function
|
|
5594
|
+
function normalizeSessionState(value) {
|
|
5595
|
+
const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
|
|
5596
|
+
return {
|
|
5597
|
+
notConfiguredExtensions: Array.isArray(notConfiguredExtensions) ? notConfiguredExtensions.filter((item) => typeof item === "string").sort() : []
|
|
5598
|
+
};
|
|
5599
|
+
}
|
|
5600
|
+
function isRecord10(value) {
|
|
3480
5601
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3481
5602
|
}
|
|
3482
5603
|
|
|
3483
|
-
//
|
|
5604
|
+
// src/mutated-file-paths.ts
|
|
3484
5605
|
var MUTATION_TOOL_NAMES = new Set(["apply_patch", "write", "edit", "multiedit", "multi_edit"]);
|
|
3485
5606
|
function extractMutatedFilePaths(input) {
|
|
3486
5607
|
if (!isMutationTool(input.tool_name))
|
|
3487
5608
|
return [];
|
|
3488
5609
|
if (isFailedToolResponse(input.tool_response))
|
|
3489
5610
|
return [];
|
|
3490
|
-
const toolInput =
|
|
5611
|
+
const toolInput = isRecord11(input.tool_input) ? input.tool_input : {};
|
|
3491
5612
|
const paths = new Set;
|
|
3492
5613
|
addStringValue(paths, toolInput["path"]);
|
|
3493
5614
|
addStringValue(paths, toolInput["filePath"]);
|
|
@@ -3506,7 +5627,7 @@ function isMutationTool(value) {
|
|
|
3506
5627
|
return MUTATION_TOOL_NAMES.has(value.toLowerCase());
|
|
3507
5628
|
}
|
|
3508
5629
|
function isFailedToolResponse(value) {
|
|
3509
|
-
if (!
|
|
5630
|
+
if (!isRecord11(value))
|
|
3510
5631
|
return false;
|
|
3511
5632
|
return value["isError"] === true || value["is_error"] === true || value["error"] === true || value["status"] === "error";
|
|
3512
5633
|
}
|
|
@@ -3532,9 +5653,9 @@ function addPatchInput(paths, value) {
|
|
|
3532
5653
|
return;
|
|
3533
5654
|
for (const line of value.split(`
|
|
3534
5655
|
`)) {
|
|
3535
|
-
const
|
|
3536
|
-
if (
|
|
3537
|
-
paths.add(
|
|
5656
|
+
const path2 = extractPatchHeaderPath(line);
|
|
5657
|
+
if (path2 !== undefined)
|
|
5658
|
+
paths.add(path2);
|
|
3538
5659
|
}
|
|
3539
5660
|
}
|
|
3540
5661
|
function extractPatchHeaderPath(line) {
|
|
@@ -3549,7 +5670,7 @@ function addPatchFiles(paths, value) {
|
|
|
3549
5670
|
if (!Array.isArray(value))
|
|
3550
5671
|
return;
|
|
3551
5672
|
for (const item of value) {
|
|
3552
|
-
if (!
|
|
5673
|
+
if (!isRecord11(item))
|
|
3553
5674
|
continue;
|
|
3554
5675
|
addStringValue(paths, item["path"]);
|
|
3555
5676
|
addStringValue(paths, item["filePath"]);
|
|
@@ -3558,18 +5679,15 @@ function addPatchFiles(paths, value) {
|
|
|
3558
5679
|
addStringValue(paths, item["move_path"]);
|
|
3559
5680
|
}
|
|
3560
5681
|
}
|
|
3561
|
-
function
|
|
5682
|
+
function isRecord11(value) {
|
|
3562
5683
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3563
5684
|
}
|
|
3564
5685
|
|
|
3565
|
-
//
|
|
3566
|
-
var CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
|
|
3567
|
-
var UNSUPPORTED_EXTENSION_TEXT = "No LSP server configured for extension:";
|
|
5686
|
+
// src/codex-hook.ts
|
|
3568
5687
|
var DIAGNOSTIC_START_PATTERN = /(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/g;
|
|
3569
5688
|
var DIAGNOSTIC_CHUNK_PATTERN = /^(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/;
|
|
3570
5689
|
var DEFAULT_MAX_HOOK_FEEDBACK_CHARS = 8000;
|
|
3571
5690
|
var CONTEXT_PRESSURE_MAX_HOOK_FEEDBACK_CHARS = 1200;
|
|
3572
|
-
var MAX_CONCURRENT_DIAGNOSTICS = 4;
|
|
3573
5691
|
var CONTEXT_PRESSURE_MARKERS = [
|
|
3574
5692
|
"context compacted",
|
|
3575
5693
|
"context_length_exceeded",
|
|
@@ -3581,29 +5699,47 @@ var CONTEXT_PRESSURE_MARKERS = [
|
|
|
3581
5699
|
];
|
|
3582
5700
|
async function runLspDiagnosticsText(filePath) {
|
|
3583
5701
|
ensureLspDaemonCliEnv();
|
|
3584
|
-
const result = await
|
|
5702
|
+
const result = await callDiagnosticsViaDaemon2(filePath, { context: codexLspRequestContext() });
|
|
5703
|
+
return postEditOutcomeFromDaemonResult(result);
|
|
5704
|
+
}
|
|
5705
|
+
function postEditOutcomeFromDaemonResult(result) {
|
|
5706
|
+
const availability = notConfiguredAvailability(result.details);
|
|
5707
|
+
if (availability !== undefined)
|
|
5708
|
+
return { kind: "not_configured", extension: availability.extension };
|
|
3585
5709
|
return result.content.map((block) => block.text).join(`
|
|
3586
5710
|
`);
|
|
3587
5711
|
}
|
|
5712
|
+
function notConfiguredAvailability(details) {
|
|
5713
|
+
if (!isRecord12(details))
|
|
5714
|
+
return;
|
|
5715
|
+
const availability = details["availability"];
|
|
5716
|
+
if (!isRecord12(availability))
|
|
5717
|
+
return;
|
|
5718
|
+
if (availability["kind"] !== "not_configured")
|
|
5719
|
+
return;
|
|
5720
|
+
const extension = availability["extension"];
|
|
5721
|
+
return typeof extension === "string" && extension.length > 0 ? { extension } : undefined;
|
|
5722
|
+
}
|
|
5723
|
+
function codexLspRequestContext(env = process.env, cwd = process.cwd()) {
|
|
5724
|
+
const canonicalCwd3 = realpathSync6(resolve11(cwd));
|
|
5725
|
+
const codexHome = resolve11(env["CODEX_HOME"]?.trim() || join11(homedir3(), ".codex"));
|
|
5726
|
+
return parseLspRequestContext2({
|
|
5727
|
+
cwd: canonicalCwd3,
|
|
5728
|
+
projectConfigPaths: [join11(canonicalCwd3, ".codex", "lsp-client.json")],
|
|
5729
|
+
userConfigPath: join11(codexHome, "lsp-client.json"),
|
|
5730
|
+
installDecisionsPath: join11(codexHome, "lsp-install-decisions.json"),
|
|
5731
|
+
capabilities: { installDecisionTool: true }
|
|
5732
|
+
});
|
|
5733
|
+
}
|
|
3588
5734
|
async function runLspPostToolUseHook(input, runDiagnostics = runLspDiagnosticsText) {
|
|
3589
5735
|
const sessionId = sessionIdFrom(input);
|
|
3590
|
-
const filePaths = extractMutatedFilePaths(input)
|
|
5736
|
+
const filePaths = extractMutatedFilePaths(input);
|
|
3591
5737
|
if (filePaths.length === 0)
|
|
3592
5738
|
return "";
|
|
3593
|
-
const
|
|
3594
|
-
const
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
continue;
|
|
3598
|
-
const unavailable = isUnavailableLspDiagnostics(diagnostics);
|
|
3599
|
-
observations.push({ filePath, unavailable });
|
|
3600
|
-
if (isCleanDiagnostics(diagnostics))
|
|
3601
|
-
continue;
|
|
3602
|
-
if (unavailable)
|
|
3603
|
-
continue;
|
|
3604
|
-
blocks.push({ filePath, diagnostics });
|
|
3605
|
-
}
|
|
3606
|
-
recordLspDiagnosticsObservations(sessionId, observations);
|
|
5739
|
+
const cache = readLspPostEditCache(sessionId);
|
|
5740
|
+
const result = await collectPostEditDiagnostics({ filePaths, runDiagnostics, cache });
|
|
5741
|
+
writeLspPostEditCache(sessionId, cache);
|
|
5742
|
+
const blocks = result.blocks.filter(({ diagnostics }) => !isLspDaemonUnreachableDiagnostics(diagnostics));
|
|
3607
5743
|
if (blocks.length === 0)
|
|
3608
5744
|
return "";
|
|
3609
5745
|
const rawReason = blocks.map(formatDiagnosticBlock).join(`
|
|
@@ -3625,38 +5761,6 @@ async function runLspPostCompactHook(input) {
|
|
|
3625
5761
|
markLspSessionCompacted(sessionIdFrom(input));
|
|
3626
5762
|
return "";
|
|
3627
5763
|
}
|
|
3628
|
-
async function collectDiagnostics(filePaths, runDiagnostics) {
|
|
3629
|
-
const results = [];
|
|
3630
|
-
let nextIndex = 0;
|
|
3631
|
-
const workerCount = Math.min(MAX_CONCURRENT_DIAGNOSTICS, filePaths.length);
|
|
3632
|
-
async function worker() {
|
|
3633
|
-
for (;; ) {
|
|
3634
|
-
const index = nextIndex;
|
|
3635
|
-
nextIndex += 1;
|
|
3636
|
-
const filePath = filePaths[index];
|
|
3637
|
-
if (filePath === undefined)
|
|
3638
|
-
return;
|
|
3639
|
-
results[index] = { filePath, diagnostics: await collectFileDiagnostics(filePath, runDiagnostics) };
|
|
3640
|
-
}
|
|
3641
|
-
}
|
|
3642
|
-
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
3643
|
-
return results;
|
|
3644
|
-
}
|
|
3645
|
-
async function collectFileDiagnostics(filePath, runDiagnostics) {
|
|
3646
|
-
try {
|
|
3647
|
-
return (await runDiagnostics(filePath)).trim();
|
|
3648
|
-
} catch (error) {
|
|
3649
|
-
return formatDiagnosticsError(error);
|
|
3650
|
-
}
|
|
3651
|
-
}
|
|
3652
|
-
function formatDiagnosticsError(error) {
|
|
3653
|
-
if (error instanceof Error) {
|
|
3654
|
-
const message = error.message.trim();
|
|
3655
|
-
if (message.length > 0)
|
|
3656
|
-
return message;
|
|
3657
|
-
}
|
|
3658
|
-
return String(error).trim();
|
|
3659
|
-
}
|
|
3660
5764
|
function formatDiagnosticBlock({ filePath, diagnostics }) {
|
|
3661
5765
|
return `LSP diagnostics after editing ${filePath}:
|
|
3662
5766
|
|
|
@@ -3739,14 +5843,11 @@ function limitHookText(text2, maxChars) {
|
|
|
3739
5843
|
const head = text2.slice(0, maxChars - marker.length).replace(/[ \t\r\n]+$/, "");
|
|
3740
5844
|
return `${head}${marker}`;
|
|
3741
5845
|
}
|
|
3742
|
-
function
|
|
3743
|
-
return diagnostics.length === 0 || diagnostics === CLEAN_DIAGNOSTICS_TEXT || diagnostics.startsWith(UNSUPPORTED_EXTENSION_TEXT);
|
|
3744
|
-
}
|
|
3745
|
-
function isRecord8(value) {
|
|
5846
|
+
function isRecord12(value) {
|
|
3746
5847
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3747
5848
|
}
|
|
3748
5849
|
|
|
3749
|
-
//
|
|
5850
|
+
// src/codex-hook-cli.ts
|
|
3750
5851
|
async function runPostToolUseHookCli(stdin = processStdin) {
|
|
3751
5852
|
await runHookCli((input) => runLspPostToolUseHook(input), stdin);
|
|
3752
5853
|
}
|
|
@@ -3759,16 +5860,12 @@ async function runHookCli(runHook, stdin) {
|
|
|
3759
5860
|
if (!raw.trim())
|
|
3760
5861
|
return;
|
|
3761
5862
|
const parsed = JSON.parse(raw);
|
|
3762
|
-
const input =
|
|
5863
|
+
const input = isRecord12(parsed) ? parsed : {};
|
|
3763
5864
|
const output = await runHook(input);
|
|
3764
5865
|
if (output)
|
|
3765
5866
|
process.stdout.write(output);
|
|
3766
5867
|
} catch {
|
|
3767
5868
|
return;
|
|
3768
|
-
} finally {
|
|
3769
|
-
await disposeDefaultLspManager().catch(() => {
|
|
3770
|
-
return;
|
|
3771
|
-
});
|
|
3772
5869
|
}
|
|
3773
5870
|
}
|
|
3774
5871
|
async function readStdin(stdin) {
|
|
@@ -3780,7 +5877,7 @@ async function readStdin(stdin) {
|
|
|
3780
5877
|
return raw;
|
|
3781
5878
|
}
|
|
3782
5879
|
|
|
3783
|
-
//
|
|
5880
|
+
// src/cli.ts
|
|
3784
5881
|
async function main() {
|
|
3785
5882
|
const [command = "mcp", subcommand = ""] = argv.slice(2);
|
|
3786
5883
|
if (command === "hook" && subcommand === "post-tool-use") {
|
|
@@ -3807,14 +5904,14 @@ main().catch((error) => {
|
|
|
3807
5904
|
async function runPackageLspMcpCli() {
|
|
3808
5905
|
const cliPath = resolveLspDaemonCliPath();
|
|
3809
5906
|
const child = spawn3(execPath2, [cliPath, "mcp"], { stdio: "inherit" });
|
|
3810
|
-
await new Promise((
|
|
5907
|
+
await new Promise((resolve10, reject) => {
|
|
3811
5908
|
child.once("error", reject);
|
|
3812
5909
|
child.once("exit", (code, signal) => {
|
|
3813
5910
|
if (code !== null && code !== 0)
|
|
3814
5911
|
process.exitCode = code;
|
|
3815
5912
|
if (code === null && signal !== null)
|
|
3816
5913
|
process.exitCode = 1;
|
|
3817
|
-
|
|
5914
|
+
resolve10();
|
|
3818
5915
|
});
|
|
3819
5916
|
});
|
|
3820
5917
|
}
|