oh-my-opencode 4.18.0 → 4.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3677 -0
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3154 -0
- package/.agents/skills/work-with-pr/SKILL.md +16 -37
- package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/.opencode/skills/work-with-pr/SKILL.md +16 -37
- package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/bin/AGENTS.md +33 -0
- package/dist/cli/index.js +500 -158
- package/dist/cli-node/index.js +500 -158
- package/dist/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.d.ts +6 -0
- package/dist/hooks/category-skill-reminder/hook.d.ts +9 -1
- package/dist/hooks/comment-checker/hook.d.ts +8 -1
- package/dist/hooks/todo-continuation-enforcer/types.d.ts +3 -0
- package/dist/index.js +956 -702
- package/dist/plugin/messages-transform.d.ts +1 -0
- package/dist/plugin-handlers/prometheus-agent-config-builder.d.ts +2 -0
- package/dist/tui.js +43 -4
- package/package.json +16 -16
- package/packages/git-bash-mcp/dist/cli.js +81 -19
- package/packages/lsp-core/package.json +4 -0
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
- package/packages/lsp-core/src/lsp/client.ts +262 -80
- package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
- package/packages/lsp-core/src/lsp/connection.ts +12 -6
- package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +221 -0
- package/packages/lsp-core/src/lsp/directory-diagnostics.ts +61 -28
- package/packages/lsp-core/src/lsp/errors.ts +11 -0
- package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
- package/packages/lsp-core/src/lsp/formatters.ts +3 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
- package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
- package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
- package/packages/lsp-core/src/lsp/transport.ts +96 -70
- package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
- package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
- package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
- package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
- package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
- package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
- package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
- package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
- package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
- package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
- package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
- package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
- package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
- package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
- package/packages/lsp-core/src/mcp.ts +18 -7
- package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
- package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
- package/packages/lsp-core/src/post-edit/index.ts +1 -0
- package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
- package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
- package/packages/lsp-core/src/request-context.test.ts +171 -0
- package/packages/lsp-core/src/request-context.ts +222 -9
- package/packages/lsp-core/src/tool-surface.test.ts +4 -1
- package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
- package/packages/lsp-core/src/tools/navigation.ts +12 -12
- package/packages/lsp-core/src/tools/rename.ts +10 -15
- package/packages/lsp-core/src/tools/symbols.ts +11 -11
- package/packages/lsp-core/src/tools/types.ts +2 -1
- package/packages/lsp-daemon/dist/cli.js +3330 -764
- package/packages/lsp-daemon/dist/client.d.ts +105 -0
- package/packages/lsp-daemon/dist/client.js +5995 -0
- package/packages/lsp-daemon/dist/daemon-client.d.ts +12 -7
- package/packages/lsp-daemon/dist/daemon-client.js +139 -32
- package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-server.js +40 -15
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +10 -8
- package/packages/lsp-daemon/dist/ensure-daemon.js +135 -51
- package/packages/lsp-daemon/dist/index.d.ts +2 -2
- package/packages/lsp-daemon/dist/index.js +3093 -786
- package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
- package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
- package/packages/lsp-daemon/dist/lock.js +14 -4
- package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
- package/packages/lsp-daemon/dist/ownership.js +168 -0
- package/packages/lsp-daemon/dist/paths.d.ts +33 -9
- package/packages/lsp-daemon/dist/paths.js +72 -33
- package/packages/lsp-daemon/dist/proxy.d.ts +5 -0
- package/packages/lsp-daemon/dist/proxy.js +123 -16
- package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
- package/packages/lsp-daemon/dist/request-routing.js +71 -22
- package/packages/lsp-daemon/dist/run-daemon.js +9 -2
- package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
- package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
- package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
- package/packages/lsp-daemon/package.json +12 -3
- package/packages/lsp-tools-mcp/dist/cli.js +2189 -454
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
- package/packages/lsp-tools-mcp/dist/mcp.js +2206 -471
- package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
- package/packages/lsp-tools-mcp/dist/tools.js +2119 -447
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/.mcp.json +2 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +100 -28
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +100 -28
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +21 -9
- package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +35 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +69 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +57 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/.mcp.json +2 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +3033 -936
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +20 -5
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -3
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/README.md +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
- package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +15 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +20 -0
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +50 -33
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +50 -33
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +50 -33
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +3 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +5 -6
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +5 -6
- package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +2 -2
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +26 -14
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
- package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +9 -1
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -0
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +50 -33
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
- package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
- package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +7 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +34 -3
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
|
@@ -1,34 +1,194 @@
|
|
|
1
|
-
// ../lsp-core/src/tools/diagnostics.ts
|
|
2
|
-
import { resolve as resolve4 } from "node:path";
|
|
3
|
-
|
|
4
1
|
// ../lsp-core/src/lsp/client-wrapper.ts
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { dirname as
|
|
2
|
+
import { existsSync as existsSync9, statSync as statSync3 } from "node:fs";
|
|
3
|
+
import { dirname as dirname6, join as join4, resolve as resolve7 } from "node:path";
|
|
7
4
|
|
|
8
5
|
// ../lsp-core/src/request-context.ts
|
|
9
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
class LspRequestContextParseError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
name = "LspRequestContextParseError";
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class LspRequestContextUnavailableError extends Error {
|
|
21
|
+
name = "LspRequestContextUnavailableError";
|
|
22
|
+
constructor() {
|
|
23
|
+
super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
10
26
|
var storage = new AsyncLocalStorage;
|
|
27
|
+
var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
28
|
+
var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
|
|
11
29
|
function runWithRequestContext(context, fn) {
|
|
12
30
|
return storage.run(context, fn);
|
|
13
31
|
}
|
|
32
|
+
function lspRequestContext() {
|
|
33
|
+
const context = storage.getStore();
|
|
34
|
+
if (!context)
|
|
35
|
+
throw new LspRequestContextUnavailableError;
|
|
36
|
+
return context;
|
|
37
|
+
}
|
|
14
38
|
function contextCwd() {
|
|
15
|
-
return
|
|
39
|
+
return lspRequestContext().cwd;
|
|
16
40
|
}
|
|
17
41
|
function contextEnv(key) {
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
return
|
|
21
|
-
|
|
42
|
+
const context = lspRequestContext();
|
|
43
|
+
if (key === "LSP_TOOLS_MCP_PROJECT_CONFIG")
|
|
44
|
+
return context.projectConfigPaths.join(delimiter);
|
|
45
|
+
if (key === "LSP_TOOLS_MCP_USER_CONFIG")
|
|
46
|
+
return context.userConfigPath;
|
|
47
|
+
if (key === "LSP_TOOLS_MCP_INSTALL_DECISIONS")
|
|
48
|
+
return context.installDecisionsPath;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
function createStandaloneMcpRequestContext(input = {}) {
|
|
52
|
+
const env = input.env ?? process.env;
|
|
53
|
+
const cwd = canonicalCwd(input.cwd ?? process.cwd());
|
|
54
|
+
const home = input.homeDir ?? homedir();
|
|
55
|
+
const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
|
|
56
|
+
const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
|
|
57
|
+
const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
|
|
58
|
+
return parseLspRequestContext({
|
|
59
|
+
cwd,
|
|
60
|
+
projectConfigPaths,
|
|
61
|
+
userConfigPath,
|
|
62
|
+
installDecisionsPath,
|
|
63
|
+
capabilities: { installDecisionTool: true }
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function parseLspRequestContext(value) {
|
|
67
|
+
if (!isRecord(value)) {
|
|
68
|
+
throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
|
|
69
|
+
}
|
|
70
|
+
rejectUnknownFields(value, CONTEXT_FIELDS, "context");
|
|
71
|
+
const cwd = stringField(value, "cwd");
|
|
72
|
+
const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
|
|
73
|
+
const userConfigPath = stringField(value, "userConfigPath");
|
|
74
|
+
const installDecisionsPath = stringField(value, "installDecisionsPath");
|
|
75
|
+
const capabilities = capabilitiesField(value["capabilities"]);
|
|
76
|
+
const canonical = canonicalCwd(cwd);
|
|
77
|
+
for (const path of projectConfigPaths) {
|
|
78
|
+
requireAbsolutePath(path, "projectConfigPaths");
|
|
79
|
+
const projectPath = canonicalizeExistingOrNearestAncestor(path);
|
|
80
|
+
if (!isPathInside(canonical, projectPath)) {
|
|
81
|
+
throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
requireAbsolutePath(userConfigPath, "userConfigPath");
|
|
85
|
+
requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
|
|
86
|
+
return {
|
|
87
|
+
cwd: canonical,
|
|
88
|
+
projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
|
|
89
|
+
userConfigPath,
|
|
90
|
+
installDecisionsPath,
|
|
91
|
+
capabilities
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function translateProjectConfigEnv(value, cwd) {
|
|
95
|
+
if (value === undefined || value.length === 0)
|
|
96
|
+
return [join(cwd, ".codex", "lsp-client.json")];
|
|
97
|
+
return value.split(delimiter).filter((entry) => entry.length > 0).map((entry) => isAbsolute(entry) ? entry : join(cwd, entry));
|
|
98
|
+
}
|
|
99
|
+
function translateHomeConfigEnv(value, home, fallback) {
|
|
100
|
+
if (value === undefined || value.length === 0)
|
|
101
|
+
return join(home, fallback);
|
|
102
|
+
return isAbsolute(value) ? value : join(home, value);
|
|
103
|
+
}
|
|
104
|
+
function canonicalCwd(cwd) {
|
|
105
|
+
const resolved = resolve(cwd);
|
|
106
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
107
|
+
throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
108
|
+
}
|
|
109
|
+
return realpathSync(resolved);
|
|
110
|
+
}
|
|
111
|
+
function canonicalizeExistingOrNearestAncestor(path) {
|
|
112
|
+
let current = resolve(path);
|
|
113
|
+
const suffix = [];
|
|
114
|
+
while (true) {
|
|
115
|
+
try {
|
|
116
|
+
const existing = realpathSync(current);
|
|
117
|
+
return suffix.length === 0 ? existing : join(existing, ...suffix);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (!isMissingPathError(error))
|
|
120
|
+
throw error;
|
|
121
|
+
const parent = dirname(current);
|
|
122
|
+
if (parent === current)
|
|
123
|
+
throw error;
|
|
124
|
+
suffix.unshift(basename(current));
|
|
125
|
+
current = parent;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function capabilitiesField(value) {
|
|
130
|
+
if (!isRecord(value)) {
|
|
131
|
+
throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
132
|
+
}
|
|
133
|
+
rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
|
|
134
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
135
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
136
|
+
throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
|
|
137
|
+
}
|
|
138
|
+
return { installDecisionTool };
|
|
139
|
+
}
|
|
140
|
+
function stringField(value, field) {
|
|
141
|
+
const fieldValue = value[field];
|
|
142
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
143
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
144
|
+
}
|
|
145
|
+
return fieldValue;
|
|
146
|
+
}
|
|
147
|
+
function stringArrayField(value, field) {
|
|
148
|
+
const fieldValue = value[field];
|
|
149
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
150
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
|
|
151
|
+
}
|
|
152
|
+
return fieldValue;
|
|
153
|
+
}
|
|
154
|
+
function requireAbsolutePath(path, field) {
|
|
155
|
+
if (!isAbsolute(path)) {
|
|
156
|
+
throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function isPathInside(parent, child) {
|
|
160
|
+
const childPath = resolve(child);
|
|
161
|
+
const relativePath = relative(parent, childPath);
|
|
162
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
163
|
+
}
|
|
164
|
+
function isMissingPathError(error) {
|
|
165
|
+
const code = errorCode(error);
|
|
166
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
167
|
+
}
|
|
168
|
+
function rejectUnknownFields(value, allowed, scope) {
|
|
169
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
170
|
+
if (unknown.length > 0) {
|
|
171
|
+
throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function isRecord(value) {
|
|
175
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
176
|
+
}
|
|
177
|
+
function errorCode(error) {
|
|
178
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
179
|
+
return;
|
|
180
|
+
const code = Reflect.get(error, "code");
|
|
181
|
+
return typeof code === "string" ? code : undefined;
|
|
22
182
|
}
|
|
23
183
|
|
|
24
184
|
// ../lsp-core/src/lsp/effective-extension.ts
|
|
25
|
-
import { basename, extname } from "node:path";
|
|
185
|
+
import { basename as basename2, extname } from "node:path";
|
|
26
186
|
var BASENAME_EXTENSIONS = {
|
|
27
187
|
Dockerfile: ".dockerfile",
|
|
28
188
|
Containerfile: ".dockerfile"
|
|
29
189
|
};
|
|
30
190
|
function effectiveExtension(filePath) {
|
|
31
|
-
return BASENAME_EXTENSIONS[
|
|
191
|
+
return BASENAME_EXTENSIONS[basename2(filePath)] ?? extname(filePath);
|
|
32
192
|
}
|
|
33
193
|
|
|
34
194
|
// ../lsp-core/src/lsp/errors.ts
|
|
@@ -78,7 +238,12 @@ class LspInvalidPathError extends Error {
|
|
|
78
238
|
}
|
|
79
239
|
|
|
80
240
|
class LspServerLookupError extends Error {
|
|
241
|
+
lookup;
|
|
81
242
|
name = "LspServerLookupError";
|
|
243
|
+
constructor(message, lookup) {
|
|
244
|
+
super(message);
|
|
245
|
+
this.lookup = lookup;
|
|
246
|
+
}
|
|
82
247
|
}
|
|
83
248
|
|
|
84
249
|
class LspServerInitializingError extends Error {
|
|
@@ -98,17 +263,18 @@ function isLspDeadConnectionError(err) {
|
|
|
98
263
|
}
|
|
99
264
|
|
|
100
265
|
// ../lsp-core/src/lsp/cleanup-errors.ts
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
266
|
+
function writeCleanupError(message) {
|
|
267
|
+
process.stderr.write(`${message}
|
|
268
|
+
`);
|
|
269
|
+
}
|
|
270
|
+
function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
|
|
104
271
|
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
-
|
|
272
|
+
logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
|
|
106
273
|
}
|
|
107
274
|
|
|
108
275
|
// ../lsp-core/src/lsp/client.ts
|
|
109
|
-
import {
|
|
110
|
-
import {
|
|
111
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
276
|
+
import { resolve as resolve6 } from "node:path";
|
|
277
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
112
278
|
|
|
113
279
|
// ../lsp-core/src/lsp/connection.ts
|
|
114
280
|
import { pathToFileURL } from "node:url";
|
|
@@ -172,28 +338,80 @@ class JsonRpcConnection {
|
|
|
172
338
|
onError(handler) {
|
|
173
339
|
this.errorHandlers.push(handler);
|
|
174
340
|
}
|
|
175
|
-
async sendRequest(method, params) {
|
|
341
|
+
async sendRequest(method, params, options = {}) {
|
|
176
342
|
if (this.disposed)
|
|
177
343
|
throw new Error("JSON-RPC connection is disposed");
|
|
178
344
|
const id = this.nextRequestId;
|
|
179
345
|
this.nextRequestId += 1;
|
|
346
|
+
const key = String(id);
|
|
180
347
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
181
|
-
|
|
182
|
-
|
|
348
|
+
let requestWritten = false;
|
|
349
|
+
let cancelAfterWrite = false;
|
|
350
|
+
let settled = false;
|
|
351
|
+
const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
352
|
+
const responsePromise = new Promise((resolve2, reject) => {
|
|
353
|
+
const cleanup = () => {
|
|
354
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
355
|
+
};
|
|
356
|
+
const settleCancel = () => {
|
|
357
|
+
if (settled)
|
|
358
|
+
return;
|
|
359
|
+
settled = true;
|
|
360
|
+
this.pendingRequests.delete(key);
|
|
361
|
+
cleanup();
|
|
362
|
+
const rejectCancelled = () => reject(abortError(options.signal));
|
|
363
|
+
if (!requestWritten) {
|
|
364
|
+
cancelAfterWrite = true;
|
|
365
|
+
rejectCancelled();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
writeCancel().then(rejectCancelled, (error) => {
|
|
369
|
+
this.emitError(toError(error));
|
|
370
|
+
rejectCancelled();
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
const onAbort = () => settleCancel();
|
|
374
|
+
this.pendingRequests.set(key, {
|
|
183
375
|
resolve(result) {
|
|
184
|
-
|
|
376
|
+
settled = true;
|
|
377
|
+
cleanup();
|
|
378
|
+
resolve2(result);
|
|
185
379
|
},
|
|
186
|
-
reject
|
|
380
|
+
reject(error) {
|
|
381
|
+
settled = true;
|
|
382
|
+
cleanup();
|
|
383
|
+
reject(error);
|
|
384
|
+
},
|
|
385
|
+
cleanup
|
|
187
386
|
});
|
|
387
|
+
if (options.signal?.aborted) {
|
|
388
|
+
settleCancel();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
188
392
|
});
|
|
393
|
+
if (settled)
|
|
394
|
+
return responsePromise;
|
|
189
395
|
try {
|
|
190
396
|
await this.writeMessage(message);
|
|
397
|
+
requestWritten = true;
|
|
398
|
+
if (cancelAfterWrite)
|
|
399
|
+
await writeCancel();
|
|
191
400
|
} catch (error) {
|
|
192
|
-
|
|
401
|
+
if (settled)
|
|
402
|
+
return responsePromise;
|
|
403
|
+
const pending = this.pendingRequests.get(key);
|
|
404
|
+
if (pending) {
|
|
405
|
+
pending.cleanup();
|
|
406
|
+
this.pendingRequests.delete(key);
|
|
407
|
+
}
|
|
193
408
|
throw error;
|
|
194
409
|
}
|
|
195
410
|
return responsePromise;
|
|
196
411
|
}
|
|
412
|
+
pendingRequestCount() {
|
|
413
|
+
return this.pendingRequests.size;
|
|
414
|
+
}
|
|
197
415
|
async sendNotification(method, params) {
|
|
198
416
|
if (this.disposed)
|
|
199
417
|
return;
|
|
@@ -210,6 +428,7 @@ class JsonRpcConnection {
|
|
|
210
428
|
this.reader.off("error", this.handleStreamError);
|
|
211
429
|
this.writer.off("error", this.handleStreamError);
|
|
212
430
|
for (const pending of this.pendingRequests.values()) {
|
|
431
|
+
pending.cleanup();
|
|
213
432
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
214
433
|
}
|
|
215
434
|
this.pendingRequests.clear();
|
|
@@ -285,6 +504,7 @@ class JsonRpcConnection {
|
|
|
285
504
|
if (!pending)
|
|
286
505
|
return;
|
|
287
506
|
this.pendingRequests.delete(String(id));
|
|
507
|
+
pending.cleanup();
|
|
288
508
|
if ("error" in message) {
|
|
289
509
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
290
510
|
return;
|
|
@@ -298,7 +518,11 @@ class JsonRpcConnection {
|
|
|
298
518
|
try {
|
|
299
519
|
handler(params);
|
|
300
520
|
} catch (error) {
|
|
301
|
-
|
|
521
|
+
if (error instanceof Error) {
|
|
522
|
+
this.emitError(error);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
this.emitError(new Error(String(error)));
|
|
302
526
|
}
|
|
303
527
|
}
|
|
304
528
|
handleRequest(message) {
|
|
@@ -323,13 +547,13 @@ class JsonRpcConnection {
|
|
|
323
547
|
const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
324
548
|
\r
|
|
325
549
|
${body}`;
|
|
326
|
-
return new Promise((
|
|
550
|
+
return new Promise((resolve2, reject) => {
|
|
327
551
|
this.writer.write(payload, (error) => {
|
|
328
552
|
if (error) {
|
|
329
553
|
reject(error);
|
|
330
554
|
return;
|
|
331
555
|
}
|
|
332
|
-
|
|
556
|
+
resolve2();
|
|
333
557
|
});
|
|
334
558
|
});
|
|
335
559
|
}
|
|
@@ -339,6 +563,14 @@ ${body}`;
|
|
|
339
563
|
}
|
|
340
564
|
}
|
|
341
565
|
}
|
|
566
|
+
function abortError(signal) {
|
|
567
|
+
const reason = signal?.reason;
|
|
568
|
+
if (reason instanceof Error)
|
|
569
|
+
return reason;
|
|
570
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
571
|
+
error.name = "AbortError";
|
|
572
|
+
return error;
|
|
573
|
+
}
|
|
342
574
|
function parseContentLength(headers) {
|
|
343
575
|
for (const line of headers.split(`\r
|
|
344
576
|
`)) {
|
|
@@ -378,8 +610,8 @@ function toError(error) {
|
|
|
378
610
|
|
|
379
611
|
// ../lsp-core/src/lsp/process.ts
|
|
380
612
|
import { spawn, spawnSync } from "node:child_process";
|
|
381
|
-
import { existsSync, statSync } from "node:fs";
|
|
382
|
-
import { delimiter, join } from "node:path";
|
|
613
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
|
|
614
|
+
import { delimiter as delimiter2, join as join2 } from "node:path";
|
|
383
615
|
function isMissingProcessError(error) {
|
|
384
616
|
if (!(error instanceof Error) || !("code" in error))
|
|
385
617
|
return false;
|
|
@@ -392,10 +624,10 @@ function reportKillError(context, error) {
|
|
|
392
624
|
}
|
|
393
625
|
function validateCwd(cwd) {
|
|
394
626
|
try {
|
|
395
|
-
if (!
|
|
627
|
+
if (!existsSync2(cwd)) {
|
|
396
628
|
return { valid: false, error: `Working directory does not exist: ${cwd}` };
|
|
397
629
|
}
|
|
398
|
-
const stats =
|
|
630
|
+
const stats = statSync2(cwd);
|
|
399
631
|
if (!stats.isDirectory()) {
|
|
400
632
|
return { valid: false, error: `Path is not a directory: ${cwd}` };
|
|
401
633
|
}
|
|
@@ -408,9 +640,9 @@ function validateCwd(cwd) {
|
|
|
408
640
|
}
|
|
409
641
|
}
|
|
410
642
|
function wrap(proc) {
|
|
411
|
-
const exitedPromise = new Promise((
|
|
412
|
-
proc.once("close", (code) =>
|
|
413
|
-
proc.once("error", () =>
|
|
643
|
+
const exitedPromise = new Promise((resolve2) => {
|
|
644
|
+
proc.once("close", (code) => resolve2(code ?? 0));
|
|
645
|
+
proc.once("error", () => resolve2(1));
|
|
414
646
|
});
|
|
415
647
|
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
416
648
|
throw new LspProcessSpawnError("Spawned process is missing one of stdin/stdout/stderr pipes");
|
|
@@ -464,7 +696,7 @@ function isWindowsShellShim(command) {
|
|
|
464
696
|
return lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat");
|
|
465
697
|
}
|
|
466
698
|
function splitPath(pathValue, platform) {
|
|
467
|
-
const separator = platform === "win32" ? ";" :
|
|
699
|
+
const separator = platform === "win32" ? ";" : delimiter2;
|
|
468
700
|
return pathValue.split(separator).filter(Boolean);
|
|
469
701
|
}
|
|
470
702
|
function getWindowsPathExtensions(env) {
|
|
@@ -479,8 +711,8 @@ function resolveWindowsCommand(command, env) {
|
|
|
479
711
|
const extensions = getWindowsPathExtensions(env);
|
|
480
712
|
for (const baseDirectory of baseDirectories) {
|
|
481
713
|
for (const extension of extensions) {
|
|
482
|
-
const candidate = baseDirectory ?
|
|
483
|
-
if (
|
|
714
|
+
const candidate = baseDirectory ? join2(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
|
|
715
|
+
if (existsSync2(candidate))
|
|
484
716
|
return candidate;
|
|
485
717
|
}
|
|
486
718
|
}
|
|
@@ -525,16 +757,16 @@ function spawnProcess(command, options) {
|
|
|
525
757
|
return wrap(proc);
|
|
526
758
|
}
|
|
527
759
|
|
|
528
|
-
// ../lsp-core/src/lsp/transport.ts
|
|
529
|
-
function
|
|
760
|
+
// ../lsp-core/src/lsp/transport-protocol.ts
|
|
761
|
+
function isRecord2(value) {
|
|
530
762
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
531
763
|
}
|
|
532
764
|
function parseConfigurationItems(params) {
|
|
533
|
-
if (!
|
|
765
|
+
if (!isRecord2(params) || !Array.isArray(params["items"]))
|
|
534
766
|
return [];
|
|
535
767
|
const items = [];
|
|
536
768
|
for (const item of params["items"]) {
|
|
537
|
-
if (!
|
|
769
|
+
if (!isRecord2(item))
|
|
538
770
|
continue;
|
|
539
771
|
const section = item["section"];
|
|
540
772
|
items.push(section === undefined || typeof section !== "string" ? {} : { section });
|
|
@@ -542,10 +774,35 @@ function parseConfigurationItems(params) {
|
|
|
542
774
|
return items;
|
|
543
775
|
}
|
|
544
776
|
function parseDiagnosticsParams(params) {
|
|
545
|
-
if (!
|
|
777
|
+
if (!isRecord2(params) || typeof params["uri"] !== "string")
|
|
546
778
|
return null;
|
|
547
779
|
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
548
|
-
|
|
780
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
781
|
+
return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
|
|
782
|
+
}
|
|
783
|
+
function createLspSpawnEnv(_root, input) {
|
|
784
|
+
return { ...input };
|
|
785
|
+
}
|
|
786
|
+
function isDiagnostic(value) {
|
|
787
|
+
return isRecord2(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
788
|
+
}
|
|
789
|
+
function isRange(value) {
|
|
790
|
+
return isRecord2(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
791
|
+
}
|
|
792
|
+
function isPosition(value) {
|
|
793
|
+
return isRecord2(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ../lsp-core/src/lsp/transport.ts
|
|
797
|
+
class LspClientNotStartedError extends Error {
|
|
798
|
+
serverId;
|
|
799
|
+
root;
|
|
800
|
+
name = "LspClientNotStartedError";
|
|
801
|
+
constructor(serverId, root) {
|
|
802
|
+
super("LSP client not started");
|
|
803
|
+
this.serverId = serverId;
|
|
804
|
+
this.root = root;
|
|
805
|
+
}
|
|
549
806
|
}
|
|
550
807
|
|
|
551
808
|
class LspClientTransport {
|
|
@@ -558,6 +815,8 @@ class LspClientTransport {
|
|
|
558
815
|
diagnosticsStore = new Map;
|
|
559
816
|
requestTimeoutMs;
|
|
560
817
|
initializeTimeoutMs;
|
|
818
|
+
workspaceApplyEditHandler = null;
|
|
819
|
+
diagnosticPullSupported = false;
|
|
561
820
|
constructor(root, server, timeouts = {}) {
|
|
562
821
|
this.root = root;
|
|
563
822
|
this.server = server;
|
|
@@ -570,6 +829,21 @@ class LspClientTransport {
|
|
|
570
829
|
command() {
|
|
571
830
|
return [...this.server.command];
|
|
572
831
|
}
|
|
832
|
+
setWorkspaceApplyEditHandler(handler) {
|
|
833
|
+
this.workspaceApplyEditHandler = handler;
|
|
834
|
+
}
|
|
835
|
+
hasWorkspaceApplyEditHandler() {
|
|
836
|
+
return this.workspaceApplyEditHandler !== null;
|
|
837
|
+
}
|
|
838
|
+
setDiagnosticPullSupported(supported) {
|
|
839
|
+
this.diagnosticPullSupported = supported;
|
|
840
|
+
}
|
|
841
|
+
isDiagnosticPullSupported() {
|
|
842
|
+
return this.diagnosticPullSupported;
|
|
843
|
+
}
|
|
844
|
+
handlePublishDiagnostics(params) {
|
|
845
|
+
this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
|
|
846
|
+
}
|
|
573
847
|
async start() {
|
|
574
848
|
const env = createLspSpawnEnv(this.root, {
|
|
575
849
|
...process.env,
|
|
@@ -580,7 +854,6 @@ class LspClientTransport {
|
|
|
580
854
|
env
|
|
581
855
|
});
|
|
582
856
|
this.startStderrReading();
|
|
583
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
584
857
|
if (this.proc.exitCode !== null) {
|
|
585
858
|
const stderr = this.stderrBuffer.join(`
|
|
586
859
|
`);
|
|
@@ -590,7 +863,7 @@ class LspClientTransport {
|
|
|
590
863
|
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
591
864
|
const diagnosticsParams = parseDiagnosticsParams(params);
|
|
592
865
|
if (diagnosticsParams?.uri) {
|
|
593
|
-
this.
|
|
866
|
+
this.handlePublishDiagnostics(diagnosticsParams);
|
|
594
867
|
}
|
|
595
868
|
});
|
|
596
869
|
this.connection.onRequest("workspace/configuration", (params) => {
|
|
@@ -603,6 +876,9 @@ class LspClientTransport {
|
|
|
603
876
|
});
|
|
604
877
|
this.connection.onRequest("client/registerCapability", () => null);
|
|
605
878
|
this.connection.onRequest("window/workDoneProgress/create", () => null);
|
|
879
|
+
if (this.workspaceApplyEditHandler) {
|
|
880
|
+
this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
|
|
881
|
+
}
|
|
606
882
|
this.connection.onClose(() => {
|
|
607
883
|
this.processExited = true;
|
|
608
884
|
});
|
|
@@ -631,30 +907,25 @@ class LspClientTransport {
|
|
|
631
907
|
}
|
|
632
908
|
async sendRequest(method, ...args) {
|
|
633
909
|
if (!this.connection)
|
|
634
|
-
throw new
|
|
910
|
+
throw new LspClientNotStartedError(this.server.id, this.root);
|
|
635
911
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
636
912
|
const stderrTail = this.stderrBuffer.slice(-10).join(`
|
|
637
913
|
`);
|
|
638
914
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
|
|
639
915
|
}
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
916
|
+
const options = args[1];
|
|
917
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
918
|
+
const timeoutController = new AbortController;
|
|
919
|
+
const timeoutHandle = setTimeout(() => {
|
|
920
|
+
const stderrTail = this.stderrBuffer.slice(-5).join(`
|
|
645
921
|
`);
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
922
|
+
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
923
|
+
}, timeoutMs);
|
|
924
|
+
const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
|
|
649
925
|
try {
|
|
650
|
-
const
|
|
651
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
652
|
-
if (timeoutHandle !== null)
|
|
653
|
-
clearTimeout(timeoutHandle);
|
|
926
|
+
const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
|
|
654
927
|
return result;
|
|
655
928
|
} catch (error) {
|
|
656
|
-
if (timeoutHandle !== null)
|
|
657
|
-
clearTimeout(timeoutHandle);
|
|
658
929
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
659
930
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
|
|
660
931
|
`) || undefined);
|
|
@@ -663,6 +934,9 @@ class LspClientTransport {
|
|
|
663
934
|
throw new LspConnectionClosedError(this.server.id, this.root, error.message);
|
|
664
935
|
}
|
|
665
936
|
throw error;
|
|
937
|
+
} finally {
|
|
938
|
+
clearTimeout(timeoutHandle);
|
|
939
|
+
combinedSignal.dispose();
|
|
666
940
|
}
|
|
667
941
|
}
|
|
668
942
|
async sendNotification(method, ...args) {
|
|
@@ -691,17 +965,17 @@ class LspClientTransport {
|
|
|
691
965
|
try {
|
|
692
966
|
await this.sendRequest("shutdown");
|
|
693
967
|
} catch (error) {
|
|
694
|
-
reportBestEffortCleanupError("shutdown request", error);
|
|
968
|
+
reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
|
|
695
969
|
}
|
|
696
970
|
try {
|
|
697
971
|
await this.sendNotification("exit");
|
|
698
972
|
} catch (error) {
|
|
699
|
-
reportBestEffortCleanupError("exit notification", error);
|
|
973
|
+
reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
|
|
700
974
|
}
|
|
701
975
|
try {
|
|
702
976
|
this.connection.dispose();
|
|
703
977
|
} catch (error) {
|
|
704
|
-
reportBestEffortCleanupError("connection dispose", error);
|
|
978
|
+
reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
|
|
705
979
|
}
|
|
706
980
|
this.connection = null;
|
|
707
981
|
}
|
|
@@ -712,8 +986,8 @@ class LspClientTransport {
|
|
|
712
986
|
try {
|
|
713
987
|
proc.kill();
|
|
714
988
|
let timeoutId;
|
|
715
|
-
const timeoutPromise = new Promise((
|
|
716
|
-
timeoutId = setTimeout(
|
|
989
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
990
|
+
timeoutId = setTimeout(resolve2, STOP_HARD_KILL_TIMEOUT_MS);
|
|
717
991
|
});
|
|
718
992
|
await Promise.race([
|
|
719
993
|
proc.exited.then(() => {
|
|
@@ -729,14 +1003,14 @@ class LspClientTransport {
|
|
|
729
1003
|
proc.kill("SIGKILL");
|
|
730
1004
|
await Promise.race([
|
|
731
1005
|
proc.exited,
|
|
732
|
-
new Promise((
|
|
1006
|
+
new Promise((resolve2) => setTimeout(resolve2, STOP_SIGKILL_GRACE_MS))
|
|
733
1007
|
]);
|
|
734
1008
|
} catch (error) {
|
|
735
|
-
reportBestEffortCleanupError("hard process kill", error);
|
|
1009
|
+
reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
|
|
736
1010
|
}
|
|
737
1011
|
}
|
|
738
1012
|
} catch (error) {
|
|
739
|
-
reportBestEffortCleanupError("process stop", error);
|
|
1013
|
+
reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
|
|
740
1014
|
}
|
|
741
1015
|
}
|
|
742
1016
|
this.processExited = true;
|
|
@@ -746,26 +1020,45 @@ class LspClientTransport {
|
|
|
746
1020
|
return this.diagnosticsStore.get(uri) ?? [];
|
|
747
1021
|
}
|
|
748
1022
|
}
|
|
749
|
-
function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1023
|
+
function combineAbortSignals(primary, secondary) {
|
|
1024
|
+
const controller = new AbortController;
|
|
1025
|
+
const abortFrom = (signal) => {
|
|
1026
|
+
if (!controller.signal.aborted)
|
|
1027
|
+
controller.abort(signal.reason);
|
|
1028
|
+
};
|
|
1029
|
+
const onPrimaryAbort = () => {
|
|
1030
|
+
if (primary)
|
|
1031
|
+
abortFrom(primary);
|
|
1032
|
+
};
|
|
1033
|
+
const onSecondaryAbort = () => abortFrom(secondary);
|
|
1034
|
+
if (primary?.aborted)
|
|
1035
|
+
abortFrom(primary);
|
|
1036
|
+
else
|
|
1037
|
+
primary?.addEventListener("abort", onPrimaryAbort, { once: true });
|
|
1038
|
+
if (secondary.aborted)
|
|
1039
|
+
abortFrom(secondary);
|
|
1040
|
+
else
|
|
1041
|
+
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
|
|
1042
|
+
return {
|
|
1043
|
+
signal: controller.signal,
|
|
1044
|
+
dispose: () => {
|
|
1045
|
+
primary?.removeEventListener("abort", onPrimaryAbort);
|
|
1046
|
+
secondary.removeEventListener("abort", onSecondaryAbort);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
760
1049
|
}
|
|
761
1050
|
|
|
762
1051
|
// ../lsp-core/src/lsp/connection.ts
|
|
763
|
-
|
|
1052
|
+
function supportsDiagnosticPull(capabilities) {
|
|
1053
|
+
if (capabilities === undefined)
|
|
1054
|
+
return false;
|
|
1055
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
1056
|
+
}
|
|
764
1057
|
|
|
765
1058
|
class LspClientConnection extends LspClientTransport {
|
|
766
1059
|
async initialize() {
|
|
767
1060
|
const rootUri = pathToFileURL(this.root).href;
|
|
768
|
-
await this.sendRequest("initialize", {
|
|
1061
|
+
const result = await this.sendRequest("initialize", {
|
|
769
1062
|
processId: process.pid,
|
|
770
1063
|
rootUri,
|
|
771
1064
|
rootPath: this.root,
|
|
@@ -779,8 +1072,7 @@ class LspClientConnection extends LspClientTransport {
|
|
|
779
1072
|
publishDiagnostics: {},
|
|
780
1073
|
rename: {
|
|
781
1074
|
prepareSupport: true,
|
|
782
|
-
prepareSupportDefaultBehavior: 1
|
|
783
|
-
honorsChangeAnnotations: true
|
|
1075
|
+
prepareSupportDefaultBehavior: 1
|
|
784
1076
|
},
|
|
785
1077
|
codeAction: {
|
|
786
1078
|
codeActionLiteralSupport: {
|
|
@@ -809,22 +1101,28 @@ class LspClientConnection extends LspClientTransport {
|
|
|
809
1101
|
symbol: {},
|
|
810
1102
|
workspaceFolders: true,
|
|
811
1103
|
configuration: true,
|
|
812
|
-
applyEdit: true,
|
|
1104
|
+
...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
|
|
813
1105
|
workspaceEdit: {
|
|
814
|
-
documentChanges: true
|
|
1106
|
+
documentChanges: true,
|
|
1107
|
+
resourceOperations: ["create", "rename", "delete"]
|
|
815
1108
|
}
|
|
816
1109
|
}
|
|
817
1110
|
},
|
|
818
1111
|
initializationOptions: this.server.initialization
|
|
819
1112
|
}, { timeoutMs: this.initializeTimeoutMs });
|
|
1113
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
820
1114
|
await this.sendNotification("initialized");
|
|
821
1115
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
822
1116
|
settings: { json: { validate: { enable: true } } }
|
|
823
1117
|
});
|
|
824
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
825
1118
|
}
|
|
826
1119
|
}
|
|
827
1120
|
|
|
1121
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1122
|
+
import { readFileSync, realpathSync as realpathSync2 } from "node:fs";
|
|
1123
|
+
import { relative as relative2, resolve as resolve2 } from "node:path";
|
|
1124
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1125
|
+
|
|
828
1126
|
// ../lsp-core/src/lsp/language-mappings.ts
|
|
829
1127
|
var SYMBOL_KIND_MAP = {
|
|
830
1128
|
1: "File",
|
|
@@ -997,82 +1295,1422 @@ function getLanguageId(ext) {
|
|
|
997
1295
|
return EXT_TO_LANG[ext] ?? "plaintext";
|
|
998
1296
|
}
|
|
999
1297
|
|
|
1298
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1299
|
+
var WATCHED_FILE_BATCH_SIZE = 128;
|
|
1300
|
+
var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1301
|
+
function canonicalPath(filePath) {
|
|
1302
|
+
const absolute = resolve2(filePath);
|
|
1303
|
+
try {
|
|
1304
|
+
return realpathSync2(absolute);
|
|
1305
|
+
} catch {
|
|
1306
|
+
return absolute;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
function isSameOrDescendant(candidate, parent) {
|
|
1310
|
+
const suffix = relative2(parent, candidate);
|
|
1311
|
+
return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
|
|
1312
|
+
}
|
|
1313
|
+
function movedPath(candidate, oldPath, newPath) {
|
|
1314
|
+
const suffix = relative2(oldPath, candidate);
|
|
1315
|
+
return suffix === "" ? newPath : resolve2(newPath, suffix);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
class WorkspaceDocumentState {
|
|
1319
|
+
sendNotification;
|
|
1320
|
+
clearDiagnostics;
|
|
1321
|
+
openDocuments = new Map;
|
|
1322
|
+
openByUri = new Map;
|
|
1323
|
+
openPromises = new Map;
|
|
1324
|
+
now;
|
|
1325
|
+
versionlessPublishQuiescenceMs;
|
|
1326
|
+
constructor(sendNotification, clearDiagnostics, options = {}) {
|
|
1327
|
+
this.sendNotification = sendNotification;
|
|
1328
|
+
this.clearDiagnostics = clearDiagnostics;
|
|
1329
|
+
this.now = options.now ?? (() => Date.now());
|
|
1330
|
+
this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
1331
|
+
}
|
|
1332
|
+
async openFile(filePath) {
|
|
1333
|
+
const path = canonicalPath(filePath);
|
|
1334
|
+
const existingOpen = this.openPromises.get(path);
|
|
1335
|
+
if (existingOpen) {
|
|
1336
|
+
await existingOpen;
|
|
1337
|
+
return this.openFile(path);
|
|
1338
|
+
}
|
|
1339
|
+
const text = readFileSync(path, "utf-8");
|
|
1340
|
+
const existing = this.openDocuments.get(path);
|
|
1341
|
+
if (!existing)
|
|
1342
|
+
return this.openDocumentSingleFlight(path, text);
|
|
1343
|
+
if (existing.text === text)
|
|
1344
|
+
return;
|
|
1345
|
+
await this.changeDocument(existing, text);
|
|
1346
|
+
}
|
|
1347
|
+
getVersion(filePath) {
|
|
1348
|
+
return this.openDocuments.get(canonicalPath(filePath))?.version;
|
|
1349
|
+
}
|
|
1350
|
+
getStoredDiagnostics(uri) {
|
|
1351
|
+
const state = this.openByUri.get(uri);
|
|
1352
|
+
if (!state)
|
|
1353
|
+
return [];
|
|
1354
|
+
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
1355
|
+
}
|
|
1356
|
+
captureDiagnosticSnapshot(filePath) {
|
|
1357
|
+
const state = this.openDocuments.get(canonicalPath(filePath));
|
|
1358
|
+
if (!state)
|
|
1359
|
+
return null;
|
|
1360
|
+
return {
|
|
1361
|
+
path: state.path,
|
|
1362
|
+
uri: state.uri,
|
|
1363
|
+
version: state.version,
|
|
1364
|
+
documentGeneration: state.generation,
|
|
1365
|
+
publishGeneration: state.publishGeneration
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
isCurrentSnapshot(snapshot) {
|
|
1369
|
+
const state = this.openDocuments.get(snapshot.path);
|
|
1370
|
+
return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
|
|
1371
|
+
}
|
|
1372
|
+
getPullCache(snapshot) {
|
|
1373
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1374
|
+
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
|
|
1375
|
+
return null;
|
|
1376
|
+
return state.pullCache;
|
|
1377
|
+
}
|
|
1378
|
+
recordPullDiagnostics(snapshot, report) {
|
|
1379
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1380
|
+
if (!state)
|
|
1381
|
+
return;
|
|
1382
|
+
state.pullCache = {
|
|
1383
|
+
documentVersion: snapshot.version,
|
|
1384
|
+
diagnostics: [...report.diagnostics],
|
|
1385
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
recordPublishedDiagnostics(params) {
|
|
1389
|
+
const state = this.openByUri.get(params.uri);
|
|
1390
|
+
if (!state)
|
|
1391
|
+
return;
|
|
1392
|
+
state.publishGeneration += 1;
|
|
1393
|
+
state.lastPublish = {
|
|
1394
|
+
diagnostics: [...params.diagnostics],
|
|
1395
|
+
publishGeneration: state.publishGeneration,
|
|
1396
|
+
documentGenerationAtArrival: state.generation,
|
|
1397
|
+
arrivedAt: this.now(),
|
|
1398
|
+
...params.version === undefined ? {} : { version: params.version }
|
|
1399
|
+
};
|
|
1400
|
+
this.notifyWaiters(state);
|
|
1401
|
+
}
|
|
1402
|
+
resolvePushDiagnostics(snapshot) {
|
|
1403
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1404
|
+
if (!state?.lastPublish)
|
|
1405
|
+
return { status: "missing" };
|
|
1406
|
+
const publish = state.lastPublish;
|
|
1407
|
+
if (publish.version !== undefined) {
|
|
1408
|
+
return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
|
|
1409
|
+
}
|
|
1410
|
+
if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
|
|
1411
|
+
return { status: "missing" };
|
|
1412
|
+
const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
|
|
1413
|
+
const waitMs = Math.max(0, readyAt - this.now());
|
|
1414
|
+
return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
|
|
1415
|
+
}
|
|
1416
|
+
waitForDiagnosticsActivity(snapshot, timeoutMs) {
|
|
1417
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1418
|
+
if (!state || timeoutMs <= 0)
|
|
1419
|
+
return Promise.resolve();
|
|
1420
|
+
return new Promise((resolveActivity) => {
|
|
1421
|
+
let settled = false;
|
|
1422
|
+
const finish = () => {
|
|
1423
|
+
if (settled)
|
|
1424
|
+
return;
|
|
1425
|
+
settled = true;
|
|
1426
|
+
clearTimeout(timer);
|
|
1427
|
+
state.waiters.delete(finish);
|
|
1428
|
+
resolveActivity();
|
|
1429
|
+
};
|
|
1430
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1431
|
+
if (typeof timer.unref === "function")
|
|
1432
|
+
timer.unref();
|
|
1433
|
+
state.waiters.add(finish);
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
validateVersions(operations) {
|
|
1437
|
+
const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
|
|
1438
|
+
for (const operation of operations) {
|
|
1439
|
+
if (operation.kind === "text") {
|
|
1440
|
+
const current = versions.get(operation.path);
|
|
1441
|
+
if (operation.documentVersion !== null && current !== operation.documentVersion) {
|
|
1442
|
+
const observed = current === undefined ? "closed document" : `open document version ${current}`;
|
|
1443
|
+
return {
|
|
1444
|
+
changeIndex: operation.changeIndex,
|
|
1445
|
+
message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
if (current !== undefined)
|
|
1449
|
+
versions.set(operation.path, current + 1);
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (operation.kind === "rename") {
|
|
1453
|
+
const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
|
|
1454
|
+
for (const [path] of moved)
|
|
1455
|
+
versions.delete(path);
|
|
1456
|
+
for (const [path] of moved)
|
|
1457
|
+
versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (operation.kind === "delete") {
|
|
1461
|
+
for (const path of [...versions.keys()]) {
|
|
1462
|
+
if (isSameOrDescendant(path, operation.path))
|
|
1463
|
+
versions.delete(path);
|
|
1464
|
+
}
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
|
|
1468
|
+
versions.set(operation.path, 1);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return null;
|
|
1472
|
+
}
|
|
1473
|
+
async synchronize(delta) {
|
|
1474
|
+
const watched = [];
|
|
1475
|
+
for (const mutation of delta.operations)
|
|
1476
|
+
await this.synchronizeMutation(mutation, watched);
|
|
1477
|
+
for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
|
|
1478
|
+
await this.sendNotification("workspace/didChangeWatchedFiles", {
|
|
1479
|
+
changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
async synchronizeMutation(mutation, watched) {
|
|
1484
|
+
if (mutation.kind === "text") {
|
|
1485
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1486
|
+
if (state)
|
|
1487
|
+
await this.changeDocument(state, mutation.afterText);
|
|
1488
|
+
else
|
|
1489
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
if (mutation.kind === "create") {
|
|
1493
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1494
|
+
if (state) {
|
|
1495
|
+
await this.closeDocument(state);
|
|
1496
|
+
await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
|
|
1497
|
+
} else {
|
|
1498
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
|
|
1499
|
+
}
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (mutation.kind === "rename") {
|
|
1503
|
+
const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
|
|
1504
|
+
for (const state of moved)
|
|
1505
|
+
await this.closeDocument(state);
|
|
1506
|
+
for (const state of moved) {
|
|
1507
|
+
const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
|
|
1508
|
+
await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
|
|
1509
|
+
}
|
|
1510
|
+
if (moved.length === 0) {
|
|
1511
|
+
watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
|
|
1512
|
+
watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
|
|
1513
|
+
}
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
|
|
1517
|
+
for (const state of removed)
|
|
1518
|
+
await this.closeDocument(state);
|
|
1519
|
+
if (removed.length === 0)
|
|
1520
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
|
|
1521
|
+
}
|
|
1522
|
+
async openDocumentSingleFlight(path, text) {
|
|
1523
|
+
const existing = this.openPromises.get(path);
|
|
1524
|
+
if (existing)
|
|
1525
|
+
return existing;
|
|
1526
|
+
const open = (async () => {
|
|
1527
|
+
const state = {
|
|
1528
|
+
path,
|
|
1529
|
+
uri: pathToFileURL2(path).href,
|
|
1530
|
+
languageId: getLanguageId(effectiveExtension(path)),
|
|
1531
|
+
text,
|
|
1532
|
+
version: 1,
|
|
1533
|
+
generation: 1,
|
|
1534
|
+
publishGeneration: 0,
|
|
1535
|
+
waiters: new Set
|
|
1536
|
+
};
|
|
1537
|
+
this.openDocuments.set(path, state);
|
|
1538
|
+
this.openByUri.set(state.uri, state);
|
|
1539
|
+
this.notifyWaiters(state);
|
|
1540
|
+
await this.sendNotification("textDocument/didOpen", {
|
|
1541
|
+
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
|
|
1542
|
+
});
|
|
1543
|
+
})().finally(() => {
|
|
1544
|
+
this.openPromises.delete(path);
|
|
1545
|
+
});
|
|
1546
|
+
this.openPromises.set(path, open);
|
|
1547
|
+
return open;
|
|
1548
|
+
}
|
|
1549
|
+
async changeDocument(state, text) {
|
|
1550
|
+
state.text = text;
|
|
1551
|
+
state.version += 1;
|
|
1552
|
+
state.generation += 1;
|
|
1553
|
+
this.clearDiagnostics(state.uri);
|
|
1554
|
+
this.notifyWaiters(state);
|
|
1555
|
+
await this.sendNotification("textDocument/didChange", {
|
|
1556
|
+
textDocument: { uri: state.uri, version: state.version },
|
|
1557
|
+
contentChanges: [{ text }]
|
|
1558
|
+
});
|
|
1559
|
+
await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
|
|
1560
|
+
}
|
|
1561
|
+
async closeDocument(state) {
|
|
1562
|
+
this.openDocuments.delete(state.path);
|
|
1563
|
+
this.openByUri.delete(state.uri);
|
|
1564
|
+
this.clearDiagnostics(state.uri);
|
|
1565
|
+
this.notifyWaiters(state);
|
|
1566
|
+
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
1567
|
+
}
|
|
1568
|
+
notifyWaiters(state) {
|
|
1569
|
+
for (const waiter of [...state.waiters])
|
|
1570
|
+
waiter();
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
|
|
1575
|
+
var CONCURRENT_FAILURE_REASON_BY_PHASE = {
|
|
1576
|
+
applying: "workspace/applyEdit is already in progress for this workspace mutation",
|
|
1577
|
+
settled: "workspace/applyEdit was already handled for this workspace mutation"
|
|
1578
|
+
};
|
|
1579
|
+
function workspaceApplyEditConcurrentFailureReason(phase) {
|
|
1580
|
+
return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1584
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1585
|
+
|
|
1586
|
+
// ../lsp-core/src/lsp/workspace-edit-path.ts
|
|
1587
|
+
import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync3 } from "node:fs";
|
|
1588
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3 } from "node:path";
|
|
1589
|
+
import { fileURLToPath } from "node:url";
|
|
1590
|
+
|
|
1591
|
+
class WorkspaceEditPathError extends Error {
|
|
1592
|
+
path;
|
|
1593
|
+
detail;
|
|
1594
|
+
name = "WorkspaceEditPathError";
|
|
1595
|
+
constructor(path, detail) {
|
|
1596
|
+
super(`${detail}: ${path}`);
|
|
1597
|
+
this.path = path;
|
|
1598
|
+
this.detail = detail;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
1602
|
+
const relativePath = relative3(workspaceRoot, filePath);
|
|
1603
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
|
|
1604
|
+
}
|
|
1605
|
+
function canonicalizeMissingPath(filePath) {
|
|
1606
|
+
let ancestor = filePath;
|
|
1607
|
+
while (!existsSync3(ancestor)) {
|
|
1608
|
+
const parent = dirname2(ancestor);
|
|
1609
|
+
if (parent === ancestor)
|
|
1610
|
+
throw new WorkspaceEditPathError(filePath, "no existing ancestor");
|
|
1611
|
+
ancestor = parent;
|
|
1612
|
+
}
|
|
1613
|
+
return resolve3(realpathSync3(ancestor), relative3(ancestor, filePath));
|
|
1614
|
+
}
|
|
1615
|
+
function canonicalWorkspaceRoot(workspaceRoot) {
|
|
1616
|
+
try {
|
|
1617
|
+
const canonical = realpathSync3(resolve3(workspaceRoot));
|
|
1618
|
+
if (!lstatSync(canonical).isDirectory()) {
|
|
1619
|
+
return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
|
|
1620
|
+
}
|
|
1621
|
+
return {
|
|
1622
|
+
success: true,
|
|
1623
|
+
path: canonical,
|
|
1624
|
+
requestedPath: resolve3(workspaceRoot),
|
|
1625
|
+
followedSymbolicLink: existsSync3(resolve3(workspaceRoot)) && lstatSync(resolve3(workspaceRoot)).isSymbolicLink()
|
|
1626
|
+
};
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1629
|
+
return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
|
|
1633
|
+
let requestedPath;
|
|
1634
|
+
try {
|
|
1635
|
+
const parsed = new URL(uri);
|
|
1636
|
+
if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
|
|
1637
|
+
return { success: false, error: `non-file URI ${uri}` };
|
|
1638
|
+
}
|
|
1639
|
+
requestedPath = resolve3(fileURLToPath(parsed));
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1642
|
+
return { success: false, error: `non-file URI ${uri}: ${detail}` };
|
|
1643
|
+
}
|
|
1644
|
+
try {
|
|
1645
|
+
const canonical = existsSync3(requestedPath) ? realpathSync3(requestedPath) : canonicalizeMissingPath(requestedPath);
|
|
1646
|
+
if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
|
|
1647
|
+
return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
|
|
1648
|
+
}
|
|
1649
|
+
return {
|
|
1650
|
+
success: true,
|
|
1651
|
+
path: canonical,
|
|
1652
|
+
requestedPath,
|
|
1653
|
+
followedSymbolicLink: existsSync3(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
|
|
1654
|
+
};
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1657
|
+
return { success: false, error: `${requestedPath}: ${detail}` };
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function snapshotPath(path, includeChildren) {
|
|
1661
|
+
if (!existsSync3(path))
|
|
1662
|
+
return { kind: "missing" };
|
|
1663
|
+
const stats = lstatSync(path);
|
|
1664
|
+
if (stats.isFile())
|
|
1665
|
+
return { kind: "file", content: readFileSync2(path, "utf-8") };
|
|
1666
|
+
if (stats.isDirectory()) {
|
|
1667
|
+
return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
|
|
1668
|
+
}
|
|
1669
|
+
throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1673
|
+
var DEFAULT_IO = {
|
|
1674
|
+
writeFile(path, content) {
|
|
1675
|
+
writeFileSync(path, content, "utf-8");
|
|
1676
|
+
},
|
|
1677
|
+
rename(oldPath, newPath) {
|
|
1678
|
+
renameSync(oldPath, newPath);
|
|
1679
|
+
},
|
|
1680
|
+
remove(path, recursive) {
|
|
1681
|
+
rmSync(path, { recursive, force: false });
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
function snapshotsEqual(expected, actual) {
|
|
1685
|
+
if (expected.kind !== actual.kind)
|
|
1686
|
+
return false;
|
|
1687
|
+
if (expected.kind === "file" && actual.kind === "file")
|
|
1688
|
+
return expected.content === actual.content;
|
|
1689
|
+
if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
|
|
1690
|
+
return JSON.stringify(expected.children) === JSON.stringify(actual.children);
|
|
1691
|
+
}
|
|
1692
|
+
return true;
|
|
1693
|
+
}
|
|
1694
|
+
function liveSnapshot(path, expected) {
|
|
1695
|
+
return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
|
|
1696
|
+
}
|
|
1697
|
+
function firstOperationIndex(plan) {
|
|
1698
|
+
return plan.operations[0]?.changeIndex ?? 0;
|
|
1699
|
+
}
|
|
1700
|
+
function failedCommit(plan, failure) {
|
|
1701
|
+
const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
|
|
1702
|
+
return {
|
|
1703
|
+
result: {
|
|
1704
|
+
success: false,
|
|
1705
|
+
filesModified,
|
|
1706
|
+
totalEdits,
|
|
1707
|
+
errors: [`change ${changeIndex}: ${message}`],
|
|
1708
|
+
failedChange: changeIndex,
|
|
1709
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1710
|
+
},
|
|
1711
|
+
delta: mutationDelta(mutations),
|
|
1712
|
+
fingerprint: plan.fingerprint
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
function verifySnapshots(plan) {
|
|
1716
|
+
for (const [path, expected] of plan.snapshots) {
|
|
1717
|
+
let actual;
|
|
1718
|
+
try {
|
|
1719
|
+
actual = liveSnapshot(path, expected);
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1722
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1723
|
+
return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
|
|
1724
|
+
}
|
|
1725
|
+
if (!snapshotsEqual(expected, actual)) {
|
|
1726
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1727
|
+
return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
function addModifiedPath(paths, path) {
|
|
1733
|
+
if (!paths.includes(path))
|
|
1734
|
+
paths.push(path);
|
|
1735
|
+
}
|
|
1736
|
+
function reportedPath(plan, path) {
|
|
1737
|
+
return plan.reportedPathByCanonical.get(path) ?? path;
|
|
1738
|
+
}
|
|
1739
|
+
function changedPathsForMutation(mutation) {
|
|
1740
|
+
return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
|
|
1741
|
+
}
|
|
1742
|
+
function mutationDelta(operations) {
|
|
1743
|
+
const changedPaths = new Set;
|
|
1744
|
+
for (const operation of operations) {
|
|
1745
|
+
for (const path of changedPathsForMutation(operation))
|
|
1746
|
+
changedPaths.add(path);
|
|
1747
|
+
}
|
|
1748
|
+
return { operations, changedPaths: [...changedPaths].sort() };
|
|
1749
|
+
}
|
|
1750
|
+
function resolveIo(overrides) {
|
|
1751
|
+
return {
|
|
1752
|
+
writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
|
|
1753
|
+
rename: overrides?.rename ?? DEFAULT_IO.rename,
|
|
1754
|
+
remove: overrides?.remove ?? DEFAULT_IO.remove
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function commitOperation(context, operation) {
|
|
1758
|
+
const { plan, io, accumulator } = context;
|
|
1759
|
+
if (operation.kind === "noop")
|
|
1760
|
+
return;
|
|
1761
|
+
if (operation.kind === "text") {
|
|
1762
|
+
io.writeFile(operation.path, operation.afterText);
|
|
1763
|
+
accumulator.mutations.push({
|
|
1764
|
+
kind: "text",
|
|
1765
|
+
path: operation.path,
|
|
1766
|
+
beforeText: operation.beforeText,
|
|
1767
|
+
afterText: operation.afterText
|
|
1768
|
+
});
|
|
1769
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1770
|
+
accumulator.totalEdits += operation.editCount;
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
if (operation.kind === "create") {
|
|
1774
|
+
io.writeFile(operation.path, "");
|
|
1775
|
+
accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
|
|
1776
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (operation.kind === "rename") {
|
|
1780
|
+
if (operation.replaceDestination) {
|
|
1781
|
+
const targetKind = existsSync4(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
|
|
1782
|
+
io.remove(operation.newPath, targetKind === "directory");
|
|
1783
|
+
accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
|
|
1784
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1785
|
+
}
|
|
1786
|
+
io.rename(operation.oldPath, operation.newPath);
|
|
1787
|
+
accumulator.mutations.push({
|
|
1788
|
+
kind: "rename",
|
|
1789
|
+
oldPath: operation.oldPath,
|
|
1790
|
+
newPath: operation.newPath,
|
|
1791
|
+
sourceKind: operation.sourceKind
|
|
1792
|
+
});
|
|
1793
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
io.remove(operation.path, operation.recursive);
|
|
1797
|
+
accumulator.mutations.push({
|
|
1798
|
+
kind: "delete",
|
|
1799
|
+
path: operation.path,
|
|
1800
|
+
targetKind: operation.targetKind
|
|
1801
|
+
});
|
|
1802
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1803
|
+
}
|
|
1804
|
+
function commitWorkspaceEditPlan(plan, options = {}) {
|
|
1805
|
+
if (options.signal?.aborted) {
|
|
1806
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
1807
|
+
}
|
|
1808
|
+
const stale = verifySnapshots(plan);
|
|
1809
|
+
if (stale)
|
|
1810
|
+
return stale;
|
|
1811
|
+
if (options.signal?.aborted) {
|
|
1812
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
1813
|
+
}
|
|
1814
|
+
const io = resolveIo(options.io);
|
|
1815
|
+
const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
|
|
1816
|
+
const context = { plan, io, accumulator };
|
|
1817
|
+
let lateAbort = false;
|
|
1818
|
+
for (const operation of plan.operations) {
|
|
1819
|
+
try {
|
|
1820
|
+
commitOperation(context, operation);
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1823
|
+
return failedCommit(plan, {
|
|
1824
|
+
message: `I/O failure during ${operation.kind}: ${detail}`,
|
|
1825
|
+
changeIndex: operation.changeIndex,
|
|
1826
|
+
mutations: accumulator.mutations,
|
|
1827
|
+
filesModified: accumulator.filesModified,
|
|
1828
|
+
totalEdits: accumulator.totalEdits,
|
|
1829
|
+
lateAbort: lateAbort || options.signal?.aborted === true
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
if (options.signal?.aborted)
|
|
1833
|
+
lateAbort = true;
|
|
1834
|
+
}
|
|
1835
|
+
const result = {
|
|
1836
|
+
success: true,
|
|
1837
|
+
filesModified: accumulator.filesModified,
|
|
1838
|
+
totalEdits: accumulator.totalEdits,
|
|
1839
|
+
errors: [],
|
|
1840
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1841
|
+
};
|
|
1842
|
+
return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
|
|
1846
|
+
import { createHash } from "node:crypto";
|
|
1847
|
+
function canonicalFingerprint(operations) {
|
|
1848
|
+
const canonical = operations.map((operation) => {
|
|
1849
|
+
switch (operation.kind) {
|
|
1850
|
+
case "text":
|
|
1851
|
+
return {
|
|
1852
|
+
kind: operation.kind,
|
|
1853
|
+
changeIndex: operation.changeIndex,
|
|
1854
|
+
path: operation.path,
|
|
1855
|
+
edits: operation.edits,
|
|
1856
|
+
version: operation.version
|
|
1857
|
+
};
|
|
1858
|
+
case "rename":
|
|
1859
|
+
return {
|
|
1860
|
+
kind: operation.kind,
|
|
1861
|
+
changeIndex: operation.changeIndex,
|
|
1862
|
+
oldPath: operation.oldPath,
|
|
1863
|
+
newPath: operation.newPath,
|
|
1864
|
+
overwrite: operation.overwrite,
|
|
1865
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
1866
|
+
};
|
|
1867
|
+
case "create":
|
|
1868
|
+
return {
|
|
1869
|
+
kind: operation.kind,
|
|
1870
|
+
changeIndex: operation.changeIndex,
|
|
1871
|
+
path: operation.path,
|
|
1872
|
+
overwrite: operation.overwrite,
|
|
1873
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
1874
|
+
};
|
|
1875
|
+
case "delete":
|
|
1876
|
+
return {
|
|
1877
|
+
kind: operation.kind,
|
|
1878
|
+
changeIndex: operation.changeIndex,
|
|
1879
|
+
path: operation.path,
|
|
1880
|
+
recursive: operation.recursive,
|
|
1881
|
+
ignoreIfNotExists: operation.ignoreIfNotExists
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
});
|
|
1885
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// ../lsp-core/src/lsp/workspace-edit-types.ts
|
|
1889
|
+
class WorkspaceEditValidationError extends Error {
|
|
1890
|
+
changeIndex;
|
|
1891
|
+
detail;
|
|
1892
|
+
name = "WorkspaceEditValidationError";
|
|
1893
|
+
constructor(changeIndex, detail) {
|
|
1894
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
1895
|
+
this.changeIndex = changeIndex;
|
|
1896
|
+
this.detail = detail;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
|
|
1901
|
+
function isRecord3(value) {
|
|
1902
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1903
|
+
}
|
|
1904
|
+
function parsePosition(value) {
|
|
1905
|
+
if (!isRecord3(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
return { line: value["line"], character: value["character"] };
|
|
1909
|
+
}
|
|
1910
|
+
function parseRange(value) {
|
|
1911
|
+
if (!isRecord3(value))
|
|
1912
|
+
return null;
|
|
1913
|
+
const start = parsePosition(value["start"]);
|
|
1914
|
+
const end = parsePosition(value["end"]);
|
|
1915
|
+
return start && end ? { start, end } : null;
|
|
1916
|
+
}
|
|
1917
|
+
function parseTextEdits(value, changeIndex) {
|
|
1918
|
+
if (!Array.isArray(value)) {
|
|
1919
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
|
|
1920
|
+
}
|
|
1921
|
+
const edits = [];
|
|
1922
|
+
for (const candidate of value) {
|
|
1923
|
+
if (!isRecord3(candidate) || typeof candidate["newText"] !== "string") {
|
|
1924
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
|
|
1925
|
+
}
|
|
1926
|
+
if ("annotationId" in candidate) {
|
|
1927
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
|
|
1928
|
+
}
|
|
1929
|
+
const range = parseRange(candidate["range"]);
|
|
1930
|
+
if (!range)
|
|
1931
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
|
|
1932
|
+
edits.push({ range, newText: candidate["newText"] });
|
|
1933
|
+
}
|
|
1934
|
+
return edits;
|
|
1935
|
+
}
|
|
1936
|
+
function parseBooleanOption(options, key, changeIndex) {
|
|
1937
|
+
const value = options[key];
|
|
1938
|
+
if (value === undefined)
|
|
1939
|
+
return false;
|
|
1940
|
+
if (typeof value !== "boolean") {
|
|
1941
|
+
throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
|
|
1942
|
+
}
|
|
1943
|
+
return value;
|
|
1944
|
+
}
|
|
1945
|
+
function parseOptions(value, allowed, changeIndex) {
|
|
1946
|
+
if (value === undefined)
|
|
1947
|
+
return {};
|
|
1948
|
+
if (!isRecord3(value))
|
|
1949
|
+
throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
|
|
1950
|
+
for (const key of Object.keys(value)) {
|
|
1951
|
+
if (!allowed.includes(key))
|
|
1952
|
+
throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
|
|
1953
|
+
}
|
|
1954
|
+
const parsed = {};
|
|
1955
|
+
for (const key of allowed)
|
|
1956
|
+
parsed[key] = parseBooleanOption(value, key, changeIndex);
|
|
1957
|
+
return parsed;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
|
|
1961
|
+
function parseResourceChange(input) {
|
|
1962
|
+
const kind = input.change["kind"];
|
|
1963
|
+
if (kind === "create" || kind === "delete") {
|
|
1964
|
+
parseSinglePathResource(input, kind);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (kind !== "rename") {
|
|
1968
|
+
throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
|
|
1969
|
+
}
|
|
1970
|
+
parseRename(input);
|
|
1971
|
+
}
|
|
1972
|
+
function parseSinglePathResource(input, kind) {
|
|
1973
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1974
|
+
if (typeof change["uri"] !== "string")
|
|
1975
|
+
throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
|
|
1976
|
+
const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
|
|
1977
|
+
if (!resolvedPath.success) {
|
|
1978
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
if (kind === "create") {
|
|
1982
|
+
const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
1983
|
+
target.operations.push({
|
|
1984
|
+
kind,
|
|
1985
|
+
changeIndex,
|
|
1986
|
+
path: resolvedPath.path,
|
|
1987
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1988
|
+
overwrite: options2["overwrite"] ?? false,
|
|
1989
|
+
ignoreIfExists: options2["ignoreIfExists"] ?? false,
|
|
1990
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
1991
|
+
});
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
|
|
1995
|
+
target.operations.push({
|
|
1996
|
+
kind,
|
|
1997
|
+
changeIndex,
|
|
1998
|
+
path: resolvedPath.path,
|
|
1999
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2000
|
+
recursive: options["recursive"] ?? false,
|
|
2001
|
+
ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
|
|
2002
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
function parseRename(input) {
|
|
2006
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2007
|
+
if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
|
|
2008
|
+
throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
|
|
2009
|
+
}
|
|
2010
|
+
const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
|
|
2011
|
+
const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
|
|
2012
|
+
if (!oldPath.success || !newPath.success) {
|
|
2013
|
+
target.failures.push({
|
|
2014
|
+
changeIndex,
|
|
2015
|
+
message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
|
|
2016
|
+
});
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2020
|
+
target.operations.push({
|
|
2021
|
+
kind: "rename",
|
|
2022
|
+
changeIndex,
|
|
2023
|
+
oldPath: oldPath.path,
|
|
2024
|
+
newPath: newPath.path,
|
|
2025
|
+
reportedOldPath: oldPath.requestedPath,
|
|
2026
|
+
reportedNewPath: newPath.requestedPath,
|
|
2027
|
+
overwrite: options["overwrite"] ?? false,
|
|
2028
|
+
ignoreIfExists: options["ignoreIfExists"] ?? false,
|
|
2029
|
+
followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// ../lsp-core/src/lsp/workspace-edit-parser.ts
|
|
2034
|
+
function failureResult(failures) {
|
|
2035
|
+
const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
|
|
2036
|
+
const first = sorted[0];
|
|
2037
|
+
return {
|
|
2038
|
+
success: false,
|
|
2039
|
+
filesModified: [],
|
|
2040
|
+
totalEdits: 0,
|
|
2041
|
+
errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
|
|
2042
|
+
...first ? { failedChange: first.changeIndex } : {}
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
function parseWorkspaceEdit(edit, workspaceRoot) {
|
|
2046
|
+
if (!isRecord3(edit))
|
|
2047
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
|
|
2048
|
+
if (edit["changeAnnotations"] !== undefined) {
|
|
2049
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
|
|
2050
|
+
}
|
|
2051
|
+
const hasChanges = edit["changes"] !== undefined;
|
|
2052
|
+
const hasDocumentChanges = edit["documentChanges"] !== undefined;
|
|
2053
|
+
if (hasChanges && hasDocumentChanges) {
|
|
2054
|
+
return {
|
|
2055
|
+
operations: [],
|
|
2056
|
+
failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
const target = { operations: [], failures: [] };
|
|
2060
|
+
if (hasChanges)
|
|
2061
|
+
return parseChanges(edit["changes"], workspaceRoot, target);
|
|
2062
|
+
return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
|
|
2063
|
+
}
|
|
2064
|
+
function parseChanges(value, workspaceRoot, target) {
|
|
2065
|
+
if (!isRecord3(value))
|
|
2066
|
+
return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
|
|
2067
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
2068
|
+
for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
|
|
2069
|
+
const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
|
|
2070
|
+
if (!resolvedPath.success) {
|
|
2071
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
try {
|
|
2075
|
+
target.operations.push({
|
|
2076
|
+
kind: "text",
|
|
2077
|
+
changeIndex,
|
|
2078
|
+
path: resolvedPath.path,
|
|
2079
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2080
|
+
edits: parseTextEdits(rawEdits, changeIndex),
|
|
2081
|
+
version: null
|
|
2082
|
+
});
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2085
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
throw error;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
return target;
|
|
2092
|
+
}
|
|
2093
|
+
function parseDocumentChanges(value, workspaceRoot, target) {
|
|
2094
|
+
if (value === undefined)
|
|
2095
|
+
return target;
|
|
2096
|
+
if (!Array.isArray(value)) {
|
|
2097
|
+
return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
|
|
2098
|
+
}
|
|
2099
|
+
for (const [changeIndex, change] of value.entries()) {
|
|
2100
|
+
try {
|
|
2101
|
+
parseDocumentChange({ change, changeIndex, workspaceRoot, target });
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2104
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
throw error;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
return target;
|
|
2111
|
+
}
|
|
2112
|
+
function parseDocumentChange(input) {
|
|
2113
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2114
|
+
if (!isRecord3(change))
|
|
2115
|
+
throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
|
|
2116
|
+
if ("annotationId" in change) {
|
|
2117
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
|
|
2118
|
+
}
|
|
2119
|
+
if (typeof change["kind"] === "string") {
|
|
2120
|
+
parseResourceChange({ change, changeIndex, workspaceRoot, target });
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
const identifier = change["textDocument"];
|
|
2124
|
+
if (!isRecord3(identifier) || typeof identifier["uri"] !== "string") {
|
|
2125
|
+
throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
|
|
2126
|
+
}
|
|
2127
|
+
const version = identifier["version"];
|
|
2128
|
+
if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
|
|
2129
|
+
throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
|
|
2130
|
+
}
|
|
2131
|
+
const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
|
|
2132
|
+
if (!resolvedPath.success) {
|
|
2133
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
target.operations.push({
|
|
2137
|
+
kind: "text",
|
|
2138
|
+
changeIndex,
|
|
2139
|
+
path: resolvedPath.path,
|
|
2140
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2141
|
+
edits: parseTextEdits(change["edits"], changeIndex),
|
|
2142
|
+
version
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2147
|
+
import { dirname as dirname3, relative as relative4, resolve as resolve4 } from "node:path";
|
|
2148
|
+
|
|
2149
|
+
// ../lsp-core/src/lsp/workspace-edit-text.ts
|
|
2150
|
+
function comparePosition(left, right) {
|
|
2151
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
2152
|
+
}
|
|
2153
|
+
function positionsEqual(left, right) {
|
|
2154
|
+
return left.line === right.line && left.character === right.character;
|
|
2155
|
+
}
|
|
2156
|
+
function rangesEqual(left, right) {
|
|
2157
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
2158
|
+
}
|
|
2159
|
+
function isEmptyRange(range) {
|
|
2160
|
+
return positionsEqual(range.start, range.end);
|
|
2161
|
+
}
|
|
2162
|
+
function formatRange(range) {
|
|
2163
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
2164
|
+
}
|
|
2165
|
+
function validatePosition(position, label, context) {
|
|
2166
|
+
const { lines, changeIndex } = context;
|
|
2167
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
2168
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
2169
|
+
}
|
|
2170
|
+
if (position.line < 0 || position.character < 0) {
|
|
2171
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
2172
|
+
}
|
|
2173
|
+
const line = lines[position.line];
|
|
2174
|
+
if (line === undefined) {
|
|
2175
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
2176
|
+
}
|
|
2177
|
+
if (position.character > line.length) {
|
|
2178
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
function validateRange(range, lines, changeIndex) {
|
|
2182
|
+
const context = { lines, changeIndex };
|
|
2183
|
+
validatePosition(range.start, "start", context);
|
|
2184
|
+
validatePosition(range.end, "end", context);
|
|
2185
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
2186
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
function sortAndDeduplicate(edits) {
|
|
2190
|
+
const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
|
|
2191
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
2192
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
2193
|
+
});
|
|
2194
|
+
const unique = [];
|
|
2195
|
+
for (const entry of sorted) {
|
|
2196
|
+
const previous = unique.at(-1);
|
|
2197
|
+
if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
unique.push(entry.edit);
|
|
2201
|
+
}
|
|
2202
|
+
return unique;
|
|
2203
|
+
}
|
|
2204
|
+
function validateNoOverlap(edits, changeIndex) {
|
|
2205
|
+
for (let index = 0;index < edits.length - 1; index += 1) {
|
|
2206
|
+
const later = edits[index];
|
|
2207
|
+
const earlier = edits[index + 1];
|
|
2208
|
+
if (later === undefined || earlier === undefined)
|
|
2209
|
+
continue;
|
|
2210
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
2211
|
+
throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
function applyNormalizedTextEdits(content, edits) {
|
|
2216
|
+
const lines = content.split(`
|
|
2217
|
+
`);
|
|
2218
|
+
for (const edit of edits) {
|
|
2219
|
+
const { start, end } = edit.range;
|
|
2220
|
+
const startLine = lines[start.line];
|
|
2221
|
+
const endLine = lines[end.line];
|
|
2222
|
+
if (startLine === undefined || endLine === undefined)
|
|
2223
|
+
continue;
|
|
2224
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
2225
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
|
|
2226
|
+
`));
|
|
2227
|
+
}
|
|
2228
|
+
return lines.join(`
|
|
2229
|
+
`);
|
|
2230
|
+
}
|
|
2231
|
+
function normalizeTextEdits(content, edits, changeIndex) {
|
|
2232
|
+
const lines = content.split(`
|
|
2233
|
+
`);
|
|
2234
|
+
for (const edit of edits) {
|
|
2235
|
+
validateRange(edit.range, lines, changeIndex);
|
|
2236
|
+
}
|
|
2237
|
+
const normalized = sortAndDeduplicate(edits);
|
|
2238
|
+
validateNoOverlap(normalized, changeIndex);
|
|
2239
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2243
|
+
function isSameOrDescendant2(candidate, parent) {
|
|
2244
|
+
const relativePath = relative4(parent, candidate);
|
|
2245
|
+
return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
|
|
2246
|
+
}
|
|
2247
|
+
function removeVirtualSubtree(virtual, path) {
|
|
2248
|
+
for (const candidate of [...virtual.keys()]) {
|
|
2249
|
+
if (isSameOrDescendant2(candidate, path))
|
|
2250
|
+
virtual.delete(candidate);
|
|
2251
|
+
}
|
|
2252
|
+
virtual.set(path, { kind: "missing" });
|
|
2253
|
+
}
|
|
2254
|
+
function moveVirtualSubtree(virtual, oldPath, newPath) {
|
|
2255
|
+
const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
|
|
2256
|
+
removeVirtualSubtree(virtual, oldPath);
|
|
2257
|
+
removeVirtualSubtree(virtual, newPath);
|
|
2258
|
+
for (const [candidate, entry] of moved) {
|
|
2259
|
+
const suffix = relative4(oldPath, candidate);
|
|
2260
|
+
virtual.set(suffix === "" ? newPath : resolve4(newPath, suffix), entry);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
function virtualDirectoryHasChildren(virtual, path) {
|
|
2264
|
+
for (const [candidate, entry] of virtual) {
|
|
2265
|
+
if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
|
|
2266
|
+
return true;
|
|
2267
|
+
}
|
|
2268
|
+
return false;
|
|
2269
|
+
}
|
|
2270
|
+
function requireVirtualParent(virtual, path, changeIndex) {
|
|
2271
|
+
if (virtual.get(dirname3(path))?.kind !== "directory") {
|
|
2272
|
+
throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function simulateOperations(parsed, snapshots) {
|
|
2276
|
+
const virtual = new Map(snapshots);
|
|
2277
|
+
const planned = [];
|
|
2278
|
+
const failures = [];
|
|
2279
|
+
for (const operation of parsed) {
|
|
2280
|
+
try {
|
|
2281
|
+
planned.push(simulateOperation(operation, virtual));
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2284
|
+
failures.push({ changeIndex: operation.changeIndex, message: error.detail });
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
throw error;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return { operations: planned, failures };
|
|
2291
|
+
}
|
|
2292
|
+
function simulateOperation(operation, virtual) {
|
|
2293
|
+
switch (operation.kind) {
|
|
2294
|
+
case "text":
|
|
2295
|
+
return simulateText(operation, virtual);
|
|
2296
|
+
case "create":
|
|
2297
|
+
return simulateCreate(operation, virtual);
|
|
2298
|
+
case "rename":
|
|
2299
|
+
return simulateRename(operation, virtual);
|
|
2300
|
+
case "delete":
|
|
2301
|
+
return simulateDelete(operation, virtual);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
function rejectSymbolicLink(operation) {
|
|
2305
|
+
if (operation.followedSymbolicLink) {
|
|
2306
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
function simulateText(operation, virtual) {
|
|
2310
|
+
const entry = virtual.get(operation.path);
|
|
2311
|
+
if (entry?.kind !== "file")
|
|
2312
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
|
|
2313
|
+
const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
|
|
2314
|
+
virtual.set(operation.path, { kind: "file", content: normalized.text });
|
|
2315
|
+
return {
|
|
2316
|
+
kind: "text",
|
|
2317
|
+
changeIndex: operation.changeIndex,
|
|
2318
|
+
path: operation.path,
|
|
2319
|
+
beforeText: entry.content,
|
|
2320
|
+
afterText: normalized.text,
|
|
2321
|
+
editCount: normalized.edits.length,
|
|
2322
|
+
documentVersion: operation.version
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
function simulateCreate(operation, virtual) {
|
|
2326
|
+
rejectSymbolicLink(operation);
|
|
2327
|
+
requireVirtualParent(virtual, operation.path, operation.changeIndex);
|
|
2328
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2329
|
+
if (target.kind !== "missing") {
|
|
2330
|
+
if (operation.overwrite && target.kind === "file") {
|
|
2331
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2332
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
|
|
2333
|
+
}
|
|
2334
|
+
if (operation.ignoreIfExists)
|
|
2335
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2336
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
|
|
2337
|
+
}
|
|
2338
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2339
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
|
|
2340
|
+
}
|
|
2341
|
+
function simulateRename(operation, virtual) {
|
|
2342
|
+
rejectSymbolicLink(operation);
|
|
2343
|
+
const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
|
|
2344
|
+
if (source.kind === "missing") {
|
|
2345
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
|
|
2346
|
+
}
|
|
2347
|
+
if (operation.oldPath === operation.newPath)
|
|
2348
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2349
|
+
if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
|
|
2350
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
|
|
2351
|
+
}
|
|
2352
|
+
requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
|
|
2353
|
+
const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
|
|
2354
|
+
if (destination.kind !== "missing" && !operation.overwrite) {
|
|
2355
|
+
if (operation.ignoreIfExists)
|
|
2356
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2357
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
|
|
2358
|
+
}
|
|
2359
|
+
moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
|
|
2360
|
+
return {
|
|
2361
|
+
kind: "rename",
|
|
2362
|
+
changeIndex: operation.changeIndex,
|
|
2363
|
+
oldPath: operation.oldPath,
|
|
2364
|
+
newPath: operation.newPath,
|
|
2365
|
+
sourceKind: source.kind,
|
|
2366
|
+
replaceDestination: destination.kind !== "missing"
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
function simulateDelete(operation, virtual) {
|
|
2370
|
+
rejectSymbolicLink(operation);
|
|
2371
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2372
|
+
if (target.kind === "missing") {
|
|
2373
|
+
if (operation.ignoreIfNotExists)
|
|
2374
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2375
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
|
|
2376
|
+
}
|
|
2377
|
+
if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
|
|
2378
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
|
|
2379
|
+
}
|
|
2380
|
+
removeVirtualSubtree(virtual, operation.path);
|
|
2381
|
+
return {
|
|
2382
|
+
kind: "delete",
|
|
2383
|
+
changeIndex: operation.changeIndex,
|
|
2384
|
+
path: operation.path,
|
|
2385
|
+
targetKind: target.kind,
|
|
2386
|
+
recursive: operation.recursive
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// ../lsp-core/src/lsp/workspace-edit-snapshot.ts
|
|
2391
|
+
import { existsSync as existsSync5, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2392
|
+
import { dirname as dirname4, resolve as resolve5 } from "node:path";
|
|
2393
|
+
class WorkspaceSnapshotBuilder {
|
|
2394
|
+
workspaceRoot;
|
|
2395
|
+
snapshots = new Map;
|
|
2396
|
+
constructor(workspaceRoot) {
|
|
2397
|
+
this.workspaceRoot = workspaceRoot;
|
|
2398
|
+
}
|
|
2399
|
+
build(operations) {
|
|
2400
|
+
this.add(this.workspaceRoot, false);
|
|
2401
|
+
for (const operation of operations) {
|
|
2402
|
+
switch (operation.kind) {
|
|
2403
|
+
case "rename":
|
|
2404
|
+
this.add(operation.oldPath, true);
|
|
2405
|
+
this.add(operation.newPath, true);
|
|
2406
|
+
break;
|
|
2407
|
+
case "delete":
|
|
2408
|
+
this.add(operation.path, true);
|
|
2409
|
+
break;
|
|
2410
|
+
case "text":
|
|
2411
|
+
case "create":
|
|
2412
|
+
this.add(operation.path, false);
|
|
2413
|
+
break;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
return this.snapshots;
|
|
2417
|
+
}
|
|
2418
|
+
add(path, includeChildren) {
|
|
2419
|
+
let candidate = path;
|
|
2420
|
+
while (true) {
|
|
2421
|
+
const existing = this.snapshots.get(candidate);
|
|
2422
|
+
if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
|
|
2423
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
|
|
2424
|
+
}
|
|
2425
|
+
if (candidate === this.workspaceRoot)
|
|
2426
|
+
break;
|
|
2427
|
+
candidate = dirname4(candidate);
|
|
2428
|
+
}
|
|
2429
|
+
if (!includeChildren || !existsSync5(path) || !lstatSync3(path).isDirectory())
|
|
2430
|
+
return;
|
|
2431
|
+
for (const child of readdirSync2(path))
|
|
2432
|
+
this.add(resolve5(path, child), true);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
function snapshotOperations(operations, workspaceRoot) {
|
|
2436
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// ../lsp-core/src/lsp/workspace-edit-plan.ts
|
|
2440
|
+
class PlanPathIndex {
|
|
2441
|
+
firstChangeByPath = new Map;
|
|
2442
|
+
reportedPathByCanonical = new Map;
|
|
2443
|
+
build(operations) {
|
|
2444
|
+
for (const operation of operations) {
|
|
2445
|
+
switch (operation.kind) {
|
|
2446
|
+
case "rename":
|
|
2447
|
+
this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
|
|
2448
|
+
this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
|
|
2449
|
+
break;
|
|
2450
|
+
case "text":
|
|
2451
|
+
case "create":
|
|
2452
|
+
case "delete":
|
|
2453
|
+
this.add(operation.path, operation.reportedPath, operation.changeIndex);
|
|
2454
|
+
break;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
add(path, reportedPath2, changeIndex) {
|
|
2459
|
+
if (!this.firstChangeByPath.has(path))
|
|
2460
|
+
this.firstChangeByPath.set(path, changeIndex);
|
|
2461
|
+
if (!this.reportedPathByCanonical.has(path))
|
|
2462
|
+
this.reportedPathByCanonical.set(path, reportedPath2);
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
function fingerprintWorkspaceEdit(edit, workspaceRoot) {
|
|
2466
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2467
|
+
if (!root.success)
|
|
2468
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2469
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2470
|
+
if (parsed.failures.length > 0)
|
|
2471
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2472
|
+
return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
|
|
2473
|
+
}
|
|
2474
|
+
function planWorkspaceEdit(edit, workspaceRoot) {
|
|
2475
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2476
|
+
if (!root.success)
|
|
2477
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2478
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2479
|
+
if (parsed.failures.length > 0)
|
|
2480
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2481
|
+
let snapshots;
|
|
2482
|
+
try {
|
|
2483
|
+
snapshots = snapshotOperations(parsed.operations, root.path);
|
|
2484
|
+
} catch (error) {
|
|
2485
|
+
return {
|
|
2486
|
+
success: false,
|
|
2487
|
+
result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
const simulated = simulateOperations(parsed.operations, snapshots);
|
|
2491
|
+
if (simulated.failures.length > 0)
|
|
2492
|
+
return { success: false, result: failureResult(simulated.failures) };
|
|
2493
|
+
const paths = new PlanPathIndex;
|
|
2494
|
+
paths.build(parsed.operations);
|
|
2495
|
+
const plan = {
|
|
2496
|
+
workspaceRoot: root.path,
|
|
2497
|
+
operations: simulated.operations,
|
|
2498
|
+
snapshots,
|
|
2499
|
+
firstChangeByPath: paths.firstChangeByPath,
|
|
2500
|
+
reportedPathByCanonical: paths.reportedPathByCanonical,
|
|
2501
|
+
fingerprint: canonicalFingerprint(parsed.operations)
|
|
2502
|
+
};
|
|
2503
|
+
return { success: true, plan };
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// ../lsp-core/src/lsp/workspace-mutation-controller.ts
|
|
2507
|
+
function failure(message, failedChange, base) {
|
|
2508
|
+
return {
|
|
2509
|
+
success: false,
|
|
2510
|
+
filesModified: base?.filesModified ?? [],
|
|
2511
|
+
totalEdits: base?.totalEdits ?? 0,
|
|
2512
|
+
errors: [message],
|
|
2513
|
+
...failedChange === undefined ? {} : { failedChange },
|
|
2514
|
+
...base?.lateAbort ? { lateAbort: true } : {}
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
function responseFor(result) {
|
|
2518
|
+
if (result.success)
|
|
2519
|
+
return { applied: true };
|
|
2520
|
+
return {
|
|
2521
|
+
applied: false,
|
|
2522
|
+
failureReason: result.errors[0] ?? "workspace edit failed",
|
|
2523
|
+
...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
function isRecord4(value) {
|
|
2527
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
class WorkspaceMutationController {
|
|
2531
|
+
workspaceRoot;
|
|
2532
|
+
documents;
|
|
2533
|
+
activeLease = null;
|
|
2534
|
+
nextLeaseId = 1;
|
|
2535
|
+
io;
|
|
2536
|
+
constructor(workspaceRoot, documents) {
|
|
2537
|
+
this.workspaceRoot = workspaceRoot;
|
|
2538
|
+
this.documents = documents;
|
|
2539
|
+
}
|
|
2540
|
+
setIo(io) {
|
|
2541
|
+
this.io = io;
|
|
2542
|
+
}
|
|
2543
|
+
acquire(signal) {
|
|
2544
|
+
if (this.activeLease)
|
|
2545
|
+
return { success: false, result: failure("workspace mutation is already in progress") };
|
|
2546
|
+
if (signal?.aborted)
|
|
2547
|
+
return { success: false, result: failure("cancelled before mutating request") };
|
|
2548
|
+
const lease = {
|
|
2549
|
+
id: this.nextLeaseId,
|
|
2550
|
+
phase: "idle",
|
|
2551
|
+
...signal === undefined ? {} : { signal }
|
|
2552
|
+
};
|
|
2553
|
+
this.nextLeaseId += 1;
|
|
2554
|
+
this.activeLease = lease;
|
|
2555
|
+
return { success: true, lease };
|
|
2556
|
+
}
|
|
2557
|
+
release(lease) {
|
|
2558
|
+
if (this.activeLease?.id !== lease.id)
|
|
2559
|
+
return;
|
|
2560
|
+
this.activeLease.phase = "sealed";
|
|
2561
|
+
this.activeLease = null;
|
|
2562
|
+
}
|
|
2563
|
+
isBeforeCommit(lease) {
|
|
2564
|
+
return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
|
|
2565
|
+
}
|
|
2566
|
+
async handleApplyEdit(params) {
|
|
2567
|
+
const lease = this.activeLease;
|
|
2568
|
+
if (!lease)
|
|
2569
|
+
return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
|
|
2570
|
+
if (lease.phase !== "idle") {
|
|
2571
|
+
return {
|
|
2572
|
+
applied: false,
|
|
2573
|
+
failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
lease.phase = "applying";
|
|
2577
|
+
lease.applyCompletion = new Promise((resolve6) => {
|
|
2578
|
+
lease.resolveApply = resolve6;
|
|
2579
|
+
});
|
|
2580
|
+
const edit = isRecord4(params) ? params["edit"] : undefined;
|
|
2581
|
+
const record = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
|
|
2582
|
+
lease.serverApply = record;
|
|
2583
|
+
lease.phase = "settled";
|
|
2584
|
+
lease.resolveApply?.();
|
|
2585
|
+
return responseFor(record.result);
|
|
2586
|
+
}
|
|
2587
|
+
async reconcileRename(leaseToken, edit) {
|
|
2588
|
+
const lease = this.requireActiveLease(leaseToken);
|
|
2589
|
+
if (!lease)
|
|
2590
|
+
return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
|
|
2591
|
+
if (lease.phase === "applying")
|
|
2592
|
+
await lease.applyCompletion;
|
|
2593
|
+
if (lease.serverApply)
|
|
2594
|
+
return this.reconcileServerApply(lease.serverApply, edit);
|
|
2595
|
+
lease.phase = "sealed";
|
|
2596
|
+
if (!edit)
|
|
2597
|
+
return { edit, apply: failure("No edit provided") };
|
|
2598
|
+
const applied = await this.applyEdit(edit, lease);
|
|
2599
|
+
return { edit, apply: applied.result };
|
|
2600
|
+
}
|
|
2601
|
+
reconcileServerApply(record, edit) {
|
|
2602
|
+
if (!edit)
|
|
2603
|
+
return { edit, apply: record.result };
|
|
2604
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
|
|
2605
|
+
if (fingerprint.success && record.fingerprint !== null && fingerprint.fingerprint === record.fingerprint) {
|
|
2606
|
+
return { edit, apply: record.result };
|
|
2607
|
+
}
|
|
2608
|
+
return {
|
|
2609
|
+
edit,
|
|
2610
|
+
apply: failure("rename result conflicts with server-applied workspace edit", 0, record.result)
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
async applyEdit(edit, lease) {
|
|
2614
|
+
const planned = planWorkspaceEdit(edit, this.workspaceRoot);
|
|
2615
|
+
if (!planned.success)
|
|
2616
|
+
return { fingerprint: null, result: planned.result };
|
|
2617
|
+
const versionFailure = this.documents.validateVersions(planned.plan.operations);
|
|
2618
|
+
if (versionFailure) {
|
|
2619
|
+
return {
|
|
2620
|
+
fingerprint: planned.plan.fingerprint,
|
|
2621
|
+
result: failure(versionFailure.message, versionFailure.changeIndex)
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
const commit = commitWorkspaceEditPlan(planned.plan, {
|
|
2625
|
+
...lease.signal === undefined ? {} : { signal: lease.signal },
|
|
2626
|
+
...this.io === undefined ? {} : { io: this.io }
|
|
2627
|
+
});
|
|
2628
|
+
let result = commit.result;
|
|
2629
|
+
if (commit.delta.operations.length > 0) {
|
|
2630
|
+
try {
|
|
2631
|
+
await this.documents.synchronize(commit.delta);
|
|
2632
|
+
} catch (error) {
|
|
2633
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2634
|
+
result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
if (lease.signal?.aborted && !result.lateAbort)
|
|
2638
|
+
result = { ...result, lateAbort: true };
|
|
2639
|
+
return { fingerprint: planned.plan.fingerprint, result };
|
|
2640
|
+
}
|
|
2641
|
+
requireActiveLease(lease) {
|
|
2642
|
+
return this.activeLease?.id === lease.id ? this.activeLease : null;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
1000
2646
|
// ../lsp-core/src/lsp/client.ts
|
|
1001
|
-
var
|
|
1002
|
-
var
|
|
2647
|
+
var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
|
|
2648
|
+
var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1003
2649
|
|
|
1004
2650
|
class LspClient extends LspClientConnection {
|
|
1005
|
-
openedFiles = new Set;
|
|
1006
|
-
documentVersions = new Map;
|
|
1007
|
-
lastSyncedText = new Map;
|
|
1008
2651
|
diagnosticPullErrors = [];
|
|
2652
|
+
documents;
|
|
2653
|
+
workspaceMutations;
|
|
2654
|
+
diagnosticsFreshnessTimeoutMs;
|
|
2655
|
+
constructor(root, server, options = {}) {
|
|
2656
|
+
super(root, server, options);
|
|
2657
|
+
this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
|
|
2658
|
+
this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
|
|
2659
|
+
versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
|
|
2660
|
+
});
|
|
2661
|
+
this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
|
|
2662
|
+
this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
|
|
2663
|
+
}
|
|
1009
2664
|
getDiagnosticPullErrors() {
|
|
1010
2665
|
return this.diagnosticPullErrors;
|
|
1011
2666
|
}
|
|
1012
2667
|
async openFile(filePath) {
|
|
1013
|
-
const absPath =
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
textDocument: {
|
|
1022
|
-
uri,
|
|
1023
|
-
languageId,
|
|
1024
|
-
version,
|
|
1025
|
-
text
|
|
1026
|
-
}
|
|
1027
|
-
});
|
|
1028
|
-
this.openedFiles.add(absPath);
|
|
1029
|
-
this.documentVersions.set(uri, version);
|
|
1030
|
-
this.lastSyncedText.set(uri, text);
|
|
1031
|
-
await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
const prevText = this.lastSyncedText.get(uri);
|
|
1035
|
-
if (prevText === text) {
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
|
|
1039
|
-
this.documentVersions.set(uri, nextVersion);
|
|
1040
|
-
this.lastSyncedText.set(uri, text);
|
|
1041
|
-
await this.sendNotification("textDocument/didChange", {
|
|
1042
|
-
textDocument: { uri, version: nextVersion },
|
|
1043
|
-
contentChanges: [{ text }]
|
|
1044
|
-
});
|
|
1045
|
-
await this.sendNotification("textDocument/didSave", {
|
|
1046
|
-
textDocument: { uri },
|
|
1047
|
-
text
|
|
1048
|
-
});
|
|
2668
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2669
|
+
await this.documents.openFile(absPath);
|
|
2670
|
+
}
|
|
2671
|
+
getOpenDocumentVersion(filePath) {
|
|
2672
|
+
return this.documents.getVersion(this.resolveWorkspacePath(filePath));
|
|
2673
|
+
}
|
|
2674
|
+
getStoredDiagnostics(uri) {
|
|
2675
|
+
return [...this.documents.getStoredDiagnostics(uri)];
|
|
1049
2676
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
2677
|
+
setWorkspaceEditIo(io) {
|
|
2678
|
+
this.workspaceMutations.setIo(io);
|
|
2679
|
+
}
|
|
2680
|
+
handlePublishDiagnostics(params) {
|
|
2681
|
+
super.handlePublishDiagnostics(params);
|
|
2682
|
+
this.documents.recordPublishedDiagnostics(params);
|
|
2683
|
+
}
|
|
2684
|
+
async definition(filePath, line, character, signal) {
|
|
2685
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1052
2686
|
await this.openFile(absPath);
|
|
2687
|
+
const options = signal === undefined ? {} : { signal };
|
|
1053
2688
|
return this.sendRequest("textDocument/definition", {
|
|
1054
|
-
textDocument: { uri:
|
|
2689
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1055
2690
|
position: { line: line - 1, character }
|
|
1056
|
-
});
|
|
2691
|
+
}, options);
|
|
1057
2692
|
}
|
|
1058
|
-
async references(filePath, line, character, includeDeclaration = true) {
|
|
1059
|
-
const absPath =
|
|
2693
|
+
async references(filePath, line, character, includeDeclaration = true, signal) {
|
|
2694
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1060
2695
|
await this.openFile(absPath);
|
|
2696
|
+
const options = signal === undefined ? {} : { signal };
|
|
1061
2697
|
return this.sendRequest("textDocument/references", {
|
|
1062
|
-
textDocument: { uri:
|
|
2698
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1063
2699
|
position: { line: line - 1, character },
|
|
1064
2700
|
context: { includeDeclaration }
|
|
1065
|
-
});
|
|
2701
|
+
}, options);
|
|
1066
2702
|
}
|
|
1067
|
-
async documentSymbols(filePath) {
|
|
1068
|
-
const absPath =
|
|
2703
|
+
async documentSymbols(filePath, signal) {
|
|
2704
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1069
2705
|
await this.openFile(absPath);
|
|
2706
|
+
const options = signal === undefined ? {} : { signal };
|
|
1070
2707
|
return this.sendRequest("textDocument/documentSymbol", {
|
|
1071
|
-
textDocument: { uri:
|
|
1072
|
-
});
|
|
2708
|
+
textDocument: { uri: pathToFileURL3(absPath).href }
|
|
2709
|
+
}, options);
|
|
1073
2710
|
}
|
|
1074
|
-
async workspaceSymbols(query) {
|
|
1075
|
-
|
|
2711
|
+
async workspaceSymbols(query, signal) {
|
|
2712
|
+
const options = signal === undefined ? {} : { signal };
|
|
2713
|
+
return this.sendRequest("workspace/symbol", { query }, options);
|
|
1076
2714
|
}
|
|
1077
2715
|
isUnsupportedDiagnosticPullError(error) {
|
|
1078
2716
|
if (!(error instanceof Error))
|
|
@@ -1082,43 +2720,174 @@ class LspClient extends LspClientConnection {
|
|
|
1082
2720
|
return true;
|
|
1083
2721
|
return /unsupported|not supported|method not found|unknown request/i.test(error.message);
|
|
1084
2722
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
1092
|
-
textDocument: { uri }
|
|
1093
|
-
});
|
|
1094
|
-
if (result.items) {
|
|
1095
|
-
return { items: result.items };
|
|
2723
|
+
freshnessTimeout(absPath) {
|
|
2724
|
+
return {
|
|
2725
|
+
items: [],
|
|
2726
|
+
transientError: {
|
|
2727
|
+
kind: "freshness_timeout",
|
|
2728
|
+
message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
|
|
1096
2729
|
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
parseDiagnosticPullReport(value) {
|
|
2733
|
+
if (value.kind === "unchanged") {
|
|
2734
|
+
return {
|
|
2735
|
+
type: "unchanged",
|
|
2736
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
return {
|
|
2740
|
+
type: "full",
|
|
2741
|
+
diagnostics: value.items ?? [],
|
|
2742
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
async diagnostics(filePath, signal) {
|
|
2746
|
+
signal?.throwIfAborted();
|
|
2747
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2748
|
+
const uri = pathToFileURL3(absPath).href;
|
|
2749
|
+
await this.openFile(absPath);
|
|
2750
|
+
const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
|
|
2751
|
+
for (;; ) {
|
|
2752
|
+
signal?.throwIfAborted();
|
|
2753
|
+
const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
|
|
2754
|
+
if (!snapshot)
|
|
2755
|
+
return this.freshnessTimeout(absPath);
|
|
2756
|
+
const push = this.documents.resolvePushDiagnostics(snapshot);
|
|
2757
|
+
if (push.status === "ready")
|
|
2758
|
+
return { items: [...push.diagnostics] };
|
|
2759
|
+
let pushFallbackOnly = !this.isDiagnosticPullSupported();
|
|
2760
|
+
if (!pushFallbackOnly) {
|
|
2761
|
+
const cached = this.documents.getPullCache(snapshot);
|
|
2762
|
+
try {
|
|
2763
|
+
const remainingMs2 = deadlineAt - Date.now();
|
|
2764
|
+
if (remainingMs2 <= 0)
|
|
2765
|
+
return this.freshnessTimeout(absPath);
|
|
2766
|
+
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
2767
|
+
textDocument: { uri },
|
|
2768
|
+
...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
|
|
2769
|
+
}, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
|
|
2770
|
+
if (!this.documents.isCurrentSnapshot(snapshot))
|
|
2771
|
+
continue;
|
|
2772
|
+
const report = this.parseDiagnosticPullReport(result);
|
|
2773
|
+
if (report.type === "full") {
|
|
2774
|
+
this.documents.recordPullDiagnostics(snapshot, {
|
|
2775
|
+
kind: "full",
|
|
2776
|
+
diagnostics: report.diagnostics,
|
|
2777
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
2778
|
+
});
|
|
2779
|
+
return { items: [...report.diagnostics] };
|
|
2780
|
+
}
|
|
2781
|
+
if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
|
|
2782
|
+
return { items: [...cached.diagnostics] };
|
|
2783
|
+
}
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
if (this.isUnsupportedDiagnosticPullError(error)) {
|
|
2786
|
+
this.setDiagnosticPullSupported(false);
|
|
2787
|
+
pushFallbackOnly = true;
|
|
2788
|
+
} else if (error instanceof LspRequestTimeoutError) {
|
|
2789
|
+
pushFallbackOnly = true;
|
|
2790
|
+
} else {
|
|
2791
|
+
this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
2792
|
+
throw error;
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
1100
2795
|
}
|
|
2796
|
+
if (!pushFallbackOnly)
|
|
2797
|
+
continue;
|
|
2798
|
+
const remainingMs = deadlineAt - Date.now();
|
|
2799
|
+
if (remainingMs <= 0)
|
|
2800
|
+
return this.freshnessTimeout(absPath);
|
|
2801
|
+
const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
|
|
2802
|
+
await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
|
|
1101
2803
|
}
|
|
1102
|
-
return { items: this.getStoredDiagnostics(uri) };
|
|
1103
2804
|
}
|
|
1104
|
-
async prepareRename(filePath, line, character) {
|
|
1105
|
-
const absPath =
|
|
2805
|
+
async prepareRename(filePath, line, character, signal) {
|
|
2806
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1106
2807
|
await this.openFile(absPath);
|
|
2808
|
+
const options = signal === undefined ? {} : { signal };
|
|
1107
2809
|
return this.sendRequest("textDocument/prepareRename", {
|
|
1108
|
-
textDocument: { uri:
|
|
2810
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1109
2811
|
position: { line: line - 1, character }
|
|
1110
|
-
});
|
|
2812
|
+
}, options);
|
|
1111
2813
|
}
|
|
1112
|
-
async rename(filePath, line, character, newName) {
|
|
1113
|
-
const absPath =
|
|
2814
|
+
async rename(filePath, line, character, newName, signal) {
|
|
2815
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1114
2816
|
await this.openFile(absPath);
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
2817
|
+
const acquired = this.workspaceMutations.acquire(signal);
|
|
2818
|
+
if (!acquired.success)
|
|
2819
|
+
return { edit: null, apply: acquired.result };
|
|
2820
|
+
const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
|
|
2821
|
+
try {
|
|
2822
|
+
const renameParams = {
|
|
2823
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
2824
|
+
position: { line: line - 1, character },
|
|
2825
|
+
newName
|
|
2826
|
+
};
|
|
2827
|
+
const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
|
|
2828
|
+
signal: preCommitSignal.signal
|
|
2829
|
+
});
|
|
2830
|
+
return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
|
|
2831
|
+
} finally {
|
|
2832
|
+
preCommitSignal?.dispose();
|
|
2833
|
+
this.workspaceMutations.release(acquired.lease);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
resolveWorkspacePath(filePath) {
|
|
2837
|
+
return resolve6(this.root, filePath);
|
|
1120
2838
|
}
|
|
1121
2839
|
}
|
|
2840
|
+
function waitForDiagnosticsActivity(wait, signal) {
|
|
2841
|
+
if (!signal)
|
|
2842
|
+
return wait;
|
|
2843
|
+
if (signal.aborted)
|
|
2844
|
+
return Promise.reject(abortError2(signal));
|
|
2845
|
+
return new Promise((resolve7, reject) => {
|
|
2846
|
+
const onAbort = () => {
|
|
2847
|
+
signal.removeEventListener("abort", onAbort);
|
|
2848
|
+
reject(abortError2(signal));
|
|
2849
|
+
};
|
|
2850
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2851
|
+
wait.then(() => {
|
|
2852
|
+
signal.removeEventListener("abort", onAbort);
|
|
2853
|
+
resolve7();
|
|
2854
|
+
}, (error) => {
|
|
2855
|
+
signal.removeEventListener("abort", onAbort);
|
|
2856
|
+
reject(error);
|
|
2857
|
+
});
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
function createPreCommitAbortSignal(source, isBeforeCommit) {
|
|
2861
|
+
if (!source)
|
|
2862
|
+
return;
|
|
2863
|
+
const controller = new AbortController;
|
|
2864
|
+
const onAbort = () => {
|
|
2865
|
+
if (isBeforeCommit() && !controller.signal.aborted)
|
|
2866
|
+
controller.abort(preCommitAbortReason(source));
|
|
2867
|
+
};
|
|
2868
|
+
if (source.aborted)
|
|
2869
|
+
onAbort();
|
|
2870
|
+
else
|
|
2871
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
2872
|
+
return {
|
|
2873
|
+
signal: controller.signal,
|
|
2874
|
+
dispose: () => source.removeEventListener("abort", onAbort)
|
|
2875
|
+
};
|
|
2876
|
+
}
|
|
2877
|
+
function preCommitAbortReason(source) {
|
|
2878
|
+
const reason = source.reason;
|
|
2879
|
+
if (reason instanceof Error && reason.name !== "AbortError")
|
|
2880
|
+
return reason;
|
|
2881
|
+
return new Error("LSP request cancelled before workspace edit commit");
|
|
2882
|
+
}
|
|
2883
|
+
function abortError2(signal) {
|
|
2884
|
+
const reason = signal.reason;
|
|
2885
|
+
if (reason instanceof Error)
|
|
2886
|
+
return reason;
|
|
2887
|
+
const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
|
|
2888
|
+
error.name = "AbortError";
|
|
2889
|
+
return error;
|
|
2890
|
+
}
|
|
1122
2891
|
|
|
1123
2892
|
// ../lsp-core/src/lsp/process-signal-cleanup.ts
|
|
1124
2893
|
function installProcessSignalCleanup(cleanup) {
|
|
@@ -1149,7 +2918,7 @@ async function stopClientBestEffort(client) {
|
|
|
1149
2918
|
function awaitWithSignal(promise, signal) {
|
|
1150
2919
|
if (!signal)
|
|
1151
2920
|
return promise;
|
|
1152
|
-
return new Promise((
|
|
2921
|
+
return new Promise((resolve7, reject) => {
|
|
1153
2922
|
let settled = false;
|
|
1154
2923
|
const onAbort = () => {
|
|
1155
2924
|
if (settled)
|
|
@@ -1167,7 +2936,7 @@ function awaitWithSignal(promise, signal) {
|
|
|
1167
2936
|
return;
|
|
1168
2937
|
settled = true;
|
|
1169
2938
|
signal.removeEventListener("abort", onAbort);
|
|
1170
|
-
|
|
2939
|
+
resolve7(value);
|
|
1171
2940
|
}, (err) => {
|
|
1172
2941
|
if (settled)
|
|
1173
2942
|
return;
|
|
@@ -1421,21 +3190,17 @@ async function disposeDefaultLspManager() {
|
|
|
1421
3190
|
}
|
|
1422
3191
|
|
|
1423
3192
|
// ../lsp-core/src/lsp/server-install-state.ts
|
|
1424
|
-
import { existsSync as
|
|
1425
|
-
import {
|
|
1426
|
-
import { dirname, isAbsolute, join as join2 } from "node:path";
|
|
3193
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3194
|
+
import { dirname as dirname5 } from "node:path";
|
|
1427
3195
|
function getInstallDecisionsPath() {
|
|
1428
|
-
|
|
1429
|
-
if (!override)
|
|
1430
|
-
return join2(homedir(), ".codex", "lsp-install-decisions.json");
|
|
1431
|
-
return isAbsolute(override) ? override : join2(homedir(), override);
|
|
3196
|
+
return lspRequestContext().installDecisionsPath;
|
|
1432
3197
|
}
|
|
1433
3198
|
function loadInstallDecisions() {
|
|
1434
3199
|
const path = getInstallDecisionsPath();
|
|
1435
|
-
if (!
|
|
3200
|
+
if (!existsSync6(path))
|
|
1436
3201
|
return {};
|
|
1437
3202
|
try {
|
|
1438
|
-
const parsed = JSON.parse(
|
|
3203
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1439
3204
|
return isInstallDecisions(parsed) ? parsed : {};
|
|
1440
3205
|
} catch {
|
|
1441
3206
|
return {};
|
|
@@ -1454,28 +3219,26 @@ function isInstallDecision(value) {
|
|
|
1454
3219
|
}
|
|
1455
3220
|
function writeInstallDecisions(decisions) {
|
|
1456
3221
|
const path = getInstallDecisionsPath();
|
|
1457
|
-
mkdirSync(
|
|
3222
|
+
mkdirSync(dirname5(path), { recursive: true });
|
|
1458
3223
|
const tmpPath = `${path}.tmp`;
|
|
1459
|
-
|
|
3224
|
+
writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
|
|
1460
3225
|
`, "utf8");
|
|
1461
|
-
|
|
3226
|
+
renameSync2(tmpPath, path);
|
|
1462
3227
|
}
|
|
1463
3228
|
function isInstallDecisions(value) {
|
|
1464
|
-
return
|
|
3229
|
+
return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
|
|
1465
3230
|
}
|
|
1466
3231
|
function isInstallDecisionRecord(value) {
|
|
1467
|
-
if (!
|
|
3232
|
+
if (!isRecord5(value))
|
|
1468
3233
|
return false;
|
|
1469
3234
|
return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
|
|
1470
3235
|
}
|
|
1471
|
-
function
|
|
3236
|
+
function isRecord5(value) {
|
|
1472
3237
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1473
3238
|
}
|
|
1474
3239
|
|
|
1475
3240
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1476
|
-
import { existsSync as
|
|
1477
|
-
import { homedir as homedir2 } from "node:os";
|
|
1478
|
-
import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
3241
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
1479
3242
|
|
|
1480
3243
|
// ../lsp-core/src/lsp/server-definitions.ts
|
|
1481
3244
|
var LSP_INSTALL_HINTS = {
|
|
@@ -1625,27 +3388,17 @@ var BUILTIN_SERVERS = {
|
|
|
1625
3388
|
};
|
|
1626
3389
|
|
|
1627
3390
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1628
|
-
function resolveProjectConfigPath(path) {
|
|
1629
|
-
return isAbsolute2(path) ? path : join3(contextCwd(), path);
|
|
1630
|
-
}
|
|
1631
3391
|
function getProjectConfigPaths() {
|
|
1632
|
-
|
|
1633
|
-
if (projectOverride) {
|
|
1634
|
-
return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
|
|
1635
|
-
}
|
|
1636
|
-
return [join3(contextCwd(), ".codex", "lsp-client.json")];
|
|
3392
|
+
return lspRequestContext().projectConfigPaths;
|
|
1637
3393
|
}
|
|
1638
3394
|
function getUserConfigPath() {
|
|
1639
|
-
|
|
1640
|
-
if (!userOverride)
|
|
1641
|
-
return join3(homedir2(), ".codex", "lsp-client.json");
|
|
1642
|
-
return isAbsolute2(userOverride) ? userOverride : join3(homedir2(), userOverride);
|
|
3395
|
+
return lspRequestContext().userConfigPath;
|
|
1643
3396
|
}
|
|
1644
3397
|
function loadJsonFile(path) {
|
|
1645
|
-
if (!
|
|
3398
|
+
if (!existsSync7(path))
|
|
1646
3399
|
return null;
|
|
1647
3400
|
try {
|
|
1648
|
-
const parsed = JSON.parse(
|
|
3401
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1649
3402
|
return isConfigJson(parsed) ? parsed : null;
|
|
1650
3403
|
} catch {
|
|
1651
3404
|
return null;
|
|
@@ -1784,16 +3537,16 @@ function applyOptionalServerFields(server, entry) {
|
|
|
1784
3537
|
}
|
|
1785
3538
|
}
|
|
1786
3539
|
function isConfigJson(value) {
|
|
1787
|
-
if (!
|
|
3540
|
+
if (!isRecord6(value))
|
|
1788
3541
|
return false;
|
|
1789
3542
|
const lsp = value["lsp"];
|
|
1790
|
-
return lsp === undefined ||
|
|
3543
|
+
return lsp === undefined || isRecord6(lsp);
|
|
1791
3544
|
}
|
|
1792
3545
|
function parseLspEntry(value) {
|
|
1793
3546
|
return isLspEntry(value) ? value : null;
|
|
1794
3547
|
}
|
|
1795
3548
|
function isLspEntry(value) {
|
|
1796
|
-
if (!
|
|
3549
|
+
if (!isRecord6(value))
|
|
1797
3550
|
return false;
|
|
1798
3551
|
const disabled = value["disabled"];
|
|
1799
3552
|
const command = value["command"];
|
|
@@ -1801,15 +3554,15 @@ function isLspEntry(value) {
|
|
|
1801
3554
|
const priority = value["priority"];
|
|
1802
3555
|
const env = value["env"];
|
|
1803
3556
|
const initialization = value["initialization"];
|
|
1804
|
-
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 ||
|
|
3557
|
+
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));
|
|
1805
3558
|
}
|
|
1806
3559
|
function isStringArray(value) {
|
|
1807
3560
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
1808
3561
|
}
|
|
1809
3562
|
function isStringRecord(value) {
|
|
1810
|
-
return
|
|
3563
|
+
return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
|
|
1811
3564
|
}
|
|
1812
|
-
function
|
|
3565
|
+
function isRecord6(value) {
|
|
1813
3566
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1814
3567
|
}
|
|
1815
3568
|
function getDisabledServerIds() {
|
|
@@ -1830,8 +3583,8 @@ function getDisabledServerIds() {
|
|
|
1830
3583
|
}
|
|
1831
3584
|
|
|
1832
3585
|
// ../lsp-core/src/lsp/server-installation.ts
|
|
1833
|
-
import { existsSync as
|
|
1834
|
-
import { delimiter as delimiter3, join as
|
|
3586
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
3587
|
+
import { delimiter as delimiter3, join as join3 } from "node:path";
|
|
1835
3588
|
function isServerInstalled(command, _workingDirectory) {
|
|
1836
3589
|
if (command.length === 0)
|
|
1837
3590
|
return false;
|
|
@@ -1839,7 +3592,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
1839
3592
|
if (!cmd)
|
|
1840
3593
|
return false;
|
|
1841
3594
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
1842
|
-
if (
|
|
3595
|
+
if (existsSync8(cmd))
|
|
1843
3596
|
return true;
|
|
1844
3597
|
}
|
|
1845
3598
|
const isWindows = process.platform === "win32";
|
|
@@ -1860,7 +3613,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
1860
3613
|
const paths = pathEnv.split(delimiter3);
|
|
1861
3614
|
for (const p of paths) {
|
|
1862
3615
|
for (const suffix of exts) {
|
|
1863
|
-
if (
|
|
3616
|
+
if (existsSync8(join3(p, cmd + suffix))) {
|
|
1864
3617
|
return true;
|
|
1865
3618
|
}
|
|
1866
3619
|
}
|
|
@@ -1959,39 +3712,50 @@ function getAllServers() {
|
|
|
1959
3712
|
var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
1960
3713
|
function isDirectoryPath(filePath) {
|
|
1961
3714
|
try {
|
|
1962
|
-
return
|
|
3715
|
+
return statSync3(filePath).isDirectory();
|
|
1963
3716
|
} catch {
|
|
1964
3717
|
return false;
|
|
1965
3718
|
}
|
|
1966
3719
|
}
|
|
1967
3720
|
function findWorkspaceRoot(filePath) {
|
|
1968
|
-
const abs =
|
|
3721
|
+
const abs = resolvePathInsideContext(filePath);
|
|
1969
3722
|
let dir = abs;
|
|
1970
3723
|
if (!isDirectoryPath(dir)) {
|
|
1971
|
-
dir =
|
|
3724
|
+
dir = dirname6(dir);
|
|
1972
3725
|
}
|
|
1973
3726
|
let prevDir = "";
|
|
1974
3727
|
while (dir !== prevDir) {
|
|
1975
3728
|
for (const marker of WORKSPACE_MARKERS) {
|
|
1976
|
-
if (
|
|
3729
|
+
if (existsSync9(join4(dir, marker))) {
|
|
1977
3730
|
return dir;
|
|
1978
3731
|
}
|
|
1979
3732
|
}
|
|
1980
3733
|
prevDir = dir;
|
|
1981
|
-
dir =
|
|
3734
|
+
dir = dirname6(dir);
|
|
3735
|
+
}
|
|
3736
|
+
return dirname6(abs);
|
|
3737
|
+
}
|
|
3738
|
+
function resolvePathInsideContext(filePath) {
|
|
3739
|
+
const cwd = contextCwd();
|
|
3740
|
+
const abs = resolve7(cwd, filePath);
|
|
3741
|
+
const canonical = canonicalizeExistingOrNearestAncestor(abs);
|
|
3742
|
+
if (!isPathInside(cwd, canonical)) {
|
|
3743
|
+
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
1982
3744
|
}
|
|
1983
|
-
return
|
|
3745
|
+
return canonical;
|
|
1984
3746
|
}
|
|
1985
3747
|
function formatServerLookupError(result) {
|
|
1986
3748
|
if (result.status === "not_installed") {
|
|
1987
3749
|
return formatNotInstalled(result);
|
|
1988
3750
|
}
|
|
3751
|
+
const context = lspRequestContext();
|
|
3752
|
+
const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
|
|
1989
3753
|
return [
|
|
1990
3754
|
`No LSP server configured for extension: ${result.extension}`,
|
|
1991
3755
|
"",
|
|
1992
3756
|
`Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
|
|
1993
3757
|
"",
|
|
1994
|
-
|
|
3758
|
+
`Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
|
|
1995
3759
|
" {",
|
|
1996
3760
|
' "lsp": {',
|
|
1997
3761
|
' "my-server": {',
|
|
@@ -2007,6 +3771,7 @@ function formatNotInstalled(result) {
|
|
|
2007
3771
|
const { server, installHint } = result;
|
|
2008
3772
|
const extensions = server.extensions.join(", ");
|
|
2009
3773
|
const decision = loadInstallDecision(server.id)?.decision;
|
|
3774
|
+
const context = lspRequestContext();
|
|
2010
3775
|
if (decision === "declined") {
|
|
2011
3776
|
return `LSP server '${server.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
|
|
2012
3777
|
}
|
|
@@ -2022,6 +3787,17 @@ function formatNotInstalled(result) {
|
|
|
2022
3787
|
"The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
|
|
2023
3788
|
` ${installHint}`
|
|
2024
3789
|
].join(`
|
|
3790
|
+
`);
|
|
3791
|
+
}
|
|
3792
|
+
if (!context.capabilities.installDecisionTool) {
|
|
3793
|
+
return [
|
|
3794
|
+
...header,
|
|
3795
|
+
"To install, run:",
|
|
3796
|
+
` ${installHint}`,
|
|
3797
|
+
"",
|
|
3798
|
+
"ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
|
|
3799
|
+
"Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
|
|
3800
|
+
].join(`
|
|
2025
3801
|
`);
|
|
2026
3802
|
}
|
|
2027
3803
|
return [
|
|
@@ -2033,6 +3809,7 @@ function formatNotInstalled(result) {
|
|
|
2033
3809
|
"- If the user agrees: run the install command above, then retry this tool.",
|
|
2034
3810
|
"- If the user declines, OR has NOT explicitly asked for LSP installation:",
|
|
2035
3811
|
` call lsp_install_decision { server_id: "${server.id}", decision: "declined" },`,
|
|
3812
|
+
` which writes to ${context.installDecisionsPath},`,
|
|
2036
3813
|
" then ignore this message and proceed WITHOUT LSP."
|
|
2037
3814
|
].join(`
|
|
2038
3815
|
`);
|
|
@@ -2046,14 +3823,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
|
|
|
2046
3823
|
"prepareRename"
|
|
2047
3824
|
]);
|
|
2048
3825
|
async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
2049
|
-
const absPath =
|
|
3826
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2050
3827
|
if (isDirectoryPath(absPath)) {
|
|
2051
3828
|
throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
|
|
2052
3829
|
}
|
|
2053
3830
|
const ext = effectiveExtension(absPath);
|
|
2054
3831
|
const result = findServerForExtension(ext);
|
|
2055
3832
|
if (result.status !== "found") {
|
|
2056
|
-
throw new LspServerLookupError(formatServerLookupError(result));
|
|
3833
|
+
throw new LspServerLookupError(formatServerLookupError(result), result);
|
|
2057
3834
|
}
|
|
2058
3835
|
const server = result.server;
|
|
2059
3836
|
const root = findWorkspaceRoot(absPath);
|
|
@@ -2081,11 +3858,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
|
2081
3858
|
}
|
|
2082
3859
|
|
|
2083
3860
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2084
|
-
import { existsSync as
|
|
2085
|
-
import { join as
|
|
3861
|
+
import { existsSync as existsSync10, lstatSync as lstatSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
3862
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
2086
3863
|
|
|
2087
3864
|
// ../lsp-core/src/lsp/formatters.ts
|
|
2088
|
-
import { fileURLToPath } from "node:url";
|
|
3865
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2089
3866
|
var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
2090
3867
|
error: 1,
|
|
2091
3868
|
warning: 2,
|
|
@@ -2093,7 +3870,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
|
2093
3870
|
hint: 4
|
|
2094
3871
|
};
|
|
2095
3872
|
function uriToPath(uri) {
|
|
2096
|
-
return
|
|
3873
|
+
return fileURLToPath2(uri);
|
|
2097
3874
|
}
|
|
2098
3875
|
function formatLocation(loc) {
|
|
2099
3876
|
if ("targetUri" in loc) {
|
|
@@ -2179,6 +3956,9 @@ function formatApplyResult(result) {
|
|
|
2179
3956
|
for (const file of result.filesModified) {
|
|
2180
3957
|
lines.push(` - ${file}`);
|
|
2181
3958
|
}
|
|
3959
|
+
if (result.lateAbort) {
|
|
3960
|
+
lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
|
|
3961
|
+
}
|
|
2182
3962
|
} else {
|
|
2183
3963
|
lines.push("Failed to apply some changes:");
|
|
2184
3964
|
for (const err of result.errors) {
|
|
@@ -2194,6 +3974,7 @@ function formatApplyResult(result) {
|
|
|
2194
3974
|
|
|
2195
3975
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2196
3976
|
var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
3977
|
+
var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
|
|
2197
3978
|
function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
2198
3979
|
const files = [];
|
|
2199
3980
|
function walk(currentDir) {
|
|
@@ -2201,17 +3982,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2201
3982
|
return;
|
|
2202
3983
|
let entries = [];
|
|
2203
3984
|
try {
|
|
2204
|
-
entries =
|
|
3985
|
+
entries = readdirSync3(currentDir);
|
|
2205
3986
|
} catch {
|
|
2206
3987
|
return;
|
|
2207
3988
|
}
|
|
2208
3989
|
for (const entry of entries) {
|
|
2209
3990
|
if (files.length >= maxFiles)
|
|
2210
3991
|
return;
|
|
2211
|
-
const fullPath =
|
|
3992
|
+
const fullPath = join5(currentDir, entry);
|
|
2212
3993
|
let stat;
|
|
2213
3994
|
try {
|
|
2214
|
-
stat =
|
|
3995
|
+
stat = lstatSync4(fullPath);
|
|
2215
3996
|
} catch {
|
|
2216
3997
|
continue;
|
|
2217
3998
|
}
|
|
@@ -2229,52 +4010,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2229
4010
|
walk(dir);
|
|
2230
4011
|
return files;
|
|
2231
4012
|
}
|
|
2232
|
-
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
|
|
4013
|
+
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
|
|
2233
4014
|
if (!extension.startsWith(".")) {
|
|
2234
4015
|
throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
|
|
2235
4016
|
}
|
|
2236
|
-
const absDir =
|
|
2237
|
-
if (!
|
|
4017
|
+
const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
|
|
4018
|
+
if (!existsSync10(absDir)) {
|
|
2238
4019
|
throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
|
|
2239
4020
|
}
|
|
2240
|
-
const serverResult = findServerForExtension(extension);
|
|
4021
|
+
const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
|
|
2241
4022
|
if (serverResult.status !== "found") {
|
|
2242
4023
|
throw new LspServerLookupError(formatServerLookupError(serverResult));
|
|
2243
4024
|
}
|
|
2244
4025
|
const server = serverResult.server;
|
|
2245
|
-
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
|
|
4026
|
+
const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
|
|
2246
4027
|
const wasCapped = allFiles.length > maxFiles;
|
|
2247
4028
|
const filesToProcess = allFiles.slice(0, maxFiles);
|
|
2248
4029
|
if (filesToProcess.length === 0) {
|
|
2249
|
-
|
|
4030
|
+
const output = [
|
|
2250
4031
|
`Directory: ${absDir}`,
|
|
2251
4032
|
`Extension: ${extension}`,
|
|
2252
4033
|
"Files scanned: 0",
|
|
2253
4034
|
`No files found with extension "${extension}".`
|
|
2254
4035
|
].join(`
|
|
2255
4036
|
`);
|
|
4037
|
+
return { output, totalDiagnostics: 0, fileFailures: [] };
|
|
2256
4038
|
}
|
|
2257
|
-
const root = findWorkspaceRoot(absDir);
|
|
2258
|
-
const manager = getLspManager();
|
|
4039
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
|
|
4040
|
+
const manager = options.manager ?? getLspManager();
|
|
2259
4041
|
const allDiagnostics = [];
|
|
2260
4042
|
const fileErrors = [];
|
|
2261
|
-
const
|
|
4043
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
|
|
4044
|
+
options.signal?.throwIfAborted();
|
|
4045
|
+
const client = await manager.getClient(root, server, options.signal);
|
|
2262
4046
|
try {
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
file,
|
|
2274
|
-
|
|
2275
|
-
|
|
4047
|
+
let nextIndex = 0;
|
|
4048
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
|
|
4049
|
+
for (;; ) {
|
|
4050
|
+
if (options.signal?.aborted)
|
|
4051
|
+
return;
|
|
4052
|
+
const file = filesToProcess[nextIndex];
|
|
4053
|
+
nextIndex += 1;
|
|
4054
|
+
if (file === undefined)
|
|
4055
|
+
return;
|
|
4056
|
+
try {
|
|
4057
|
+
const result = await client.diagnostics(file, options.signal);
|
|
4058
|
+
const filtered = filterDiagnosticsBySeverity(result.items, severity);
|
|
4059
|
+
allDiagnostics.push(...filtered.map((diagnostic) => ({
|
|
4060
|
+
filePath: file,
|
|
4061
|
+
diagnostic
|
|
4062
|
+
})));
|
|
4063
|
+
} catch (e) {
|
|
4064
|
+
fileErrors.push({
|
|
4065
|
+
file,
|
|
4066
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4067
|
+
});
|
|
4068
|
+
}
|
|
2276
4069
|
}
|
|
2277
|
-
}
|
|
4070
|
+
});
|
|
4071
|
+
await Promise.all(workers);
|
|
2278
4072
|
} finally {
|
|
2279
4073
|
manager.releaseClient(root, server.id);
|
|
2280
4074
|
}
|
|
@@ -2302,13 +4096,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
|
|
|
2302
4096
|
lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
2303
4097
|
}
|
|
2304
4098
|
}
|
|
2305
|
-
return lines.join(`
|
|
2306
|
-
`);
|
|
4099
|
+
return { output: lines.join(`
|
|
4100
|
+
`), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
|
|
2307
4101
|
}
|
|
2308
4102
|
|
|
2309
4103
|
// ../lsp-core/src/lsp/infer-extension.ts
|
|
2310
|
-
import { lstatSync as
|
|
2311
|
-
import { join as
|
|
4104
|
+
import { lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
|
|
4105
|
+
import { join as join6 } from "node:path";
|
|
2312
4106
|
var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
2313
4107
|
var MAX_SCAN_ENTRIES = 500;
|
|
2314
4108
|
function inferExtensionFromDirectory(directory) {
|
|
@@ -2319,17 +4113,17 @@ function inferExtensionFromDirectory(directory) {
|
|
|
2319
4113
|
return;
|
|
2320
4114
|
let entries;
|
|
2321
4115
|
try {
|
|
2322
|
-
entries =
|
|
4116
|
+
entries = readdirSync4(dir);
|
|
2323
4117
|
} catch {
|
|
2324
4118
|
return;
|
|
2325
4119
|
}
|
|
2326
4120
|
for (const entry of entries) {
|
|
2327
4121
|
if (scanned >= MAX_SCAN_ENTRIES)
|
|
2328
4122
|
return;
|
|
2329
|
-
const fullPath =
|
|
4123
|
+
const fullPath = join6(dir, entry);
|
|
2330
4124
|
let stat;
|
|
2331
4125
|
try {
|
|
2332
|
-
stat =
|
|
4126
|
+
stat = lstatSync5(fullPath);
|
|
2333
4127
|
} catch {
|
|
2334
4128
|
continue;
|
|
2335
4129
|
}
|
|
@@ -2403,13 +4197,48 @@ function missingDependencyResult(error, details) {
|
|
|
2403
4197
|
details: {
|
|
2404
4198
|
...details,
|
|
2405
4199
|
error: message,
|
|
2406
|
-
errorKind: "missing_dependency"
|
|
4200
|
+
errorKind: "missing_dependency",
|
|
4201
|
+
...availabilityDetails(error)
|
|
2407
4202
|
}
|
|
2408
4203
|
};
|
|
2409
4204
|
}
|
|
4205
|
+
function availabilityDetails(error) {
|
|
4206
|
+
const availability = missingDependencyAvailability(error);
|
|
4207
|
+
return availability === null ? {} : { availability };
|
|
4208
|
+
}
|
|
4209
|
+
function missingDependencyAvailability(error) {
|
|
4210
|
+
if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
|
|
4211
|
+
return null;
|
|
4212
|
+
const context = lspRequestContext();
|
|
4213
|
+
switch (error.lookup.status) {
|
|
4214
|
+
case "not_configured":
|
|
4215
|
+
return {
|
|
4216
|
+
kind: "not_configured",
|
|
4217
|
+
extension: error.lookup.extension,
|
|
4218
|
+
availableServers: [...error.lookup.availableServers],
|
|
4219
|
+
projectConfigPaths: [...context.projectConfigPaths],
|
|
4220
|
+
userConfigPath: context.userConfigPath,
|
|
4221
|
+
installDecisionTool: context.capabilities.installDecisionTool
|
|
4222
|
+
};
|
|
4223
|
+
case "not_installed":
|
|
4224
|
+
return {
|
|
4225
|
+
kind: "not_installed",
|
|
4226
|
+
serverId: error.lookup.server.id,
|
|
4227
|
+
command: [...error.lookup.server.command],
|
|
4228
|
+
extensions: [...error.lookup.server.extensions],
|
|
4229
|
+
installHint: error.lookup.installHint,
|
|
4230
|
+
installDecisionTool: context.capabilities.installDecisionTool,
|
|
4231
|
+
installDecisionsPath: context.installDecisionsPath
|
|
4232
|
+
};
|
|
4233
|
+
default: {
|
|
4234
|
+
const exhaustive = error.lookup;
|
|
4235
|
+
return exhaustive;
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
2410
4239
|
|
|
2411
4240
|
// ../lsp-core/src/tools/parameters.ts
|
|
2412
|
-
function
|
|
4241
|
+
function isRecord7(value) {
|
|
2413
4242
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2414
4243
|
}
|
|
2415
4244
|
function requireString(params, key) {
|
|
@@ -2466,7 +4295,7 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2466
4295
|
const filePath = requireString(params, "filePath");
|
|
2467
4296
|
const severity = severityFilter(params);
|
|
2468
4297
|
try {
|
|
2469
|
-
const absPath =
|
|
4298
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2470
4299
|
if (isDirectoryPath(absPath)) {
|
|
2471
4300
|
const extension = inferExtensionFromDirectory(absPath);
|
|
2472
4301
|
if (!extension) {
|
|
@@ -2483,18 +4312,33 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2483
4312
|
};
|
|
2484
4313
|
return text(message, details3);
|
|
2485
4314
|
}
|
|
2486
|
-
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
|
|
4315
|
+
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
|
|
2487
4316
|
const details2 = {
|
|
2488
4317
|
filePath,
|
|
2489
4318
|
severity,
|
|
2490
4319
|
mode: "directory",
|
|
2491
4320
|
diagnostics: [],
|
|
4321
|
+
totalDiagnostics: output2.totalDiagnostics,
|
|
4322
|
+
truncated: false,
|
|
4323
|
+
fileFailures: [...output2.fileFailures]
|
|
4324
|
+
};
|
|
4325
|
+
return text(output2.output, details2);
|
|
4326
|
+
}
|
|
4327
|
+
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
|
|
4328
|
+
if (result.transientError) {
|
|
4329
|
+
const message = result.transientError.message;
|
|
4330
|
+
const details2 = {
|
|
4331
|
+
filePath,
|
|
4332
|
+
severity,
|
|
4333
|
+
mode: "file",
|
|
4334
|
+
diagnostics: [],
|
|
2492
4335
|
totalDiagnostics: 0,
|
|
2493
|
-
truncated: false
|
|
4336
|
+
truncated: false,
|
|
4337
|
+
error: message,
|
|
4338
|
+
errorKind: result.transientError.kind
|
|
2494
4339
|
};
|
|
2495
|
-
return text(
|
|
4340
|
+
return text(message, details2, true);
|
|
2496
4341
|
}
|
|
2497
|
-
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
|
|
2498
4342
|
const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
|
|
2499
4343
|
const total = diagnostics.length;
|
|
2500
4344
|
const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
|
|
@@ -2555,7 +4399,7 @@ async function executeLspGotoDefinition(params, signal) {
|
|
|
2555
4399
|
const line = requireNumber(params, "line");
|
|
2556
4400
|
const character = requireNumber(params, "character");
|
|
2557
4401
|
try {
|
|
2558
|
-
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
|
|
4402
|
+
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
|
|
2559
4403
|
const locations = !result ? [] : Array.isArray(result) ? result : [result];
|
|
2560
4404
|
const details = { filePath, line, character, locations };
|
|
2561
4405
|
if (locations.length === 0)
|
|
@@ -2580,7 +4424,7 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2580
4424
|
const character = requireNumber(params, "character");
|
|
2581
4425
|
const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
|
|
2582
4426
|
try {
|
|
2583
|
-
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
|
|
4427
|
+
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
|
|
2584
4428
|
const references = Array.isArray(result) ? result : [];
|
|
2585
4429
|
const total = references.length;
|
|
2586
4430
|
const truncated = total > DEFAULT_MAX_REFERENCES;
|
|
@@ -2616,181 +4460,13 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2616
4460
|
}
|
|
2617
4461
|
}
|
|
2618
4462
|
|
|
2619
|
-
// ../lsp-core/src/lsp/workspace-edit.ts
|
|
2620
|
-
import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2621
|
-
import { dirname as dirname3, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
|
|
2622
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2623
|
-
function errorMessage2(error) {
|
|
2624
|
-
return error instanceof Error ? error.message : String(error);
|
|
2625
|
-
}
|
|
2626
|
-
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
2627
|
-
const relativePath = relative(workspaceRoot, filePath);
|
|
2628
|
-
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
|
|
2629
|
-
}
|
|
2630
|
-
function realpathForValidation(filePath) {
|
|
2631
|
-
if (existsSync7(filePath))
|
|
2632
|
-
return realpathSync(filePath);
|
|
2633
|
-
const parent = dirname3(filePath);
|
|
2634
|
-
return resolve5(realpathSync(parent), relative(parent, filePath));
|
|
2635
|
-
}
|
|
2636
|
-
function uriToWorkspacePath(uri, workspaceRoot) {
|
|
2637
|
-
let filePath;
|
|
2638
|
-
try {
|
|
2639
|
-
filePath = fileURLToPath2(uri);
|
|
2640
|
-
} catch (error) {
|
|
2641
|
-
return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
|
|
2642
|
-
}
|
|
2643
|
-
let validatedPath;
|
|
2644
|
-
try {
|
|
2645
|
-
validatedPath = realpathForValidation(filePath);
|
|
2646
|
-
} catch (error) {
|
|
2647
|
-
return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
|
|
2648
|
-
}
|
|
2649
|
-
if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
|
|
2650
|
-
return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
|
|
2651
|
-
}
|
|
2652
|
-
return { success: true, path: filePath };
|
|
2653
|
-
}
|
|
2654
|
-
function applyTextEditsToFile(filePath, edits) {
|
|
2655
|
-
try {
|
|
2656
|
-
const content = readFileSync4(filePath, "utf-8");
|
|
2657
|
-
const lines = content.split(`
|
|
2658
|
-
`);
|
|
2659
|
-
const sortedEdits = [...edits].sort((a, b) => {
|
|
2660
|
-
if (b.range.start.line !== a.range.start.line) {
|
|
2661
|
-
return b.range.start.line - a.range.start.line;
|
|
2662
|
-
}
|
|
2663
|
-
return b.range.start.character - a.range.start.character;
|
|
2664
|
-
});
|
|
2665
|
-
for (const edit of sortedEdits) {
|
|
2666
|
-
const startLine = edit.range.start.line;
|
|
2667
|
-
const startChar = edit.range.start.character;
|
|
2668
|
-
const endLine = edit.range.end.line;
|
|
2669
|
-
const endChar = edit.range.end.character;
|
|
2670
|
-
if (startLine === endLine) {
|
|
2671
|
-
const line = lines[startLine] ?? "";
|
|
2672
|
-
lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
|
|
2673
|
-
} else {
|
|
2674
|
-
const firstLine = lines[startLine] ?? "";
|
|
2675
|
-
const lastLine = lines[endLine] ?? "";
|
|
2676
|
-
const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
|
|
2677
|
-
lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
|
|
2678
|
-
`));
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
writeFileSync2(filePath, lines.join(`
|
|
2682
|
-
`), "utf-8");
|
|
2683
|
-
return { success: true, editCount: edits.length };
|
|
2684
|
-
} catch (err) {
|
|
2685
|
-
return {
|
|
2686
|
-
success: false,
|
|
2687
|
-
editCount: 0,
|
|
2688
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2689
|
-
};
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
function applyWorkspaceEdit(edit, options = {}) {
|
|
2693
|
-
if (!edit) {
|
|
2694
|
-
return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
|
|
2695
|
-
}
|
|
2696
|
-
const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
|
|
2697
|
-
const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
|
|
2698
|
-
if (edit.changes) {
|
|
2699
|
-
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
2700
|
-
const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
|
|
2701
|
-
if (!validatedPath.success) {
|
|
2702
|
-
result.success = false;
|
|
2703
|
-
result.errors.push(validatedPath.error);
|
|
2704
|
-
continue;
|
|
2705
|
-
}
|
|
2706
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, edits);
|
|
2707
|
-
if (applyResult.success) {
|
|
2708
|
-
result.filesModified.push(validatedPath.path);
|
|
2709
|
-
result.totalEdits += applyResult.editCount;
|
|
2710
|
-
} else {
|
|
2711
|
-
result.success = false;
|
|
2712
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
if (edit.documentChanges) {
|
|
2717
|
-
for (const change of edit.documentChanges) {
|
|
2718
|
-
if (!("kind" in change)) {
|
|
2719
|
-
const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
|
|
2720
|
-
if (!validatedPath.success) {
|
|
2721
|
-
result.success = false;
|
|
2722
|
-
result.errors.push(validatedPath.error);
|
|
2723
|
-
continue;
|
|
2724
|
-
}
|
|
2725
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
|
|
2726
|
-
if (applyResult.success) {
|
|
2727
|
-
result.filesModified.push(validatedPath.path);
|
|
2728
|
-
result.totalEdits += applyResult.editCount;
|
|
2729
|
-
} else {
|
|
2730
|
-
result.success = false;
|
|
2731
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2732
|
-
}
|
|
2733
|
-
continue;
|
|
2734
|
-
}
|
|
2735
|
-
if (change.kind === "create") {
|
|
2736
|
-
try {
|
|
2737
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2738
|
-
if (!validatedPath.success) {
|
|
2739
|
-
result.success = false;
|
|
2740
|
-
result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
|
|
2741
|
-
continue;
|
|
2742
|
-
}
|
|
2743
|
-
writeFileSync2(validatedPath.path, "", "utf-8");
|
|
2744
|
-
result.filesModified.push(validatedPath.path);
|
|
2745
|
-
} catch (err) {
|
|
2746
|
-
result.success = false;
|
|
2747
|
-
result.errors.push(`Create ${change.uri}: ${String(err)}`);
|
|
2748
|
-
}
|
|
2749
|
-
} else if (change.kind === "rename") {
|
|
2750
|
-
try {
|
|
2751
|
-
const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
|
|
2752
|
-
const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
|
|
2753
|
-
if (!oldPath.success || !newPath.success) {
|
|
2754
|
-
const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
|
|
2755
|
-
result.success = false;
|
|
2756
|
-
result.errors.push(`Rename ${change.oldUri}: ${error}`);
|
|
2757
|
-
continue;
|
|
2758
|
-
}
|
|
2759
|
-
const content = readFileSync4(oldPath.path, "utf-8");
|
|
2760
|
-
writeFileSync2(newPath.path, content, "utf-8");
|
|
2761
|
-
unlinkSync(oldPath.path);
|
|
2762
|
-
result.filesModified.push(newPath.path);
|
|
2763
|
-
} catch (err) {
|
|
2764
|
-
result.success = false;
|
|
2765
|
-
result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
|
|
2766
|
-
}
|
|
2767
|
-
} else if (change.kind === "delete") {
|
|
2768
|
-
try {
|
|
2769
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2770
|
-
if (!validatedPath.success) {
|
|
2771
|
-
result.success = false;
|
|
2772
|
-
result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
|
|
2773
|
-
continue;
|
|
2774
|
-
}
|
|
2775
|
-
unlinkSync(validatedPath.path);
|
|
2776
|
-
result.filesModified.push(validatedPath.path);
|
|
2777
|
-
} catch (err) {
|
|
2778
|
-
result.success = false;
|
|
2779
|
-
result.errors.push(`Delete ${change.uri}: ${String(err)}`);
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
return result;
|
|
2785
|
-
}
|
|
2786
|
-
|
|
2787
4463
|
// ../lsp-core/src/tools/rename.ts
|
|
2788
4464
|
async function executeLspPrepareRename(params, signal) {
|
|
2789
4465
|
const filePath = requireString(params, "filePath");
|
|
2790
4466
|
const line = requireNumber(params, "line");
|
|
2791
4467
|
const character = requireNumber(params, "character");
|
|
2792
4468
|
try {
|
|
2793
|
-
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
|
|
4469
|
+
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
|
|
2794
4470
|
const details = { filePath, line, character, result };
|
|
2795
4471
|
return text(formatPrepareRenameResult(result), details);
|
|
2796
4472
|
} catch (error) {
|
|
@@ -2811,13 +4487,9 @@ async function executeLspRename(params, signal) {
|
|
|
2811
4487
|
const character = requireNumber(params, "character");
|
|
2812
4488
|
const newName = requireString(params, "newName");
|
|
2813
4489
|
try {
|
|
2814
|
-
const
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
}), "rename", clientOptions(signal));
|
|
2818
|
-
const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
|
|
2819
|
-
const details = { filePath, line, character, newName, apply, edit: edit.edit };
|
|
2820
|
-
return text(formatApplyResult(apply), details, !apply.success);
|
|
4490
|
+
const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
|
|
4491
|
+
const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
|
|
4492
|
+
return text(formatApplyResult(result.apply), details, !result.apply.success);
|
|
2821
4493
|
} catch (error) {
|
|
2822
4494
|
const missingDependency = missingDependencyResult(error, {
|
|
2823
4495
|
filePath,
|
|
@@ -2892,10 +4564,10 @@ async function executeLspSymbols(params, signal) {
|
|
|
2892
4564
|
errorKind: "missing_query"
|
|
2893
4565
|
});
|
|
2894
4566
|
}
|
|
2895
|
-
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
|
|
4567
|
+
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
|
|
2896
4568
|
return formatSymbolsResult(filePath, scope, symbols2, limit, query);
|
|
2897
4569
|
}
|
|
2898
|
-
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
|
|
4570
|
+
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
|
|
2899
4571
|
return formatSymbolsResult(filePath, scope, symbols, limit);
|
|
2900
4572
|
} catch (error) {
|
|
2901
4573
|
const query = optionalString(params, "query");
|
|
@@ -3061,10 +4733,10 @@ function matchesToolName(tool, name) {
|
|
|
3061
4733
|
return tool.name === name || (tool.aliases?.includes(name) ?? false);
|
|
3062
4734
|
}
|
|
3063
4735
|
function coerceToolArguments(value) {
|
|
3064
|
-
return
|
|
4736
|
+
return isRecord7(value) ? value : {};
|
|
3065
4737
|
}
|
|
3066
4738
|
export {
|
|
3067
|
-
|
|
4739
|
+
isRecord7 as isRecord,
|
|
3068
4740
|
executeLspTool,
|
|
3069
4741
|
executeLspSymbols,
|
|
3070
4742
|
executeLspStatus,
|