oh-my-opencode 4.17.1 → 4.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/codex-qa/SKILL.md +2 -0
- package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -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/dist/cli/get-local-version/types.d.ts +1 -1
- package/dist/cli/index.js +498 -165
- package/dist/cli-node/index.js +498 -165
- package/dist/index.js +425 -392
- package/dist/skills/frontend/SKILL.md +1 -1
- package/dist/skills/frontend/references/design/README.md +9 -0
- package/dist/skills/frontend/references/design/design-system-architecture.md +4 -2
- package/dist/skills/frontend/references/design/layout-skill.md +107 -0
- package/dist/skills/programming/SKILL.md +12 -2
- package/package.json +17 -16
- 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 +104 -0
- package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
- 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 +3114 -747
- package/packages/lsp-daemon/dist/client.d.ts +105 -0
- package/packages/lsp-daemon/dist/client.js +5851 -0
- package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
- package/packages/lsp-daemon/dist/daemon-client.js +113 -30
- 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 +8 -7
- package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
- package/packages/lsp-daemon/dist/index.d.ts +2 -2
- package/packages/lsp-daemon/dist/index.js +2862 -754
- 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 +3 -0
- package/packages/lsp-daemon/dist/proxy.js +54 -3
- 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 +2115 -442
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
- package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
- package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
- package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -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/package.json +1 -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/dist/.omo-runtime-manifest.json +55 -0
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
- 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 +19 -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 +8 -6
- 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/directive.md +1 -1
- 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/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 +37 -10
- 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 +37 -10
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +37 -10
- 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 +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +10 -9
- 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 +1 -1
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +9 -0
- package/packages/omo-codex/plugin/skills/frontend/references/design/design-system-architecture.md +4 -2
- package/packages/omo-codex/plugin/skills/frontend/references/design/layout-skill.md +107 -0
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +12 -2
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +37 -10
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +10 -9
- 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/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +351 -74
- package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/README.md +9 -0
- package/packages/shared-skills/skills/frontend/references/design/design-system-architecture.md +4 -2
- package/packages/shared-skills/skills/frontend/references/design/layout-skill.md +107 -0
- package/packages/shared-skills/skills/programming/SKILL.md +12 -2
|
@@ -1,80 +1,542 @@
|
|
|
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";
|
|
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
|
|
20
16
|
import { connect as connect2 } from "node:net";
|
|
21
|
-
import {
|
|
22
|
-
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";
|
|
23
22
|
import { connect } from "node:net";
|
|
24
|
-
import { dirname as
|
|
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
|
+
if (await deps.probe(paths))
|
|
430
|
+
return;
|
|
431
|
+
deps.spawnDaemon(paths);
|
|
432
|
+
await waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs);
|
|
433
|
+
}
|
|
434
|
+
async function waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs) {
|
|
435
|
+
const deadline = deps.now() + readyTimeoutMs;
|
|
436
|
+
for (;; ) {
|
|
437
|
+
if (await deps.probe(paths))
|
|
438
|
+
return;
|
|
439
|
+
if (deps.now() >= deadline)
|
|
440
|
+
throw new DaemonUnreachableError(paths.socket);
|
|
441
|
+
await deps.sleep(pollIntervalMs);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async function probeDaemon(paths, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
445
|
+
const token = readAuthToken(paths);
|
|
446
|
+
if (!token)
|
|
447
|
+
return false;
|
|
448
|
+
return await pingDaemon(paths, token, timeoutMs) !== null;
|
|
449
|
+
}
|
|
450
|
+
function pingDaemon(paths, token, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
451
|
+
return new Promise((resolve22) => {
|
|
452
|
+
const socket = connect(paths.socket);
|
|
453
|
+
let settled = false;
|
|
454
|
+
const finish = (value) => {
|
|
455
|
+
if (settled)
|
|
456
|
+
return;
|
|
457
|
+
settled = true;
|
|
458
|
+
socket.destroy();
|
|
459
|
+
resolve22(value);
|
|
460
|
+
};
|
|
461
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
462
|
+
timer.unref?.();
|
|
463
|
+
const decoder = createLineDecoder((message) => {
|
|
464
|
+
clearTimeout(timer);
|
|
465
|
+
finish(parsePingResponse(message));
|
|
466
|
+
});
|
|
467
|
+
socket.once("connect", () => {
|
|
468
|
+
socket.write(encodeJsonLine({ jsonrpc: "2.0", id: 1, method: "omo/ping", params: { _omo: authEnvelope(token) } }));
|
|
469
|
+
});
|
|
470
|
+
socket.on("data", (chunk) => decoder.push(chunk));
|
|
471
|
+
socket.once("error", () => {
|
|
472
|
+
clearTimeout(timer);
|
|
473
|
+
finish(null);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
function spawnDaemonProcess(paths) {
|
|
478
|
+
mkdirSync2(dirname3(paths.log), { recursive: true });
|
|
479
|
+
const logFd = openSync2(paths.log, "a");
|
|
480
|
+
try {
|
|
481
|
+
const child = spawn(execPath, [paths.cliPath, "daemon"], {
|
|
482
|
+
detached: true,
|
|
483
|
+
stdio: ["ignore", logFd, logFd]
|
|
484
|
+
});
|
|
485
|
+
child.unref();
|
|
486
|
+
} finally {
|
|
487
|
+
closeSync2(logFd);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function defaultEnsureDaemonDeps() {
|
|
491
|
+
return {
|
|
492
|
+
probe: (paths) => probeDaemon(paths),
|
|
493
|
+
spawnDaemon: (paths) => spawnDaemonProcess(paths),
|
|
494
|
+
sleep: (ms) => new Promise((resolve22) => {
|
|
495
|
+
setTimeout(resolve22, ms);
|
|
496
|
+
}),
|
|
497
|
+
now: () => Date.now()
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function parsePingResponse(message) {
|
|
501
|
+
if (!message || typeof message !== "object" || Array.isArray(message))
|
|
502
|
+
return null;
|
|
503
|
+
const result = Reflect.get(message, "result");
|
|
504
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
505
|
+
return null;
|
|
506
|
+
const pid = Reflect.get(result, "pid");
|
|
507
|
+
const nonce = Reflect.get(result, "nonce");
|
|
508
|
+
const startedAt = Reflect.get(result, "startedAt");
|
|
509
|
+
const endpoint = Reflect.get(result, "endpoint");
|
|
510
|
+
if (typeof pid !== "number" || typeof nonce !== "string" || typeof startedAt !== "string")
|
|
511
|
+
return null;
|
|
512
|
+
if (!endpoint || typeof endpoint !== "object" || Array.isArray(endpoint))
|
|
513
|
+
return null;
|
|
514
|
+
const path2 = Reflect.get(endpoint, "path");
|
|
515
|
+
const kind = Reflect.get(endpoint, "kind");
|
|
516
|
+
if (typeof path2 !== "string")
|
|
517
|
+
return null;
|
|
518
|
+
if (kind === "windows")
|
|
519
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
|
|
520
|
+
if (kind === "missing")
|
|
521
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
|
|
522
|
+
const dev = Reflect.get(endpoint, "dev");
|
|
523
|
+
const ino = Reflect.get(endpoint, "ino");
|
|
524
|
+
if (kind === "unix" && typeof dev === "number" && typeof ino === "number") {
|
|
525
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2, dev, ino } };
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
var HEADER_SEPARATOR = Buffer.from(`\r
|
|
530
|
+
\r
|
|
531
|
+
`);
|
|
532
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60000;
|
|
533
|
+
var BASENAME_EXTENSIONS = {
|
|
534
|
+
Dockerfile: ".dockerfile",
|
|
535
|
+
Containerfile: ".dockerfile"
|
|
536
|
+
};
|
|
537
|
+
function effectiveExtension(filePath) {
|
|
538
|
+
return BASENAME_EXTENSIONS[basename2(filePath)] ?? extname(filePath);
|
|
67
539
|
}
|
|
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
540
|
|
|
79
541
|
class LspConnectionClosedError extends Error {
|
|
80
542
|
serverId;
|
|
@@ -122,7 +584,12 @@ class LspInvalidPathError extends Error {
|
|
|
122
584
|
}
|
|
123
585
|
|
|
124
586
|
class LspServerLookupError extends Error {
|
|
587
|
+
lookup;
|
|
125
588
|
name = "LspServerLookupError";
|
|
589
|
+
constructor(message, lookup) {
|
|
590
|
+
super(message);
|
|
591
|
+
this.lookup = lookup;
|
|
592
|
+
}
|
|
126
593
|
}
|
|
127
594
|
|
|
128
595
|
class LspServerInitializingError extends Error {
|
|
@@ -140,7 +607,25 @@ class LspProcessSpawnError extends Error {
|
|
|
140
607
|
function isLspDeadConnectionError(err) {
|
|
141
608
|
return err instanceof LspConnectionClosedError || err instanceof LspProcessExitedError;
|
|
142
609
|
}
|
|
143
|
-
|
|
610
|
+
function writeCleanupError(message) {
|
|
611
|
+
process.stderr.write(`${message}
|
|
612
|
+
`);
|
|
613
|
+
}
|
|
614
|
+
function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
|
|
615
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
616
|
+
logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
|
|
617
|
+
}
|
|
618
|
+
var DEFAULT_MAX_REFERENCES = 200;
|
|
619
|
+
var DEFAULT_MAX_SYMBOLS = 200;
|
|
620
|
+
var DEFAULT_MAX_DIAGNOSTICS = 200;
|
|
621
|
+
var DEFAULT_MAX_DIRECTORY_FILES = 50;
|
|
622
|
+
var REQUEST_TIMEOUT_MS = 15000;
|
|
623
|
+
var INIT_TIMEOUT_MS = 60000;
|
|
624
|
+
var IDLE_TIMEOUT_MS = 5 * 60000;
|
|
625
|
+
var REAPER_INTERVAL_MS = 60000;
|
|
626
|
+
var STOP_HARD_KILL_TIMEOUT_MS = 5000;
|
|
627
|
+
var STOP_SIGKILL_GRACE_MS = 1000;
|
|
628
|
+
var HEADER_SEPARATOR2 = `\r
|
|
144
629
|
\r
|
|
145
630
|
`;
|
|
146
631
|
var PARSE_ERROR = -32700;
|
|
@@ -186,28 +671,80 @@ class JsonRpcConnection {
|
|
|
186
671
|
onError(handler) {
|
|
187
672
|
this.errorHandlers.push(handler);
|
|
188
673
|
}
|
|
189
|
-
async sendRequest(method, params) {
|
|
674
|
+
async sendRequest(method, params, options = {}) {
|
|
190
675
|
if (this.disposed)
|
|
191
676
|
throw new Error("JSON-RPC connection is disposed");
|
|
192
677
|
const id = this.nextRequestId;
|
|
193
678
|
this.nextRequestId += 1;
|
|
679
|
+
const key = String(id);
|
|
194
680
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
195
|
-
|
|
196
|
-
|
|
681
|
+
let requestWritten = false;
|
|
682
|
+
let cancelAfterWrite = false;
|
|
683
|
+
let settled = false;
|
|
684
|
+
const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
685
|
+
const responsePromise = new Promise((resolve22, reject) => {
|
|
686
|
+
const cleanup = () => {
|
|
687
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
688
|
+
};
|
|
689
|
+
const settleCancel = () => {
|
|
690
|
+
if (settled)
|
|
691
|
+
return;
|
|
692
|
+
settled = true;
|
|
693
|
+
this.pendingRequests.delete(key);
|
|
694
|
+
cleanup();
|
|
695
|
+
const rejectCancelled = () => reject(abortError(options.signal));
|
|
696
|
+
if (!requestWritten) {
|
|
697
|
+
cancelAfterWrite = true;
|
|
698
|
+
rejectCancelled();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
writeCancel().then(rejectCancelled, (error) => {
|
|
702
|
+
this.emitError(toError(error));
|
|
703
|
+
rejectCancelled();
|
|
704
|
+
});
|
|
705
|
+
};
|
|
706
|
+
const onAbort = () => settleCancel();
|
|
707
|
+
this.pendingRequests.set(key, {
|
|
197
708
|
resolve(result) {
|
|
198
|
-
|
|
709
|
+
settled = true;
|
|
710
|
+
cleanup();
|
|
711
|
+
resolve22(result);
|
|
199
712
|
},
|
|
200
|
-
reject
|
|
713
|
+
reject(error) {
|
|
714
|
+
settled = true;
|
|
715
|
+
cleanup();
|
|
716
|
+
reject(error);
|
|
717
|
+
},
|
|
718
|
+
cleanup
|
|
201
719
|
});
|
|
720
|
+
if (options.signal?.aborted) {
|
|
721
|
+
settleCancel();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
202
725
|
});
|
|
726
|
+
if (settled)
|
|
727
|
+
return responsePromise;
|
|
203
728
|
try {
|
|
204
729
|
await this.writeMessage(message);
|
|
730
|
+
requestWritten = true;
|
|
731
|
+
if (cancelAfterWrite)
|
|
732
|
+
await writeCancel();
|
|
205
733
|
} catch (error) {
|
|
206
|
-
|
|
734
|
+
if (settled)
|
|
735
|
+
return responsePromise;
|
|
736
|
+
const pending = this.pendingRequests.get(key);
|
|
737
|
+
if (pending) {
|
|
738
|
+
pending.cleanup();
|
|
739
|
+
this.pendingRequests.delete(key);
|
|
740
|
+
}
|
|
207
741
|
throw error;
|
|
208
742
|
}
|
|
209
743
|
return responsePromise;
|
|
210
744
|
}
|
|
745
|
+
pendingRequestCount() {
|
|
746
|
+
return this.pendingRequests.size;
|
|
747
|
+
}
|
|
211
748
|
async sendNotification(method, params) {
|
|
212
749
|
if (this.disposed)
|
|
213
750
|
return;
|
|
@@ -224,6 +761,7 @@ class JsonRpcConnection {
|
|
|
224
761
|
this.reader.off("error", this.handleStreamError);
|
|
225
762
|
this.writer.off("error", this.handleStreamError);
|
|
226
763
|
for (const pending of this.pendingRequests.values()) {
|
|
764
|
+
pending.cleanup();
|
|
227
765
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
228
766
|
}
|
|
229
767
|
this.pendingRequests.clear();
|
|
@@ -245,17 +783,17 @@ class JsonRpcConnection {
|
|
|
245
783
|
};
|
|
246
784
|
drainInputBuffer() {
|
|
247
785
|
while (true) {
|
|
248
|
-
const headerEnd = this.inputBuffer.indexOf(
|
|
786
|
+
const headerEnd = this.inputBuffer.indexOf(HEADER_SEPARATOR2);
|
|
249
787
|
if (headerEnd === -1)
|
|
250
788
|
return;
|
|
251
789
|
const headers = this.inputBuffer.subarray(0, headerEnd).toString("ascii");
|
|
252
|
-
const contentLength =
|
|
790
|
+
const contentLength = parseContentLength2(headers);
|
|
253
791
|
if (contentLength === null) {
|
|
254
792
|
this.inputBuffer = Buffer.alloc(0);
|
|
255
793
|
this.emitError(new Error("JSON-RPC message is missing Content-Length header"));
|
|
256
794
|
return;
|
|
257
795
|
}
|
|
258
|
-
const bodyStart = headerEnd + Buffer.byteLength(
|
|
796
|
+
const bodyStart = headerEnd + Buffer.byteLength(HEADER_SEPARATOR2);
|
|
259
797
|
const bodyEnd = bodyStart + contentLength;
|
|
260
798
|
if (this.inputBuffer.length < bodyEnd)
|
|
261
799
|
return;
|
|
@@ -299,6 +837,7 @@ class JsonRpcConnection {
|
|
|
299
837
|
if (!pending)
|
|
300
838
|
return;
|
|
301
839
|
this.pendingRequests.delete(String(id));
|
|
840
|
+
pending.cleanup();
|
|
302
841
|
if ("error" in message) {
|
|
303
842
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
304
843
|
return;
|
|
@@ -312,7 +851,11 @@ class JsonRpcConnection {
|
|
|
312
851
|
try {
|
|
313
852
|
handler(params);
|
|
314
853
|
} catch (error) {
|
|
315
|
-
|
|
854
|
+
if (error instanceof Error) {
|
|
855
|
+
this.emitError(error);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
this.emitError(new Error(String(error)));
|
|
316
859
|
}
|
|
317
860
|
}
|
|
318
861
|
handleRequest(message) {
|
|
@@ -337,13 +880,13 @@ class JsonRpcConnection {
|
|
|
337
880
|
const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
338
881
|
\r
|
|
339
882
|
${body}`;
|
|
340
|
-
return new Promise((
|
|
883
|
+
return new Promise((resolve22, reject) => {
|
|
341
884
|
this.writer.write(payload, (error) => {
|
|
342
885
|
if (error) {
|
|
343
886
|
reject(error);
|
|
344
887
|
return;
|
|
345
888
|
}
|
|
346
|
-
|
|
889
|
+
resolve22();
|
|
347
890
|
});
|
|
348
891
|
});
|
|
349
892
|
}
|
|
@@ -353,7 +896,15 @@ ${body}`;
|
|
|
353
896
|
}
|
|
354
897
|
}
|
|
355
898
|
}
|
|
356
|
-
function
|
|
899
|
+
function abortError(signal) {
|
|
900
|
+
const reason = signal?.reason;
|
|
901
|
+
if (reason instanceof Error)
|
|
902
|
+
return reason;
|
|
903
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
904
|
+
error.name = "AbortError";
|
|
905
|
+
return error;
|
|
906
|
+
}
|
|
907
|
+
function parseContentLength2(headers) {
|
|
357
908
|
for (const line of headers.split(`\r
|
|
358
909
|
`)) {
|
|
359
910
|
const separatorIndex = line.indexOf(":");
|
|
@@ -401,10 +952,10 @@ function reportKillError(context, error) {
|
|
|
401
952
|
}
|
|
402
953
|
function validateCwd(cwd) {
|
|
403
954
|
try {
|
|
404
|
-
if (!
|
|
955
|
+
if (!existsSync2(cwd)) {
|
|
405
956
|
return { valid: false, error: `Working directory does not exist: ${cwd}` };
|
|
406
957
|
}
|
|
407
|
-
const stats =
|
|
958
|
+
const stats = statSync3(cwd);
|
|
408
959
|
if (!stats.isDirectory()) {
|
|
409
960
|
return { valid: false, error: `Path is not a directory: ${cwd}` };
|
|
410
961
|
}
|
|
@@ -417,9 +968,9 @@ function validateCwd(cwd) {
|
|
|
417
968
|
}
|
|
418
969
|
}
|
|
419
970
|
function wrap(proc) {
|
|
420
|
-
const exitedPromise = new Promise((
|
|
421
|
-
proc.once("close", (code) =>
|
|
422
|
-
proc.once("error", () =>
|
|
971
|
+
const exitedPromise = new Promise((resolve22) => {
|
|
972
|
+
proc.once("close", (code) => resolve22(code ?? 0));
|
|
973
|
+
proc.once("error", () => resolve22(1));
|
|
423
974
|
});
|
|
424
975
|
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
425
976
|
throw new LspProcessSpawnError("Spawned process is missing one of stdin/stdout/stderr pipes");
|
|
@@ -473,7 +1024,7 @@ function isWindowsShellShim(command) {
|
|
|
473
1024
|
return lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat");
|
|
474
1025
|
}
|
|
475
1026
|
function splitPath(pathValue, platform) {
|
|
476
|
-
const separator = platform === "win32" ? ";" :
|
|
1027
|
+
const separator = platform === "win32" ? ";" : delimiter2;
|
|
477
1028
|
return pathValue.split(separator).filter(Boolean);
|
|
478
1029
|
}
|
|
479
1030
|
function getWindowsPathExtensions(env) {
|
|
@@ -488,8 +1039,8 @@ function resolveWindowsCommand(command, env) {
|
|
|
488
1039
|
const extensions = getWindowsPathExtensions(env);
|
|
489
1040
|
for (const baseDirectory of baseDirectories) {
|
|
490
1041
|
for (const extension of extensions) {
|
|
491
|
-
const candidate = baseDirectory ?
|
|
492
|
-
if (
|
|
1042
|
+
const candidate = baseDirectory ? join2(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
|
|
1043
|
+
if (existsSync2(candidate))
|
|
493
1044
|
return candidate;
|
|
494
1045
|
}
|
|
495
1046
|
}
|
|
@@ -523,7 +1074,7 @@ function spawnProcess(command, options) {
|
|
|
523
1074
|
throw new LspProcessSpawnError("[lsp] empty command");
|
|
524
1075
|
}
|
|
525
1076
|
const preparedCommand = createSpawnCommand(command, process.platform, process.env["ComSpec"] ?? "cmd.exe", options.env);
|
|
526
|
-
const proc =
|
|
1077
|
+
const proc = spawn2(preparedCommand.command, preparedCommand.args, {
|
|
527
1078
|
cwd: options.cwd,
|
|
528
1079
|
env: options.env,
|
|
529
1080
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -533,15 +1084,15 @@ function spawnProcess(command, options) {
|
|
|
533
1084
|
});
|
|
534
1085
|
return wrap(proc);
|
|
535
1086
|
}
|
|
536
|
-
function
|
|
1087
|
+
function isRecord2(value) {
|
|
537
1088
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
538
1089
|
}
|
|
539
1090
|
function parseConfigurationItems(params) {
|
|
540
|
-
if (!
|
|
1091
|
+
if (!isRecord2(params) || !Array.isArray(params["items"]))
|
|
541
1092
|
return [];
|
|
542
1093
|
const items = [];
|
|
543
1094
|
for (const item of params["items"]) {
|
|
544
|
-
if (!
|
|
1095
|
+
if (!isRecord2(item))
|
|
545
1096
|
continue;
|
|
546
1097
|
const section = item["section"];
|
|
547
1098
|
items.push(section === undefined || typeof section !== "string" ? {} : { section });
|
|
@@ -549,10 +1100,34 @@ function parseConfigurationItems(params) {
|
|
|
549
1100
|
return items;
|
|
550
1101
|
}
|
|
551
1102
|
function parseDiagnosticsParams(params) {
|
|
552
|
-
if (!
|
|
1103
|
+
if (!isRecord2(params) || typeof params["uri"] !== "string")
|
|
553
1104
|
return null;
|
|
554
1105
|
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
555
|
-
|
|
1106
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
1107
|
+
return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
|
|
1108
|
+
}
|
|
1109
|
+
function createLspSpawnEnv(_root, input) {
|
|
1110
|
+
return { ...input };
|
|
1111
|
+
}
|
|
1112
|
+
function isDiagnostic(value) {
|
|
1113
|
+
return isRecord2(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
1114
|
+
}
|
|
1115
|
+
function isRange(value) {
|
|
1116
|
+
return isRecord2(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
1117
|
+
}
|
|
1118
|
+
function isPosition(value) {
|
|
1119
|
+
return isRecord2(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
class LspClientNotStartedError extends Error {
|
|
1123
|
+
serverId;
|
|
1124
|
+
root;
|
|
1125
|
+
name = "LspClientNotStartedError";
|
|
1126
|
+
constructor(serverId, root) {
|
|
1127
|
+
super("LSP client not started");
|
|
1128
|
+
this.serverId = serverId;
|
|
1129
|
+
this.root = root;
|
|
1130
|
+
}
|
|
556
1131
|
}
|
|
557
1132
|
|
|
558
1133
|
class LspClientTransport {
|
|
@@ -565,9 +1140,11 @@ class LspClientTransport {
|
|
|
565
1140
|
diagnosticsStore = new Map;
|
|
566
1141
|
requestTimeoutMs;
|
|
567
1142
|
initializeTimeoutMs;
|
|
568
|
-
|
|
1143
|
+
workspaceApplyEditHandler = null;
|
|
1144
|
+
diagnosticPullSupported = false;
|
|
1145
|
+
constructor(root, server2, timeouts = {}) {
|
|
569
1146
|
this.root = root;
|
|
570
|
-
this.server =
|
|
1147
|
+
this.server = server2;
|
|
571
1148
|
this.requestTimeoutMs = timeouts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
572
1149
|
this.initializeTimeoutMs = timeouts.initializeTimeoutMs ?? INIT_TIMEOUT_MS;
|
|
573
1150
|
}
|
|
@@ -577,6 +1154,21 @@ class LspClientTransport {
|
|
|
577
1154
|
command() {
|
|
578
1155
|
return [...this.server.command];
|
|
579
1156
|
}
|
|
1157
|
+
setWorkspaceApplyEditHandler(handler) {
|
|
1158
|
+
this.workspaceApplyEditHandler = handler;
|
|
1159
|
+
}
|
|
1160
|
+
hasWorkspaceApplyEditHandler() {
|
|
1161
|
+
return this.workspaceApplyEditHandler !== null;
|
|
1162
|
+
}
|
|
1163
|
+
setDiagnosticPullSupported(supported) {
|
|
1164
|
+
this.diagnosticPullSupported = supported;
|
|
1165
|
+
}
|
|
1166
|
+
isDiagnosticPullSupported() {
|
|
1167
|
+
return this.diagnosticPullSupported;
|
|
1168
|
+
}
|
|
1169
|
+
handlePublishDiagnostics(params) {
|
|
1170
|
+
this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
|
|
1171
|
+
}
|
|
580
1172
|
async start() {
|
|
581
1173
|
const env = createLspSpawnEnv(this.root, {
|
|
582
1174
|
...process.env,
|
|
@@ -587,7 +1179,6 @@ class LspClientTransport {
|
|
|
587
1179
|
env
|
|
588
1180
|
});
|
|
589
1181
|
this.startStderrReading();
|
|
590
|
-
await new Promise((resolve6) => setTimeout(resolve6, 100));
|
|
591
1182
|
if (this.proc.exitCode !== null) {
|
|
592
1183
|
const stderr = this.stderrBuffer.join(`
|
|
593
1184
|
`);
|
|
@@ -597,7 +1188,7 @@ class LspClientTransport {
|
|
|
597
1188
|
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
598
1189
|
const diagnosticsParams = parseDiagnosticsParams(params);
|
|
599
1190
|
if (diagnosticsParams?.uri) {
|
|
600
|
-
this.
|
|
1191
|
+
this.handlePublishDiagnostics(diagnosticsParams);
|
|
601
1192
|
}
|
|
602
1193
|
});
|
|
603
1194
|
this.connection.onRequest("workspace/configuration", (params) => {
|
|
@@ -610,6 +1201,9 @@ class LspClientTransport {
|
|
|
610
1201
|
});
|
|
611
1202
|
this.connection.onRequest("client/registerCapability", () => null);
|
|
612
1203
|
this.connection.onRequest("window/workDoneProgress/create", () => null);
|
|
1204
|
+
if (this.workspaceApplyEditHandler) {
|
|
1205
|
+
this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
|
|
1206
|
+
}
|
|
613
1207
|
this.connection.onClose(() => {
|
|
614
1208
|
this.processExited = true;
|
|
615
1209
|
});
|
|
@@ -638,30 +1232,25 @@ class LspClientTransport {
|
|
|
638
1232
|
}
|
|
639
1233
|
async sendRequest(method, ...args) {
|
|
640
1234
|
if (!this.connection)
|
|
641
|
-
throw new
|
|
1235
|
+
throw new LspClientNotStartedError(this.server.id, this.root);
|
|
642
1236
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
643
1237
|
const stderrTail = this.stderrBuffer.slice(-10).join(`
|
|
644
1238
|
`);
|
|
645
1239
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
|
|
646
1240
|
}
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
1241
|
+
const options = args[1];
|
|
1242
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
1243
|
+
const timeoutController = new AbortController;
|
|
1244
|
+
const timeoutHandle = setTimeout(() => {
|
|
1245
|
+
const stderrTail = this.stderrBuffer.slice(-5).join(`
|
|
652
1246
|
`);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1247
|
+
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
1248
|
+
}, timeoutMs);
|
|
1249
|
+
const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
|
|
656
1250
|
try {
|
|
657
|
-
const
|
|
658
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
659
|
-
if (timeoutHandle !== null)
|
|
660
|
-
clearTimeout(timeoutHandle);
|
|
1251
|
+
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
1252
|
return result;
|
|
662
1253
|
} catch (error) {
|
|
663
|
-
if (timeoutHandle !== null)
|
|
664
|
-
clearTimeout(timeoutHandle);
|
|
665
1254
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
666
1255
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
|
|
667
1256
|
`) || undefined);
|
|
@@ -670,6 +1259,9 @@ class LspClientTransport {
|
|
|
670
1259
|
throw new LspConnectionClosedError(this.server.id, this.root, error.message);
|
|
671
1260
|
}
|
|
672
1261
|
throw error;
|
|
1262
|
+
} finally {
|
|
1263
|
+
clearTimeout(timeoutHandle);
|
|
1264
|
+
combinedSignal.dispose();
|
|
673
1265
|
}
|
|
674
1266
|
}
|
|
675
1267
|
async sendNotification(method, ...args) {
|
|
@@ -698,17 +1290,17 @@ class LspClientTransport {
|
|
|
698
1290
|
try {
|
|
699
1291
|
await this.sendRequest("shutdown");
|
|
700
1292
|
} catch (error) {
|
|
701
|
-
reportBestEffortCleanupError("shutdown request", error);
|
|
1293
|
+
reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
|
|
702
1294
|
}
|
|
703
1295
|
try {
|
|
704
1296
|
await this.sendNotification("exit");
|
|
705
1297
|
} catch (error) {
|
|
706
|
-
reportBestEffortCleanupError("exit notification", error);
|
|
1298
|
+
reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
|
|
707
1299
|
}
|
|
708
1300
|
try {
|
|
709
1301
|
this.connection.dispose();
|
|
710
1302
|
} catch (error) {
|
|
711
|
-
reportBestEffortCleanupError("connection dispose", error);
|
|
1303
|
+
reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
|
|
712
1304
|
}
|
|
713
1305
|
this.connection = null;
|
|
714
1306
|
}
|
|
@@ -719,8 +1311,8 @@ class LspClientTransport {
|
|
|
719
1311
|
try {
|
|
720
1312
|
proc.kill();
|
|
721
1313
|
let timeoutId;
|
|
722
|
-
const timeoutPromise = new Promise((
|
|
723
|
-
timeoutId = setTimeout(
|
|
1314
|
+
const timeoutPromise = new Promise((resolve22) => {
|
|
1315
|
+
timeoutId = setTimeout(resolve22, STOP_HARD_KILL_TIMEOUT_MS);
|
|
724
1316
|
});
|
|
725
1317
|
await Promise.race([
|
|
726
1318
|
proc.exited.then(() => {
|
|
@@ -736,14 +1328,14 @@ class LspClientTransport {
|
|
|
736
1328
|
proc.kill("SIGKILL");
|
|
737
1329
|
await Promise.race([
|
|
738
1330
|
proc.exited,
|
|
739
|
-
new Promise((
|
|
1331
|
+
new Promise((resolve22) => setTimeout(resolve22, STOP_SIGKILL_GRACE_MS))
|
|
740
1332
|
]);
|
|
741
1333
|
} catch (error) {
|
|
742
|
-
reportBestEffortCleanupError("hard process kill", error);
|
|
1334
|
+
reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
|
|
743
1335
|
}
|
|
744
1336
|
}
|
|
745
1337
|
} catch (error) {
|
|
746
|
-
reportBestEffortCleanupError("process stop", error);
|
|
1338
|
+
reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
|
|
747
1339
|
}
|
|
748
1340
|
}
|
|
749
1341
|
this.processExited = true;
|
|
@@ -753,24 +1345,43 @@ class LspClientTransport {
|
|
|
753
1345
|
return this.diagnosticsStore.get(uri) ?? [];
|
|
754
1346
|
}
|
|
755
1347
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
|
|
1348
|
+
function combineAbortSignals(primary, secondary) {
|
|
1349
|
+
const controller = new AbortController;
|
|
1350
|
+
const abortFrom = (signal) => {
|
|
1351
|
+
if (!controller.signal.aborted)
|
|
1352
|
+
controller.abort(signal.reason);
|
|
1353
|
+
};
|
|
1354
|
+
const onPrimaryAbort = () => {
|
|
1355
|
+
if (primary)
|
|
1356
|
+
abortFrom(primary);
|
|
1357
|
+
};
|
|
1358
|
+
const onSecondaryAbort = () => abortFrom(secondary);
|
|
1359
|
+
if (primary?.aborted)
|
|
1360
|
+
abortFrom(primary);
|
|
1361
|
+
else
|
|
1362
|
+
primary?.addEventListener("abort", onPrimaryAbort, { once: true });
|
|
1363
|
+
if (secondary.aborted)
|
|
1364
|
+
abortFrom(secondary);
|
|
1365
|
+
else
|
|
1366
|
+
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
|
|
1367
|
+
return {
|
|
1368
|
+
signal: controller.signal,
|
|
1369
|
+
dispose: () => {
|
|
1370
|
+
primary?.removeEventListener("abort", onPrimaryAbort);
|
|
1371
|
+
secondary.removeEventListener("abort", onSecondaryAbort);
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
764
1374
|
}
|
|
765
|
-
function
|
|
766
|
-
|
|
1375
|
+
function supportsDiagnosticPull(capabilities) {
|
|
1376
|
+
if (capabilities === undefined)
|
|
1377
|
+
return false;
|
|
1378
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
767
1379
|
}
|
|
768
|
-
var INITIALIZE_SETTLE_MS = 300;
|
|
769
1380
|
|
|
770
1381
|
class LspClientConnection extends LspClientTransport {
|
|
771
1382
|
async initialize() {
|
|
772
1383
|
const rootUri = pathToFileURL(this.root).href;
|
|
773
|
-
await this.sendRequest("initialize", {
|
|
1384
|
+
const result = await this.sendRequest("initialize", {
|
|
774
1385
|
processId: process.pid,
|
|
775
1386
|
rootUri,
|
|
776
1387
|
rootPath: this.root,
|
|
@@ -784,8 +1395,7 @@ class LspClientConnection extends LspClientTransport {
|
|
|
784
1395
|
publishDiagnostics: {},
|
|
785
1396
|
rename: {
|
|
786
1397
|
prepareSupport: true,
|
|
787
|
-
prepareSupportDefaultBehavior: 1
|
|
788
|
-
honorsChangeAnnotations: true
|
|
1398
|
+
prepareSupportDefaultBehavior: 1
|
|
789
1399
|
},
|
|
790
1400
|
codeAction: {
|
|
791
1401
|
codeActionLiteralSupport: {
|
|
@@ -814,28 +1424,22 @@ class LspClientConnection extends LspClientTransport {
|
|
|
814
1424
|
symbol: {},
|
|
815
1425
|
workspaceFolders: true,
|
|
816
1426
|
configuration: true,
|
|
817
|
-
applyEdit: true,
|
|
1427
|
+
...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
|
|
818
1428
|
workspaceEdit: {
|
|
819
|
-
documentChanges: true
|
|
1429
|
+
documentChanges: true,
|
|
1430
|
+
resourceOperations: ["create", "rename", "delete"]
|
|
820
1431
|
}
|
|
821
1432
|
}
|
|
822
1433
|
},
|
|
823
1434
|
initializationOptions: this.server.initialization
|
|
824
1435
|
}, { timeoutMs: this.initializeTimeoutMs });
|
|
1436
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
825
1437
|
await this.sendNotification("initialized");
|
|
826
1438
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
827
1439
|
settings: { json: { validate: { enable: true } } }
|
|
828
1440
|
});
|
|
829
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
830
1441
|
}
|
|
831
1442
|
}
|
|
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
1443
|
var SYMBOL_KIND_MAP = {
|
|
840
1444
|
1: "File",
|
|
841
1445
|
2: "Module",
|
|
@@ -1006,81 +1610,1384 @@ var EXT_TO_LANG = {
|
|
|
1006
1610
|
function getLanguageId(ext) {
|
|
1007
1611
|
return EXT_TO_LANG[ext] ?? "plaintext";
|
|
1008
1612
|
}
|
|
1009
|
-
var
|
|
1010
|
-
var
|
|
1613
|
+
var WATCHED_FILE_BATCH_SIZE = 128;
|
|
1614
|
+
var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1615
|
+
function canonicalPath(filePath) {
|
|
1616
|
+
const absolute = resolve2(filePath);
|
|
1617
|
+
try {
|
|
1618
|
+
return realpathSync2(absolute);
|
|
1619
|
+
} catch {
|
|
1620
|
+
return absolute;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function isSameOrDescendant(candidate, parent) {
|
|
1624
|
+
const suffix = relative2(parent, candidate);
|
|
1625
|
+
return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
|
|
1626
|
+
}
|
|
1627
|
+
function movedPath(candidate, oldPath, newPath) {
|
|
1628
|
+
const suffix = relative2(oldPath, candidate);
|
|
1629
|
+
return suffix === "" ? newPath : resolve2(newPath, suffix);
|
|
1630
|
+
}
|
|
1011
1631
|
|
|
1012
|
-
class
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1632
|
+
class WorkspaceDocumentState {
|
|
1633
|
+
sendNotification;
|
|
1634
|
+
clearDiagnostics;
|
|
1635
|
+
openDocuments = new Map;
|
|
1636
|
+
openByUri = new Map;
|
|
1637
|
+
openPromises = new Map;
|
|
1638
|
+
now;
|
|
1639
|
+
versionlessPublishQuiescenceMs;
|
|
1640
|
+
constructor(sendNotification, clearDiagnostics, options = {}) {
|
|
1641
|
+
this.sendNotification = sendNotification;
|
|
1642
|
+
this.clearDiagnostics = clearDiagnostics;
|
|
1643
|
+
this.now = options.now ?? (() => Date.now());
|
|
1644
|
+
this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
1019
1645
|
}
|
|
1020
1646
|
async openFile(filePath) {
|
|
1021
|
-
const
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1647
|
+
const path2 = canonicalPath(filePath);
|
|
1648
|
+
const existingOpen = this.openPromises.get(path2);
|
|
1649
|
+
if (existingOpen) {
|
|
1650
|
+
await existingOpen;
|
|
1651
|
+
return this.openFile(path2);
|
|
1652
|
+
}
|
|
1653
|
+
const text = readFileSync2(path2, "utf-8");
|
|
1654
|
+
const existing = this.openDocuments.get(path2);
|
|
1655
|
+
if (!existing)
|
|
1656
|
+
return this.openDocumentSingleFlight(path2, text);
|
|
1657
|
+
if (existing.text === text)
|
|
1658
|
+
return;
|
|
1659
|
+
await this.changeDocument(existing, text);
|
|
1660
|
+
}
|
|
1661
|
+
getVersion(filePath) {
|
|
1662
|
+
return this.openDocuments.get(canonicalPath(filePath))?.version;
|
|
1663
|
+
}
|
|
1664
|
+
getStoredDiagnostics(uri) {
|
|
1665
|
+
const state = this.openByUri.get(uri);
|
|
1666
|
+
if (!state)
|
|
1667
|
+
return [];
|
|
1668
|
+
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
1669
|
+
}
|
|
1670
|
+
captureDiagnosticSnapshot(filePath) {
|
|
1671
|
+
const state = this.openDocuments.get(canonicalPath(filePath));
|
|
1672
|
+
if (!state)
|
|
1673
|
+
return null;
|
|
1674
|
+
return {
|
|
1675
|
+
path: state.path,
|
|
1676
|
+
uri: state.uri,
|
|
1677
|
+
version: state.version,
|
|
1678
|
+
documentGeneration: state.generation,
|
|
1679
|
+
publishGeneration: state.publishGeneration
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
isCurrentSnapshot(snapshot) {
|
|
1683
|
+
const state = this.openDocuments.get(snapshot.path);
|
|
1684
|
+
return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
|
|
1685
|
+
}
|
|
1686
|
+
getPullCache(snapshot) {
|
|
1687
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1688
|
+
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
|
|
1689
|
+
return null;
|
|
1690
|
+
return state.pullCache;
|
|
1691
|
+
}
|
|
1692
|
+
recordPullDiagnostics(snapshot, report) {
|
|
1693
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1694
|
+
if (!state)
|
|
1695
|
+
return;
|
|
1696
|
+
state.pullCache = {
|
|
1697
|
+
documentVersion: snapshot.version,
|
|
1698
|
+
diagnostics: [...report.diagnostics],
|
|
1699
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
recordPublishedDiagnostics(params) {
|
|
1703
|
+
const state = this.openByUri.get(params.uri);
|
|
1704
|
+
if (!state)
|
|
1705
|
+
return;
|
|
1706
|
+
state.publishGeneration += 1;
|
|
1707
|
+
state.lastPublish = {
|
|
1708
|
+
diagnostics: [...params.diagnostics],
|
|
1709
|
+
publishGeneration: state.publishGeneration,
|
|
1710
|
+
documentGenerationAtArrival: state.generation,
|
|
1711
|
+
arrivedAt: this.now(),
|
|
1712
|
+
...params.version === undefined ? {} : { version: params.version }
|
|
1713
|
+
};
|
|
1714
|
+
this.notifyWaiters(state);
|
|
1715
|
+
}
|
|
1716
|
+
resolvePushDiagnostics(snapshot) {
|
|
1717
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1718
|
+
if (!state?.lastPublish)
|
|
1719
|
+
return { status: "missing" };
|
|
1720
|
+
const publish = state.lastPublish;
|
|
1721
|
+
if (publish.version !== undefined) {
|
|
1722
|
+
return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
|
|
1723
|
+
}
|
|
1724
|
+
if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
|
|
1725
|
+
return { status: "missing" };
|
|
1726
|
+
const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
|
|
1727
|
+
const waitMs = Math.max(0, readyAt - this.now());
|
|
1728
|
+
return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
|
|
1729
|
+
}
|
|
1730
|
+
waitForDiagnosticsActivity(snapshot, timeoutMs) {
|
|
1731
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1732
|
+
if (!state || timeoutMs <= 0)
|
|
1733
|
+
return Promise.resolve();
|
|
1734
|
+
return new Promise((resolveActivity) => {
|
|
1735
|
+
let settled = false;
|
|
1736
|
+
const finish = () => {
|
|
1737
|
+
if (settled)
|
|
1738
|
+
return;
|
|
1739
|
+
settled = true;
|
|
1740
|
+
clearTimeout(timer);
|
|
1741
|
+
state.waiters.delete(finish);
|
|
1742
|
+
resolveActivity();
|
|
1743
|
+
};
|
|
1744
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1745
|
+
if (typeof timer.unref === "function")
|
|
1746
|
+
timer.unref();
|
|
1747
|
+
state.waiters.add(finish);
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
validateVersions(operations) {
|
|
1751
|
+
const versions = new Map([...this.openDocuments].map(([path2, state]) => [path2, state.version]));
|
|
1752
|
+
for (const operation of operations) {
|
|
1753
|
+
if (operation.kind === "text") {
|
|
1754
|
+
const current = versions.get(operation.path);
|
|
1755
|
+
if (operation.documentVersion !== null && current !== operation.documentVersion) {
|
|
1756
|
+
const observed = current === undefined ? "closed document" : `open document version ${current}`;
|
|
1757
|
+
return {
|
|
1758
|
+
changeIndex: operation.changeIndex,
|
|
1759
|
+
message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
if (current !== undefined)
|
|
1763
|
+
versions.set(operation.path, current + 1);
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
if (operation.kind === "rename") {
|
|
1767
|
+
const moved = [...versions].filter(([path2]) => isSameOrDescendant(path2, operation.oldPath));
|
|
1768
|
+
for (const [path2] of moved)
|
|
1769
|
+
versions.delete(path2);
|
|
1770
|
+
for (const [path2] of moved)
|
|
1771
|
+
versions.set(movedPath(path2, operation.oldPath, operation.newPath), 1);
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if (operation.kind === "delete") {
|
|
1775
|
+
for (const path2 of [...versions.keys()]) {
|
|
1776
|
+
if (isSameOrDescendant(path2, operation.path))
|
|
1777
|
+
versions.delete(path2);
|
|
1034
1778
|
}
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
|
|
1782
|
+
versions.set(operation.path, 1);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
async synchronize(delta) {
|
|
1788
|
+
const watched = [];
|
|
1789
|
+
for (const mutation of delta.operations)
|
|
1790
|
+
await this.synchronizeMutation(mutation, watched);
|
|
1791
|
+
for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
|
|
1792
|
+
await this.sendNotification("workspace/didChangeWatchedFiles", {
|
|
1793
|
+
changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
|
|
1035
1794
|
});
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
async synchronizeMutation(mutation, watched) {
|
|
1798
|
+
if (mutation.kind === "text") {
|
|
1799
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1800
|
+
if (state)
|
|
1801
|
+
await this.changeDocument(state, mutation.afterText);
|
|
1802
|
+
else
|
|
1803
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
|
|
1040
1804
|
return;
|
|
1041
1805
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1806
|
+
if (mutation.kind === "create") {
|
|
1807
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1808
|
+
if (state) {
|
|
1809
|
+
await this.closeDocument(state);
|
|
1810
|
+
await this.openDocumentSingleFlight(mutation.path, readFileSync2(mutation.path, "utf-8"));
|
|
1811
|
+
} else {
|
|
1812
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
|
|
1813
|
+
}
|
|
1044
1814
|
return;
|
|
1045
1815
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1816
|
+
if (mutation.kind === "rename") {
|
|
1817
|
+
const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
|
|
1818
|
+
for (const state of moved)
|
|
1819
|
+
await this.closeDocument(state);
|
|
1820
|
+
for (const state of moved) {
|
|
1821
|
+
const path2 = movedPath(state.path, mutation.oldPath, mutation.newPath);
|
|
1822
|
+
await this.openDocumentSingleFlight(path2, readFileSync2(path2, "utf-8"));
|
|
1823
|
+
}
|
|
1824
|
+
if (moved.length === 0) {
|
|
1825
|
+
watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
|
|
1826
|
+
watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
|
|
1827
|
+
}
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
|
|
1831
|
+
for (const state of removed)
|
|
1832
|
+
await this.closeDocument(state);
|
|
1833
|
+
if (removed.length === 0)
|
|
1834
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
|
|
1835
|
+
}
|
|
1836
|
+
async openDocumentSingleFlight(path2, text) {
|
|
1837
|
+
const existing = this.openPromises.get(path2);
|
|
1838
|
+
if (existing)
|
|
1839
|
+
return existing;
|
|
1840
|
+
const open = (async () => {
|
|
1841
|
+
const state = {
|
|
1842
|
+
path: path2,
|
|
1843
|
+
uri: pathToFileURL2(path2).href,
|
|
1844
|
+
languageId: getLanguageId(effectiveExtension(path2)),
|
|
1845
|
+
text,
|
|
1846
|
+
version: 1,
|
|
1847
|
+
generation: 1,
|
|
1848
|
+
publishGeneration: 0,
|
|
1849
|
+
waiters: new Set
|
|
1850
|
+
};
|
|
1851
|
+
this.openDocuments.set(path2, state);
|
|
1852
|
+
this.openByUri.set(state.uri, state);
|
|
1853
|
+
this.notifyWaiters(state);
|
|
1854
|
+
await this.sendNotification("textDocument/didOpen", {
|
|
1855
|
+
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
|
|
1856
|
+
});
|
|
1857
|
+
})().finally(() => {
|
|
1858
|
+
this.openPromises.delete(path2);
|
|
1859
|
+
});
|
|
1860
|
+
this.openPromises.set(path2, open);
|
|
1861
|
+
return open;
|
|
1862
|
+
}
|
|
1863
|
+
async changeDocument(state, text) {
|
|
1864
|
+
state.text = text;
|
|
1865
|
+
state.version += 1;
|
|
1866
|
+
state.generation += 1;
|
|
1867
|
+
this.clearDiagnostics(state.uri);
|
|
1868
|
+
this.notifyWaiters(state);
|
|
1049
1869
|
await this.sendNotification("textDocument/didChange", {
|
|
1050
|
-
textDocument: { uri, version:
|
|
1870
|
+
textDocument: { uri: state.uri, version: state.version },
|
|
1051
1871
|
contentChanges: [{ text }]
|
|
1052
1872
|
});
|
|
1053
|
-
await this.sendNotification("textDocument/didSave", {
|
|
1054
|
-
|
|
1055
|
-
|
|
1873
|
+
await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
|
|
1874
|
+
}
|
|
1875
|
+
async closeDocument(state) {
|
|
1876
|
+
this.openDocuments.delete(state.path);
|
|
1877
|
+
this.openByUri.delete(state.uri);
|
|
1878
|
+
this.clearDiagnostics(state.uri);
|
|
1879
|
+
this.notifyWaiters(state);
|
|
1880
|
+
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
1881
|
+
}
|
|
1882
|
+
notifyWaiters(state) {
|
|
1883
|
+
for (const waiter of [...state.waiters])
|
|
1884
|
+
waiter();
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
var CONCURRENT_FAILURE_REASON_BY_PHASE = {
|
|
1888
|
+
applying: "workspace/applyEdit is already in progress for this workspace mutation",
|
|
1889
|
+
settled: "workspace/applyEdit was already handled for this workspace mutation"
|
|
1890
|
+
};
|
|
1891
|
+
function workspaceApplyEditConcurrentFailureReason(phase) {
|
|
1892
|
+
return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
class WorkspaceEditPathError extends Error {
|
|
1896
|
+
path;
|
|
1897
|
+
detail;
|
|
1898
|
+
name = "WorkspaceEditPathError";
|
|
1899
|
+
constructor(path2, detail) {
|
|
1900
|
+
super(`${detail}: ${path2}`);
|
|
1901
|
+
this.path = path2;
|
|
1902
|
+
this.detail = detail;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
1906
|
+
const relativePath = relative3(workspaceRoot, filePath);
|
|
1907
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
|
|
1908
|
+
}
|
|
1909
|
+
function canonicalizeMissingPath(filePath) {
|
|
1910
|
+
let ancestor = filePath;
|
|
1911
|
+
while (!existsSync3(ancestor)) {
|
|
1912
|
+
const parent = dirname4(ancestor);
|
|
1913
|
+
if (parent === ancestor)
|
|
1914
|
+
throw new WorkspaceEditPathError(filePath, "no existing ancestor");
|
|
1915
|
+
ancestor = parent;
|
|
1916
|
+
}
|
|
1917
|
+
return resolve3(realpathSync3(ancestor), relative3(ancestor, filePath));
|
|
1918
|
+
}
|
|
1919
|
+
function canonicalWorkspaceRoot(workspaceRoot) {
|
|
1920
|
+
try {
|
|
1921
|
+
const canonical = realpathSync3(resolve3(workspaceRoot));
|
|
1922
|
+
if (!lstatSync2(canonical).isDirectory()) {
|
|
1923
|
+
return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
|
|
1924
|
+
}
|
|
1925
|
+
return {
|
|
1926
|
+
success: true,
|
|
1927
|
+
path: canonical,
|
|
1928
|
+
requestedPath: resolve3(workspaceRoot),
|
|
1929
|
+
followedSymbolicLink: existsSync3(resolve3(workspaceRoot)) && lstatSync2(resolve3(workspaceRoot)).isSymbolicLink()
|
|
1930
|
+
};
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1933
|
+
return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
|
|
1937
|
+
let requestedPath;
|
|
1938
|
+
try {
|
|
1939
|
+
const parsed = new URL(uri);
|
|
1940
|
+
if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
|
|
1941
|
+
return { success: false, error: `non-file URI ${uri}` };
|
|
1942
|
+
}
|
|
1943
|
+
requestedPath = resolve3(fileURLToPath2(parsed));
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1946
|
+
return { success: false, error: `non-file URI ${uri}: ${detail}` };
|
|
1947
|
+
}
|
|
1948
|
+
try {
|
|
1949
|
+
const canonical = existsSync3(requestedPath) ? realpathSync3(requestedPath) : canonicalizeMissingPath(requestedPath);
|
|
1950
|
+
if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
|
|
1951
|
+
return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
|
|
1952
|
+
}
|
|
1953
|
+
return {
|
|
1954
|
+
success: true,
|
|
1955
|
+
path: canonical,
|
|
1956
|
+
requestedPath,
|
|
1957
|
+
followedSymbolicLink: existsSync3(requestedPath) && lstatSync2(requestedPath).isSymbolicLink()
|
|
1958
|
+
};
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1961
|
+
return { success: false, error: `${requestedPath}: ${detail}` };
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function snapshotPath(path2, includeChildren) {
|
|
1965
|
+
if (!existsSync3(path2))
|
|
1966
|
+
return { kind: "missing" };
|
|
1967
|
+
const stats = lstatSync2(path2);
|
|
1968
|
+
if (stats.isFile())
|
|
1969
|
+
return { kind: "file", content: readFileSync3(path2, "utf-8") };
|
|
1970
|
+
if (stats.isDirectory()) {
|
|
1971
|
+
return includeChildren ? { kind: "directory", children: readdirSync(path2).sort() } : { kind: "directory" };
|
|
1972
|
+
}
|
|
1973
|
+
throw new WorkspaceEditPathError(path2, "unsupported filesystem entry");
|
|
1974
|
+
}
|
|
1975
|
+
var DEFAULT_IO = {
|
|
1976
|
+
writeFile(path2, content) {
|
|
1977
|
+
writeFileSync(path2, content, "utf-8");
|
|
1978
|
+
},
|
|
1979
|
+
rename(oldPath, newPath) {
|
|
1980
|
+
renameSync(oldPath, newPath);
|
|
1981
|
+
},
|
|
1982
|
+
remove(path2, recursive) {
|
|
1983
|
+
rmSync(path2, { recursive, force: false });
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
function snapshotsEqual(expected, actual) {
|
|
1987
|
+
if (expected.kind !== actual.kind)
|
|
1988
|
+
return false;
|
|
1989
|
+
if (expected.kind === "file" && actual.kind === "file")
|
|
1990
|
+
return expected.content === actual.content;
|
|
1991
|
+
if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
|
|
1992
|
+
return JSON.stringify(expected.children) === JSON.stringify(actual.children);
|
|
1993
|
+
}
|
|
1994
|
+
return true;
|
|
1995
|
+
}
|
|
1996
|
+
function liveSnapshot(path2, expected) {
|
|
1997
|
+
return snapshotPath(path2, expected.kind === "directory" && expected.children !== undefined);
|
|
1998
|
+
}
|
|
1999
|
+
function firstOperationIndex(plan) {
|
|
2000
|
+
return plan.operations[0]?.changeIndex ?? 0;
|
|
2001
|
+
}
|
|
2002
|
+
function failedCommit(plan, failure) {
|
|
2003
|
+
const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
|
|
2004
|
+
return {
|
|
2005
|
+
result: {
|
|
2006
|
+
success: false,
|
|
2007
|
+
filesModified,
|
|
2008
|
+
totalEdits,
|
|
2009
|
+
errors: [`change ${changeIndex}: ${message}`],
|
|
2010
|
+
failedChange: changeIndex,
|
|
2011
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
2012
|
+
},
|
|
2013
|
+
delta: mutationDelta(mutations),
|
|
2014
|
+
fingerprint: plan.fingerprint
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
function verifySnapshots(plan) {
|
|
2018
|
+
for (const [path2, expected] of plan.snapshots) {
|
|
2019
|
+
let actual;
|
|
2020
|
+
try {
|
|
2021
|
+
actual = liveSnapshot(path2, expected);
|
|
2022
|
+
} catch (error) {
|
|
2023
|
+
const changeIndex = plan.firstChangeByPath.get(path2) ?? firstOperationIndex(plan);
|
|
2024
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2025
|
+
return failedCommit(plan, { message: `cannot verify snapshot for ${path2}: ${detail}`, changeIndex });
|
|
2026
|
+
}
|
|
2027
|
+
if (!snapshotsEqual(expected, actual)) {
|
|
2028
|
+
const changeIndex = plan.firstChangeByPath.get(path2) ?? firstOperationIndex(plan);
|
|
2029
|
+
return failedCommit(plan, { message: `workspace state changed before commit: ${path2}`, changeIndex });
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
function addModifiedPath(paths, path2) {
|
|
2035
|
+
if (!paths.includes(path2))
|
|
2036
|
+
paths.push(path2);
|
|
2037
|
+
}
|
|
2038
|
+
function reportedPath(plan, path2) {
|
|
2039
|
+
return plan.reportedPathByCanonical.get(path2) ?? path2;
|
|
2040
|
+
}
|
|
2041
|
+
function changedPathsForMutation(mutation) {
|
|
2042
|
+
return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
|
|
2043
|
+
}
|
|
2044
|
+
function mutationDelta(operations) {
|
|
2045
|
+
const changedPaths = new Set;
|
|
2046
|
+
for (const operation of operations) {
|
|
2047
|
+
for (const path2 of changedPathsForMutation(operation))
|
|
2048
|
+
changedPaths.add(path2);
|
|
2049
|
+
}
|
|
2050
|
+
return { operations, changedPaths: [...changedPaths].sort() };
|
|
2051
|
+
}
|
|
2052
|
+
function resolveIo(overrides) {
|
|
2053
|
+
return {
|
|
2054
|
+
writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
|
|
2055
|
+
rename: overrides?.rename ?? DEFAULT_IO.rename,
|
|
2056
|
+
remove: overrides?.remove ?? DEFAULT_IO.remove
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
function commitOperation(context, operation) {
|
|
2060
|
+
const { plan, io, accumulator } = context;
|
|
2061
|
+
if (operation.kind === "noop")
|
|
2062
|
+
return;
|
|
2063
|
+
if (operation.kind === "text") {
|
|
2064
|
+
io.writeFile(operation.path, operation.afterText);
|
|
2065
|
+
accumulator.mutations.push({
|
|
2066
|
+
kind: "text",
|
|
2067
|
+
path: operation.path,
|
|
2068
|
+
beforeText: operation.beforeText,
|
|
2069
|
+
afterText: operation.afterText
|
|
2070
|
+
});
|
|
2071
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2072
|
+
accumulator.totalEdits += operation.editCount;
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
if (operation.kind === "create") {
|
|
2076
|
+
io.writeFile(operation.path, "");
|
|
2077
|
+
accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
|
|
2078
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (operation.kind === "rename") {
|
|
2082
|
+
if (operation.replaceDestination) {
|
|
2083
|
+
const targetKind = existsSync4(operation.newPath) && lstatSync3(operation.newPath).isDirectory() ? "directory" : "file";
|
|
2084
|
+
io.remove(operation.newPath, targetKind === "directory");
|
|
2085
|
+
accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
|
|
2086
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
2087
|
+
}
|
|
2088
|
+
io.rename(operation.oldPath, operation.newPath);
|
|
2089
|
+
accumulator.mutations.push({
|
|
2090
|
+
kind: "rename",
|
|
2091
|
+
oldPath: operation.oldPath,
|
|
2092
|
+
newPath: operation.newPath,
|
|
2093
|
+
sourceKind: operation.sourceKind
|
|
2094
|
+
});
|
|
2095
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
io.remove(operation.path, operation.recursive);
|
|
2099
|
+
accumulator.mutations.push({
|
|
2100
|
+
kind: "delete",
|
|
2101
|
+
path: operation.path,
|
|
2102
|
+
targetKind: operation.targetKind
|
|
2103
|
+
});
|
|
2104
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2105
|
+
}
|
|
2106
|
+
function commitWorkspaceEditPlan(plan, options = {}) {
|
|
2107
|
+
if (options.signal?.aborted) {
|
|
2108
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2109
|
+
}
|
|
2110
|
+
const stale = verifySnapshots(plan);
|
|
2111
|
+
if (stale)
|
|
2112
|
+
return stale;
|
|
2113
|
+
if (options.signal?.aborted) {
|
|
2114
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2115
|
+
}
|
|
2116
|
+
const io = resolveIo(options.io);
|
|
2117
|
+
const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
|
|
2118
|
+
const context = { plan, io, accumulator };
|
|
2119
|
+
let lateAbort = false;
|
|
2120
|
+
for (const operation of plan.operations) {
|
|
2121
|
+
try {
|
|
2122
|
+
commitOperation(context, operation);
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2125
|
+
return failedCommit(plan, {
|
|
2126
|
+
message: `I/O failure during ${operation.kind}: ${detail}`,
|
|
2127
|
+
changeIndex: operation.changeIndex,
|
|
2128
|
+
mutations: accumulator.mutations,
|
|
2129
|
+
filesModified: accumulator.filesModified,
|
|
2130
|
+
totalEdits: accumulator.totalEdits,
|
|
2131
|
+
lateAbort: lateAbort || options.signal?.aborted === true
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
if (options.signal?.aborted)
|
|
2135
|
+
lateAbort = true;
|
|
2136
|
+
}
|
|
2137
|
+
const result = {
|
|
2138
|
+
success: true,
|
|
2139
|
+
filesModified: accumulator.filesModified,
|
|
2140
|
+
totalEdits: accumulator.totalEdits,
|
|
2141
|
+
errors: [],
|
|
2142
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
2143
|
+
};
|
|
2144
|
+
return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
|
|
2145
|
+
}
|
|
2146
|
+
function canonicalFingerprint(operations) {
|
|
2147
|
+
const canonical = operations.map((operation) => {
|
|
2148
|
+
switch (operation.kind) {
|
|
2149
|
+
case "text":
|
|
2150
|
+
return {
|
|
2151
|
+
kind: operation.kind,
|
|
2152
|
+
changeIndex: operation.changeIndex,
|
|
2153
|
+
path: operation.path,
|
|
2154
|
+
edits: operation.edits,
|
|
2155
|
+
version: operation.version
|
|
2156
|
+
};
|
|
2157
|
+
case "rename":
|
|
2158
|
+
return {
|
|
2159
|
+
kind: operation.kind,
|
|
2160
|
+
changeIndex: operation.changeIndex,
|
|
2161
|
+
oldPath: operation.oldPath,
|
|
2162
|
+
newPath: operation.newPath,
|
|
2163
|
+
overwrite: operation.overwrite,
|
|
2164
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2165
|
+
};
|
|
2166
|
+
case "create":
|
|
2167
|
+
return {
|
|
2168
|
+
kind: operation.kind,
|
|
2169
|
+
changeIndex: operation.changeIndex,
|
|
2170
|
+
path: operation.path,
|
|
2171
|
+
overwrite: operation.overwrite,
|
|
2172
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2173
|
+
};
|
|
2174
|
+
case "delete":
|
|
2175
|
+
return {
|
|
2176
|
+
kind: operation.kind,
|
|
2177
|
+
changeIndex: operation.changeIndex,
|
|
2178
|
+
path: operation.path,
|
|
2179
|
+
recursive: operation.recursive,
|
|
2180
|
+
ignoreIfNotExists: operation.ignoreIfNotExists
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
return createHash2("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
class WorkspaceEditValidationError extends Error {
|
|
2188
|
+
changeIndex;
|
|
2189
|
+
detail;
|
|
2190
|
+
name = "WorkspaceEditValidationError";
|
|
2191
|
+
constructor(changeIndex, detail) {
|
|
2192
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
2193
|
+
this.changeIndex = changeIndex;
|
|
2194
|
+
this.detail = detail;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
function isRecord3(value) {
|
|
2198
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2199
|
+
}
|
|
2200
|
+
function parsePosition(value) {
|
|
2201
|
+
if (!isRecord3(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
|
|
2202
|
+
return null;
|
|
2203
|
+
}
|
|
2204
|
+
return { line: value["line"], character: value["character"] };
|
|
2205
|
+
}
|
|
2206
|
+
function parseRange(value) {
|
|
2207
|
+
if (!isRecord3(value))
|
|
2208
|
+
return null;
|
|
2209
|
+
const start = parsePosition(value["start"]);
|
|
2210
|
+
const end = parsePosition(value["end"]);
|
|
2211
|
+
return start && end ? { start, end } : null;
|
|
2212
|
+
}
|
|
2213
|
+
function parseTextEdits(value, changeIndex) {
|
|
2214
|
+
if (!Array.isArray(value)) {
|
|
2215
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
|
|
2216
|
+
}
|
|
2217
|
+
const edits = [];
|
|
2218
|
+
for (const candidate of value) {
|
|
2219
|
+
if (!isRecord3(candidate) || typeof candidate["newText"] !== "string") {
|
|
2220
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
|
|
2221
|
+
}
|
|
2222
|
+
if ("annotationId" in candidate) {
|
|
2223
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
|
|
2224
|
+
}
|
|
2225
|
+
const range = parseRange(candidate["range"]);
|
|
2226
|
+
if (!range)
|
|
2227
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
|
|
2228
|
+
edits.push({ range, newText: candidate["newText"] });
|
|
2229
|
+
}
|
|
2230
|
+
return edits;
|
|
2231
|
+
}
|
|
2232
|
+
function parseBooleanOption(options, key, changeIndex) {
|
|
2233
|
+
const value = options[key];
|
|
2234
|
+
if (value === undefined)
|
|
2235
|
+
return false;
|
|
2236
|
+
if (typeof value !== "boolean") {
|
|
2237
|
+
throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
|
|
2238
|
+
}
|
|
2239
|
+
return value;
|
|
2240
|
+
}
|
|
2241
|
+
function parseOptions(value, allowed, changeIndex) {
|
|
2242
|
+
if (value === undefined)
|
|
2243
|
+
return {};
|
|
2244
|
+
if (!isRecord3(value))
|
|
2245
|
+
throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
|
|
2246
|
+
for (const key of Object.keys(value)) {
|
|
2247
|
+
if (!allowed.includes(key))
|
|
2248
|
+
throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
|
|
2249
|
+
}
|
|
2250
|
+
const parsed = {};
|
|
2251
|
+
for (const key of allowed)
|
|
2252
|
+
parsed[key] = parseBooleanOption(value, key, changeIndex);
|
|
2253
|
+
return parsed;
|
|
2254
|
+
}
|
|
2255
|
+
function parseResourceChange(input) {
|
|
2256
|
+
const kind = input.change["kind"];
|
|
2257
|
+
if (kind === "create" || kind === "delete") {
|
|
2258
|
+
parseSinglePathResource(input, kind);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
if (kind !== "rename") {
|
|
2262
|
+
throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
|
|
2263
|
+
}
|
|
2264
|
+
parseRename(input);
|
|
2265
|
+
}
|
|
2266
|
+
function parseSinglePathResource(input, kind) {
|
|
2267
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2268
|
+
if (typeof change["uri"] !== "string")
|
|
2269
|
+
throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
|
|
2270
|
+
const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
|
|
2271
|
+
if (!resolvedPath.success) {
|
|
2272
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (kind === "create") {
|
|
2276
|
+
const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2277
|
+
target.operations.push({
|
|
2278
|
+
kind,
|
|
2279
|
+
changeIndex,
|
|
2280
|
+
path: resolvedPath.path,
|
|
2281
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2282
|
+
overwrite: options2["overwrite"] ?? false,
|
|
2283
|
+
ignoreIfExists: options2["ignoreIfExists"] ?? false,
|
|
2284
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2285
|
+
});
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
2288
|
+
const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
|
|
2289
|
+
target.operations.push({
|
|
2290
|
+
kind,
|
|
2291
|
+
changeIndex,
|
|
2292
|
+
path: resolvedPath.path,
|
|
2293
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2294
|
+
recursive: options["recursive"] ?? false,
|
|
2295
|
+
ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
|
|
2296
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
function parseRename(input) {
|
|
2300
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2301
|
+
if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
|
|
2302
|
+
throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
|
|
2303
|
+
}
|
|
2304
|
+
const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
|
|
2305
|
+
const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
|
|
2306
|
+
if (!oldPath.success || !newPath.success) {
|
|
2307
|
+
target.failures.push({
|
|
2308
|
+
changeIndex,
|
|
2309
|
+
message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
|
|
2310
|
+
});
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2314
|
+
target.operations.push({
|
|
2315
|
+
kind: "rename",
|
|
2316
|
+
changeIndex,
|
|
2317
|
+
oldPath: oldPath.path,
|
|
2318
|
+
newPath: newPath.path,
|
|
2319
|
+
reportedOldPath: oldPath.requestedPath,
|
|
2320
|
+
reportedNewPath: newPath.requestedPath,
|
|
2321
|
+
overwrite: options["overwrite"] ?? false,
|
|
2322
|
+
ignoreIfExists: options["ignoreIfExists"] ?? false,
|
|
2323
|
+
followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
function failureResult(failures) {
|
|
2327
|
+
const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
|
|
2328
|
+
const first = sorted[0];
|
|
2329
|
+
return {
|
|
2330
|
+
success: false,
|
|
2331
|
+
filesModified: [],
|
|
2332
|
+
totalEdits: 0,
|
|
2333
|
+
errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
|
|
2334
|
+
...first ? { failedChange: first.changeIndex } : {}
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
function parseWorkspaceEdit(edit, workspaceRoot) {
|
|
2338
|
+
if (!isRecord3(edit))
|
|
2339
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
|
|
2340
|
+
if (edit["changeAnnotations"] !== undefined) {
|
|
2341
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
|
|
2342
|
+
}
|
|
2343
|
+
const hasChanges = edit["changes"] !== undefined;
|
|
2344
|
+
const hasDocumentChanges = edit["documentChanges"] !== undefined;
|
|
2345
|
+
if (hasChanges && hasDocumentChanges) {
|
|
2346
|
+
return {
|
|
2347
|
+
operations: [],
|
|
2348
|
+
failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
const target = { operations: [], failures: [] };
|
|
2352
|
+
if (hasChanges)
|
|
2353
|
+
return parseChanges(edit["changes"], workspaceRoot, target);
|
|
2354
|
+
return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
|
|
2355
|
+
}
|
|
2356
|
+
function parseChanges(value, workspaceRoot, target) {
|
|
2357
|
+
if (!isRecord3(value))
|
|
2358
|
+
return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
|
|
2359
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
2360
|
+
for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
|
|
2361
|
+
const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
|
|
2362
|
+
if (!resolvedPath.success) {
|
|
2363
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
try {
|
|
2367
|
+
target.operations.push({
|
|
2368
|
+
kind: "text",
|
|
2369
|
+
changeIndex,
|
|
2370
|
+
path: resolvedPath.path,
|
|
2371
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2372
|
+
edits: parseTextEdits(rawEdits, changeIndex),
|
|
2373
|
+
version: null
|
|
2374
|
+
});
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2377
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2378
|
+
continue;
|
|
2379
|
+
}
|
|
2380
|
+
throw error;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
return target;
|
|
2384
|
+
}
|
|
2385
|
+
function parseDocumentChanges(value, workspaceRoot, target) {
|
|
2386
|
+
if (value === undefined)
|
|
2387
|
+
return target;
|
|
2388
|
+
if (!Array.isArray(value)) {
|
|
2389
|
+
return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
|
|
2390
|
+
}
|
|
2391
|
+
for (const [changeIndex, change] of value.entries()) {
|
|
2392
|
+
try {
|
|
2393
|
+
parseDocumentChange({ change, changeIndex, workspaceRoot, target });
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2396
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
throw error;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
return target;
|
|
2403
|
+
}
|
|
2404
|
+
function parseDocumentChange(input) {
|
|
2405
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2406
|
+
if (!isRecord3(change))
|
|
2407
|
+
throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
|
|
2408
|
+
if ("annotationId" in change) {
|
|
2409
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
|
|
2410
|
+
}
|
|
2411
|
+
if (typeof change["kind"] === "string") {
|
|
2412
|
+
parseResourceChange({ change, changeIndex, workspaceRoot, target });
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
const identifier = change["textDocument"];
|
|
2416
|
+
if (!isRecord3(identifier) || typeof identifier["uri"] !== "string") {
|
|
2417
|
+
throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
|
|
2418
|
+
}
|
|
2419
|
+
const version = identifier["version"];
|
|
2420
|
+
if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
|
|
2421
|
+
throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
|
|
2422
|
+
}
|
|
2423
|
+
const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
|
|
2424
|
+
if (!resolvedPath.success) {
|
|
2425
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
target.operations.push({
|
|
2429
|
+
kind: "text",
|
|
2430
|
+
changeIndex,
|
|
2431
|
+
path: resolvedPath.path,
|
|
2432
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2433
|
+
edits: parseTextEdits(change["edits"], changeIndex),
|
|
2434
|
+
version
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
function comparePosition(left, right) {
|
|
2438
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
2439
|
+
}
|
|
2440
|
+
function positionsEqual(left, right) {
|
|
2441
|
+
return left.line === right.line && left.character === right.character;
|
|
2442
|
+
}
|
|
2443
|
+
function rangesEqual(left, right) {
|
|
2444
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
2445
|
+
}
|
|
2446
|
+
function isEmptyRange(range) {
|
|
2447
|
+
return positionsEqual(range.start, range.end);
|
|
2448
|
+
}
|
|
2449
|
+
function formatRange(range) {
|
|
2450
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
2451
|
+
}
|
|
2452
|
+
function validatePosition(position, label, context) {
|
|
2453
|
+
const { lines, changeIndex } = context;
|
|
2454
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
2455
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
2456
|
+
}
|
|
2457
|
+
if (position.line < 0 || position.character < 0) {
|
|
2458
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
2459
|
+
}
|
|
2460
|
+
const line = lines[position.line];
|
|
2461
|
+
if (line === undefined) {
|
|
2462
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
2463
|
+
}
|
|
2464
|
+
if (position.character > line.length) {
|
|
2465
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
function validateRange(range, lines, changeIndex) {
|
|
2469
|
+
const context = { lines, changeIndex };
|
|
2470
|
+
validatePosition(range.start, "start", context);
|
|
2471
|
+
validatePosition(range.end, "end", context);
|
|
2472
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
2473
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
function sortAndDeduplicate(edits) {
|
|
2477
|
+
const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
|
|
2478
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
2479
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
2480
|
+
});
|
|
2481
|
+
const unique = [];
|
|
2482
|
+
for (const entry of sorted) {
|
|
2483
|
+
const previous = unique.at(-1);
|
|
2484
|
+
if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
unique.push(entry.edit);
|
|
2488
|
+
}
|
|
2489
|
+
return unique;
|
|
2490
|
+
}
|
|
2491
|
+
function validateNoOverlap(edits, changeIndex) {
|
|
2492
|
+
for (let index = 0;index < edits.length - 1; index += 1) {
|
|
2493
|
+
const later = edits[index];
|
|
2494
|
+
const earlier = edits[index + 1];
|
|
2495
|
+
if (later === undefined || earlier === undefined)
|
|
2496
|
+
continue;
|
|
2497
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
2498
|
+
throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
function applyNormalizedTextEdits(content, edits) {
|
|
2503
|
+
const lines = content.split(`
|
|
2504
|
+
`);
|
|
2505
|
+
for (const edit of edits) {
|
|
2506
|
+
const { start, end } = edit.range;
|
|
2507
|
+
const startLine = lines[start.line];
|
|
2508
|
+
const endLine = lines[end.line];
|
|
2509
|
+
if (startLine === undefined || endLine === undefined)
|
|
2510
|
+
continue;
|
|
2511
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
2512
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
|
|
2513
|
+
`));
|
|
2514
|
+
}
|
|
2515
|
+
return lines.join(`
|
|
2516
|
+
`);
|
|
2517
|
+
}
|
|
2518
|
+
function normalizeTextEdits(content, edits, changeIndex) {
|
|
2519
|
+
const lines = content.split(`
|
|
2520
|
+
`);
|
|
2521
|
+
for (const edit of edits) {
|
|
2522
|
+
validateRange(edit.range, lines, changeIndex);
|
|
2523
|
+
}
|
|
2524
|
+
const normalized = sortAndDeduplicate(edits);
|
|
2525
|
+
validateNoOverlap(normalized, changeIndex);
|
|
2526
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
2527
|
+
}
|
|
2528
|
+
function isSameOrDescendant2(candidate, parent) {
|
|
2529
|
+
const relativePath = relative4(parent, candidate);
|
|
2530
|
+
return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
|
|
2531
|
+
}
|
|
2532
|
+
function removeVirtualSubtree(virtual, path2) {
|
|
2533
|
+
for (const candidate of [...virtual.keys()]) {
|
|
2534
|
+
if (isSameOrDescendant2(candidate, path2))
|
|
2535
|
+
virtual.delete(candidate);
|
|
2536
|
+
}
|
|
2537
|
+
virtual.set(path2, { kind: "missing" });
|
|
2538
|
+
}
|
|
2539
|
+
function moveVirtualSubtree(virtual, oldPath, newPath) {
|
|
2540
|
+
const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
|
|
2541
|
+
removeVirtualSubtree(virtual, oldPath);
|
|
2542
|
+
removeVirtualSubtree(virtual, newPath);
|
|
2543
|
+
for (const [candidate, entry] of moved) {
|
|
2544
|
+
const suffix = relative4(oldPath, candidate);
|
|
2545
|
+
virtual.set(suffix === "" ? newPath : resolve4(newPath, suffix), entry);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
function virtualDirectoryHasChildren(virtual, path2) {
|
|
2549
|
+
for (const [candidate, entry] of virtual) {
|
|
2550
|
+
if (candidate !== path2 && entry.kind !== "missing" && isSameOrDescendant2(candidate, path2))
|
|
2551
|
+
return true;
|
|
2552
|
+
}
|
|
2553
|
+
return false;
|
|
2554
|
+
}
|
|
2555
|
+
function requireVirtualParent(virtual, path2, changeIndex) {
|
|
2556
|
+
if (virtual.get(dirname5(path2))?.kind !== "directory") {
|
|
2557
|
+
throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path2}`);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
function simulateOperations(parsed, snapshots) {
|
|
2561
|
+
const virtual = new Map(snapshots);
|
|
2562
|
+
const planned = [];
|
|
2563
|
+
const failures = [];
|
|
2564
|
+
for (const operation of parsed) {
|
|
2565
|
+
try {
|
|
2566
|
+
planned.push(simulateOperation(operation, virtual));
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2569
|
+
failures.push({ changeIndex: operation.changeIndex, message: error.detail });
|
|
2570
|
+
continue;
|
|
2571
|
+
}
|
|
2572
|
+
throw error;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
return { operations: planned, failures };
|
|
2576
|
+
}
|
|
2577
|
+
function simulateOperation(operation, virtual) {
|
|
2578
|
+
switch (operation.kind) {
|
|
2579
|
+
case "text":
|
|
2580
|
+
return simulateText(operation, virtual);
|
|
2581
|
+
case "create":
|
|
2582
|
+
return simulateCreate(operation, virtual);
|
|
2583
|
+
case "rename":
|
|
2584
|
+
return simulateRename(operation, virtual);
|
|
2585
|
+
case "delete":
|
|
2586
|
+
return simulateDelete(operation, virtual);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
function rejectSymbolicLink(operation) {
|
|
2590
|
+
if (operation.followedSymbolicLink) {
|
|
2591
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
function simulateText(operation, virtual) {
|
|
2595
|
+
const entry = virtual.get(operation.path);
|
|
2596
|
+
if (entry?.kind !== "file")
|
|
2597
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
|
|
2598
|
+
const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
|
|
2599
|
+
virtual.set(operation.path, { kind: "file", content: normalized.text });
|
|
2600
|
+
return {
|
|
2601
|
+
kind: "text",
|
|
2602
|
+
changeIndex: operation.changeIndex,
|
|
2603
|
+
path: operation.path,
|
|
2604
|
+
beforeText: entry.content,
|
|
2605
|
+
afterText: normalized.text,
|
|
2606
|
+
editCount: normalized.edits.length,
|
|
2607
|
+
documentVersion: operation.version
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
function simulateCreate(operation, virtual) {
|
|
2611
|
+
rejectSymbolicLink(operation);
|
|
2612
|
+
requireVirtualParent(virtual, operation.path, operation.changeIndex);
|
|
2613
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2614
|
+
if (target.kind !== "missing") {
|
|
2615
|
+
if (operation.overwrite && target.kind === "file") {
|
|
2616
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2617
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
|
|
2618
|
+
}
|
|
2619
|
+
if (operation.ignoreIfExists)
|
|
2620
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2621
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
|
|
2622
|
+
}
|
|
2623
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2624
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
|
|
2625
|
+
}
|
|
2626
|
+
function simulateRename(operation, virtual) {
|
|
2627
|
+
rejectSymbolicLink(operation);
|
|
2628
|
+
const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
|
|
2629
|
+
if (source.kind === "missing") {
|
|
2630
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
|
|
2631
|
+
}
|
|
2632
|
+
if (operation.oldPath === operation.newPath)
|
|
2633
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2634
|
+
if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
|
|
2635
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
|
|
2636
|
+
}
|
|
2637
|
+
requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
|
|
2638
|
+
const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
|
|
2639
|
+
if (destination.kind !== "missing" && !operation.overwrite) {
|
|
2640
|
+
if (operation.ignoreIfExists)
|
|
2641
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2642
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
|
|
2643
|
+
}
|
|
2644
|
+
moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
|
|
2645
|
+
return {
|
|
2646
|
+
kind: "rename",
|
|
2647
|
+
changeIndex: operation.changeIndex,
|
|
2648
|
+
oldPath: operation.oldPath,
|
|
2649
|
+
newPath: operation.newPath,
|
|
2650
|
+
sourceKind: source.kind,
|
|
2651
|
+
replaceDestination: destination.kind !== "missing"
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
function simulateDelete(operation, virtual) {
|
|
2655
|
+
rejectSymbolicLink(operation);
|
|
2656
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2657
|
+
if (target.kind === "missing") {
|
|
2658
|
+
if (operation.ignoreIfNotExists)
|
|
2659
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2660
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
|
|
2661
|
+
}
|
|
2662
|
+
if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
|
|
2663
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
|
|
2664
|
+
}
|
|
2665
|
+
removeVirtualSubtree(virtual, operation.path);
|
|
2666
|
+
return {
|
|
2667
|
+
kind: "delete",
|
|
2668
|
+
changeIndex: operation.changeIndex,
|
|
2669
|
+
path: operation.path,
|
|
2670
|
+
targetKind: target.kind,
|
|
2671
|
+
recursive: operation.recursive
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
class WorkspaceSnapshotBuilder {
|
|
2676
|
+
workspaceRoot;
|
|
2677
|
+
snapshots = new Map;
|
|
2678
|
+
constructor(workspaceRoot) {
|
|
2679
|
+
this.workspaceRoot = workspaceRoot;
|
|
2680
|
+
}
|
|
2681
|
+
build(operations) {
|
|
2682
|
+
this.add(this.workspaceRoot, false);
|
|
2683
|
+
for (const operation of operations) {
|
|
2684
|
+
switch (operation.kind) {
|
|
2685
|
+
case "rename":
|
|
2686
|
+
this.add(operation.oldPath, true);
|
|
2687
|
+
this.add(operation.newPath, true);
|
|
2688
|
+
break;
|
|
2689
|
+
case "delete":
|
|
2690
|
+
this.add(operation.path, true);
|
|
2691
|
+
break;
|
|
2692
|
+
case "text":
|
|
2693
|
+
case "create":
|
|
2694
|
+
this.add(operation.path, false);
|
|
2695
|
+
break;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
return this.snapshots;
|
|
2699
|
+
}
|
|
2700
|
+
add(path2, includeChildren) {
|
|
2701
|
+
let candidate = path2;
|
|
2702
|
+
while (true) {
|
|
2703
|
+
const existing = this.snapshots.get(candidate);
|
|
2704
|
+
if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
|
|
2705
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path2));
|
|
2706
|
+
}
|
|
2707
|
+
if (candidate === this.workspaceRoot)
|
|
2708
|
+
break;
|
|
2709
|
+
candidate = dirname6(candidate);
|
|
2710
|
+
}
|
|
2711
|
+
if (!includeChildren || !existsSync5(path2) || !lstatSync4(path2).isDirectory())
|
|
2712
|
+
return;
|
|
2713
|
+
for (const child of readdirSync2(path2))
|
|
2714
|
+
this.add(resolve5(path2, child), true);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function snapshotOperations(operations, workspaceRoot) {
|
|
2718
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
class PlanPathIndex {
|
|
2722
|
+
firstChangeByPath = new Map;
|
|
2723
|
+
reportedPathByCanonical = new Map;
|
|
2724
|
+
build(operations) {
|
|
2725
|
+
for (const operation of operations) {
|
|
2726
|
+
switch (operation.kind) {
|
|
2727
|
+
case "rename":
|
|
2728
|
+
this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
|
|
2729
|
+
this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
|
|
2730
|
+
break;
|
|
2731
|
+
case "text":
|
|
2732
|
+
case "create":
|
|
2733
|
+
case "delete":
|
|
2734
|
+
this.add(operation.path, operation.reportedPath, operation.changeIndex);
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
add(path2, reportedPath2, changeIndex) {
|
|
2740
|
+
if (!this.firstChangeByPath.has(path2))
|
|
2741
|
+
this.firstChangeByPath.set(path2, changeIndex);
|
|
2742
|
+
if (!this.reportedPathByCanonical.has(path2))
|
|
2743
|
+
this.reportedPathByCanonical.set(path2, reportedPath2);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
function fingerprintWorkspaceEdit(edit, workspaceRoot) {
|
|
2747
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2748
|
+
if (!root.success)
|
|
2749
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2750
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2751
|
+
if (parsed.failures.length > 0)
|
|
2752
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2753
|
+
return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
|
|
2754
|
+
}
|
|
2755
|
+
function planWorkspaceEdit(edit, workspaceRoot) {
|
|
2756
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2757
|
+
if (!root.success)
|
|
2758
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2759
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2760
|
+
if (parsed.failures.length > 0)
|
|
2761
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2762
|
+
let snapshots;
|
|
2763
|
+
try {
|
|
2764
|
+
snapshots = snapshotOperations(parsed.operations, root.path);
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
return {
|
|
2767
|
+
success: false,
|
|
2768
|
+
result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
const simulated = simulateOperations(parsed.operations, snapshots);
|
|
2772
|
+
if (simulated.failures.length > 0)
|
|
2773
|
+
return { success: false, result: failureResult(simulated.failures) };
|
|
2774
|
+
const paths = new PlanPathIndex;
|
|
2775
|
+
paths.build(parsed.operations);
|
|
2776
|
+
const plan = {
|
|
2777
|
+
workspaceRoot: root.path,
|
|
2778
|
+
operations: simulated.operations,
|
|
2779
|
+
snapshots,
|
|
2780
|
+
firstChangeByPath: paths.firstChangeByPath,
|
|
2781
|
+
reportedPathByCanonical: paths.reportedPathByCanonical,
|
|
2782
|
+
fingerprint: canonicalFingerprint(parsed.operations)
|
|
2783
|
+
};
|
|
2784
|
+
return { success: true, plan };
|
|
2785
|
+
}
|
|
2786
|
+
function failure(message, failedChange, base) {
|
|
2787
|
+
return {
|
|
2788
|
+
success: false,
|
|
2789
|
+
filesModified: base?.filesModified ?? [],
|
|
2790
|
+
totalEdits: base?.totalEdits ?? 0,
|
|
2791
|
+
errors: [message],
|
|
2792
|
+
...failedChange === undefined ? {} : { failedChange },
|
|
2793
|
+
...base?.lateAbort ? { lateAbort: true } : {}
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
function responseFor(result) {
|
|
2797
|
+
if (result.success)
|
|
2798
|
+
return { applied: true };
|
|
2799
|
+
return {
|
|
2800
|
+
applied: false,
|
|
2801
|
+
failureReason: result.errors[0] ?? "workspace edit failed",
|
|
2802
|
+
...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
function isRecord4(value) {
|
|
2806
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
class WorkspaceMutationController {
|
|
2810
|
+
workspaceRoot;
|
|
2811
|
+
documents;
|
|
2812
|
+
activeLease = null;
|
|
2813
|
+
nextLeaseId = 1;
|
|
2814
|
+
io;
|
|
2815
|
+
constructor(workspaceRoot, documents) {
|
|
2816
|
+
this.workspaceRoot = workspaceRoot;
|
|
2817
|
+
this.documents = documents;
|
|
2818
|
+
}
|
|
2819
|
+
setIo(io) {
|
|
2820
|
+
this.io = io;
|
|
2821
|
+
}
|
|
2822
|
+
acquire(signal) {
|
|
2823
|
+
if (this.activeLease)
|
|
2824
|
+
return { success: false, result: failure("workspace mutation is already in progress") };
|
|
2825
|
+
if (signal?.aborted)
|
|
2826
|
+
return { success: false, result: failure("cancelled before mutating request") };
|
|
2827
|
+
const lease = {
|
|
2828
|
+
id: this.nextLeaseId,
|
|
2829
|
+
phase: "idle",
|
|
2830
|
+
...signal === undefined ? {} : { signal }
|
|
2831
|
+
};
|
|
2832
|
+
this.nextLeaseId += 1;
|
|
2833
|
+
this.activeLease = lease;
|
|
2834
|
+
return { success: true, lease };
|
|
2835
|
+
}
|
|
2836
|
+
release(lease) {
|
|
2837
|
+
if (this.activeLease?.id !== lease.id)
|
|
2838
|
+
return;
|
|
2839
|
+
this.activeLease.phase = "sealed";
|
|
2840
|
+
this.activeLease = null;
|
|
2841
|
+
}
|
|
2842
|
+
isBeforeCommit(lease) {
|
|
2843
|
+
return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
|
|
2844
|
+
}
|
|
2845
|
+
async handleApplyEdit(params) {
|
|
2846
|
+
const lease = this.activeLease;
|
|
2847
|
+
if (!lease)
|
|
2848
|
+
return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
|
|
2849
|
+
if (lease.phase !== "idle") {
|
|
2850
|
+
return {
|
|
2851
|
+
applied: false,
|
|
2852
|
+
failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
lease.phase = "applying";
|
|
2856
|
+
lease.applyCompletion = new Promise((resolve62) => {
|
|
2857
|
+
lease.resolveApply = resolve62;
|
|
2858
|
+
});
|
|
2859
|
+
const edit = isRecord4(params) ? params["edit"] : undefined;
|
|
2860
|
+
const record2 = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
|
|
2861
|
+
lease.serverApply = record2;
|
|
2862
|
+
lease.phase = "settled";
|
|
2863
|
+
lease.resolveApply?.();
|
|
2864
|
+
return responseFor(record2.result);
|
|
2865
|
+
}
|
|
2866
|
+
async reconcileRename(leaseToken, edit) {
|
|
2867
|
+
const lease = this.requireActiveLease(leaseToken);
|
|
2868
|
+
if (!lease)
|
|
2869
|
+
return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
|
|
2870
|
+
if (lease.phase === "applying")
|
|
2871
|
+
await lease.applyCompletion;
|
|
2872
|
+
if (lease.serverApply)
|
|
2873
|
+
return this.reconcileServerApply(lease.serverApply, edit);
|
|
2874
|
+
lease.phase = "sealed";
|
|
2875
|
+
if (!edit)
|
|
2876
|
+
return { edit, apply: failure("No edit provided") };
|
|
2877
|
+
const applied = await this.applyEdit(edit, lease);
|
|
2878
|
+
return { edit, apply: applied.result };
|
|
2879
|
+
}
|
|
2880
|
+
reconcileServerApply(record2, edit) {
|
|
2881
|
+
if (!edit)
|
|
2882
|
+
return { edit, apply: record2.result };
|
|
2883
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
|
|
2884
|
+
if (fingerprint.success && record2.fingerprint !== null && fingerprint.fingerprint === record2.fingerprint) {
|
|
2885
|
+
return { edit, apply: record2.result };
|
|
2886
|
+
}
|
|
2887
|
+
return {
|
|
2888
|
+
edit,
|
|
2889
|
+
apply: failure("rename result conflicts with server-applied workspace edit", 0, record2.result)
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
async applyEdit(edit, lease) {
|
|
2893
|
+
const planned = planWorkspaceEdit(edit, this.workspaceRoot);
|
|
2894
|
+
if (!planned.success)
|
|
2895
|
+
return { fingerprint: null, result: planned.result };
|
|
2896
|
+
const versionFailure = this.documents.validateVersions(planned.plan.operations);
|
|
2897
|
+
if (versionFailure) {
|
|
2898
|
+
return {
|
|
2899
|
+
fingerprint: planned.plan.fingerprint,
|
|
2900
|
+
result: failure(versionFailure.message, versionFailure.changeIndex)
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
const commit = commitWorkspaceEditPlan(planned.plan, {
|
|
2904
|
+
...lease.signal === undefined ? {} : { signal: lease.signal },
|
|
2905
|
+
...this.io === undefined ? {} : { io: this.io }
|
|
1056
2906
|
});
|
|
2907
|
+
let result = commit.result;
|
|
2908
|
+
if (commit.delta.operations.length > 0) {
|
|
2909
|
+
try {
|
|
2910
|
+
await this.documents.synchronize(commit.delta);
|
|
2911
|
+
} catch (error) {
|
|
2912
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2913
|
+
result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
if (lease.signal?.aborted && !result.lateAbort)
|
|
2917
|
+
result = { ...result, lateAbort: true };
|
|
2918
|
+
return { fingerprint: planned.plan.fingerprint, result };
|
|
2919
|
+
}
|
|
2920
|
+
requireActiveLease(lease) {
|
|
2921
|
+
return this.activeLease?.id === lease.id ? this.activeLease : null;
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
|
|
2925
|
+
var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
2926
|
+
|
|
2927
|
+
class LspClient extends LspClientConnection {
|
|
2928
|
+
diagnosticPullErrors = [];
|
|
2929
|
+
documents;
|
|
2930
|
+
workspaceMutations;
|
|
2931
|
+
diagnosticsFreshnessTimeoutMs;
|
|
2932
|
+
constructor(root, server2, options = {}) {
|
|
2933
|
+
super(root, server2, options);
|
|
2934
|
+
this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
|
|
2935
|
+
this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
|
|
2936
|
+
versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
|
|
2937
|
+
});
|
|
2938
|
+
this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
|
|
2939
|
+
this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
|
|
2940
|
+
}
|
|
2941
|
+
getDiagnosticPullErrors() {
|
|
2942
|
+
return this.diagnosticPullErrors;
|
|
1057
2943
|
}
|
|
1058
|
-
async
|
|
1059
|
-
const absPath =
|
|
2944
|
+
async openFile(filePath) {
|
|
2945
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2946
|
+
await this.documents.openFile(absPath);
|
|
2947
|
+
}
|
|
2948
|
+
getOpenDocumentVersion(filePath) {
|
|
2949
|
+
return this.documents.getVersion(this.resolveWorkspacePath(filePath));
|
|
2950
|
+
}
|
|
2951
|
+
getStoredDiagnostics(uri) {
|
|
2952
|
+
return [...this.documents.getStoredDiagnostics(uri)];
|
|
2953
|
+
}
|
|
2954
|
+
setWorkspaceEditIo(io) {
|
|
2955
|
+
this.workspaceMutations.setIo(io);
|
|
2956
|
+
}
|
|
2957
|
+
handlePublishDiagnostics(params) {
|
|
2958
|
+
super.handlePublishDiagnostics(params);
|
|
2959
|
+
this.documents.recordPublishedDiagnostics(params);
|
|
2960
|
+
}
|
|
2961
|
+
async definition(filePath, line, character, signal) {
|
|
2962
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1060
2963
|
await this.openFile(absPath);
|
|
2964
|
+
const options = signal === undefined ? {} : { signal };
|
|
1061
2965
|
return this.sendRequest("textDocument/definition", {
|
|
1062
|
-
textDocument: { uri:
|
|
2966
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1063
2967
|
position: { line: line - 1, character }
|
|
1064
|
-
});
|
|
2968
|
+
}, options);
|
|
1065
2969
|
}
|
|
1066
|
-
async references(filePath, line, character, includeDeclaration = true) {
|
|
1067
|
-
const absPath =
|
|
2970
|
+
async references(filePath, line, character, includeDeclaration = true, signal) {
|
|
2971
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1068
2972
|
await this.openFile(absPath);
|
|
2973
|
+
const options = signal === undefined ? {} : { signal };
|
|
1069
2974
|
return this.sendRequest("textDocument/references", {
|
|
1070
|
-
textDocument: { uri:
|
|
2975
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1071
2976
|
position: { line: line - 1, character },
|
|
1072
2977
|
context: { includeDeclaration }
|
|
1073
|
-
});
|
|
2978
|
+
}, options);
|
|
1074
2979
|
}
|
|
1075
|
-
async documentSymbols(filePath) {
|
|
1076
|
-
const absPath =
|
|
2980
|
+
async documentSymbols(filePath, signal) {
|
|
2981
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1077
2982
|
await this.openFile(absPath);
|
|
2983
|
+
const options = signal === undefined ? {} : { signal };
|
|
1078
2984
|
return this.sendRequest("textDocument/documentSymbol", {
|
|
1079
|
-
textDocument: { uri:
|
|
1080
|
-
});
|
|
2985
|
+
textDocument: { uri: pathToFileURL3(absPath).href }
|
|
2986
|
+
}, options);
|
|
1081
2987
|
}
|
|
1082
|
-
async workspaceSymbols(query) {
|
|
1083
|
-
|
|
2988
|
+
async workspaceSymbols(query, signal) {
|
|
2989
|
+
const options = signal === undefined ? {} : { signal };
|
|
2990
|
+
return this.sendRequest("workspace/symbol", { query }, options);
|
|
1084
2991
|
}
|
|
1085
2992
|
isUnsupportedDiagnosticPullError(error) {
|
|
1086
2993
|
if (!(error instanceof Error))
|
|
@@ -1090,42 +2997,173 @@ class LspClient extends LspClientConnection {
|
|
|
1090
2997
|
return true;
|
|
1091
2998
|
return /unsupported|not supported|method not found|unknown request/i.test(error.message);
|
|
1092
2999
|
}
|
|
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 };
|
|
3000
|
+
freshnessTimeout(absPath) {
|
|
3001
|
+
return {
|
|
3002
|
+
items: [],
|
|
3003
|
+
transientError: {
|
|
3004
|
+
kind: "freshness_timeout",
|
|
3005
|
+
message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
|
|
1104
3006
|
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
parseDiagnosticPullReport(value) {
|
|
3010
|
+
if (value.kind === "unchanged") {
|
|
3011
|
+
return {
|
|
3012
|
+
type: "unchanged",
|
|
3013
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
return {
|
|
3017
|
+
type: "full",
|
|
3018
|
+
diagnostics: value.items ?? [],
|
|
3019
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
async diagnostics(filePath, signal) {
|
|
3023
|
+
signal?.throwIfAborted();
|
|
3024
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
3025
|
+
const uri = pathToFileURL3(absPath).href;
|
|
3026
|
+
await this.openFile(absPath);
|
|
3027
|
+
const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
|
|
3028
|
+
for (;; ) {
|
|
3029
|
+
signal?.throwIfAborted();
|
|
3030
|
+
const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
|
|
3031
|
+
if (!snapshot)
|
|
3032
|
+
return this.freshnessTimeout(absPath);
|
|
3033
|
+
const push = this.documents.resolvePushDiagnostics(snapshot);
|
|
3034
|
+
if (push.status === "ready")
|
|
3035
|
+
return { items: [...push.diagnostics] };
|
|
3036
|
+
let pushFallbackOnly = !this.isDiagnosticPullSupported();
|
|
3037
|
+
if (!pushFallbackOnly) {
|
|
3038
|
+
const cached = this.documents.getPullCache(snapshot);
|
|
3039
|
+
try {
|
|
3040
|
+
const remainingMs2 = deadlineAt - Date.now();
|
|
3041
|
+
if (remainingMs2 <= 0)
|
|
3042
|
+
return this.freshnessTimeout(absPath);
|
|
3043
|
+
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
3044
|
+
textDocument: { uri },
|
|
3045
|
+
...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
|
|
3046
|
+
}, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
|
|
3047
|
+
if (!this.documents.isCurrentSnapshot(snapshot))
|
|
3048
|
+
continue;
|
|
3049
|
+
const report = this.parseDiagnosticPullReport(result);
|
|
3050
|
+
if (report.type === "full") {
|
|
3051
|
+
this.documents.recordPullDiagnostics(snapshot, {
|
|
3052
|
+
kind: "full",
|
|
3053
|
+
diagnostics: report.diagnostics,
|
|
3054
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
3055
|
+
});
|
|
3056
|
+
return { items: [...report.diagnostics] };
|
|
3057
|
+
}
|
|
3058
|
+
if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
|
|
3059
|
+
return { items: [...cached.diagnostics] };
|
|
3060
|
+
}
|
|
3061
|
+
} catch (error) {
|
|
3062
|
+
if (this.isUnsupportedDiagnosticPullError(error)) {
|
|
3063
|
+
this.setDiagnosticPullSupported(false);
|
|
3064
|
+
pushFallbackOnly = true;
|
|
3065
|
+
} else if (error instanceof LspRequestTimeoutError) {
|
|
3066
|
+
pushFallbackOnly = true;
|
|
3067
|
+
} else {
|
|
3068
|
+
this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
3069
|
+
throw error;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
1108
3072
|
}
|
|
3073
|
+
if (!pushFallbackOnly)
|
|
3074
|
+
continue;
|
|
3075
|
+
const remainingMs = deadlineAt - Date.now();
|
|
3076
|
+
if (remainingMs <= 0)
|
|
3077
|
+
return this.freshnessTimeout(absPath);
|
|
3078
|
+
const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
|
|
3079
|
+
await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
|
|
1109
3080
|
}
|
|
1110
|
-
return { items: this.getStoredDiagnostics(uri) };
|
|
1111
3081
|
}
|
|
1112
|
-
async prepareRename(filePath, line, character) {
|
|
1113
|
-
const absPath =
|
|
3082
|
+
async prepareRename(filePath, line, character, signal) {
|
|
3083
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1114
3084
|
await this.openFile(absPath);
|
|
3085
|
+
const options = signal === undefined ? {} : { signal };
|
|
1115
3086
|
return this.sendRequest("textDocument/prepareRename", {
|
|
1116
|
-
textDocument: { uri:
|
|
3087
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1117
3088
|
position: { line: line - 1, character }
|
|
1118
|
-
});
|
|
3089
|
+
}, options);
|
|
1119
3090
|
}
|
|
1120
|
-
async rename(filePath, line, character, newName) {
|
|
1121
|
-
const absPath =
|
|
3091
|
+
async rename(filePath, line, character, newName, signal) {
|
|
3092
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1122
3093
|
await this.openFile(absPath);
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
3094
|
+
const acquired = this.workspaceMutations.acquire(signal);
|
|
3095
|
+
if (!acquired.success)
|
|
3096
|
+
return { edit: null, apply: acquired.result };
|
|
3097
|
+
const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
|
|
3098
|
+
try {
|
|
3099
|
+
const renameParams = {
|
|
3100
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
3101
|
+
position: { line: line - 1, character },
|
|
3102
|
+
newName
|
|
3103
|
+
};
|
|
3104
|
+
const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
|
|
3105
|
+
signal: preCommitSignal.signal
|
|
3106
|
+
});
|
|
3107
|
+
return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
|
|
3108
|
+
} finally {
|
|
3109
|
+
preCommitSignal?.dispose();
|
|
3110
|
+
this.workspaceMutations.release(acquired.lease);
|
|
3111
|
+
}
|
|
1128
3112
|
}
|
|
3113
|
+
resolveWorkspacePath(filePath) {
|
|
3114
|
+
return resolve6(this.root, filePath);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
function waitForDiagnosticsActivity(wait, signal) {
|
|
3118
|
+
if (!signal)
|
|
3119
|
+
return wait;
|
|
3120
|
+
if (signal.aborted)
|
|
3121
|
+
return Promise.reject(abortError2(signal));
|
|
3122
|
+
return new Promise((resolve72, reject) => {
|
|
3123
|
+
const onAbort = () => {
|
|
3124
|
+
signal.removeEventListener("abort", onAbort);
|
|
3125
|
+
reject(abortError2(signal));
|
|
3126
|
+
};
|
|
3127
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3128
|
+
wait.then(() => {
|
|
3129
|
+
signal.removeEventListener("abort", onAbort);
|
|
3130
|
+
resolve72();
|
|
3131
|
+
}, (error) => {
|
|
3132
|
+
signal.removeEventListener("abort", onAbort);
|
|
3133
|
+
reject(error);
|
|
3134
|
+
});
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
function createPreCommitAbortSignal(source, isBeforeCommit) {
|
|
3138
|
+
if (!source)
|
|
3139
|
+
return;
|
|
3140
|
+
const controller = new AbortController;
|
|
3141
|
+
const onAbort = () => {
|
|
3142
|
+
if (isBeforeCommit() && !controller.signal.aborted)
|
|
3143
|
+
controller.abort(preCommitAbortReason(source));
|
|
3144
|
+
};
|
|
3145
|
+
if (source.aborted)
|
|
3146
|
+
onAbort();
|
|
3147
|
+
else
|
|
3148
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
3149
|
+
return {
|
|
3150
|
+
signal: controller.signal,
|
|
3151
|
+
dispose: () => source.removeEventListener("abort", onAbort)
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
function preCommitAbortReason(source) {
|
|
3155
|
+
const reason = source.reason;
|
|
3156
|
+
if (reason instanceof Error && reason.name !== "AbortError")
|
|
3157
|
+
return reason;
|
|
3158
|
+
return new Error("LSP request cancelled before workspace edit commit");
|
|
3159
|
+
}
|
|
3160
|
+
function abortError2(signal) {
|
|
3161
|
+
const reason = signal.reason;
|
|
3162
|
+
if (reason instanceof Error)
|
|
3163
|
+
return reason;
|
|
3164
|
+
const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
|
|
3165
|
+
error.name = "AbortError";
|
|
3166
|
+
return error;
|
|
1129
3167
|
}
|
|
1130
3168
|
function installProcessSignalCleanup(cleanup) {
|
|
1131
3169
|
const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM"];
|
|
@@ -1153,7 +3191,7 @@ async function stopClientBestEffort(client) {
|
|
|
1153
3191
|
function awaitWithSignal(promise, signal) {
|
|
1154
3192
|
if (!signal)
|
|
1155
3193
|
return promise;
|
|
1156
|
-
return new Promise((
|
|
3194
|
+
return new Promise((resolve72, reject) => {
|
|
1157
3195
|
let settled = false;
|
|
1158
3196
|
const onAbort = () => {
|
|
1159
3197
|
if (settled)
|
|
@@ -1171,7 +3209,7 @@ function awaitWithSignal(promise, signal) {
|
|
|
1171
3209
|
return;
|
|
1172
3210
|
settled = true;
|
|
1173
3211
|
signal.removeEventListener("abort", onAbort);
|
|
1174
|
-
|
|
3212
|
+
resolve72(value);
|
|
1175
3213
|
}, (err) => {
|
|
1176
3214
|
if (settled)
|
|
1177
3215
|
return;
|
|
@@ -1196,7 +3234,7 @@ class LspManager {
|
|
|
1196
3234
|
this.idleTimeoutMs = options.idleTimeoutMs ?? IDLE_TIMEOUT_MS;
|
|
1197
3235
|
this.initTimeoutMs = options.initTimeoutMs ?? INIT_TIMEOUT_MS;
|
|
1198
3236
|
this.reaperIntervalMs = options.reaperIntervalMs ?? REAPER_INTERVAL_MS;
|
|
1199
|
-
this.clientFactory = options.clientFactory ?? ((root,
|
|
3237
|
+
this.clientFactory = options.clientFactory ?? ((root, server2) => new LspClient(root, server2));
|
|
1200
3238
|
this.now = options.now ?? (() => Date.now());
|
|
1201
3239
|
this.startReaper();
|
|
1202
3240
|
this.signalDisposer = installProcessSignalCleanup(() => this.stopAll());
|
|
@@ -1234,12 +3272,12 @@ class LspManager {
|
|
|
1234
3272
|
await stopClientBestEffort(managed.client);
|
|
1235
3273
|
}
|
|
1236
3274
|
}
|
|
1237
|
-
async getClient(root,
|
|
3275
|
+
async getClient(root, server2, signal) {
|
|
1238
3276
|
if (this.disposed) {
|
|
1239
3277
|
throw new Error("LspManager has been disposed");
|
|
1240
3278
|
}
|
|
1241
3279
|
signal?.throwIfAborted();
|
|
1242
|
-
const key = this.getKey(root,
|
|
3280
|
+
const key = this.getKey(root, server2.id);
|
|
1243
3281
|
let managed = this.clients.get(key);
|
|
1244
3282
|
if (managed) {
|
|
1245
3283
|
const t = this.now();
|
|
@@ -1268,13 +3306,13 @@ class LspManager {
|
|
|
1268
3306
|
if (!managed.client.isAlive()) {
|
|
1269
3307
|
await stopClientBestEffort(managed.client);
|
|
1270
3308
|
this.clients.delete(key);
|
|
1271
|
-
return this.getClient(root,
|
|
3309
|
+
return this.getClient(root, server2, signal);
|
|
1272
3310
|
}
|
|
1273
3311
|
managed.refCount++;
|
|
1274
3312
|
managed.lastUsedAt = this.now();
|
|
1275
3313
|
return managed.client;
|
|
1276
3314
|
}
|
|
1277
|
-
const client = this.clientFactory(root,
|
|
3315
|
+
const client = this.clientFactory(root, server2);
|
|
1278
3316
|
const initStartedAt = this.now();
|
|
1279
3317
|
const initPromise = (async () => {
|
|
1280
3318
|
await client.start();
|
|
@@ -1330,13 +3368,13 @@ class LspManager {
|
|
|
1330
3368
|
this.clients.delete(key);
|
|
1331
3369
|
stopClientBestEffort(managed.client);
|
|
1332
3370
|
}
|
|
1333
|
-
warmupClient(root,
|
|
3371
|
+
warmupClient(root, server2) {
|
|
1334
3372
|
if (this.disposed)
|
|
1335
3373
|
return;
|
|
1336
|
-
const key = this.getKey(root,
|
|
3374
|
+
const key = this.getKey(root, server2.id);
|
|
1337
3375
|
if (this.clients.has(key))
|
|
1338
3376
|
return;
|
|
1339
|
-
const client = this.clientFactory(root,
|
|
3377
|
+
const client = this.clientFactory(root, server2);
|
|
1340
3378
|
const initStartedAt = this.now();
|
|
1341
3379
|
const initPromise = (async () => {
|
|
1342
3380
|
await client.start();
|
|
@@ -1416,230 +3454,15 @@ function getLspManager() {
|
|
|
1416
3454
|
}
|
|
1417
3455
|
return _defaultInstance;
|
|
1418
3456
|
}
|
|
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
3457
|
function getInstallDecisionsPath() {
|
|
1632
|
-
|
|
1633
|
-
if (!override)
|
|
1634
|
-
return join3(homedir2(), ".codex", "lsp-install-decisions.json");
|
|
1635
|
-
return isAbsolute(override) ? override : join3(homedir2(), override);
|
|
3458
|
+
return lspRequestContext().installDecisionsPath;
|
|
1636
3459
|
}
|
|
1637
3460
|
function loadInstallDecisions() {
|
|
1638
|
-
const
|
|
1639
|
-
if (!
|
|
3461
|
+
const path2 = getInstallDecisionsPath();
|
|
3462
|
+
if (!existsSync6(path2))
|
|
1640
3463
|
return {};
|
|
1641
3464
|
try {
|
|
1642
|
-
const parsed = JSON.parse(
|
|
3465
|
+
const parsed = JSON.parse(readFileSync4(path2, "utf8"));
|
|
1643
3466
|
return isInstallDecisions(parsed) ? parsed : {};
|
|
1644
3467
|
} catch {
|
|
1645
3468
|
return {};
|
|
@@ -1657,22 +3480,22 @@ function isInstallDecision(value) {
|
|
|
1657
3480
|
return value === "declined" || value === "allowed";
|
|
1658
3481
|
}
|
|
1659
3482
|
function writeInstallDecisions(decisions) {
|
|
1660
|
-
const
|
|
1661
|
-
mkdirSync3(
|
|
1662
|
-
const tmpPath = `${
|
|
1663
|
-
|
|
3483
|
+
const path2 = getInstallDecisionsPath();
|
|
3484
|
+
mkdirSync3(dirname7(path2), { recursive: true });
|
|
3485
|
+
const tmpPath = `${path2}.tmp`;
|
|
3486
|
+
writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
|
|
1664
3487
|
`, "utf8");
|
|
1665
|
-
|
|
3488
|
+
renameSync2(tmpPath, path2);
|
|
1666
3489
|
}
|
|
1667
3490
|
function isInstallDecisions(value) {
|
|
1668
|
-
return
|
|
3491
|
+
return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
|
|
1669
3492
|
}
|
|
1670
3493
|
function isInstallDecisionRecord(value) {
|
|
1671
|
-
if (!
|
|
3494
|
+
if (!isRecord5(value))
|
|
1672
3495
|
return false;
|
|
1673
3496
|
return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
|
|
1674
3497
|
}
|
|
1675
|
-
function
|
|
3498
|
+
function isRecord5(value) {
|
|
1676
3499
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1677
3500
|
}
|
|
1678
3501
|
var LSP_INSTALL_HINTS = {
|
|
@@ -1820,27 +3643,17 @@ var BUILTIN_SERVERS = {
|
|
|
1820
3643
|
extensions: [".razor", ".cshtml"]
|
|
1821
3644
|
}
|
|
1822
3645
|
};
|
|
1823
|
-
function resolveProjectConfigPath(path) {
|
|
1824
|
-
return isAbsolute2(path) ? path : join4(contextCwd(), path);
|
|
1825
|
-
}
|
|
1826
3646
|
function getProjectConfigPaths() {
|
|
1827
|
-
|
|
1828
|
-
if (projectOverride) {
|
|
1829
|
-
return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
|
|
1830
|
-
}
|
|
1831
|
-
return [join4(contextCwd(), ".codex", "lsp-client.json")];
|
|
3647
|
+
return lspRequestContext().projectConfigPaths;
|
|
1832
3648
|
}
|
|
1833
3649
|
function getUserConfigPath() {
|
|
1834
|
-
|
|
1835
|
-
if (!userOverride)
|
|
1836
|
-
return join4(homedir3(), ".codex", "lsp-client.json");
|
|
1837
|
-
return isAbsolute2(userOverride) ? userOverride : join4(homedir3(), userOverride);
|
|
3650
|
+
return lspRequestContext().userConfigPath;
|
|
1838
3651
|
}
|
|
1839
|
-
function loadJsonFile(
|
|
1840
|
-
if (!
|
|
3652
|
+
function loadJsonFile(path2) {
|
|
3653
|
+
if (!existsSync7(path2))
|
|
1841
3654
|
return null;
|
|
1842
3655
|
try {
|
|
1843
|
-
const parsed = JSON.parse(
|
|
3656
|
+
const parsed = JSON.parse(readFileSync5(path2, "utf-8"));
|
|
1844
3657
|
return isConfigJson(parsed) ? parsed : null;
|
|
1845
3658
|
} catch {
|
|
1846
3659
|
return null;
|
|
@@ -1857,8 +3670,8 @@ function loadAllConfigs() {
|
|
|
1857
3670
|
return configs;
|
|
1858
3671
|
}
|
|
1859
3672
|
function loadFirstJsonFile(paths) {
|
|
1860
|
-
for (const
|
|
1861
|
-
const config = loadJsonFile(
|
|
3673
|
+
for (const path2 of paths) {
|
|
3674
|
+
const config = loadJsonFile(path2);
|
|
1862
3675
|
if (config)
|
|
1863
3676
|
return config;
|
|
1864
3677
|
}
|
|
@@ -1979,16 +3792,16 @@ function applyOptionalServerFields(server2, entry) {
|
|
|
1979
3792
|
}
|
|
1980
3793
|
}
|
|
1981
3794
|
function isConfigJson(value) {
|
|
1982
|
-
if (!
|
|
3795
|
+
if (!isRecord6(value))
|
|
1983
3796
|
return false;
|
|
1984
3797
|
const lsp = value["lsp"];
|
|
1985
|
-
return lsp === undefined ||
|
|
3798
|
+
return lsp === undefined || isRecord6(lsp);
|
|
1986
3799
|
}
|
|
1987
3800
|
function parseLspEntry(value) {
|
|
1988
3801
|
return isLspEntry(value) ? value : null;
|
|
1989
3802
|
}
|
|
1990
3803
|
function isLspEntry(value) {
|
|
1991
|
-
if (!
|
|
3804
|
+
if (!isRecord6(value))
|
|
1992
3805
|
return false;
|
|
1993
3806
|
const disabled = value["disabled"];
|
|
1994
3807
|
const command = value["command"];
|
|
@@ -1996,15 +3809,15 @@ function isLspEntry(value) {
|
|
|
1996
3809
|
const priority = value["priority"];
|
|
1997
3810
|
const env = value["env"];
|
|
1998
3811
|
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 ||
|
|
3812
|
+
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
3813
|
}
|
|
2001
3814
|
function isStringArray(value) {
|
|
2002
3815
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2003
3816
|
}
|
|
2004
3817
|
function isStringRecord(value) {
|
|
2005
|
-
return
|
|
3818
|
+
return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
|
|
2006
3819
|
}
|
|
2007
|
-
function
|
|
3820
|
+
function isRecord6(value) {
|
|
2008
3821
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2009
3822
|
}
|
|
2010
3823
|
function getDisabledServerIds() {
|
|
@@ -2030,7 +3843,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2030
3843
|
if (!cmd)
|
|
2031
3844
|
return false;
|
|
2032
3845
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
2033
|
-
if (
|
|
3846
|
+
if (existsSync8(cmd))
|
|
2034
3847
|
return true;
|
|
2035
3848
|
}
|
|
2036
3849
|
const isWindows = process.platform === "win32";
|
|
@@ -2051,7 +3864,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2051
3864
|
const paths = pathEnv.split(delimiter3);
|
|
2052
3865
|
for (const p of paths) {
|
|
2053
3866
|
for (const suffix of exts) {
|
|
2054
|
-
if (
|
|
3867
|
+
if (existsSync8(join3(p, cmd + suffix))) {
|
|
2055
3868
|
return true;
|
|
2056
3869
|
}
|
|
2057
3870
|
}
|
|
@@ -2146,39 +3959,50 @@ function getAllServers() {
|
|
|
2146
3959
|
var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
2147
3960
|
function isDirectoryPath(filePath) {
|
|
2148
3961
|
try {
|
|
2149
|
-
return
|
|
3962
|
+
return statSync4(filePath).isDirectory();
|
|
2150
3963
|
} catch {
|
|
2151
3964
|
return false;
|
|
2152
3965
|
}
|
|
2153
3966
|
}
|
|
2154
3967
|
function findWorkspaceRoot(filePath) {
|
|
2155
|
-
const abs =
|
|
3968
|
+
const abs = resolvePathInsideContext(filePath);
|
|
2156
3969
|
let dir = abs;
|
|
2157
3970
|
if (!isDirectoryPath(dir)) {
|
|
2158
|
-
dir =
|
|
3971
|
+
dir = dirname8(dir);
|
|
2159
3972
|
}
|
|
2160
3973
|
let prevDir = "";
|
|
2161
3974
|
while (dir !== prevDir) {
|
|
2162
3975
|
for (const marker of WORKSPACE_MARKERS) {
|
|
2163
|
-
if (
|
|
3976
|
+
if (existsSync9(join4(dir, marker))) {
|
|
2164
3977
|
return dir;
|
|
2165
3978
|
}
|
|
2166
3979
|
}
|
|
2167
3980
|
prevDir = dir;
|
|
2168
|
-
dir =
|
|
3981
|
+
dir = dirname8(dir);
|
|
2169
3982
|
}
|
|
2170
|
-
return
|
|
3983
|
+
return dirname8(abs);
|
|
3984
|
+
}
|
|
3985
|
+
function resolvePathInsideContext(filePath) {
|
|
3986
|
+
const cwd = contextCwd();
|
|
3987
|
+
const abs = resolve7(cwd, filePath);
|
|
3988
|
+
const canonical = canonicalizeExistingOrNearestAncestor(abs);
|
|
3989
|
+
if (!isPathInside(cwd, canonical)) {
|
|
3990
|
+
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
3991
|
+
}
|
|
3992
|
+
return canonical;
|
|
2171
3993
|
}
|
|
2172
3994
|
function formatServerLookupError(result) {
|
|
2173
3995
|
if (result.status === "not_installed") {
|
|
2174
3996
|
return formatNotInstalled(result);
|
|
2175
3997
|
}
|
|
3998
|
+
const context = lspRequestContext();
|
|
3999
|
+
const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
|
|
2176
4000
|
return [
|
|
2177
4001
|
`No LSP server configured for extension: ${result.extension}`,
|
|
2178
4002
|
"",
|
|
2179
4003
|
`Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
|
|
2180
4004
|
"",
|
|
2181
|
-
|
|
4005
|
+
`Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
|
|
2182
4006
|
" {",
|
|
2183
4007
|
' "lsp": {',
|
|
2184
4008
|
' "my-server": {',
|
|
@@ -2194,6 +4018,7 @@ function formatNotInstalled(result) {
|
|
|
2194
4018
|
const { server: server2, installHint } = result;
|
|
2195
4019
|
const extensions = server2.extensions.join(", ");
|
|
2196
4020
|
const decision = loadInstallDecision(server2.id)?.decision;
|
|
4021
|
+
const context = lspRequestContext();
|
|
2197
4022
|
if (decision === "declined") {
|
|
2198
4023
|
return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
|
|
2199
4024
|
}
|
|
@@ -2209,6 +4034,17 @@ function formatNotInstalled(result) {
|
|
|
2209
4034
|
"The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
|
|
2210
4035
|
` ${installHint}`
|
|
2211
4036
|
].join(`
|
|
4037
|
+
`);
|
|
4038
|
+
}
|
|
4039
|
+
if (!context.capabilities.installDecisionTool) {
|
|
4040
|
+
return [
|
|
4041
|
+
...header,
|
|
4042
|
+
"To install, run:",
|
|
4043
|
+
` ${installHint}`,
|
|
4044
|
+
"",
|
|
4045
|
+
"ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
|
|
4046
|
+
"Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
|
|
4047
|
+
].join(`
|
|
2212
4048
|
`);
|
|
2213
4049
|
}
|
|
2214
4050
|
return [
|
|
@@ -2220,6 +4056,7 @@ function formatNotInstalled(result) {
|
|
|
2220
4056
|
"- If the user agrees: run the install command above, then retry this tool.",
|
|
2221
4057
|
"- If the user declines, OR has NOT explicitly asked for LSP installation:",
|
|
2222
4058
|
` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
|
|
4059
|
+
` which writes to ${context.installDecisionsPath},`,
|
|
2223
4060
|
" then ignore this message and proceed WITHOUT LSP."
|
|
2224
4061
|
].join(`
|
|
2225
4062
|
`);
|
|
@@ -2233,14 +4070,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
|
|
|
2233
4070
|
"prepareRename"
|
|
2234
4071
|
]);
|
|
2235
4072
|
async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
2236
|
-
const absPath =
|
|
4073
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2237
4074
|
if (isDirectoryPath(absPath)) {
|
|
2238
4075
|
throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
|
|
2239
4076
|
}
|
|
2240
4077
|
const ext = effectiveExtension(absPath);
|
|
2241
4078
|
const result = findServerForExtension(ext);
|
|
2242
4079
|
if (result.status !== "found") {
|
|
2243
|
-
throw new LspServerLookupError(formatServerLookupError(result));
|
|
4080
|
+
throw new LspServerLookupError(formatServerLookupError(result), result);
|
|
2244
4081
|
}
|
|
2245
4082
|
const server2 = result.server;
|
|
2246
4083
|
const root = findWorkspaceRoot(absPath);
|
|
@@ -2273,7 +4110,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
|
2273
4110
|
hint: 4
|
|
2274
4111
|
};
|
|
2275
4112
|
function uriToPath(uri) {
|
|
2276
|
-
return
|
|
4113
|
+
return fileURLToPath3(uri);
|
|
2277
4114
|
}
|
|
2278
4115
|
function formatLocation(loc) {
|
|
2279
4116
|
if ("targetUri" in loc) {
|
|
@@ -2359,6 +4196,9 @@ function formatApplyResult(result) {
|
|
|
2359
4196
|
for (const file of result.filesModified) {
|
|
2360
4197
|
lines.push(` - ${file}`);
|
|
2361
4198
|
}
|
|
4199
|
+
if (result.lateAbort) {
|
|
4200
|
+
lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
|
|
4201
|
+
}
|
|
2362
4202
|
} else {
|
|
2363
4203
|
lines.push("Failed to apply some changes:");
|
|
2364
4204
|
for (const err of result.errors) {
|
|
@@ -2372,6 +4212,7 @@ function formatApplyResult(result) {
|
|
|
2372
4212
|
`);
|
|
2373
4213
|
}
|
|
2374
4214
|
var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
4215
|
+
var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
|
|
2375
4216
|
function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
2376
4217
|
const files = [];
|
|
2377
4218
|
function walk(currentDir) {
|
|
@@ -2379,17 +4220,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2379
4220
|
return;
|
|
2380
4221
|
let entries = [];
|
|
2381
4222
|
try {
|
|
2382
|
-
entries =
|
|
4223
|
+
entries = readdirSync3(currentDir);
|
|
2383
4224
|
} catch {
|
|
2384
4225
|
return;
|
|
2385
4226
|
}
|
|
2386
4227
|
for (const entry of entries) {
|
|
2387
4228
|
if (files.length >= maxFiles)
|
|
2388
4229
|
return;
|
|
2389
|
-
const fullPath =
|
|
4230
|
+
const fullPath = join5(currentDir, entry);
|
|
2390
4231
|
let stat;
|
|
2391
4232
|
try {
|
|
2392
|
-
stat =
|
|
4233
|
+
stat = lstatSync5(fullPath);
|
|
2393
4234
|
} catch {
|
|
2394
4235
|
continue;
|
|
2395
4236
|
}
|
|
@@ -2407,52 +4248,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2407
4248
|
walk(dir);
|
|
2408
4249
|
return files;
|
|
2409
4250
|
}
|
|
2410
|
-
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
|
|
4251
|
+
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
|
|
2411
4252
|
if (!extension.startsWith(".")) {
|
|
2412
4253
|
throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
|
|
2413
4254
|
}
|
|
2414
|
-
const absDir =
|
|
2415
|
-
if (!
|
|
4255
|
+
const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
|
|
4256
|
+
if (!existsSync10(absDir)) {
|
|
2416
4257
|
throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
|
|
2417
4258
|
}
|
|
2418
|
-
const serverResult = findServerForExtension(extension);
|
|
4259
|
+
const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
|
|
2419
4260
|
if (serverResult.status !== "found") {
|
|
2420
4261
|
throw new LspServerLookupError(formatServerLookupError(serverResult));
|
|
2421
4262
|
}
|
|
2422
4263
|
const server2 = serverResult.server;
|
|
2423
|
-
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
|
|
4264
|
+
const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
|
|
2424
4265
|
const wasCapped = allFiles.length > maxFiles;
|
|
2425
4266
|
const filesToProcess = allFiles.slice(0, maxFiles);
|
|
2426
4267
|
if (filesToProcess.length === 0) {
|
|
2427
|
-
|
|
4268
|
+
const output = [
|
|
2428
4269
|
`Directory: ${absDir}`,
|
|
2429
4270
|
`Extension: ${extension}`,
|
|
2430
4271
|
"Files scanned: 0",
|
|
2431
4272
|
`No files found with extension "${extension}".`
|
|
2432
4273
|
].join(`
|
|
2433
4274
|
`);
|
|
4275
|
+
return { output, totalDiagnostics: 0, fileFailures: [] };
|
|
2434
4276
|
}
|
|
2435
|
-
const root = findWorkspaceRoot(absDir);
|
|
2436
|
-
const manager = getLspManager();
|
|
4277
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
|
|
4278
|
+
const manager = options.manager ?? getLspManager();
|
|
2437
4279
|
const allDiagnostics = [];
|
|
2438
4280
|
const fileErrors = [];
|
|
4281
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
|
|
4282
|
+
options.signal?.throwIfAborted();
|
|
2439
4283
|
const client = await manager.getClient(root, server2);
|
|
2440
4284
|
try {
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
file,
|
|
2452
|
-
|
|
2453
|
-
|
|
4285
|
+
let nextIndex = 0;
|
|
4286
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
|
|
4287
|
+
for (;; ) {
|
|
4288
|
+
if (options.signal?.aborted)
|
|
4289
|
+
return;
|
|
4290
|
+
const file = filesToProcess[nextIndex];
|
|
4291
|
+
nextIndex += 1;
|
|
4292
|
+
if (file === undefined)
|
|
4293
|
+
return;
|
|
4294
|
+
try {
|
|
4295
|
+
const result = await client.diagnostics(file, options.signal);
|
|
4296
|
+
const filtered = filterDiagnosticsBySeverity(result.items, severity);
|
|
4297
|
+
allDiagnostics.push(...filtered.map((diagnostic) => ({
|
|
4298
|
+
filePath: file,
|
|
4299
|
+
diagnostic
|
|
4300
|
+
})));
|
|
4301
|
+
} catch (e) {
|
|
4302
|
+
fileErrors.push({
|
|
4303
|
+
file,
|
|
4304
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4305
|
+
});
|
|
4306
|
+
}
|
|
2454
4307
|
}
|
|
2455
|
-
}
|
|
4308
|
+
});
|
|
4309
|
+
await Promise.all(workers);
|
|
2456
4310
|
} finally {
|
|
2457
4311
|
manager.releaseClient(root, server2.id);
|
|
2458
4312
|
}
|
|
@@ -2480,8 +4334,8 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
|
|
|
2480
4334
|
lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
2481
4335
|
}
|
|
2482
4336
|
}
|
|
2483
|
-
return lines.join(`
|
|
2484
|
-
`);
|
|
4337
|
+
return { output: lines.join(`
|
|
4338
|
+
`), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
|
|
2485
4339
|
}
|
|
2486
4340
|
var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
2487
4341
|
var MAX_SCAN_ENTRIES = 500;
|
|
@@ -2493,17 +4347,17 @@ function inferExtensionFromDirectory(directory) {
|
|
|
2493
4347
|
return;
|
|
2494
4348
|
let entries;
|
|
2495
4349
|
try {
|
|
2496
|
-
entries =
|
|
4350
|
+
entries = readdirSync4(dir);
|
|
2497
4351
|
} catch {
|
|
2498
4352
|
return;
|
|
2499
4353
|
}
|
|
2500
4354
|
for (const entry of entries) {
|
|
2501
4355
|
if (scanned >= MAX_SCAN_ENTRIES)
|
|
2502
4356
|
return;
|
|
2503
|
-
const fullPath =
|
|
4357
|
+
const fullPath = join6(dir, entry);
|
|
2504
4358
|
let stat;
|
|
2505
4359
|
try {
|
|
2506
|
-
stat =
|
|
4360
|
+
stat = lstatSync6(fullPath);
|
|
2507
4361
|
} catch {
|
|
2508
4362
|
continue;
|
|
2509
4363
|
}
|
|
@@ -2574,10 +4428,45 @@ function missingDependencyResult(error, details) {
|
|
|
2574
4428
|
details: {
|
|
2575
4429
|
...details,
|
|
2576
4430
|
error: message,
|
|
2577
|
-
errorKind: "missing_dependency"
|
|
4431
|
+
errorKind: "missing_dependency",
|
|
4432
|
+
...availabilityDetails(error)
|
|
2578
4433
|
}
|
|
2579
4434
|
};
|
|
2580
4435
|
}
|
|
4436
|
+
function availabilityDetails(error) {
|
|
4437
|
+
const availability = missingDependencyAvailability(error);
|
|
4438
|
+
return availability === null ? {} : { availability };
|
|
4439
|
+
}
|
|
4440
|
+
function missingDependencyAvailability(error) {
|
|
4441
|
+
if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
|
|
4442
|
+
return null;
|
|
4443
|
+
const context = lspRequestContext();
|
|
4444
|
+
switch (error.lookup.status) {
|
|
4445
|
+
case "not_configured":
|
|
4446
|
+
return {
|
|
4447
|
+
kind: "not_configured",
|
|
4448
|
+
extension: error.lookup.extension,
|
|
4449
|
+
availableServers: [...error.lookup.availableServers],
|
|
4450
|
+
projectConfigPaths: [...context.projectConfigPaths],
|
|
4451
|
+
userConfigPath: context.userConfigPath,
|
|
4452
|
+
installDecisionTool: context.capabilities.installDecisionTool
|
|
4453
|
+
};
|
|
4454
|
+
case "not_installed":
|
|
4455
|
+
return {
|
|
4456
|
+
kind: "not_installed",
|
|
4457
|
+
serverId: error.lookup.server.id,
|
|
4458
|
+
command: [...error.lookup.server.command],
|
|
4459
|
+
extensions: [...error.lookup.server.extensions],
|
|
4460
|
+
installHint: error.lookup.installHint,
|
|
4461
|
+
installDecisionTool: context.capabilities.installDecisionTool,
|
|
4462
|
+
installDecisionsPath: context.installDecisionsPath
|
|
4463
|
+
};
|
|
4464
|
+
default: {
|
|
4465
|
+
const exhaustive = error.lookup;
|
|
4466
|
+
return exhaustive;
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
2581
4470
|
function requireString(params, key) {
|
|
2582
4471
|
const value = params[key];
|
|
2583
4472
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -2628,7 +4517,7 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2628
4517
|
const filePath = requireString(params, "filePath");
|
|
2629
4518
|
const severity = severityFilter(params);
|
|
2630
4519
|
try {
|
|
2631
|
-
const absPath =
|
|
4520
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2632
4521
|
if (isDirectoryPath(absPath)) {
|
|
2633
4522
|
const extension = inferExtensionFromDirectory(absPath);
|
|
2634
4523
|
if (!extension) {
|
|
@@ -2645,18 +4534,33 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2645
4534
|
};
|
|
2646
4535
|
return text(message, details3);
|
|
2647
4536
|
}
|
|
2648
|
-
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
|
|
4537
|
+
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
|
|
2649
4538
|
const details2 = {
|
|
2650
4539
|
filePath,
|
|
2651
4540
|
severity,
|
|
2652
4541
|
mode: "directory",
|
|
2653
4542
|
diagnostics: [],
|
|
4543
|
+
totalDiagnostics: output2.totalDiagnostics,
|
|
4544
|
+
truncated: false,
|
|
4545
|
+
fileFailures: [...output2.fileFailures]
|
|
4546
|
+
};
|
|
4547
|
+
return text(output2.output, details2);
|
|
4548
|
+
}
|
|
4549
|
+
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
|
|
4550
|
+
if (result.transientError) {
|
|
4551
|
+
const message = result.transientError.message;
|
|
4552
|
+
const details2 = {
|
|
4553
|
+
filePath,
|
|
4554
|
+
severity,
|
|
4555
|
+
mode: "file",
|
|
4556
|
+
diagnostics: [],
|
|
2654
4557
|
totalDiagnostics: 0,
|
|
2655
|
-
truncated: false
|
|
4558
|
+
truncated: false,
|
|
4559
|
+
error: message,
|
|
4560
|
+
errorKind: result.transientError.kind
|
|
2656
4561
|
};
|
|
2657
|
-
return text(
|
|
4562
|
+
return text(message, details2, true);
|
|
2658
4563
|
}
|
|
2659
|
-
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
|
|
2660
4564
|
const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
|
|
2661
4565
|
const total = diagnostics.length;
|
|
2662
4566
|
const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
|
|
@@ -2714,7 +4618,7 @@ async function executeLspGotoDefinition(params, signal) {
|
|
|
2714
4618
|
const line = requireNumber(params, "line");
|
|
2715
4619
|
const character = requireNumber(params, "character");
|
|
2716
4620
|
try {
|
|
2717
|
-
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
|
|
4621
|
+
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
|
|
2718
4622
|
const locations = !result ? [] : Array.isArray(result) ? result : [result];
|
|
2719
4623
|
const details = { filePath, line, character, locations };
|
|
2720
4624
|
if (locations.length === 0)
|
|
@@ -2733,216 +4637,53 @@ async function executeLspGotoDefinition(params, signal) {
|
|
|
2733
4637
|
throw error;
|
|
2734
4638
|
}
|
|
2735
4639
|
}
|
|
2736
|
-
async function executeLspFindReferences(params, signal) {
|
|
2737
|
-
const filePath = requireString(params, "filePath");
|
|
2738
|
-
const line = requireNumber(params, "line");
|
|
2739
|
-
const character = requireNumber(params, "character");
|
|
2740
|
-
const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
|
|
2741
|
-
try {
|
|
2742
|
-
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
|
|
2743
|
-
const references = Array.isArray(result) ? result : [];
|
|
2744
|
-
const total = references.length;
|
|
2745
|
-
const truncated = total > DEFAULT_MAX_REFERENCES;
|
|
2746
|
-
const limited = truncated ? references.slice(0, DEFAULT_MAX_REFERENCES) : references;
|
|
2747
|
-
const details = {
|
|
2748
|
-
filePath,
|
|
2749
|
-
line,
|
|
2750
|
-
character,
|
|
2751
|
-
references,
|
|
2752
|
-
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
|
-
}
|
|
4640
|
+
async function executeLspFindReferences(params, signal) {
|
|
4641
|
+
const filePath = requireString(params, "filePath");
|
|
4642
|
+
const line = requireNumber(params, "line");
|
|
4643
|
+
const character = requireNumber(params, "character");
|
|
4644
|
+
const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
|
|
4645
|
+
try {
|
|
4646
|
+
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
|
|
4647
|
+
const references = Array.isArray(result) ? result : [];
|
|
4648
|
+
const total = references.length;
|
|
4649
|
+
const truncated = total > DEFAULT_MAX_REFERENCES;
|
|
4650
|
+
const limited = truncated ? references.slice(0, DEFAULT_MAX_REFERENCES) : references;
|
|
4651
|
+
const details = {
|
|
4652
|
+
filePath,
|
|
4653
|
+
line,
|
|
4654
|
+
character,
|
|
4655
|
+
references,
|
|
4656
|
+
totalReferences: total,
|
|
4657
|
+
truncated
|
|
4658
|
+
};
|
|
4659
|
+
if (total === 0)
|
|
4660
|
+
return text("No references found", details);
|
|
4661
|
+
const output = [
|
|
4662
|
+
...truncated ? [`Found ${total} references (showing first ${DEFAULT_MAX_REFERENCES}):`] : [],
|
|
4663
|
+
...limited.map(formatLocation)
|
|
4664
|
+
].join(`
|
|
4665
|
+
`);
|
|
4666
|
+
return text(output, details);
|
|
4667
|
+
} catch (error) {
|
|
4668
|
+
const missingDependency = missingDependencyResult(error, {
|
|
4669
|
+
filePath,
|
|
4670
|
+
line,
|
|
4671
|
+
character,
|
|
4672
|
+
references: [],
|
|
4673
|
+
totalReferences: 0,
|
|
4674
|
+
truncated: false
|
|
4675
|
+
});
|
|
4676
|
+
if (missingDependency)
|
|
4677
|
+
return missingDependency;
|
|
4678
|
+
throw error;
|
|
2937
4679
|
}
|
|
2938
|
-
return result;
|
|
2939
4680
|
}
|
|
2940
4681
|
async function executeLspPrepareRename(params, signal) {
|
|
2941
4682
|
const filePath = requireString(params, "filePath");
|
|
2942
4683
|
const line = requireNumber(params, "line");
|
|
2943
4684
|
const character = requireNumber(params, "character");
|
|
2944
4685
|
try {
|
|
2945
|
-
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
|
|
4686
|
+
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
|
|
2946
4687
|
const details = { filePath, line, character, result };
|
|
2947
4688
|
return text(formatPrepareRenameResult(result), details);
|
|
2948
4689
|
} catch (error) {
|
|
@@ -2963,13 +4704,9 @@ async function executeLspRename(params, signal) {
|
|
|
2963
4704
|
const character = requireNumber(params, "character");
|
|
2964
4705
|
const newName = requireString(params, "newName");
|
|
2965
4706
|
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);
|
|
4707
|
+
const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
|
|
4708
|
+
const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
|
|
4709
|
+
return text(formatApplyResult(result.apply), details, !result.apply.success);
|
|
2973
4710
|
} catch (error) {
|
|
2974
4711
|
const missingDependency = missingDependencyResult(error, {
|
|
2975
4712
|
filePath,
|
|
@@ -3038,10 +4775,10 @@ async function executeLspSymbols(params, signal) {
|
|
|
3038
4775
|
errorKind: "missing_query"
|
|
3039
4776
|
});
|
|
3040
4777
|
}
|
|
3041
|
-
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
|
|
4778
|
+
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
|
|
3042
4779
|
return formatSymbolsResult(filePath, scope, symbols2, limit, query);
|
|
3043
4780
|
}
|
|
3044
|
-
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
|
|
4781
|
+
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
|
|
3045
4782
|
return formatSymbolsResult(filePath, scope, symbols, limit);
|
|
3046
4783
|
} catch (error) {
|
|
3047
4784
|
const query = optionalString(params, "query");
|
|
@@ -3195,35 +4932,8 @@ var LSP_MCP_TOOLS = [
|
|
|
3195
4932
|
}
|
|
3196
4933
|
];
|
|
3197
4934
|
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
4935
|
var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
3226
|
-
var
|
|
4936
|
+
var nextProxyRequestId = 1;
|
|
3227
4937
|
|
|
3228
4938
|
class DaemonRequestError extends Error {
|
|
3229
4939
|
requestWritten;
|
|
@@ -3233,44 +4943,59 @@ class DaemonRequestError extends Error {
|
|
|
3233
4943
|
this.requestWritten = requestWritten;
|
|
3234
4944
|
}
|
|
3235
4945
|
}
|
|
3236
|
-
|
|
4946
|
+
|
|
4947
|
+
class DaemonAuthenticationRejectedError extends DaemonRequestError {
|
|
4948
|
+
constructor() {
|
|
4949
|
+
super("daemon authentication failed before dispatch", true);
|
|
4950
|
+
this.name = "DaemonAuthenticationRejectedError";
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
|
|
4954
|
+
class DaemonRequestCancelledError extends DaemonRequestError {
|
|
4955
|
+
constructor(requestWritten) {
|
|
4956
|
+
super("daemon request cancelled", requestWritten);
|
|
4957
|
+
this.name = "DaemonRequestCancelledError";
|
|
4958
|
+
}
|
|
4959
|
+
}
|
|
4960
|
+
async function callToolViaDaemon(name, args, options) {
|
|
4961
|
+
const context = requireContext(options.context);
|
|
3237
4962
|
const paths = options.paths ?? daemonPaths();
|
|
3238
4963
|
const ensure = options.ensure ?? ensureDaemonRunning;
|
|
3239
4964
|
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
3240
|
-
const requestArgs = withContext(args,
|
|
4965
|
+
const requestArgs = withContext(args, context);
|
|
3241
4966
|
let lastError;
|
|
3242
|
-
|
|
4967
|
+
let authRefreshUsed = false;
|
|
4968
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
3243
4969
|
try {
|
|
3244
4970
|
await ensure(paths);
|
|
3245
|
-
|
|
4971
|
+
const token = readAuthToken(paths);
|
|
4972
|
+
if (!token)
|
|
4973
|
+
throw new DaemonRequestError("daemon auth token missing", false);
|
|
4974
|
+
const sendOptions = options.signal === undefined ? { timeoutMs } : { timeoutMs, signal: options.signal };
|
|
4975
|
+
return await sendToolCall(paths, token, name, requestArgs, sendOptions);
|
|
3246
4976
|
} catch (error) {
|
|
3247
4977
|
lastError = error;
|
|
3248
|
-
if (error instanceof
|
|
4978
|
+
if (error instanceof DaemonAuthenticationRejectedError && !authRefreshUsed) {
|
|
4979
|
+
authRefreshUsed = true;
|
|
4980
|
+
continue;
|
|
4981
|
+
}
|
|
4982
|
+
if (error instanceof DaemonRequestCancelledError)
|
|
4983
|
+
break;
|
|
4984
|
+
if (error instanceof DaemonRequestError && (error.requestWritten || !isRetryableTool(name)))
|
|
3249
4985
|
break;
|
|
3250
4986
|
}
|
|
3251
4987
|
}
|
|
3252
4988
|
return daemonUnreachableResult(paths, lastError);
|
|
3253
4989
|
}
|
|
3254
|
-
function callDiagnosticsViaDaemon(filePath, options
|
|
4990
|
+
function callDiagnosticsViaDaemon(filePath, options) {
|
|
3255
4991
|
return callToolViaDaemon("diagnostics", { filePath, severity: "error" }, options);
|
|
3256
4992
|
}
|
|
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 };
|
|
4993
|
+
function requireContext(context) {
|
|
4994
|
+
if (!context)
|
|
4995
|
+
throw new DaemonRequestError("daemon tool context is required", false);
|
|
4996
|
+
return parseLspRequestContext(context);
|
|
3270
4997
|
}
|
|
3271
4998
|
function withContext(args, context) {
|
|
3272
|
-
if (!context || context.cwd === undefined && context.env === undefined)
|
|
3273
|
-
return args;
|
|
3274
4999
|
return { ...args, [CONTEXT_KEY]: context };
|
|
3275
5000
|
}
|
|
3276
5001
|
function daemonUnreachableResult(paths, error) {
|
|
@@ -3284,39 +5009,84 @@ function daemonUnreachableResult(paths, error) {
|
|
|
3284
5009
|
`);
|
|
3285
5010
|
return { content: [{ type: "text", text: text2 }], isError: true };
|
|
3286
5011
|
}
|
|
3287
|
-
function sendToolCall(
|
|
3288
|
-
return new Promise((
|
|
3289
|
-
const socket = connect2(
|
|
5012
|
+
function sendToolCall(paths, token, name, args, options) {
|
|
5013
|
+
return new Promise((resolve9, reject) => {
|
|
5014
|
+
const socket = connect2(paths.socket);
|
|
5015
|
+
const requestId = allocateProxyRequestId();
|
|
3290
5016
|
let settled = false;
|
|
3291
5017
|
let requestWritten = false;
|
|
5018
|
+
let cancelAfterWrite = false;
|
|
5019
|
+
const cancelPayload = () => encodeJsonLine({
|
|
5020
|
+
jsonrpc: "2.0",
|
|
5021
|
+
method: "$/cancelRequest",
|
|
5022
|
+
params: { _omo: authEnvelope(token), id: requestId }
|
|
5023
|
+
});
|
|
3292
5024
|
const finish = (run) => {
|
|
3293
5025
|
if (settled)
|
|
3294
5026
|
return;
|
|
3295
5027
|
settled = true;
|
|
3296
5028
|
clearTimeout(timer);
|
|
3297
|
-
|
|
5029
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
5030
|
+
destroyAfterCancel();
|
|
3298
5031
|
run();
|
|
3299
5032
|
};
|
|
3300
|
-
const
|
|
5033
|
+
const sendCancel = () => {
|
|
5034
|
+
if (!requestWritten) {
|
|
5035
|
+
cancelAfterWrite = true;
|
|
5036
|
+
return;
|
|
5037
|
+
}
|
|
5038
|
+
if (!socket.writable)
|
|
5039
|
+
return;
|
|
5040
|
+
socket.write(cancelPayload());
|
|
5041
|
+
};
|
|
5042
|
+
const destroyAfterCancel = () => {
|
|
5043
|
+
socket.destroy();
|
|
5044
|
+
};
|
|
5045
|
+
const onAbort = () => {
|
|
5046
|
+
sendCancel();
|
|
5047
|
+
finish(() => reject(new DaemonRequestCancelledError(requestWritten)));
|
|
5048
|
+
};
|
|
5049
|
+
const timer = setTimeout(() => {
|
|
5050
|
+
sendCancel();
|
|
5051
|
+
finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten)));
|
|
5052
|
+
}, options.timeoutMs);
|
|
3301
5053
|
timer.unref();
|
|
5054
|
+
if (options.signal?.aborted) {
|
|
5055
|
+
onAbort();
|
|
5056
|
+
return;
|
|
5057
|
+
}
|
|
5058
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3302
5059
|
const decoder = createLineDecoder((message) => {
|
|
3303
|
-
|
|
5060
|
+
if (isAuthErrorResponse(message)) {
|
|
5061
|
+
finish(() => reject(new DaemonAuthenticationRejectedError));
|
|
5062
|
+
return;
|
|
5063
|
+
}
|
|
5064
|
+
const result = toToolResult(message, requestId);
|
|
3304
5065
|
if (result)
|
|
3305
|
-
finish(() =>
|
|
5066
|
+
finish(() => resolve9(result));
|
|
3306
5067
|
else
|
|
3307
5068
|
finish(() => reject(new DaemonRequestError("invalid daemon response", requestWritten)));
|
|
3308
5069
|
});
|
|
3309
5070
|
socket.once("connect", () => {
|
|
3310
|
-
|
|
3311
|
-
|
|
5071
|
+
const payload = encodeJsonLine({
|
|
5072
|
+
jsonrpc: "2.0",
|
|
5073
|
+
id: requestId,
|
|
5074
|
+
method: "tools/call",
|
|
5075
|
+
params: { _omo: authEnvelope(token), name, arguments: args }
|
|
5076
|
+
});
|
|
5077
|
+
socket.write(payload, () => {
|
|
5078
|
+
requestWritten = true;
|
|
5079
|
+
if (cancelAfterWrite && socket.writable)
|
|
5080
|
+
socket.write(cancelPayload());
|
|
5081
|
+
});
|
|
3312
5082
|
});
|
|
3313
5083
|
socket.on("data", (chunk) => decoder.push(chunk));
|
|
3314
5084
|
socket.once("error", (error) => finish(() => reject(new DaemonRequestError(error.message, requestWritten))));
|
|
3315
5085
|
socket.once("close", () => finish(() => reject(new DaemonRequestError("daemon connection closed", requestWritten))));
|
|
3316
5086
|
});
|
|
3317
5087
|
}
|
|
3318
|
-
function toToolResult(message) {
|
|
3319
|
-
if (!isPlainRecord(message) || message["id"] !==
|
|
5088
|
+
function toToolResult(message, requestId) {
|
|
5089
|
+
if (!isPlainRecord(message) || message["id"] !== requestId)
|
|
3320
5090
|
return null;
|
|
3321
5091
|
const result = message["result"];
|
|
3322
5092
|
if (!isPlainRecord(result) || !Array.isArray(result["content"]))
|
|
@@ -3327,167 +5097,436 @@ function toToolResult(message) {
|
|
|
3327
5097
|
details: result["details"]
|
|
3328
5098
|
};
|
|
3329
5099
|
}
|
|
5100
|
+
function allocateProxyRequestId() {
|
|
5101
|
+
const id = nextProxyRequestId;
|
|
5102
|
+
nextProxyRequestId += 1;
|
|
5103
|
+
if (nextProxyRequestId > Number.MAX_SAFE_INTEGER)
|
|
5104
|
+
nextProxyRequestId = 1;
|
|
5105
|
+
return id;
|
|
5106
|
+
}
|
|
5107
|
+
function isRetryableTool(name) {
|
|
5108
|
+
return name !== "rename" && name !== "lsp_rename";
|
|
5109
|
+
}
|
|
3330
5110
|
function errorText(error) {
|
|
3331
5111
|
return error instanceof Error ? error.message : String(error);
|
|
3332
5112
|
}
|
|
5113
|
+
var OMO_LSP_DAEMON_CLI2 = "OMO_LSP_DAEMON_CLI";
|
|
5114
|
+
var OMO_LSP_DAEMON_VERSION2 = "OMO_LSP_DAEMON_VERSION";
|
|
5115
|
+
var DAEMON_VERSION_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
5116
|
+
|
|
5117
|
+
class InvalidRuntimeOverrideError2 extends Error {
|
|
5118
|
+
code = "invalid_runtime_override";
|
|
5119
|
+
reason;
|
|
5120
|
+
constructor(reason, message) {
|
|
5121
|
+
super(message);
|
|
5122
|
+
this.name = "InvalidRuntimeOverrideError";
|
|
5123
|
+
this.reason = reason;
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
function validateDaemonVersion2(version) {
|
|
5127
|
+
if (!DAEMON_VERSION_PATTERN2.test(version)) {
|
|
5128
|
+
throw new Error("LSP daemon version must match [A-Za-z0-9][A-Za-z0-9._+-]{0,127}");
|
|
5129
|
+
}
|
|
5130
|
+
return version;
|
|
5131
|
+
}
|
|
5132
|
+
function resolveDaemonRuntime2(env, defaults) {
|
|
5133
|
+
const cliOverride = env[OMO_LSP_DAEMON_CLI2];
|
|
5134
|
+
const versionOverride = env[OMO_LSP_DAEMON_VERSION2];
|
|
5135
|
+
const hasCliOverride = cliOverride !== undefined;
|
|
5136
|
+
const hasVersionOverride = versionOverride !== undefined;
|
|
5137
|
+
if (hasCliOverride !== hasVersionOverride) {
|
|
5138
|
+
throw new InvalidRuntimeOverrideError2("paired_values_required", `${OMO_LSP_DAEMON_CLI2} and ${OMO_LSP_DAEMON_VERSION2} must be set together`);
|
|
5139
|
+
}
|
|
5140
|
+
if (!hasCliOverride || !hasVersionOverride) {
|
|
5141
|
+
if (!isAbsolute4(defaults.cliPath)) {
|
|
5142
|
+
throw new InvalidRuntimeOverrideError2("packaged_cli_must_be_absolute", "Packaged LSP daemon CLI path must be absolute");
|
|
5143
|
+
}
|
|
5144
|
+
return { cliPath: defaults.cliPath, version: validateDaemonVersion2(defaults.version) };
|
|
5145
|
+
}
|
|
5146
|
+
if (!isAbsolute4(cliOverride)) {
|
|
5147
|
+
throw new InvalidRuntimeOverrideError2("cli_must_be_absolute", `${OMO_LSP_DAEMON_CLI2} must be an absolute path to an existing regular file`);
|
|
5148
|
+
}
|
|
5149
|
+
const stat = statSync5(cliOverride, { throwIfNoEntry: false });
|
|
5150
|
+
if (!stat) {
|
|
5151
|
+
throw new InvalidRuntimeOverrideError2("cli_not_found", `${OMO_LSP_DAEMON_CLI2} points to a missing file`);
|
|
5152
|
+
}
|
|
5153
|
+
if (!stat.isFile()) {
|
|
5154
|
+
throw new InvalidRuntimeOverrideError2("cli_not_file", `${OMO_LSP_DAEMON_CLI2} must point to a regular file`);
|
|
5155
|
+
}
|
|
5156
|
+
return { cliPath: cliOverride, version: validateDaemonVersion2(versionOverride) };
|
|
5157
|
+
}
|
|
5158
|
+
function callDiagnosticsViaDaemon2(filePath, options) {
|
|
5159
|
+
return callDiagnosticsViaDaemon(filePath, options);
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
// ../../../../lsp-core/src/post-edit/orchestration.ts
|
|
5163
|
+
import { extname as extname2 } from "node:path";
|
|
5164
|
+
var DEFAULT_MAX_CONCURRENCY = 4;
|
|
5165
|
+
var CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
|
|
5166
|
+
function createPostEditNotConfiguredCache() {
|
|
5167
|
+
return { notConfiguredExtensions: new Set };
|
|
5168
|
+
}
|
|
5169
|
+
async function collectPostEditDiagnostics(input) {
|
|
5170
|
+
const cache = input.cache === undefined ? createPostEditNotConfiguredCache() : input.cache;
|
|
5171
|
+
const filePaths = firstSeenDiagnosticTargets(input.filePaths, cache);
|
|
5172
|
+
const results = await runBoundedDiagnostics(filePaths, input.runDiagnostics, input.maxConcurrency);
|
|
5173
|
+
const blocks = [];
|
|
5174
|
+
const observations = [];
|
|
5175
|
+
for (const result of results) {
|
|
5176
|
+
const classification = classifyDiagnostics(result.outcome);
|
|
5177
|
+
switch (classification.kind) {
|
|
5178
|
+
case "clean":
|
|
5179
|
+
observations.push({ filePath: result.filePath, kind: "clean" });
|
|
5180
|
+
break;
|
|
5181
|
+
case "not_configured":
|
|
5182
|
+
cache.notConfiguredExtensions.add(classification.extension);
|
|
5183
|
+
observations.push({ filePath: result.filePath, kind: "not_configured" });
|
|
5184
|
+
break;
|
|
5185
|
+
case "block":
|
|
5186
|
+
blocks.push({ filePath: result.filePath, diagnostics: classification.diagnostics });
|
|
5187
|
+
observations.push({ filePath: result.filePath, kind: "block" });
|
|
5188
|
+
break;
|
|
5189
|
+
default: {
|
|
5190
|
+
const exhaustive = classification;
|
|
5191
|
+
return exhaustive;
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
return { blocks, observations };
|
|
5196
|
+
}
|
|
5197
|
+
function firstSeenDiagnosticTargets(filePaths, cache) {
|
|
5198
|
+
const seen = new Set;
|
|
5199
|
+
const targets = [];
|
|
5200
|
+
for (const filePath of filePaths) {
|
|
5201
|
+
if (filePath.length === 0 || seen.has(filePath))
|
|
5202
|
+
continue;
|
|
5203
|
+
seen.add(filePath);
|
|
5204
|
+
const extension = extensionKey(filePath);
|
|
5205
|
+
if (extension !== undefined && cache.notConfiguredExtensions.has(extension))
|
|
5206
|
+
continue;
|
|
5207
|
+
targets.push({ index: targets.length, filePath });
|
|
5208
|
+
}
|
|
5209
|
+
return targets;
|
|
5210
|
+
}
|
|
5211
|
+
async function runBoundedDiagnostics(filePaths, runDiagnostics, maxConcurrency) {
|
|
5212
|
+
const results = [];
|
|
5213
|
+
const workerCount = Math.min(Math.max(1, maxConcurrency ?? DEFAULT_MAX_CONCURRENCY), filePaths.length);
|
|
5214
|
+
let nextIndex = 0;
|
|
5215
|
+
async function worker() {
|
|
5216
|
+
for (;; ) {
|
|
5217
|
+
const target = filePaths[nextIndex];
|
|
5218
|
+
nextIndex += 1;
|
|
5219
|
+
if (target === undefined)
|
|
5220
|
+
return;
|
|
5221
|
+
results[target.index] = {
|
|
5222
|
+
index: target.index,
|
|
5223
|
+
filePath: target.filePath,
|
|
5224
|
+
outcome: await collectFileDiagnostics(target.filePath, runDiagnostics)
|
|
5225
|
+
};
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
5229
|
+
return results.filter((result) => result !== undefined);
|
|
5230
|
+
}
|
|
5231
|
+
async function collectFileDiagnostics(filePath, runDiagnostics) {
|
|
5232
|
+
try {
|
|
5233
|
+
return normalizeDiagnosticsOutcome(await runDiagnostics(filePath));
|
|
5234
|
+
} catch (error) {
|
|
5235
|
+
if (error instanceof Error)
|
|
5236
|
+
return formatDiagnosticsError(error);
|
|
5237
|
+
return normalizeDiagnosticsText(String(error));
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
function normalizeDiagnosticsOutcome(outcome) {
|
|
5241
|
+
if (typeof outcome === "string")
|
|
5242
|
+
return normalizeDiagnosticsText(outcome);
|
|
5243
|
+
return outcome;
|
|
5244
|
+
}
|
|
5245
|
+
function normalizeDiagnosticsText(text2) {
|
|
5246
|
+
return text2.trim();
|
|
5247
|
+
}
|
|
5248
|
+
function formatDiagnosticsError(error) {
|
|
5249
|
+
const message = normalizeDiagnosticsText(error.message);
|
|
5250
|
+
return message.length > 0 ? message : normalizeDiagnosticsText(String(error));
|
|
5251
|
+
}
|
|
5252
|
+
function classifyDiagnostics(outcome) {
|
|
5253
|
+
if (typeof outcome !== "string") {
|
|
5254
|
+
return { kind: "not_configured", extension: outcome.extension };
|
|
5255
|
+
}
|
|
5256
|
+
const diagnostics = outcome;
|
|
5257
|
+
if (diagnostics.length === 0 || diagnostics === CLEAN_DIAGNOSTICS_TEXT)
|
|
5258
|
+
return { kind: "clean" };
|
|
5259
|
+
return { kind: "block", diagnostics };
|
|
5260
|
+
}
|
|
5261
|
+
function extensionKey(filePath) {
|
|
5262
|
+
const extension = extname2(filePath).toLowerCase();
|
|
5263
|
+
return extension.length === 0 ? undefined : extension;
|
|
5264
|
+
}
|
|
5265
|
+
// ../../../../lsp-core/src/request-context.ts
|
|
5266
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
5267
|
+
import { existsSync as existsSync11, realpathSync as realpathSync4, statSync as statSync6 } from "node:fs";
|
|
5268
|
+
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
5269
|
|
|
3334
|
-
|
|
3335
|
-
|
|
5270
|
+
class LspRequestContextParseError2 extends Error {
|
|
5271
|
+
code;
|
|
5272
|
+
name = "LspRequestContextParseError";
|
|
5273
|
+
constructor(code, message) {
|
|
5274
|
+
super(message);
|
|
5275
|
+
this.code = code;
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
var storage2 = new AsyncLocalStorage2;
|
|
5279
|
+
var CONTEXT_FIELDS2 = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
5280
|
+
var CAPABILITY_FIELDS2 = new Set(["installDecisionTool"]);
|
|
5281
|
+
function parseLspRequestContext2(value) {
|
|
5282
|
+
if (!isRecord7(value)) {
|
|
5283
|
+
throw new LspRequestContextParseError2("invalid_context", "LSP request context must be an object.");
|
|
5284
|
+
}
|
|
5285
|
+
rejectUnknownFields2(value, CONTEXT_FIELDS2, "context");
|
|
5286
|
+
const cwd = stringField2(value, "cwd");
|
|
5287
|
+
const projectConfigPaths = stringArrayField2(value, "projectConfigPaths");
|
|
5288
|
+
const userConfigPath = stringField2(value, "userConfigPath");
|
|
5289
|
+
const installDecisionsPath = stringField2(value, "installDecisionsPath");
|
|
5290
|
+
const capabilities = capabilitiesField2(value["capabilities"]);
|
|
5291
|
+
const canonical = canonicalCwd2(cwd);
|
|
5292
|
+
for (const path2 of projectConfigPaths) {
|
|
5293
|
+
requireAbsolutePath2(path2, "projectConfigPaths");
|
|
5294
|
+
const projectPath = canonicalizeExistingOrNearestAncestor2(path2);
|
|
5295
|
+
if (!isPathInside2(canonical, projectPath)) {
|
|
5296
|
+
throw new LspRequestContextParseError2("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path2}`);
|
|
5297
|
+
}
|
|
5298
|
+
}
|
|
5299
|
+
requireAbsolutePath2(userConfigPath, "userConfigPath");
|
|
5300
|
+
requireAbsolutePath2(installDecisionsPath, "installDecisionsPath");
|
|
5301
|
+
return {
|
|
5302
|
+
cwd: canonical,
|
|
5303
|
+
projectConfigPaths: projectConfigPaths.map((path2) => canonicalizeExistingOrNearestAncestor2(path2)),
|
|
5304
|
+
userConfigPath,
|
|
5305
|
+
installDecisionsPath,
|
|
5306
|
+
capabilities
|
|
5307
|
+
};
|
|
5308
|
+
}
|
|
5309
|
+
function canonicalCwd2(cwd) {
|
|
5310
|
+
const resolved = resolve9(cwd);
|
|
5311
|
+
if (!existsSync11(resolved) || !statSync6(resolved).isDirectory()) {
|
|
5312
|
+
throw new LspRequestContextParseError2("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
5313
|
+
}
|
|
5314
|
+
return realpathSync4(resolved);
|
|
5315
|
+
}
|
|
5316
|
+
function canonicalizeExistingOrNearestAncestor2(path2) {
|
|
5317
|
+
let current = resolve9(path2);
|
|
5318
|
+
const suffix = [];
|
|
5319
|
+
while (true) {
|
|
5320
|
+
try {
|
|
5321
|
+
const existing = realpathSync4(current);
|
|
5322
|
+
return suffix.length === 0 ? existing : join7(existing, ...suffix);
|
|
5323
|
+
} catch (error) {
|
|
5324
|
+
if (!isMissingPathError2(error))
|
|
5325
|
+
throw error;
|
|
5326
|
+
const parent = dirname2(current);
|
|
5327
|
+
if (parent === current)
|
|
5328
|
+
throw error;
|
|
5329
|
+
suffix.unshift(basename3(current));
|
|
5330
|
+
current = parent;
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
function capabilitiesField2(value) {
|
|
5335
|
+
if (!isRecord7(value)) {
|
|
5336
|
+
throw new LspRequestContextParseError2("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
5337
|
+
}
|
|
5338
|
+
rejectUnknownFields2(value, CAPABILITY_FIELDS2, "capabilities");
|
|
5339
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
5340
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
5341
|
+
throw new LspRequestContextParseError2("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
|
|
5342
|
+
}
|
|
5343
|
+
return { installDecisionTool };
|
|
5344
|
+
}
|
|
5345
|
+
function stringField2(value, field) {
|
|
5346
|
+
const fieldValue = value[field];
|
|
5347
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
5348
|
+
throw new LspRequestContextParseError2("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
5349
|
+
}
|
|
5350
|
+
return fieldValue;
|
|
5351
|
+
}
|
|
5352
|
+
function stringArrayField2(value, field) {
|
|
5353
|
+
const fieldValue = value[field];
|
|
5354
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
5355
|
+
throw new LspRequestContextParseError2("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
|
|
5356
|
+
}
|
|
5357
|
+
return fieldValue;
|
|
5358
|
+
}
|
|
5359
|
+
function requireAbsolutePath2(path2, field) {
|
|
5360
|
+
if (!isAbsolute5(path2)) {
|
|
5361
|
+
throw new LspRequestContextParseError2("relative_path", `LSP request context.${field} must be absolute: ${path2}`);
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
function isPathInside2(parent, child) {
|
|
5365
|
+
const childPath = resolve9(child);
|
|
5366
|
+
const relativePath = relative5(parent, childPath);
|
|
5367
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute5(relativePath);
|
|
5368
|
+
}
|
|
5369
|
+
function isMissingPathError2(error) {
|
|
5370
|
+
const code = errorCode2(error);
|
|
5371
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
5372
|
+
}
|
|
5373
|
+
function rejectUnknownFields2(value, allowed, scope) {
|
|
5374
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
5375
|
+
if (unknown.length > 0) {
|
|
5376
|
+
throw new LspRequestContextParseError2("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
function isRecord7(value) {
|
|
5380
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5381
|
+
}
|
|
5382
|
+
function errorCode2(error) {
|
|
5383
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
5384
|
+
return;
|
|
5385
|
+
const code = Reflect.get(error, "code");
|
|
5386
|
+
return typeof code === "string" ? code : undefined;
|
|
5387
|
+
}
|
|
3336
5388
|
|
|
3337
|
-
//
|
|
3338
|
-
import { existsSync as
|
|
5389
|
+
// src/daemon-cli-path.ts
|
|
5390
|
+
import { existsSync as existsSync13, readFileSync as readFileSync6 } from "node:fs";
|
|
3339
5391
|
import { createRequire as createRequire2 } from "node:module";
|
|
3340
|
-
import { dirname as
|
|
5392
|
+
import { dirname as dirname9, join as join9 } from "node:path";
|
|
3341
5393
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3342
5394
|
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";
|
|
5395
|
+
var PACKAGE_LSP_DAEMON_CLI = "@code-yeongyu/lsp-daemon/cli";
|
|
3346
5396
|
function ensureLspDaemonCliEnv(env = process.env) {
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
5397
|
+
const runtime = resolveDaemonRuntime2(env, resolveLspDaemonCli());
|
|
5398
|
+
env[OMO_LSP_DAEMON_CLI2] = runtime.cliPath;
|
|
5399
|
+
env[OMO_LSP_DAEMON_VERSION2] = runtime.version;
|
|
5400
|
+
}
|
|
5401
|
+
function resolveLspDaemonCliPath(env = process.env) {
|
|
5402
|
+
const runtime = resolveDaemonRuntime2(env, resolveLspDaemonCli());
|
|
5403
|
+
if (env[OMO_LSP_DAEMON_CLI2] === undefined)
|
|
5404
|
+
env[OMO_LSP_DAEMON_CLI2] = runtime.cliPath;
|
|
5405
|
+
if (env[OMO_LSP_DAEMON_VERSION2] === undefined)
|
|
5406
|
+
env[OMO_LSP_DAEMON_VERSION2] = runtime.version;
|
|
5407
|
+
return runtime.cliPath;
|
|
3357
5408
|
}
|
|
3358
5409
|
function resolveLspDaemonCli() {
|
|
3359
5410
|
const packageCli = resolvePackageLspDaemonCliPath();
|
|
3360
5411
|
if (packageCli !== null)
|
|
3361
5412
|
return resolveConfiguredLspDaemonCli(packageCli);
|
|
3362
5413
|
const bundledCli = fileURLToPath4(new URL("../../lsp-daemon/dist/cli.js", import.meta.url));
|
|
3363
|
-
if (
|
|
5414
|
+
if (existsSync13(bundledCli))
|
|
3364
5415
|
return resolveConfiguredLspDaemonCli(bundledCli);
|
|
3365
5416
|
return resolveConfiguredLspDaemonCli(bundledCli);
|
|
3366
5417
|
}
|
|
3367
5418
|
function resolvePackageLspDaemonCliPath() {
|
|
3368
5419
|
try {
|
|
3369
5420
|
return requireFromHere2.resolve(PACKAGE_LSP_DAEMON_CLI);
|
|
3370
|
-
} catch {
|
|
5421
|
+
} catch (error) {
|
|
5422
|
+
if (!(error instanceof Error))
|
|
5423
|
+
throw error;
|
|
3371
5424
|
return null;
|
|
3372
5425
|
}
|
|
3373
5426
|
}
|
|
3374
5427
|
function resolveConfiguredLspDaemonCli(cliPath) {
|
|
5428
|
+
const version = readDaemonPackageVersion(cliPath);
|
|
5429
|
+
if (version === null) {
|
|
5430
|
+
throw new Error(`Unable to determine packaged LSP daemon version beside ${cliPath}`);
|
|
5431
|
+
}
|
|
3375
5432
|
return {
|
|
3376
5433
|
cliPath,
|
|
3377
|
-
version
|
|
5434
|
+
version
|
|
3378
5435
|
};
|
|
3379
5436
|
}
|
|
3380
5437
|
function readDaemonPackageVersion(cliPath) {
|
|
3381
5438
|
try {
|
|
3382
|
-
const parsed = JSON.parse(readFileSync6(join9(
|
|
3383
|
-
if (
|
|
5439
|
+
const parsed = JSON.parse(readFileSync6(join9(dirname9(cliPath), "package.json"), "utf8"));
|
|
5440
|
+
if (isRecord9(parsed) && typeof parsed["version"] === "string" && parsed["version"].length > 0) {
|
|
3384
5441
|
return parsed["version"];
|
|
3385
5442
|
}
|
|
3386
|
-
} catch {
|
|
5443
|
+
} catch (error) {
|
|
5444
|
+
if (!(error instanceof Error))
|
|
5445
|
+
throw error;
|
|
5446
|
+
}
|
|
3387
5447
|
return null;
|
|
3388
5448
|
}
|
|
3389
|
-
function
|
|
5449
|
+
function isRecord9(value) {
|
|
3390
5450
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3391
5451
|
}
|
|
3392
5452
|
|
|
3393
|
-
//
|
|
5453
|
+
// src/lsp-session-state.ts
|
|
3394
5454
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3395
|
-
import { homedir
|
|
3396
|
-
import { dirname as
|
|
5455
|
+
import { homedir } from "node:os";
|
|
5456
|
+
import { dirname as dirname11, join as join10 } from "node:path";
|
|
3397
5457
|
function sessionIdFrom(input) {
|
|
3398
5458
|
return typeof input.session_id === "string" && input.session_id.length > 0 ? input.session_id : undefined;
|
|
3399
5459
|
}
|
|
3400
|
-
function
|
|
5460
|
+
function readLspPostEditCache(sessionId) {
|
|
3401
5461
|
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;
|
|
5462
|
+
return { notConfiguredExtensions: new Set };
|
|
3410
5463
|
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() });
|
|
5464
|
+
return { notConfiguredExtensions: new Set(state.notConfiguredExtensions) };
|
|
3423
5465
|
}
|
|
3424
|
-
function
|
|
5466
|
+
function writeLspPostEditCache(sessionId, cache) {
|
|
3425
5467
|
if (sessionId === undefined)
|
|
3426
5468
|
return;
|
|
3427
|
-
const state = readSessionState(sessionStatePath(sessionId));
|
|
3428
|
-
if (state.unavailableExtensions.length === 0)
|
|
3429
|
-
return;
|
|
3430
5469
|
writeSessionState(sessionStatePath(sessionId), {
|
|
3431
|
-
|
|
3432
|
-
postCompactProbePending: true
|
|
5470
|
+
notConfiguredExtensions: [...cache.notConfiguredExtensions].sort()
|
|
3433
5471
|
});
|
|
3434
5472
|
}
|
|
3435
|
-
function
|
|
3436
|
-
|
|
3437
|
-
|
|
5473
|
+
function markLspSessionCompacted(sessionId) {
|
|
5474
|
+
if (sessionId === undefined)
|
|
5475
|
+
return;
|
|
5476
|
+
writeSessionState(sessionStatePath(sessionId), emptyState());
|
|
3438
5477
|
}
|
|
3439
5478
|
function isLspDaemonUnreachableDiagnostics(diagnostics) {
|
|
3440
5479
|
return diagnostics.includes("LSP daemon unreachable");
|
|
3441
5480
|
}
|
|
3442
5481
|
function sessionStatePath(sessionId) {
|
|
3443
|
-
const root = process.env["PLUGIN_DATA"] ??
|
|
3444
|
-
return
|
|
5482
|
+
const root = process.env["PLUGIN_DATA"] ?? join10(homedir(), ".codex", "codex-lsp");
|
|
5483
|
+
return join10(root, "sessions", `${safePathSegment(sessionId)}.json`);
|
|
3445
5484
|
}
|
|
3446
|
-
function readSessionState(
|
|
5485
|
+
function readSessionState(path2) {
|
|
3447
5486
|
try {
|
|
3448
|
-
const parsed = JSON.parse(readFileSync8(
|
|
3449
|
-
if (isLspSessionState(parsed))
|
|
3450
|
-
return parsed;
|
|
5487
|
+
const parsed = JSON.parse(readFileSync8(path2, "utf8"));
|
|
5488
|
+
if (isRecord10(parsed) && isLspSessionState(parsed))
|
|
5489
|
+
return normalizeSessionState(parsed);
|
|
3451
5490
|
return emptyState();
|
|
3452
5491
|
} catch (error) {
|
|
3453
|
-
if (error instanceof SyntaxError ||
|
|
5492
|
+
if (error instanceof SyntaxError || isRecord10(error) && error["code"] === "ENOENT")
|
|
3454
5493
|
return emptyState();
|
|
3455
5494
|
throw error;
|
|
3456
5495
|
}
|
|
3457
5496
|
}
|
|
3458
|
-
function writeSessionState(
|
|
3459
|
-
mkdirSync4(
|
|
3460
|
-
writeFileSync3(
|
|
5497
|
+
function writeSessionState(path2, state) {
|
|
5498
|
+
mkdirSync4(dirname11(path2), { recursive: true });
|
|
5499
|
+
writeFileSync3(path2, `${JSON.stringify(state)}
|
|
3461
5500
|
`);
|
|
3462
5501
|
}
|
|
3463
5502
|
function emptyState() {
|
|
3464
|
-
return {
|
|
3465
|
-
}
|
|
3466
|
-
function extensionKey(filePath) {
|
|
3467
|
-
const extension = extname2(filePath).toLowerCase();
|
|
3468
|
-
return extension.length === 0 ? undefined : extension;
|
|
5503
|
+
return { notConfiguredExtensions: [] };
|
|
3469
5504
|
}
|
|
3470
5505
|
function safePathSegment(value) {
|
|
3471
5506
|
return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown-session";
|
|
3472
5507
|
}
|
|
3473
5508
|
function isLspSessionState(value) {
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
const postCompactProbePending = value["postCompactProbePending"];
|
|
3477
|
-
return value["unavailableExtensions"].every((item) => typeof item === "string") && (postCompactProbePending === undefined || typeof postCompactProbePending === "boolean");
|
|
5509
|
+
const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
|
|
5510
|
+
return Array.isArray(notConfiguredExtensions) && notConfiguredExtensions.every((item) => typeof item === "string");
|
|
3478
5511
|
}
|
|
3479
|
-
function
|
|
5512
|
+
function normalizeSessionState(value) {
|
|
5513
|
+
const notConfiguredExtensions = value["notConfiguredExtensions"] ?? value["unavailableExtensions"];
|
|
5514
|
+
return {
|
|
5515
|
+
notConfiguredExtensions: Array.isArray(notConfiguredExtensions) ? notConfiguredExtensions.filter((item) => typeof item === "string").sort() : []
|
|
5516
|
+
};
|
|
5517
|
+
}
|
|
5518
|
+
function isRecord10(value) {
|
|
3480
5519
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3481
5520
|
}
|
|
3482
5521
|
|
|
3483
|
-
//
|
|
5522
|
+
// src/mutated-file-paths.ts
|
|
3484
5523
|
var MUTATION_TOOL_NAMES = new Set(["apply_patch", "write", "edit", "multiedit", "multi_edit"]);
|
|
3485
5524
|
function extractMutatedFilePaths(input) {
|
|
3486
5525
|
if (!isMutationTool(input.tool_name))
|
|
3487
5526
|
return [];
|
|
3488
5527
|
if (isFailedToolResponse(input.tool_response))
|
|
3489
5528
|
return [];
|
|
3490
|
-
const toolInput =
|
|
5529
|
+
const toolInput = isRecord11(input.tool_input) ? input.tool_input : {};
|
|
3491
5530
|
const paths = new Set;
|
|
3492
5531
|
addStringValue(paths, toolInput["path"]);
|
|
3493
5532
|
addStringValue(paths, toolInput["filePath"]);
|
|
@@ -3506,7 +5545,7 @@ function isMutationTool(value) {
|
|
|
3506
5545
|
return MUTATION_TOOL_NAMES.has(value.toLowerCase());
|
|
3507
5546
|
}
|
|
3508
5547
|
function isFailedToolResponse(value) {
|
|
3509
|
-
if (!
|
|
5548
|
+
if (!isRecord11(value))
|
|
3510
5549
|
return false;
|
|
3511
5550
|
return value["isError"] === true || value["is_error"] === true || value["error"] === true || value["status"] === "error";
|
|
3512
5551
|
}
|
|
@@ -3532,9 +5571,9 @@ function addPatchInput(paths, value) {
|
|
|
3532
5571
|
return;
|
|
3533
5572
|
for (const line of value.split(`
|
|
3534
5573
|
`)) {
|
|
3535
|
-
const
|
|
3536
|
-
if (
|
|
3537
|
-
paths.add(
|
|
5574
|
+
const path2 = extractPatchHeaderPath(line);
|
|
5575
|
+
if (path2 !== undefined)
|
|
5576
|
+
paths.add(path2);
|
|
3538
5577
|
}
|
|
3539
5578
|
}
|
|
3540
5579
|
function extractPatchHeaderPath(line) {
|
|
@@ -3549,7 +5588,7 @@ function addPatchFiles(paths, value) {
|
|
|
3549
5588
|
if (!Array.isArray(value))
|
|
3550
5589
|
return;
|
|
3551
5590
|
for (const item of value) {
|
|
3552
|
-
if (!
|
|
5591
|
+
if (!isRecord11(item))
|
|
3553
5592
|
continue;
|
|
3554
5593
|
addStringValue(paths, item["path"]);
|
|
3555
5594
|
addStringValue(paths, item["filePath"]);
|
|
@@ -3558,18 +5597,15 @@ function addPatchFiles(paths, value) {
|
|
|
3558
5597
|
addStringValue(paths, item["move_path"]);
|
|
3559
5598
|
}
|
|
3560
5599
|
}
|
|
3561
|
-
function
|
|
5600
|
+
function isRecord11(value) {
|
|
3562
5601
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3563
5602
|
}
|
|
3564
5603
|
|
|
3565
|
-
//
|
|
3566
|
-
var CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
|
|
3567
|
-
var UNSUPPORTED_EXTENSION_TEXT = "No LSP server configured for extension:";
|
|
5604
|
+
// src/codex-hook.ts
|
|
3568
5605
|
var DIAGNOSTIC_START_PATTERN = /(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/g;
|
|
3569
5606
|
var DIAGNOSTIC_CHUNK_PATTERN = /^(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/;
|
|
3570
5607
|
var DEFAULT_MAX_HOOK_FEEDBACK_CHARS = 8000;
|
|
3571
5608
|
var CONTEXT_PRESSURE_MAX_HOOK_FEEDBACK_CHARS = 1200;
|
|
3572
|
-
var MAX_CONCURRENT_DIAGNOSTICS = 4;
|
|
3573
5609
|
var CONTEXT_PRESSURE_MARKERS = [
|
|
3574
5610
|
"context compacted",
|
|
3575
5611
|
"context_length_exceeded",
|
|
@@ -3581,29 +5617,47 @@ var CONTEXT_PRESSURE_MARKERS = [
|
|
|
3581
5617
|
];
|
|
3582
5618
|
async function runLspDiagnosticsText(filePath) {
|
|
3583
5619
|
ensureLspDaemonCliEnv();
|
|
3584
|
-
const result = await
|
|
5620
|
+
const result = await callDiagnosticsViaDaemon2(filePath, { context: codexLspRequestContext() });
|
|
5621
|
+
return postEditOutcomeFromDaemonResult(result);
|
|
5622
|
+
}
|
|
5623
|
+
function postEditOutcomeFromDaemonResult(result) {
|
|
5624
|
+
const availability = notConfiguredAvailability(result.details);
|
|
5625
|
+
if (availability !== undefined)
|
|
5626
|
+
return { kind: "not_configured", extension: availability.extension };
|
|
3585
5627
|
return result.content.map((block) => block.text).join(`
|
|
3586
5628
|
`);
|
|
3587
5629
|
}
|
|
5630
|
+
function notConfiguredAvailability(details) {
|
|
5631
|
+
if (!isRecord12(details))
|
|
5632
|
+
return;
|
|
5633
|
+
const availability = details["availability"];
|
|
5634
|
+
if (!isRecord12(availability))
|
|
5635
|
+
return;
|
|
5636
|
+
if (availability["kind"] !== "not_configured")
|
|
5637
|
+
return;
|
|
5638
|
+
const extension = availability["extension"];
|
|
5639
|
+
return typeof extension === "string" && extension.length > 0 ? { extension } : undefined;
|
|
5640
|
+
}
|
|
5641
|
+
function codexLspRequestContext(env = process.env, cwd = process.cwd()) {
|
|
5642
|
+
const canonicalCwd3 = realpathSync6(resolve11(cwd));
|
|
5643
|
+
const codexHome = resolve11(env["CODEX_HOME"]?.trim() || join11(homedir3(), ".codex"));
|
|
5644
|
+
return parseLspRequestContext2({
|
|
5645
|
+
cwd: canonicalCwd3,
|
|
5646
|
+
projectConfigPaths: [join11(canonicalCwd3, ".codex", "lsp-client.json")],
|
|
5647
|
+
userConfigPath: join11(codexHome, "lsp-client.json"),
|
|
5648
|
+
installDecisionsPath: join11(codexHome, "lsp-install-decisions.json"),
|
|
5649
|
+
capabilities: { installDecisionTool: true }
|
|
5650
|
+
});
|
|
5651
|
+
}
|
|
3588
5652
|
async function runLspPostToolUseHook(input, runDiagnostics = runLspDiagnosticsText) {
|
|
3589
5653
|
const sessionId = sessionIdFrom(input);
|
|
3590
|
-
const filePaths = extractMutatedFilePaths(input)
|
|
5654
|
+
const filePaths = extractMutatedFilePaths(input);
|
|
3591
5655
|
if (filePaths.length === 0)
|
|
3592
5656
|
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);
|
|
5657
|
+
const cache = readLspPostEditCache(sessionId);
|
|
5658
|
+
const result = await collectPostEditDiagnostics({ filePaths, runDiagnostics, cache });
|
|
5659
|
+
writeLspPostEditCache(sessionId, cache);
|
|
5660
|
+
const blocks = result.blocks.filter(({ diagnostics }) => !isLspDaemonUnreachableDiagnostics(diagnostics));
|
|
3607
5661
|
if (blocks.length === 0)
|
|
3608
5662
|
return "";
|
|
3609
5663
|
const rawReason = blocks.map(formatDiagnosticBlock).join(`
|
|
@@ -3625,38 +5679,6 @@ async function runLspPostCompactHook(input) {
|
|
|
3625
5679
|
markLspSessionCompacted(sessionIdFrom(input));
|
|
3626
5680
|
return "";
|
|
3627
5681
|
}
|
|
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
5682
|
function formatDiagnosticBlock({ filePath, diagnostics }) {
|
|
3661
5683
|
return `LSP diagnostics after editing ${filePath}:
|
|
3662
5684
|
|
|
@@ -3739,14 +5761,11 @@ function limitHookText(text2, maxChars) {
|
|
|
3739
5761
|
const head = text2.slice(0, maxChars - marker.length).replace(/[ \t\r\n]+$/, "");
|
|
3740
5762
|
return `${head}${marker}`;
|
|
3741
5763
|
}
|
|
3742
|
-
function
|
|
3743
|
-
return diagnostics.length === 0 || diagnostics === CLEAN_DIAGNOSTICS_TEXT || diagnostics.startsWith(UNSUPPORTED_EXTENSION_TEXT);
|
|
3744
|
-
}
|
|
3745
|
-
function isRecord8(value) {
|
|
5764
|
+
function isRecord12(value) {
|
|
3746
5765
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3747
5766
|
}
|
|
3748
5767
|
|
|
3749
|
-
//
|
|
5768
|
+
// src/codex-hook-cli.ts
|
|
3750
5769
|
async function runPostToolUseHookCli(stdin = processStdin) {
|
|
3751
5770
|
await runHookCli((input) => runLspPostToolUseHook(input), stdin);
|
|
3752
5771
|
}
|
|
@@ -3759,16 +5778,12 @@ async function runHookCli(runHook, stdin) {
|
|
|
3759
5778
|
if (!raw.trim())
|
|
3760
5779
|
return;
|
|
3761
5780
|
const parsed = JSON.parse(raw);
|
|
3762
|
-
const input =
|
|
5781
|
+
const input = isRecord12(parsed) ? parsed : {};
|
|
3763
5782
|
const output = await runHook(input);
|
|
3764
5783
|
if (output)
|
|
3765
5784
|
process.stdout.write(output);
|
|
3766
5785
|
} catch {
|
|
3767
5786
|
return;
|
|
3768
|
-
} finally {
|
|
3769
|
-
await disposeDefaultLspManager().catch(() => {
|
|
3770
|
-
return;
|
|
3771
|
-
});
|
|
3772
5787
|
}
|
|
3773
5788
|
}
|
|
3774
5789
|
async function readStdin(stdin) {
|
|
@@ -3780,7 +5795,7 @@ async function readStdin(stdin) {
|
|
|
3780
5795
|
return raw;
|
|
3781
5796
|
}
|
|
3782
5797
|
|
|
3783
|
-
//
|
|
5798
|
+
// src/cli.ts
|
|
3784
5799
|
async function main() {
|
|
3785
5800
|
const [command = "mcp", subcommand = ""] = argv.slice(2);
|
|
3786
5801
|
if (command === "hook" && subcommand === "post-tool-use") {
|
|
@@ -3807,14 +5822,14 @@ main().catch((error) => {
|
|
|
3807
5822
|
async function runPackageLspMcpCli() {
|
|
3808
5823
|
const cliPath = resolveLspDaemonCliPath();
|
|
3809
5824
|
const child = spawn3(execPath2, [cliPath, "mcp"], { stdio: "inherit" });
|
|
3810
|
-
await new Promise((
|
|
5825
|
+
await new Promise((resolve10, reject) => {
|
|
3811
5826
|
child.once("error", reject);
|
|
3812
5827
|
child.once("exit", (code, signal) => {
|
|
3813
5828
|
if (code !== null && code !== 0)
|
|
3814
5829
|
process.exitCode = code;
|
|
3815
5830
|
if (code === null && signal !== null)
|
|
3816
5831
|
process.exitCode = 1;
|
|
3817
|
-
|
|
5832
|
+
resolve10();
|
|
3818
5833
|
});
|
|
3819
5834
|
});
|
|
3820
5835
|
}
|