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,31 +1,16 @@
|
|
|
1
1
|
// ../lsp-core/src/lsp/cleanup-errors.ts
|
|
2
|
-
function
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
function writeCleanupError(message) {
|
|
3
|
+
process.stderr.write(`${message}
|
|
4
|
+
`);
|
|
5
|
+
}
|
|
6
|
+
function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
|
|
5
7
|
const message = error instanceof Error ? error.message : String(error);
|
|
6
|
-
|
|
8
|
+
logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
// ../lsp-core/src/lsp/client.ts
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
13
|
-
|
|
14
|
-
// ../lsp-core/src/request-context.ts
|
|
15
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
16
|
-
var storage = new AsyncLocalStorage;
|
|
17
|
-
function runWithRequestContext(context, fn) {
|
|
18
|
-
return storage.run(context, fn);
|
|
19
|
-
}
|
|
20
|
-
function contextCwd() {
|
|
21
|
-
return storage.getStore()?.cwd ?? process.cwd();
|
|
22
|
-
}
|
|
23
|
-
function contextEnv(key) {
|
|
24
|
-
const store = storage.getStore();
|
|
25
|
-
if (store?.env)
|
|
26
|
-
return store.env[key];
|
|
27
|
-
return process.env[key];
|
|
28
|
-
}
|
|
12
|
+
import { resolve as resolve5 } from "node:path";
|
|
13
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
29
14
|
|
|
30
15
|
// ../lsp-core/src/lsp/connection.ts
|
|
31
16
|
import { pathToFileURL } from "node:url";
|
|
@@ -89,7 +74,12 @@ class LspInvalidPathError extends Error {
|
|
|
89
74
|
}
|
|
90
75
|
|
|
91
76
|
class LspServerLookupError extends Error {
|
|
77
|
+
lookup;
|
|
92
78
|
name = "LspServerLookupError";
|
|
79
|
+
constructor(message, lookup) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.lookup = lookup;
|
|
82
|
+
}
|
|
93
83
|
}
|
|
94
84
|
|
|
95
85
|
class LspServerInitializingError extends Error {
|
|
@@ -155,28 +145,80 @@ class JsonRpcConnection {
|
|
|
155
145
|
onError(handler) {
|
|
156
146
|
this.errorHandlers.push(handler);
|
|
157
147
|
}
|
|
158
|
-
async sendRequest(method, params) {
|
|
148
|
+
async sendRequest(method, params, options = {}) {
|
|
159
149
|
if (this.disposed)
|
|
160
150
|
throw new Error("JSON-RPC connection is disposed");
|
|
161
151
|
const id = this.nextRequestId;
|
|
162
152
|
this.nextRequestId += 1;
|
|
153
|
+
const key = String(id);
|
|
163
154
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
155
|
+
let requestWritten = false;
|
|
156
|
+
let cancelAfterWrite = false;
|
|
157
|
+
let settled = false;
|
|
158
|
+
const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
164
159
|
const responsePromise = new Promise((resolve, reject) => {
|
|
165
|
-
|
|
160
|
+
const cleanup = () => {
|
|
161
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
162
|
+
};
|
|
163
|
+
const settleCancel = () => {
|
|
164
|
+
if (settled)
|
|
165
|
+
return;
|
|
166
|
+
settled = true;
|
|
167
|
+
this.pendingRequests.delete(key);
|
|
168
|
+
cleanup();
|
|
169
|
+
const rejectCancelled = () => reject(abortError(options.signal));
|
|
170
|
+
if (!requestWritten) {
|
|
171
|
+
cancelAfterWrite = true;
|
|
172
|
+
rejectCancelled();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
writeCancel().then(rejectCancelled, (error) => {
|
|
176
|
+
this.emitError(toError(error));
|
|
177
|
+
rejectCancelled();
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
const onAbort = () => settleCancel();
|
|
181
|
+
this.pendingRequests.set(key, {
|
|
166
182
|
resolve(result) {
|
|
183
|
+
settled = true;
|
|
184
|
+
cleanup();
|
|
167
185
|
resolve(result);
|
|
168
186
|
},
|
|
169
|
-
reject
|
|
187
|
+
reject(error) {
|
|
188
|
+
settled = true;
|
|
189
|
+
cleanup();
|
|
190
|
+
reject(error);
|
|
191
|
+
},
|
|
192
|
+
cleanup
|
|
170
193
|
});
|
|
194
|
+
if (options.signal?.aborted) {
|
|
195
|
+
settleCancel();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
171
199
|
});
|
|
200
|
+
if (settled)
|
|
201
|
+
return responsePromise;
|
|
172
202
|
try {
|
|
173
203
|
await this.writeMessage(message);
|
|
204
|
+
requestWritten = true;
|
|
205
|
+
if (cancelAfterWrite)
|
|
206
|
+
await writeCancel();
|
|
174
207
|
} catch (error) {
|
|
175
|
-
|
|
208
|
+
if (settled)
|
|
209
|
+
return responsePromise;
|
|
210
|
+
const pending = this.pendingRequests.get(key);
|
|
211
|
+
if (pending) {
|
|
212
|
+
pending.cleanup();
|
|
213
|
+
this.pendingRequests.delete(key);
|
|
214
|
+
}
|
|
176
215
|
throw error;
|
|
177
216
|
}
|
|
178
217
|
return responsePromise;
|
|
179
218
|
}
|
|
219
|
+
pendingRequestCount() {
|
|
220
|
+
return this.pendingRequests.size;
|
|
221
|
+
}
|
|
180
222
|
async sendNotification(method, params) {
|
|
181
223
|
if (this.disposed)
|
|
182
224
|
return;
|
|
@@ -193,6 +235,7 @@ class JsonRpcConnection {
|
|
|
193
235
|
this.reader.off("error", this.handleStreamError);
|
|
194
236
|
this.writer.off("error", this.handleStreamError);
|
|
195
237
|
for (const pending of this.pendingRequests.values()) {
|
|
238
|
+
pending.cleanup();
|
|
196
239
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
197
240
|
}
|
|
198
241
|
this.pendingRequests.clear();
|
|
@@ -268,6 +311,7 @@ class JsonRpcConnection {
|
|
|
268
311
|
if (!pending)
|
|
269
312
|
return;
|
|
270
313
|
this.pendingRequests.delete(String(id));
|
|
314
|
+
pending.cleanup();
|
|
271
315
|
if ("error" in message) {
|
|
272
316
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
273
317
|
return;
|
|
@@ -281,7 +325,11 @@ class JsonRpcConnection {
|
|
|
281
325
|
try {
|
|
282
326
|
handler(params);
|
|
283
327
|
} catch (error) {
|
|
284
|
-
|
|
328
|
+
if (error instanceof Error) {
|
|
329
|
+
this.emitError(error);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.emitError(new Error(String(error)));
|
|
285
333
|
}
|
|
286
334
|
}
|
|
287
335
|
handleRequest(message) {
|
|
@@ -322,6 +370,14 @@ ${body}`;
|
|
|
322
370
|
}
|
|
323
371
|
}
|
|
324
372
|
}
|
|
373
|
+
function abortError(signal) {
|
|
374
|
+
const reason = signal?.reason;
|
|
375
|
+
if (reason instanceof Error)
|
|
376
|
+
return reason;
|
|
377
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
378
|
+
error.name = "AbortError";
|
|
379
|
+
return error;
|
|
380
|
+
}
|
|
325
381
|
function parseContentLength(headers) {
|
|
326
382
|
for (const line of headers.split(`\r
|
|
327
383
|
`)) {
|
|
@@ -508,7 +564,7 @@ function spawnProcess(command, options) {
|
|
|
508
564
|
return wrap(proc);
|
|
509
565
|
}
|
|
510
566
|
|
|
511
|
-
// ../lsp-core/src/lsp/transport.ts
|
|
567
|
+
// ../lsp-core/src/lsp/transport-protocol.ts
|
|
512
568
|
function isRecord(value) {
|
|
513
569
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
514
570
|
}
|
|
@@ -528,7 +584,32 @@ function parseDiagnosticsParams(params) {
|
|
|
528
584
|
if (!isRecord(params) || typeof params["uri"] !== "string")
|
|
529
585
|
return null;
|
|
530
586
|
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
531
|
-
|
|
587
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
588
|
+
return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
|
|
589
|
+
}
|
|
590
|
+
function createLspSpawnEnv(_root, input) {
|
|
591
|
+
return { ...input };
|
|
592
|
+
}
|
|
593
|
+
function isDiagnostic(value) {
|
|
594
|
+
return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
595
|
+
}
|
|
596
|
+
function isRange(value) {
|
|
597
|
+
return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
598
|
+
}
|
|
599
|
+
function isPosition(value) {
|
|
600
|
+
return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ../lsp-core/src/lsp/transport.ts
|
|
604
|
+
class LspClientNotStartedError extends Error {
|
|
605
|
+
serverId;
|
|
606
|
+
root;
|
|
607
|
+
name = "LspClientNotStartedError";
|
|
608
|
+
constructor(serverId, root) {
|
|
609
|
+
super("LSP client not started");
|
|
610
|
+
this.serverId = serverId;
|
|
611
|
+
this.root = root;
|
|
612
|
+
}
|
|
532
613
|
}
|
|
533
614
|
|
|
534
615
|
class LspClientTransport {
|
|
@@ -541,6 +622,8 @@ class LspClientTransport {
|
|
|
541
622
|
diagnosticsStore = new Map;
|
|
542
623
|
requestTimeoutMs;
|
|
543
624
|
initializeTimeoutMs;
|
|
625
|
+
workspaceApplyEditHandler = null;
|
|
626
|
+
diagnosticPullSupported = false;
|
|
544
627
|
constructor(root, server, timeouts = {}) {
|
|
545
628
|
this.root = root;
|
|
546
629
|
this.server = server;
|
|
@@ -553,6 +636,21 @@ class LspClientTransport {
|
|
|
553
636
|
command() {
|
|
554
637
|
return [...this.server.command];
|
|
555
638
|
}
|
|
639
|
+
setWorkspaceApplyEditHandler(handler) {
|
|
640
|
+
this.workspaceApplyEditHandler = handler;
|
|
641
|
+
}
|
|
642
|
+
hasWorkspaceApplyEditHandler() {
|
|
643
|
+
return this.workspaceApplyEditHandler !== null;
|
|
644
|
+
}
|
|
645
|
+
setDiagnosticPullSupported(supported) {
|
|
646
|
+
this.diagnosticPullSupported = supported;
|
|
647
|
+
}
|
|
648
|
+
isDiagnosticPullSupported() {
|
|
649
|
+
return this.diagnosticPullSupported;
|
|
650
|
+
}
|
|
651
|
+
handlePublishDiagnostics(params) {
|
|
652
|
+
this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
|
|
653
|
+
}
|
|
556
654
|
async start() {
|
|
557
655
|
const env = createLspSpawnEnv(this.root, {
|
|
558
656
|
...process.env,
|
|
@@ -563,7 +661,6 @@ class LspClientTransport {
|
|
|
563
661
|
env
|
|
564
662
|
});
|
|
565
663
|
this.startStderrReading();
|
|
566
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
567
664
|
if (this.proc.exitCode !== null) {
|
|
568
665
|
const stderr = this.stderrBuffer.join(`
|
|
569
666
|
`);
|
|
@@ -573,7 +670,7 @@ class LspClientTransport {
|
|
|
573
670
|
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
574
671
|
const diagnosticsParams = parseDiagnosticsParams(params);
|
|
575
672
|
if (diagnosticsParams?.uri) {
|
|
576
|
-
this.
|
|
673
|
+
this.handlePublishDiagnostics(diagnosticsParams);
|
|
577
674
|
}
|
|
578
675
|
});
|
|
579
676
|
this.connection.onRequest("workspace/configuration", (params) => {
|
|
@@ -586,6 +683,9 @@ class LspClientTransport {
|
|
|
586
683
|
});
|
|
587
684
|
this.connection.onRequest("client/registerCapability", () => null);
|
|
588
685
|
this.connection.onRequest("window/workDoneProgress/create", () => null);
|
|
686
|
+
if (this.workspaceApplyEditHandler) {
|
|
687
|
+
this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
|
|
688
|
+
}
|
|
589
689
|
this.connection.onClose(() => {
|
|
590
690
|
this.processExited = true;
|
|
591
691
|
});
|
|
@@ -614,30 +714,25 @@ class LspClientTransport {
|
|
|
614
714
|
}
|
|
615
715
|
async sendRequest(method, ...args) {
|
|
616
716
|
if (!this.connection)
|
|
617
|
-
throw new
|
|
717
|
+
throw new LspClientNotStartedError(this.server.id, this.root);
|
|
618
718
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
619
719
|
const stderrTail = this.stderrBuffer.slice(-10).join(`
|
|
620
720
|
`);
|
|
621
721
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
|
|
622
722
|
}
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
723
|
+
const options = args[1];
|
|
724
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
725
|
+
const timeoutController = new AbortController;
|
|
726
|
+
const timeoutHandle = setTimeout(() => {
|
|
727
|
+
const stderrTail = this.stderrBuffer.slice(-5).join(`
|
|
628
728
|
`);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
729
|
+
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
730
|
+
}, timeoutMs);
|
|
731
|
+
const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
|
|
632
732
|
try {
|
|
633
|
-
const
|
|
634
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
635
|
-
if (timeoutHandle !== null)
|
|
636
|
-
clearTimeout(timeoutHandle);
|
|
733
|
+
const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
|
|
637
734
|
return result;
|
|
638
735
|
} catch (error) {
|
|
639
|
-
if (timeoutHandle !== null)
|
|
640
|
-
clearTimeout(timeoutHandle);
|
|
641
736
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
642
737
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
|
|
643
738
|
`) || undefined);
|
|
@@ -646,6 +741,9 @@ class LspClientTransport {
|
|
|
646
741
|
throw new LspConnectionClosedError(this.server.id, this.root, error.message);
|
|
647
742
|
}
|
|
648
743
|
throw error;
|
|
744
|
+
} finally {
|
|
745
|
+
clearTimeout(timeoutHandle);
|
|
746
|
+
combinedSignal.dispose();
|
|
649
747
|
}
|
|
650
748
|
}
|
|
651
749
|
async sendNotification(method, ...args) {
|
|
@@ -674,17 +772,17 @@ class LspClientTransport {
|
|
|
674
772
|
try {
|
|
675
773
|
await this.sendRequest("shutdown");
|
|
676
774
|
} catch (error) {
|
|
677
|
-
reportBestEffortCleanupError("shutdown request", error);
|
|
775
|
+
reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
|
|
678
776
|
}
|
|
679
777
|
try {
|
|
680
778
|
await this.sendNotification("exit");
|
|
681
779
|
} catch (error) {
|
|
682
|
-
reportBestEffortCleanupError("exit notification", error);
|
|
780
|
+
reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
|
|
683
781
|
}
|
|
684
782
|
try {
|
|
685
783
|
this.connection.dispose();
|
|
686
784
|
} catch (error) {
|
|
687
|
-
reportBestEffortCleanupError("connection dispose", error);
|
|
785
|
+
reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
|
|
688
786
|
}
|
|
689
787
|
this.connection = null;
|
|
690
788
|
}
|
|
@@ -715,11 +813,11 @@ class LspClientTransport {
|
|
|
715
813
|
new Promise((resolve) => setTimeout(resolve, STOP_SIGKILL_GRACE_MS))
|
|
716
814
|
]);
|
|
717
815
|
} catch (error) {
|
|
718
|
-
reportBestEffortCleanupError("hard process kill", error);
|
|
816
|
+
reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
|
|
719
817
|
}
|
|
720
818
|
}
|
|
721
819
|
} catch (error) {
|
|
722
|
-
reportBestEffortCleanupError("process stop", error);
|
|
820
|
+
reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
|
|
723
821
|
}
|
|
724
822
|
}
|
|
725
823
|
this.processExited = true;
|
|
@@ -729,26 +827,45 @@ class LspClientTransport {
|
|
|
729
827
|
return this.diagnosticsStore.get(uri) ?? [];
|
|
730
828
|
}
|
|
731
829
|
}
|
|
732
|
-
function
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
830
|
+
function combineAbortSignals(primary, secondary) {
|
|
831
|
+
const controller = new AbortController;
|
|
832
|
+
const abortFrom = (signal) => {
|
|
833
|
+
if (!controller.signal.aborted)
|
|
834
|
+
controller.abort(signal.reason);
|
|
835
|
+
};
|
|
836
|
+
const onPrimaryAbort = () => {
|
|
837
|
+
if (primary)
|
|
838
|
+
abortFrom(primary);
|
|
839
|
+
};
|
|
840
|
+
const onSecondaryAbort = () => abortFrom(secondary);
|
|
841
|
+
if (primary?.aborted)
|
|
842
|
+
abortFrom(primary);
|
|
843
|
+
else
|
|
844
|
+
primary?.addEventListener("abort", onPrimaryAbort, { once: true });
|
|
845
|
+
if (secondary.aborted)
|
|
846
|
+
abortFrom(secondary);
|
|
847
|
+
else
|
|
848
|
+
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
|
|
849
|
+
return {
|
|
850
|
+
signal: controller.signal,
|
|
851
|
+
dispose: () => {
|
|
852
|
+
primary?.removeEventListener("abort", onPrimaryAbort);
|
|
853
|
+
secondary.removeEventListener("abort", onSecondaryAbort);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
743
856
|
}
|
|
744
857
|
|
|
745
858
|
// ../lsp-core/src/lsp/connection.ts
|
|
746
|
-
|
|
859
|
+
function supportsDiagnosticPull(capabilities) {
|
|
860
|
+
if (capabilities === undefined)
|
|
861
|
+
return false;
|
|
862
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
863
|
+
}
|
|
747
864
|
|
|
748
865
|
class LspClientConnection extends LspClientTransport {
|
|
749
866
|
async initialize() {
|
|
750
867
|
const rootUri = pathToFileURL(this.root).href;
|
|
751
|
-
await this.sendRequest("initialize", {
|
|
868
|
+
const result = await this.sendRequest("initialize", {
|
|
752
869
|
processId: process.pid,
|
|
753
870
|
rootUri,
|
|
754
871
|
rootPath: this.root,
|
|
@@ -762,8 +879,7 @@ class LspClientConnection extends LspClientTransport {
|
|
|
762
879
|
publishDiagnostics: {},
|
|
763
880
|
rename: {
|
|
764
881
|
prepareSupport: true,
|
|
765
|
-
prepareSupportDefaultBehavior: 1
|
|
766
|
-
honorsChangeAnnotations: true
|
|
882
|
+
prepareSupportDefaultBehavior: 1
|
|
767
883
|
},
|
|
768
884
|
codeAction: {
|
|
769
885
|
codeActionLiteralSupport: {
|
|
@@ -792,22 +908,28 @@ class LspClientConnection extends LspClientTransport {
|
|
|
792
908
|
symbol: {},
|
|
793
909
|
workspaceFolders: true,
|
|
794
910
|
configuration: true,
|
|
795
|
-
applyEdit: true,
|
|
911
|
+
...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
|
|
796
912
|
workspaceEdit: {
|
|
797
|
-
documentChanges: true
|
|
913
|
+
documentChanges: true,
|
|
914
|
+
resourceOperations: ["create", "rename", "delete"]
|
|
798
915
|
}
|
|
799
916
|
}
|
|
800
917
|
},
|
|
801
918
|
initializationOptions: this.server.initialization
|
|
802
919
|
}, { timeoutMs: this.initializeTimeoutMs });
|
|
920
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
803
921
|
await this.sendNotification("initialized");
|
|
804
922
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
805
923
|
settings: { json: { validate: { enable: true } } }
|
|
806
924
|
});
|
|
807
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
808
925
|
}
|
|
809
926
|
}
|
|
810
927
|
|
|
928
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
929
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
930
|
+
import { relative, resolve } from "node:path";
|
|
931
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
932
|
+
|
|
811
933
|
// ../lsp-core/src/lsp/effective-extension.ts
|
|
812
934
|
import { basename, extname } from "node:path";
|
|
813
935
|
var BASENAME_EXTENSIONS = {
|
|
@@ -990,191 +1112,1662 @@ function getLanguageId(ext) {
|
|
|
990
1112
|
return EXT_TO_LANG[ext] ?? "plaintext";
|
|
991
1113
|
}
|
|
992
1114
|
|
|
993
|
-
// ../lsp-core/src/lsp/
|
|
994
|
-
var
|
|
995
|
-
var
|
|
1115
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1116
|
+
var WATCHED_FILE_BATCH_SIZE = 128;
|
|
1117
|
+
var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1118
|
+
function canonicalPath(filePath) {
|
|
1119
|
+
const absolute = resolve(filePath);
|
|
1120
|
+
try {
|
|
1121
|
+
return realpathSync(absolute);
|
|
1122
|
+
} catch {
|
|
1123
|
+
return absolute;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function isSameOrDescendant(candidate, parent) {
|
|
1127
|
+
const suffix = relative(parent, candidate);
|
|
1128
|
+
return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
|
|
1129
|
+
}
|
|
1130
|
+
function movedPath(candidate, oldPath, newPath) {
|
|
1131
|
+
const suffix = relative(oldPath, candidate);
|
|
1132
|
+
return suffix === "" ? newPath : resolve(newPath, suffix);
|
|
1133
|
+
}
|
|
996
1134
|
|
|
997
|
-
class
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1135
|
+
class WorkspaceDocumentState {
|
|
1136
|
+
sendNotification;
|
|
1137
|
+
clearDiagnostics;
|
|
1138
|
+
openDocuments = new Map;
|
|
1139
|
+
openByUri = new Map;
|
|
1140
|
+
openPromises = new Map;
|
|
1141
|
+
now;
|
|
1142
|
+
versionlessPublishQuiescenceMs;
|
|
1143
|
+
constructor(sendNotification, clearDiagnostics, options = {}) {
|
|
1144
|
+
this.sendNotification = sendNotification;
|
|
1145
|
+
this.clearDiagnostics = clearDiagnostics;
|
|
1146
|
+
this.now = options.now ?? (() => Date.now());
|
|
1147
|
+
this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
1004
1148
|
}
|
|
1005
1149
|
async openFile(filePath) {
|
|
1006
|
-
const
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
const languageId = getLanguageId(ext);
|
|
1012
|
-
const version = 1;
|
|
1013
|
-
await this.sendNotification("textDocument/didOpen", {
|
|
1014
|
-
textDocument: {
|
|
1015
|
-
uri,
|
|
1016
|
-
languageId,
|
|
1017
|
-
version,
|
|
1018
|
-
text
|
|
1019
|
-
}
|
|
1020
|
-
});
|
|
1021
|
-
this.openedFiles.add(absPath);
|
|
1022
|
-
this.documentVersions.set(uri, version);
|
|
1023
|
-
this.lastSyncedText.set(uri, text);
|
|
1024
|
-
await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
|
|
1025
|
-
return;
|
|
1150
|
+
const path = canonicalPath(filePath);
|
|
1151
|
+
const existingOpen = this.openPromises.get(path);
|
|
1152
|
+
if (existingOpen) {
|
|
1153
|
+
await existingOpen;
|
|
1154
|
+
return this.openFile(path);
|
|
1026
1155
|
}
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1156
|
+
const text = readFileSync(path, "utf-8");
|
|
1157
|
+
const existing = this.openDocuments.get(path);
|
|
1158
|
+
if (!existing)
|
|
1159
|
+
return this.openDocumentSingleFlight(path, text);
|
|
1160
|
+
if (existing.text === text)
|
|
1029
1161
|
return;
|
|
1030
|
-
|
|
1031
|
-
const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
|
|
1032
|
-
this.documentVersions.set(uri, nextVersion);
|
|
1033
|
-
this.lastSyncedText.set(uri, text);
|
|
1034
|
-
await this.sendNotification("textDocument/didChange", {
|
|
1035
|
-
textDocument: { uri, version: nextVersion },
|
|
1036
|
-
contentChanges: [{ text }]
|
|
1037
|
-
});
|
|
1038
|
-
await this.sendNotification("textDocument/didSave", {
|
|
1039
|
-
textDocument: { uri },
|
|
1040
|
-
text
|
|
1041
|
-
});
|
|
1162
|
+
await this.changeDocument(existing, text);
|
|
1042
1163
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
await this.openFile(absPath);
|
|
1046
|
-
return this.sendRequest("textDocument/definition", {
|
|
1047
|
-
textDocument: { uri: pathToFileURL2(absPath).href },
|
|
1048
|
-
position: { line: line - 1, character }
|
|
1049
|
-
});
|
|
1164
|
+
getVersion(filePath) {
|
|
1165
|
+
return this.openDocuments.get(canonicalPath(filePath))?.version;
|
|
1050
1166
|
}
|
|
1051
|
-
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1167
|
+
getStoredDiagnostics(uri) {
|
|
1168
|
+
const state = this.openByUri.get(uri);
|
|
1169
|
+
if (!state)
|
|
1170
|
+
return [];
|
|
1171
|
+
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
1172
|
+
}
|
|
1173
|
+
captureDiagnosticSnapshot(filePath) {
|
|
1174
|
+
const state = this.openDocuments.get(canonicalPath(filePath));
|
|
1175
|
+
if (!state)
|
|
1176
|
+
return null;
|
|
1177
|
+
return {
|
|
1178
|
+
path: state.path,
|
|
1179
|
+
uri: state.uri,
|
|
1180
|
+
version: state.version,
|
|
1181
|
+
documentGeneration: state.generation,
|
|
1182
|
+
publishGeneration: state.publishGeneration
|
|
1183
|
+
};
|
|
1059
1184
|
}
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
return this.sendRequest("textDocument/documentSymbol", {
|
|
1064
|
-
textDocument: { uri: pathToFileURL2(absPath).href }
|
|
1065
|
-
});
|
|
1185
|
+
isCurrentSnapshot(snapshot) {
|
|
1186
|
+
const state = this.openDocuments.get(snapshot.path);
|
|
1187
|
+
return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
|
|
1066
1188
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1189
|
+
getPullCache(snapshot) {
|
|
1190
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1191
|
+
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
|
|
1192
|
+
return null;
|
|
1193
|
+
return state.pullCache;
|
|
1069
1194
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1195
|
+
recordPullDiagnostics(snapshot, report) {
|
|
1196
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1197
|
+
if (!state)
|
|
1198
|
+
return;
|
|
1199
|
+
state.pullCache = {
|
|
1200
|
+
documentVersion: snapshot.version,
|
|
1201
|
+
diagnostics: [...report.diagnostics],
|
|
1202
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
1203
|
+
};
|
|
1077
1204
|
}
|
|
1078
|
-
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1205
|
+
recordPublishedDiagnostics(params) {
|
|
1206
|
+
const state = this.openByUri.get(params.uri);
|
|
1207
|
+
if (!state)
|
|
1208
|
+
return;
|
|
1209
|
+
state.publishGeneration += 1;
|
|
1210
|
+
state.lastPublish = {
|
|
1211
|
+
diagnostics: [...params.diagnostics],
|
|
1212
|
+
publishGeneration: state.publishGeneration,
|
|
1213
|
+
documentGenerationAtArrival: state.generation,
|
|
1214
|
+
arrivedAt: this.now(),
|
|
1215
|
+
...params.version === undefined ? {} : { version: params.version }
|
|
1216
|
+
};
|
|
1217
|
+
this.notifyWaiters(state);
|
|
1218
|
+
}
|
|
1219
|
+
resolvePushDiagnostics(snapshot) {
|
|
1220
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1221
|
+
if (!state?.lastPublish)
|
|
1222
|
+
return { status: "missing" };
|
|
1223
|
+
const publish = state.lastPublish;
|
|
1224
|
+
if (publish.version !== undefined) {
|
|
1225
|
+
return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
|
|
1226
|
+
}
|
|
1227
|
+
if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
|
|
1228
|
+
return { status: "missing" };
|
|
1229
|
+
const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
|
|
1230
|
+
const waitMs = Math.max(0, readyAt - this.now());
|
|
1231
|
+
return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
|
|
1232
|
+
}
|
|
1233
|
+
waitForDiagnosticsActivity(snapshot, timeoutMs) {
|
|
1234
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1235
|
+
if (!state || timeoutMs <= 0)
|
|
1236
|
+
return Promise.resolve();
|
|
1237
|
+
return new Promise((resolveActivity) => {
|
|
1238
|
+
let settled = false;
|
|
1239
|
+
const finish = () => {
|
|
1240
|
+
if (settled)
|
|
1241
|
+
return;
|
|
1242
|
+
settled = true;
|
|
1243
|
+
clearTimeout(timer);
|
|
1244
|
+
state.waiters.delete(finish);
|
|
1245
|
+
resolveActivity();
|
|
1246
|
+
};
|
|
1247
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1248
|
+
if (typeof timer.unref === "function")
|
|
1249
|
+
timer.unref();
|
|
1250
|
+
state.waiters.add(finish);
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
validateVersions(operations) {
|
|
1254
|
+
const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
|
|
1255
|
+
for (const operation of operations) {
|
|
1256
|
+
if (operation.kind === "text") {
|
|
1257
|
+
const current = versions.get(operation.path);
|
|
1258
|
+
if (operation.documentVersion !== null && current !== operation.documentVersion) {
|
|
1259
|
+
const observed = current === undefined ? "closed document" : `open document version ${current}`;
|
|
1260
|
+
return {
|
|
1261
|
+
changeIndex: operation.changeIndex,
|
|
1262
|
+
message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
if (current !== undefined)
|
|
1266
|
+
versions.set(operation.path, current + 1);
|
|
1267
|
+
continue;
|
|
1089
1268
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1269
|
+
if (operation.kind === "rename") {
|
|
1270
|
+
const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
|
|
1271
|
+
for (const [path] of moved)
|
|
1272
|
+
versions.delete(path);
|
|
1273
|
+
for (const [path] of moved)
|
|
1274
|
+
versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (operation.kind === "delete") {
|
|
1278
|
+
for (const path of [...versions.keys()]) {
|
|
1279
|
+
if (isSameOrDescendant(path, operation.path))
|
|
1280
|
+
versions.delete(path);
|
|
1281
|
+
}
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
|
|
1285
|
+
versions.set(operation.path, 1);
|
|
1093
1286
|
}
|
|
1094
1287
|
}
|
|
1095
|
-
return
|
|
1288
|
+
return null;
|
|
1096
1289
|
}
|
|
1097
|
-
async
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1290
|
+
async synchronize(delta) {
|
|
1291
|
+
const watched = [];
|
|
1292
|
+
for (const mutation of delta.operations)
|
|
1293
|
+
await this.synchronizeMutation(mutation, watched);
|
|
1294
|
+
for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
|
|
1295
|
+
await this.sendNotification("workspace/didChangeWatchedFiles", {
|
|
1296
|
+
changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1104
1299
|
}
|
|
1105
|
-
async
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1300
|
+
async synchronizeMutation(mutation, watched) {
|
|
1301
|
+
if (mutation.kind === "text") {
|
|
1302
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1303
|
+
if (state)
|
|
1304
|
+
await this.changeDocument(state, mutation.afterText);
|
|
1305
|
+
else
|
|
1306
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (mutation.kind === "create") {
|
|
1310
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1311
|
+
if (state) {
|
|
1312
|
+
await this.closeDocument(state);
|
|
1313
|
+
await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
|
|
1314
|
+
} else {
|
|
1315
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
|
|
1316
|
+
}
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (mutation.kind === "rename") {
|
|
1320
|
+
const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
|
|
1321
|
+
for (const state of moved)
|
|
1322
|
+
await this.closeDocument(state);
|
|
1323
|
+
for (const state of moved) {
|
|
1324
|
+
const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
|
|
1325
|
+
await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
|
|
1326
|
+
}
|
|
1327
|
+
if (moved.length === 0) {
|
|
1328
|
+
watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
|
|
1329
|
+
watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
|
|
1330
|
+
}
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
|
|
1334
|
+
for (const state of removed)
|
|
1335
|
+
await this.closeDocument(state);
|
|
1336
|
+
if (removed.length === 0)
|
|
1337
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
|
|
1338
|
+
}
|
|
1339
|
+
async openDocumentSingleFlight(path, text) {
|
|
1340
|
+
const existing = this.openPromises.get(path);
|
|
1341
|
+
if (existing)
|
|
1342
|
+
return existing;
|
|
1343
|
+
const open = (async () => {
|
|
1344
|
+
const state = {
|
|
1345
|
+
path,
|
|
1346
|
+
uri: pathToFileURL2(path).href,
|
|
1347
|
+
languageId: getLanguageId(effectiveExtension(path)),
|
|
1348
|
+
text,
|
|
1349
|
+
version: 1,
|
|
1350
|
+
generation: 1,
|
|
1351
|
+
publishGeneration: 0,
|
|
1352
|
+
waiters: new Set
|
|
1353
|
+
};
|
|
1354
|
+
this.openDocuments.set(path, state);
|
|
1355
|
+
this.openByUri.set(state.uri, state);
|
|
1356
|
+
this.notifyWaiters(state);
|
|
1357
|
+
await this.sendNotification("textDocument/didOpen", {
|
|
1358
|
+
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
|
|
1359
|
+
});
|
|
1360
|
+
})().finally(() => {
|
|
1361
|
+
this.openPromises.delete(path);
|
|
1112
1362
|
});
|
|
1363
|
+
this.openPromises.set(path, open);
|
|
1364
|
+
return open;
|
|
1365
|
+
}
|
|
1366
|
+
async changeDocument(state, text) {
|
|
1367
|
+
state.text = text;
|
|
1368
|
+
state.version += 1;
|
|
1369
|
+
state.generation += 1;
|
|
1370
|
+
this.clearDiagnostics(state.uri);
|
|
1371
|
+
this.notifyWaiters(state);
|
|
1372
|
+
await this.sendNotification("textDocument/didChange", {
|
|
1373
|
+
textDocument: { uri: state.uri, version: state.version },
|
|
1374
|
+
contentChanges: [{ text }]
|
|
1375
|
+
});
|
|
1376
|
+
await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
|
|
1377
|
+
}
|
|
1378
|
+
async closeDocument(state) {
|
|
1379
|
+
this.openDocuments.delete(state.path);
|
|
1380
|
+
this.openByUri.delete(state.uri);
|
|
1381
|
+
this.clearDiagnostics(state.uri);
|
|
1382
|
+
this.notifyWaiters(state);
|
|
1383
|
+
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
1384
|
+
}
|
|
1385
|
+
notifyWaiters(state) {
|
|
1386
|
+
for (const waiter of [...state.waiters])
|
|
1387
|
+
waiter();
|
|
1113
1388
|
}
|
|
1114
1389
|
}
|
|
1115
1390
|
|
|
1116
|
-
// ../lsp-core/src/lsp/
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
};
|
|
1124
|
-
for (const signal of signals) {
|
|
1125
|
-
process.on(signal, handler);
|
|
1126
|
-
}
|
|
1127
|
-
return () => {
|
|
1128
|
-
for (const signal of signals) {
|
|
1129
|
-
process.removeListener(signal, handler);
|
|
1130
|
-
}
|
|
1131
|
-
};
|
|
1391
|
+
// ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
|
|
1392
|
+
var CONCURRENT_FAILURE_REASON_BY_PHASE = {
|
|
1393
|
+
applying: "workspace/applyEdit is already in progress for this workspace mutation",
|
|
1394
|
+
settled: "workspace/applyEdit was already handled for this workspace mutation"
|
|
1395
|
+
};
|
|
1396
|
+
function workspaceApplyEditConcurrentFailureReason(phase) {
|
|
1397
|
+
return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
|
|
1132
1398
|
}
|
|
1133
1399
|
|
|
1134
|
-
// ../lsp-core/src/lsp/
|
|
1135
|
-
|
|
1400
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1401
|
+
import { existsSync as existsSync3, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1402
|
+
|
|
1403
|
+
// ../lsp-core/src/lsp/workspace-edit-path.ts
|
|
1404
|
+
import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync2 } from "node:fs";
|
|
1405
|
+
import { dirname, isAbsolute, relative as relative2, resolve as resolve2 } from "node:path";
|
|
1406
|
+
import { fileURLToPath } from "node:url";
|
|
1407
|
+
|
|
1408
|
+
class WorkspaceEditPathError extends Error {
|
|
1409
|
+
path;
|
|
1410
|
+
detail;
|
|
1411
|
+
name = "WorkspaceEditPathError";
|
|
1412
|
+
constructor(path, detail) {
|
|
1413
|
+
super(`${detail}: ${path}`);
|
|
1414
|
+
this.path = path;
|
|
1415
|
+
this.detail = detail;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
1419
|
+
const relativePath = relative2(workspaceRoot, filePath);
|
|
1420
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
1421
|
+
}
|
|
1422
|
+
function canonicalizeMissingPath(filePath) {
|
|
1423
|
+
let ancestor = filePath;
|
|
1424
|
+
while (!existsSync2(ancestor)) {
|
|
1425
|
+
const parent = dirname(ancestor);
|
|
1426
|
+
if (parent === ancestor)
|
|
1427
|
+
throw new WorkspaceEditPathError(filePath, "no existing ancestor");
|
|
1428
|
+
ancestor = parent;
|
|
1429
|
+
}
|
|
1430
|
+
return resolve2(realpathSync2(ancestor), relative2(ancestor, filePath));
|
|
1431
|
+
}
|
|
1432
|
+
function canonicalWorkspaceRoot(workspaceRoot) {
|
|
1136
1433
|
try {
|
|
1137
|
-
|
|
1434
|
+
const canonical = realpathSync2(resolve2(workspaceRoot));
|
|
1435
|
+
if (!lstatSync(canonical).isDirectory()) {
|
|
1436
|
+
return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
|
|
1437
|
+
}
|
|
1438
|
+
return {
|
|
1439
|
+
success: true,
|
|
1440
|
+
path: canonical,
|
|
1441
|
+
requestedPath: resolve2(workspaceRoot),
|
|
1442
|
+
followedSymbolicLink: existsSync2(resolve2(workspaceRoot)) && lstatSync(resolve2(workspaceRoot)).isSymbolicLink()
|
|
1443
|
+
};
|
|
1138
1444
|
} catch (error) {
|
|
1139
|
-
|
|
1445
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1446
|
+
return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
|
|
1140
1447
|
}
|
|
1141
1448
|
}
|
|
1142
|
-
function
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
if (settled)
|
|
1149
|
-
return;
|
|
1150
|
-
settled = true;
|
|
1151
|
-
reject(new DOMException("Aborted", "AbortError"));
|
|
1152
|
-
};
|
|
1153
|
-
if (signal.aborted) {
|
|
1154
|
-
onAbort();
|
|
1155
|
-
return;
|
|
1449
|
+
function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
|
|
1450
|
+
let requestedPath;
|
|
1451
|
+
try {
|
|
1452
|
+
const parsed = new URL(uri);
|
|
1453
|
+
if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
|
|
1454
|
+
return { success: false, error: `non-file URI ${uri}` };
|
|
1156
1455
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1456
|
+
requestedPath = resolve2(fileURLToPath(parsed));
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1459
|
+
return { success: false, error: `non-file URI ${uri}: ${detail}` };
|
|
1460
|
+
}
|
|
1461
|
+
try {
|
|
1462
|
+
const canonical = existsSync2(requestedPath) ? realpathSync2(requestedPath) : canonicalizeMissingPath(requestedPath);
|
|
1463
|
+
if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
|
|
1464
|
+
return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
|
|
1465
|
+
}
|
|
1466
|
+
return {
|
|
1467
|
+
success: true,
|
|
1468
|
+
path: canonical,
|
|
1469
|
+
requestedPath,
|
|
1470
|
+
followedSymbolicLink: existsSync2(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
|
|
1471
|
+
};
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1474
|
+
return { success: false, error: `${requestedPath}: ${detail}` };
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function snapshotPath(path, includeChildren) {
|
|
1478
|
+
if (!existsSync2(path))
|
|
1479
|
+
return { kind: "missing" };
|
|
1480
|
+
const stats = lstatSync(path);
|
|
1481
|
+
if (stats.isFile())
|
|
1482
|
+
return { kind: "file", content: readFileSync2(path, "utf-8") };
|
|
1483
|
+
if (stats.isDirectory()) {
|
|
1484
|
+
return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
|
|
1485
|
+
}
|
|
1486
|
+
throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
|
|
1172
1487
|
}
|
|
1173
1488
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1489
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1490
|
+
var DEFAULT_IO = {
|
|
1491
|
+
writeFile(path, content) {
|
|
1492
|
+
writeFileSync(path, content, "utf-8");
|
|
1493
|
+
},
|
|
1494
|
+
rename(oldPath, newPath) {
|
|
1495
|
+
renameSync(oldPath, newPath);
|
|
1496
|
+
},
|
|
1497
|
+
remove(path, recursive) {
|
|
1498
|
+
rmSync(path, { recursive, force: false });
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
function snapshotsEqual(expected, actual) {
|
|
1502
|
+
if (expected.kind !== actual.kind)
|
|
1503
|
+
return false;
|
|
1504
|
+
if (expected.kind === "file" && actual.kind === "file")
|
|
1505
|
+
return expected.content === actual.content;
|
|
1506
|
+
if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
|
|
1507
|
+
return JSON.stringify(expected.children) === JSON.stringify(actual.children);
|
|
1508
|
+
}
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
function liveSnapshot(path, expected) {
|
|
1512
|
+
return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
|
|
1513
|
+
}
|
|
1514
|
+
function firstOperationIndex(plan) {
|
|
1515
|
+
return plan.operations[0]?.changeIndex ?? 0;
|
|
1516
|
+
}
|
|
1517
|
+
function failedCommit(plan, failure) {
|
|
1518
|
+
const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
|
|
1519
|
+
return {
|
|
1520
|
+
result: {
|
|
1521
|
+
success: false,
|
|
1522
|
+
filesModified,
|
|
1523
|
+
totalEdits,
|
|
1524
|
+
errors: [`change ${changeIndex}: ${message}`],
|
|
1525
|
+
failedChange: changeIndex,
|
|
1526
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1527
|
+
},
|
|
1528
|
+
delta: mutationDelta(mutations),
|
|
1529
|
+
fingerprint: plan.fingerprint
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function verifySnapshots(plan) {
|
|
1533
|
+
for (const [path, expected] of plan.snapshots) {
|
|
1534
|
+
let actual;
|
|
1535
|
+
try {
|
|
1536
|
+
actual = liveSnapshot(path, expected);
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1539
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1540
|
+
return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
|
|
1541
|
+
}
|
|
1542
|
+
if (!snapshotsEqual(expected, actual)) {
|
|
1543
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1544
|
+
return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
function addModifiedPath(paths, path) {
|
|
1550
|
+
if (!paths.includes(path))
|
|
1551
|
+
paths.push(path);
|
|
1552
|
+
}
|
|
1553
|
+
function reportedPath(plan, path) {
|
|
1554
|
+
return plan.reportedPathByCanonical.get(path) ?? path;
|
|
1555
|
+
}
|
|
1556
|
+
function changedPathsForMutation(mutation) {
|
|
1557
|
+
return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
|
|
1558
|
+
}
|
|
1559
|
+
function mutationDelta(operations) {
|
|
1560
|
+
const changedPaths = new Set;
|
|
1561
|
+
for (const operation of operations) {
|
|
1562
|
+
for (const path of changedPathsForMutation(operation))
|
|
1563
|
+
changedPaths.add(path);
|
|
1564
|
+
}
|
|
1565
|
+
return { operations, changedPaths: [...changedPaths].sort() };
|
|
1566
|
+
}
|
|
1567
|
+
function resolveIo(overrides) {
|
|
1568
|
+
return {
|
|
1569
|
+
writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
|
|
1570
|
+
rename: overrides?.rename ?? DEFAULT_IO.rename,
|
|
1571
|
+
remove: overrides?.remove ?? DEFAULT_IO.remove
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function commitOperation(context, operation) {
|
|
1575
|
+
const { plan, io, accumulator } = context;
|
|
1576
|
+
if (operation.kind === "noop")
|
|
1577
|
+
return;
|
|
1578
|
+
if (operation.kind === "text") {
|
|
1579
|
+
io.writeFile(operation.path, operation.afterText);
|
|
1580
|
+
accumulator.mutations.push({
|
|
1581
|
+
kind: "text",
|
|
1582
|
+
path: operation.path,
|
|
1583
|
+
beforeText: operation.beforeText,
|
|
1584
|
+
afterText: operation.afterText
|
|
1585
|
+
});
|
|
1586
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1587
|
+
accumulator.totalEdits += operation.editCount;
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (operation.kind === "create") {
|
|
1591
|
+
io.writeFile(operation.path, "");
|
|
1592
|
+
accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
|
|
1593
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (operation.kind === "rename") {
|
|
1597
|
+
if (operation.replaceDestination) {
|
|
1598
|
+
const targetKind = existsSync3(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
|
|
1599
|
+
io.remove(operation.newPath, targetKind === "directory");
|
|
1600
|
+
accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
|
|
1601
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1602
|
+
}
|
|
1603
|
+
io.rename(operation.oldPath, operation.newPath);
|
|
1604
|
+
accumulator.mutations.push({
|
|
1605
|
+
kind: "rename",
|
|
1606
|
+
oldPath: operation.oldPath,
|
|
1607
|
+
newPath: operation.newPath,
|
|
1608
|
+
sourceKind: operation.sourceKind
|
|
1609
|
+
});
|
|
1610
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
io.remove(operation.path, operation.recursive);
|
|
1614
|
+
accumulator.mutations.push({
|
|
1615
|
+
kind: "delete",
|
|
1616
|
+
path: operation.path,
|
|
1617
|
+
targetKind: operation.targetKind
|
|
1618
|
+
});
|
|
1619
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1620
|
+
}
|
|
1621
|
+
function commitWorkspaceEditPlan(plan, options = {}) {
|
|
1622
|
+
if (options.signal?.aborted) {
|
|
1623
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
1624
|
+
}
|
|
1625
|
+
const stale = verifySnapshots(plan);
|
|
1626
|
+
if (stale)
|
|
1627
|
+
return stale;
|
|
1628
|
+
if (options.signal?.aborted) {
|
|
1629
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
1630
|
+
}
|
|
1631
|
+
const io = resolveIo(options.io);
|
|
1632
|
+
const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
|
|
1633
|
+
const context = { plan, io, accumulator };
|
|
1634
|
+
let lateAbort = false;
|
|
1635
|
+
for (const operation of plan.operations) {
|
|
1636
|
+
try {
|
|
1637
|
+
commitOperation(context, operation);
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1640
|
+
return failedCommit(plan, {
|
|
1641
|
+
message: `I/O failure during ${operation.kind}: ${detail}`,
|
|
1642
|
+
changeIndex: operation.changeIndex,
|
|
1643
|
+
mutations: accumulator.mutations,
|
|
1644
|
+
filesModified: accumulator.filesModified,
|
|
1645
|
+
totalEdits: accumulator.totalEdits,
|
|
1646
|
+
lateAbort: lateAbort || options.signal?.aborted === true
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
if (options.signal?.aborted)
|
|
1650
|
+
lateAbort = true;
|
|
1651
|
+
}
|
|
1652
|
+
const result = {
|
|
1653
|
+
success: true,
|
|
1654
|
+
filesModified: accumulator.filesModified,
|
|
1655
|
+
totalEdits: accumulator.totalEdits,
|
|
1656
|
+
errors: [],
|
|
1657
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1658
|
+
};
|
|
1659
|
+
return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
|
|
1663
|
+
import { createHash } from "node:crypto";
|
|
1664
|
+
function canonicalFingerprint(operations) {
|
|
1665
|
+
const canonical = operations.map((operation) => {
|
|
1666
|
+
switch (operation.kind) {
|
|
1667
|
+
case "text":
|
|
1668
|
+
return {
|
|
1669
|
+
kind: operation.kind,
|
|
1670
|
+
changeIndex: operation.changeIndex,
|
|
1671
|
+
path: operation.path,
|
|
1672
|
+
edits: operation.edits,
|
|
1673
|
+
version: operation.version
|
|
1674
|
+
};
|
|
1675
|
+
case "rename":
|
|
1676
|
+
return {
|
|
1677
|
+
kind: operation.kind,
|
|
1678
|
+
changeIndex: operation.changeIndex,
|
|
1679
|
+
oldPath: operation.oldPath,
|
|
1680
|
+
newPath: operation.newPath,
|
|
1681
|
+
overwrite: operation.overwrite,
|
|
1682
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
1683
|
+
};
|
|
1684
|
+
case "create":
|
|
1685
|
+
return {
|
|
1686
|
+
kind: operation.kind,
|
|
1687
|
+
changeIndex: operation.changeIndex,
|
|
1688
|
+
path: operation.path,
|
|
1689
|
+
overwrite: operation.overwrite,
|
|
1690
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
1691
|
+
};
|
|
1692
|
+
case "delete":
|
|
1693
|
+
return {
|
|
1694
|
+
kind: operation.kind,
|
|
1695
|
+
changeIndex: operation.changeIndex,
|
|
1696
|
+
path: operation.path,
|
|
1697
|
+
recursive: operation.recursive,
|
|
1698
|
+
ignoreIfNotExists: operation.ignoreIfNotExists
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// ../lsp-core/src/lsp/workspace-edit-types.ts
|
|
1706
|
+
class WorkspaceEditValidationError extends Error {
|
|
1707
|
+
changeIndex;
|
|
1708
|
+
detail;
|
|
1709
|
+
name = "WorkspaceEditValidationError";
|
|
1710
|
+
constructor(changeIndex, detail) {
|
|
1711
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
1712
|
+
this.changeIndex = changeIndex;
|
|
1713
|
+
this.detail = detail;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
|
|
1718
|
+
function isRecord2(value) {
|
|
1719
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1720
|
+
}
|
|
1721
|
+
function parsePosition(value) {
|
|
1722
|
+
if (!isRecord2(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
return { line: value["line"], character: value["character"] };
|
|
1726
|
+
}
|
|
1727
|
+
function parseRange(value) {
|
|
1728
|
+
if (!isRecord2(value))
|
|
1729
|
+
return null;
|
|
1730
|
+
const start = parsePosition(value["start"]);
|
|
1731
|
+
const end = parsePosition(value["end"]);
|
|
1732
|
+
return start && end ? { start, end } : null;
|
|
1733
|
+
}
|
|
1734
|
+
function parseTextEdits(value, changeIndex) {
|
|
1735
|
+
if (!Array.isArray(value)) {
|
|
1736
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
|
|
1737
|
+
}
|
|
1738
|
+
const edits = [];
|
|
1739
|
+
for (const candidate of value) {
|
|
1740
|
+
if (!isRecord2(candidate) || typeof candidate["newText"] !== "string") {
|
|
1741
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
|
|
1742
|
+
}
|
|
1743
|
+
if ("annotationId" in candidate) {
|
|
1744
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
|
|
1745
|
+
}
|
|
1746
|
+
const range = parseRange(candidate["range"]);
|
|
1747
|
+
if (!range)
|
|
1748
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
|
|
1749
|
+
edits.push({ range, newText: candidate["newText"] });
|
|
1750
|
+
}
|
|
1751
|
+
return edits;
|
|
1752
|
+
}
|
|
1753
|
+
function parseBooleanOption(options, key, changeIndex) {
|
|
1754
|
+
const value = options[key];
|
|
1755
|
+
if (value === undefined)
|
|
1756
|
+
return false;
|
|
1757
|
+
if (typeof value !== "boolean") {
|
|
1758
|
+
throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
|
|
1759
|
+
}
|
|
1760
|
+
return value;
|
|
1761
|
+
}
|
|
1762
|
+
function parseOptions(value, allowed, changeIndex) {
|
|
1763
|
+
if (value === undefined)
|
|
1764
|
+
return {};
|
|
1765
|
+
if (!isRecord2(value))
|
|
1766
|
+
throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
|
|
1767
|
+
for (const key of Object.keys(value)) {
|
|
1768
|
+
if (!allowed.includes(key))
|
|
1769
|
+
throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
|
|
1770
|
+
}
|
|
1771
|
+
const parsed = {};
|
|
1772
|
+
for (const key of allowed)
|
|
1773
|
+
parsed[key] = parseBooleanOption(value, key, changeIndex);
|
|
1774
|
+
return parsed;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
|
|
1778
|
+
function parseResourceChange(input) {
|
|
1779
|
+
const kind = input.change["kind"];
|
|
1780
|
+
if (kind === "create" || kind === "delete") {
|
|
1781
|
+
parseSinglePathResource(input, kind);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (kind !== "rename") {
|
|
1785
|
+
throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
|
|
1786
|
+
}
|
|
1787
|
+
parseRename(input);
|
|
1788
|
+
}
|
|
1789
|
+
function parseSinglePathResource(input, kind) {
|
|
1790
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1791
|
+
if (typeof change["uri"] !== "string")
|
|
1792
|
+
throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
|
|
1793
|
+
const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
|
|
1794
|
+
if (!resolvedPath.success) {
|
|
1795
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (kind === "create") {
|
|
1799
|
+
const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
1800
|
+
target.operations.push({
|
|
1801
|
+
kind,
|
|
1802
|
+
changeIndex,
|
|
1803
|
+
path: resolvedPath.path,
|
|
1804
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1805
|
+
overwrite: options2["overwrite"] ?? false,
|
|
1806
|
+
ignoreIfExists: options2["ignoreIfExists"] ?? false,
|
|
1807
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
1808
|
+
});
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
|
|
1812
|
+
target.operations.push({
|
|
1813
|
+
kind,
|
|
1814
|
+
changeIndex,
|
|
1815
|
+
path: resolvedPath.path,
|
|
1816
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1817
|
+
recursive: options["recursive"] ?? false,
|
|
1818
|
+
ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
|
|
1819
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
function parseRename(input) {
|
|
1823
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1824
|
+
if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
|
|
1825
|
+
throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
|
|
1826
|
+
}
|
|
1827
|
+
const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
|
|
1828
|
+
const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
|
|
1829
|
+
if (!oldPath.success || !newPath.success) {
|
|
1830
|
+
target.failures.push({
|
|
1831
|
+
changeIndex,
|
|
1832
|
+
message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
|
|
1833
|
+
});
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
1837
|
+
target.operations.push({
|
|
1838
|
+
kind: "rename",
|
|
1839
|
+
changeIndex,
|
|
1840
|
+
oldPath: oldPath.path,
|
|
1841
|
+
newPath: newPath.path,
|
|
1842
|
+
reportedOldPath: oldPath.requestedPath,
|
|
1843
|
+
reportedNewPath: newPath.requestedPath,
|
|
1844
|
+
overwrite: options["overwrite"] ?? false,
|
|
1845
|
+
ignoreIfExists: options["ignoreIfExists"] ?? false,
|
|
1846
|
+
followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// ../lsp-core/src/lsp/workspace-edit-parser.ts
|
|
1851
|
+
function failureResult(failures) {
|
|
1852
|
+
const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
|
|
1853
|
+
const first = sorted[0];
|
|
1854
|
+
return {
|
|
1855
|
+
success: false,
|
|
1856
|
+
filesModified: [],
|
|
1857
|
+
totalEdits: 0,
|
|
1858
|
+
errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
|
|
1859
|
+
...first ? { failedChange: first.changeIndex } : {}
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function parseWorkspaceEdit(edit, workspaceRoot) {
|
|
1863
|
+
if (!isRecord2(edit))
|
|
1864
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
|
|
1865
|
+
if (edit["changeAnnotations"] !== undefined) {
|
|
1866
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
|
|
1867
|
+
}
|
|
1868
|
+
const hasChanges = edit["changes"] !== undefined;
|
|
1869
|
+
const hasDocumentChanges = edit["documentChanges"] !== undefined;
|
|
1870
|
+
if (hasChanges && hasDocumentChanges) {
|
|
1871
|
+
return {
|
|
1872
|
+
operations: [],
|
|
1873
|
+
failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
const target = { operations: [], failures: [] };
|
|
1877
|
+
if (hasChanges)
|
|
1878
|
+
return parseChanges(edit["changes"], workspaceRoot, target);
|
|
1879
|
+
return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
|
|
1880
|
+
}
|
|
1881
|
+
function parseChanges(value, workspaceRoot, target) {
|
|
1882
|
+
if (!isRecord2(value))
|
|
1883
|
+
return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
|
|
1884
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
1885
|
+
for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
|
|
1886
|
+
const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
|
|
1887
|
+
if (!resolvedPath.success) {
|
|
1888
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
target.operations.push({
|
|
1893
|
+
kind: "text",
|
|
1894
|
+
changeIndex,
|
|
1895
|
+
path: resolvedPath.path,
|
|
1896
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1897
|
+
edits: parseTextEdits(rawEdits, changeIndex),
|
|
1898
|
+
version: null
|
|
1899
|
+
});
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
1902
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
throw error;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
return target;
|
|
1909
|
+
}
|
|
1910
|
+
function parseDocumentChanges(value, workspaceRoot, target) {
|
|
1911
|
+
if (value === undefined)
|
|
1912
|
+
return target;
|
|
1913
|
+
if (!Array.isArray(value)) {
|
|
1914
|
+
return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
|
|
1915
|
+
}
|
|
1916
|
+
for (const [changeIndex, change] of value.entries()) {
|
|
1917
|
+
try {
|
|
1918
|
+
parseDocumentChange({ change, changeIndex, workspaceRoot, target });
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
1921
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
throw error;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
return target;
|
|
1928
|
+
}
|
|
1929
|
+
function parseDocumentChange(input) {
|
|
1930
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1931
|
+
if (!isRecord2(change))
|
|
1932
|
+
throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
|
|
1933
|
+
if ("annotationId" in change) {
|
|
1934
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
|
|
1935
|
+
}
|
|
1936
|
+
if (typeof change["kind"] === "string") {
|
|
1937
|
+
parseResourceChange({ change, changeIndex, workspaceRoot, target });
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const identifier = change["textDocument"];
|
|
1941
|
+
if (!isRecord2(identifier) || typeof identifier["uri"] !== "string") {
|
|
1942
|
+
throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
|
|
1943
|
+
}
|
|
1944
|
+
const version = identifier["version"];
|
|
1945
|
+
if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
|
|
1946
|
+
throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
|
|
1947
|
+
}
|
|
1948
|
+
const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
|
|
1949
|
+
if (!resolvedPath.success) {
|
|
1950
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
target.operations.push({
|
|
1954
|
+
kind: "text",
|
|
1955
|
+
changeIndex,
|
|
1956
|
+
path: resolvedPath.path,
|
|
1957
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1958
|
+
edits: parseTextEdits(change["edits"], changeIndex),
|
|
1959
|
+
version
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
1964
|
+
import { dirname as dirname2, relative as relative3, resolve as resolve3 } from "node:path";
|
|
1965
|
+
|
|
1966
|
+
// ../lsp-core/src/lsp/workspace-edit-text.ts
|
|
1967
|
+
function comparePosition(left, right) {
|
|
1968
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
1969
|
+
}
|
|
1970
|
+
function positionsEqual(left, right) {
|
|
1971
|
+
return left.line === right.line && left.character === right.character;
|
|
1972
|
+
}
|
|
1973
|
+
function rangesEqual(left, right) {
|
|
1974
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
1975
|
+
}
|
|
1976
|
+
function isEmptyRange(range) {
|
|
1977
|
+
return positionsEqual(range.start, range.end);
|
|
1978
|
+
}
|
|
1979
|
+
function formatRange(range) {
|
|
1980
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
1981
|
+
}
|
|
1982
|
+
function validatePosition(position, label, context) {
|
|
1983
|
+
const { lines, changeIndex } = context;
|
|
1984
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
1985
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
1986
|
+
}
|
|
1987
|
+
if (position.line < 0 || position.character < 0) {
|
|
1988
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
1989
|
+
}
|
|
1990
|
+
const line = lines[position.line];
|
|
1991
|
+
if (line === undefined) {
|
|
1992
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
1993
|
+
}
|
|
1994
|
+
if (position.character > line.length) {
|
|
1995
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
function validateRange(range, lines, changeIndex) {
|
|
1999
|
+
const context = { lines, changeIndex };
|
|
2000
|
+
validatePosition(range.start, "start", context);
|
|
2001
|
+
validatePosition(range.end, "end", context);
|
|
2002
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
2003
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function sortAndDeduplicate(edits) {
|
|
2007
|
+
const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
|
|
2008
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
2009
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
2010
|
+
});
|
|
2011
|
+
const unique = [];
|
|
2012
|
+
for (const entry of sorted) {
|
|
2013
|
+
const previous = unique.at(-1);
|
|
2014
|
+
if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
unique.push(entry.edit);
|
|
2018
|
+
}
|
|
2019
|
+
return unique;
|
|
2020
|
+
}
|
|
2021
|
+
function validateNoOverlap(edits, changeIndex) {
|
|
2022
|
+
for (let index = 0;index < edits.length - 1; index += 1) {
|
|
2023
|
+
const later = edits[index];
|
|
2024
|
+
const earlier = edits[index + 1];
|
|
2025
|
+
if (later === undefined || earlier === undefined)
|
|
2026
|
+
continue;
|
|
2027
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
2028
|
+
throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
function applyNormalizedTextEdits(content, edits) {
|
|
2033
|
+
const lines = content.split(`
|
|
2034
|
+
`);
|
|
2035
|
+
for (const edit of edits) {
|
|
2036
|
+
const { start, end } = edit.range;
|
|
2037
|
+
const startLine = lines[start.line];
|
|
2038
|
+
const endLine = lines[end.line];
|
|
2039
|
+
if (startLine === undefined || endLine === undefined)
|
|
2040
|
+
continue;
|
|
2041
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
2042
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
|
|
2043
|
+
`));
|
|
2044
|
+
}
|
|
2045
|
+
return lines.join(`
|
|
2046
|
+
`);
|
|
2047
|
+
}
|
|
2048
|
+
function normalizeTextEdits(content, edits, changeIndex) {
|
|
2049
|
+
const lines = content.split(`
|
|
2050
|
+
`);
|
|
2051
|
+
for (const edit of edits) {
|
|
2052
|
+
validateRange(edit.range, lines, changeIndex);
|
|
2053
|
+
}
|
|
2054
|
+
const normalized = sortAndDeduplicate(edits);
|
|
2055
|
+
validateNoOverlap(normalized, changeIndex);
|
|
2056
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2060
|
+
function isSameOrDescendant2(candidate, parent) {
|
|
2061
|
+
const relativePath = relative3(parent, candidate);
|
|
2062
|
+
return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
|
|
2063
|
+
}
|
|
2064
|
+
function removeVirtualSubtree(virtual, path) {
|
|
2065
|
+
for (const candidate of [...virtual.keys()]) {
|
|
2066
|
+
if (isSameOrDescendant2(candidate, path))
|
|
2067
|
+
virtual.delete(candidate);
|
|
2068
|
+
}
|
|
2069
|
+
virtual.set(path, { kind: "missing" });
|
|
2070
|
+
}
|
|
2071
|
+
function moveVirtualSubtree(virtual, oldPath, newPath) {
|
|
2072
|
+
const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
|
|
2073
|
+
removeVirtualSubtree(virtual, oldPath);
|
|
2074
|
+
removeVirtualSubtree(virtual, newPath);
|
|
2075
|
+
for (const [candidate, entry] of moved) {
|
|
2076
|
+
const suffix = relative3(oldPath, candidate);
|
|
2077
|
+
virtual.set(suffix === "" ? newPath : resolve3(newPath, suffix), entry);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function virtualDirectoryHasChildren(virtual, path) {
|
|
2081
|
+
for (const [candidate, entry] of virtual) {
|
|
2082
|
+
if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
|
|
2083
|
+
return true;
|
|
2084
|
+
}
|
|
2085
|
+
return false;
|
|
2086
|
+
}
|
|
2087
|
+
function requireVirtualParent(virtual, path, changeIndex) {
|
|
2088
|
+
if (virtual.get(dirname2(path))?.kind !== "directory") {
|
|
2089
|
+
throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
function simulateOperations(parsed, snapshots) {
|
|
2093
|
+
const virtual = new Map(snapshots);
|
|
2094
|
+
const planned = [];
|
|
2095
|
+
const failures = [];
|
|
2096
|
+
for (const operation of parsed) {
|
|
2097
|
+
try {
|
|
2098
|
+
planned.push(simulateOperation(operation, virtual));
|
|
2099
|
+
} catch (error) {
|
|
2100
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2101
|
+
failures.push({ changeIndex: operation.changeIndex, message: error.detail });
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
throw error;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
return { operations: planned, failures };
|
|
2108
|
+
}
|
|
2109
|
+
function simulateOperation(operation, virtual) {
|
|
2110
|
+
switch (operation.kind) {
|
|
2111
|
+
case "text":
|
|
2112
|
+
return simulateText(operation, virtual);
|
|
2113
|
+
case "create":
|
|
2114
|
+
return simulateCreate(operation, virtual);
|
|
2115
|
+
case "rename":
|
|
2116
|
+
return simulateRename(operation, virtual);
|
|
2117
|
+
case "delete":
|
|
2118
|
+
return simulateDelete(operation, virtual);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
function rejectSymbolicLink(operation) {
|
|
2122
|
+
if (operation.followedSymbolicLink) {
|
|
2123
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
function simulateText(operation, virtual) {
|
|
2127
|
+
const entry = virtual.get(operation.path);
|
|
2128
|
+
if (entry?.kind !== "file")
|
|
2129
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
|
|
2130
|
+
const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
|
|
2131
|
+
virtual.set(operation.path, { kind: "file", content: normalized.text });
|
|
2132
|
+
return {
|
|
2133
|
+
kind: "text",
|
|
2134
|
+
changeIndex: operation.changeIndex,
|
|
2135
|
+
path: operation.path,
|
|
2136
|
+
beforeText: entry.content,
|
|
2137
|
+
afterText: normalized.text,
|
|
2138
|
+
editCount: normalized.edits.length,
|
|
2139
|
+
documentVersion: operation.version
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
function simulateCreate(operation, virtual) {
|
|
2143
|
+
rejectSymbolicLink(operation);
|
|
2144
|
+
requireVirtualParent(virtual, operation.path, operation.changeIndex);
|
|
2145
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2146
|
+
if (target.kind !== "missing") {
|
|
2147
|
+
if (operation.overwrite && target.kind === "file") {
|
|
2148
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2149
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
|
|
2150
|
+
}
|
|
2151
|
+
if (operation.ignoreIfExists)
|
|
2152
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2153
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
|
|
2154
|
+
}
|
|
2155
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2156
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
|
|
2157
|
+
}
|
|
2158
|
+
function simulateRename(operation, virtual) {
|
|
2159
|
+
rejectSymbolicLink(operation);
|
|
2160
|
+
const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
|
|
2161
|
+
if (source.kind === "missing") {
|
|
2162
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
|
|
2163
|
+
}
|
|
2164
|
+
if (operation.oldPath === operation.newPath)
|
|
2165
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2166
|
+
if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
|
|
2167
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
|
|
2168
|
+
}
|
|
2169
|
+
requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
|
|
2170
|
+
const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
|
|
2171
|
+
if (destination.kind !== "missing" && !operation.overwrite) {
|
|
2172
|
+
if (operation.ignoreIfExists)
|
|
2173
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2174
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
|
|
2175
|
+
}
|
|
2176
|
+
moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
|
|
2177
|
+
return {
|
|
2178
|
+
kind: "rename",
|
|
2179
|
+
changeIndex: operation.changeIndex,
|
|
2180
|
+
oldPath: operation.oldPath,
|
|
2181
|
+
newPath: operation.newPath,
|
|
2182
|
+
sourceKind: source.kind,
|
|
2183
|
+
replaceDestination: destination.kind !== "missing"
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function simulateDelete(operation, virtual) {
|
|
2187
|
+
rejectSymbolicLink(operation);
|
|
2188
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2189
|
+
if (target.kind === "missing") {
|
|
2190
|
+
if (operation.ignoreIfNotExists)
|
|
2191
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2192
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
|
|
2193
|
+
}
|
|
2194
|
+
if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
|
|
2195
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
|
|
2196
|
+
}
|
|
2197
|
+
removeVirtualSubtree(virtual, operation.path);
|
|
2198
|
+
return {
|
|
2199
|
+
kind: "delete",
|
|
2200
|
+
changeIndex: operation.changeIndex,
|
|
2201
|
+
path: operation.path,
|
|
2202
|
+
targetKind: target.kind,
|
|
2203
|
+
recursive: operation.recursive
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// ../lsp-core/src/lsp/workspace-edit-snapshot.ts
|
|
2208
|
+
import { existsSync as existsSync4, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2209
|
+
import { dirname as dirname3, resolve as resolve4 } from "node:path";
|
|
2210
|
+
class WorkspaceSnapshotBuilder {
|
|
2211
|
+
workspaceRoot;
|
|
2212
|
+
snapshots = new Map;
|
|
2213
|
+
constructor(workspaceRoot) {
|
|
2214
|
+
this.workspaceRoot = workspaceRoot;
|
|
2215
|
+
}
|
|
2216
|
+
build(operations) {
|
|
2217
|
+
this.add(this.workspaceRoot, false);
|
|
2218
|
+
for (const operation of operations) {
|
|
2219
|
+
switch (operation.kind) {
|
|
2220
|
+
case "rename":
|
|
2221
|
+
this.add(operation.oldPath, true);
|
|
2222
|
+
this.add(operation.newPath, true);
|
|
2223
|
+
break;
|
|
2224
|
+
case "delete":
|
|
2225
|
+
this.add(operation.path, true);
|
|
2226
|
+
break;
|
|
2227
|
+
case "text":
|
|
2228
|
+
case "create":
|
|
2229
|
+
this.add(operation.path, false);
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
return this.snapshots;
|
|
2234
|
+
}
|
|
2235
|
+
add(path, includeChildren) {
|
|
2236
|
+
let candidate = path;
|
|
2237
|
+
while (true) {
|
|
2238
|
+
const existing = this.snapshots.get(candidate);
|
|
2239
|
+
if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
|
|
2240
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
|
|
2241
|
+
}
|
|
2242
|
+
if (candidate === this.workspaceRoot)
|
|
2243
|
+
break;
|
|
2244
|
+
candidate = dirname3(candidate);
|
|
2245
|
+
}
|
|
2246
|
+
if (!includeChildren || !existsSync4(path) || !lstatSync3(path).isDirectory())
|
|
2247
|
+
return;
|
|
2248
|
+
for (const child of readdirSync2(path))
|
|
2249
|
+
this.add(resolve4(path, child), true);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
function snapshotOperations(operations, workspaceRoot) {
|
|
2253
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// ../lsp-core/src/lsp/workspace-edit-plan.ts
|
|
2257
|
+
class PlanPathIndex {
|
|
2258
|
+
firstChangeByPath = new Map;
|
|
2259
|
+
reportedPathByCanonical = new Map;
|
|
2260
|
+
build(operations) {
|
|
2261
|
+
for (const operation of operations) {
|
|
2262
|
+
switch (operation.kind) {
|
|
2263
|
+
case "rename":
|
|
2264
|
+
this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
|
|
2265
|
+
this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
|
|
2266
|
+
break;
|
|
2267
|
+
case "text":
|
|
2268
|
+
case "create":
|
|
2269
|
+
case "delete":
|
|
2270
|
+
this.add(operation.path, operation.reportedPath, operation.changeIndex);
|
|
2271
|
+
break;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
add(path, reportedPath2, changeIndex) {
|
|
2276
|
+
if (!this.firstChangeByPath.has(path))
|
|
2277
|
+
this.firstChangeByPath.set(path, changeIndex);
|
|
2278
|
+
if (!this.reportedPathByCanonical.has(path))
|
|
2279
|
+
this.reportedPathByCanonical.set(path, reportedPath2);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
function fingerprintWorkspaceEdit(edit, workspaceRoot) {
|
|
2283
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2284
|
+
if (!root.success)
|
|
2285
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2286
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2287
|
+
if (parsed.failures.length > 0)
|
|
2288
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2289
|
+
return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
|
|
2290
|
+
}
|
|
2291
|
+
function planWorkspaceEdit(edit, workspaceRoot) {
|
|
2292
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2293
|
+
if (!root.success)
|
|
2294
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2295
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2296
|
+
if (parsed.failures.length > 0)
|
|
2297
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2298
|
+
let snapshots;
|
|
2299
|
+
try {
|
|
2300
|
+
snapshots = snapshotOperations(parsed.operations, root.path);
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
return {
|
|
2303
|
+
success: false,
|
|
2304
|
+
result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
const simulated = simulateOperations(parsed.operations, snapshots);
|
|
2308
|
+
if (simulated.failures.length > 0)
|
|
2309
|
+
return { success: false, result: failureResult(simulated.failures) };
|
|
2310
|
+
const paths = new PlanPathIndex;
|
|
2311
|
+
paths.build(parsed.operations);
|
|
2312
|
+
const plan = {
|
|
2313
|
+
workspaceRoot: root.path,
|
|
2314
|
+
operations: simulated.operations,
|
|
2315
|
+
snapshots,
|
|
2316
|
+
firstChangeByPath: paths.firstChangeByPath,
|
|
2317
|
+
reportedPathByCanonical: paths.reportedPathByCanonical,
|
|
2318
|
+
fingerprint: canonicalFingerprint(parsed.operations)
|
|
2319
|
+
};
|
|
2320
|
+
return { success: true, plan };
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// ../lsp-core/src/lsp/workspace-mutation-controller.ts
|
|
2324
|
+
function failure(message, failedChange, base) {
|
|
2325
|
+
return {
|
|
2326
|
+
success: false,
|
|
2327
|
+
filesModified: base?.filesModified ?? [],
|
|
2328
|
+
totalEdits: base?.totalEdits ?? 0,
|
|
2329
|
+
errors: [message],
|
|
2330
|
+
...failedChange === undefined ? {} : { failedChange },
|
|
2331
|
+
...base?.lateAbort ? { lateAbort: true } : {}
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
function responseFor(result) {
|
|
2335
|
+
if (result.success)
|
|
2336
|
+
return { applied: true };
|
|
2337
|
+
return {
|
|
2338
|
+
applied: false,
|
|
2339
|
+
failureReason: result.errors[0] ?? "workspace edit failed",
|
|
2340
|
+
...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
function isRecord3(value) {
|
|
2344
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
class WorkspaceMutationController {
|
|
2348
|
+
workspaceRoot;
|
|
2349
|
+
documents;
|
|
2350
|
+
activeLease = null;
|
|
2351
|
+
nextLeaseId = 1;
|
|
2352
|
+
io;
|
|
2353
|
+
constructor(workspaceRoot, documents) {
|
|
2354
|
+
this.workspaceRoot = workspaceRoot;
|
|
2355
|
+
this.documents = documents;
|
|
2356
|
+
}
|
|
2357
|
+
setIo(io) {
|
|
2358
|
+
this.io = io;
|
|
2359
|
+
}
|
|
2360
|
+
acquire(signal) {
|
|
2361
|
+
if (this.activeLease)
|
|
2362
|
+
return { success: false, result: failure("workspace mutation is already in progress") };
|
|
2363
|
+
if (signal?.aborted)
|
|
2364
|
+
return { success: false, result: failure("cancelled before mutating request") };
|
|
2365
|
+
const lease = {
|
|
2366
|
+
id: this.nextLeaseId,
|
|
2367
|
+
phase: "idle",
|
|
2368
|
+
...signal === undefined ? {} : { signal }
|
|
2369
|
+
};
|
|
2370
|
+
this.nextLeaseId += 1;
|
|
2371
|
+
this.activeLease = lease;
|
|
2372
|
+
return { success: true, lease };
|
|
2373
|
+
}
|
|
2374
|
+
release(lease) {
|
|
2375
|
+
if (this.activeLease?.id !== lease.id)
|
|
2376
|
+
return;
|
|
2377
|
+
this.activeLease.phase = "sealed";
|
|
2378
|
+
this.activeLease = null;
|
|
2379
|
+
}
|
|
2380
|
+
isBeforeCommit(lease) {
|
|
2381
|
+
return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
|
|
2382
|
+
}
|
|
2383
|
+
async handleApplyEdit(params) {
|
|
2384
|
+
const lease = this.activeLease;
|
|
2385
|
+
if (!lease)
|
|
2386
|
+
return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
|
|
2387
|
+
if (lease.phase !== "idle") {
|
|
2388
|
+
return {
|
|
2389
|
+
applied: false,
|
|
2390
|
+
failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
lease.phase = "applying";
|
|
2394
|
+
lease.applyCompletion = new Promise((resolve5) => {
|
|
2395
|
+
lease.resolveApply = resolve5;
|
|
2396
|
+
});
|
|
2397
|
+
const edit = isRecord3(params) ? params["edit"] : undefined;
|
|
2398
|
+
const record = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
|
|
2399
|
+
lease.serverApply = record;
|
|
2400
|
+
lease.phase = "settled";
|
|
2401
|
+
lease.resolveApply?.();
|
|
2402
|
+
return responseFor(record.result);
|
|
2403
|
+
}
|
|
2404
|
+
async reconcileRename(leaseToken, edit) {
|
|
2405
|
+
const lease = this.requireActiveLease(leaseToken);
|
|
2406
|
+
if (!lease)
|
|
2407
|
+
return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
|
|
2408
|
+
if (lease.phase === "applying")
|
|
2409
|
+
await lease.applyCompletion;
|
|
2410
|
+
if (lease.serverApply)
|
|
2411
|
+
return this.reconcileServerApply(lease.serverApply, edit);
|
|
2412
|
+
lease.phase = "sealed";
|
|
2413
|
+
if (!edit)
|
|
2414
|
+
return { edit, apply: failure("No edit provided") };
|
|
2415
|
+
const applied = await this.applyEdit(edit, lease);
|
|
2416
|
+
return { edit, apply: applied.result };
|
|
2417
|
+
}
|
|
2418
|
+
reconcileServerApply(record, edit) {
|
|
2419
|
+
if (!edit)
|
|
2420
|
+
return { edit, apply: record.result };
|
|
2421
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
|
|
2422
|
+
if (fingerprint.success && record.fingerprint !== null && fingerprint.fingerprint === record.fingerprint) {
|
|
2423
|
+
return { edit, apply: record.result };
|
|
2424
|
+
}
|
|
2425
|
+
return {
|
|
2426
|
+
edit,
|
|
2427
|
+
apply: failure("rename result conflicts with server-applied workspace edit", 0, record.result)
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
async applyEdit(edit, lease) {
|
|
2431
|
+
const planned = planWorkspaceEdit(edit, this.workspaceRoot);
|
|
2432
|
+
if (!planned.success)
|
|
2433
|
+
return { fingerprint: null, result: planned.result };
|
|
2434
|
+
const versionFailure = this.documents.validateVersions(planned.plan.operations);
|
|
2435
|
+
if (versionFailure) {
|
|
2436
|
+
return {
|
|
2437
|
+
fingerprint: planned.plan.fingerprint,
|
|
2438
|
+
result: failure(versionFailure.message, versionFailure.changeIndex)
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
const commit = commitWorkspaceEditPlan(planned.plan, {
|
|
2442
|
+
...lease.signal === undefined ? {} : { signal: lease.signal },
|
|
2443
|
+
...this.io === undefined ? {} : { io: this.io }
|
|
2444
|
+
});
|
|
2445
|
+
let result = commit.result;
|
|
2446
|
+
if (commit.delta.operations.length > 0) {
|
|
2447
|
+
try {
|
|
2448
|
+
await this.documents.synchronize(commit.delta);
|
|
2449
|
+
} catch (error) {
|
|
2450
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2451
|
+
result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
if (lease.signal?.aborted && !result.lateAbort)
|
|
2455
|
+
result = { ...result, lateAbort: true };
|
|
2456
|
+
return { fingerprint: planned.plan.fingerprint, result };
|
|
2457
|
+
}
|
|
2458
|
+
requireActiveLease(lease) {
|
|
2459
|
+
return this.activeLease?.id === lease.id ? this.activeLease : null;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// ../lsp-core/src/lsp/client.ts
|
|
2464
|
+
var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
|
|
2465
|
+
var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
2466
|
+
|
|
2467
|
+
class LspClient extends LspClientConnection {
|
|
2468
|
+
diagnosticPullErrors = [];
|
|
2469
|
+
documents;
|
|
2470
|
+
workspaceMutations;
|
|
2471
|
+
diagnosticsFreshnessTimeoutMs;
|
|
2472
|
+
constructor(root, server, options = {}) {
|
|
2473
|
+
super(root, server, options);
|
|
2474
|
+
this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
|
|
2475
|
+
this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
|
|
2476
|
+
versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
|
|
2477
|
+
});
|
|
2478
|
+
this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
|
|
2479
|
+
this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
|
|
2480
|
+
}
|
|
2481
|
+
getDiagnosticPullErrors() {
|
|
2482
|
+
return this.diagnosticPullErrors;
|
|
2483
|
+
}
|
|
2484
|
+
async openFile(filePath) {
|
|
2485
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2486
|
+
await this.documents.openFile(absPath);
|
|
2487
|
+
}
|
|
2488
|
+
getOpenDocumentVersion(filePath) {
|
|
2489
|
+
return this.documents.getVersion(this.resolveWorkspacePath(filePath));
|
|
2490
|
+
}
|
|
2491
|
+
getStoredDiagnostics(uri) {
|
|
2492
|
+
return [...this.documents.getStoredDiagnostics(uri)];
|
|
2493
|
+
}
|
|
2494
|
+
setWorkspaceEditIo(io) {
|
|
2495
|
+
this.workspaceMutations.setIo(io);
|
|
2496
|
+
}
|
|
2497
|
+
handlePublishDiagnostics(params) {
|
|
2498
|
+
super.handlePublishDiagnostics(params);
|
|
2499
|
+
this.documents.recordPublishedDiagnostics(params);
|
|
2500
|
+
}
|
|
2501
|
+
async definition(filePath, line, character, signal) {
|
|
2502
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2503
|
+
await this.openFile(absPath);
|
|
2504
|
+
const options = signal === undefined ? {} : { signal };
|
|
2505
|
+
return this.sendRequest("textDocument/definition", {
|
|
2506
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
2507
|
+
position: { line: line - 1, character }
|
|
2508
|
+
}, options);
|
|
2509
|
+
}
|
|
2510
|
+
async references(filePath, line, character, includeDeclaration = true, signal) {
|
|
2511
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2512
|
+
await this.openFile(absPath);
|
|
2513
|
+
const options = signal === undefined ? {} : { signal };
|
|
2514
|
+
return this.sendRequest("textDocument/references", {
|
|
2515
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
2516
|
+
position: { line: line - 1, character },
|
|
2517
|
+
context: { includeDeclaration }
|
|
2518
|
+
}, options);
|
|
2519
|
+
}
|
|
2520
|
+
async documentSymbols(filePath, signal) {
|
|
2521
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2522
|
+
await this.openFile(absPath);
|
|
2523
|
+
const options = signal === undefined ? {} : { signal };
|
|
2524
|
+
return this.sendRequest("textDocument/documentSymbol", {
|
|
2525
|
+
textDocument: { uri: pathToFileURL3(absPath).href }
|
|
2526
|
+
}, options);
|
|
2527
|
+
}
|
|
2528
|
+
async workspaceSymbols(query, signal) {
|
|
2529
|
+
const options = signal === undefined ? {} : { signal };
|
|
2530
|
+
return this.sendRequest("workspace/symbol", { query }, options);
|
|
2531
|
+
}
|
|
2532
|
+
isUnsupportedDiagnosticPullError(error) {
|
|
2533
|
+
if (!(error instanceof Error))
|
|
2534
|
+
return false;
|
|
2535
|
+
const code = "code" in error && typeof error.code === "number" ? error.code : undefined;
|
|
2536
|
+
if (code === -32601)
|
|
2537
|
+
return true;
|
|
2538
|
+
return /unsupported|not supported|method not found|unknown request/i.test(error.message);
|
|
2539
|
+
}
|
|
2540
|
+
freshnessTimeout(absPath) {
|
|
2541
|
+
return {
|
|
2542
|
+
items: [],
|
|
2543
|
+
transientError: {
|
|
2544
|
+
kind: "freshness_timeout",
|
|
2545
|
+
message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
parseDiagnosticPullReport(value) {
|
|
2550
|
+
if (value.kind === "unchanged") {
|
|
2551
|
+
return {
|
|
2552
|
+
type: "unchanged",
|
|
2553
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
return {
|
|
2557
|
+
type: "full",
|
|
2558
|
+
diagnostics: value.items ?? [],
|
|
2559
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
async diagnostics(filePath, signal) {
|
|
2563
|
+
signal?.throwIfAborted();
|
|
2564
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2565
|
+
const uri = pathToFileURL3(absPath).href;
|
|
2566
|
+
await this.openFile(absPath);
|
|
2567
|
+
const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
|
|
2568
|
+
for (;; ) {
|
|
2569
|
+
signal?.throwIfAborted();
|
|
2570
|
+
const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
|
|
2571
|
+
if (!snapshot)
|
|
2572
|
+
return this.freshnessTimeout(absPath);
|
|
2573
|
+
const push = this.documents.resolvePushDiagnostics(snapshot);
|
|
2574
|
+
if (push.status === "ready")
|
|
2575
|
+
return { items: [...push.diagnostics] };
|
|
2576
|
+
let pushFallbackOnly = !this.isDiagnosticPullSupported();
|
|
2577
|
+
if (!pushFallbackOnly) {
|
|
2578
|
+
const cached = this.documents.getPullCache(snapshot);
|
|
2579
|
+
try {
|
|
2580
|
+
const remainingMs2 = deadlineAt - Date.now();
|
|
2581
|
+
if (remainingMs2 <= 0)
|
|
2582
|
+
return this.freshnessTimeout(absPath);
|
|
2583
|
+
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
2584
|
+
textDocument: { uri },
|
|
2585
|
+
...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
|
|
2586
|
+
}, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
|
|
2587
|
+
if (!this.documents.isCurrentSnapshot(snapshot))
|
|
2588
|
+
continue;
|
|
2589
|
+
const report = this.parseDiagnosticPullReport(result);
|
|
2590
|
+
if (report.type === "full") {
|
|
2591
|
+
this.documents.recordPullDiagnostics(snapshot, {
|
|
2592
|
+
kind: "full",
|
|
2593
|
+
diagnostics: report.diagnostics,
|
|
2594
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
2595
|
+
});
|
|
2596
|
+
return { items: [...report.diagnostics] };
|
|
2597
|
+
}
|
|
2598
|
+
if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
|
|
2599
|
+
return { items: [...cached.diagnostics] };
|
|
2600
|
+
}
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
if (this.isUnsupportedDiagnosticPullError(error)) {
|
|
2603
|
+
this.setDiagnosticPullSupported(false);
|
|
2604
|
+
pushFallbackOnly = true;
|
|
2605
|
+
} else if (error instanceof LspRequestTimeoutError) {
|
|
2606
|
+
pushFallbackOnly = true;
|
|
2607
|
+
} else {
|
|
2608
|
+
this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
2609
|
+
throw error;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
if (!pushFallbackOnly)
|
|
2614
|
+
continue;
|
|
2615
|
+
const remainingMs = deadlineAt - Date.now();
|
|
2616
|
+
if (remainingMs <= 0)
|
|
2617
|
+
return this.freshnessTimeout(absPath);
|
|
2618
|
+
const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
|
|
2619
|
+
await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
async prepareRename(filePath, line, character, signal) {
|
|
2623
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2624
|
+
await this.openFile(absPath);
|
|
2625
|
+
const options = signal === undefined ? {} : { signal };
|
|
2626
|
+
return this.sendRequest("textDocument/prepareRename", {
|
|
2627
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
2628
|
+
position: { line: line - 1, character }
|
|
2629
|
+
}, options);
|
|
2630
|
+
}
|
|
2631
|
+
async rename(filePath, line, character, newName, signal) {
|
|
2632
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2633
|
+
await this.openFile(absPath);
|
|
2634
|
+
const acquired = this.workspaceMutations.acquire(signal);
|
|
2635
|
+
if (!acquired.success)
|
|
2636
|
+
return { edit: null, apply: acquired.result };
|
|
2637
|
+
const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
|
|
2638
|
+
try {
|
|
2639
|
+
const renameParams = {
|
|
2640
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
2641
|
+
position: { line: line - 1, character },
|
|
2642
|
+
newName
|
|
2643
|
+
};
|
|
2644
|
+
const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
|
|
2645
|
+
signal: preCommitSignal.signal
|
|
2646
|
+
});
|
|
2647
|
+
return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
|
|
2648
|
+
} finally {
|
|
2649
|
+
preCommitSignal?.dispose();
|
|
2650
|
+
this.workspaceMutations.release(acquired.lease);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
resolveWorkspacePath(filePath) {
|
|
2654
|
+
return resolve5(this.root, filePath);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
function waitForDiagnosticsActivity(wait, signal) {
|
|
2658
|
+
if (!signal)
|
|
2659
|
+
return wait;
|
|
2660
|
+
if (signal.aborted)
|
|
2661
|
+
return Promise.reject(abortError2(signal));
|
|
2662
|
+
return new Promise((resolve6, reject) => {
|
|
2663
|
+
const onAbort = () => {
|
|
2664
|
+
signal.removeEventListener("abort", onAbort);
|
|
2665
|
+
reject(abortError2(signal));
|
|
2666
|
+
};
|
|
2667
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2668
|
+
wait.then(() => {
|
|
2669
|
+
signal.removeEventListener("abort", onAbort);
|
|
2670
|
+
resolve6();
|
|
2671
|
+
}, (error) => {
|
|
2672
|
+
signal.removeEventListener("abort", onAbort);
|
|
2673
|
+
reject(error);
|
|
2674
|
+
});
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
function createPreCommitAbortSignal(source, isBeforeCommit) {
|
|
2678
|
+
if (!source)
|
|
2679
|
+
return;
|
|
2680
|
+
const controller = new AbortController;
|
|
2681
|
+
const onAbort = () => {
|
|
2682
|
+
if (isBeforeCommit() && !controller.signal.aborted)
|
|
2683
|
+
controller.abort(preCommitAbortReason(source));
|
|
2684
|
+
};
|
|
2685
|
+
if (source.aborted)
|
|
2686
|
+
onAbort();
|
|
2687
|
+
else
|
|
2688
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
2689
|
+
return {
|
|
2690
|
+
signal: controller.signal,
|
|
2691
|
+
dispose: () => source.removeEventListener("abort", onAbort)
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
function preCommitAbortReason(source) {
|
|
2695
|
+
const reason = source.reason;
|
|
2696
|
+
if (reason instanceof Error && reason.name !== "AbortError")
|
|
2697
|
+
return reason;
|
|
2698
|
+
return new Error("LSP request cancelled before workspace edit commit");
|
|
2699
|
+
}
|
|
2700
|
+
function abortError2(signal) {
|
|
2701
|
+
const reason = signal.reason;
|
|
2702
|
+
if (reason instanceof Error)
|
|
2703
|
+
return reason;
|
|
2704
|
+
const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
|
|
2705
|
+
error.name = "AbortError";
|
|
2706
|
+
return error;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
// ../lsp-core/src/lsp/process-signal-cleanup.ts
|
|
2710
|
+
function installProcessSignalCleanup(cleanup) {
|
|
2711
|
+
const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM"];
|
|
2712
|
+
const handler = () => {
|
|
2713
|
+
cleanup().catch((error) => {
|
|
2714
|
+
reportBestEffortCleanupError("signal cleanup", error);
|
|
2715
|
+
});
|
|
2716
|
+
};
|
|
2717
|
+
for (const signal of signals) {
|
|
2718
|
+
process.on(signal, handler);
|
|
2719
|
+
}
|
|
2720
|
+
return () => {
|
|
2721
|
+
for (const signal of signals) {
|
|
2722
|
+
process.removeListener(signal, handler);
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// ../lsp-core/src/lsp/manager.ts
|
|
2728
|
+
async function stopClientBestEffort(client) {
|
|
2729
|
+
try {
|
|
2730
|
+
await client.stop();
|
|
2731
|
+
} catch (error) {
|
|
2732
|
+
reportBestEffortCleanupError("client stop", error);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
function awaitWithSignal(promise, signal) {
|
|
2736
|
+
if (!signal)
|
|
2737
|
+
return promise;
|
|
2738
|
+
return new Promise((resolve6, reject) => {
|
|
2739
|
+
let settled = false;
|
|
2740
|
+
const onAbort = () => {
|
|
2741
|
+
if (settled)
|
|
2742
|
+
return;
|
|
2743
|
+
settled = true;
|
|
2744
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
2745
|
+
};
|
|
2746
|
+
if (signal.aborted) {
|
|
2747
|
+
onAbort();
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2751
|
+
promise.then((value) => {
|
|
2752
|
+
if (settled)
|
|
2753
|
+
return;
|
|
2754
|
+
settled = true;
|
|
2755
|
+
signal.removeEventListener("abort", onAbort);
|
|
2756
|
+
resolve6(value);
|
|
2757
|
+
}, (err) => {
|
|
2758
|
+
if (settled)
|
|
2759
|
+
return;
|
|
2760
|
+
settled = true;
|
|
2761
|
+
signal.removeEventListener("abort", onAbort);
|
|
2762
|
+
reject(err);
|
|
2763
|
+
});
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
class LspManager {
|
|
2768
|
+
clients = new Map;
|
|
2769
|
+
reaperHandle = null;
|
|
2770
|
+
signalDisposer = null;
|
|
1178
2771
|
disposed = false;
|
|
1179
2772
|
idleTimeoutMs;
|
|
1180
2773
|
initTimeoutMs;
|
|
@@ -1412,8 +3005,179 @@ async function disposeDefaultLspManager() {
|
|
|
1412
3005
|
await m.stopAll();
|
|
1413
3006
|
}
|
|
1414
3007
|
}
|
|
1415
|
-
// src/daemon-client.ts
|
|
1416
|
-
import { connect
|
|
3008
|
+
// src/daemon-client.ts
|
|
3009
|
+
import { connect } from "node:net";
|
|
3010
|
+
import { homedir as homedir3 } from "node:os";
|
|
3011
|
+
import { join as join7 } from "node:path";
|
|
3012
|
+
|
|
3013
|
+
// ../lsp-core/src/request-context.ts
|
|
3014
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3015
|
+
import { existsSync as existsSync5, realpathSync as realpathSync3, statSync as statSync2 } from "node:fs";
|
|
3016
|
+
import { homedir } from "node:os";
|
|
3017
|
+
import { basename as basename2, delimiter as delimiter2, dirname as dirname4, isAbsolute as isAbsolute2, join as join2, relative as relative4, resolve as resolve6 } from "node:path";
|
|
3018
|
+
|
|
3019
|
+
class LspRequestContextParseError extends Error {
|
|
3020
|
+
code;
|
|
3021
|
+
name = "LspRequestContextParseError";
|
|
3022
|
+
constructor(code, message) {
|
|
3023
|
+
super(message);
|
|
3024
|
+
this.code = code;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
class LspRequestContextUnavailableError extends Error {
|
|
3029
|
+
name = "LspRequestContextUnavailableError";
|
|
3030
|
+
constructor() {
|
|
3031
|
+
super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
var storage = new AsyncLocalStorage;
|
|
3035
|
+
var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
3036
|
+
var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
|
|
3037
|
+
function runWithRequestContext(context, fn) {
|
|
3038
|
+
return storage.run(context, fn);
|
|
3039
|
+
}
|
|
3040
|
+
function lspRequestContext() {
|
|
3041
|
+
const context = storage.getStore();
|
|
3042
|
+
if (!context)
|
|
3043
|
+
throw new LspRequestContextUnavailableError;
|
|
3044
|
+
return context;
|
|
3045
|
+
}
|
|
3046
|
+
function contextCwd() {
|
|
3047
|
+
return lspRequestContext().cwd;
|
|
3048
|
+
}
|
|
3049
|
+
function createStandaloneMcpRequestContext(input = {}) {
|
|
3050
|
+
const env = input.env ?? process.env;
|
|
3051
|
+
const cwd = canonicalCwd(input.cwd ?? process.cwd());
|
|
3052
|
+
const home = input.homeDir ?? homedir();
|
|
3053
|
+
const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
|
|
3054
|
+
const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
|
|
3055
|
+
const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
|
|
3056
|
+
return parseLspRequestContext({
|
|
3057
|
+
cwd,
|
|
3058
|
+
projectConfigPaths,
|
|
3059
|
+
userConfigPath,
|
|
3060
|
+
installDecisionsPath,
|
|
3061
|
+
capabilities: { installDecisionTool: true }
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
function parseLspRequestContext(value) {
|
|
3065
|
+
if (!isRecord4(value)) {
|
|
3066
|
+
throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
|
|
3067
|
+
}
|
|
3068
|
+
rejectUnknownFields(value, CONTEXT_FIELDS, "context");
|
|
3069
|
+
const cwd = stringField(value, "cwd");
|
|
3070
|
+
const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
|
|
3071
|
+
const userConfigPath = stringField(value, "userConfigPath");
|
|
3072
|
+
const installDecisionsPath = stringField(value, "installDecisionsPath");
|
|
3073
|
+
const capabilities = capabilitiesField(value["capabilities"]);
|
|
3074
|
+
const canonical = canonicalCwd(cwd);
|
|
3075
|
+
for (const path of projectConfigPaths) {
|
|
3076
|
+
requireAbsolutePath(path, "projectConfigPaths");
|
|
3077
|
+
const projectPath = canonicalizeExistingOrNearestAncestor(path);
|
|
3078
|
+
if (!isPathInside(canonical, projectPath)) {
|
|
3079
|
+
throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
requireAbsolutePath(userConfigPath, "userConfigPath");
|
|
3083
|
+
requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
|
|
3084
|
+
return {
|
|
3085
|
+
cwd: canonical,
|
|
3086
|
+
projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
|
|
3087
|
+
userConfigPath,
|
|
3088
|
+
installDecisionsPath,
|
|
3089
|
+
capabilities
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
function translateProjectConfigEnv(value, cwd) {
|
|
3093
|
+
if (value === undefined || value.length === 0)
|
|
3094
|
+
return [join2(cwd, ".codex", "lsp-client.json")];
|
|
3095
|
+
return value.split(delimiter2).filter((entry) => entry.length > 0).map((entry) => isAbsolute2(entry) ? entry : join2(cwd, entry));
|
|
3096
|
+
}
|
|
3097
|
+
function translateHomeConfigEnv(value, home, fallback) {
|
|
3098
|
+
if (value === undefined || value.length === 0)
|
|
3099
|
+
return join2(home, fallback);
|
|
3100
|
+
return isAbsolute2(value) ? value : join2(home, value);
|
|
3101
|
+
}
|
|
3102
|
+
function canonicalCwd(cwd) {
|
|
3103
|
+
const resolved = resolve6(cwd);
|
|
3104
|
+
if (!existsSync5(resolved) || !statSync2(resolved).isDirectory()) {
|
|
3105
|
+
throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
3106
|
+
}
|
|
3107
|
+
return realpathSync3(resolved);
|
|
3108
|
+
}
|
|
3109
|
+
function canonicalizeExistingOrNearestAncestor(path) {
|
|
3110
|
+
let current = resolve6(path);
|
|
3111
|
+
const suffix = [];
|
|
3112
|
+
while (true) {
|
|
3113
|
+
try {
|
|
3114
|
+
const existing = realpathSync3(current);
|
|
3115
|
+
return suffix.length === 0 ? existing : join2(existing, ...suffix);
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
if (!isMissingPathError(error))
|
|
3118
|
+
throw error;
|
|
3119
|
+
const parent = dirname4(current);
|
|
3120
|
+
if (parent === current)
|
|
3121
|
+
throw error;
|
|
3122
|
+
suffix.unshift(basename2(current));
|
|
3123
|
+
current = parent;
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
function capabilitiesField(value) {
|
|
3128
|
+
if (!isRecord4(value)) {
|
|
3129
|
+
throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
3130
|
+
}
|
|
3131
|
+
rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
|
|
3132
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
3133
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
3134
|
+
throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
|
|
3135
|
+
}
|
|
3136
|
+
return { installDecisionTool };
|
|
3137
|
+
}
|
|
3138
|
+
function stringField(value, field) {
|
|
3139
|
+
const fieldValue = value[field];
|
|
3140
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
3141
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
3142
|
+
}
|
|
3143
|
+
return fieldValue;
|
|
3144
|
+
}
|
|
3145
|
+
function stringArrayField(value, field) {
|
|
3146
|
+
const fieldValue = value[field];
|
|
3147
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
3148
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
|
|
3149
|
+
}
|
|
3150
|
+
return fieldValue;
|
|
3151
|
+
}
|
|
3152
|
+
function requireAbsolutePath(path, field) {
|
|
3153
|
+
if (!isAbsolute2(path)) {
|
|
3154
|
+
throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
function isPathInside(parent, child) {
|
|
3158
|
+
const childPath = resolve6(child);
|
|
3159
|
+
const relativePath = relative4(parent, childPath);
|
|
3160
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
|
|
3161
|
+
}
|
|
3162
|
+
function isMissingPathError(error) {
|
|
3163
|
+
const code = errorCode(error);
|
|
3164
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
3165
|
+
}
|
|
3166
|
+
function rejectUnknownFields(value, allowed, scope) {
|
|
3167
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
3168
|
+
if (unknown.length > 0) {
|
|
3169
|
+
throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
function isRecord4(value) {
|
|
3173
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3174
|
+
}
|
|
3175
|
+
function errorCode(error) {
|
|
3176
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
3177
|
+
return;
|
|
3178
|
+
const code = Reflect.get(error, "code");
|
|
3179
|
+
return typeof code === "string" ? code : undefined;
|
|
3180
|
+
}
|
|
1417
3181
|
|
|
1418
3182
|
// ../mcp-stdio-core/src/record.ts
|
|
1419
3183
|
function isPlainRecord(value) {
|
|
@@ -1422,75 +3186,404 @@ function isPlainRecord(value) {
|
|
|
1422
3186
|
|
|
1423
3187
|
// src/ensure-daemon.ts
|
|
1424
3188
|
import { spawn as spawn2 } from "node:child_process";
|
|
1425
|
-
import { closeSync as closeSync2,
|
|
1426
|
-
import {
|
|
1427
|
-
import { dirname as
|
|
3189
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2 } from "node:fs";
|
|
3190
|
+
import { Socket } from "node:net";
|
|
3191
|
+
import { dirname as dirname6 } from "node:path";
|
|
1428
3192
|
import { execPath } from "node:process";
|
|
1429
|
-
import { fileURLToPath } from "node:url";
|
|
1430
3193
|
|
|
1431
|
-
// src/
|
|
1432
|
-
import {
|
|
1433
|
-
import {
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
3194
|
+
// src/ipc-protocol.ts
|
|
3195
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
3196
|
+
import {
|
|
3197
|
+
chmodSync,
|
|
3198
|
+
closeSync,
|
|
3199
|
+
constants,
|
|
3200
|
+
fchmodSync,
|
|
3201
|
+
fstatSync,
|
|
3202
|
+
lstatSync as lstatSync4,
|
|
3203
|
+
mkdirSync,
|
|
3204
|
+
openSync,
|
|
3205
|
+
readFileSync as readFileSync3,
|
|
3206
|
+
unlinkSync,
|
|
3207
|
+
writeSync
|
|
3208
|
+
} from "node:fs";
|
|
3209
|
+
import { dirname as dirname5 } from "node:path";
|
|
3210
|
+
var OMO_DAEMON_PROTOCOL_VERSION = 1;
|
|
3211
|
+
var AUTH_ERROR_CODE = -32001;
|
|
3212
|
+
var PROTOCOL_ERROR_CODE = -32002;
|
|
3213
|
+
var AUTH_TOKEN_BYTES = 32;
|
|
3214
|
+
|
|
3215
|
+
class UnsafePrivateDirectoryError extends Error {
|
|
3216
|
+
path;
|
|
3217
|
+
reason;
|
|
3218
|
+
name = "UnsafePrivateDirectoryError";
|
|
3219
|
+
code = "unsafe_private_directory";
|
|
3220
|
+
constructor(path, reason) {
|
|
3221
|
+
super(`unsafe private directory ${path}: ${reason}`);
|
|
3222
|
+
this.path = path;
|
|
3223
|
+
this.reason = reason;
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
function authEnvelope(token) {
|
|
3227
|
+
return { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, token };
|
|
3228
|
+
}
|
|
3229
|
+
function readAuthToken(paths) {
|
|
1437
3230
|
try {
|
|
1438
|
-
|
|
1439
|
-
return
|
|
3231
|
+
const token = readFileSync3(paths.auth, "utf8").trim();
|
|
3232
|
+
return token.length > 0 ? token : null;
|
|
1440
3233
|
} catch (error) {
|
|
1441
|
-
|
|
3234
|
+
if (error instanceof Error)
|
|
3235
|
+
return null;
|
|
3236
|
+
throw error;
|
|
1442
3237
|
}
|
|
1443
3238
|
}
|
|
1444
|
-
function
|
|
3239
|
+
function readOrCreateAuthToken(paths) {
|
|
3240
|
+
const existing = readAuthToken(paths);
|
|
3241
|
+
if (existing)
|
|
3242
|
+
return existing;
|
|
3243
|
+
return createAuthToken(paths);
|
|
3244
|
+
}
|
|
3245
|
+
function rotateAuthToken(paths) {
|
|
1445
3246
|
try {
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
3247
|
+
unlinkSync(paths.auth);
|
|
3248
|
+
} catch (error) {
|
|
3249
|
+
if (!(error instanceof Error))
|
|
3250
|
+
throw error;
|
|
1450
3251
|
}
|
|
3252
|
+
return createAuthToken(paths);
|
|
1451
3253
|
}
|
|
1452
|
-
function
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
3254
|
+
function authenticateMessage(raw, expectedToken) {
|
|
3255
|
+
const id = jsonRpcId(raw);
|
|
3256
|
+
if (!isPlainRecord(raw))
|
|
3257
|
+
return authError(id);
|
|
3258
|
+
const params = raw["params"];
|
|
3259
|
+
if (!isPlainRecord(params))
|
|
3260
|
+
return authError(id);
|
|
3261
|
+
const envelope = params["_omo"];
|
|
3262
|
+
if (!isPlainRecord(envelope))
|
|
3263
|
+
return authError(id);
|
|
3264
|
+
const protocolVersion = envelope["protocolVersion"];
|
|
3265
|
+
if (protocolVersion !== OMO_DAEMON_PROTOCOL_VERSION)
|
|
3266
|
+
return protocolError(id);
|
|
3267
|
+
const token = envelope["token"];
|
|
3268
|
+
if (typeof token !== "string" || !tokenMatches(token, expectedToken))
|
|
3269
|
+
return authError(id);
|
|
3270
|
+
const cleanParams = { ...params };
|
|
3271
|
+
delete cleanParams["_omo"];
|
|
3272
|
+
return { input: { ...raw, params: cleanParams }, id, method: typeof raw["method"] === "string" ? raw["method"] : undefined };
|
|
3273
|
+
}
|
|
3274
|
+
function isAuthErrorResponse(message) {
|
|
3275
|
+
if (!isPlainRecord(message))
|
|
3276
|
+
return false;
|
|
3277
|
+
const error = message["error"];
|
|
3278
|
+
if (!isPlainRecord(error))
|
|
3279
|
+
return false;
|
|
3280
|
+
const data = error["data"];
|
|
3281
|
+
return error["code"] === AUTH_ERROR_CODE && isPlainRecord(data) && data["code"] === "daemon_authentication_failed";
|
|
3282
|
+
}
|
|
3283
|
+
function writePrivateFile(path, data) {
|
|
3284
|
+
const fd = openSync(path, "w", 384);
|
|
3285
|
+
try {
|
|
3286
|
+
writeSync(fd, data);
|
|
3287
|
+
} finally {
|
|
3288
|
+
closeSync(fd);
|
|
1460
3289
|
}
|
|
1461
|
-
|
|
3290
|
+
setPrivateFileMode(path);
|
|
1462
3291
|
}
|
|
1463
|
-
function
|
|
3292
|
+
function ensurePrivateDirectory(path, options = {}) {
|
|
1464
3293
|
try {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
3294
|
+
mkdirSync(path, { recursive: true, mode: 448 });
|
|
3295
|
+
} catch (error) {
|
|
3296
|
+
if (errorCode2(error) !== "EEXIST")
|
|
3297
|
+
throw error;
|
|
3298
|
+
}
|
|
3299
|
+
if (process.platform === "win32")
|
|
3300
|
+
return;
|
|
3301
|
+
const before = validatePrivateDirectory(path, options);
|
|
3302
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
3303
|
+
try {
|
|
3304
|
+
fchmodSync(fd, 448);
|
|
3305
|
+
const after = validatePrivateDirectory(path, options);
|
|
3306
|
+
const openStats = fstatSync(fd);
|
|
3307
|
+
if (!sameDirectory(before, after) || !sameDirectory(before, openStats)) {
|
|
3308
|
+
throw new UnsafePrivateDirectoryError(path, "changed_during_chmod");
|
|
3309
|
+
}
|
|
3310
|
+
} finally {
|
|
1468
3311
|
closeSync(fd);
|
|
1469
|
-
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
function setPrivateFileMode(path) {
|
|
3315
|
+
if (process.platform !== "win32")
|
|
3316
|
+
chmodSync(path, 384);
|
|
3317
|
+
}
|
|
3318
|
+
function createAuthToken(paths) {
|
|
3319
|
+
ensurePrivateDirectory(dirname5(paths.auth));
|
|
3320
|
+
const token = randomBytes(AUTH_TOKEN_BYTES).toString("base64url");
|
|
3321
|
+
let fd;
|
|
3322
|
+
try {
|
|
3323
|
+
fd = openSync(paths.auth, "wx", 384);
|
|
1470
3324
|
} catch (error) {
|
|
1471
|
-
if (error
|
|
1472
|
-
|
|
3325
|
+
if (errorCode2(error) === "EEXIST") {
|
|
3326
|
+
const existing = readAuthToken(paths);
|
|
3327
|
+
if (existing)
|
|
3328
|
+
return existing;
|
|
3329
|
+
}
|
|
1473
3330
|
throw error;
|
|
1474
3331
|
}
|
|
3332
|
+
try {
|
|
3333
|
+
writeSync(fd, `${token}
|
|
3334
|
+
`);
|
|
3335
|
+
} finally {
|
|
3336
|
+
closeSync(fd);
|
|
3337
|
+
}
|
|
3338
|
+
setPrivateFileMode(paths.auth);
|
|
3339
|
+
return token;
|
|
1475
3340
|
}
|
|
1476
|
-
function
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
3341
|
+
function errorCode2(error) {
|
|
3342
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
3343
|
+
return;
|
|
3344
|
+
const code = Reflect.get(error, "code");
|
|
3345
|
+
return typeof code === "string" ? code : undefined;
|
|
3346
|
+
}
|
|
3347
|
+
function validatePrivateDirectory(path, options) {
|
|
3348
|
+
const stats = options.lstat ? options.lstat(path) : lstatPrivateDirectory(path);
|
|
3349
|
+
if (stats.isSymbolicLink())
|
|
3350
|
+
throw new UnsafePrivateDirectoryError(path, "symlink");
|
|
3351
|
+
if (!stats.isDirectory())
|
|
3352
|
+
throw new UnsafePrivateDirectoryError(path, "not_directory");
|
|
3353
|
+
const currentUid = options.currentUid ? options.currentUid() : process.getuid?.();
|
|
3354
|
+
if (currentUid !== undefined && stats.uid !== currentUid) {
|
|
3355
|
+
throw new UnsafePrivateDirectoryError(path, "wrong_owner");
|
|
3356
|
+
}
|
|
3357
|
+
return stats;
|
|
3358
|
+
}
|
|
3359
|
+
function lstatPrivateDirectory(path) {
|
|
3360
|
+
return lstatSync4(path);
|
|
3361
|
+
}
|
|
3362
|
+
function sameDirectory(a, b) {
|
|
3363
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
3364
|
+
}
|
|
3365
|
+
function tokenMatches(candidate, expected) {
|
|
3366
|
+
const candidateBytes = Buffer.from(candidate);
|
|
3367
|
+
const expectedBytes = Buffer.from(expected);
|
|
3368
|
+
return candidateBytes.length === expectedBytes.length && timingSafeEqual(candidateBytes, expectedBytes);
|
|
3369
|
+
}
|
|
3370
|
+
function jsonRpcId(raw) {
|
|
3371
|
+
if (!isPlainRecord(raw))
|
|
3372
|
+
return null;
|
|
3373
|
+
const id = raw["id"];
|
|
3374
|
+
return typeof id === "string" || typeof id === "number" || id === null ? id : null;
|
|
3375
|
+
}
|
|
3376
|
+
function authError(id) {
|
|
3377
|
+
return {
|
|
3378
|
+
jsonrpc: "2.0",
|
|
3379
|
+
id,
|
|
3380
|
+
error: { code: AUTH_ERROR_CODE, message: "daemon authentication failed", data: { code: "daemon_authentication_failed" } }
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
function protocolError(id) {
|
|
3384
|
+
return {
|
|
3385
|
+
jsonrpc: "2.0",
|
|
3386
|
+
id,
|
|
3387
|
+
error: { code: PROTOCOL_ERROR_CODE, message: "daemon protocol mismatch", data: { code: "daemon_protocol_mismatch" } }
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
// src/paths.ts
|
|
3392
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3393
|
+
import { createRequire } from "node:module";
|
|
3394
|
+
import { homedir as homedir2, tmpdir, userInfo } from "node:os";
|
|
3395
|
+
import * as path from "node:path";
|
|
3396
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3397
|
+
|
|
3398
|
+
// src/runtime-contract.ts
|
|
3399
|
+
import { statSync as statSync3 } from "node:fs";
|
|
3400
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
3401
|
+
var OMO_LSP_DAEMON_DIR = "OMO_LSP_DAEMON_DIR";
|
|
3402
|
+
var OMO_LSP_DAEMON_CLI = "OMO_LSP_DAEMON_CLI";
|
|
3403
|
+
var OMO_LSP_DAEMON_VERSION = "OMO_LSP_DAEMON_VERSION";
|
|
3404
|
+
var DAEMON_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
3405
|
+
|
|
3406
|
+
class InvalidRuntimeOverrideError extends Error {
|
|
3407
|
+
code = "invalid_runtime_override";
|
|
3408
|
+
reason;
|
|
3409
|
+
constructor(reason, message) {
|
|
3410
|
+
super(message);
|
|
3411
|
+
this.name = "InvalidRuntimeOverrideError";
|
|
3412
|
+
this.reason = reason;
|
|
3413
|
+
}
|
|
1482
3414
|
}
|
|
1483
|
-
|
|
3415
|
+
|
|
3416
|
+
class InvalidDaemonVersionError extends Error {
|
|
3417
|
+
code = "invalid_daemon_version";
|
|
3418
|
+
version;
|
|
3419
|
+
constructor(version) {
|
|
3420
|
+
super("LSP daemon version must match [A-Za-z0-9][A-Za-z0-9._+-]{0,127}");
|
|
3421
|
+
this.name = "InvalidDaemonVersionError";
|
|
3422
|
+
this.version = version;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
function validateDaemonVersion(version) {
|
|
3426
|
+
if (!DAEMON_VERSION_PATTERN.test(version))
|
|
3427
|
+
throw new InvalidDaemonVersionError(version);
|
|
3428
|
+
return version;
|
|
3429
|
+
}
|
|
3430
|
+
function resolveDaemonRuntime(env, defaults) {
|
|
3431
|
+
const cliOverride = env[OMO_LSP_DAEMON_CLI];
|
|
3432
|
+
const versionOverride = env[OMO_LSP_DAEMON_VERSION];
|
|
3433
|
+
const hasCliOverride = cliOverride !== undefined;
|
|
3434
|
+
const hasVersionOverride = versionOverride !== undefined;
|
|
3435
|
+
if (hasCliOverride !== hasVersionOverride) {
|
|
3436
|
+
throw new InvalidRuntimeOverrideError("paired_values_required", `${OMO_LSP_DAEMON_CLI} and ${OMO_LSP_DAEMON_VERSION} must be set together`);
|
|
3437
|
+
}
|
|
3438
|
+
if (!hasCliOverride || !hasVersionOverride) {
|
|
3439
|
+
if (!isAbsolute3(defaults.cliPath)) {
|
|
3440
|
+
throw new InvalidRuntimeOverrideError("packaged_cli_must_be_absolute", "Packaged LSP daemon CLI path must be absolute");
|
|
3441
|
+
}
|
|
3442
|
+
return { cliPath: defaults.cliPath, version: validateDaemonVersion(defaults.version) };
|
|
3443
|
+
}
|
|
3444
|
+
if (!isAbsolute3(cliOverride)) {
|
|
3445
|
+
throw new InvalidRuntimeOverrideError("cli_must_be_absolute", `${OMO_LSP_DAEMON_CLI} must be an absolute path to an existing regular file`);
|
|
3446
|
+
}
|
|
3447
|
+
let cliStats;
|
|
1484
3448
|
try {
|
|
1485
|
-
|
|
1486
|
-
} catch (error) {
|
|
3449
|
+
cliStats = statSync3(cliOverride);
|
|
3450
|
+
} catch (error) {
|
|
3451
|
+
if (!(error instanceof Error))
|
|
3452
|
+
throw error;
|
|
3453
|
+
throw new InvalidRuntimeOverrideError("cli_not_found", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
|
|
3454
|
+
}
|
|
3455
|
+
if (!cliStats.isFile()) {
|
|
3456
|
+
throw new InvalidRuntimeOverrideError("cli_not_file", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
|
|
3457
|
+
}
|
|
3458
|
+
return { cliPath: cliOverride, version: validateDaemonVersion(versionOverride) };
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// src/paths.ts
|
|
3462
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
3463
|
+
var MAX_SOCKET_PATH_LENGTH = 100;
|
|
3464
|
+
|
|
3465
|
+
class InvalidDaemonDirectoryError extends Error {
|
|
3466
|
+
code = "invalid_daemon_directory";
|
|
3467
|
+
directory;
|
|
3468
|
+
constructor(directory) {
|
|
3469
|
+
super(`${OMO_LSP_DAEMON_DIR} must be an absolute path`);
|
|
3470
|
+
this.name = "InvalidDaemonDirectoryError";
|
|
3471
|
+
this.directory = directory;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
function resolveDaemonVersion(requireFn = requireFromHere) {
|
|
3475
|
+
for (const candidate of ["./package.json", "../package.json"]) {
|
|
3476
|
+
let loaded;
|
|
3477
|
+
try {
|
|
3478
|
+
loaded = requireFn(candidate);
|
|
3479
|
+
} catch (error) {
|
|
3480
|
+
if (!(error instanceof Error))
|
|
3481
|
+
throw error;
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3484
|
+
if (typeof loaded === "object" && loaded !== null && "version" in loaded) {
|
|
3485
|
+
const version = Reflect.get(loaded, "version");
|
|
3486
|
+
if (typeof version === "string")
|
|
3487
|
+
return validateDaemonVersion(version);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
return "0";
|
|
3491
|
+
}
|
|
3492
|
+
function packagedRuntimeDefaults() {
|
|
3493
|
+
return {
|
|
3494
|
+
cliPath: fileURLToPath2(new URL("./cli.js", import.meta.url)),
|
|
3495
|
+
version: resolveDaemonVersion()
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
function daemonBaseDir(env = process.env, platform = defaultDaemonPlatform()) {
|
|
3499
|
+
const override = env[OMO_LSP_DAEMON_DIR];
|
|
3500
|
+
if (override !== undefined) {
|
|
3501
|
+
if (!platform.path.isAbsolute(override))
|
|
3502
|
+
throw new InvalidDaemonDirectoryError(override);
|
|
3503
|
+
return platform.path.resolve(override);
|
|
3504
|
+
}
|
|
3505
|
+
return platform.path.resolve(platform.path.join(platform.homedir(), ".omo", "lsp-daemon"));
|
|
3506
|
+
}
|
|
3507
|
+
function daemonPaths(env = process.env, runtimeDefaults = packagedRuntimeDefaults(), platform = defaultDaemonPlatform()) {
|
|
3508
|
+
const runtime = resolveDaemonRuntime(env, runtimeDefaults);
|
|
3509
|
+
const baseDir = daemonBaseDir(env, platform);
|
|
3510
|
+
const dir = platform.path.resolve(platform.path.join(baseDir, `v${runtime.version}`));
|
|
3511
|
+
return {
|
|
3512
|
+
version: runtime.version,
|
|
3513
|
+
cliPath: runtime.cliPath,
|
|
3514
|
+
dir,
|
|
3515
|
+
socket: resolveSocketPath(dir, runtime.version, platform),
|
|
3516
|
+
lock: platform.path.join(dir, "daemon.lock"),
|
|
3517
|
+
pid: platform.path.join(dir, "daemon.pid"),
|
|
3518
|
+
auth: platform.path.join(dir, "daemon.auth"),
|
|
3519
|
+
endpoint: platform.path.join(dir, "daemon.endpoint"),
|
|
3520
|
+
owner: platform.path.join(dir, "daemon.owner"),
|
|
3521
|
+
log: platform.path.join(dir, "daemon.log")
|
|
3522
|
+
};
|
|
3523
|
+
}
|
|
3524
|
+
function defaultDaemonPlatform() {
|
|
3525
|
+
return {
|
|
3526
|
+
platform: process.platform,
|
|
3527
|
+
homedir: homedir2,
|
|
3528
|
+
tmpdir,
|
|
3529
|
+
getuid: () => typeof process.getuid === "function" ? process.getuid() : undefined,
|
|
3530
|
+
username: () => userInfo().username,
|
|
3531
|
+
path
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
function resolveSocketPath(dir, version, platform) {
|
|
3535
|
+
const canonicalVersionDir = platform.path.resolve(dir);
|
|
3536
|
+
if (platform.platform === "win32") {
|
|
3537
|
+
const currentUserDiscriminator = `${platform.getuid() ?? "win"}:${platform.username()}:${platform.path.resolve(platform.homedir())}`;
|
|
3538
|
+
const digest = shortDigest(`${canonicalVersionDir}\x00${currentUserDiscriminator}`);
|
|
3539
|
+
return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
|
|
3540
|
+
}
|
|
3541
|
+
const natural = platform.path.join(canonicalVersionDir, "daemon.sock");
|
|
3542
|
+
if (natural.length < MAX_SOCKET_PATH_LENGTH)
|
|
3543
|
+
return natural;
|
|
3544
|
+
return platform.path.join(platform.tmpdir(), `omo-lsp-${version}-${shortDigest(canonicalVersionDir)}`, "daemon.sock");
|
|
3545
|
+
}
|
|
3546
|
+
function shortDigest(value) {
|
|
3547
|
+
return createHash2("sha256").update(value).digest("hex").slice(0, 16);
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// src/socket-jsonrpc.ts
|
|
3551
|
+
function encodeJsonLine(message) {
|
|
3552
|
+
return `${JSON.stringify(message)}
|
|
3553
|
+
`;
|
|
3554
|
+
}
|
|
3555
|
+
function createLineDecoder(onMessage, onParseError) {
|
|
3556
|
+
let buffer = "";
|
|
3557
|
+
return {
|
|
3558
|
+
push(chunk) {
|
|
3559
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
3560
|
+
let index = buffer.indexOf(`
|
|
3561
|
+
`);
|
|
3562
|
+
while (index !== -1) {
|
|
3563
|
+
const raw = buffer.slice(0, index).trim();
|
|
3564
|
+
buffer = buffer.slice(index + 1);
|
|
3565
|
+
if (raw.length > 0) {
|
|
3566
|
+
try {
|
|
3567
|
+
onMessage(JSON.parse(raw));
|
|
3568
|
+
} catch (error) {
|
|
3569
|
+
if (error instanceof Error) {
|
|
3570
|
+
onParseError?.(raw, error);
|
|
3571
|
+
} else {
|
|
3572
|
+
throw error;
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
index = buffer.indexOf(`
|
|
3577
|
+
`);
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
1487
3581
|
}
|
|
1488
3582
|
|
|
1489
3583
|
// src/ensure-daemon.ts
|
|
1490
3584
|
var PROBE_TIMEOUT_MS = 500;
|
|
1491
3585
|
var DEFAULT_READY_TIMEOUT_MS = 5000;
|
|
1492
3586
|
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
1493
|
-
var CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
|
|
1494
3587
|
|
|
1495
3588
|
class DaemonUnreachableError extends Error {
|
|
1496
3589
|
constructor(socketPath) {
|
|
@@ -1501,58 +3594,72 @@ class DaemonUnreachableError extends Error {
|
|
|
1501
3594
|
async function ensureDaemonRunning(paths, deps = defaultEnsureDaemonDeps(), options = {}) {
|
|
1502
3595
|
const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
1503
3596
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1504
|
-
|
|
3597
|
+
const signal = options.signal;
|
|
3598
|
+
throwIfAborted(signal);
|
|
3599
|
+
if (await awaitWithSignal2(deps.probe(paths, signal), signal))
|
|
1505
3600
|
return;
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
try {
|
|
1512
|
-
if (await deps.probe(paths.socket))
|
|
1513
|
-
return;
|
|
1514
|
-
deps.cleanupStaleSocket(paths.socket);
|
|
1515
|
-
deps.spawnDaemon(paths);
|
|
1516
|
-
await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
|
|
1517
|
-
} finally {
|
|
1518
|
-
lock.release();
|
|
1519
|
-
}
|
|
3601
|
+
throwIfAborted(signal);
|
|
3602
|
+
deps.spawnDaemon(paths);
|
|
3603
|
+
await waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs, signal);
|
|
1520
3604
|
}
|
|
1521
|
-
async function waitUntilReachable(
|
|
3605
|
+
async function waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs, signal) {
|
|
1522
3606
|
const deadline = deps.now() + readyTimeoutMs;
|
|
1523
3607
|
for (;; ) {
|
|
1524
|
-
|
|
3608
|
+
throwIfAborted(signal);
|
|
3609
|
+
if (await awaitWithSignal2(deps.probe(paths, signal), signal))
|
|
1525
3610
|
return;
|
|
1526
3611
|
if (deps.now() >= deadline)
|
|
1527
|
-
throw new DaemonUnreachableError(
|
|
1528
|
-
await deps.sleep(pollIntervalMs);
|
|
3612
|
+
throw new DaemonUnreachableError(paths.socket);
|
|
3613
|
+
await awaitWithSignal2(deps.sleep(pollIntervalMs, signal), signal);
|
|
1529
3614
|
}
|
|
1530
3615
|
}
|
|
1531
|
-
function
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
3616
|
+
async function probeDaemon(paths, timeoutMs = PROBE_TIMEOUT_MS, signal) {
|
|
3617
|
+
const token = readAuthToken(paths);
|
|
3618
|
+
if (!token)
|
|
3619
|
+
return false;
|
|
3620
|
+
return await pingDaemon(paths, token, timeoutMs, signal) !== null;
|
|
3621
|
+
}
|
|
3622
|
+
function pingDaemon(paths, token, timeoutMs = PROBE_TIMEOUT_MS, signal) {
|
|
3623
|
+
return new Promise((resolve7) => {
|
|
3624
|
+
const socket = new Socket;
|
|
3625
|
+
let settled = false;
|
|
3626
|
+
let timer;
|
|
3627
|
+
const finish = (value) => {
|
|
3628
|
+
if (settled)
|
|
3629
|
+
return;
|
|
3630
|
+
settled = true;
|
|
3631
|
+
if (timer !== undefined)
|
|
3632
|
+
clearTimeout(timer);
|
|
3633
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1535
3634
|
socket.destroy();
|
|
1536
|
-
|
|
3635
|
+
resolve7(value);
|
|
1537
3636
|
};
|
|
1538
|
-
const
|
|
1539
|
-
|
|
3637
|
+
const onAbort = () => finish(null);
|
|
3638
|
+
const decoder = createLineDecoder((message) => {
|
|
3639
|
+
finish(parsePingResponse(message));
|
|
3640
|
+
});
|
|
1540
3641
|
socket.once("connect", () => {
|
|
1541
|
-
|
|
1542
|
-
finish(true);
|
|
3642
|
+
socket.write(encodeJsonLine({ jsonrpc: "2.0", id: 1, method: "omo/ping", params: { _omo: authEnvelope(token) } }));
|
|
1543
3643
|
});
|
|
3644
|
+
socket.on("data", (chunk) => decoder.push(chunk));
|
|
1544
3645
|
socket.once("error", () => {
|
|
1545
|
-
|
|
1546
|
-
finish(false);
|
|
3646
|
+
finish(null);
|
|
1547
3647
|
});
|
|
3648
|
+
timer = setTimeout(() => finish(null), timeoutMs);
|
|
3649
|
+
timer.unref?.();
|
|
3650
|
+
if (signal?.aborted) {
|
|
3651
|
+
onAbort();
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3655
|
+
socket.connect(paths.socket);
|
|
1548
3656
|
});
|
|
1549
3657
|
}
|
|
1550
3658
|
function spawnDaemonProcess(paths) {
|
|
1551
|
-
mkdirSync2(
|
|
3659
|
+
mkdirSync2(dirname6(paths.log), { recursive: true });
|
|
1552
3660
|
const logFd = openSync2(paths.log, "a");
|
|
1553
3661
|
try {
|
|
1554
|
-
const
|
|
1555
|
-
const child = spawn2(execPath, [cliPath, "daemon"], {
|
|
3662
|
+
const child = spawn2(execPath, [paths.cliPath, "daemon"], {
|
|
1556
3663
|
detached: true,
|
|
1557
3664
|
stdio: ["ignore", logFd, logFd]
|
|
1558
3665
|
});
|
|
@@ -1561,81 +3668,92 @@ function spawnDaemonProcess(paths) {
|
|
|
1561
3668
|
closeSync2(logFd);
|
|
1562
3669
|
}
|
|
1563
3670
|
}
|
|
1564
|
-
function resolveDaemonCliPath(env = process.env) {
|
|
1565
|
-
const override = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
|
|
1566
|
-
if (override)
|
|
1567
|
-
return override;
|
|
1568
|
-
return fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
1569
|
-
}
|
|
1570
3671
|
function defaultEnsureDaemonDeps() {
|
|
1571
3672
|
return {
|
|
1572
|
-
probe: (
|
|
1573
|
-
acquireLock: (lockPath) => tryAcquireLock(lockPath),
|
|
1574
|
-
cleanupStaleSocket: (socketPath) => {
|
|
1575
|
-
if (existsSync2(socketPath))
|
|
1576
|
-
unlinkQuietly(socketPath);
|
|
1577
|
-
},
|
|
3673
|
+
probe: (paths, signal) => probeDaemon(paths, PROBE_TIMEOUT_MS, signal),
|
|
1578
3674
|
spawnDaemon: (paths) => spawnDaemonProcess(paths),
|
|
1579
|
-
sleep: (ms) =>
|
|
1580
|
-
setTimeout(resolve2, ms);
|
|
1581
|
-
}),
|
|
3675
|
+
sleep: (ms, signal) => sleepWithSignal(ms, signal),
|
|
1582
3676
|
now: () => Date.now()
|
|
1583
3677
|
};
|
|
1584
3678
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
|
|
3679
|
+
function sleepWithSignal(ms, signal) {
|
|
3680
|
+
return new Promise((resolve7) => {
|
|
3681
|
+
let settled = false;
|
|
3682
|
+
const finish = () => {
|
|
3683
|
+
if (settled)
|
|
3684
|
+
return;
|
|
3685
|
+
settled = true;
|
|
3686
|
+
clearTimeout(timer);
|
|
3687
|
+
signal?.removeEventListener("abort", finish);
|
|
3688
|
+
resolve7();
|
|
3689
|
+
};
|
|
3690
|
+
const timer = setTimeout(finish, ms);
|
|
3691
|
+
if (signal?.aborted) {
|
|
3692
|
+
finish();
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
3696
|
+
});
|
|
1603
3697
|
}
|
|
1604
|
-
function
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
pid: join2(dir, "daemon.pid"),
|
|
1623
|
-
log: join2(dir, "daemon.log")
|
|
1624
|
-
};
|
|
3698
|
+
function awaitWithSignal2(promise, signal) {
|
|
3699
|
+
if (!signal)
|
|
3700
|
+
return promise;
|
|
3701
|
+
if (signal.aborted)
|
|
3702
|
+
return Promise.reject(abortError3(signal));
|
|
3703
|
+
return new Promise((resolve7, reject) => {
|
|
3704
|
+
let settled = false;
|
|
3705
|
+
const finish = (run) => {
|
|
3706
|
+
if (settled)
|
|
3707
|
+
return;
|
|
3708
|
+
settled = true;
|
|
3709
|
+
signal.removeEventListener("abort", onAbort);
|
|
3710
|
+
run();
|
|
3711
|
+
};
|
|
3712
|
+
const onAbort = () => finish(() => reject(abortError3(signal)));
|
|
3713
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3714
|
+
promise.then((value) => finish(() => resolve7(value)), (error) => finish(() => reject(error)));
|
|
3715
|
+
});
|
|
1625
3716
|
}
|
|
1626
|
-
function
|
|
1627
|
-
|
|
1628
|
-
|
|
3717
|
+
function throwIfAborted(signal) {
|
|
3718
|
+
if (signal?.aborted)
|
|
3719
|
+
throw abortError3(signal);
|
|
1629
3720
|
}
|
|
1630
|
-
function
|
|
1631
|
-
const
|
|
1632
|
-
if (
|
|
1633
|
-
return
|
|
3721
|
+
function abortError3(signal) {
|
|
3722
|
+
const reason = signal.reason;
|
|
3723
|
+
if (reason instanceof Error)
|
|
3724
|
+
return reason;
|
|
3725
|
+
const error = new Error(typeof reason === "string" ? reason : "daemon startup cancelled");
|
|
3726
|
+
error.name = "AbortError";
|
|
3727
|
+
return error;
|
|
3728
|
+
}
|
|
3729
|
+
function parsePingResponse(message) {
|
|
3730
|
+
if (!message || typeof message !== "object" || Array.isArray(message))
|
|
3731
|
+
return null;
|
|
3732
|
+
const result = Reflect.get(message, "result");
|
|
3733
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
3734
|
+
return null;
|
|
3735
|
+
const pid = Reflect.get(result, "pid");
|
|
3736
|
+
const nonce = Reflect.get(result, "nonce");
|
|
3737
|
+
const startedAt = Reflect.get(result, "startedAt");
|
|
3738
|
+
const endpoint = Reflect.get(result, "endpoint");
|
|
3739
|
+
if (typeof pid !== "number" || typeof nonce !== "string" || typeof startedAt !== "string")
|
|
3740
|
+
return null;
|
|
3741
|
+
if (!endpoint || typeof endpoint !== "object" || Array.isArray(endpoint))
|
|
3742
|
+
return null;
|
|
3743
|
+
const path2 = Reflect.get(endpoint, "path");
|
|
3744
|
+
const kind = Reflect.get(endpoint, "kind");
|
|
3745
|
+
if (typeof path2 !== "string")
|
|
3746
|
+
return null;
|
|
3747
|
+
if (kind === "windows")
|
|
3748
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
|
|
3749
|
+
if (kind === "missing")
|
|
3750
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
|
|
3751
|
+
const dev = Reflect.get(endpoint, "dev");
|
|
3752
|
+
const ino = Reflect.get(endpoint, "ino");
|
|
3753
|
+
if (kind === "unix" && typeof dev === "number" && typeof ino === "number") {
|
|
3754
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2, dev, ino } };
|
|
1634
3755
|
}
|
|
1635
|
-
|
|
1636
|
-
if (natural.length < MAX_SOCKET_PATH_LENGTH)
|
|
1637
|
-
return natural;
|
|
1638
|
-
return join2(tmpdir(), `omo-lsp-${version}-${digest}.sock`);
|
|
3756
|
+
return null;
|
|
1639
3757
|
}
|
|
1640
3758
|
// ../mcp-stdio-core/src/responses.ts
|
|
1641
3759
|
function successResponse(id, result) {
|
|
@@ -1644,7 +3762,7 @@ function successResponse(id, result) {
|
|
|
1644
3762
|
function errorResponse(id, code, message, data) {
|
|
1645
3763
|
return { jsonrpc: "2.0", id, error: data === undefined ? { code, message } : { code, message, data } };
|
|
1646
3764
|
}
|
|
1647
|
-
function
|
|
3765
|
+
function jsonRpcId2(value) {
|
|
1648
3766
|
return typeof value === "string" || typeof value === "number" || value === null ? value : null;
|
|
1649
3767
|
}
|
|
1650
3768
|
function messageFromError(error) {
|
|
@@ -1672,16 +3790,45 @@ async function* readStdioJsonRpcMessages(input) {
|
|
|
1672
3790
|
yield parseJsonPayload(trailing, "line");
|
|
1673
3791
|
}
|
|
1674
3792
|
}
|
|
1675
|
-
function writeStdioJsonRpcResponse(output, response, responseMode) {
|
|
3793
|
+
async function writeStdioJsonRpcResponse(output, response, responseMode) {
|
|
1676
3794
|
const body = JSON.stringify(response);
|
|
1677
|
-
|
|
1678
|
-
output.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
3795
|
+
const payload = responseMode === "framed" ? `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
1679
3796
|
\r
|
|
1680
|
-
${body}`
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
3797
|
+
${body}` : `${body}
|
|
3798
|
+
`;
|
|
3799
|
+
await writeChunk(output, payload);
|
|
3800
|
+
}
|
|
3801
|
+
function writeChunk(output, chunk) {
|
|
3802
|
+
return new Promise((resolve7, reject) => {
|
|
3803
|
+
let settled = false;
|
|
3804
|
+
const onError = (error) => {
|
|
3805
|
+
if (settled)
|
|
3806
|
+
return;
|
|
3807
|
+
settled = true;
|
|
3808
|
+
reject(error);
|
|
3809
|
+
};
|
|
3810
|
+
output.once("error", onError);
|
|
3811
|
+
try {
|
|
3812
|
+
output.write(chunk, (error) => {
|
|
3813
|
+
if (settled)
|
|
3814
|
+
return;
|
|
3815
|
+
settled = true;
|
|
3816
|
+
if (error) {
|
|
3817
|
+
queueMicrotask(() => output.removeListener("error", onError));
|
|
3818
|
+
reject(error);
|
|
3819
|
+
return;
|
|
3820
|
+
}
|
|
3821
|
+
output.removeListener("error", onError);
|
|
3822
|
+
resolve7();
|
|
3823
|
+
});
|
|
3824
|
+
} catch (error) {
|
|
3825
|
+
output.removeListener("error", onError);
|
|
3826
|
+
if (settled)
|
|
3827
|
+
return;
|
|
3828
|
+
settled = true;
|
|
3829
|
+
reject(error);
|
|
3830
|
+
}
|
|
3831
|
+
});
|
|
1685
3832
|
}
|
|
1686
3833
|
function readNextMessage(buffer) {
|
|
1687
3834
|
if (buffer.length === 0)
|
|
@@ -1777,40 +3924,70 @@ async function runJsonRpcStdioServer(config) {
|
|
|
1777
3924
|
break;
|
|
1778
3925
|
idleTimer.arm();
|
|
1779
3926
|
if (message.kind === "parse_error") {
|
|
1780
|
-
handleParseError(message, config, log)
|
|
3927
|
+
if (!await handleParseError(message, config, log))
|
|
3928
|
+
break;
|
|
1781
3929
|
continue;
|
|
1782
3930
|
}
|
|
1783
|
-
await handleRequest(message, config, log)
|
|
3931
|
+
if (!await handleRequest(message, config, log))
|
|
3932
|
+
break;
|
|
1784
3933
|
}
|
|
1785
3934
|
} finally {
|
|
1786
3935
|
idleTimer.clear();
|
|
1787
3936
|
log("stdio_stopped");
|
|
1788
3937
|
}
|
|
1789
3938
|
}
|
|
1790
|
-
function handleParseError(message, config, log) {
|
|
3939
|
+
async function handleParseError(message, config, log) {
|
|
1791
3940
|
log("parse_error", { message: message.message });
|
|
1792
3941
|
const response = config.parseErrorResponse?.(message.message) ?? errorResponse(null, -32700, "Parse error", message.message);
|
|
1793
|
-
if (response
|
|
1794
|
-
|
|
1795
|
-
|
|
3942
|
+
if (response === undefined)
|
|
3943
|
+
return true;
|
|
3944
|
+
return writeResponse(response, {
|
|
3945
|
+
output: config.output,
|
|
3946
|
+
responseMode: message.responseMode,
|
|
3947
|
+
log
|
|
3948
|
+
});
|
|
1796
3949
|
}
|
|
1797
3950
|
async function handleRequest(message, config, log) {
|
|
1798
3951
|
const parsed = message.payload;
|
|
1799
|
-
const id = isPlainRecord(parsed) ?
|
|
3952
|
+
const id = isPlainRecord(parsed) ? jsonRpcId2(parsed["id"]) : null;
|
|
1800
3953
|
const method = isPlainRecord(parsed) && typeof parsed["method"] === "string" ? parsed["method"] : null;
|
|
1801
3954
|
log("request", { id: id === null ? null : String(id), method });
|
|
3955
|
+
let response;
|
|
1802
3956
|
try {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
3957
|
+
response = await config.handler(parsed, config.handlerOptions);
|
|
3958
|
+
} catch (error) {
|
|
3959
|
+
if (config.onHandlerError === undefined)
|
|
3960
|
+
throw error;
|
|
3961
|
+
config.onHandlerError(error);
|
|
3962
|
+
return true;
|
|
3963
|
+
}
|
|
3964
|
+
if (response === undefined)
|
|
3965
|
+
return true;
|
|
3966
|
+
if (!await writeResponse(response, {
|
|
3967
|
+
output: config.output,
|
|
3968
|
+
responseMode: message.responseMode,
|
|
3969
|
+
log
|
|
3970
|
+
}))
|
|
3971
|
+
return false;
|
|
3972
|
+
log("response", { id: String(response.id), method, is_error: response.error !== undefined });
|
|
3973
|
+
return true;
|
|
3974
|
+
}
|
|
3975
|
+
async function writeResponse(response, context) {
|
|
3976
|
+
try {
|
|
3977
|
+
await writeStdioJsonRpcResponse(context.output, response, context.responseMode);
|
|
3978
|
+
return true;
|
|
1808
3979
|
} catch (error) {
|
|
1809
|
-
if (
|
|
3980
|
+
if (!isTerminalOutputError(error))
|
|
1810
3981
|
throw error;
|
|
1811
|
-
|
|
3982
|
+
context.log("output_error", { message: messageFromError(error) });
|
|
3983
|
+
return false;
|
|
1812
3984
|
}
|
|
1813
3985
|
}
|
|
3986
|
+
function isTerminalOutputError(error) {
|
|
3987
|
+
if (!(error instanceof Error) || !("code" in error))
|
|
3988
|
+
return false;
|
|
3989
|
+
return error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED" || error.code === "ERR_STREAM_WRITE_AFTER_END";
|
|
3990
|
+
}
|
|
1814
3991
|
function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
|
|
1815
3992
|
let timer = null;
|
|
1816
3993
|
let isClosed = false;
|
|
@@ -1836,29 +4013,22 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
|
|
|
1836
4013
|
closed: () => isClosed
|
|
1837
4014
|
};
|
|
1838
4015
|
}
|
|
1839
|
-
// ../lsp-core/src/tools/diagnostics.ts
|
|
1840
|
-
import { resolve as resolve4 } from "node:path";
|
|
1841
|
-
|
|
1842
4016
|
// ../lsp-core/src/lsp/client-wrapper.ts
|
|
1843
|
-
import { existsSync as
|
|
1844
|
-
import { dirname as
|
|
4017
|
+
import { existsSync as existsSync9, statSync as statSync4 } from "node:fs";
|
|
4018
|
+
import { dirname as dirname8, join as join4, resolve as resolve7 } from "node:path";
|
|
1845
4019
|
|
|
1846
4020
|
// ../lsp-core/src/lsp/server-install-state.ts
|
|
1847
|
-
import { existsSync as
|
|
1848
|
-
import {
|
|
1849
|
-
import { dirname as dirname3, isAbsolute, join as join3 } from "node:path";
|
|
4021
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4022
|
+
import { dirname as dirname7 } from "node:path";
|
|
1850
4023
|
function getInstallDecisionsPath() {
|
|
1851
|
-
|
|
1852
|
-
if (!override)
|
|
1853
|
-
return join3(homedir2(), ".codex", "lsp-install-decisions.json");
|
|
1854
|
-
return isAbsolute(override) ? override : join3(homedir2(), override);
|
|
4024
|
+
return lspRequestContext().installDecisionsPath;
|
|
1855
4025
|
}
|
|
1856
4026
|
function loadInstallDecisions() {
|
|
1857
|
-
const
|
|
1858
|
-
if (!
|
|
4027
|
+
const path2 = getInstallDecisionsPath();
|
|
4028
|
+
if (!existsSync6(path2))
|
|
1859
4029
|
return {};
|
|
1860
4030
|
try {
|
|
1861
|
-
const parsed = JSON.parse(
|
|
4031
|
+
const parsed = JSON.parse(readFileSync4(path2, "utf8"));
|
|
1862
4032
|
return isInstallDecisions(parsed) ? parsed : {};
|
|
1863
4033
|
} catch {
|
|
1864
4034
|
return {};
|
|
@@ -1876,29 +4046,27 @@ function isInstallDecision(value) {
|
|
|
1876
4046
|
return value === "declined" || value === "allowed";
|
|
1877
4047
|
}
|
|
1878
4048
|
function writeInstallDecisions(decisions) {
|
|
1879
|
-
const
|
|
1880
|
-
mkdirSync3(
|
|
1881
|
-
const tmpPath = `${
|
|
1882
|
-
|
|
4049
|
+
const path2 = getInstallDecisionsPath();
|
|
4050
|
+
mkdirSync3(dirname7(path2), { recursive: true });
|
|
4051
|
+
const tmpPath = `${path2}.tmp`;
|
|
4052
|
+
writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
|
|
1883
4053
|
`, "utf8");
|
|
1884
|
-
|
|
4054
|
+
renameSync2(tmpPath, path2);
|
|
1885
4055
|
}
|
|
1886
4056
|
function isInstallDecisions(value) {
|
|
1887
|
-
return
|
|
4057
|
+
return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
|
|
1888
4058
|
}
|
|
1889
4059
|
function isInstallDecisionRecord(value) {
|
|
1890
|
-
if (!
|
|
4060
|
+
if (!isRecord5(value))
|
|
1891
4061
|
return false;
|
|
1892
4062
|
return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
|
|
1893
4063
|
}
|
|
1894
|
-
function
|
|
4064
|
+
function isRecord5(value) {
|
|
1895
4065
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1896
4066
|
}
|
|
1897
4067
|
|
|
1898
4068
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1899
|
-
import { existsSync as
|
|
1900
|
-
import { homedir as homedir3 } from "node:os";
|
|
1901
|
-
import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join4 } from "node:path";
|
|
4069
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
|
|
1902
4070
|
|
|
1903
4071
|
// ../lsp-core/src/lsp/server-definitions.ts
|
|
1904
4072
|
var LSP_INSTALL_HINTS = {
|
|
@@ -2048,27 +4216,17 @@ var BUILTIN_SERVERS = {
|
|
|
2048
4216
|
};
|
|
2049
4217
|
|
|
2050
4218
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
2051
|
-
function resolveProjectConfigPath(path) {
|
|
2052
|
-
return isAbsolute2(path) ? path : join4(contextCwd(), path);
|
|
2053
|
-
}
|
|
2054
4219
|
function getProjectConfigPaths() {
|
|
2055
|
-
|
|
2056
|
-
if (projectOverride) {
|
|
2057
|
-
return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
|
|
2058
|
-
}
|
|
2059
|
-
return [join4(contextCwd(), ".codex", "lsp-client.json")];
|
|
4220
|
+
return lspRequestContext().projectConfigPaths;
|
|
2060
4221
|
}
|
|
2061
4222
|
function getUserConfigPath() {
|
|
2062
|
-
|
|
2063
|
-
if (!userOverride)
|
|
2064
|
-
return join4(homedir3(), ".codex", "lsp-client.json");
|
|
2065
|
-
return isAbsolute2(userOverride) ? userOverride : join4(homedir3(), userOverride);
|
|
4223
|
+
return lspRequestContext().userConfigPath;
|
|
2066
4224
|
}
|
|
2067
|
-
function loadJsonFile(
|
|
2068
|
-
if (!
|
|
4225
|
+
function loadJsonFile(path2) {
|
|
4226
|
+
if (!existsSync7(path2))
|
|
2069
4227
|
return null;
|
|
2070
4228
|
try {
|
|
2071
|
-
const parsed = JSON.parse(
|
|
4229
|
+
const parsed = JSON.parse(readFileSync5(path2, "utf-8"));
|
|
2072
4230
|
return isConfigJson(parsed) ? parsed : null;
|
|
2073
4231
|
} catch {
|
|
2074
4232
|
return null;
|
|
@@ -2085,8 +4243,8 @@ function loadAllConfigs() {
|
|
|
2085
4243
|
return configs;
|
|
2086
4244
|
}
|
|
2087
4245
|
function loadFirstJsonFile(paths) {
|
|
2088
|
-
for (const
|
|
2089
|
-
const config = loadJsonFile(
|
|
4246
|
+
for (const path2 of paths) {
|
|
4247
|
+
const config = loadJsonFile(path2);
|
|
2090
4248
|
if (config)
|
|
2091
4249
|
return config;
|
|
2092
4250
|
}
|
|
@@ -2207,16 +4365,16 @@ function applyOptionalServerFields(server2, entry) {
|
|
|
2207
4365
|
}
|
|
2208
4366
|
}
|
|
2209
4367
|
function isConfigJson(value) {
|
|
2210
|
-
if (!
|
|
4368
|
+
if (!isRecord6(value))
|
|
2211
4369
|
return false;
|
|
2212
4370
|
const lsp = value["lsp"];
|
|
2213
|
-
return lsp === undefined ||
|
|
4371
|
+
return lsp === undefined || isRecord6(lsp);
|
|
2214
4372
|
}
|
|
2215
4373
|
function parseLspEntry(value) {
|
|
2216
4374
|
return isLspEntry(value) ? value : null;
|
|
2217
4375
|
}
|
|
2218
4376
|
function isLspEntry(value) {
|
|
2219
|
-
if (!
|
|
4377
|
+
if (!isRecord6(value))
|
|
2220
4378
|
return false;
|
|
2221
4379
|
const disabled = value["disabled"];
|
|
2222
4380
|
const command = value["command"];
|
|
@@ -2224,15 +4382,15 @@ function isLspEntry(value) {
|
|
|
2224
4382
|
const priority = value["priority"];
|
|
2225
4383
|
const env = value["env"];
|
|
2226
4384
|
const initialization = value["initialization"];
|
|
2227
|
-
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 ||
|
|
4385
|
+
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));
|
|
2228
4386
|
}
|
|
2229
4387
|
function isStringArray(value) {
|
|
2230
4388
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2231
4389
|
}
|
|
2232
4390
|
function isStringRecord(value) {
|
|
2233
|
-
return
|
|
4391
|
+
return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
|
|
2234
4392
|
}
|
|
2235
|
-
function
|
|
4393
|
+
function isRecord6(value) {
|
|
2236
4394
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2237
4395
|
}
|
|
2238
4396
|
function getDisabledServerIds() {
|
|
@@ -2253,8 +4411,8 @@ function getDisabledServerIds() {
|
|
|
2253
4411
|
}
|
|
2254
4412
|
|
|
2255
4413
|
// ../lsp-core/src/lsp/server-installation.ts
|
|
2256
|
-
import { existsSync as
|
|
2257
|
-
import { delimiter as delimiter3, join as
|
|
4414
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
4415
|
+
import { delimiter as delimiter3, join as join3 } from "node:path";
|
|
2258
4416
|
function isServerInstalled(command, _workingDirectory) {
|
|
2259
4417
|
if (command.length === 0)
|
|
2260
4418
|
return false;
|
|
@@ -2262,7 +4420,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2262
4420
|
if (!cmd)
|
|
2263
4421
|
return false;
|
|
2264
4422
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
2265
|
-
if (
|
|
4423
|
+
if (existsSync8(cmd))
|
|
2266
4424
|
return true;
|
|
2267
4425
|
}
|
|
2268
4426
|
const isWindows = process.platform === "win32";
|
|
@@ -2283,7 +4441,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2283
4441
|
const paths = pathEnv.split(delimiter3);
|
|
2284
4442
|
for (const p of paths) {
|
|
2285
4443
|
for (const suffix of exts) {
|
|
2286
|
-
if (
|
|
4444
|
+
if (existsSync8(join3(p, cmd + suffix))) {
|
|
2287
4445
|
return true;
|
|
2288
4446
|
}
|
|
2289
4447
|
}
|
|
@@ -2382,39 +4540,50 @@ function getAllServers() {
|
|
|
2382
4540
|
var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
2383
4541
|
function isDirectoryPath(filePath) {
|
|
2384
4542
|
try {
|
|
2385
|
-
return
|
|
4543
|
+
return statSync4(filePath).isDirectory();
|
|
2386
4544
|
} catch {
|
|
2387
4545
|
return false;
|
|
2388
4546
|
}
|
|
2389
4547
|
}
|
|
2390
4548
|
function findWorkspaceRoot(filePath) {
|
|
2391
|
-
const abs =
|
|
4549
|
+
const abs = resolvePathInsideContext(filePath);
|
|
2392
4550
|
let dir = abs;
|
|
2393
4551
|
if (!isDirectoryPath(dir)) {
|
|
2394
|
-
dir =
|
|
4552
|
+
dir = dirname8(dir);
|
|
2395
4553
|
}
|
|
2396
4554
|
let prevDir = "";
|
|
2397
4555
|
while (dir !== prevDir) {
|
|
2398
4556
|
for (const marker of WORKSPACE_MARKERS) {
|
|
2399
|
-
if (
|
|
4557
|
+
if (existsSync9(join4(dir, marker))) {
|
|
2400
4558
|
return dir;
|
|
2401
4559
|
}
|
|
2402
4560
|
}
|
|
2403
4561
|
prevDir = dir;
|
|
2404
|
-
dir =
|
|
4562
|
+
dir = dirname8(dir);
|
|
4563
|
+
}
|
|
4564
|
+
return dirname8(abs);
|
|
4565
|
+
}
|
|
4566
|
+
function resolvePathInsideContext(filePath) {
|
|
4567
|
+
const cwd = contextCwd();
|
|
4568
|
+
const abs = resolve7(cwd, filePath);
|
|
4569
|
+
const canonical = canonicalizeExistingOrNearestAncestor(abs);
|
|
4570
|
+
if (!isPathInside(cwd, canonical)) {
|
|
4571
|
+
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
2405
4572
|
}
|
|
2406
|
-
return
|
|
4573
|
+
return canonical;
|
|
2407
4574
|
}
|
|
2408
4575
|
function formatServerLookupError(result) {
|
|
2409
4576
|
if (result.status === "not_installed") {
|
|
2410
4577
|
return formatNotInstalled(result);
|
|
2411
4578
|
}
|
|
4579
|
+
const context = lspRequestContext();
|
|
4580
|
+
const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
|
|
2412
4581
|
return [
|
|
2413
4582
|
`No LSP server configured for extension: ${result.extension}`,
|
|
2414
4583
|
"",
|
|
2415
4584
|
`Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
|
|
2416
4585
|
"",
|
|
2417
|
-
|
|
4586
|
+
`Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
|
|
2418
4587
|
" {",
|
|
2419
4588
|
' "lsp": {',
|
|
2420
4589
|
' "my-server": {',
|
|
@@ -2430,6 +4599,7 @@ function formatNotInstalled(result) {
|
|
|
2430
4599
|
const { server: server2, installHint } = result;
|
|
2431
4600
|
const extensions = server2.extensions.join(", ");
|
|
2432
4601
|
const decision = loadInstallDecision(server2.id)?.decision;
|
|
4602
|
+
const context = lspRequestContext();
|
|
2433
4603
|
if (decision === "declined") {
|
|
2434
4604
|
return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
|
|
2435
4605
|
}
|
|
@@ -2445,6 +4615,17 @@ function formatNotInstalled(result) {
|
|
|
2445
4615
|
"The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
|
|
2446
4616
|
` ${installHint}`
|
|
2447
4617
|
].join(`
|
|
4618
|
+
`);
|
|
4619
|
+
}
|
|
4620
|
+
if (!context.capabilities.installDecisionTool) {
|
|
4621
|
+
return [
|
|
4622
|
+
...header,
|
|
4623
|
+
"To install, run:",
|
|
4624
|
+
` ${installHint}`,
|
|
4625
|
+
"",
|
|
4626
|
+
"ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
|
|
4627
|
+
"Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
|
|
4628
|
+
].join(`
|
|
2448
4629
|
`);
|
|
2449
4630
|
}
|
|
2450
4631
|
return [
|
|
@@ -2456,6 +4637,7 @@ function formatNotInstalled(result) {
|
|
|
2456
4637
|
"- If the user agrees: run the install command above, then retry this tool.",
|
|
2457
4638
|
"- If the user declines, OR has NOT explicitly asked for LSP installation:",
|
|
2458
4639
|
` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
|
|
4640
|
+
` which writes to ${context.installDecisionsPath},`,
|
|
2459
4641
|
" then ignore this message and proceed WITHOUT LSP."
|
|
2460
4642
|
].join(`
|
|
2461
4643
|
`);
|
|
@@ -2469,14 +4651,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
|
|
|
2469
4651
|
"prepareRename"
|
|
2470
4652
|
]);
|
|
2471
4653
|
async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
2472
|
-
const absPath =
|
|
4654
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2473
4655
|
if (isDirectoryPath(absPath)) {
|
|
2474
4656
|
throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
|
|
2475
4657
|
}
|
|
2476
4658
|
const ext = effectiveExtension(absPath);
|
|
2477
4659
|
const result = findServerForExtension(ext);
|
|
2478
4660
|
if (result.status !== "found") {
|
|
2479
|
-
throw new LspServerLookupError(formatServerLookupError(result));
|
|
4661
|
+
throw new LspServerLookupError(formatServerLookupError(result), result);
|
|
2480
4662
|
}
|
|
2481
4663
|
const server2 = result.server;
|
|
2482
4664
|
const root = findWorkspaceRoot(absPath);
|
|
@@ -2504,11 +4686,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
|
2504
4686
|
}
|
|
2505
4687
|
|
|
2506
4688
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2507
|
-
import { existsSync as
|
|
2508
|
-
import { join as
|
|
4689
|
+
import { existsSync as existsSync10, lstatSync as lstatSync5, readdirSync as readdirSync3 } from "node:fs";
|
|
4690
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
2509
4691
|
|
|
2510
4692
|
// ../lsp-core/src/lsp/formatters.ts
|
|
2511
|
-
import { fileURLToPath as
|
|
4693
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2512
4694
|
var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
2513
4695
|
error: 1,
|
|
2514
4696
|
warning: 2,
|
|
@@ -2516,7 +4698,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
|
2516
4698
|
hint: 4
|
|
2517
4699
|
};
|
|
2518
4700
|
function uriToPath(uri) {
|
|
2519
|
-
return
|
|
4701
|
+
return fileURLToPath3(uri);
|
|
2520
4702
|
}
|
|
2521
4703
|
function formatLocation(loc) {
|
|
2522
4704
|
if ("targetUri" in loc) {
|
|
@@ -2602,6 +4784,9 @@ function formatApplyResult(result) {
|
|
|
2602
4784
|
for (const file of result.filesModified) {
|
|
2603
4785
|
lines.push(` - ${file}`);
|
|
2604
4786
|
}
|
|
4787
|
+
if (result.lateAbort) {
|
|
4788
|
+
lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
|
|
4789
|
+
}
|
|
2605
4790
|
} else {
|
|
2606
4791
|
lines.push("Failed to apply some changes:");
|
|
2607
4792
|
for (const err of result.errors) {
|
|
@@ -2617,6 +4802,7 @@ function formatApplyResult(result) {
|
|
|
2617
4802
|
|
|
2618
4803
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2619
4804
|
var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
4805
|
+
var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
|
|
2620
4806
|
function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
2621
4807
|
const files = [];
|
|
2622
4808
|
function walk(currentDir) {
|
|
@@ -2624,17 +4810,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2624
4810
|
return;
|
|
2625
4811
|
let entries = [];
|
|
2626
4812
|
try {
|
|
2627
|
-
entries =
|
|
4813
|
+
entries = readdirSync3(currentDir);
|
|
2628
4814
|
} catch {
|
|
2629
4815
|
return;
|
|
2630
4816
|
}
|
|
2631
4817
|
for (const entry of entries) {
|
|
2632
4818
|
if (files.length >= maxFiles)
|
|
2633
4819
|
return;
|
|
2634
|
-
const fullPath =
|
|
4820
|
+
const fullPath = join5(currentDir, entry);
|
|
2635
4821
|
let stat;
|
|
2636
4822
|
try {
|
|
2637
|
-
stat =
|
|
4823
|
+
stat = lstatSync5(fullPath);
|
|
2638
4824
|
} catch {
|
|
2639
4825
|
continue;
|
|
2640
4826
|
}
|
|
@@ -2652,52 +4838,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2652
4838
|
walk(dir);
|
|
2653
4839
|
return files;
|
|
2654
4840
|
}
|
|
2655
|
-
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
|
|
4841
|
+
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
|
|
2656
4842
|
if (!extension.startsWith(".")) {
|
|
2657
4843
|
throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
|
|
2658
4844
|
}
|
|
2659
|
-
const absDir =
|
|
2660
|
-
if (!
|
|
4845
|
+
const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
|
|
4846
|
+
if (!existsSync10(absDir)) {
|
|
2661
4847
|
throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
|
|
2662
4848
|
}
|
|
2663
|
-
const serverResult = findServerForExtension(extension);
|
|
4849
|
+
const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
|
|
2664
4850
|
if (serverResult.status !== "found") {
|
|
2665
4851
|
throw new LspServerLookupError(formatServerLookupError(serverResult));
|
|
2666
4852
|
}
|
|
2667
4853
|
const server2 = serverResult.server;
|
|
2668
|
-
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
|
|
4854
|
+
const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
|
|
2669
4855
|
const wasCapped = allFiles.length > maxFiles;
|
|
2670
4856
|
const filesToProcess = allFiles.slice(0, maxFiles);
|
|
2671
4857
|
if (filesToProcess.length === 0) {
|
|
2672
|
-
|
|
4858
|
+
const output = [
|
|
2673
4859
|
`Directory: ${absDir}`,
|
|
2674
4860
|
`Extension: ${extension}`,
|
|
2675
4861
|
"Files scanned: 0",
|
|
2676
4862
|
`No files found with extension "${extension}".`
|
|
2677
4863
|
].join(`
|
|
2678
4864
|
`);
|
|
4865
|
+
return { output, totalDiagnostics: 0, fileFailures: [] };
|
|
2679
4866
|
}
|
|
2680
|
-
const root = findWorkspaceRoot(absDir);
|
|
2681
|
-
const manager = getLspManager();
|
|
4867
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
|
|
4868
|
+
const manager = options.manager ?? getLspManager();
|
|
2682
4869
|
const allDiagnostics = [];
|
|
2683
4870
|
const fileErrors = [];
|
|
2684
|
-
const
|
|
4871
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
|
|
4872
|
+
options.signal?.throwIfAborted();
|
|
4873
|
+
const client = await manager.getClient(root, server2, options.signal);
|
|
2685
4874
|
try {
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
file,
|
|
2697
|
-
|
|
2698
|
-
|
|
4875
|
+
let nextIndex = 0;
|
|
4876
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
|
|
4877
|
+
for (;; ) {
|
|
4878
|
+
if (options.signal?.aborted)
|
|
4879
|
+
return;
|
|
4880
|
+
const file = filesToProcess[nextIndex];
|
|
4881
|
+
nextIndex += 1;
|
|
4882
|
+
if (file === undefined)
|
|
4883
|
+
return;
|
|
4884
|
+
try {
|
|
4885
|
+
const result = await client.diagnostics(file, options.signal);
|
|
4886
|
+
const filtered = filterDiagnosticsBySeverity(result.items, severity);
|
|
4887
|
+
allDiagnostics.push(...filtered.map((diagnostic) => ({
|
|
4888
|
+
filePath: file,
|
|
4889
|
+
diagnostic
|
|
4890
|
+
})));
|
|
4891
|
+
} catch (e) {
|
|
4892
|
+
fileErrors.push({
|
|
4893
|
+
file,
|
|
4894
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4895
|
+
});
|
|
4896
|
+
}
|
|
2699
4897
|
}
|
|
2700
|
-
}
|
|
4898
|
+
});
|
|
4899
|
+
await Promise.all(workers);
|
|
2701
4900
|
} finally {
|
|
2702
4901
|
manager.releaseClient(root, server2.id);
|
|
2703
4902
|
}
|
|
@@ -2725,13 +4924,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
|
|
|
2725
4924
|
lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
2726
4925
|
}
|
|
2727
4926
|
}
|
|
2728
|
-
return lines.join(`
|
|
2729
|
-
`);
|
|
4927
|
+
return { output: lines.join(`
|
|
4928
|
+
`), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
|
|
2730
4929
|
}
|
|
2731
4930
|
|
|
2732
4931
|
// ../lsp-core/src/lsp/infer-extension.ts
|
|
2733
|
-
import { lstatSync as
|
|
2734
|
-
import { join as
|
|
4932
|
+
import { lstatSync as lstatSync6, readdirSync as readdirSync4 } from "node:fs";
|
|
4933
|
+
import { join as join6 } from "node:path";
|
|
2735
4934
|
var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
2736
4935
|
var MAX_SCAN_ENTRIES = 500;
|
|
2737
4936
|
function inferExtensionFromDirectory(directory) {
|
|
@@ -2742,17 +4941,17 @@ function inferExtensionFromDirectory(directory) {
|
|
|
2742
4941
|
return;
|
|
2743
4942
|
let entries;
|
|
2744
4943
|
try {
|
|
2745
|
-
entries =
|
|
4944
|
+
entries = readdirSync4(dir);
|
|
2746
4945
|
} catch {
|
|
2747
4946
|
return;
|
|
2748
4947
|
}
|
|
2749
4948
|
for (const entry of entries) {
|
|
2750
4949
|
if (scanned >= MAX_SCAN_ENTRIES)
|
|
2751
4950
|
return;
|
|
2752
|
-
const fullPath =
|
|
4951
|
+
const fullPath = join6(dir, entry);
|
|
2753
4952
|
let stat;
|
|
2754
4953
|
try {
|
|
2755
|
-
stat =
|
|
4954
|
+
stat = lstatSync6(fullPath);
|
|
2756
4955
|
} catch {
|
|
2757
4956
|
continue;
|
|
2758
4957
|
}
|
|
@@ -2826,13 +5025,48 @@ function missingDependencyResult(error, details) {
|
|
|
2826
5025
|
details: {
|
|
2827
5026
|
...details,
|
|
2828
5027
|
error: message,
|
|
2829
|
-
errorKind: "missing_dependency"
|
|
5028
|
+
errorKind: "missing_dependency",
|
|
5029
|
+
...availabilityDetails(error)
|
|
2830
5030
|
}
|
|
2831
5031
|
};
|
|
2832
5032
|
}
|
|
5033
|
+
function availabilityDetails(error) {
|
|
5034
|
+
const availability = missingDependencyAvailability(error);
|
|
5035
|
+
return availability === null ? {} : { availability };
|
|
5036
|
+
}
|
|
5037
|
+
function missingDependencyAvailability(error) {
|
|
5038
|
+
if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
|
|
5039
|
+
return null;
|
|
5040
|
+
const context = lspRequestContext();
|
|
5041
|
+
switch (error.lookup.status) {
|
|
5042
|
+
case "not_configured":
|
|
5043
|
+
return {
|
|
5044
|
+
kind: "not_configured",
|
|
5045
|
+
extension: error.lookup.extension,
|
|
5046
|
+
availableServers: [...error.lookup.availableServers],
|
|
5047
|
+
projectConfigPaths: [...context.projectConfigPaths],
|
|
5048
|
+
userConfigPath: context.userConfigPath,
|
|
5049
|
+
installDecisionTool: context.capabilities.installDecisionTool
|
|
5050
|
+
};
|
|
5051
|
+
case "not_installed":
|
|
5052
|
+
return {
|
|
5053
|
+
kind: "not_installed",
|
|
5054
|
+
serverId: error.lookup.server.id,
|
|
5055
|
+
command: [...error.lookup.server.command],
|
|
5056
|
+
extensions: [...error.lookup.server.extensions],
|
|
5057
|
+
installHint: error.lookup.installHint,
|
|
5058
|
+
installDecisionTool: context.capabilities.installDecisionTool,
|
|
5059
|
+
installDecisionsPath: context.installDecisionsPath
|
|
5060
|
+
};
|
|
5061
|
+
default: {
|
|
5062
|
+
const exhaustive = error.lookup;
|
|
5063
|
+
return exhaustive;
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
2833
5067
|
|
|
2834
5068
|
// ../lsp-core/src/tools/parameters.ts
|
|
2835
|
-
function
|
|
5069
|
+
function isRecord7(value) {
|
|
2836
5070
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2837
5071
|
}
|
|
2838
5072
|
function requireString(params, key) {
|
|
@@ -2889,7 +5123,7 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2889
5123
|
const filePath = requireString(params, "filePath");
|
|
2890
5124
|
const severity = severityFilter(params);
|
|
2891
5125
|
try {
|
|
2892
|
-
const absPath =
|
|
5126
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2893
5127
|
if (isDirectoryPath(absPath)) {
|
|
2894
5128
|
const extension = inferExtensionFromDirectory(absPath);
|
|
2895
5129
|
if (!extension) {
|
|
@@ -2906,18 +5140,33 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2906
5140
|
};
|
|
2907
5141
|
return text(message, details3);
|
|
2908
5142
|
}
|
|
2909
|
-
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
|
|
5143
|
+
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
|
|
2910
5144
|
const details2 = {
|
|
2911
5145
|
filePath,
|
|
2912
5146
|
severity,
|
|
2913
5147
|
mode: "directory",
|
|
2914
5148
|
diagnostics: [],
|
|
5149
|
+
totalDiagnostics: output2.totalDiagnostics,
|
|
5150
|
+
truncated: false,
|
|
5151
|
+
fileFailures: [...output2.fileFailures]
|
|
5152
|
+
};
|
|
5153
|
+
return text(output2.output, details2);
|
|
5154
|
+
}
|
|
5155
|
+
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
|
|
5156
|
+
if (result.transientError) {
|
|
5157
|
+
const message = result.transientError.message;
|
|
5158
|
+
const details2 = {
|
|
5159
|
+
filePath,
|
|
5160
|
+
severity,
|
|
5161
|
+
mode: "file",
|
|
5162
|
+
diagnostics: [],
|
|
2915
5163
|
totalDiagnostics: 0,
|
|
2916
|
-
truncated: false
|
|
5164
|
+
truncated: false,
|
|
5165
|
+
error: message,
|
|
5166
|
+
errorKind: result.transientError.kind
|
|
2917
5167
|
};
|
|
2918
|
-
return text(
|
|
5168
|
+
return text(message, details2, true);
|
|
2919
5169
|
}
|
|
2920
|
-
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
|
|
2921
5170
|
const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
|
|
2922
5171
|
const total = diagnostics.length;
|
|
2923
5172
|
const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
|
|
@@ -2978,7 +5227,7 @@ async function executeLspGotoDefinition(params, signal) {
|
|
|
2978
5227
|
const line = requireNumber(params, "line");
|
|
2979
5228
|
const character = requireNumber(params, "character");
|
|
2980
5229
|
try {
|
|
2981
|
-
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
|
|
5230
|
+
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
|
|
2982
5231
|
const locations = !result ? [] : Array.isArray(result) ? result : [result];
|
|
2983
5232
|
const details = { filePath, line, character, locations };
|
|
2984
5233
|
if (locations.length === 0)
|
|
@@ -3003,7 +5252,7 @@ async function executeLspFindReferences(params, signal) {
|
|
|
3003
5252
|
const character = requireNumber(params, "character");
|
|
3004
5253
|
const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
|
|
3005
5254
|
try {
|
|
3006
|
-
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
|
|
5255
|
+
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
|
|
3007
5256
|
const references = Array.isArray(result) ? result : [];
|
|
3008
5257
|
const total = references.length;
|
|
3009
5258
|
const truncated = total > DEFAULT_MAX_REFERENCES;
|
|
@@ -3039,181 +5288,13 @@ async function executeLspFindReferences(params, signal) {
|
|
|
3039
5288
|
}
|
|
3040
5289
|
}
|
|
3041
5290
|
|
|
3042
|
-
// ../lsp-core/src/lsp/workspace-edit.ts
|
|
3043
|
-
import { existsSync as existsSync8, readFileSync as readFileSync5, realpathSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3044
|
-
import { dirname as dirname5, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
|
|
3045
|
-
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3046
|
-
function errorMessage2(error) {
|
|
3047
|
-
return error instanceof Error ? error.message : String(error);
|
|
3048
|
-
}
|
|
3049
|
-
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
3050
|
-
const relativePath = relative(workspaceRoot, filePath);
|
|
3051
|
-
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
|
|
3052
|
-
}
|
|
3053
|
-
function realpathForValidation(filePath) {
|
|
3054
|
-
if (existsSync8(filePath))
|
|
3055
|
-
return realpathSync(filePath);
|
|
3056
|
-
const parent = dirname5(filePath);
|
|
3057
|
-
return resolve5(realpathSync(parent), relative(parent, filePath));
|
|
3058
|
-
}
|
|
3059
|
-
function uriToWorkspacePath(uri, workspaceRoot) {
|
|
3060
|
-
let filePath;
|
|
3061
|
-
try {
|
|
3062
|
-
filePath = fileURLToPath3(uri);
|
|
3063
|
-
} catch (error) {
|
|
3064
|
-
return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
|
|
3065
|
-
}
|
|
3066
|
-
let validatedPath;
|
|
3067
|
-
try {
|
|
3068
|
-
validatedPath = realpathForValidation(filePath);
|
|
3069
|
-
} catch (error) {
|
|
3070
|
-
return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
|
|
3071
|
-
}
|
|
3072
|
-
if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
|
|
3073
|
-
return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
|
|
3074
|
-
}
|
|
3075
|
-
return { success: true, path: filePath };
|
|
3076
|
-
}
|
|
3077
|
-
function applyTextEditsToFile(filePath, edits) {
|
|
3078
|
-
try {
|
|
3079
|
-
const content = readFileSync5(filePath, "utf-8");
|
|
3080
|
-
const lines = content.split(`
|
|
3081
|
-
`);
|
|
3082
|
-
const sortedEdits = [...edits].sort((a, b) => {
|
|
3083
|
-
if (b.range.start.line !== a.range.start.line) {
|
|
3084
|
-
return b.range.start.line - a.range.start.line;
|
|
3085
|
-
}
|
|
3086
|
-
return b.range.start.character - a.range.start.character;
|
|
3087
|
-
});
|
|
3088
|
-
for (const edit of sortedEdits) {
|
|
3089
|
-
const startLine = edit.range.start.line;
|
|
3090
|
-
const startChar = edit.range.start.character;
|
|
3091
|
-
const endLine = edit.range.end.line;
|
|
3092
|
-
const endChar = edit.range.end.character;
|
|
3093
|
-
if (startLine === endLine) {
|
|
3094
|
-
const line = lines[startLine] ?? "";
|
|
3095
|
-
lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
|
|
3096
|
-
} else {
|
|
3097
|
-
const firstLine = lines[startLine] ?? "";
|
|
3098
|
-
const lastLine = lines[endLine] ?? "";
|
|
3099
|
-
const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
|
|
3100
|
-
lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
|
|
3101
|
-
`));
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
|
-
writeFileSync2(filePath, lines.join(`
|
|
3105
|
-
`), "utf-8");
|
|
3106
|
-
return { success: true, editCount: edits.length };
|
|
3107
|
-
} catch (err) {
|
|
3108
|
-
return {
|
|
3109
|
-
success: false,
|
|
3110
|
-
editCount: 0,
|
|
3111
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3112
|
-
};
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
function applyWorkspaceEdit(edit, options = {}) {
|
|
3116
|
-
if (!edit) {
|
|
3117
|
-
return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
|
|
3118
|
-
}
|
|
3119
|
-
const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
|
|
3120
|
-
const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
|
|
3121
|
-
if (edit.changes) {
|
|
3122
|
-
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
3123
|
-
const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
|
|
3124
|
-
if (!validatedPath.success) {
|
|
3125
|
-
result.success = false;
|
|
3126
|
-
result.errors.push(validatedPath.error);
|
|
3127
|
-
continue;
|
|
3128
|
-
}
|
|
3129
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, edits);
|
|
3130
|
-
if (applyResult.success) {
|
|
3131
|
-
result.filesModified.push(validatedPath.path);
|
|
3132
|
-
result.totalEdits += applyResult.editCount;
|
|
3133
|
-
} else {
|
|
3134
|
-
result.success = false;
|
|
3135
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
3136
|
-
}
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
if (edit.documentChanges) {
|
|
3140
|
-
for (const change of edit.documentChanges) {
|
|
3141
|
-
if (!("kind" in change)) {
|
|
3142
|
-
const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
|
|
3143
|
-
if (!validatedPath.success) {
|
|
3144
|
-
result.success = false;
|
|
3145
|
-
result.errors.push(validatedPath.error);
|
|
3146
|
-
continue;
|
|
3147
|
-
}
|
|
3148
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
|
|
3149
|
-
if (applyResult.success) {
|
|
3150
|
-
result.filesModified.push(validatedPath.path);
|
|
3151
|
-
result.totalEdits += applyResult.editCount;
|
|
3152
|
-
} else {
|
|
3153
|
-
result.success = false;
|
|
3154
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
3155
|
-
}
|
|
3156
|
-
continue;
|
|
3157
|
-
}
|
|
3158
|
-
if (change.kind === "create") {
|
|
3159
|
-
try {
|
|
3160
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
3161
|
-
if (!validatedPath.success) {
|
|
3162
|
-
result.success = false;
|
|
3163
|
-
result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
|
|
3164
|
-
continue;
|
|
3165
|
-
}
|
|
3166
|
-
writeFileSync2(validatedPath.path, "", "utf-8");
|
|
3167
|
-
result.filesModified.push(validatedPath.path);
|
|
3168
|
-
} catch (err) {
|
|
3169
|
-
result.success = false;
|
|
3170
|
-
result.errors.push(`Create ${change.uri}: ${String(err)}`);
|
|
3171
|
-
}
|
|
3172
|
-
} else if (change.kind === "rename") {
|
|
3173
|
-
try {
|
|
3174
|
-
const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
|
|
3175
|
-
const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
|
|
3176
|
-
if (!oldPath.success || !newPath.success) {
|
|
3177
|
-
const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
|
|
3178
|
-
result.success = false;
|
|
3179
|
-
result.errors.push(`Rename ${change.oldUri}: ${error}`);
|
|
3180
|
-
continue;
|
|
3181
|
-
}
|
|
3182
|
-
const content = readFileSync5(oldPath.path, "utf-8");
|
|
3183
|
-
writeFileSync2(newPath.path, content, "utf-8");
|
|
3184
|
-
unlinkSync2(oldPath.path);
|
|
3185
|
-
result.filesModified.push(newPath.path);
|
|
3186
|
-
} catch (err) {
|
|
3187
|
-
result.success = false;
|
|
3188
|
-
result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
|
|
3189
|
-
}
|
|
3190
|
-
} else if (change.kind === "delete") {
|
|
3191
|
-
try {
|
|
3192
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
3193
|
-
if (!validatedPath.success) {
|
|
3194
|
-
result.success = false;
|
|
3195
|
-
result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
|
|
3196
|
-
continue;
|
|
3197
|
-
}
|
|
3198
|
-
unlinkSync2(validatedPath.path);
|
|
3199
|
-
result.filesModified.push(validatedPath.path);
|
|
3200
|
-
} catch (err) {
|
|
3201
|
-
result.success = false;
|
|
3202
|
-
result.errors.push(`Delete ${change.uri}: ${String(err)}`);
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
}
|
|
3207
|
-
return result;
|
|
3208
|
-
}
|
|
3209
|
-
|
|
3210
5291
|
// ../lsp-core/src/tools/rename.ts
|
|
3211
5292
|
async function executeLspPrepareRename(params, signal) {
|
|
3212
5293
|
const filePath = requireString(params, "filePath");
|
|
3213
5294
|
const line = requireNumber(params, "line");
|
|
3214
5295
|
const character = requireNumber(params, "character");
|
|
3215
5296
|
try {
|
|
3216
|
-
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
|
|
5297
|
+
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
|
|
3217
5298
|
const details = { filePath, line, character, result };
|
|
3218
5299
|
return text(formatPrepareRenameResult(result), details);
|
|
3219
5300
|
} catch (error) {
|
|
@@ -3234,13 +5315,9 @@ async function executeLspRename(params, signal) {
|
|
|
3234
5315
|
const character = requireNumber(params, "character");
|
|
3235
5316
|
const newName = requireString(params, "newName");
|
|
3236
5317
|
try {
|
|
3237
|
-
const
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
}), "rename", clientOptions(signal));
|
|
3241
|
-
const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
|
|
3242
|
-
const details = { filePath, line, character, newName, apply, edit: edit.edit };
|
|
3243
|
-
return text(formatApplyResult(apply), details, !apply.success);
|
|
5318
|
+
const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
|
|
5319
|
+
const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
|
|
5320
|
+
return text(formatApplyResult(result.apply), details, !result.apply.success);
|
|
3244
5321
|
} catch (error) {
|
|
3245
5322
|
const missingDependency = missingDependencyResult(error, {
|
|
3246
5323
|
filePath,
|
|
@@ -3315,10 +5392,10 @@ async function executeLspSymbols(params, signal) {
|
|
|
3315
5392
|
errorKind: "missing_query"
|
|
3316
5393
|
});
|
|
3317
5394
|
}
|
|
3318
|
-
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
|
|
5395
|
+
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
|
|
3319
5396
|
return formatSymbolsResult(filePath, scope, symbols2, limit, query);
|
|
3320
5397
|
}
|
|
3321
|
-
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
|
|
5398
|
+
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
|
|
3322
5399
|
return formatSymbolsResult(filePath, scope, symbols, limit);
|
|
3323
5400
|
} catch (error) {
|
|
3324
5401
|
const query = optionalString(params, "query");
|
|
@@ -3484,16 +5561,16 @@ function matchesToolName(tool, name) {
|
|
|
3484
5561
|
return tool.name === name || (tool.aliases?.includes(name) ?? false);
|
|
3485
5562
|
}
|
|
3486
5563
|
function coerceToolArguments(value) {
|
|
3487
|
-
return
|
|
5564
|
+
return isRecord7(value) ? value : {};
|
|
3488
5565
|
}
|
|
3489
5566
|
// ../lsp-core/src/mcp.ts
|
|
3490
5567
|
var SERVER_NAME = "lsp";
|
|
3491
5568
|
var SERVER_VERSION = "0.1.0";
|
|
3492
|
-
async function handleLspMcpRequest(input) {
|
|
5569
|
+
async function handleLspMcpRequest(input, options = {}) {
|
|
3493
5570
|
if (!isPlainRecord(input)) {
|
|
3494
5571
|
return errorResponse(null, -32600, "Invalid Request");
|
|
3495
5572
|
}
|
|
3496
|
-
const id =
|
|
5573
|
+
const id = jsonRpcId2(input["id"]);
|
|
3497
5574
|
const method = input["method"];
|
|
3498
5575
|
if (method === "notifications/initialized")
|
|
3499
5576
|
return;
|
|
@@ -3511,24 +5588,27 @@ async function handleLspMcpRequest(input) {
|
|
|
3511
5588
|
return successResponse(id, { tools: LSP_MCP_TOOLS.map(describeTool) });
|
|
3512
5589
|
}
|
|
3513
5590
|
if (method === "tools/call") {
|
|
3514
|
-
return handleToolCall(id, input["params"]);
|
|
5591
|
+
return handleToolCall(id, input["params"], options.signal);
|
|
3515
5592
|
}
|
|
3516
5593
|
return errorResponse(id, -32601, `Method not found: ${String(method)}`);
|
|
3517
5594
|
}
|
|
3518
|
-
async function handleToolCall(id, params) {
|
|
5595
|
+
async function handleToolCall(id, params, signal) {
|
|
3519
5596
|
if (!isPlainRecord(params) || typeof params["name"] !== "string") {
|
|
3520
5597
|
return errorResponse(id, -32602, "tools/call requires params.name");
|
|
3521
5598
|
}
|
|
3522
5599
|
try {
|
|
3523
|
-
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
|
|
5600
|
+
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
|
|
3524
5601
|
return successResponse(id, {
|
|
3525
5602
|
content: result.content,
|
|
3526
5603
|
isError: result.isError ?? false,
|
|
3527
5604
|
details: result.details
|
|
3528
5605
|
});
|
|
3529
5606
|
} catch (error) {
|
|
5607
|
+
if (!(error instanceof Error)) {
|
|
5608
|
+
throw error;
|
|
5609
|
+
}
|
|
3530
5610
|
return successResponse(id, {
|
|
3531
|
-
content: [{ type: "text", text:
|
|
5611
|
+
content: [{ type: "text", text: error.message }],
|
|
3532
5612
|
isError: true
|
|
3533
5613
|
});
|
|
3534
5614
|
}
|
|
@@ -3549,77 +5629,91 @@ function requestedProtocolVersion(params) {
|
|
|
3549
5629
|
|
|
3550
5630
|
// src/request-routing.ts
|
|
3551
5631
|
var CONTEXT_KEY = "_context";
|
|
5632
|
+
|
|
5633
|
+
class InvalidDaemonRequestError extends Error {
|
|
5634
|
+
name = "InvalidDaemonRequestError";
|
|
5635
|
+
}
|
|
3552
5636
|
function extractRequestContext(raw) {
|
|
3553
5637
|
if (!isPlainRecord(raw) || raw["method"] !== "tools/call")
|
|
3554
5638
|
return { input: raw, context: undefined };
|
|
3555
5639
|
const params = raw["params"];
|
|
3556
5640
|
if (!isPlainRecord(params))
|
|
3557
|
-
|
|
5641
|
+
throw new InvalidDaemonRequestError("Daemon tools/call params must be an object.");
|
|
3558
5642
|
const args = params["arguments"];
|
|
3559
5643
|
if (!isPlainRecord(args))
|
|
3560
|
-
|
|
5644
|
+
throw new InvalidDaemonRequestError("Daemon tools/call arguments must be an object.");
|
|
5645
|
+
if (!Object.hasOwn(args, CONTEXT_KEY)) {
|
|
5646
|
+
throw new InvalidDaemonRequestError("Daemon tools/call arguments must include _context.");
|
|
5647
|
+
}
|
|
3561
5648
|
const context = parseContext(args[CONTEXT_KEY]);
|
|
3562
|
-
if (!context)
|
|
3563
|
-
return { input: raw, context: undefined };
|
|
3564
5649
|
const cleanedArgs = { ...args };
|
|
3565
5650
|
delete cleanedArgs[CONTEXT_KEY];
|
|
3566
5651
|
const cleaned = { ...raw, params: { ...params, arguments: cleanedArgs } };
|
|
3567
5652
|
return { input: cleaned, context };
|
|
3568
5653
|
}
|
|
3569
|
-
function handleDaemonMessage(raw) {
|
|
3570
|
-
const
|
|
3571
|
-
if (
|
|
3572
|
-
return
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
if (
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
5654
|
+
function handleDaemonMessage(raw, state) {
|
|
5655
|
+
const authenticated = authenticateMessage(raw, state.token);
|
|
5656
|
+
if ("error" in authenticated)
|
|
5657
|
+
return Promise.resolve(authenticated);
|
|
5658
|
+
if (authenticated.method === "omo/ping") {
|
|
5659
|
+
return Promise.resolve({
|
|
5660
|
+
jsonrpc: "2.0",
|
|
5661
|
+
id: authenticated.id,
|
|
5662
|
+
result: { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, ...state.owner }
|
|
5663
|
+
});
|
|
5664
|
+
}
|
|
5665
|
+
if (authenticated.method === "$/cancelRequest") {
|
|
5666
|
+
const targetId = cancellationTargetId(authenticated.input);
|
|
5667
|
+
if (targetId !== undefined)
|
|
5668
|
+
state.activeRequests?.get(String(targetId))?.abort();
|
|
5669
|
+
return Promise.resolve(undefined);
|
|
5670
|
+
}
|
|
5671
|
+
let routed;
|
|
5672
|
+
try {
|
|
5673
|
+
routed = extractRequestContext(authenticated.input);
|
|
5674
|
+
} catch (error) {
|
|
5675
|
+
const message = error instanceof Error ? error.message : "invalid daemon request";
|
|
5676
|
+
return Promise.resolve({
|
|
5677
|
+
jsonrpc: "2.0",
|
|
5678
|
+
id: authenticated.id,
|
|
5679
|
+
error: { code: -32602, message, data: { code: "invalid_daemon_request" } }
|
|
5680
|
+
});
|
|
5681
|
+
}
|
|
5682
|
+
const { input, context } = routed;
|
|
5683
|
+
const key = routeRequestKey(authenticated.id);
|
|
5684
|
+
if (key === undefined || !state.activeRequests) {
|
|
5685
|
+
if (context)
|
|
5686
|
+
return runWithRequestContext(context, () => handleLspMcpRequest(input));
|
|
5687
|
+
return handleLspMcpRequest(input);
|
|
5688
|
+
}
|
|
5689
|
+
const controller = new AbortController;
|
|
5690
|
+
state.activeRequests.set(key, controller);
|
|
5691
|
+
const options = { signal: controller.signal };
|
|
5692
|
+
const run = context ? runWithRequestContext(context, () => handleLspMcpRequest(input, options)) : handleLspMcpRequest(input, options);
|
|
5693
|
+
return run.finally(() => {
|
|
5694
|
+
if (state.activeRequests?.get(key) === controller)
|
|
5695
|
+
state.activeRequests.delete(key);
|
|
5696
|
+
});
|
|
3586
5697
|
}
|
|
3587
|
-
function
|
|
3588
|
-
return
|
|
5698
|
+
function routeRequestKey(id) {
|
|
5699
|
+
return typeof id === "string" || typeof id === "number" ? String(id) : undefined;
|
|
3589
5700
|
}
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
5701
|
+
function cancellationTargetId(input) {
|
|
5702
|
+
const params = input["params"];
|
|
5703
|
+
if (!isPlainRecord(params))
|
|
5704
|
+
return;
|
|
5705
|
+
const id = params["id"];
|
|
5706
|
+
return typeof id === "string" || typeof id === "number" ? id : undefined;
|
|
3595
5707
|
}
|
|
3596
|
-
function
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
3601
|
-
let index = buffer.indexOf(`
|
|
3602
|
-
`);
|
|
3603
|
-
while (index !== -1) {
|
|
3604
|
-
const raw = buffer.slice(0, index).trim();
|
|
3605
|
-
buffer = buffer.slice(index + 1);
|
|
3606
|
-
if (raw.length > 0) {
|
|
3607
|
-
try {
|
|
3608
|
-
onMessage(JSON.parse(raw));
|
|
3609
|
-
} catch (error) {
|
|
3610
|
-
onParseError?.(raw, error);
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
index = buffer.indexOf(`
|
|
3614
|
-
`);
|
|
3615
|
-
}
|
|
3616
|
-
}
|
|
3617
|
-
};
|
|
5708
|
+
function parseContext(value) {
|
|
5709
|
+
if (!isPlainRecord(value))
|
|
5710
|
+
throw new InvalidDaemonRequestError("LSP request _context must be an object.");
|
|
5711
|
+
return parseLspRequestContext(value);
|
|
3618
5712
|
}
|
|
3619
5713
|
|
|
3620
5714
|
// src/daemon-client.ts
|
|
3621
5715
|
var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
3622
|
-
var
|
|
5716
|
+
var nextProxyRequestId = 1;
|
|
3623
5717
|
|
|
3624
5718
|
class DaemonRequestError extends Error {
|
|
3625
5719
|
requestWritten;
|
|
@@ -3629,46 +5723,93 @@ class DaemonRequestError extends Error {
|
|
|
3629
5723
|
this.requestWritten = requestWritten;
|
|
3630
5724
|
}
|
|
3631
5725
|
}
|
|
3632
|
-
|
|
5726
|
+
|
|
5727
|
+
class DaemonAuthenticationRejectedError extends DaemonRequestError {
|
|
5728
|
+
constructor() {
|
|
5729
|
+
super("daemon authentication failed before dispatch", true);
|
|
5730
|
+
this.name = "DaemonAuthenticationRejectedError";
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5734
|
+
class DaemonRequestCancelledError extends DaemonRequestError {
|
|
5735
|
+
constructor(requestWritten) {
|
|
5736
|
+
super("daemon request cancelled", requestWritten);
|
|
5737
|
+
this.name = "DaemonRequestCancelledError";
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
async function callToolViaDaemon(name, args, options) {
|
|
5741
|
+
const context = requireContext(options.context);
|
|
3633
5742
|
const paths = options.paths ?? daemonPaths();
|
|
3634
|
-
const ensure = options.ensure ?? ensureDaemonRunning;
|
|
5743
|
+
const ensure = options.ensure ?? ((ensurePaths, signal) => ensureDaemonRunning(ensurePaths, undefined, signal === undefined ? {} : { signal }));
|
|
3635
5744
|
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
3636
|
-
const requestArgs = withContext(args,
|
|
5745
|
+
const requestArgs = withContext(args, context);
|
|
3637
5746
|
let lastError;
|
|
3638
|
-
|
|
5747
|
+
let authRefreshUsed = false;
|
|
5748
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
3639
5749
|
try {
|
|
3640
|
-
await
|
|
3641
|
-
|
|
5750
|
+
await ensureDaemonAvailable(paths, ensure, options.signal);
|
|
5751
|
+
const token = readAuthToken(paths);
|
|
5752
|
+
if (!token)
|
|
5753
|
+
throw new DaemonRequestError("daemon auth token missing", false);
|
|
5754
|
+
const sendOptions = options.signal === undefined ? { timeoutMs } : { timeoutMs, signal: options.signal };
|
|
5755
|
+
return await sendToolCall(paths, token, name, requestArgs, sendOptions);
|
|
3642
5756
|
} catch (error) {
|
|
3643
5757
|
lastError = error;
|
|
3644
|
-
if (error instanceof
|
|
5758
|
+
if (error instanceof DaemonAuthenticationRejectedError && !authRefreshUsed) {
|
|
5759
|
+
authRefreshUsed = true;
|
|
5760
|
+
continue;
|
|
5761
|
+
}
|
|
5762
|
+
if (error instanceof DaemonRequestCancelledError)
|
|
5763
|
+
break;
|
|
5764
|
+
if (error instanceof DaemonRequestError && (error.requestWritten || !isRetryableTool(name)))
|
|
3645
5765
|
break;
|
|
3646
5766
|
}
|
|
3647
5767
|
}
|
|
3648
5768
|
return daemonUnreachableResult(paths, lastError);
|
|
3649
5769
|
}
|
|
3650
|
-
function callDiagnosticsViaDaemon(filePath, options
|
|
5770
|
+
function callDiagnosticsViaDaemon(filePath, options) {
|
|
3651
5771
|
return callToolViaDaemon("diagnostics", { filePath, severity: "error" }, options);
|
|
3652
5772
|
}
|
|
3653
|
-
var FORWARDED_ENV_KEYS = [
|
|
3654
|
-
"LSP_TOOLS_MCP_PROJECT_CONFIG",
|
|
3655
|
-
"LSP_TOOLS_MCP_USER_CONFIG",
|
|
3656
|
-
"LSP_TOOLS_MCP_INSTALL_DECISIONS"
|
|
3657
|
-
];
|
|
3658
5773
|
function currentRequestContext(env = process.env) {
|
|
3659
|
-
const
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
5774
|
+
const cwd = process.cwd();
|
|
5775
|
+
const home = env["HOME"] ?? homedir3();
|
|
5776
|
+
return parseLspRequestContext({
|
|
5777
|
+
cwd,
|
|
5778
|
+
projectConfigPaths: [join7(cwd, ".codex", "lsp-client.json")],
|
|
5779
|
+
userConfigPath: join7(home, ".codex", "lsp-client.json"),
|
|
5780
|
+
installDecisionsPath: join7(home, ".codex", "lsp-install-decisions.json"),
|
|
5781
|
+
capabilities: { installDecisionTool: true }
|
|
5782
|
+
});
|
|
5783
|
+
}
|
|
5784
|
+
function requireContext(context) {
|
|
5785
|
+
if (!context)
|
|
5786
|
+
throw new DaemonRequestError("daemon tool context is required", false);
|
|
5787
|
+
return parseLspRequestContext(context);
|
|
3666
5788
|
}
|
|
3667
5789
|
function withContext(args, context) {
|
|
3668
|
-
if (!context || context.cwd === undefined && context.env === undefined)
|
|
3669
|
-
return args;
|
|
3670
5790
|
return { ...args, [CONTEXT_KEY]: context };
|
|
3671
5791
|
}
|
|
5792
|
+
function ensureDaemonAvailable(paths, ensure, signal) {
|
|
5793
|
+
if (!signal)
|
|
5794
|
+
return ensure(paths);
|
|
5795
|
+
return new Promise((resolve9, reject) => {
|
|
5796
|
+
let settled = false;
|
|
5797
|
+
const finish = (run) => {
|
|
5798
|
+
if (settled)
|
|
5799
|
+
return;
|
|
5800
|
+
settled = true;
|
|
5801
|
+
signal.removeEventListener("abort", onAbort);
|
|
5802
|
+
run();
|
|
5803
|
+
};
|
|
5804
|
+
const onAbort = () => finish(() => reject(new DaemonRequestCancelledError(false)));
|
|
5805
|
+
if (signal.aborted) {
|
|
5806
|
+
onAbort();
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
5809
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5810
|
+
Promise.resolve().then(() => ensure(paths, signal)).then(() => finish(() => resolve9()), (error) => finish(() => reject(signal.aborted ? new DaemonRequestCancelledError(false) : error)));
|
|
5811
|
+
});
|
|
5812
|
+
}
|
|
3672
5813
|
function daemonUnreachableResult(paths, error) {
|
|
3673
5814
|
const text2 = [
|
|
3674
5815
|
`LSP daemon unreachable: ${errorText(error)}.`,
|
|
@@ -3680,39 +5821,84 @@ function daemonUnreachableResult(paths, error) {
|
|
|
3680
5821
|
`);
|
|
3681
5822
|
return { content: [{ type: "text", text: text2 }], isError: true };
|
|
3682
5823
|
}
|
|
3683
|
-
function sendToolCall(
|
|
3684
|
-
return new Promise((
|
|
3685
|
-
const socket =
|
|
5824
|
+
function sendToolCall(paths, token, name, args, options) {
|
|
5825
|
+
return new Promise((resolve9, reject) => {
|
|
5826
|
+
const socket = connect(paths.socket);
|
|
5827
|
+
const requestId = allocateProxyRequestId();
|
|
3686
5828
|
let settled = false;
|
|
3687
5829
|
let requestWritten = false;
|
|
5830
|
+
let cancelAfterWrite = false;
|
|
5831
|
+
const cancelPayload = () => encodeJsonLine({
|
|
5832
|
+
jsonrpc: "2.0",
|
|
5833
|
+
method: "$/cancelRequest",
|
|
5834
|
+
params: { _omo: authEnvelope(token), id: requestId }
|
|
5835
|
+
});
|
|
3688
5836
|
const finish = (run) => {
|
|
3689
5837
|
if (settled)
|
|
3690
5838
|
return;
|
|
3691
5839
|
settled = true;
|
|
3692
5840
|
clearTimeout(timer);
|
|
3693
|
-
|
|
5841
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
5842
|
+
destroyAfterCancel();
|
|
3694
5843
|
run();
|
|
3695
5844
|
};
|
|
3696
|
-
const
|
|
5845
|
+
const sendCancel = () => {
|
|
5846
|
+
if (!requestWritten) {
|
|
5847
|
+
cancelAfterWrite = true;
|
|
5848
|
+
return;
|
|
5849
|
+
}
|
|
5850
|
+
if (!socket.writable)
|
|
5851
|
+
return;
|
|
5852
|
+
socket.write(cancelPayload());
|
|
5853
|
+
};
|
|
5854
|
+
const destroyAfterCancel = () => {
|
|
5855
|
+
socket.destroy();
|
|
5856
|
+
};
|
|
5857
|
+
const onAbort = () => {
|
|
5858
|
+
sendCancel();
|
|
5859
|
+
finish(() => reject(new DaemonRequestCancelledError(requestWritten)));
|
|
5860
|
+
};
|
|
5861
|
+
const timer = setTimeout(() => {
|
|
5862
|
+
sendCancel();
|
|
5863
|
+
finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten)));
|
|
5864
|
+
}, options.timeoutMs);
|
|
3697
5865
|
timer.unref();
|
|
5866
|
+
if (options.signal?.aborted) {
|
|
5867
|
+
onAbort();
|
|
5868
|
+
return;
|
|
5869
|
+
}
|
|
5870
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3698
5871
|
const decoder = createLineDecoder((message) => {
|
|
3699
|
-
|
|
5872
|
+
if (isAuthErrorResponse(message)) {
|
|
5873
|
+
finish(() => reject(new DaemonAuthenticationRejectedError));
|
|
5874
|
+
return;
|
|
5875
|
+
}
|
|
5876
|
+
const result = toToolResult(message, requestId);
|
|
3700
5877
|
if (result)
|
|
3701
|
-
finish(() =>
|
|
5878
|
+
finish(() => resolve9(result));
|
|
3702
5879
|
else
|
|
3703
5880
|
finish(() => reject(new DaemonRequestError("invalid daemon response", requestWritten)));
|
|
3704
5881
|
});
|
|
3705
5882
|
socket.once("connect", () => {
|
|
3706
|
-
|
|
3707
|
-
|
|
5883
|
+
const payload = encodeJsonLine({
|
|
5884
|
+
jsonrpc: "2.0",
|
|
5885
|
+
id: requestId,
|
|
5886
|
+
method: "tools/call",
|
|
5887
|
+
params: { _omo: authEnvelope(token), name, arguments: args }
|
|
5888
|
+
});
|
|
5889
|
+
socket.write(payload, () => {
|
|
5890
|
+
requestWritten = true;
|
|
5891
|
+
if (cancelAfterWrite && socket.writable)
|
|
5892
|
+
socket.write(cancelPayload());
|
|
5893
|
+
});
|
|
3708
5894
|
});
|
|
3709
5895
|
socket.on("data", (chunk) => decoder.push(chunk));
|
|
3710
5896
|
socket.once("error", (error) => finish(() => reject(new DaemonRequestError(error.message, requestWritten))));
|
|
3711
5897
|
socket.once("close", () => finish(() => reject(new DaemonRequestError("daemon connection closed", requestWritten))));
|
|
3712
5898
|
});
|
|
3713
5899
|
}
|
|
3714
|
-
function toToolResult(message) {
|
|
3715
|
-
if (!isPlainRecord(message) || message["id"] !==
|
|
5900
|
+
function toToolResult(message, requestId) {
|
|
5901
|
+
if (!isPlainRecord(message) || message["id"] !== requestId)
|
|
3716
5902
|
return null;
|
|
3717
5903
|
const result = message["result"];
|
|
3718
5904
|
if (!isPlainRecord(result) || !Array.isArray(result["content"]))
|
|
@@ -3723,34 +5909,109 @@ function toToolResult(message) {
|
|
|
3723
5909
|
details: result["details"]
|
|
3724
5910
|
};
|
|
3725
5911
|
}
|
|
5912
|
+
function allocateProxyRequestId() {
|
|
5913
|
+
const id = nextProxyRequestId;
|
|
5914
|
+
nextProxyRequestId += 1;
|
|
5915
|
+
if (nextProxyRequestId > Number.MAX_SAFE_INTEGER)
|
|
5916
|
+
nextProxyRequestId = 1;
|
|
5917
|
+
return id;
|
|
5918
|
+
}
|
|
5919
|
+
function isRetryableTool(name) {
|
|
5920
|
+
return name !== "rename" && name !== "lsp_rename";
|
|
5921
|
+
}
|
|
3726
5922
|
function errorText(error) {
|
|
3727
5923
|
return error instanceof Error ? error.message : String(error);
|
|
3728
5924
|
}
|
|
3729
5925
|
// src/proxy.ts
|
|
5926
|
+
import { existsSync as existsSync11, realpathSync as realpathSync4 } from "node:fs";
|
|
5927
|
+
import { basename as basename3, delimiter as delimiter4, dirname as dirname9, isAbsolute as isAbsolute4 } from "node:path";
|
|
5928
|
+
var DEFAULT_STARTUP_TIMEOUT_MS = 1e4;
|
|
3730
5929
|
async function runMcpStdioProxy(options = {}) {
|
|
3731
5930
|
const input = options.input ?? process.stdin;
|
|
3732
5931
|
const output = options.output ?? process.stdout;
|
|
3733
|
-
const
|
|
3734
|
-
const
|
|
3735
|
-
const
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
5932
|
+
const stderr = options.stderr ?? process.stderr;
|
|
5933
|
+
const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
5934
|
+
const lifecycleController = new AbortController;
|
|
5935
|
+
const abortFromInput = () => lifecycleController.abort();
|
|
5936
|
+
let startupExpired = false;
|
|
5937
|
+
let startupTimer;
|
|
5938
|
+
const clearStartupWatchdog = () => {
|
|
5939
|
+
if (startupTimer === undefined)
|
|
5940
|
+
return;
|
|
5941
|
+
clearTimeout(startupTimer);
|
|
5942
|
+
startupTimer = undefined;
|
|
5943
|
+
};
|
|
5944
|
+
input.once("end", abortFromInput);
|
|
5945
|
+
input.once("close", abortFromInput);
|
|
5946
|
+
if (startupTimeoutMs > 0) {
|
|
5947
|
+
startupTimer = setTimeout(() => {
|
|
5948
|
+
startupExpired = true;
|
|
5949
|
+
input.destroy();
|
|
5950
|
+
try {
|
|
5951
|
+
stderr.write(`[lsp-daemon] no MCP request received within ${startupTimeoutMs}ms; exiting
|
|
3744
5952
|
`);
|
|
3745
|
-
|
|
3746
|
-
|
|
5953
|
+
} catch (error) {
|
|
5954
|
+
if (stderr !== process.stderr) {
|
|
5955
|
+
process.stderr.write(`[lsp-daemon] startup diagnostic failed: ${error instanceof Error ? error.message : String(error)}
|
|
5956
|
+
`);
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
}, startupTimeoutMs);
|
|
5960
|
+
}
|
|
5961
|
+
try {
|
|
5962
|
+
const paths = options.paths ?? daemonPaths();
|
|
5963
|
+
const env = options.env ?? process.env;
|
|
5964
|
+
const cwd = options.cwd ?? inferOpenCodeProjectCwd(env["LSP_TOOLS_MCP_PROJECT_CONFIG"]);
|
|
5965
|
+
const contextEnv = cwd === undefined ? env : canonicalizeContextEnv(env);
|
|
5966
|
+
const contextInput = {
|
|
5967
|
+
env: contextEnv,
|
|
5968
|
+
...cwd === undefined ? {} : { cwd },
|
|
5969
|
+
...options.homeDir === undefined ? {} : { homeDir: options.homeDir }
|
|
5970
|
+
};
|
|
5971
|
+
const context = options.context ?? createStandaloneMcpRequestContext(contextInput);
|
|
5972
|
+
const callOptions = {
|
|
5973
|
+
paths,
|
|
5974
|
+
context,
|
|
5975
|
+
...options.ensure ? { ensure: options.ensure } : {},
|
|
5976
|
+
signal: lifecycleController.signal
|
|
5977
|
+
};
|
|
5978
|
+
await runJsonRpcStdioServer({
|
|
5979
|
+
input,
|
|
5980
|
+
output,
|
|
5981
|
+
idleTimeoutMs: 0,
|
|
5982
|
+
handler: (request, requestOptions) => {
|
|
5983
|
+
clearStartupWatchdog();
|
|
5984
|
+
return runWithRequestContext(context, () => handleProxyRequest(request, requestOptions));
|
|
5985
|
+
},
|
|
5986
|
+
handlerOptions: callOptions,
|
|
5987
|
+
onHandlerError: (error) => {
|
|
5988
|
+
stderr.write(`[lsp-daemon] proxy error: ${error instanceof Error ? error.message : String(error)}
|
|
5989
|
+
`);
|
|
5990
|
+
}
|
|
5991
|
+
});
|
|
5992
|
+
} catch (error) {
|
|
5993
|
+
if (!startupExpired || !isPrematureCloseError(error))
|
|
5994
|
+
throw error;
|
|
5995
|
+
} finally {
|
|
5996
|
+
if (startupTimer !== undefined)
|
|
5997
|
+
clearTimeout(startupTimer);
|
|
5998
|
+
input.removeListener("end", abortFromInput);
|
|
5999
|
+
input.removeListener("close", abortFromInput);
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
function isPrematureCloseError(error) {
|
|
6003
|
+
return error instanceof Error && Reflect.get(error, "code") === "ERR_STREAM_PREMATURE_CLOSE";
|
|
3747
6004
|
}
|
|
3748
6005
|
async function handleProxyRequest(parsed, callOptions) {
|
|
3749
6006
|
const toolCall = asToolCall(parsed);
|
|
3750
6007
|
if (!toolCall)
|
|
3751
6008
|
return handleLspMcpRequest(parsed);
|
|
3752
6009
|
const result = await callToolViaDaemon(toolCall.name, toolCall.args, callOptions);
|
|
3753
|
-
return successResponse(toolCall.id, {
|
|
6010
|
+
return successResponse(toolCall.id, {
|
|
6011
|
+
content: result.content,
|
|
6012
|
+
isError: result.isError ?? false,
|
|
6013
|
+
details: result.details
|
|
6014
|
+
});
|
|
3754
6015
|
}
|
|
3755
6016
|
function asToolCall(parsed) {
|
|
3756
6017
|
if (!isPlainRecord(parsed) || parsed["method"] !== "tools/call")
|
|
@@ -3759,14 +6020,60 @@ function asToolCall(parsed) {
|
|
|
3759
6020
|
if (!isPlainRecord(params) || typeof params["name"] !== "string")
|
|
3760
6021
|
return null;
|
|
3761
6022
|
const args = params["arguments"];
|
|
3762
|
-
return { id:
|
|
6023
|
+
return { id: jsonRpcId2(parsed["id"]), name: params["name"], args: isPlainRecord(args) ? args : {} };
|
|
6024
|
+
}
|
|
6025
|
+
function inferOpenCodeProjectCwd(projectConfigEnv) {
|
|
6026
|
+
if (!projectConfigEnv)
|
|
6027
|
+
return;
|
|
6028
|
+
for (const entry of projectConfigEnv.split(delimiter4)) {
|
|
6029
|
+
const projectRoot = projectRootFromOpenCodeConfigPath(entry);
|
|
6030
|
+
if (projectRoot)
|
|
6031
|
+
return projectRoot;
|
|
6032
|
+
}
|
|
6033
|
+
return;
|
|
6034
|
+
}
|
|
6035
|
+
function canonicalizeContextEnv(env) {
|
|
6036
|
+
return {
|
|
6037
|
+
...env,
|
|
6038
|
+
LSP_TOOLS_MCP_PROJECT_CONFIG: canonicalizePathList(env["LSP_TOOLS_MCP_PROJECT_CONFIG"]),
|
|
6039
|
+
LSP_TOOLS_MCP_USER_CONFIG: canonicalizePath(env["LSP_TOOLS_MCP_USER_CONFIG"]),
|
|
6040
|
+
LSP_TOOLS_MCP_INSTALL_DECISIONS: canonicalizePath(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"])
|
|
6041
|
+
};
|
|
6042
|
+
}
|
|
6043
|
+
function canonicalizePathList(value) {
|
|
6044
|
+
if (value === undefined)
|
|
6045
|
+
return;
|
|
6046
|
+
return value.split(delimiter4).map((entry) => canonicalizePath(entry) ?? entry).join(delimiter4);
|
|
6047
|
+
}
|
|
6048
|
+
function canonicalizePath(value) {
|
|
6049
|
+
if (value === undefined || !isAbsolute4(value) || !existsSync11(value))
|
|
6050
|
+
return value;
|
|
6051
|
+
return realpathSync4(value);
|
|
6052
|
+
}
|
|
6053
|
+
function projectRootFromOpenCodeConfigPath(path2) {
|
|
6054
|
+
if (basename3(path2) !== "lsp.json" && basename3(path2) !== "lsp-client.json")
|
|
6055
|
+
return;
|
|
6056
|
+
const configDir = dirname9(path2);
|
|
6057
|
+
const configDirName = basename3(configDir);
|
|
6058
|
+
if (configDirName !== ".opencode" && configDirName !== ".omo")
|
|
6059
|
+
return;
|
|
6060
|
+
return dirname9(configDir);
|
|
3763
6061
|
}
|
|
3764
6062
|
export {
|
|
6063
|
+
validateDaemonVersion,
|
|
3765
6064
|
runMcpStdioProxy,
|
|
6065
|
+
resolveDaemonRuntime,
|
|
6066
|
+
probeDaemon,
|
|
3766
6067
|
ensureDaemonRunning,
|
|
3767
6068
|
disposeDefaultLspManager,
|
|
3768
6069
|
daemonPaths,
|
|
3769
6070
|
currentRequestContext,
|
|
3770
6071
|
callToolViaDaemon,
|
|
3771
|
-
callDiagnosticsViaDaemon
|
|
6072
|
+
callDiagnosticsViaDaemon,
|
|
6073
|
+
OMO_LSP_DAEMON_VERSION,
|
|
6074
|
+
OMO_LSP_DAEMON_DIR,
|
|
6075
|
+
OMO_LSP_DAEMON_CLI,
|
|
6076
|
+
InvalidRuntimeOverrideError,
|
|
6077
|
+
InvalidDaemonVersionError,
|
|
6078
|
+
InvalidDaemonDirectoryError
|
|
3772
6079
|
};
|