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
|
@@ -37,16 +37,45 @@ async function* readStdioJsonRpcMessages(input) {
|
|
|
37
37
|
yield parseJsonPayload(trailing, "line");
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
function writeStdioJsonRpcResponse(output, response, responseMode) {
|
|
40
|
+
async function writeStdioJsonRpcResponse(output, response, responseMode) {
|
|
41
41
|
const body = JSON.stringify(response);
|
|
42
|
-
|
|
43
|
-
output.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
42
|
+
const payload = responseMode === "framed" ? `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
44
43
|
\r
|
|
45
|
-
${body}`
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
${body}` : `${body}
|
|
45
|
+
`;
|
|
46
|
+
await writeChunk(output, payload);
|
|
47
|
+
}
|
|
48
|
+
function writeChunk(output, chunk) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
let settled = false;
|
|
51
|
+
const onError = (error) => {
|
|
52
|
+
if (settled)
|
|
53
|
+
return;
|
|
54
|
+
settled = true;
|
|
55
|
+
reject(error);
|
|
56
|
+
};
|
|
57
|
+
output.once("error", onError);
|
|
58
|
+
try {
|
|
59
|
+
output.write(chunk, (error) => {
|
|
60
|
+
if (settled)
|
|
61
|
+
return;
|
|
62
|
+
settled = true;
|
|
63
|
+
if (error) {
|
|
64
|
+
queueMicrotask(() => output.removeListener("error", onError));
|
|
65
|
+
reject(error);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
output.removeListener("error", onError);
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
output.removeListener("error", onError);
|
|
73
|
+
if (settled)
|
|
74
|
+
return;
|
|
75
|
+
settled = true;
|
|
76
|
+
reject(error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
50
79
|
}
|
|
51
80
|
function readNextMessage(buffer) {
|
|
52
81
|
if (buffer.length === 0)
|
|
@@ -142,40 +171,70 @@ async function runJsonRpcStdioServer(config) {
|
|
|
142
171
|
break;
|
|
143
172
|
idleTimer.arm();
|
|
144
173
|
if (message.kind === "parse_error") {
|
|
145
|
-
handleParseError(message, config, log)
|
|
174
|
+
if (!await handleParseError(message, config, log))
|
|
175
|
+
break;
|
|
146
176
|
continue;
|
|
147
177
|
}
|
|
148
|
-
await handleRequest(message, config, log)
|
|
178
|
+
if (!await handleRequest(message, config, log))
|
|
179
|
+
break;
|
|
149
180
|
}
|
|
150
181
|
} finally {
|
|
151
182
|
idleTimer.clear();
|
|
152
183
|
log("stdio_stopped");
|
|
153
184
|
}
|
|
154
185
|
}
|
|
155
|
-
function handleParseError(message, config, log) {
|
|
186
|
+
async function handleParseError(message, config, log) {
|
|
156
187
|
log("parse_error", { message: message.message });
|
|
157
188
|
const response = config.parseErrorResponse?.(message.message) ?? errorResponse(null, -32700, "Parse error", message.message);
|
|
158
|
-
if (response
|
|
159
|
-
|
|
160
|
-
|
|
189
|
+
if (response === undefined)
|
|
190
|
+
return true;
|
|
191
|
+
return writeResponse(response, {
|
|
192
|
+
output: config.output,
|
|
193
|
+
responseMode: message.responseMode,
|
|
194
|
+
log
|
|
195
|
+
});
|
|
161
196
|
}
|
|
162
197
|
async function handleRequest(message, config, log) {
|
|
163
198
|
const parsed = message.payload;
|
|
164
199
|
const id = isPlainRecord(parsed) ? jsonRpcId(parsed["id"]) : null;
|
|
165
200
|
const method = isPlainRecord(parsed) && typeof parsed["method"] === "string" ? parsed["method"] : null;
|
|
166
201
|
log("request", { id: id === null ? null : String(id), method });
|
|
202
|
+
let response;
|
|
167
203
|
try {
|
|
168
|
-
|
|
169
|
-
if (response === undefined)
|
|
170
|
-
return;
|
|
171
|
-
writeStdioJsonRpcResponse(config.output, response, message.responseMode);
|
|
172
|
-
log("response", { id: String(response.id), method, is_error: response.error !== undefined });
|
|
204
|
+
response = await config.handler(parsed, config.handlerOptions);
|
|
173
205
|
} catch (error) {
|
|
174
206
|
if (config.onHandlerError === undefined)
|
|
175
207
|
throw error;
|
|
176
208
|
config.onHandlerError(error);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (response === undefined)
|
|
212
|
+
return true;
|
|
213
|
+
if (!await writeResponse(response, {
|
|
214
|
+
output: config.output,
|
|
215
|
+
responseMode: message.responseMode,
|
|
216
|
+
log
|
|
217
|
+
}))
|
|
218
|
+
return false;
|
|
219
|
+
log("response", { id: String(response.id), method, is_error: response.error !== undefined });
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
async function writeResponse(response, context) {
|
|
223
|
+
try {
|
|
224
|
+
await writeStdioJsonRpcResponse(context.output, response, context.responseMode);
|
|
225
|
+
return true;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (!isTerminalOutputError(error))
|
|
228
|
+
throw error;
|
|
229
|
+
context.log("output_error", { message: messageFromError(error) });
|
|
230
|
+
return false;
|
|
177
231
|
}
|
|
178
232
|
}
|
|
233
|
+
function isTerminalOutputError(error) {
|
|
234
|
+
if (!(error instanceof Error) || !("code" in error))
|
|
235
|
+
return false;
|
|
236
|
+
return error.code === "EPIPE" || error.code === "ERR_STREAM_DESTROYED" || error.code === "ERR_STREAM_WRITE_AFTER_END";
|
|
237
|
+
}
|
|
179
238
|
function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
|
|
180
239
|
let timer = null;
|
|
181
240
|
let isClosed = false;
|
|
@@ -201,37 +260,197 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
|
|
|
201
260
|
closed: () => isClosed
|
|
202
261
|
};
|
|
203
262
|
}
|
|
204
|
-
// ../lsp-core/src/tools/diagnostics.ts
|
|
205
|
-
import { resolve as resolve4 } from "node:path";
|
|
206
|
-
|
|
207
263
|
// ../lsp-core/src/lsp/client-wrapper.ts
|
|
208
|
-
import { existsSync as
|
|
209
|
-
import { dirname as
|
|
264
|
+
import { existsSync as existsSync9, statSync as statSync3 } from "node:fs";
|
|
265
|
+
import { dirname as dirname6, join as join4, resolve as resolve7 } from "node:path";
|
|
210
266
|
|
|
211
267
|
// ../lsp-core/src/request-context.ts
|
|
212
268
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
269
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
270
|
+
import { homedir } from "node:os";
|
|
271
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
272
|
+
|
|
273
|
+
class LspRequestContextParseError extends Error {
|
|
274
|
+
code;
|
|
275
|
+
name = "LspRequestContextParseError";
|
|
276
|
+
constructor(code, message) {
|
|
277
|
+
super(message);
|
|
278
|
+
this.code = code;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
class LspRequestContextUnavailableError extends Error {
|
|
283
|
+
name = "LspRequestContextUnavailableError";
|
|
284
|
+
constructor() {
|
|
285
|
+
super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
213
288
|
var storage = new AsyncLocalStorage;
|
|
289
|
+
var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
290
|
+
var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
|
|
214
291
|
function runWithRequestContext(context, fn) {
|
|
215
292
|
return storage.run(context, fn);
|
|
216
293
|
}
|
|
294
|
+
function lspRequestContext() {
|
|
295
|
+
const context = storage.getStore();
|
|
296
|
+
if (!context)
|
|
297
|
+
throw new LspRequestContextUnavailableError;
|
|
298
|
+
return context;
|
|
299
|
+
}
|
|
217
300
|
function contextCwd() {
|
|
218
|
-
return
|
|
301
|
+
return lspRequestContext().cwd;
|
|
219
302
|
}
|
|
220
303
|
function contextEnv(key) {
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
return
|
|
224
|
-
|
|
304
|
+
const context = lspRequestContext();
|
|
305
|
+
if (key === "LSP_TOOLS_MCP_PROJECT_CONFIG")
|
|
306
|
+
return context.projectConfigPaths.join(delimiter);
|
|
307
|
+
if (key === "LSP_TOOLS_MCP_USER_CONFIG")
|
|
308
|
+
return context.userConfigPath;
|
|
309
|
+
if (key === "LSP_TOOLS_MCP_INSTALL_DECISIONS")
|
|
310
|
+
return context.installDecisionsPath;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
function createStandaloneMcpRequestContext(input = {}) {
|
|
314
|
+
const env = input.env ?? process.env;
|
|
315
|
+
const cwd = canonicalCwd(input.cwd ?? process.cwd());
|
|
316
|
+
const home = input.homeDir ?? homedir();
|
|
317
|
+
const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
|
|
318
|
+
const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
|
|
319
|
+
const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
|
|
320
|
+
return parseLspRequestContext({
|
|
321
|
+
cwd,
|
|
322
|
+
projectConfigPaths,
|
|
323
|
+
userConfigPath,
|
|
324
|
+
installDecisionsPath,
|
|
325
|
+
capabilities: { installDecisionTool: true }
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function parseLspRequestContext(value) {
|
|
329
|
+
if (!isRecord(value)) {
|
|
330
|
+
throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
|
|
331
|
+
}
|
|
332
|
+
rejectUnknownFields(value, CONTEXT_FIELDS, "context");
|
|
333
|
+
const cwd = stringField(value, "cwd");
|
|
334
|
+
const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
|
|
335
|
+
const userConfigPath = stringField(value, "userConfigPath");
|
|
336
|
+
const installDecisionsPath = stringField(value, "installDecisionsPath");
|
|
337
|
+
const capabilities = capabilitiesField(value["capabilities"]);
|
|
338
|
+
const canonical = canonicalCwd(cwd);
|
|
339
|
+
for (const path of projectConfigPaths) {
|
|
340
|
+
requireAbsolutePath(path, "projectConfigPaths");
|
|
341
|
+
const projectPath = canonicalizeExistingOrNearestAncestor(path);
|
|
342
|
+
if (!isPathInside(canonical, projectPath)) {
|
|
343
|
+
throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
requireAbsolutePath(userConfigPath, "userConfigPath");
|
|
347
|
+
requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
|
|
348
|
+
return {
|
|
349
|
+
cwd: canonical,
|
|
350
|
+
projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
|
|
351
|
+
userConfigPath,
|
|
352
|
+
installDecisionsPath,
|
|
353
|
+
capabilities
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function translateProjectConfigEnv(value, cwd) {
|
|
357
|
+
if (value === undefined || value.length === 0)
|
|
358
|
+
return [join(cwd, ".codex", "lsp-client.json")];
|
|
359
|
+
return value.split(delimiter).filter((entry) => entry.length > 0).map((entry) => isAbsolute(entry) ? entry : join(cwd, entry));
|
|
360
|
+
}
|
|
361
|
+
function translateHomeConfigEnv(value, home, fallback) {
|
|
362
|
+
if (value === undefined || value.length === 0)
|
|
363
|
+
return join(home, fallback);
|
|
364
|
+
return isAbsolute(value) ? value : join(home, value);
|
|
365
|
+
}
|
|
366
|
+
function canonicalCwd(cwd) {
|
|
367
|
+
const resolved = resolve(cwd);
|
|
368
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
369
|
+
throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
370
|
+
}
|
|
371
|
+
return realpathSync(resolved);
|
|
372
|
+
}
|
|
373
|
+
function canonicalizeExistingOrNearestAncestor(path) {
|
|
374
|
+
let current = resolve(path);
|
|
375
|
+
const suffix = [];
|
|
376
|
+
while (true) {
|
|
377
|
+
try {
|
|
378
|
+
const existing = realpathSync(current);
|
|
379
|
+
return suffix.length === 0 ? existing : join(existing, ...suffix);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (!isMissingPathError(error))
|
|
382
|
+
throw error;
|
|
383
|
+
const parent = dirname(current);
|
|
384
|
+
if (parent === current)
|
|
385
|
+
throw error;
|
|
386
|
+
suffix.unshift(basename(current));
|
|
387
|
+
current = parent;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function capabilitiesField(value) {
|
|
392
|
+
if (!isRecord(value)) {
|
|
393
|
+
throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
394
|
+
}
|
|
395
|
+
rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
|
|
396
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
397
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
398
|
+
throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
|
|
399
|
+
}
|
|
400
|
+
return { installDecisionTool };
|
|
401
|
+
}
|
|
402
|
+
function stringField(value, field) {
|
|
403
|
+
const fieldValue = value[field];
|
|
404
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
405
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
406
|
+
}
|
|
407
|
+
return fieldValue;
|
|
408
|
+
}
|
|
409
|
+
function stringArrayField(value, field) {
|
|
410
|
+
const fieldValue = value[field];
|
|
411
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
412
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
|
|
413
|
+
}
|
|
414
|
+
return fieldValue;
|
|
415
|
+
}
|
|
416
|
+
function requireAbsolutePath(path, field) {
|
|
417
|
+
if (!isAbsolute(path)) {
|
|
418
|
+
throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function isPathInside(parent, child) {
|
|
422
|
+
const childPath = resolve(child);
|
|
423
|
+
const relativePath = relative(parent, childPath);
|
|
424
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
425
|
+
}
|
|
426
|
+
function isMissingPathError(error) {
|
|
427
|
+
const code = errorCode(error);
|
|
428
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
429
|
+
}
|
|
430
|
+
function rejectUnknownFields(value, allowed, scope) {
|
|
431
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
432
|
+
if (unknown.length > 0) {
|
|
433
|
+
throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function isRecord(value) {
|
|
437
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
438
|
+
}
|
|
439
|
+
function errorCode(error) {
|
|
440
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
441
|
+
return;
|
|
442
|
+
const code = Reflect.get(error, "code");
|
|
443
|
+
return typeof code === "string" ? code : undefined;
|
|
225
444
|
}
|
|
226
445
|
|
|
227
446
|
// ../lsp-core/src/lsp/effective-extension.ts
|
|
228
|
-
import { basename, extname } from "node:path";
|
|
447
|
+
import { basename as basename2, extname } from "node:path";
|
|
229
448
|
var BASENAME_EXTENSIONS = {
|
|
230
449
|
Dockerfile: ".dockerfile",
|
|
231
450
|
Containerfile: ".dockerfile"
|
|
232
451
|
};
|
|
233
452
|
function effectiveExtension(filePath) {
|
|
234
|
-
return BASENAME_EXTENSIONS[
|
|
453
|
+
return BASENAME_EXTENSIONS[basename2(filePath)] ?? extname(filePath);
|
|
235
454
|
}
|
|
236
455
|
|
|
237
456
|
// ../lsp-core/src/lsp/errors.ts
|
|
@@ -281,7 +500,12 @@ class LspInvalidPathError extends Error {
|
|
|
281
500
|
}
|
|
282
501
|
|
|
283
502
|
class LspServerLookupError extends Error {
|
|
503
|
+
lookup;
|
|
284
504
|
name = "LspServerLookupError";
|
|
505
|
+
constructor(message, lookup) {
|
|
506
|
+
super(message);
|
|
507
|
+
this.lookup = lookup;
|
|
508
|
+
}
|
|
285
509
|
}
|
|
286
510
|
|
|
287
511
|
class LspServerInitializingError extends Error {
|
|
@@ -301,17 +525,18 @@ function isLspDeadConnectionError(err) {
|
|
|
301
525
|
}
|
|
302
526
|
|
|
303
527
|
// ../lsp-core/src/lsp/cleanup-errors.ts
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
528
|
+
function writeCleanupError(message) {
|
|
529
|
+
process.stderr.write(`${message}
|
|
530
|
+
`);
|
|
531
|
+
}
|
|
532
|
+
function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
|
|
307
533
|
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
-
|
|
534
|
+
logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
|
|
309
535
|
}
|
|
310
536
|
|
|
311
537
|
// ../lsp-core/src/lsp/client.ts
|
|
312
|
-
import {
|
|
313
|
-
import {
|
|
314
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
538
|
+
import { resolve as resolve6 } from "node:path";
|
|
539
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
315
540
|
|
|
316
541
|
// ../lsp-core/src/lsp/connection.ts
|
|
317
542
|
import { pathToFileURL } from "node:url";
|
|
@@ -375,28 +600,80 @@ class JsonRpcConnection {
|
|
|
375
600
|
onError(handler) {
|
|
376
601
|
this.errorHandlers.push(handler);
|
|
377
602
|
}
|
|
378
|
-
async sendRequest(method, params) {
|
|
603
|
+
async sendRequest(method, params, options = {}) {
|
|
379
604
|
if (this.disposed)
|
|
380
605
|
throw new Error("JSON-RPC connection is disposed");
|
|
381
606
|
const id = this.nextRequestId;
|
|
382
607
|
this.nextRequestId += 1;
|
|
608
|
+
const key = String(id);
|
|
383
609
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
384
|
-
|
|
385
|
-
|
|
610
|
+
let requestWritten = false;
|
|
611
|
+
let cancelAfterWrite = false;
|
|
612
|
+
let settled = false;
|
|
613
|
+
const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
614
|
+
const responsePromise = new Promise((resolve2, reject) => {
|
|
615
|
+
const cleanup = () => {
|
|
616
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
617
|
+
};
|
|
618
|
+
const settleCancel = () => {
|
|
619
|
+
if (settled)
|
|
620
|
+
return;
|
|
621
|
+
settled = true;
|
|
622
|
+
this.pendingRequests.delete(key);
|
|
623
|
+
cleanup();
|
|
624
|
+
const rejectCancelled = () => reject(abortError(options.signal));
|
|
625
|
+
if (!requestWritten) {
|
|
626
|
+
cancelAfterWrite = true;
|
|
627
|
+
rejectCancelled();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
writeCancel().then(rejectCancelled, (error) => {
|
|
631
|
+
this.emitError(toError(error));
|
|
632
|
+
rejectCancelled();
|
|
633
|
+
});
|
|
634
|
+
};
|
|
635
|
+
const onAbort = () => settleCancel();
|
|
636
|
+
this.pendingRequests.set(key, {
|
|
386
637
|
resolve(result) {
|
|
387
|
-
|
|
638
|
+
settled = true;
|
|
639
|
+
cleanup();
|
|
640
|
+
resolve2(result);
|
|
641
|
+
},
|
|
642
|
+
reject(error) {
|
|
643
|
+
settled = true;
|
|
644
|
+
cleanup();
|
|
645
|
+
reject(error);
|
|
388
646
|
},
|
|
389
|
-
|
|
647
|
+
cleanup
|
|
390
648
|
});
|
|
649
|
+
if (options.signal?.aborted) {
|
|
650
|
+
settleCancel();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
391
654
|
});
|
|
655
|
+
if (settled)
|
|
656
|
+
return responsePromise;
|
|
392
657
|
try {
|
|
393
658
|
await this.writeMessage(message);
|
|
659
|
+
requestWritten = true;
|
|
660
|
+
if (cancelAfterWrite)
|
|
661
|
+
await writeCancel();
|
|
394
662
|
} catch (error) {
|
|
395
|
-
|
|
663
|
+
if (settled)
|
|
664
|
+
return responsePromise;
|
|
665
|
+
const pending = this.pendingRequests.get(key);
|
|
666
|
+
if (pending) {
|
|
667
|
+
pending.cleanup();
|
|
668
|
+
this.pendingRequests.delete(key);
|
|
669
|
+
}
|
|
396
670
|
throw error;
|
|
397
671
|
}
|
|
398
672
|
return responsePromise;
|
|
399
673
|
}
|
|
674
|
+
pendingRequestCount() {
|
|
675
|
+
return this.pendingRequests.size;
|
|
676
|
+
}
|
|
400
677
|
async sendNotification(method, params) {
|
|
401
678
|
if (this.disposed)
|
|
402
679
|
return;
|
|
@@ -413,6 +690,7 @@ class JsonRpcConnection {
|
|
|
413
690
|
this.reader.off("error", this.handleStreamError);
|
|
414
691
|
this.writer.off("error", this.handleStreamError);
|
|
415
692
|
for (const pending of this.pendingRequests.values()) {
|
|
693
|
+
pending.cleanup();
|
|
416
694
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
417
695
|
}
|
|
418
696
|
this.pendingRequests.clear();
|
|
@@ -488,6 +766,7 @@ class JsonRpcConnection {
|
|
|
488
766
|
if (!pending)
|
|
489
767
|
return;
|
|
490
768
|
this.pendingRequests.delete(String(id));
|
|
769
|
+
pending.cleanup();
|
|
491
770
|
if ("error" in message) {
|
|
492
771
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
493
772
|
return;
|
|
@@ -501,7 +780,11 @@ class JsonRpcConnection {
|
|
|
501
780
|
try {
|
|
502
781
|
handler(params);
|
|
503
782
|
} catch (error) {
|
|
504
|
-
|
|
783
|
+
if (error instanceof Error) {
|
|
784
|
+
this.emitError(error);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
this.emitError(new Error(String(error)));
|
|
505
788
|
}
|
|
506
789
|
}
|
|
507
790
|
handleRequest(message) {
|
|
@@ -526,13 +809,13 @@ class JsonRpcConnection {
|
|
|
526
809
|
const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
527
810
|
\r
|
|
528
811
|
${body}`;
|
|
529
|
-
return new Promise((
|
|
812
|
+
return new Promise((resolve2, reject) => {
|
|
530
813
|
this.writer.write(payload, (error) => {
|
|
531
814
|
if (error) {
|
|
532
815
|
reject(error);
|
|
533
816
|
return;
|
|
534
817
|
}
|
|
535
|
-
|
|
818
|
+
resolve2();
|
|
536
819
|
});
|
|
537
820
|
});
|
|
538
821
|
}
|
|
@@ -542,6 +825,14 @@ ${body}`;
|
|
|
542
825
|
}
|
|
543
826
|
}
|
|
544
827
|
}
|
|
828
|
+
function abortError(signal) {
|
|
829
|
+
const reason = signal?.reason;
|
|
830
|
+
if (reason instanceof Error)
|
|
831
|
+
return reason;
|
|
832
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
833
|
+
error.name = "AbortError";
|
|
834
|
+
return error;
|
|
835
|
+
}
|
|
545
836
|
function parseContentLength2(headers) {
|
|
546
837
|
for (const line of headers.split(`\r
|
|
547
838
|
`)) {
|
|
@@ -581,8 +872,8 @@ function toError(error) {
|
|
|
581
872
|
|
|
582
873
|
// ../lsp-core/src/lsp/process.ts
|
|
583
874
|
import { spawn, spawnSync } from "node:child_process";
|
|
584
|
-
import { existsSync, statSync } from "node:fs";
|
|
585
|
-
import { delimiter, join } from "node:path";
|
|
875
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
|
|
876
|
+
import { delimiter as delimiter2, join as join2 } from "node:path";
|
|
586
877
|
function isMissingProcessError(error) {
|
|
587
878
|
if (!(error instanceof Error) || !("code" in error))
|
|
588
879
|
return false;
|
|
@@ -595,10 +886,10 @@ function reportKillError(context, error) {
|
|
|
595
886
|
}
|
|
596
887
|
function validateCwd(cwd) {
|
|
597
888
|
try {
|
|
598
|
-
if (!
|
|
889
|
+
if (!existsSync2(cwd)) {
|
|
599
890
|
return { valid: false, error: `Working directory does not exist: ${cwd}` };
|
|
600
891
|
}
|
|
601
|
-
const stats =
|
|
892
|
+
const stats = statSync2(cwd);
|
|
602
893
|
if (!stats.isDirectory()) {
|
|
603
894
|
return { valid: false, error: `Path is not a directory: ${cwd}` };
|
|
604
895
|
}
|
|
@@ -611,9 +902,9 @@ function validateCwd(cwd) {
|
|
|
611
902
|
}
|
|
612
903
|
}
|
|
613
904
|
function wrap(proc) {
|
|
614
|
-
const exitedPromise = new Promise((
|
|
615
|
-
proc.once("close", (code) =>
|
|
616
|
-
proc.once("error", () =>
|
|
905
|
+
const exitedPromise = new Promise((resolve2) => {
|
|
906
|
+
proc.once("close", (code) => resolve2(code ?? 0));
|
|
907
|
+
proc.once("error", () => resolve2(1));
|
|
617
908
|
});
|
|
618
909
|
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
619
910
|
throw new LspProcessSpawnError("Spawned process is missing one of stdin/stdout/stderr pipes");
|
|
@@ -667,7 +958,7 @@ function isWindowsShellShim(command) {
|
|
|
667
958
|
return lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat");
|
|
668
959
|
}
|
|
669
960
|
function splitPath(pathValue, platform) {
|
|
670
|
-
const separator = platform === "win32" ? ";" :
|
|
961
|
+
const separator = platform === "win32" ? ";" : delimiter2;
|
|
671
962
|
return pathValue.split(separator).filter(Boolean);
|
|
672
963
|
}
|
|
673
964
|
function getWindowsPathExtensions(env) {
|
|
@@ -682,8 +973,8 @@ function resolveWindowsCommand(command, env) {
|
|
|
682
973
|
const extensions = getWindowsPathExtensions(env);
|
|
683
974
|
for (const baseDirectory of baseDirectories) {
|
|
684
975
|
for (const extension of extensions) {
|
|
685
|
-
const candidate = baseDirectory ?
|
|
686
|
-
if (
|
|
976
|
+
const candidate = baseDirectory ? join2(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
|
|
977
|
+
if (existsSync2(candidate))
|
|
687
978
|
return candidate;
|
|
688
979
|
}
|
|
689
980
|
}
|
|
@@ -728,16 +1019,16 @@ function spawnProcess(command, options) {
|
|
|
728
1019
|
return wrap(proc);
|
|
729
1020
|
}
|
|
730
1021
|
|
|
731
|
-
// ../lsp-core/src/lsp/transport.ts
|
|
732
|
-
function
|
|
1022
|
+
// ../lsp-core/src/lsp/transport-protocol.ts
|
|
1023
|
+
function isRecord2(value) {
|
|
733
1024
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
734
1025
|
}
|
|
735
1026
|
function parseConfigurationItems(params) {
|
|
736
|
-
if (!
|
|
1027
|
+
if (!isRecord2(params) || !Array.isArray(params["items"]))
|
|
737
1028
|
return [];
|
|
738
1029
|
const items = [];
|
|
739
1030
|
for (const item of params["items"]) {
|
|
740
|
-
if (!
|
|
1031
|
+
if (!isRecord2(item))
|
|
741
1032
|
continue;
|
|
742
1033
|
const section = item["section"];
|
|
743
1034
|
items.push(section === undefined || typeof section !== "string" ? {} : { section });
|
|
@@ -745,10 +1036,35 @@ function parseConfigurationItems(params) {
|
|
|
745
1036
|
return items;
|
|
746
1037
|
}
|
|
747
1038
|
function parseDiagnosticsParams(params) {
|
|
748
|
-
if (!
|
|
1039
|
+
if (!isRecord2(params) || typeof params["uri"] !== "string")
|
|
749
1040
|
return null;
|
|
750
1041
|
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
751
|
-
|
|
1042
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
1043
|
+
return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
|
|
1044
|
+
}
|
|
1045
|
+
function createLspSpawnEnv(_root, input) {
|
|
1046
|
+
return { ...input };
|
|
1047
|
+
}
|
|
1048
|
+
function isDiagnostic(value) {
|
|
1049
|
+
return isRecord2(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
1050
|
+
}
|
|
1051
|
+
function isRange(value) {
|
|
1052
|
+
return isRecord2(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
1053
|
+
}
|
|
1054
|
+
function isPosition(value) {
|
|
1055
|
+
return isRecord2(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// ../lsp-core/src/lsp/transport.ts
|
|
1059
|
+
class LspClientNotStartedError extends Error {
|
|
1060
|
+
serverId;
|
|
1061
|
+
root;
|
|
1062
|
+
name = "LspClientNotStartedError";
|
|
1063
|
+
constructor(serverId, root) {
|
|
1064
|
+
super("LSP client not started");
|
|
1065
|
+
this.serverId = serverId;
|
|
1066
|
+
this.root = root;
|
|
1067
|
+
}
|
|
752
1068
|
}
|
|
753
1069
|
|
|
754
1070
|
class LspClientTransport {
|
|
@@ -761,6 +1077,8 @@ class LspClientTransport {
|
|
|
761
1077
|
diagnosticsStore = new Map;
|
|
762
1078
|
requestTimeoutMs;
|
|
763
1079
|
initializeTimeoutMs;
|
|
1080
|
+
workspaceApplyEditHandler = null;
|
|
1081
|
+
diagnosticPullSupported = false;
|
|
764
1082
|
constructor(root, server2, timeouts = {}) {
|
|
765
1083
|
this.root = root;
|
|
766
1084
|
this.server = server2;
|
|
@@ -773,6 +1091,21 @@ class LspClientTransport {
|
|
|
773
1091
|
command() {
|
|
774
1092
|
return [...this.server.command];
|
|
775
1093
|
}
|
|
1094
|
+
setWorkspaceApplyEditHandler(handler) {
|
|
1095
|
+
this.workspaceApplyEditHandler = handler;
|
|
1096
|
+
}
|
|
1097
|
+
hasWorkspaceApplyEditHandler() {
|
|
1098
|
+
return this.workspaceApplyEditHandler !== null;
|
|
1099
|
+
}
|
|
1100
|
+
setDiagnosticPullSupported(supported) {
|
|
1101
|
+
this.diagnosticPullSupported = supported;
|
|
1102
|
+
}
|
|
1103
|
+
isDiagnosticPullSupported() {
|
|
1104
|
+
return this.diagnosticPullSupported;
|
|
1105
|
+
}
|
|
1106
|
+
handlePublishDiagnostics(params) {
|
|
1107
|
+
this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
|
|
1108
|
+
}
|
|
776
1109
|
async start() {
|
|
777
1110
|
const env = createLspSpawnEnv(this.root, {
|
|
778
1111
|
...process.env,
|
|
@@ -783,7 +1116,6 @@ class LspClientTransport {
|
|
|
783
1116
|
env
|
|
784
1117
|
});
|
|
785
1118
|
this.startStderrReading();
|
|
786
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
787
1119
|
if (this.proc.exitCode !== null) {
|
|
788
1120
|
const stderr = this.stderrBuffer.join(`
|
|
789
1121
|
`);
|
|
@@ -793,7 +1125,7 @@ class LspClientTransport {
|
|
|
793
1125
|
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
794
1126
|
const diagnosticsParams = parseDiagnosticsParams(params);
|
|
795
1127
|
if (diagnosticsParams?.uri) {
|
|
796
|
-
this.
|
|
1128
|
+
this.handlePublishDiagnostics(diagnosticsParams);
|
|
797
1129
|
}
|
|
798
1130
|
});
|
|
799
1131
|
this.connection.onRequest("workspace/configuration", (params) => {
|
|
@@ -806,6 +1138,9 @@ class LspClientTransport {
|
|
|
806
1138
|
});
|
|
807
1139
|
this.connection.onRequest("client/registerCapability", () => null);
|
|
808
1140
|
this.connection.onRequest("window/workDoneProgress/create", () => null);
|
|
1141
|
+
if (this.workspaceApplyEditHandler) {
|
|
1142
|
+
this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
|
|
1143
|
+
}
|
|
809
1144
|
this.connection.onClose(() => {
|
|
810
1145
|
this.processExited = true;
|
|
811
1146
|
});
|
|
@@ -834,30 +1169,25 @@ class LspClientTransport {
|
|
|
834
1169
|
}
|
|
835
1170
|
async sendRequest(method, ...args) {
|
|
836
1171
|
if (!this.connection)
|
|
837
|
-
throw new
|
|
1172
|
+
throw new LspClientNotStartedError(this.server.id, this.root);
|
|
838
1173
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
839
1174
|
const stderrTail = this.stderrBuffer.slice(-10).join(`
|
|
840
1175
|
`);
|
|
841
1176
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
|
|
842
1177
|
}
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
1178
|
+
const options = args[1];
|
|
1179
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
1180
|
+
const timeoutController = new AbortController;
|
|
1181
|
+
const timeoutHandle = setTimeout(() => {
|
|
1182
|
+
const stderrTail = this.stderrBuffer.slice(-5).join(`
|
|
848
1183
|
`);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1184
|
+
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
1185
|
+
}, timeoutMs);
|
|
1186
|
+
const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
|
|
852
1187
|
try {
|
|
853
|
-
const
|
|
854
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
855
|
-
if (timeoutHandle !== null)
|
|
856
|
-
clearTimeout(timeoutHandle);
|
|
1188
|
+
const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
|
|
857
1189
|
return result;
|
|
858
1190
|
} catch (error) {
|
|
859
|
-
if (timeoutHandle !== null)
|
|
860
|
-
clearTimeout(timeoutHandle);
|
|
861
1191
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
862
1192
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
|
|
863
1193
|
`) || undefined);
|
|
@@ -866,6 +1196,9 @@ class LspClientTransport {
|
|
|
866
1196
|
throw new LspConnectionClosedError(this.server.id, this.root, error.message);
|
|
867
1197
|
}
|
|
868
1198
|
throw error;
|
|
1199
|
+
} finally {
|
|
1200
|
+
clearTimeout(timeoutHandle);
|
|
1201
|
+
combinedSignal.dispose();
|
|
869
1202
|
}
|
|
870
1203
|
}
|
|
871
1204
|
async sendNotification(method, ...args) {
|
|
@@ -894,17 +1227,17 @@ class LspClientTransport {
|
|
|
894
1227
|
try {
|
|
895
1228
|
await this.sendRequest("shutdown");
|
|
896
1229
|
} catch (error) {
|
|
897
|
-
reportBestEffortCleanupError("shutdown request", error);
|
|
1230
|
+
reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
|
|
898
1231
|
}
|
|
899
1232
|
try {
|
|
900
1233
|
await this.sendNotification("exit");
|
|
901
1234
|
} catch (error) {
|
|
902
|
-
reportBestEffortCleanupError("exit notification", error);
|
|
1235
|
+
reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
|
|
903
1236
|
}
|
|
904
1237
|
try {
|
|
905
1238
|
this.connection.dispose();
|
|
906
1239
|
} catch (error) {
|
|
907
|
-
reportBestEffortCleanupError("connection dispose", error);
|
|
1240
|
+
reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
|
|
908
1241
|
}
|
|
909
1242
|
this.connection = null;
|
|
910
1243
|
}
|
|
@@ -915,8 +1248,8 @@ class LspClientTransport {
|
|
|
915
1248
|
try {
|
|
916
1249
|
proc.kill();
|
|
917
1250
|
let timeoutId;
|
|
918
|
-
const timeoutPromise = new Promise((
|
|
919
|
-
timeoutId = setTimeout(
|
|
1251
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
1252
|
+
timeoutId = setTimeout(resolve2, STOP_HARD_KILL_TIMEOUT_MS);
|
|
920
1253
|
});
|
|
921
1254
|
await Promise.race([
|
|
922
1255
|
proc.exited.then(() => {
|
|
@@ -932,14 +1265,14 @@ class LspClientTransport {
|
|
|
932
1265
|
proc.kill("SIGKILL");
|
|
933
1266
|
await Promise.race([
|
|
934
1267
|
proc.exited,
|
|
935
|
-
new Promise((
|
|
1268
|
+
new Promise((resolve2) => setTimeout(resolve2, STOP_SIGKILL_GRACE_MS))
|
|
936
1269
|
]);
|
|
937
1270
|
} catch (error) {
|
|
938
|
-
reportBestEffortCleanupError("hard process kill", error);
|
|
1271
|
+
reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
|
|
939
1272
|
}
|
|
940
1273
|
}
|
|
941
1274
|
} catch (error) {
|
|
942
|
-
reportBestEffortCleanupError("process stop", error);
|
|
1275
|
+
reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
|
|
943
1276
|
}
|
|
944
1277
|
}
|
|
945
1278
|
this.processExited = true;
|
|
@@ -949,26 +1282,45 @@ class LspClientTransport {
|
|
|
949
1282
|
return this.diagnosticsStore.get(uri) ?? [];
|
|
950
1283
|
}
|
|
951
1284
|
}
|
|
952
|
-
function
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1285
|
+
function combineAbortSignals(primary, secondary) {
|
|
1286
|
+
const controller = new AbortController;
|
|
1287
|
+
const abortFrom = (signal) => {
|
|
1288
|
+
if (!controller.signal.aborted)
|
|
1289
|
+
controller.abort(signal.reason);
|
|
1290
|
+
};
|
|
1291
|
+
const onPrimaryAbort = () => {
|
|
1292
|
+
if (primary)
|
|
1293
|
+
abortFrom(primary);
|
|
1294
|
+
};
|
|
1295
|
+
const onSecondaryAbort = () => abortFrom(secondary);
|
|
1296
|
+
if (primary?.aborted)
|
|
1297
|
+
abortFrom(primary);
|
|
1298
|
+
else
|
|
1299
|
+
primary?.addEventListener("abort", onPrimaryAbort, { once: true });
|
|
1300
|
+
if (secondary.aborted)
|
|
1301
|
+
abortFrom(secondary);
|
|
1302
|
+
else
|
|
1303
|
+
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
|
|
1304
|
+
return {
|
|
1305
|
+
signal: controller.signal,
|
|
1306
|
+
dispose: () => {
|
|
1307
|
+
primary?.removeEventListener("abort", onPrimaryAbort);
|
|
1308
|
+
secondary.removeEventListener("abort", onSecondaryAbort);
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
963
1311
|
}
|
|
964
1312
|
|
|
965
1313
|
// ../lsp-core/src/lsp/connection.ts
|
|
966
|
-
|
|
1314
|
+
function supportsDiagnosticPull(capabilities) {
|
|
1315
|
+
if (capabilities === undefined)
|
|
1316
|
+
return false;
|
|
1317
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
1318
|
+
}
|
|
967
1319
|
|
|
968
1320
|
class LspClientConnection extends LspClientTransport {
|
|
969
1321
|
async initialize() {
|
|
970
1322
|
const rootUri = pathToFileURL(this.root).href;
|
|
971
|
-
await this.sendRequest("initialize", {
|
|
1323
|
+
const result = await this.sendRequest("initialize", {
|
|
972
1324
|
processId: process.pid,
|
|
973
1325
|
rootUri,
|
|
974
1326
|
rootPath: this.root,
|
|
@@ -982,8 +1334,7 @@ class LspClientConnection extends LspClientTransport {
|
|
|
982
1334
|
publishDiagnostics: {},
|
|
983
1335
|
rename: {
|
|
984
1336
|
prepareSupport: true,
|
|
985
|
-
prepareSupportDefaultBehavior: 1
|
|
986
|
-
honorsChangeAnnotations: true
|
|
1337
|
+
prepareSupportDefaultBehavior: 1
|
|
987
1338
|
},
|
|
988
1339
|
codeAction: {
|
|
989
1340
|
codeActionLiteralSupport: {
|
|
@@ -1012,22 +1363,28 @@ class LspClientConnection extends LspClientTransport {
|
|
|
1012
1363
|
symbol: {},
|
|
1013
1364
|
workspaceFolders: true,
|
|
1014
1365
|
configuration: true,
|
|
1015
|
-
applyEdit: true,
|
|
1366
|
+
...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
|
|
1016
1367
|
workspaceEdit: {
|
|
1017
|
-
documentChanges: true
|
|
1368
|
+
documentChanges: true,
|
|
1369
|
+
resourceOperations: ["create", "rename", "delete"]
|
|
1018
1370
|
}
|
|
1019
1371
|
}
|
|
1020
1372
|
},
|
|
1021
1373
|
initializationOptions: this.server.initialization
|
|
1022
1374
|
}, { timeoutMs: this.initializeTimeoutMs });
|
|
1375
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
1023
1376
|
await this.sendNotification("initialized");
|
|
1024
1377
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
1025
1378
|
settings: { json: { validate: { enable: true } } }
|
|
1026
1379
|
});
|
|
1027
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
1028
1380
|
}
|
|
1029
1381
|
}
|
|
1030
1382
|
|
|
1383
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1384
|
+
import { readFileSync, realpathSync as realpathSync2 } from "node:fs";
|
|
1385
|
+
import { relative as relative2, resolve as resolve2 } from "node:path";
|
|
1386
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1387
|
+
|
|
1031
1388
|
// ../lsp-core/src/lsp/language-mappings.ts
|
|
1032
1389
|
var SYMBOL_KIND_MAP = {
|
|
1033
1390
|
1: "File",
|
|
@@ -1200,82 +1557,1422 @@ function getLanguageId(ext) {
|
|
|
1200
1557
|
return EXT_TO_LANG[ext] ?? "plaintext";
|
|
1201
1558
|
}
|
|
1202
1559
|
|
|
1560
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1561
|
+
var WATCHED_FILE_BATCH_SIZE = 128;
|
|
1562
|
+
var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1563
|
+
function canonicalPath(filePath) {
|
|
1564
|
+
const absolute = resolve2(filePath);
|
|
1565
|
+
try {
|
|
1566
|
+
return realpathSync2(absolute);
|
|
1567
|
+
} catch {
|
|
1568
|
+
return absolute;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function isSameOrDescendant(candidate, parent) {
|
|
1572
|
+
const suffix = relative2(parent, candidate);
|
|
1573
|
+
return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
|
|
1574
|
+
}
|
|
1575
|
+
function movedPath(candidate, oldPath, newPath) {
|
|
1576
|
+
const suffix = relative2(oldPath, candidate);
|
|
1577
|
+
return suffix === "" ? newPath : resolve2(newPath, suffix);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
class WorkspaceDocumentState {
|
|
1581
|
+
sendNotification;
|
|
1582
|
+
clearDiagnostics;
|
|
1583
|
+
openDocuments = new Map;
|
|
1584
|
+
openByUri = new Map;
|
|
1585
|
+
openPromises = new Map;
|
|
1586
|
+
now;
|
|
1587
|
+
versionlessPublishQuiescenceMs;
|
|
1588
|
+
constructor(sendNotification, clearDiagnostics, options = {}) {
|
|
1589
|
+
this.sendNotification = sendNotification;
|
|
1590
|
+
this.clearDiagnostics = clearDiagnostics;
|
|
1591
|
+
this.now = options.now ?? (() => Date.now());
|
|
1592
|
+
this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
1593
|
+
}
|
|
1594
|
+
async openFile(filePath) {
|
|
1595
|
+
const path = canonicalPath(filePath);
|
|
1596
|
+
const existingOpen = this.openPromises.get(path);
|
|
1597
|
+
if (existingOpen) {
|
|
1598
|
+
await existingOpen;
|
|
1599
|
+
return this.openFile(path);
|
|
1600
|
+
}
|
|
1601
|
+
const text = readFileSync(path, "utf-8");
|
|
1602
|
+
const existing = this.openDocuments.get(path);
|
|
1603
|
+
if (!existing)
|
|
1604
|
+
return this.openDocumentSingleFlight(path, text);
|
|
1605
|
+
if (existing.text === text)
|
|
1606
|
+
return;
|
|
1607
|
+
await this.changeDocument(existing, text);
|
|
1608
|
+
}
|
|
1609
|
+
getVersion(filePath) {
|
|
1610
|
+
return this.openDocuments.get(canonicalPath(filePath))?.version;
|
|
1611
|
+
}
|
|
1612
|
+
getStoredDiagnostics(uri) {
|
|
1613
|
+
const state = this.openByUri.get(uri);
|
|
1614
|
+
if (!state)
|
|
1615
|
+
return [];
|
|
1616
|
+
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
1617
|
+
}
|
|
1618
|
+
captureDiagnosticSnapshot(filePath) {
|
|
1619
|
+
const state = this.openDocuments.get(canonicalPath(filePath));
|
|
1620
|
+
if (!state)
|
|
1621
|
+
return null;
|
|
1622
|
+
return {
|
|
1623
|
+
path: state.path,
|
|
1624
|
+
uri: state.uri,
|
|
1625
|
+
version: state.version,
|
|
1626
|
+
documentGeneration: state.generation,
|
|
1627
|
+
publishGeneration: state.publishGeneration
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
isCurrentSnapshot(snapshot) {
|
|
1631
|
+
const state = this.openDocuments.get(snapshot.path);
|
|
1632
|
+
return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
|
|
1633
|
+
}
|
|
1634
|
+
getPullCache(snapshot) {
|
|
1635
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1636
|
+
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
|
|
1637
|
+
return null;
|
|
1638
|
+
return state.pullCache;
|
|
1639
|
+
}
|
|
1640
|
+
recordPullDiagnostics(snapshot, report) {
|
|
1641
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1642
|
+
if (!state)
|
|
1643
|
+
return;
|
|
1644
|
+
state.pullCache = {
|
|
1645
|
+
documentVersion: snapshot.version,
|
|
1646
|
+
diagnostics: [...report.diagnostics],
|
|
1647
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
recordPublishedDiagnostics(params) {
|
|
1651
|
+
const state = this.openByUri.get(params.uri);
|
|
1652
|
+
if (!state)
|
|
1653
|
+
return;
|
|
1654
|
+
state.publishGeneration += 1;
|
|
1655
|
+
state.lastPublish = {
|
|
1656
|
+
diagnostics: [...params.diagnostics],
|
|
1657
|
+
publishGeneration: state.publishGeneration,
|
|
1658
|
+
documentGenerationAtArrival: state.generation,
|
|
1659
|
+
arrivedAt: this.now(),
|
|
1660
|
+
...params.version === undefined ? {} : { version: params.version }
|
|
1661
|
+
};
|
|
1662
|
+
this.notifyWaiters(state);
|
|
1663
|
+
}
|
|
1664
|
+
resolvePushDiagnostics(snapshot) {
|
|
1665
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1666
|
+
if (!state?.lastPublish)
|
|
1667
|
+
return { status: "missing" };
|
|
1668
|
+
const publish = state.lastPublish;
|
|
1669
|
+
if (publish.version !== undefined) {
|
|
1670
|
+
return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
|
|
1671
|
+
}
|
|
1672
|
+
if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
|
|
1673
|
+
return { status: "missing" };
|
|
1674
|
+
const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
|
|
1675
|
+
const waitMs = Math.max(0, readyAt - this.now());
|
|
1676
|
+
return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
|
|
1677
|
+
}
|
|
1678
|
+
waitForDiagnosticsActivity(snapshot, timeoutMs) {
|
|
1679
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1680
|
+
if (!state || timeoutMs <= 0)
|
|
1681
|
+
return Promise.resolve();
|
|
1682
|
+
return new Promise((resolveActivity) => {
|
|
1683
|
+
let settled = false;
|
|
1684
|
+
const finish = () => {
|
|
1685
|
+
if (settled)
|
|
1686
|
+
return;
|
|
1687
|
+
settled = true;
|
|
1688
|
+
clearTimeout(timer);
|
|
1689
|
+
state.waiters.delete(finish);
|
|
1690
|
+
resolveActivity();
|
|
1691
|
+
};
|
|
1692
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1693
|
+
if (typeof timer.unref === "function")
|
|
1694
|
+
timer.unref();
|
|
1695
|
+
state.waiters.add(finish);
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
validateVersions(operations) {
|
|
1699
|
+
const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
|
|
1700
|
+
for (const operation of operations) {
|
|
1701
|
+
if (operation.kind === "text") {
|
|
1702
|
+
const current = versions.get(operation.path);
|
|
1703
|
+
if (operation.documentVersion !== null && current !== operation.documentVersion) {
|
|
1704
|
+
const observed = current === undefined ? "closed document" : `open document version ${current}`;
|
|
1705
|
+
return {
|
|
1706
|
+
changeIndex: operation.changeIndex,
|
|
1707
|
+
message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
if (current !== undefined)
|
|
1711
|
+
versions.set(operation.path, current + 1);
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
if (operation.kind === "rename") {
|
|
1715
|
+
const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
|
|
1716
|
+
for (const [path] of moved)
|
|
1717
|
+
versions.delete(path);
|
|
1718
|
+
for (const [path] of moved)
|
|
1719
|
+
versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
if (operation.kind === "delete") {
|
|
1723
|
+
for (const path of [...versions.keys()]) {
|
|
1724
|
+
if (isSameOrDescendant(path, operation.path))
|
|
1725
|
+
versions.delete(path);
|
|
1726
|
+
}
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
|
|
1730
|
+
versions.set(operation.path, 1);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
async synchronize(delta) {
|
|
1736
|
+
const watched = [];
|
|
1737
|
+
for (const mutation of delta.operations)
|
|
1738
|
+
await this.synchronizeMutation(mutation, watched);
|
|
1739
|
+
for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
|
|
1740
|
+
await this.sendNotification("workspace/didChangeWatchedFiles", {
|
|
1741
|
+
changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async synchronizeMutation(mutation, watched) {
|
|
1746
|
+
if (mutation.kind === "text") {
|
|
1747
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1748
|
+
if (state)
|
|
1749
|
+
await this.changeDocument(state, mutation.afterText);
|
|
1750
|
+
else
|
|
1751
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
if (mutation.kind === "create") {
|
|
1755
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1756
|
+
if (state) {
|
|
1757
|
+
await this.closeDocument(state);
|
|
1758
|
+
await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
|
|
1759
|
+
} else {
|
|
1760
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
|
|
1761
|
+
}
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (mutation.kind === "rename") {
|
|
1765
|
+
const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
|
|
1766
|
+
for (const state of moved)
|
|
1767
|
+
await this.closeDocument(state);
|
|
1768
|
+
for (const state of moved) {
|
|
1769
|
+
const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
|
|
1770
|
+
await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
|
|
1771
|
+
}
|
|
1772
|
+
if (moved.length === 0) {
|
|
1773
|
+
watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
|
|
1774
|
+
watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
|
|
1775
|
+
}
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
|
|
1779
|
+
for (const state of removed)
|
|
1780
|
+
await this.closeDocument(state);
|
|
1781
|
+
if (removed.length === 0)
|
|
1782
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
|
|
1783
|
+
}
|
|
1784
|
+
async openDocumentSingleFlight(path, text) {
|
|
1785
|
+
const existing = this.openPromises.get(path);
|
|
1786
|
+
if (existing)
|
|
1787
|
+
return existing;
|
|
1788
|
+
const open = (async () => {
|
|
1789
|
+
const state = {
|
|
1790
|
+
path,
|
|
1791
|
+
uri: pathToFileURL2(path).href,
|
|
1792
|
+
languageId: getLanguageId(effectiveExtension(path)),
|
|
1793
|
+
text,
|
|
1794
|
+
version: 1,
|
|
1795
|
+
generation: 1,
|
|
1796
|
+
publishGeneration: 0,
|
|
1797
|
+
waiters: new Set
|
|
1798
|
+
};
|
|
1799
|
+
this.openDocuments.set(path, state);
|
|
1800
|
+
this.openByUri.set(state.uri, state);
|
|
1801
|
+
this.notifyWaiters(state);
|
|
1802
|
+
await this.sendNotification("textDocument/didOpen", {
|
|
1803
|
+
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
|
|
1804
|
+
});
|
|
1805
|
+
})().finally(() => {
|
|
1806
|
+
this.openPromises.delete(path);
|
|
1807
|
+
});
|
|
1808
|
+
this.openPromises.set(path, open);
|
|
1809
|
+
return open;
|
|
1810
|
+
}
|
|
1811
|
+
async changeDocument(state, text) {
|
|
1812
|
+
state.text = text;
|
|
1813
|
+
state.version += 1;
|
|
1814
|
+
state.generation += 1;
|
|
1815
|
+
this.clearDiagnostics(state.uri);
|
|
1816
|
+
this.notifyWaiters(state);
|
|
1817
|
+
await this.sendNotification("textDocument/didChange", {
|
|
1818
|
+
textDocument: { uri: state.uri, version: state.version },
|
|
1819
|
+
contentChanges: [{ text }]
|
|
1820
|
+
});
|
|
1821
|
+
await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
|
|
1822
|
+
}
|
|
1823
|
+
async closeDocument(state) {
|
|
1824
|
+
this.openDocuments.delete(state.path);
|
|
1825
|
+
this.openByUri.delete(state.uri);
|
|
1826
|
+
this.clearDiagnostics(state.uri);
|
|
1827
|
+
this.notifyWaiters(state);
|
|
1828
|
+
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
1829
|
+
}
|
|
1830
|
+
notifyWaiters(state) {
|
|
1831
|
+
for (const waiter of [...state.waiters])
|
|
1832
|
+
waiter();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
|
|
1837
|
+
var CONCURRENT_FAILURE_REASON_BY_PHASE = {
|
|
1838
|
+
applying: "workspace/applyEdit is already in progress for this workspace mutation",
|
|
1839
|
+
settled: "workspace/applyEdit was already handled for this workspace mutation"
|
|
1840
|
+
};
|
|
1841
|
+
function workspaceApplyEditConcurrentFailureReason(phase) {
|
|
1842
|
+
return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1846
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1847
|
+
|
|
1848
|
+
// ../lsp-core/src/lsp/workspace-edit-path.ts
|
|
1849
|
+
import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync3 } from "node:fs";
|
|
1850
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3 } from "node:path";
|
|
1851
|
+
import { fileURLToPath } from "node:url";
|
|
1852
|
+
|
|
1853
|
+
class WorkspaceEditPathError extends Error {
|
|
1854
|
+
path;
|
|
1855
|
+
detail;
|
|
1856
|
+
name = "WorkspaceEditPathError";
|
|
1857
|
+
constructor(path, detail) {
|
|
1858
|
+
super(`${detail}: ${path}`);
|
|
1859
|
+
this.path = path;
|
|
1860
|
+
this.detail = detail;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
1864
|
+
const relativePath = relative3(workspaceRoot, filePath);
|
|
1865
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
|
|
1866
|
+
}
|
|
1867
|
+
function canonicalizeMissingPath(filePath) {
|
|
1868
|
+
let ancestor = filePath;
|
|
1869
|
+
while (!existsSync3(ancestor)) {
|
|
1870
|
+
const parent = dirname2(ancestor);
|
|
1871
|
+
if (parent === ancestor)
|
|
1872
|
+
throw new WorkspaceEditPathError(filePath, "no existing ancestor");
|
|
1873
|
+
ancestor = parent;
|
|
1874
|
+
}
|
|
1875
|
+
return resolve3(realpathSync3(ancestor), relative3(ancestor, filePath));
|
|
1876
|
+
}
|
|
1877
|
+
function canonicalWorkspaceRoot(workspaceRoot) {
|
|
1878
|
+
try {
|
|
1879
|
+
const canonical = realpathSync3(resolve3(workspaceRoot));
|
|
1880
|
+
if (!lstatSync(canonical).isDirectory()) {
|
|
1881
|
+
return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
success: true,
|
|
1885
|
+
path: canonical,
|
|
1886
|
+
requestedPath: resolve3(workspaceRoot),
|
|
1887
|
+
followedSymbolicLink: existsSync3(resolve3(workspaceRoot)) && lstatSync(resolve3(workspaceRoot)).isSymbolicLink()
|
|
1888
|
+
};
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1891
|
+
return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
|
|
1895
|
+
let requestedPath;
|
|
1896
|
+
try {
|
|
1897
|
+
const parsed = new URL(uri);
|
|
1898
|
+
if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
|
|
1899
|
+
return { success: false, error: `non-file URI ${uri}` };
|
|
1900
|
+
}
|
|
1901
|
+
requestedPath = resolve3(fileURLToPath(parsed));
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1904
|
+
return { success: false, error: `non-file URI ${uri}: ${detail}` };
|
|
1905
|
+
}
|
|
1906
|
+
try {
|
|
1907
|
+
const canonical = existsSync3(requestedPath) ? realpathSync3(requestedPath) : canonicalizeMissingPath(requestedPath);
|
|
1908
|
+
if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
|
|
1909
|
+
return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
|
|
1910
|
+
}
|
|
1911
|
+
return {
|
|
1912
|
+
success: true,
|
|
1913
|
+
path: canonical,
|
|
1914
|
+
requestedPath,
|
|
1915
|
+
followedSymbolicLink: existsSync3(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
|
|
1916
|
+
};
|
|
1917
|
+
} catch (error) {
|
|
1918
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1919
|
+
return { success: false, error: `${requestedPath}: ${detail}` };
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
function snapshotPath(path, includeChildren) {
|
|
1923
|
+
if (!existsSync3(path))
|
|
1924
|
+
return { kind: "missing" };
|
|
1925
|
+
const stats = lstatSync(path);
|
|
1926
|
+
if (stats.isFile())
|
|
1927
|
+
return { kind: "file", content: readFileSync2(path, "utf-8") };
|
|
1928
|
+
if (stats.isDirectory()) {
|
|
1929
|
+
return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
|
|
1930
|
+
}
|
|
1931
|
+
throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1935
|
+
var DEFAULT_IO = {
|
|
1936
|
+
writeFile(path, content) {
|
|
1937
|
+
writeFileSync(path, content, "utf-8");
|
|
1938
|
+
},
|
|
1939
|
+
rename(oldPath, newPath) {
|
|
1940
|
+
renameSync(oldPath, newPath);
|
|
1941
|
+
},
|
|
1942
|
+
remove(path, recursive) {
|
|
1943
|
+
rmSync(path, { recursive, force: false });
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
function snapshotsEqual(expected, actual) {
|
|
1947
|
+
if (expected.kind !== actual.kind)
|
|
1948
|
+
return false;
|
|
1949
|
+
if (expected.kind === "file" && actual.kind === "file")
|
|
1950
|
+
return expected.content === actual.content;
|
|
1951
|
+
if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
|
|
1952
|
+
return JSON.stringify(expected.children) === JSON.stringify(actual.children);
|
|
1953
|
+
}
|
|
1954
|
+
return true;
|
|
1955
|
+
}
|
|
1956
|
+
function liveSnapshot(path, expected) {
|
|
1957
|
+
return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
|
|
1958
|
+
}
|
|
1959
|
+
function firstOperationIndex(plan) {
|
|
1960
|
+
return plan.operations[0]?.changeIndex ?? 0;
|
|
1961
|
+
}
|
|
1962
|
+
function failedCommit(plan, failure) {
|
|
1963
|
+
const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
|
|
1964
|
+
return {
|
|
1965
|
+
result: {
|
|
1966
|
+
success: false,
|
|
1967
|
+
filesModified,
|
|
1968
|
+
totalEdits,
|
|
1969
|
+
errors: [`change ${changeIndex}: ${message}`],
|
|
1970
|
+
failedChange: changeIndex,
|
|
1971
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1972
|
+
},
|
|
1973
|
+
delta: mutationDelta(mutations),
|
|
1974
|
+
fingerprint: plan.fingerprint
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
function verifySnapshots(plan) {
|
|
1978
|
+
for (const [path, expected] of plan.snapshots) {
|
|
1979
|
+
let actual;
|
|
1980
|
+
try {
|
|
1981
|
+
actual = liveSnapshot(path, expected);
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1984
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1985
|
+
return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
|
|
1986
|
+
}
|
|
1987
|
+
if (!snapshotsEqual(expected, actual)) {
|
|
1988
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1989
|
+
return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return null;
|
|
1993
|
+
}
|
|
1994
|
+
function addModifiedPath(paths, path) {
|
|
1995
|
+
if (!paths.includes(path))
|
|
1996
|
+
paths.push(path);
|
|
1997
|
+
}
|
|
1998
|
+
function reportedPath(plan, path) {
|
|
1999
|
+
return plan.reportedPathByCanonical.get(path) ?? path;
|
|
2000
|
+
}
|
|
2001
|
+
function changedPathsForMutation(mutation) {
|
|
2002
|
+
return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
|
|
2003
|
+
}
|
|
2004
|
+
function mutationDelta(operations) {
|
|
2005
|
+
const changedPaths = new Set;
|
|
2006
|
+
for (const operation of operations) {
|
|
2007
|
+
for (const path of changedPathsForMutation(operation))
|
|
2008
|
+
changedPaths.add(path);
|
|
2009
|
+
}
|
|
2010
|
+
return { operations, changedPaths: [...changedPaths].sort() };
|
|
2011
|
+
}
|
|
2012
|
+
function resolveIo(overrides) {
|
|
2013
|
+
return {
|
|
2014
|
+
writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
|
|
2015
|
+
rename: overrides?.rename ?? DEFAULT_IO.rename,
|
|
2016
|
+
remove: overrides?.remove ?? DEFAULT_IO.remove
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function commitOperation(context, operation) {
|
|
2020
|
+
const { plan, io, accumulator } = context;
|
|
2021
|
+
if (operation.kind === "noop")
|
|
2022
|
+
return;
|
|
2023
|
+
if (operation.kind === "text") {
|
|
2024
|
+
io.writeFile(operation.path, operation.afterText);
|
|
2025
|
+
accumulator.mutations.push({
|
|
2026
|
+
kind: "text",
|
|
2027
|
+
path: operation.path,
|
|
2028
|
+
beforeText: operation.beforeText,
|
|
2029
|
+
afterText: operation.afterText
|
|
2030
|
+
});
|
|
2031
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2032
|
+
accumulator.totalEdits += operation.editCount;
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (operation.kind === "create") {
|
|
2036
|
+
io.writeFile(operation.path, "");
|
|
2037
|
+
accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
|
|
2038
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
if (operation.kind === "rename") {
|
|
2042
|
+
if (operation.replaceDestination) {
|
|
2043
|
+
const targetKind = existsSync4(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
|
|
2044
|
+
io.remove(operation.newPath, targetKind === "directory");
|
|
2045
|
+
accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
|
|
2046
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
2047
|
+
}
|
|
2048
|
+
io.rename(operation.oldPath, operation.newPath);
|
|
2049
|
+
accumulator.mutations.push({
|
|
2050
|
+
kind: "rename",
|
|
2051
|
+
oldPath: operation.oldPath,
|
|
2052
|
+
newPath: operation.newPath,
|
|
2053
|
+
sourceKind: operation.sourceKind
|
|
2054
|
+
});
|
|
2055
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
io.remove(operation.path, operation.recursive);
|
|
2059
|
+
accumulator.mutations.push({
|
|
2060
|
+
kind: "delete",
|
|
2061
|
+
path: operation.path,
|
|
2062
|
+
targetKind: operation.targetKind
|
|
2063
|
+
});
|
|
2064
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2065
|
+
}
|
|
2066
|
+
function commitWorkspaceEditPlan(plan, options = {}) {
|
|
2067
|
+
if (options.signal?.aborted) {
|
|
2068
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2069
|
+
}
|
|
2070
|
+
const stale = verifySnapshots(plan);
|
|
2071
|
+
if (stale)
|
|
2072
|
+
return stale;
|
|
2073
|
+
if (options.signal?.aborted) {
|
|
2074
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2075
|
+
}
|
|
2076
|
+
const io = resolveIo(options.io);
|
|
2077
|
+
const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
|
|
2078
|
+
const context = { plan, io, accumulator };
|
|
2079
|
+
let lateAbort = false;
|
|
2080
|
+
for (const operation of plan.operations) {
|
|
2081
|
+
try {
|
|
2082
|
+
commitOperation(context, operation);
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2085
|
+
return failedCommit(plan, {
|
|
2086
|
+
message: `I/O failure during ${operation.kind}: ${detail}`,
|
|
2087
|
+
changeIndex: operation.changeIndex,
|
|
2088
|
+
mutations: accumulator.mutations,
|
|
2089
|
+
filesModified: accumulator.filesModified,
|
|
2090
|
+
totalEdits: accumulator.totalEdits,
|
|
2091
|
+
lateAbort: lateAbort || options.signal?.aborted === true
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
if (options.signal?.aborted)
|
|
2095
|
+
lateAbort = true;
|
|
2096
|
+
}
|
|
2097
|
+
const result = {
|
|
2098
|
+
success: true,
|
|
2099
|
+
filesModified: accumulator.filesModified,
|
|
2100
|
+
totalEdits: accumulator.totalEdits,
|
|
2101
|
+
errors: [],
|
|
2102
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
2103
|
+
};
|
|
2104
|
+
return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
|
|
2108
|
+
import { createHash } from "node:crypto";
|
|
2109
|
+
function canonicalFingerprint(operations) {
|
|
2110
|
+
const canonical = operations.map((operation) => {
|
|
2111
|
+
switch (operation.kind) {
|
|
2112
|
+
case "text":
|
|
2113
|
+
return {
|
|
2114
|
+
kind: operation.kind,
|
|
2115
|
+
changeIndex: operation.changeIndex,
|
|
2116
|
+
path: operation.path,
|
|
2117
|
+
edits: operation.edits,
|
|
2118
|
+
version: operation.version
|
|
2119
|
+
};
|
|
2120
|
+
case "rename":
|
|
2121
|
+
return {
|
|
2122
|
+
kind: operation.kind,
|
|
2123
|
+
changeIndex: operation.changeIndex,
|
|
2124
|
+
oldPath: operation.oldPath,
|
|
2125
|
+
newPath: operation.newPath,
|
|
2126
|
+
overwrite: operation.overwrite,
|
|
2127
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2128
|
+
};
|
|
2129
|
+
case "create":
|
|
2130
|
+
return {
|
|
2131
|
+
kind: operation.kind,
|
|
2132
|
+
changeIndex: operation.changeIndex,
|
|
2133
|
+
path: operation.path,
|
|
2134
|
+
overwrite: operation.overwrite,
|
|
2135
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2136
|
+
};
|
|
2137
|
+
case "delete":
|
|
2138
|
+
return {
|
|
2139
|
+
kind: operation.kind,
|
|
2140
|
+
changeIndex: operation.changeIndex,
|
|
2141
|
+
path: operation.path,
|
|
2142
|
+
recursive: operation.recursive,
|
|
2143
|
+
ignoreIfNotExists: operation.ignoreIfNotExists
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// ../lsp-core/src/lsp/workspace-edit-types.ts
|
|
2151
|
+
class WorkspaceEditValidationError extends Error {
|
|
2152
|
+
changeIndex;
|
|
2153
|
+
detail;
|
|
2154
|
+
name = "WorkspaceEditValidationError";
|
|
2155
|
+
constructor(changeIndex, detail) {
|
|
2156
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
2157
|
+
this.changeIndex = changeIndex;
|
|
2158
|
+
this.detail = detail;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
|
|
2163
|
+
function isRecord3(value) {
|
|
2164
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2165
|
+
}
|
|
2166
|
+
function parsePosition(value) {
|
|
2167
|
+
if (!isRecord3(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
return { line: value["line"], character: value["character"] };
|
|
2171
|
+
}
|
|
2172
|
+
function parseRange(value) {
|
|
2173
|
+
if (!isRecord3(value))
|
|
2174
|
+
return null;
|
|
2175
|
+
const start = parsePosition(value["start"]);
|
|
2176
|
+
const end = parsePosition(value["end"]);
|
|
2177
|
+
return start && end ? { start, end } : null;
|
|
2178
|
+
}
|
|
2179
|
+
function parseTextEdits(value, changeIndex) {
|
|
2180
|
+
if (!Array.isArray(value)) {
|
|
2181
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
|
|
2182
|
+
}
|
|
2183
|
+
const edits = [];
|
|
2184
|
+
for (const candidate of value) {
|
|
2185
|
+
if (!isRecord3(candidate) || typeof candidate["newText"] !== "string") {
|
|
2186
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
|
|
2187
|
+
}
|
|
2188
|
+
if ("annotationId" in candidate) {
|
|
2189
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
|
|
2190
|
+
}
|
|
2191
|
+
const range = parseRange(candidate["range"]);
|
|
2192
|
+
if (!range)
|
|
2193
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
|
|
2194
|
+
edits.push({ range, newText: candidate["newText"] });
|
|
2195
|
+
}
|
|
2196
|
+
return edits;
|
|
2197
|
+
}
|
|
2198
|
+
function parseBooleanOption(options, key, changeIndex) {
|
|
2199
|
+
const value = options[key];
|
|
2200
|
+
if (value === undefined)
|
|
2201
|
+
return false;
|
|
2202
|
+
if (typeof value !== "boolean") {
|
|
2203
|
+
throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
|
|
2204
|
+
}
|
|
2205
|
+
return value;
|
|
2206
|
+
}
|
|
2207
|
+
function parseOptions(value, allowed, changeIndex) {
|
|
2208
|
+
if (value === undefined)
|
|
2209
|
+
return {};
|
|
2210
|
+
if (!isRecord3(value))
|
|
2211
|
+
throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
|
|
2212
|
+
for (const key of Object.keys(value)) {
|
|
2213
|
+
if (!allowed.includes(key))
|
|
2214
|
+
throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
|
|
2215
|
+
}
|
|
2216
|
+
const parsed = {};
|
|
2217
|
+
for (const key of allowed)
|
|
2218
|
+
parsed[key] = parseBooleanOption(value, key, changeIndex);
|
|
2219
|
+
return parsed;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
|
|
2223
|
+
function parseResourceChange(input) {
|
|
2224
|
+
const kind = input.change["kind"];
|
|
2225
|
+
if (kind === "create" || kind === "delete") {
|
|
2226
|
+
parseSinglePathResource(input, kind);
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
if (kind !== "rename") {
|
|
2230
|
+
throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
|
|
2231
|
+
}
|
|
2232
|
+
parseRename(input);
|
|
2233
|
+
}
|
|
2234
|
+
function parseSinglePathResource(input, kind) {
|
|
2235
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2236
|
+
if (typeof change["uri"] !== "string")
|
|
2237
|
+
throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
|
|
2238
|
+
const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
|
|
2239
|
+
if (!resolvedPath.success) {
|
|
2240
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
if (kind === "create") {
|
|
2244
|
+
const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2245
|
+
target.operations.push({
|
|
2246
|
+
kind,
|
|
2247
|
+
changeIndex,
|
|
2248
|
+
path: resolvedPath.path,
|
|
2249
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2250
|
+
overwrite: options2["overwrite"] ?? false,
|
|
2251
|
+
ignoreIfExists: options2["ignoreIfExists"] ?? false,
|
|
2252
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2253
|
+
});
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
|
|
2257
|
+
target.operations.push({
|
|
2258
|
+
kind,
|
|
2259
|
+
changeIndex,
|
|
2260
|
+
path: resolvedPath.path,
|
|
2261
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2262
|
+
recursive: options["recursive"] ?? false,
|
|
2263
|
+
ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
|
|
2264
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
function parseRename(input) {
|
|
2268
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2269
|
+
if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
|
|
2270
|
+
throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
|
|
2271
|
+
}
|
|
2272
|
+
const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
|
|
2273
|
+
const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
|
|
2274
|
+
if (!oldPath.success || !newPath.success) {
|
|
2275
|
+
target.failures.push({
|
|
2276
|
+
changeIndex,
|
|
2277
|
+
message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
|
|
2278
|
+
});
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2282
|
+
target.operations.push({
|
|
2283
|
+
kind: "rename",
|
|
2284
|
+
changeIndex,
|
|
2285
|
+
oldPath: oldPath.path,
|
|
2286
|
+
newPath: newPath.path,
|
|
2287
|
+
reportedOldPath: oldPath.requestedPath,
|
|
2288
|
+
reportedNewPath: newPath.requestedPath,
|
|
2289
|
+
overwrite: options["overwrite"] ?? false,
|
|
2290
|
+
ignoreIfExists: options["ignoreIfExists"] ?? false,
|
|
2291
|
+
followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
// ../lsp-core/src/lsp/workspace-edit-parser.ts
|
|
2296
|
+
function failureResult(failures) {
|
|
2297
|
+
const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
|
|
2298
|
+
const first = sorted[0];
|
|
2299
|
+
return {
|
|
2300
|
+
success: false,
|
|
2301
|
+
filesModified: [],
|
|
2302
|
+
totalEdits: 0,
|
|
2303
|
+
errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
|
|
2304
|
+
...first ? { failedChange: first.changeIndex } : {}
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
function parseWorkspaceEdit(edit, workspaceRoot) {
|
|
2308
|
+
if (!isRecord3(edit))
|
|
2309
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
|
|
2310
|
+
if (edit["changeAnnotations"] !== undefined) {
|
|
2311
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
|
|
2312
|
+
}
|
|
2313
|
+
const hasChanges = edit["changes"] !== undefined;
|
|
2314
|
+
const hasDocumentChanges = edit["documentChanges"] !== undefined;
|
|
2315
|
+
if (hasChanges && hasDocumentChanges) {
|
|
2316
|
+
return {
|
|
2317
|
+
operations: [],
|
|
2318
|
+
failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
const target = { operations: [], failures: [] };
|
|
2322
|
+
if (hasChanges)
|
|
2323
|
+
return parseChanges(edit["changes"], workspaceRoot, target);
|
|
2324
|
+
return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
|
|
2325
|
+
}
|
|
2326
|
+
function parseChanges(value, workspaceRoot, target) {
|
|
2327
|
+
if (!isRecord3(value))
|
|
2328
|
+
return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
|
|
2329
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
2330
|
+
for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
|
|
2331
|
+
const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
|
|
2332
|
+
if (!resolvedPath.success) {
|
|
2333
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
try {
|
|
2337
|
+
target.operations.push({
|
|
2338
|
+
kind: "text",
|
|
2339
|
+
changeIndex,
|
|
2340
|
+
path: resolvedPath.path,
|
|
2341
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2342
|
+
edits: parseTextEdits(rawEdits, changeIndex),
|
|
2343
|
+
version: null
|
|
2344
|
+
});
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2347
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
throw error;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return target;
|
|
2354
|
+
}
|
|
2355
|
+
function parseDocumentChanges(value, workspaceRoot, target) {
|
|
2356
|
+
if (value === undefined)
|
|
2357
|
+
return target;
|
|
2358
|
+
if (!Array.isArray(value)) {
|
|
2359
|
+
return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
|
|
2360
|
+
}
|
|
2361
|
+
for (const [changeIndex, change] of value.entries()) {
|
|
2362
|
+
try {
|
|
2363
|
+
parseDocumentChange({ change, changeIndex, workspaceRoot, target });
|
|
2364
|
+
} catch (error) {
|
|
2365
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2366
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2367
|
+
continue;
|
|
2368
|
+
}
|
|
2369
|
+
throw error;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
return target;
|
|
2373
|
+
}
|
|
2374
|
+
function parseDocumentChange(input) {
|
|
2375
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2376
|
+
if (!isRecord3(change))
|
|
2377
|
+
throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
|
|
2378
|
+
if ("annotationId" in change) {
|
|
2379
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
|
|
2380
|
+
}
|
|
2381
|
+
if (typeof change["kind"] === "string") {
|
|
2382
|
+
parseResourceChange({ change, changeIndex, workspaceRoot, target });
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
const identifier = change["textDocument"];
|
|
2386
|
+
if (!isRecord3(identifier) || typeof identifier["uri"] !== "string") {
|
|
2387
|
+
throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
|
|
2388
|
+
}
|
|
2389
|
+
const version = identifier["version"];
|
|
2390
|
+
if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
|
|
2391
|
+
throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
|
|
2392
|
+
}
|
|
2393
|
+
const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
|
|
2394
|
+
if (!resolvedPath.success) {
|
|
2395
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
target.operations.push({
|
|
2399
|
+
kind: "text",
|
|
2400
|
+
changeIndex,
|
|
2401
|
+
path: resolvedPath.path,
|
|
2402
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2403
|
+
edits: parseTextEdits(change["edits"], changeIndex),
|
|
2404
|
+
version
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2409
|
+
import { dirname as dirname3, relative as relative4, resolve as resolve4 } from "node:path";
|
|
2410
|
+
|
|
2411
|
+
// ../lsp-core/src/lsp/workspace-edit-text.ts
|
|
2412
|
+
function comparePosition(left, right) {
|
|
2413
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
2414
|
+
}
|
|
2415
|
+
function positionsEqual(left, right) {
|
|
2416
|
+
return left.line === right.line && left.character === right.character;
|
|
2417
|
+
}
|
|
2418
|
+
function rangesEqual(left, right) {
|
|
2419
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
2420
|
+
}
|
|
2421
|
+
function isEmptyRange(range) {
|
|
2422
|
+
return positionsEqual(range.start, range.end);
|
|
2423
|
+
}
|
|
2424
|
+
function formatRange(range) {
|
|
2425
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
2426
|
+
}
|
|
2427
|
+
function validatePosition(position, label, context) {
|
|
2428
|
+
const { lines, changeIndex } = context;
|
|
2429
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
2430
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
2431
|
+
}
|
|
2432
|
+
if (position.line < 0 || position.character < 0) {
|
|
2433
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
2434
|
+
}
|
|
2435
|
+
const line = lines[position.line];
|
|
2436
|
+
if (line === undefined) {
|
|
2437
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
2438
|
+
}
|
|
2439
|
+
if (position.character > line.length) {
|
|
2440
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
function validateRange(range, lines, changeIndex) {
|
|
2444
|
+
const context = { lines, changeIndex };
|
|
2445
|
+
validatePosition(range.start, "start", context);
|
|
2446
|
+
validatePosition(range.end, "end", context);
|
|
2447
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
2448
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function sortAndDeduplicate(edits) {
|
|
2452
|
+
const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
|
|
2453
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
2454
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
2455
|
+
});
|
|
2456
|
+
const unique = [];
|
|
2457
|
+
for (const entry of sorted) {
|
|
2458
|
+
const previous = unique.at(-1);
|
|
2459
|
+
if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2462
|
+
unique.push(entry.edit);
|
|
2463
|
+
}
|
|
2464
|
+
return unique;
|
|
2465
|
+
}
|
|
2466
|
+
function validateNoOverlap(edits, changeIndex) {
|
|
2467
|
+
for (let index = 0;index < edits.length - 1; index += 1) {
|
|
2468
|
+
const later = edits[index];
|
|
2469
|
+
const earlier = edits[index + 1];
|
|
2470
|
+
if (later === undefined || earlier === undefined)
|
|
2471
|
+
continue;
|
|
2472
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
2473
|
+
throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
function applyNormalizedTextEdits(content, edits) {
|
|
2478
|
+
const lines = content.split(`
|
|
2479
|
+
`);
|
|
2480
|
+
for (const edit of edits) {
|
|
2481
|
+
const { start, end } = edit.range;
|
|
2482
|
+
const startLine = lines[start.line];
|
|
2483
|
+
const endLine = lines[end.line];
|
|
2484
|
+
if (startLine === undefined || endLine === undefined)
|
|
2485
|
+
continue;
|
|
2486
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
2487
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
|
|
2488
|
+
`));
|
|
2489
|
+
}
|
|
2490
|
+
return lines.join(`
|
|
2491
|
+
`);
|
|
2492
|
+
}
|
|
2493
|
+
function normalizeTextEdits(content, edits, changeIndex) {
|
|
2494
|
+
const lines = content.split(`
|
|
2495
|
+
`);
|
|
2496
|
+
for (const edit of edits) {
|
|
2497
|
+
validateRange(edit.range, lines, changeIndex);
|
|
2498
|
+
}
|
|
2499
|
+
const normalized = sortAndDeduplicate(edits);
|
|
2500
|
+
validateNoOverlap(normalized, changeIndex);
|
|
2501
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2505
|
+
function isSameOrDescendant2(candidate, parent) {
|
|
2506
|
+
const relativePath = relative4(parent, candidate);
|
|
2507
|
+
return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
|
|
2508
|
+
}
|
|
2509
|
+
function removeVirtualSubtree(virtual, path) {
|
|
2510
|
+
for (const candidate of [...virtual.keys()]) {
|
|
2511
|
+
if (isSameOrDescendant2(candidate, path))
|
|
2512
|
+
virtual.delete(candidate);
|
|
2513
|
+
}
|
|
2514
|
+
virtual.set(path, { kind: "missing" });
|
|
2515
|
+
}
|
|
2516
|
+
function moveVirtualSubtree(virtual, oldPath, newPath) {
|
|
2517
|
+
const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
|
|
2518
|
+
removeVirtualSubtree(virtual, oldPath);
|
|
2519
|
+
removeVirtualSubtree(virtual, newPath);
|
|
2520
|
+
for (const [candidate, entry] of moved) {
|
|
2521
|
+
const suffix = relative4(oldPath, candidate);
|
|
2522
|
+
virtual.set(suffix === "" ? newPath : resolve4(newPath, suffix), entry);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
function virtualDirectoryHasChildren(virtual, path) {
|
|
2526
|
+
for (const [candidate, entry] of virtual) {
|
|
2527
|
+
if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
|
|
2528
|
+
return true;
|
|
2529
|
+
}
|
|
2530
|
+
return false;
|
|
2531
|
+
}
|
|
2532
|
+
function requireVirtualParent(virtual, path, changeIndex) {
|
|
2533
|
+
if (virtual.get(dirname3(path))?.kind !== "directory") {
|
|
2534
|
+
throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
function simulateOperations(parsed, snapshots) {
|
|
2538
|
+
const virtual = new Map(snapshots);
|
|
2539
|
+
const planned = [];
|
|
2540
|
+
const failures = [];
|
|
2541
|
+
for (const operation of parsed) {
|
|
2542
|
+
try {
|
|
2543
|
+
planned.push(simulateOperation(operation, virtual));
|
|
2544
|
+
} catch (error) {
|
|
2545
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2546
|
+
failures.push({ changeIndex: operation.changeIndex, message: error.detail });
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
throw error;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
return { operations: planned, failures };
|
|
2553
|
+
}
|
|
2554
|
+
function simulateOperation(operation, virtual) {
|
|
2555
|
+
switch (operation.kind) {
|
|
2556
|
+
case "text":
|
|
2557
|
+
return simulateText(operation, virtual);
|
|
2558
|
+
case "create":
|
|
2559
|
+
return simulateCreate(operation, virtual);
|
|
2560
|
+
case "rename":
|
|
2561
|
+
return simulateRename(operation, virtual);
|
|
2562
|
+
case "delete":
|
|
2563
|
+
return simulateDelete(operation, virtual);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function rejectSymbolicLink(operation) {
|
|
2567
|
+
if (operation.followedSymbolicLink) {
|
|
2568
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
function simulateText(operation, virtual) {
|
|
2572
|
+
const entry = virtual.get(operation.path);
|
|
2573
|
+
if (entry?.kind !== "file")
|
|
2574
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
|
|
2575
|
+
const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
|
|
2576
|
+
virtual.set(operation.path, { kind: "file", content: normalized.text });
|
|
2577
|
+
return {
|
|
2578
|
+
kind: "text",
|
|
2579
|
+
changeIndex: operation.changeIndex,
|
|
2580
|
+
path: operation.path,
|
|
2581
|
+
beforeText: entry.content,
|
|
2582
|
+
afterText: normalized.text,
|
|
2583
|
+
editCount: normalized.edits.length,
|
|
2584
|
+
documentVersion: operation.version
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
function simulateCreate(operation, virtual) {
|
|
2588
|
+
rejectSymbolicLink(operation);
|
|
2589
|
+
requireVirtualParent(virtual, operation.path, operation.changeIndex);
|
|
2590
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2591
|
+
if (target.kind !== "missing") {
|
|
2592
|
+
if (operation.overwrite && target.kind === "file") {
|
|
2593
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2594
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
|
|
2595
|
+
}
|
|
2596
|
+
if (operation.ignoreIfExists)
|
|
2597
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2598
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
|
|
2599
|
+
}
|
|
2600
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2601
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
|
|
2602
|
+
}
|
|
2603
|
+
function simulateRename(operation, virtual) {
|
|
2604
|
+
rejectSymbolicLink(operation);
|
|
2605
|
+
const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
|
|
2606
|
+
if (source.kind === "missing") {
|
|
2607
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
|
|
2608
|
+
}
|
|
2609
|
+
if (operation.oldPath === operation.newPath)
|
|
2610
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2611
|
+
if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
|
|
2612
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
|
|
2613
|
+
}
|
|
2614
|
+
requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
|
|
2615
|
+
const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
|
|
2616
|
+
if (destination.kind !== "missing" && !operation.overwrite) {
|
|
2617
|
+
if (operation.ignoreIfExists)
|
|
2618
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2619
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
|
|
2620
|
+
}
|
|
2621
|
+
moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
|
|
2622
|
+
return {
|
|
2623
|
+
kind: "rename",
|
|
2624
|
+
changeIndex: operation.changeIndex,
|
|
2625
|
+
oldPath: operation.oldPath,
|
|
2626
|
+
newPath: operation.newPath,
|
|
2627
|
+
sourceKind: source.kind,
|
|
2628
|
+
replaceDestination: destination.kind !== "missing"
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
function simulateDelete(operation, virtual) {
|
|
2632
|
+
rejectSymbolicLink(operation);
|
|
2633
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2634
|
+
if (target.kind === "missing") {
|
|
2635
|
+
if (operation.ignoreIfNotExists)
|
|
2636
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2637
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
|
|
2638
|
+
}
|
|
2639
|
+
if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
|
|
2640
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
|
|
2641
|
+
}
|
|
2642
|
+
removeVirtualSubtree(virtual, operation.path);
|
|
2643
|
+
return {
|
|
2644
|
+
kind: "delete",
|
|
2645
|
+
changeIndex: operation.changeIndex,
|
|
2646
|
+
path: operation.path,
|
|
2647
|
+
targetKind: target.kind,
|
|
2648
|
+
recursive: operation.recursive
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// ../lsp-core/src/lsp/workspace-edit-snapshot.ts
|
|
2653
|
+
import { existsSync as existsSync5, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2654
|
+
import { dirname as dirname4, resolve as resolve5 } from "node:path";
|
|
2655
|
+
class WorkspaceSnapshotBuilder {
|
|
2656
|
+
workspaceRoot;
|
|
2657
|
+
snapshots = new Map;
|
|
2658
|
+
constructor(workspaceRoot) {
|
|
2659
|
+
this.workspaceRoot = workspaceRoot;
|
|
2660
|
+
}
|
|
2661
|
+
build(operations) {
|
|
2662
|
+
this.add(this.workspaceRoot, false);
|
|
2663
|
+
for (const operation of operations) {
|
|
2664
|
+
switch (operation.kind) {
|
|
2665
|
+
case "rename":
|
|
2666
|
+
this.add(operation.oldPath, true);
|
|
2667
|
+
this.add(operation.newPath, true);
|
|
2668
|
+
break;
|
|
2669
|
+
case "delete":
|
|
2670
|
+
this.add(operation.path, true);
|
|
2671
|
+
break;
|
|
2672
|
+
case "text":
|
|
2673
|
+
case "create":
|
|
2674
|
+
this.add(operation.path, false);
|
|
2675
|
+
break;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
return this.snapshots;
|
|
2679
|
+
}
|
|
2680
|
+
add(path, includeChildren) {
|
|
2681
|
+
let candidate = path;
|
|
2682
|
+
while (true) {
|
|
2683
|
+
const existing = this.snapshots.get(candidate);
|
|
2684
|
+
if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
|
|
2685
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
|
|
2686
|
+
}
|
|
2687
|
+
if (candidate === this.workspaceRoot)
|
|
2688
|
+
break;
|
|
2689
|
+
candidate = dirname4(candidate);
|
|
2690
|
+
}
|
|
2691
|
+
if (!includeChildren || !existsSync5(path) || !lstatSync3(path).isDirectory())
|
|
2692
|
+
return;
|
|
2693
|
+
for (const child of readdirSync2(path))
|
|
2694
|
+
this.add(resolve5(path, child), true);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
function snapshotOperations(operations, workspaceRoot) {
|
|
2698
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// ../lsp-core/src/lsp/workspace-edit-plan.ts
|
|
2702
|
+
class PlanPathIndex {
|
|
2703
|
+
firstChangeByPath = new Map;
|
|
2704
|
+
reportedPathByCanonical = new Map;
|
|
2705
|
+
build(operations) {
|
|
2706
|
+
for (const operation of operations) {
|
|
2707
|
+
switch (operation.kind) {
|
|
2708
|
+
case "rename":
|
|
2709
|
+
this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
|
|
2710
|
+
this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
|
|
2711
|
+
break;
|
|
2712
|
+
case "text":
|
|
2713
|
+
case "create":
|
|
2714
|
+
case "delete":
|
|
2715
|
+
this.add(operation.path, operation.reportedPath, operation.changeIndex);
|
|
2716
|
+
break;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
add(path, reportedPath2, changeIndex) {
|
|
2721
|
+
if (!this.firstChangeByPath.has(path))
|
|
2722
|
+
this.firstChangeByPath.set(path, changeIndex);
|
|
2723
|
+
if (!this.reportedPathByCanonical.has(path))
|
|
2724
|
+
this.reportedPathByCanonical.set(path, reportedPath2);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
function fingerprintWorkspaceEdit(edit, workspaceRoot) {
|
|
2728
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2729
|
+
if (!root.success)
|
|
2730
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2731
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2732
|
+
if (parsed.failures.length > 0)
|
|
2733
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2734
|
+
return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
|
|
2735
|
+
}
|
|
2736
|
+
function planWorkspaceEdit(edit, workspaceRoot) {
|
|
2737
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2738
|
+
if (!root.success)
|
|
2739
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2740
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2741
|
+
if (parsed.failures.length > 0)
|
|
2742
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2743
|
+
let snapshots;
|
|
2744
|
+
try {
|
|
2745
|
+
snapshots = snapshotOperations(parsed.operations, root.path);
|
|
2746
|
+
} catch (error) {
|
|
2747
|
+
return {
|
|
2748
|
+
success: false,
|
|
2749
|
+
result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
const simulated = simulateOperations(parsed.operations, snapshots);
|
|
2753
|
+
if (simulated.failures.length > 0)
|
|
2754
|
+
return { success: false, result: failureResult(simulated.failures) };
|
|
2755
|
+
const paths = new PlanPathIndex;
|
|
2756
|
+
paths.build(parsed.operations);
|
|
2757
|
+
const plan = {
|
|
2758
|
+
workspaceRoot: root.path,
|
|
2759
|
+
operations: simulated.operations,
|
|
2760
|
+
snapshots,
|
|
2761
|
+
firstChangeByPath: paths.firstChangeByPath,
|
|
2762
|
+
reportedPathByCanonical: paths.reportedPathByCanonical,
|
|
2763
|
+
fingerprint: canonicalFingerprint(parsed.operations)
|
|
2764
|
+
};
|
|
2765
|
+
return { success: true, plan };
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
// ../lsp-core/src/lsp/workspace-mutation-controller.ts
|
|
2769
|
+
function failure(message, failedChange, base) {
|
|
2770
|
+
return {
|
|
2771
|
+
success: false,
|
|
2772
|
+
filesModified: base?.filesModified ?? [],
|
|
2773
|
+
totalEdits: base?.totalEdits ?? 0,
|
|
2774
|
+
errors: [message],
|
|
2775
|
+
...failedChange === undefined ? {} : { failedChange },
|
|
2776
|
+
...base?.lateAbort ? { lateAbort: true } : {}
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
function responseFor(result) {
|
|
2780
|
+
if (result.success)
|
|
2781
|
+
return { applied: true };
|
|
2782
|
+
return {
|
|
2783
|
+
applied: false,
|
|
2784
|
+
failureReason: result.errors[0] ?? "workspace edit failed",
|
|
2785
|
+
...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
function isRecord4(value) {
|
|
2789
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
class WorkspaceMutationController {
|
|
2793
|
+
workspaceRoot;
|
|
2794
|
+
documents;
|
|
2795
|
+
activeLease = null;
|
|
2796
|
+
nextLeaseId = 1;
|
|
2797
|
+
io;
|
|
2798
|
+
constructor(workspaceRoot, documents) {
|
|
2799
|
+
this.workspaceRoot = workspaceRoot;
|
|
2800
|
+
this.documents = documents;
|
|
2801
|
+
}
|
|
2802
|
+
setIo(io) {
|
|
2803
|
+
this.io = io;
|
|
2804
|
+
}
|
|
2805
|
+
acquire(signal) {
|
|
2806
|
+
if (this.activeLease)
|
|
2807
|
+
return { success: false, result: failure("workspace mutation is already in progress") };
|
|
2808
|
+
if (signal?.aborted)
|
|
2809
|
+
return { success: false, result: failure("cancelled before mutating request") };
|
|
2810
|
+
const lease = {
|
|
2811
|
+
id: this.nextLeaseId,
|
|
2812
|
+
phase: "idle",
|
|
2813
|
+
...signal === undefined ? {} : { signal }
|
|
2814
|
+
};
|
|
2815
|
+
this.nextLeaseId += 1;
|
|
2816
|
+
this.activeLease = lease;
|
|
2817
|
+
return { success: true, lease };
|
|
2818
|
+
}
|
|
2819
|
+
release(lease) {
|
|
2820
|
+
if (this.activeLease?.id !== lease.id)
|
|
2821
|
+
return;
|
|
2822
|
+
this.activeLease.phase = "sealed";
|
|
2823
|
+
this.activeLease = null;
|
|
2824
|
+
}
|
|
2825
|
+
isBeforeCommit(lease) {
|
|
2826
|
+
return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
|
|
2827
|
+
}
|
|
2828
|
+
async handleApplyEdit(params) {
|
|
2829
|
+
const lease = this.activeLease;
|
|
2830
|
+
if (!lease)
|
|
2831
|
+
return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
|
|
2832
|
+
if (lease.phase !== "idle") {
|
|
2833
|
+
return {
|
|
2834
|
+
applied: false,
|
|
2835
|
+
failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
lease.phase = "applying";
|
|
2839
|
+
lease.applyCompletion = new Promise((resolve6) => {
|
|
2840
|
+
lease.resolveApply = resolve6;
|
|
2841
|
+
});
|
|
2842
|
+
const edit = isRecord4(params) ? params["edit"] : undefined;
|
|
2843
|
+
const record2 = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
|
|
2844
|
+
lease.serverApply = record2;
|
|
2845
|
+
lease.phase = "settled";
|
|
2846
|
+
lease.resolveApply?.();
|
|
2847
|
+
return responseFor(record2.result);
|
|
2848
|
+
}
|
|
2849
|
+
async reconcileRename(leaseToken, edit) {
|
|
2850
|
+
const lease = this.requireActiveLease(leaseToken);
|
|
2851
|
+
if (!lease)
|
|
2852
|
+
return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
|
|
2853
|
+
if (lease.phase === "applying")
|
|
2854
|
+
await lease.applyCompletion;
|
|
2855
|
+
if (lease.serverApply)
|
|
2856
|
+
return this.reconcileServerApply(lease.serverApply, edit);
|
|
2857
|
+
lease.phase = "sealed";
|
|
2858
|
+
if (!edit)
|
|
2859
|
+
return { edit, apply: failure("No edit provided") };
|
|
2860
|
+
const applied = await this.applyEdit(edit, lease);
|
|
2861
|
+
return { edit, apply: applied.result };
|
|
2862
|
+
}
|
|
2863
|
+
reconcileServerApply(record2, edit) {
|
|
2864
|
+
if (!edit)
|
|
2865
|
+
return { edit, apply: record2.result };
|
|
2866
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
|
|
2867
|
+
if (fingerprint.success && record2.fingerprint !== null && fingerprint.fingerprint === record2.fingerprint) {
|
|
2868
|
+
return { edit, apply: record2.result };
|
|
2869
|
+
}
|
|
2870
|
+
return {
|
|
2871
|
+
edit,
|
|
2872
|
+
apply: failure("rename result conflicts with server-applied workspace edit", 0, record2.result)
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
async applyEdit(edit, lease) {
|
|
2876
|
+
const planned = planWorkspaceEdit(edit, this.workspaceRoot);
|
|
2877
|
+
if (!planned.success)
|
|
2878
|
+
return { fingerprint: null, result: planned.result };
|
|
2879
|
+
const versionFailure = this.documents.validateVersions(planned.plan.operations);
|
|
2880
|
+
if (versionFailure) {
|
|
2881
|
+
return {
|
|
2882
|
+
fingerprint: planned.plan.fingerprint,
|
|
2883
|
+
result: failure(versionFailure.message, versionFailure.changeIndex)
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
const commit = commitWorkspaceEditPlan(planned.plan, {
|
|
2887
|
+
...lease.signal === undefined ? {} : { signal: lease.signal },
|
|
2888
|
+
...this.io === undefined ? {} : { io: this.io }
|
|
2889
|
+
});
|
|
2890
|
+
let result = commit.result;
|
|
2891
|
+
if (commit.delta.operations.length > 0) {
|
|
2892
|
+
try {
|
|
2893
|
+
await this.documents.synchronize(commit.delta);
|
|
2894
|
+
} catch (error) {
|
|
2895
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2896
|
+
result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
if (lease.signal?.aborted && !result.lateAbort)
|
|
2900
|
+
result = { ...result, lateAbort: true };
|
|
2901
|
+
return { fingerprint: planned.plan.fingerprint, result };
|
|
2902
|
+
}
|
|
2903
|
+
requireActiveLease(lease) {
|
|
2904
|
+
return this.activeLease?.id === lease.id ? this.activeLease : null;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
|
|
1203
2908
|
// ../lsp-core/src/lsp/client.ts
|
|
1204
|
-
var
|
|
1205
|
-
var
|
|
2909
|
+
var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
|
|
2910
|
+
var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1206
2911
|
|
|
1207
2912
|
class LspClient extends LspClientConnection {
|
|
1208
|
-
openedFiles = new Set;
|
|
1209
|
-
documentVersions = new Map;
|
|
1210
|
-
lastSyncedText = new Map;
|
|
1211
2913
|
diagnosticPullErrors = [];
|
|
2914
|
+
documents;
|
|
2915
|
+
workspaceMutations;
|
|
2916
|
+
diagnosticsFreshnessTimeoutMs;
|
|
2917
|
+
constructor(root, server2, options = {}) {
|
|
2918
|
+
super(root, server2, options);
|
|
2919
|
+
this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
|
|
2920
|
+
this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
|
|
2921
|
+
versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
|
|
2922
|
+
});
|
|
2923
|
+
this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
|
|
2924
|
+
this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
|
|
2925
|
+
}
|
|
1212
2926
|
getDiagnosticPullErrors() {
|
|
1213
2927
|
return this.diagnosticPullErrors;
|
|
1214
2928
|
}
|
|
1215
2929
|
async openFile(filePath) {
|
|
1216
|
-
const absPath =
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
version,
|
|
1228
|
-
text
|
|
1229
|
-
}
|
|
1230
|
-
});
|
|
1231
|
-
this.openedFiles.add(absPath);
|
|
1232
|
-
this.documentVersions.set(uri, version);
|
|
1233
|
-
this.lastSyncedText.set(uri, text);
|
|
1234
|
-
await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
const prevText = this.lastSyncedText.get(uri);
|
|
1238
|
-
if (prevText === text) {
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
|
|
1242
|
-
this.documentVersions.set(uri, nextVersion);
|
|
1243
|
-
this.lastSyncedText.set(uri, text);
|
|
1244
|
-
await this.sendNotification("textDocument/didChange", {
|
|
1245
|
-
textDocument: { uri, version: nextVersion },
|
|
1246
|
-
contentChanges: [{ text }]
|
|
1247
|
-
});
|
|
1248
|
-
await this.sendNotification("textDocument/didSave", {
|
|
1249
|
-
textDocument: { uri },
|
|
1250
|
-
text
|
|
1251
|
-
});
|
|
2930
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2931
|
+
await this.documents.openFile(absPath);
|
|
2932
|
+
}
|
|
2933
|
+
getOpenDocumentVersion(filePath) {
|
|
2934
|
+
return this.documents.getVersion(this.resolveWorkspacePath(filePath));
|
|
2935
|
+
}
|
|
2936
|
+
getStoredDiagnostics(uri) {
|
|
2937
|
+
return [...this.documents.getStoredDiagnostics(uri)];
|
|
2938
|
+
}
|
|
2939
|
+
setWorkspaceEditIo(io) {
|
|
2940
|
+
this.workspaceMutations.setIo(io);
|
|
1252
2941
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
2942
|
+
handlePublishDiagnostics(params) {
|
|
2943
|
+
super.handlePublishDiagnostics(params);
|
|
2944
|
+
this.documents.recordPublishedDiagnostics(params);
|
|
2945
|
+
}
|
|
2946
|
+
async definition(filePath, line, character, signal) {
|
|
2947
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1255
2948
|
await this.openFile(absPath);
|
|
2949
|
+
const options = signal === undefined ? {} : { signal };
|
|
1256
2950
|
return this.sendRequest("textDocument/definition", {
|
|
1257
|
-
textDocument: { uri:
|
|
2951
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1258
2952
|
position: { line: line - 1, character }
|
|
1259
|
-
});
|
|
2953
|
+
}, options);
|
|
1260
2954
|
}
|
|
1261
|
-
async references(filePath, line, character, includeDeclaration = true) {
|
|
1262
|
-
const absPath =
|
|
2955
|
+
async references(filePath, line, character, includeDeclaration = true, signal) {
|
|
2956
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1263
2957
|
await this.openFile(absPath);
|
|
2958
|
+
const options = signal === undefined ? {} : { signal };
|
|
1264
2959
|
return this.sendRequest("textDocument/references", {
|
|
1265
|
-
textDocument: { uri:
|
|
2960
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1266
2961
|
position: { line: line - 1, character },
|
|
1267
2962
|
context: { includeDeclaration }
|
|
1268
|
-
});
|
|
2963
|
+
}, options);
|
|
1269
2964
|
}
|
|
1270
|
-
async documentSymbols(filePath) {
|
|
1271
|
-
const absPath =
|
|
2965
|
+
async documentSymbols(filePath, signal) {
|
|
2966
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1272
2967
|
await this.openFile(absPath);
|
|
2968
|
+
const options = signal === undefined ? {} : { signal };
|
|
1273
2969
|
return this.sendRequest("textDocument/documentSymbol", {
|
|
1274
|
-
textDocument: { uri:
|
|
1275
|
-
});
|
|
2970
|
+
textDocument: { uri: pathToFileURL3(absPath).href }
|
|
2971
|
+
}, options);
|
|
1276
2972
|
}
|
|
1277
|
-
async workspaceSymbols(query) {
|
|
1278
|
-
|
|
2973
|
+
async workspaceSymbols(query, signal) {
|
|
2974
|
+
const options = signal === undefined ? {} : { signal };
|
|
2975
|
+
return this.sendRequest("workspace/symbol", { query }, options);
|
|
1279
2976
|
}
|
|
1280
2977
|
isUnsupportedDiagnosticPullError(error) {
|
|
1281
2978
|
if (!(error instanceof Error))
|
|
@@ -1285,42 +2982,173 @@ class LspClient extends LspClientConnection {
|
|
|
1285
2982
|
return true;
|
|
1286
2983
|
return /unsupported|not supported|method not found|unknown request/i.test(error.message);
|
|
1287
2984
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
1295
|
-
textDocument: { uri }
|
|
1296
|
-
});
|
|
1297
|
-
if (result.items) {
|
|
1298
|
-
return { items: result.items };
|
|
2985
|
+
freshnessTimeout(absPath) {
|
|
2986
|
+
return {
|
|
2987
|
+
items: [],
|
|
2988
|
+
transientError: {
|
|
2989
|
+
kind: "freshness_timeout",
|
|
2990
|
+
message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
|
|
1299
2991
|
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
|
|
2992
|
+
};
|
|
2993
|
+
}
|
|
2994
|
+
parseDiagnosticPullReport(value) {
|
|
2995
|
+
if (value.kind === "unchanged") {
|
|
2996
|
+
return {
|
|
2997
|
+
type: "unchanged",
|
|
2998
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
return {
|
|
3002
|
+
type: "full",
|
|
3003
|
+
diagnostics: value.items ?? [],
|
|
3004
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
async diagnostics(filePath, signal) {
|
|
3008
|
+
signal?.throwIfAborted();
|
|
3009
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
3010
|
+
const uri = pathToFileURL3(absPath).href;
|
|
3011
|
+
await this.openFile(absPath);
|
|
3012
|
+
const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
|
|
3013
|
+
for (;; ) {
|
|
3014
|
+
signal?.throwIfAborted();
|
|
3015
|
+
const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
|
|
3016
|
+
if (!snapshot)
|
|
3017
|
+
return this.freshnessTimeout(absPath);
|
|
3018
|
+
const push = this.documents.resolvePushDiagnostics(snapshot);
|
|
3019
|
+
if (push.status === "ready")
|
|
3020
|
+
return { items: [...push.diagnostics] };
|
|
3021
|
+
let pushFallbackOnly = !this.isDiagnosticPullSupported();
|
|
3022
|
+
if (!pushFallbackOnly) {
|
|
3023
|
+
const cached = this.documents.getPullCache(snapshot);
|
|
3024
|
+
try {
|
|
3025
|
+
const remainingMs2 = deadlineAt - Date.now();
|
|
3026
|
+
if (remainingMs2 <= 0)
|
|
3027
|
+
return this.freshnessTimeout(absPath);
|
|
3028
|
+
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
3029
|
+
textDocument: { uri },
|
|
3030
|
+
...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
|
|
3031
|
+
}, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
|
|
3032
|
+
if (!this.documents.isCurrentSnapshot(snapshot))
|
|
3033
|
+
continue;
|
|
3034
|
+
const report = this.parseDiagnosticPullReport(result);
|
|
3035
|
+
if (report.type === "full") {
|
|
3036
|
+
this.documents.recordPullDiagnostics(snapshot, {
|
|
3037
|
+
kind: "full",
|
|
3038
|
+
diagnostics: report.diagnostics,
|
|
3039
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
3040
|
+
});
|
|
3041
|
+
return { items: [...report.diagnostics] };
|
|
3042
|
+
}
|
|
3043
|
+
if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
|
|
3044
|
+
return { items: [...cached.diagnostics] };
|
|
3045
|
+
}
|
|
3046
|
+
} catch (error) {
|
|
3047
|
+
if (this.isUnsupportedDiagnosticPullError(error)) {
|
|
3048
|
+
this.setDiagnosticPullSupported(false);
|
|
3049
|
+
pushFallbackOnly = true;
|
|
3050
|
+
} else if (error instanceof LspRequestTimeoutError) {
|
|
3051
|
+
pushFallbackOnly = true;
|
|
3052
|
+
} else {
|
|
3053
|
+
this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
3054
|
+
throw error;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
1303
3057
|
}
|
|
3058
|
+
if (!pushFallbackOnly)
|
|
3059
|
+
continue;
|
|
3060
|
+
const remainingMs = deadlineAt - Date.now();
|
|
3061
|
+
if (remainingMs <= 0)
|
|
3062
|
+
return this.freshnessTimeout(absPath);
|
|
3063
|
+
const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
|
|
3064
|
+
await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
|
|
1304
3065
|
}
|
|
1305
|
-
return { items: this.getStoredDiagnostics(uri) };
|
|
1306
3066
|
}
|
|
1307
|
-
async prepareRename(filePath, line, character) {
|
|
1308
|
-
const absPath =
|
|
3067
|
+
async prepareRename(filePath, line, character, signal) {
|
|
3068
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1309
3069
|
await this.openFile(absPath);
|
|
3070
|
+
const options = signal === undefined ? {} : { signal };
|
|
1310
3071
|
return this.sendRequest("textDocument/prepareRename", {
|
|
1311
|
-
textDocument: { uri:
|
|
3072
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1312
3073
|
position: { line: line - 1, character }
|
|
1313
|
-
});
|
|
3074
|
+
}, options);
|
|
1314
3075
|
}
|
|
1315
|
-
async rename(filePath, line, character, newName) {
|
|
1316
|
-
const absPath =
|
|
3076
|
+
async rename(filePath, line, character, newName, signal) {
|
|
3077
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1317
3078
|
await this.openFile(absPath);
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
3079
|
+
const acquired = this.workspaceMutations.acquire(signal);
|
|
3080
|
+
if (!acquired.success)
|
|
3081
|
+
return { edit: null, apply: acquired.result };
|
|
3082
|
+
const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
|
|
3083
|
+
try {
|
|
3084
|
+
const renameParams = {
|
|
3085
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
3086
|
+
position: { line: line - 1, character },
|
|
3087
|
+
newName
|
|
3088
|
+
};
|
|
3089
|
+
const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
|
|
3090
|
+
signal: preCommitSignal.signal
|
|
3091
|
+
});
|
|
3092
|
+
return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
|
|
3093
|
+
} finally {
|
|
3094
|
+
preCommitSignal?.dispose();
|
|
3095
|
+
this.workspaceMutations.release(acquired.lease);
|
|
3096
|
+
}
|
|
1323
3097
|
}
|
|
3098
|
+
resolveWorkspacePath(filePath) {
|
|
3099
|
+
return resolve6(this.root, filePath);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
function waitForDiagnosticsActivity(wait, signal) {
|
|
3103
|
+
if (!signal)
|
|
3104
|
+
return wait;
|
|
3105
|
+
if (signal.aborted)
|
|
3106
|
+
return Promise.reject(abortError2(signal));
|
|
3107
|
+
return new Promise((resolve7, reject) => {
|
|
3108
|
+
const onAbort = () => {
|
|
3109
|
+
signal.removeEventListener("abort", onAbort);
|
|
3110
|
+
reject(abortError2(signal));
|
|
3111
|
+
};
|
|
3112
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3113
|
+
wait.then(() => {
|
|
3114
|
+
signal.removeEventListener("abort", onAbort);
|
|
3115
|
+
resolve7();
|
|
3116
|
+
}, (error) => {
|
|
3117
|
+
signal.removeEventListener("abort", onAbort);
|
|
3118
|
+
reject(error);
|
|
3119
|
+
});
|
|
3120
|
+
});
|
|
3121
|
+
}
|
|
3122
|
+
function createPreCommitAbortSignal(source, isBeforeCommit) {
|
|
3123
|
+
if (!source)
|
|
3124
|
+
return;
|
|
3125
|
+
const controller = new AbortController;
|
|
3126
|
+
const onAbort = () => {
|
|
3127
|
+
if (isBeforeCommit() && !controller.signal.aborted)
|
|
3128
|
+
controller.abort(preCommitAbortReason(source));
|
|
3129
|
+
};
|
|
3130
|
+
if (source.aborted)
|
|
3131
|
+
onAbort();
|
|
3132
|
+
else
|
|
3133
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
3134
|
+
return {
|
|
3135
|
+
signal: controller.signal,
|
|
3136
|
+
dispose: () => source.removeEventListener("abort", onAbort)
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
function preCommitAbortReason(source) {
|
|
3140
|
+
const reason = source.reason;
|
|
3141
|
+
if (reason instanceof Error && reason.name !== "AbortError")
|
|
3142
|
+
return reason;
|
|
3143
|
+
return new Error("LSP request cancelled before workspace edit commit");
|
|
3144
|
+
}
|
|
3145
|
+
function abortError2(signal) {
|
|
3146
|
+
const reason = signal.reason;
|
|
3147
|
+
if (reason instanceof Error)
|
|
3148
|
+
return reason;
|
|
3149
|
+
const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
|
|
3150
|
+
error.name = "AbortError";
|
|
3151
|
+
return error;
|
|
1324
3152
|
}
|
|
1325
3153
|
|
|
1326
3154
|
// ../lsp-core/src/lsp/process-signal-cleanup.ts
|
|
@@ -1352,7 +3180,7 @@ async function stopClientBestEffort(client) {
|
|
|
1352
3180
|
function awaitWithSignal(promise, signal) {
|
|
1353
3181
|
if (!signal)
|
|
1354
3182
|
return promise;
|
|
1355
|
-
return new Promise((
|
|
3183
|
+
return new Promise((resolve7, reject) => {
|
|
1356
3184
|
let settled = false;
|
|
1357
3185
|
const onAbort = () => {
|
|
1358
3186
|
if (settled)
|
|
@@ -1370,7 +3198,7 @@ function awaitWithSignal(promise, signal) {
|
|
|
1370
3198
|
return;
|
|
1371
3199
|
settled = true;
|
|
1372
3200
|
signal.removeEventListener("abort", onAbort);
|
|
1373
|
-
|
|
3201
|
+
resolve7(value);
|
|
1374
3202
|
}, (err) => {
|
|
1375
3203
|
if (settled)
|
|
1376
3204
|
return;
|
|
@@ -1624,21 +3452,17 @@ async function disposeDefaultLspManager() {
|
|
|
1624
3452
|
}
|
|
1625
3453
|
|
|
1626
3454
|
// ../lsp-core/src/lsp/server-install-state.ts
|
|
1627
|
-
import { existsSync as
|
|
1628
|
-
import {
|
|
1629
|
-
import { dirname, isAbsolute, join as join2 } from "node:path";
|
|
3455
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3456
|
+
import { dirname as dirname5 } from "node:path";
|
|
1630
3457
|
function getInstallDecisionsPath() {
|
|
1631
|
-
|
|
1632
|
-
if (!override)
|
|
1633
|
-
return join2(homedir(), ".codex", "lsp-install-decisions.json");
|
|
1634
|
-
return isAbsolute(override) ? override : join2(homedir(), override);
|
|
3458
|
+
return lspRequestContext().installDecisionsPath;
|
|
1635
3459
|
}
|
|
1636
3460
|
function loadInstallDecisions() {
|
|
1637
3461
|
const path = getInstallDecisionsPath();
|
|
1638
|
-
if (!
|
|
3462
|
+
if (!existsSync6(path))
|
|
1639
3463
|
return {};
|
|
1640
3464
|
try {
|
|
1641
|
-
const parsed = JSON.parse(
|
|
3465
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1642
3466
|
return isInstallDecisions(parsed) ? parsed : {};
|
|
1643
3467
|
} catch {
|
|
1644
3468
|
return {};
|
|
@@ -1657,28 +3481,26 @@ function isInstallDecision(value) {
|
|
|
1657
3481
|
}
|
|
1658
3482
|
function writeInstallDecisions(decisions) {
|
|
1659
3483
|
const path = getInstallDecisionsPath();
|
|
1660
|
-
mkdirSync(
|
|
3484
|
+
mkdirSync(dirname5(path), { recursive: true });
|
|
1661
3485
|
const tmpPath = `${path}.tmp`;
|
|
1662
|
-
|
|
3486
|
+
writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
|
|
1663
3487
|
`, "utf8");
|
|
1664
|
-
|
|
3488
|
+
renameSync2(tmpPath, path);
|
|
1665
3489
|
}
|
|
1666
3490
|
function isInstallDecisions(value) {
|
|
1667
|
-
return
|
|
3491
|
+
return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
|
|
1668
3492
|
}
|
|
1669
3493
|
function isInstallDecisionRecord(value) {
|
|
1670
|
-
if (!
|
|
3494
|
+
if (!isRecord5(value))
|
|
1671
3495
|
return false;
|
|
1672
3496
|
return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
|
|
1673
3497
|
}
|
|
1674
|
-
function
|
|
3498
|
+
function isRecord5(value) {
|
|
1675
3499
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1676
3500
|
}
|
|
1677
3501
|
|
|
1678
3502
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1679
|
-
import { existsSync as
|
|
1680
|
-
import { homedir as homedir2 } from "node:os";
|
|
1681
|
-
import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
3503
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
1682
3504
|
|
|
1683
3505
|
// ../lsp-core/src/lsp/server-definitions.ts
|
|
1684
3506
|
var LSP_INSTALL_HINTS = {
|
|
@@ -1828,27 +3650,17 @@ var BUILTIN_SERVERS = {
|
|
|
1828
3650
|
};
|
|
1829
3651
|
|
|
1830
3652
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1831
|
-
function resolveProjectConfigPath(path) {
|
|
1832
|
-
return isAbsolute2(path) ? path : join3(contextCwd(), path);
|
|
1833
|
-
}
|
|
1834
3653
|
function getProjectConfigPaths() {
|
|
1835
|
-
|
|
1836
|
-
if (projectOverride) {
|
|
1837
|
-
return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
|
|
1838
|
-
}
|
|
1839
|
-
return [join3(contextCwd(), ".codex", "lsp-client.json")];
|
|
3654
|
+
return lspRequestContext().projectConfigPaths;
|
|
1840
3655
|
}
|
|
1841
3656
|
function getUserConfigPath() {
|
|
1842
|
-
|
|
1843
|
-
if (!userOverride)
|
|
1844
|
-
return join3(homedir2(), ".codex", "lsp-client.json");
|
|
1845
|
-
return isAbsolute2(userOverride) ? userOverride : join3(homedir2(), userOverride);
|
|
3657
|
+
return lspRequestContext().userConfigPath;
|
|
1846
3658
|
}
|
|
1847
3659
|
function loadJsonFile(path) {
|
|
1848
|
-
if (!
|
|
3660
|
+
if (!existsSync7(path))
|
|
1849
3661
|
return null;
|
|
1850
3662
|
try {
|
|
1851
|
-
const parsed = JSON.parse(
|
|
3663
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1852
3664
|
return isConfigJson(parsed) ? parsed : null;
|
|
1853
3665
|
} catch {
|
|
1854
3666
|
return null;
|
|
@@ -1987,16 +3799,16 @@ function applyOptionalServerFields(server2, entry) {
|
|
|
1987
3799
|
}
|
|
1988
3800
|
}
|
|
1989
3801
|
function isConfigJson(value) {
|
|
1990
|
-
if (!
|
|
3802
|
+
if (!isRecord6(value))
|
|
1991
3803
|
return false;
|
|
1992
3804
|
const lsp = value["lsp"];
|
|
1993
|
-
return lsp === undefined ||
|
|
3805
|
+
return lsp === undefined || isRecord6(lsp);
|
|
1994
3806
|
}
|
|
1995
3807
|
function parseLspEntry(value) {
|
|
1996
3808
|
return isLspEntry(value) ? value : null;
|
|
1997
3809
|
}
|
|
1998
3810
|
function isLspEntry(value) {
|
|
1999
|
-
if (!
|
|
3811
|
+
if (!isRecord6(value))
|
|
2000
3812
|
return false;
|
|
2001
3813
|
const disabled = value["disabled"];
|
|
2002
3814
|
const command = value["command"];
|
|
@@ -2004,15 +3816,15 @@ function isLspEntry(value) {
|
|
|
2004
3816
|
const priority = value["priority"];
|
|
2005
3817
|
const env = value["env"];
|
|
2006
3818
|
const initialization = value["initialization"];
|
|
2007
|
-
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 ||
|
|
3819
|
+
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));
|
|
2008
3820
|
}
|
|
2009
3821
|
function isStringArray(value) {
|
|
2010
3822
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2011
3823
|
}
|
|
2012
3824
|
function isStringRecord(value) {
|
|
2013
|
-
return
|
|
3825
|
+
return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
|
|
2014
3826
|
}
|
|
2015
|
-
function
|
|
3827
|
+
function isRecord6(value) {
|
|
2016
3828
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2017
3829
|
}
|
|
2018
3830
|
function getDisabledServerIds() {
|
|
@@ -2033,8 +3845,8 @@ function getDisabledServerIds() {
|
|
|
2033
3845
|
}
|
|
2034
3846
|
|
|
2035
3847
|
// ../lsp-core/src/lsp/server-installation.ts
|
|
2036
|
-
import { existsSync as
|
|
2037
|
-
import { delimiter as delimiter3, join as
|
|
3848
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
3849
|
+
import { delimiter as delimiter3, join as join3 } from "node:path";
|
|
2038
3850
|
function isServerInstalled(command, _workingDirectory) {
|
|
2039
3851
|
if (command.length === 0)
|
|
2040
3852
|
return false;
|
|
@@ -2042,7 +3854,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2042
3854
|
if (!cmd)
|
|
2043
3855
|
return false;
|
|
2044
3856
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
2045
|
-
if (
|
|
3857
|
+
if (existsSync8(cmd))
|
|
2046
3858
|
return true;
|
|
2047
3859
|
}
|
|
2048
3860
|
const isWindows = process.platform === "win32";
|
|
@@ -2063,7 +3875,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2063
3875
|
const paths = pathEnv.split(delimiter3);
|
|
2064
3876
|
for (const p of paths) {
|
|
2065
3877
|
for (const suffix of exts) {
|
|
2066
|
-
if (
|
|
3878
|
+
if (existsSync8(join3(p, cmd + suffix))) {
|
|
2067
3879
|
return true;
|
|
2068
3880
|
}
|
|
2069
3881
|
}
|
|
@@ -2162,39 +3974,50 @@ function getAllServers() {
|
|
|
2162
3974
|
var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
2163
3975
|
function isDirectoryPath(filePath) {
|
|
2164
3976
|
try {
|
|
2165
|
-
return
|
|
3977
|
+
return statSync3(filePath).isDirectory();
|
|
2166
3978
|
} catch {
|
|
2167
3979
|
return false;
|
|
2168
3980
|
}
|
|
2169
3981
|
}
|
|
2170
3982
|
function findWorkspaceRoot(filePath) {
|
|
2171
|
-
const abs =
|
|
3983
|
+
const abs = resolvePathInsideContext(filePath);
|
|
2172
3984
|
let dir = abs;
|
|
2173
3985
|
if (!isDirectoryPath(dir)) {
|
|
2174
|
-
dir =
|
|
3986
|
+
dir = dirname6(dir);
|
|
2175
3987
|
}
|
|
2176
3988
|
let prevDir = "";
|
|
2177
3989
|
while (dir !== prevDir) {
|
|
2178
3990
|
for (const marker of WORKSPACE_MARKERS) {
|
|
2179
|
-
if (
|
|
3991
|
+
if (existsSync9(join4(dir, marker))) {
|
|
2180
3992
|
return dir;
|
|
2181
3993
|
}
|
|
2182
3994
|
}
|
|
2183
3995
|
prevDir = dir;
|
|
2184
|
-
dir =
|
|
3996
|
+
dir = dirname6(dir);
|
|
3997
|
+
}
|
|
3998
|
+
return dirname6(abs);
|
|
3999
|
+
}
|
|
4000
|
+
function resolvePathInsideContext(filePath) {
|
|
4001
|
+
const cwd = contextCwd();
|
|
4002
|
+
const abs = resolve7(cwd, filePath);
|
|
4003
|
+
const canonical = canonicalizeExistingOrNearestAncestor(abs);
|
|
4004
|
+
if (!isPathInside(cwd, canonical)) {
|
|
4005
|
+
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
2185
4006
|
}
|
|
2186
|
-
return
|
|
4007
|
+
return canonical;
|
|
2187
4008
|
}
|
|
2188
4009
|
function formatServerLookupError(result) {
|
|
2189
4010
|
if (result.status === "not_installed") {
|
|
2190
4011
|
return formatNotInstalled(result);
|
|
2191
4012
|
}
|
|
4013
|
+
const context = lspRequestContext();
|
|
4014
|
+
const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
|
|
2192
4015
|
return [
|
|
2193
4016
|
`No LSP server configured for extension: ${result.extension}`,
|
|
2194
4017
|
"",
|
|
2195
4018
|
`Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
|
|
2196
4019
|
"",
|
|
2197
|
-
|
|
4020
|
+
`Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
|
|
2198
4021
|
" {",
|
|
2199
4022
|
' "lsp": {',
|
|
2200
4023
|
' "my-server": {',
|
|
@@ -2210,6 +4033,7 @@ function formatNotInstalled(result) {
|
|
|
2210
4033
|
const { server: server2, installHint } = result;
|
|
2211
4034
|
const extensions = server2.extensions.join(", ");
|
|
2212
4035
|
const decision = loadInstallDecision(server2.id)?.decision;
|
|
4036
|
+
const context = lspRequestContext();
|
|
2213
4037
|
if (decision === "declined") {
|
|
2214
4038
|
return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
|
|
2215
4039
|
}
|
|
@@ -2225,6 +4049,17 @@ function formatNotInstalled(result) {
|
|
|
2225
4049
|
"The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
|
|
2226
4050
|
` ${installHint}`
|
|
2227
4051
|
].join(`
|
|
4052
|
+
`);
|
|
4053
|
+
}
|
|
4054
|
+
if (!context.capabilities.installDecisionTool) {
|
|
4055
|
+
return [
|
|
4056
|
+
...header,
|
|
4057
|
+
"To install, run:",
|
|
4058
|
+
` ${installHint}`,
|
|
4059
|
+
"",
|
|
4060
|
+
"ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
|
|
4061
|
+
"Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
|
|
4062
|
+
].join(`
|
|
2228
4063
|
`);
|
|
2229
4064
|
}
|
|
2230
4065
|
return [
|
|
@@ -2236,6 +4071,7 @@ function formatNotInstalled(result) {
|
|
|
2236
4071
|
"- If the user agrees: run the install command above, then retry this tool.",
|
|
2237
4072
|
"- If the user declines, OR has NOT explicitly asked for LSP installation:",
|
|
2238
4073
|
` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
|
|
4074
|
+
` which writes to ${context.installDecisionsPath},`,
|
|
2239
4075
|
" then ignore this message and proceed WITHOUT LSP."
|
|
2240
4076
|
].join(`
|
|
2241
4077
|
`);
|
|
@@ -2249,14 +4085,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
|
|
|
2249
4085
|
"prepareRename"
|
|
2250
4086
|
]);
|
|
2251
4087
|
async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
2252
|
-
const absPath =
|
|
4088
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2253
4089
|
if (isDirectoryPath(absPath)) {
|
|
2254
4090
|
throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
|
|
2255
4091
|
}
|
|
2256
4092
|
const ext = effectiveExtension(absPath);
|
|
2257
4093
|
const result = findServerForExtension(ext);
|
|
2258
4094
|
if (result.status !== "found") {
|
|
2259
|
-
throw new LspServerLookupError(formatServerLookupError(result));
|
|
4095
|
+
throw new LspServerLookupError(formatServerLookupError(result), result);
|
|
2260
4096
|
}
|
|
2261
4097
|
const server2 = result.server;
|
|
2262
4098
|
const root = findWorkspaceRoot(absPath);
|
|
@@ -2284,11 +4120,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
|
2284
4120
|
}
|
|
2285
4121
|
|
|
2286
4122
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2287
|
-
import { existsSync as
|
|
2288
|
-
import { join as
|
|
4123
|
+
import { existsSync as existsSync10, lstatSync as lstatSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
4124
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
2289
4125
|
|
|
2290
4126
|
// ../lsp-core/src/lsp/formatters.ts
|
|
2291
|
-
import { fileURLToPath } from "node:url";
|
|
4127
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2292
4128
|
var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
2293
4129
|
error: 1,
|
|
2294
4130
|
warning: 2,
|
|
@@ -2296,7 +4132,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
|
2296
4132
|
hint: 4
|
|
2297
4133
|
};
|
|
2298
4134
|
function uriToPath(uri) {
|
|
2299
|
-
return
|
|
4135
|
+
return fileURLToPath2(uri);
|
|
2300
4136
|
}
|
|
2301
4137
|
function formatLocation(loc) {
|
|
2302
4138
|
if ("targetUri" in loc) {
|
|
@@ -2382,6 +4218,9 @@ function formatApplyResult(result) {
|
|
|
2382
4218
|
for (const file of result.filesModified) {
|
|
2383
4219
|
lines.push(` - ${file}`);
|
|
2384
4220
|
}
|
|
4221
|
+
if (result.lateAbort) {
|
|
4222
|
+
lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
|
|
4223
|
+
}
|
|
2385
4224
|
} else {
|
|
2386
4225
|
lines.push("Failed to apply some changes:");
|
|
2387
4226
|
for (const err of result.errors) {
|
|
@@ -2397,6 +4236,7 @@ function formatApplyResult(result) {
|
|
|
2397
4236
|
|
|
2398
4237
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2399
4238
|
var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
4239
|
+
var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
|
|
2400
4240
|
function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
2401
4241
|
const files = [];
|
|
2402
4242
|
function walk(currentDir) {
|
|
@@ -2404,17 +4244,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2404
4244
|
return;
|
|
2405
4245
|
let entries = [];
|
|
2406
4246
|
try {
|
|
2407
|
-
entries =
|
|
4247
|
+
entries = readdirSync3(currentDir);
|
|
2408
4248
|
} catch {
|
|
2409
4249
|
return;
|
|
2410
4250
|
}
|
|
2411
4251
|
for (const entry of entries) {
|
|
2412
4252
|
if (files.length >= maxFiles)
|
|
2413
4253
|
return;
|
|
2414
|
-
const fullPath =
|
|
4254
|
+
const fullPath = join5(currentDir, entry);
|
|
2415
4255
|
let stat;
|
|
2416
4256
|
try {
|
|
2417
|
-
stat =
|
|
4257
|
+
stat = lstatSync4(fullPath);
|
|
2418
4258
|
} catch {
|
|
2419
4259
|
continue;
|
|
2420
4260
|
}
|
|
@@ -2432,52 +4272,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2432
4272
|
walk(dir);
|
|
2433
4273
|
return files;
|
|
2434
4274
|
}
|
|
2435
|
-
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
|
|
4275
|
+
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
|
|
2436
4276
|
if (!extension.startsWith(".")) {
|
|
2437
4277
|
throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
|
|
2438
4278
|
}
|
|
2439
|
-
const absDir =
|
|
2440
|
-
if (!
|
|
4279
|
+
const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
|
|
4280
|
+
if (!existsSync10(absDir)) {
|
|
2441
4281
|
throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
|
|
2442
4282
|
}
|
|
2443
|
-
const serverResult = findServerForExtension(extension);
|
|
4283
|
+
const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
|
|
2444
4284
|
if (serverResult.status !== "found") {
|
|
2445
4285
|
throw new LspServerLookupError(formatServerLookupError(serverResult));
|
|
2446
4286
|
}
|
|
2447
4287
|
const server2 = serverResult.server;
|
|
2448
|
-
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
|
|
4288
|
+
const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
|
|
2449
4289
|
const wasCapped = allFiles.length > maxFiles;
|
|
2450
4290
|
const filesToProcess = allFiles.slice(0, maxFiles);
|
|
2451
4291
|
if (filesToProcess.length === 0) {
|
|
2452
|
-
|
|
4292
|
+
const output = [
|
|
2453
4293
|
`Directory: ${absDir}`,
|
|
2454
4294
|
`Extension: ${extension}`,
|
|
2455
4295
|
"Files scanned: 0",
|
|
2456
4296
|
`No files found with extension "${extension}".`
|
|
2457
4297
|
].join(`
|
|
2458
4298
|
`);
|
|
4299
|
+
return { output, totalDiagnostics: 0, fileFailures: [] };
|
|
2459
4300
|
}
|
|
2460
|
-
const root = findWorkspaceRoot(absDir);
|
|
2461
|
-
const manager = getLspManager();
|
|
4301
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
|
|
4302
|
+
const manager = options.manager ?? getLspManager();
|
|
2462
4303
|
const allDiagnostics = [];
|
|
2463
4304
|
const fileErrors = [];
|
|
2464
|
-
const
|
|
4305
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
|
|
4306
|
+
options.signal?.throwIfAborted();
|
|
4307
|
+
const client = await manager.getClient(root, server2, options.signal);
|
|
2465
4308
|
try {
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
file,
|
|
2477
|
-
|
|
2478
|
-
|
|
4309
|
+
let nextIndex = 0;
|
|
4310
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
|
|
4311
|
+
for (;; ) {
|
|
4312
|
+
if (options.signal?.aborted)
|
|
4313
|
+
return;
|
|
4314
|
+
const file = filesToProcess[nextIndex];
|
|
4315
|
+
nextIndex += 1;
|
|
4316
|
+
if (file === undefined)
|
|
4317
|
+
return;
|
|
4318
|
+
try {
|
|
4319
|
+
const result = await client.diagnostics(file, options.signal);
|
|
4320
|
+
const filtered = filterDiagnosticsBySeverity(result.items, severity);
|
|
4321
|
+
allDiagnostics.push(...filtered.map((diagnostic) => ({
|
|
4322
|
+
filePath: file,
|
|
4323
|
+
diagnostic
|
|
4324
|
+
})));
|
|
4325
|
+
} catch (e) {
|
|
4326
|
+
fileErrors.push({
|
|
4327
|
+
file,
|
|
4328
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4329
|
+
});
|
|
4330
|
+
}
|
|
2479
4331
|
}
|
|
2480
|
-
}
|
|
4332
|
+
});
|
|
4333
|
+
await Promise.all(workers);
|
|
2481
4334
|
} finally {
|
|
2482
4335
|
manager.releaseClient(root, server2.id);
|
|
2483
4336
|
}
|
|
@@ -2505,13 +4358,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
|
|
|
2505
4358
|
lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
2506
4359
|
}
|
|
2507
4360
|
}
|
|
2508
|
-
return lines.join(`
|
|
2509
|
-
`);
|
|
4361
|
+
return { output: lines.join(`
|
|
4362
|
+
`), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
|
|
2510
4363
|
}
|
|
2511
4364
|
|
|
2512
4365
|
// ../lsp-core/src/lsp/infer-extension.ts
|
|
2513
|
-
import { lstatSync as
|
|
2514
|
-
import { join as
|
|
4366
|
+
import { lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
|
|
4367
|
+
import { join as join6 } from "node:path";
|
|
2515
4368
|
var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
2516
4369
|
var MAX_SCAN_ENTRIES = 500;
|
|
2517
4370
|
function inferExtensionFromDirectory(directory) {
|
|
@@ -2522,17 +4375,17 @@ function inferExtensionFromDirectory(directory) {
|
|
|
2522
4375
|
return;
|
|
2523
4376
|
let entries;
|
|
2524
4377
|
try {
|
|
2525
|
-
entries =
|
|
4378
|
+
entries = readdirSync4(dir);
|
|
2526
4379
|
} catch {
|
|
2527
4380
|
return;
|
|
2528
4381
|
}
|
|
2529
4382
|
for (const entry of entries) {
|
|
2530
4383
|
if (scanned >= MAX_SCAN_ENTRIES)
|
|
2531
4384
|
return;
|
|
2532
|
-
const fullPath =
|
|
4385
|
+
const fullPath = join6(dir, entry);
|
|
2533
4386
|
let stat;
|
|
2534
4387
|
try {
|
|
2535
|
-
stat =
|
|
4388
|
+
stat = lstatSync5(fullPath);
|
|
2536
4389
|
} catch {
|
|
2537
4390
|
continue;
|
|
2538
4391
|
}
|
|
@@ -2606,13 +4459,48 @@ function missingDependencyResult(error, details) {
|
|
|
2606
4459
|
details: {
|
|
2607
4460
|
...details,
|
|
2608
4461
|
error: message,
|
|
2609
|
-
errorKind: "missing_dependency"
|
|
4462
|
+
errorKind: "missing_dependency",
|
|
4463
|
+
...availabilityDetails(error)
|
|
2610
4464
|
}
|
|
2611
4465
|
};
|
|
2612
4466
|
}
|
|
4467
|
+
function availabilityDetails(error) {
|
|
4468
|
+
const availability = missingDependencyAvailability(error);
|
|
4469
|
+
return availability === null ? {} : { availability };
|
|
4470
|
+
}
|
|
4471
|
+
function missingDependencyAvailability(error) {
|
|
4472
|
+
if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
|
|
4473
|
+
return null;
|
|
4474
|
+
const context = lspRequestContext();
|
|
4475
|
+
switch (error.lookup.status) {
|
|
4476
|
+
case "not_configured":
|
|
4477
|
+
return {
|
|
4478
|
+
kind: "not_configured",
|
|
4479
|
+
extension: error.lookup.extension,
|
|
4480
|
+
availableServers: [...error.lookup.availableServers],
|
|
4481
|
+
projectConfigPaths: [...context.projectConfigPaths],
|
|
4482
|
+
userConfigPath: context.userConfigPath,
|
|
4483
|
+
installDecisionTool: context.capabilities.installDecisionTool
|
|
4484
|
+
};
|
|
4485
|
+
case "not_installed":
|
|
4486
|
+
return {
|
|
4487
|
+
kind: "not_installed",
|
|
4488
|
+
serverId: error.lookup.server.id,
|
|
4489
|
+
command: [...error.lookup.server.command],
|
|
4490
|
+
extensions: [...error.lookup.server.extensions],
|
|
4491
|
+
installHint: error.lookup.installHint,
|
|
4492
|
+
installDecisionTool: context.capabilities.installDecisionTool,
|
|
4493
|
+
installDecisionsPath: context.installDecisionsPath
|
|
4494
|
+
};
|
|
4495
|
+
default: {
|
|
4496
|
+
const exhaustive = error.lookup;
|
|
4497
|
+
return exhaustive;
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
2613
4501
|
|
|
2614
4502
|
// ../lsp-core/src/tools/parameters.ts
|
|
2615
|
-
function
|
|
4503
|
+
function isRecord7(value) {
|
|
2616
4504
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2617
4505
|
}
|
|
2618
4506
|
function requireString(params, key) {
|
|
@@ -2669,7 +4557,7 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2669
4557
|
const filePath = requireString(params, "filePath");
|
|
2670
4558
|
const severity = severityFilter(params);
|
|
2671
4559
|
try {
|
|
2672
|
-
const absPath =
|
|
4560
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2673
4561
|
if (isDirectoryPath(absPath)) {
|
|
2674
4562
|
const extension = inferExtensionFromDirectory(absPath);
|
|
2675
4563
|
if (!extension) {
|
|
@@ -2686,18 +4574,33 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2686
4574
|
};
|
|
2687
4575
|
return text(message, details3);
|
|
2688
4576
|
}
|
|
2689
|
-
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
|
|
4577
|
+
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
|
|
2690
4578
|
const details2 = {
|
|
2691
4579
|
filePath,
|
|
2692
4580
|
severity,
|
|
2693
4581
|
mode: "directory",
|
|
2694
4582
|
diagnostics: [],
|
|
4583
|
+
totalDiagnostics: output2.totalDiagnostics,
|
|
4584
|
+
truncated: false,
|
|
4585
|
+
fileFailures: [...output2.fileFailures]
|
|
4586
|
+
};
|
|
4587
|
+
return text(output2.output, details2);
|
|
4588
|
+
}
|
|
4589
|
+
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
|
|
4590
|
+
if (result.transientError) {
|
|
4591
|
+
const message = result.transientError.message;
|
|
4592
|
+
const details2 = {
|
|
4593
|
+
filePath,
|
|
4594
|
+
severity,
|
|
4595
|
+
mode: "file",
|
|
4596
|
+
diagnostics: [],
|
|
2695
4597
|
totalDiagnostics: 0,
|
|
2696
|
-
truncated: false
|
|
4598
|
+
truncated: false,
|
|
4599
|
+
error: message,
|
|
4600
|
+
errorKind: result.transientError.kind
|
|
2697
4601
|
};
|
|
2698
|
-
return text(
|
|
4602
|
+
return text(message, details2, true);
|
|
2699
4603
|
}
|
|
2700
|
-
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
|
|
2701
4604
|
const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
|
|
2702
4605
|
const total = diagnostics.length;
|
|
2703
4606
|
const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
|
|
@@ -2758,7 +4661,7 @@ async function executeLspGotoDefinition(params, signal) {
|
|
|
2758
4661
|
const line = requireNumber(params, "line");
|
|
2759
4662
|
const character = requireNumber(params, "character");
|
|
2760
4663
|
try {
|
|
2761
|
-
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
|
|
4664
|
+
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
|
|
2762
4665
|
const locations = !result ? [] : Array.isArray(result) ? result : [result];
|
|
2763
4666
|
const details = { filePath, line, character, locations };
|
|
2764
4667
|
if (locations.length === 0)
|
|
@@ -2783,7 +4686,7 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2783
4686
|
const character = requireNumber(params, "character");
|
|
2784
4687
|
const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
|
|
2785
4688
|
try {
|
|
2786
|
-
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
|
|
4689
|
+
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
|
|
2787
4690
|
const references = Array.isArray(result) ? result : [];
|
|
2788
4691
|
const total = references.length;
|
|
2789
4692
|
const truncated = total > DEFAULT_MAX_REFERENCES;
|
|
@@ -2819,181 +4722,13 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2819
4722
|
}
|
|
2820
4723
|
}
|
|
2821
4724
|
|
|
2822
|
-
// ../lsp-core/src/lsp/workspace-edit.ts
|
|
2823
|
-
import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2824
|
-
import { dirname as dirname3, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
|
|
2825
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2826
|
-
function errorMessage2(error) {
|
|
2827
|
-
return error instanceof Error ? error.message : String(error);
|
|
2828
|
-
}
|
|
2829
|
-
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
2830
|
-
const relativePath = relative(workspaceRoot, filePath);
|
|
2831
|
-
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
|
|
2832
|
-
}
|
|
2833
|
-
function realpathForValidation(filePath) {
|
|
2834
|
-
if (existsSync7(filePath))
|
|
2835
|
-
return realpathSync(filePath);
|
|
2836
|
-
const parent = dirname3(filePath);
|
|
2837
|
-
return resolve5(realpathSync(parent), relative(parent, filePath));
|
|
2838
|
-
}
|
|
2839
|
-
function uriToWorkspacePath(uri, workspaceRoot) {
|
|
2840
|
-
let filePath;
|
|
2841
|
-
try {
|
|
2842
|
-
filePath = fileURLToPath2(uri);
|
|
2843
|
-
} catch (error) {
|
|
2844
|
-
return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
|
|
2845
|
-
}
|
|
2846
|
-
let validatedPath;
|
|
2847
|
-
try {
|
|
2848
|
-
validatedPath = realpathForValidation(filePath);
|
|
2849
|
-
} catch (error) {
|
|
2850
|
-
return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
|
|
2851
|
-
}
|
|
2852
|
-
if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
|
|
2853
|
-
return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
|
|
2854
|
-
}
|
|
2855
|
-
return { success: true, path: filePath };
|
|
2856
|
-
}
|
|
2857
|
-
function applyTextEditsToFile(filePath, edits) {
|
|
2858
|
-
try {
|
|
2859
|
-
const content = readFileSync4(filePath, "utf-8");
|
|
2860
|
-
const lines = content.split(`
|
|
2861
|
-
`);
|
|
2862
|
-
const sortedEdits = [...edits].sort((a, b) => {
|
|
2863
|
-
if (b.range.start.line !== a.range.start.line) {
|
|
2864
|
-
return b.range.start.line - a.range.start.line;
|
|
2865
|
-
}
|
|
2866
|
-
return b.range.start.character - a.range.start.character;
|
|
2867
|
-
});
|
|
2868
|
-
for (const edit of sortedEdits) {
|
|
2869
|
-
const startLine = edit.range.start.line;
|
|
2870
|
-
const startChar = edit.range.start.character;
|
|
2871
|
-
const endLine = edit.range.end.line;
|
|
2872
|
-
const endChar = edit.range.end.character;
|
|
2873
|
-
if (startLine === endLine) {
|
|
2874
|
-
const line = lines[startLine] ?? "";
|
|
2875
|
-
lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
|
|
2876
|
-
} else {
|
|
2877
|
-
const firstLine = lines[startLine] ?? "";
|
|
2878
|
-
const lastLine = lines[endLine] ?? "";
|
|
2879
|
-
const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
|
|
2880
|
-
lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
|
|
2881
|
-
`));
|
|
2882
|
-
}
|
|
2883
|
-
}
|
|
2884
|
-
writeFileSync2(filePath, lines.join(`
|
|
2885
|
-
`), "utf-8");
|
|
2886
|
-
return { success: true, editCount: edits.length };
|
|
2887
|
-
} catch (err) {
|
|
2888
|
-
return {
|
|
2889
|
-
success: false,
|
|
2890
|
-
editCount: 0,
|
|
2891
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2892
|
-
};
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
function applyWorkspaceEdit(edit, options = {}) {
|
|
2896
|
-
if (!edit) {
|
|
2897
|
-
return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
|
|
2898
|
-
}
|
|
2899
|
-
const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
|
|
2900
|
-
const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
|
|
2901
|
-
if (edit.changes) {
|
|
2902
|
-
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
2903
|
-
const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
|
|
2904
|
-
if (!validatedPath.success) {
|
|
2905
|
-
result.success = false;
|
|
2906
|
-
result.errors.push(validatedPath.error);
|
|
2907
|
-
continue;
|
|
2908
|
-
}
|
|
2909
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, edits);
|
|
2910
|
-
if (applyResult.success) {
|
|
2911
|
-
result.filesModified.push(validatedPath.path);
|
|
2912
|
-
result.totalEdits += applyResult.editCount;
|
|
2913
|
-
} else {
|
|
2914
|
-
result.success = false;
|
|
2915
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
if (edit.documentChanges) {
|
|
2920
|
-
for (const change of edit.documentChanges) {
|
|
2921
|
-
if (!("kind" in change)) {
|
|
2922
|
-
const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
|
|
2923
|
-
if (!validatedPath.success) {
|
|
2924
|
-
result.success = false;
|
|
2925
|
-
result.errors.push(validatedPath.error);
|
|
2926
|
-
continue;
|
|
2927
|
-
}
|
|
2928
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
|
|
2929
|
-
if (applyResult.success) {
|
|
2930
|
-
result.filesModified.push(validatedPath.path);
|
|
2931
|
-
result.totalEdits += applyResult.editCount;
|
|
2932
|
-
} else {
|
|
2933
|
-
result.success = false;
|
|
2934
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2935
|
-
}
|
|
2936
|
-
continue;
|
|
2937
|
-
}
|
|
2938
|
-
if (change.kind === "create") {
|
|
2939
|
-
try {
|
|
2940
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2941
|
-
if (!validatedPath.success) {
|
|
2942
|
-
result.success = false;
|
|
2943
|
-
result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
|
|
2944
|
-
continue;
|
|
2945
|
-
}
|
|
2946
|
-
writeFileSync2(validatedPath.path, "", "utf-8");
|
|
2947
|
-
result.filesModified.push(validatedPath.path);
|
|
2948
|
-
} catch (err) {
|
|
2949
|
-
result.success = false;
|
|
2950
|
-
result.errors.push(`Create ${change.uri}: ${String(err)}`);
|
|
2951
|
-
}
|
|
2952
|
-
} else if (change.kind === "rename") {
|
|
2953
|
-
try {
|
|
2954
|
-
const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
|
|
2955
|
-
const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
|
|
2956
|
-
if (!oldPath.success || !newPath.success) {
|
|
2957
|
-
const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
|
|
2958
|
-
result.success = false;
|
|
2959
|
-
result.errors.push(`Rename ${change.oldUri}: ${error}`);
|
|
2960
|
-
continue;
|
|
2961
|
-
}
|
|
2962
|
-
const content = readFileSync4(oldPath.path, "utf-8");
|
|
2963
|
-
writeFileSync2(newPath.path, content, "utf-8");
|
|
2964
|
-
unlinkSync(oldPath.path);
|
|
2965
|
-
result.filesModified.push(newPath.path);
|
|
2966
|
-
} catch (err) {
|
|
2967
|
-
result.success = false;
|
|
2968
|
-
result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
|
|
2969
|
-
}
|
|
2970
|
-
} else if (change.kind === "delete") {
|
|
2971
|
-
try {
|
|
2972
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2973
|
-
if (!validatedPath.success) {
|
|
2974
|
-
result.success = false;
|
|
2975
|
-
result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
|
|
2976
|
-
continue;
|
|
2977
|
-
}
|
|
2978
|
-
unlinkSync(validatedPath.path);
|
|
2979
|
-
result.filesModified.push(validatedPath.path);
|
|
2980
|
-
} catch (err) {
|
|
2981
|
-
result.success = false;
|
|
2982
|
-
result.errors.push(`Delete ${change.uri}: ${String(err)}`);
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
return result;
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
4725
|
// ../lsp-core/src/tools/rename.ts
|
|
2991
4726
|
async function executeLspPrepareRename(params, signal) {
|
|
2992
4727
|
const filePath = requireString(params, "filePath");
|
|
2993
4728
|
const line = requireNumber(params, "line");
|
|
2994
4729
|
const character = requireNumber(params, "character");
|
|
2995
4730
|
try {
|
|
2996
|
-
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
|
|
4731
|
+
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
|
|
2997
4732
|
const details = { filePath, line, character, result };
|
|
2998
4733
|
return text(formatPrepareRenameResult(result), details);
|
|
2999
4734
|
} catch (error) {
|
|
@@ -3014,13 +4749,9 @@ async function executeLspRename(params, signal) {
|
|
|
3014
4749
|
const character = requireNumber(params, "character");
|
|
3015
4750
|
const newName = requireString(params, "newName");
|
|
3016
4751
|
try {
|
|
3017
|
-
const
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
}), "rename", clientOptions(signal));
|
|
3021
|
-
const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
|
|
3022
|
-
const details = { filePath, line, character, newName, apply, edit: edit.edit };
|
|
3023
|
-
return text(formatApplyResult(apply), details, !apply.success);
|
|
4752
|
+
const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
|
|
4753
|
+
const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
|
|
4754
|
+
return text(formatApplyResult(result.apply), details, !result.apply.success);
|
|
3024
4755
|
} catch (error) {
|
|
3025
4756
|
const missingDependency = missingDependencyResult(error, {
|
|
3026
4757
|
filePath,
|
|
@@ -3095,10 +4826,10 @@ async function executeLspSymbols(params, signal) {
|
|
|
3095
4826
|
errorKind: "missing_query"
|
|
3096
4827
|
});
|
|
3097
4828
|
}
|
|
3098
|
-
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
|
|
4829
|
+
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
|
|
3099
4830
|
return formatSymbolsResult(filePath, scope, symbols2, limit, query);
|
|
3100
4831
|
}
|
|
3101
|
-
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
|
|
4832
|
+
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
|
|
3102
4833
|
return formatSymbolsResult(filePath, scope, symbols, limit);
|
|
3103
4834
|
} catch (error) {
|
|
3104
4835
|
const query = optionalString(params, "query");
|
|
@@ -3264,12 +4995,12 @@ function matchesToolName(tool, name) {
|
|
|
3264
4995
|
return tool.name === name || (tool.aliases?.includes(name) ?? false);
|
|
3265
4996
|
}
|
|
3266
4997
|
function coerceToolArguments(value) {
|
|
3267
|
-
return
|
|
4998
|
+
return isRecord7(value) ? value : {};
|
|
3268
4999
|
}
|
|
3269
5000
|
// ../lsp-core/src/mcp.ts
|
|
3270
5001
|
var SERVER_NAME = "lsp";
|
|
3271
5002
|
var SERVER_VERSION = "0.1.0";
|
|
3272
|
-
async function handleLspMcpRequest(input) {
|
|
5003
|
+
async function handleLspMcpRequest(input, options = {}) {
|
|
3273
5004
|
if (!isPlainRecord(input)) {
|
|
3274
5005
|
return errorResponse(null, -32600, "Invalid Request");
|
|
3275
5006
|
}
|
|
@@ -3291,33 +5022,37 @@ async function handleLspMcpRequest(input) {
|
|
|
3291
5022
|
return successResponse(id, { tools: LSP_MCP_TOOLS.map(describeTool) });
|
|
3292
5023
|
}
|
|
3293
5024
|
if (method === "tools/call") {
|
|
3294
|
-
return handleToolCall(id, input["params"]);
|
|
5025
|
+
return handleToolCall(id, input["params"], options.signal);
|
|
3295
5026
|
}
|
|
3296
5027
|
return errorResponse(id, -32601, `Method not found: ${String(method)}`);
|
|
3297
5028
|
}
|
|
3298
5029
|
async function runMcpStdioServer(input = process.stdin, output = process.stdout) {
|
|
5030
|
+
const requestContext = createStandaloneMcpRequestContext();
|
|
3299
5031
|
await runJsonRpcStdioServer({
|
|
3300
5032
|
input,
|
|
3301
5033
|
output,
|
|
3302
5034
|
idleTimeoutMs: 0,
|
|
3303
|
-
handler: handleLspMcpRequest,
|
|
5035
|
+
handler: (request) => runWithRequestContext(requestContext, () => handleLspMcpRequest(request)),
|
|
3304
5036
|
handlerOptions: undefined
|
|
3305
5037
|
});
|
|
3306
5038
|
}
|
|
3307
|
-
async function handleToolCall(id, params) {
|
|
5039
|
+
async function handleToolCall(id, params, signal) {
|
|
3308
5040
|
if (!isPlainRecord(params) || typeof params["name"] !== "string") {
|
|
3309
5041
|
return errorResponse(id, -32602, "tools/call requires params.name");
|
|
3310
5042
|
}
|
|
3311
5043
|
try {
|
|
3312
|
-
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
|
|
5044
|
+
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
|
|
3313
5045
|
return successResponse(id, {
|
|
3314
5046
|
content: result.content,
|
|
3315
5047
|
isError: result.isError ?? false,
|
|
3316
5048
|
details: result.details
|
|
3317
5049
|
});
|
|
3318
5050
|
} catch (error) {
|
|
5051
|
+
if (!(error instanceof Error)) {
|
|
5052
|
+
throw error;
|
|
5053
|
+
}
|
|
3319
5054
|
return successResponse(id, {
|
|
3320
|
-
content: [{ type: "text", text:
|
|
5055
|
+
content: [{ type: "text", text: error.message }],
|
|
3321
5056
|
isError: true
|
|
3322
5057
|
});
|
|
3323
5058
|
}
|