oh-my-opencode 4.18.0 → 4.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -0
- package/.agents/skills/work-with-pr/SKILL.md +16 -37
- package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/.opencode/skills/work-with-pr/SKILL.md +16 -37
- package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/dist/cli/index.js +457 -154
- package/dist/cli-node/index.js +457 -154
- package/dist/index.js +415 -388
- package/package.json +16 -16
- package/packages/lsp-core/package.json +4 -0
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
- package/packages/lsp-core/src/lsp/client.ts +262 -80
- package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
- package/packages/lsp-core/src/lsp/connection.ts +12 -6
- package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +104 -0
- package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
- package/packages/lsp-core/src/lsp/errors.ts +11 -0
- package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
- package/packages/lsp-core/src/lsp/formatters.ts +3 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
- package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
- package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
- package/packages/lsp-core/src/lsp/transport.ts +96 -70
- package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
- package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
- package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
- package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
- package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
- package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
- package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
- package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
- package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
- package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
- package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
- package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
- package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
- package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
- package/packages/lsp-core/src/mcp.ts +18 -7
- package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
- package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
- package/packages/lsp-core/src/post-edit/index.ts +1 -0
- package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
- package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
- package/packages/lsp-core/src/request-context.test.ts +171 -0
- package/packages/lsp-core/src/request-context.ts +222 -9
- package/packages/lsp-core/src/tool-surface.test.ts +4 -1
- package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
- package/packages/lsp-core/src/tools/navigation.ts +12 -12
- package/packages/lsp-core/src/tools/rename.ts +10 -15
- package/packages/lsp-core/src/tools/symbols.ts +11 -11
- package/packages/lsp-core/src/tools/types.ts +2 -1
- package/packages/lsp-daemon/dist/cli.js +3114 -747
- package/packages/lsp-daemon/dist/client.d.ts +105 -0
- package/packages/lsp-daemon/dist/client.js +5851 -0
- package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
- package/packages/lsp-daemon/dist/daemon-client.js +113 -30
- package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-server.js +40 -15
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +8 -7
- package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
- package/packages/lsp-daemon/dist/index.d.ts +2 -2
- package/packages/lsp-daemon/dist/index.js +2862 -754
- package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
- package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
- package/packages/lsp-daemon/dist/lock.js +14 -4
- package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
- package/packages/lsp-daemon/dist/ownership.js +168 -0
- package/packages/lsp-daemon/dist/paths.d.ts +33 -9
- package/packages/lsp-daemon/dist/paths.js +72 -33
- package/packages/lsp-daemon/dist/proxy.d.ts +3 -0
- package/packages/lsp-daemon/dist/proxy.js +54 -3
- package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
- package/packages/lsp-daemon/dist/request-routing.js +71 -22
- package/packages/lsp-daemon/dist/run-daemon.js +9 -2
- package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
- package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
- package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
- package/packages/lsp-daemon/package.json +12 -3
- package/packages/lsp-tools-mcp/dist/cli.js +2115 -442
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
- package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
- package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
- package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +19 -5
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- 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/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +26 -14
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
- package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
- 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/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// ../lsp-core/src/lsp/cleanup-errors.ts
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
function writeCleanupError(message) {
|
|
5
|
+
process.stderr.write(`${message}
|
|
6
|
+
`);
|
|
7
|
+
}
|
|
8
|
+
function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
|
|
7
9
|
const message = error instanceof Error ? error.message : String(error);
|
|
8
|
-
|
|
10
|
+
logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
// ../lsp-core/src/lsp/client.ts
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
15
|
-
|
|
16
|
-
// ../lsp-core/src/request-context.ts
|
|
17
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
|
-
var storage = new AsyncLocalStorage;
|
|
19
|
-
function runWithRequestContext(context, fn) {
|
|
20
|
-
return storage.run(context, fn);
|
|
21
|
-
}
|
|
22
|
-
function contextCwd() {
|
|
23
|
-
return storage.getStore()?.cwd ?? process.cwd();
|
|
24
|
-
}
|
|
25
|
-
function contextEnv(key) {
|
|
26
|
-
const store = storage.getStore();
|
|
27
|
-
if (store?.env)
|
|
28
|
-
return store.env[key];
|
|
29
|
-
return process.env[key];
|
|
30
|
-
}
|
|
14
|
+
import { resolve as resolve5 } from "node:path";
|
|
15
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
31
16
|
|
|
32
17
|
// ../lsp-core/src/lsp/connection.ts
|
|
33
18
|
import { pathToFileURL } from "node:url";
|
|
@@ -91,7 +76,12 @@ class LspInvalidPathError extends Error {
|
|
|
91
76
|
}
|
|
92
77
|
|
|
93
78
|
class LspServerLookupError extends Error {
|
|
79
|
+
lookup;
|
|
94
80
|
name = "LspServerLookupError";
|
|
81
|
+
constructor(message, lookup) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.lookup = lookup;
|
|
84
|
+
}
|
|
95
85
|
}
|
|
96
86
|
|
|
97
87
|
class LspServerInitializingError extends Error {
|
|
@@ -157,28 +147,80 @@ class JsonRpcConnection {
|
|
|
157
147
|
onError(handler) {
|
|
158
148
|
this.errorHandlers.push(handler);
|
|
159
149
|
}
|
|
160
|
-
async sendRequest(method, params) {
|
|
150
|
+
async sendRequest(method, params, options = {}) {
|
|
161
151
|
if (this.disposed)
|
|
162
152
|
throw new Error("JSON-RPC connection is disposed");
|
|
163
153
|
const id = this.nextRequestId;
|
|
164
154
|
this.nextRequestId += 1;
|
|
155
|
+
const key = String(id);
|
|
165
156
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
157
|
+
let requestWritten = false;
|
|
158
|
+
let cancelAfterWrite = false;
|
|
159
|
+
let settled = false;
|
|
160
|
+
const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
166
161
|
const responsePromise = new Promise((resolve, reject) => {
|
|
167
|
-
|
|
162
|
+
const cleanup = () => {
|
|
163
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
164
|
+
};
|
|
165
|
+
const settleCancel = () => {
|
|
166
|
+
if (settled)
|
|
167
|
+
return;
|
|
168
|
+
settled = true;
|
|
169
|
+
this.pendingRequests.delete(key);
|
|
170
|
+
cleanup();
|
|
171
|
+
const rejectCancelled = () => reject(abortError(options.signal));
|
|
172
|
+
if (!requestWritten) {
|
|
173
|
+
cancelAfterWrite = true;
|
|
174
|
+
rejectCancelled();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
writeCancel().then(rejectCancelled, (error) => {
|
|
178
|
+
this.emitError(toError(error));
|
|
179
|
+
rejectCancelled();
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
const onAbort = () => settleCancel();
|
|
183
|
+
this.pendingRequests.set(key, {
|
|
168
184
|
resolve(result) {
|
|
185
|
+
settled = true;
|
|
186
|
+
cleanup();
|
|
169
187
|
resolve(result);
|
|
170
188
|
},
|
|
171
|
-
reject
|
|
189
|
+
reject(error) {
|
|
190
|
+
settled = true;
|
|
191
|
+
cleanup();
|
|
192
|
+
reject(error);
|
|
193
|
+
},
|
|
194
|
+
cleanup
|
|
172
195
|
});
|
|
196
|
+
if (options.signal?.aborted) {
|
|
197
|
+
settleCancel();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
173
201
|
});
|
|
202
|
+
if (settled)
|
|
203
|
+
return responsePromise;
|
|
174
204
|
try {
|
|
175
205
|
await this.writeMessage(message);
|
|
206
|
+
requestWritten = true;
|
|
207
|
+
if (cancelAfterWrite)
|
|
208
|
+
await writeCancel();
|
|
176
209
|
} catch (error) {
|
|
177
|
-
|
|
210
|
+
if (settled)
|
|
211
|
+
return responsePromise;
|
|
212
|
+
const pending = this.pendingRequests.get(key);
|
|
213
|
+
if (pending) {
|
|
214
|
+
pending.cleanup();
|
|
215
|
+
this.pendingRequests.delete(key);
|
|
216
|
+
}
|
|
178
217
|
throw error;
|
|
179
218
|
}
|
|
180
219
|
return responsePromise;
|
|
181
220
|
}
|
|
221
|
+
pendingRequestCount() {
|
|
222
|
+
return this.pendingRequests.size;
|
|
223
|
+
}
|
|
182
224
|
async sendNotification(method, params) {
|
|
183
225
|
if (this.disposed)
|
|
184
226
|
return;
|
|
@@ -195,6 +237,7 @@ class JsonRpcConnection {
|
|
|
195
237
|
this.reader.off("error", this.handleStreamError);
|
|
196
238
|
this.writer.off("error", this.handleStreamError);
|
|
197
239
|
for (const pending of this.pendingRequests.values()) {
|
|
240
|
+
pending.cleanup();
|
|
198
241
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
199
242
|
}
|
|
200
243
|
this.pendingRequests.clear();
|
|
@@ -270,6 +313,7 @@ class JsonRpcConnection {
|
|
|
270
313
|
if (!pending)
|
|
271
314
|
return;
|
|
272
315
|
this.pendingRequests.delete(String(id));
|
|
316
|
+
pending.cleanup();
|
|
273
317
|
if ("error" in message) {
|
|
274
318
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
275
319
|
return;
|
|
@@ -283,7 +327,11 @@ class JsonRpcConnection {
|
|
|
283
327
|
try {
|
|
284
328
|
handler(params);
|
|
285
329
|
} catch (error) {
|
|
286
|
-
|
|
330
|
+
if (error instanceof Error) {
|
|
331
|
+
this.emitError(error);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.emitError(new Error(String(error)));
|
|
287
335
|
}
|
|
288
336
|
}
|
|
289
337
|
handleRequest(message) {
|
|
@@ -324,6 +372,14 @@ ${body}`;
|
|
|
324
372
|
}
|
|
325
373
|
}
|
|
326
374
|
}
|
|
375
|
+
function abortError(signal) {
|
|
376
|
+
const reason = signal?.reason;
|
|
377
|
+
if (reason instanceof Error)
|
|
378
|
+
return reason;
|
|
379
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
380
|
+
error.name = "AbortError";
|
|
381
|
+
return error;
|
|
382
|
+
}
|
|
327
383
|
function parseContentLength(headers) {
|
|
328
384
|
for (const line of headers.split(`\r
|
|
329
385
|
`)) {
|
|
@@ -510,7 +566,7 @@ function spawnProcess(command, options) {
|
|
|
510
566
|
return wrap(proc);
|
|
511
567
|
}
|
|
512
568
|
|
|
513
|
-
// ../lsp-core/src/lsp/transport.ts
|
|
569
|
+
// ../lsp-core/src/lsp/transport-protocol.ts
|
|
514
570
|
function isRecord(value) {
|
|
515
571
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
516
572
|
}
|
|
@@ -530,7 +586,32 @@ function parseDiagnosticsParams(params) {
|
|
|
530
586
|
if (!isRecord(params) || typeof params["uri"] !== "string")
|
|
531
587
|
return null;
|
|
532
588
|
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
533
|
-
|
|
589
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
590
|
+
return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
|
|
591
|
+
}
|
|
592
|
+
function createLspSpawnEnv(_root, input) {
|
|
593
|
+
return { ...input };
|
|
594
|
+
}
|
|
595
|
+
function isDiagnostic(value) {
|
|
596
|
+
return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
597
|
+
}
|
|
598
|
+
function isRange(value) {
|
|
599
|
+
return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
600
|
+
}
|
|
601
|
+
function isPosition(value) {
|
|
602
|
+
return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ../lsp-core/src/lsp/transport.ts
|
|
606
|
+
class LspClientNotStartedError extends Error {
|
|
607
|
+
serverId;
|
|
608
|
+
root;
|
|
609
|
+
name = "LspClientNotStartedError";
|
|
610
|
+
constructor(serverId, root) {
|
|
611
|
+
super("LSP client not started");
|
|
612
|
+
this.serverId = serverId;
|
|
613
|
+
this.root = root;
|
|
614
|
+
}
|
|
534
615
|
}
|
|
535
616
|
|
|
536
617
|
class LspClientTransport {
|
|
@@ -543,6 +624,8 @@ class LspClientTransport {
|
|
|
543
624
|
diagnosticsStore = new Map;
|
|
544
625
|
requestTimeoutMs;
|
|
545
626
|
initializeTimeoutMs;
|
|
627
|
+
workspaceApplyEditHandler = null;
|
|
628
|
+
diagnosticPullSupported = false;
|
|
546
629
|
constructor(root, server, timeouts = {}) {
|
|
547
630
|
this.root = root;
|
|
548
631
|
this.server = server;
|
|
@@ -555,6 +638,21 @@ class LspClientTransport {
|
|
|
555
638
|
command() {
|
|
556
639
|
return [...this.server.command];
|
|
557
640
|
}
|
|
641
|
+
setWorkspaceApplyEditHandler(handler) {
|
|
642
|
+
this.workspaceApplyEditHandler = handler;
|
|
643
|
+
}
|
|
644
|
+
hasWorkspaceApplyEditHandler() {
|
|
645
|
+
return this.workspaceApplyEditHandler !== null;
|
|
646
|
+
}
|
|
647
|
+
setDiagnosticPullSupported(supported) {
|
|
648
|
+
this.diagnosticPullSupported = supported;
|
|
649
|
+
}
|
|
650
|
+
isDiagnosticPullSupported() {
|
|
651
|
+
return this.diagnosticPullSupported;
|
|
652
|
+
}
|
|
653
|
+
handlePublishDiagnostics(params) {
|
|
654
|
+
this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
|
|
655
|
+
}
|
|
558
656
|
async start() {
|
|
559
657
|
const env = createLspSpawnEnv(this.root, {
|
|
560
658
|
...process.env,
|
|
@@ -565,7 +663,6 @@ class LspClientTransport {
|
|
|
565
663
|
env
|
|
566
664
|
});
|
|
567
665
|
this.startStderrReading();
|
|
568
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
569
666
|
if (this.proc.exitCode !== null) {
|
|
570
667
|
const stderr = this.stderrBuffer.join(`
|
|
571
668
|
`);
|
|
@@ -575,7 +672,7 @@ class LspClientTransport {
|
|
|
575
672
|
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
576
673
|
const diagnosticsParams = parseDiagnosticsParams(params);
|
|
577
674
|
if (diagnosticsParams?.uri) {
|
|
578
|
-
this.
|
|
675
|
+
this.handlePublishDiagnostics(diagnosticsParams);
|
|
579
676
|
}
|
|
580
677
|
});
|
|
581
678
|
this.connection.onRequest("workspace/configuration", (params) => {
|
|
@@ -588,6 +685,9 @@ class LspClientTransport {
|
|
|
588
685
|
});
|
|
589
686
|
this.connection.onRequest("client/registerCapability", () => null);
|
|
590
687
|
this.connection.onRequest("window/workDoneProgress/create", () => null);
|
|
688
|
+
if (this.workspaceApplyEditHandler) {
|
|
689
|
+
this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
|
|
690
|
+
}
|
|
591
691
|
this.connection.onClose(() => {
|
|
592
692
|
this.processExited = true;
|
|
593
693
|
});
|
|
@@ -616,30 +716,25 @@ class LspClientTransport {
|
|
|
616
716
|
}
|
|
617
717
|
async sendRequest(method, ...args) {
|
|
618
718
|
if (!this.connection)
|
|
619
|
-
throw new
|
|
719
|
+
throw new LspClientNotStartedError(this.server.id, this.root);
|
|
620
720
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
621
721
|
const stderrTail = this.stderrBuffer.slice(-10).join(`
|
|
622
722
|
`);
|
|
623
723
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
|
|
624
724
|
}
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
725
|
+
const options = args[1];
|
|
726
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
727
|
+
const timeoutController = new AbortController;
|
|
728
|
+
const timeoutHandle = setTimeout(() => {
|
|
729
|
+
const stderrTail = this.stderrBuffer.slice(-5).join(`
|
|
630
730
|
`);
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
731
|
+
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
732
|
+
}, timeoutMs);
|
|
733
|
+
const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
|
|
634
734
|
try {
|
|
635
|
-
const
|
|
636
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
637
|
-
if (timeoutHandle !== null)
|
|
638
|
-
clearTimeout(timeoutHandle);
|
|
735
|
+
const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
|
|
639
736
|
return result;
|
|
640
737
|
} catch (error) {
|
|
641
|
-
if (timeoutHandle !== null)
|
|
642
|
-
clearTimeout(timeoutHandle);
|
|
643
738
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
644
739
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
|
|
645
740
|
`) || undefined);
|
|
@@ -648,6 +743,9 @@ class LspClientTransport {
|
|
|
648
743
|
throw new LspConnectionClosedError(this.server.id, this.root, error.message);
|
|
649
744
|
}
|
|
650
745
|
throw error;
|
|
746
|
+
} finally {
|
|
747
|
+
clearTimeout(timeoutHandle);
|
|
748
|
+
combinedSignal.dispose();
|
|
651
749
|
}
|
|
652
750
|
}
|
|
653
751
|
async sendNotification(method, ...args) {
|
|
@@ -676,17 +774,17 @@ class LspClientTransport {
|
|
|
676
774
|
try {
|
|
677
775
|
await this.sendRequest("shutdown");
|
|
678
776
|
} catch (error) {
|
|
679
|
-
reportBestEffortCleanupError("shutdown request", error);
|
|
777
|
+
reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
|
|
680
778
|
}
|
|
681
779
|
try {
|
|
682
780
|
await this.sendNotification("exit");
|
|
683
781
|
} catch (error) {
|
|
684
|
-
reportBestEffortCleanupError("exit notification", error);
|
|
782
|
+
reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
|
|
685
783
|
}
|
|
686
784
|
try {
|
|
687
785
|
this.connection.dispose();
|
|
688
786
|
} catch (error) {
|
|
689
|
-
reportBestEffortCleanupError("connection dispose", error);
|
|
787
|
+
reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
|
|
690
788
|
}
|
|
691
789
|
this.connection = null;
|
|
692
790
|
}
|
|
@@ -717,11 +815,11 @@ class LspClientTransport {
|
|
|
717
815
|
new Promise((resolve) => setTimeout(resolve, STOP_SIGKILL_GRACE_MS))
|
|
718
816
|
]);
|
|
719
817
|
} catch (error) {
|
|
720
|
-
reportBestEffortCleanupError("hard process kill", error);
|
|
818
|
+
reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
|
|
721
819
|
}
|
|
722
820
|
}
|
|
723
821
|
} catch (error) {
|
|
724
|
-
reportBestEffortCleanupError("process stop", error);
|
|
822
|
+
reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
|
|
725
823
|
}
|
|
726
824
|
}
|
|
727
825
|
this.processExited = true;
|
|
@@ -731,26 +829,45 @@ class LspClientTransport {
|
|
|
731
829
|
return this.diagnosticsStore.get(uri) ?? [];
|
|
732
830
|
}
|
|
733
831
|
}
|
|
734
|
-
function
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
832
|
+
function combineAbortSignals(primary, secondary) {
|
|
833
|
+
const controller = new AbortController;
|
|
834
|
+
const abortFrom = (signal) => {
|
|
835
|
+
if (!controller.signal.aborted)
|
|
836
|
+
controller.abort(signal.reason);
|
|
837
|
+
};
|
|
838
|
+
const onPrimaryAbort = () => {
|
|
839
|
+
if (primary)
|
|
840
|
+
abortFrom(primary);
|
|
841
|
+
};
|
|
842
|
+
const onSecondaryAbort = () => abortFrom(secondary);
|
|
843
|
+
if (primary?.aborted)
|
|
844
|
+
abortFrom(primary);
|
|
845
|
+
else
|
|
846
|
+
primary?.addEventListener("abort", onPrimaryAbort, { once: true });
|
|
847
|
+
if (secondary.aborted)
|
|
848
|
+
abortFrom(secondary);
|
|
849
|
+
else
|
|
850
|
+
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
|
|
851
|
+
return {
|
|
852
|
+
signal: controller.signal,
|
|
853
|
+
dispose: () => {
|
|
854
|
+
primary?.removeEventListener("abort", onPrimaryAbort);
|
|
855
|
+
secondary.removeEventListener("abort", onSecondaryAbort);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
745
858
|
}
|
|
746
859
|
|
|
747
860
|
// ../lsp-core/src/lsp/connection.ts
|
|
748
|
-
|
|
861
|
+
function supportsDiagnosticPull(capabilities) {
|
|
862
|
+
if (capabilities === undefined)
|
|
863
|
+
return false;
|
|
864
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
865
|
+
}
|
|
749
866
|
|
|
750
867
|
class LspClientConnection extends LspClientTransport {
|
|
751
868
|
async initialize() {
|
|
752
869
|
const rootUri = pathToFileURL(this.root).href;
|
|
753
|
-
await this.sendRequest("initialize", {
|
|
870
|
+
const result = await this.sendRequest("initialize", {
|
|
754
871
|
processId: process.pid,
|
|
755
872
|
rootUri,
|
|
756
873
|
rootPath: this.root,
|
|
@@ -764,8 +881,7 @@ class LspClientConnection extends LspClientTransport {
|
|
|
764
881
|
publishDiagnostics: {},
|
|
765
882
|
rename: {
|
|
766
883
|
prepareSupport: true,
|
|
767
|
-
prepareSupportDefaultBehavior: 1
|
|
768
|
-
honorsChangeAnnotations: true
|
|
884
|
+
prepareSupportDefaultBehavior: 1
|
|
769
885
|
},
|
|
770
886
|
codeAction: {
|
|
771
887
|
codeActionLiteralSupport: {
|
|
@@ -794,22 +910,28 @@ class LspClientConnection extends LspClientTransport {
|
|
|
794
910
|
symbol: {},
|
|
795
911
|
workspaceFolders: true,
|
|
796
912
|
configuration: true,
|
|
797
|
-
applyEdit: true,
|
|
913
|
+
...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
|
|
798
914
|
workspaceEdit: {
|
|
799
|
-
documentChanges: true
|
|
915
|
+
documentChanges: true,
|
|
916
|
+
resourceOperations: ["create", "rename", "delete"]
|
|
800
917
|
}
|
|
801
918
|
}
|
|
802
919
|
},
|
|
803
920
|
initializationOptions: this.server.initialization
|
|
804
921
|
}, { timeoutMs: this.initializeTimeoutMs });
|
|
922
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
805
923
|
await this.sendNotification("initialized");
|
|
806
924
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
807
925
|
settings: { json: { validate: { enable: true } } }
|
|
808
926
|
});
|
|
809
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
810
927
|
}
|
|
811
928
|
}
|
|
812
929
|
|
|
930
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
931
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
932
|
+
import { relative, resolve } from "node:path";
|
|
933
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
934
|
+
|
|
813
935
|
// ../lsp-core/src/lsp/effective-extension.ts
|
|
814
936
|
import { basename, extname } from "node:path";
|
|
815
937
|
var BASENAME_EXTENSIONS = {
|
|
@@ -992,82 +1114,1422 @@ function getLanguageId(ext) {
|
|
|
992
1114
|
return EXT_TO_LANG[ext] ?? "plaintext";
|
|
993
1115
|
}
|
|
994
1116
|
|
|
1117
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1118
|
+
var WATCHED_FILE_BATCH_SIZE = 128;
|
|
1119
|
+
var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1120
|
+
function canonicalPath(filePath) {
|
|
1121
|
+
const absolute = resolve(filePath);
|
|
1122
|
+
try {
|
|
1123
|
+
return realpathSync(absolute);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return absolute;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function isSameOrDescendant(candidate, parent) {
|
|
1129
|
+
const suffix = relative(parent, candidate);
|
|
1130
|
+
return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
|
|
1131
|
+
}
|
|
1132
|
+
function movedPath(candidate, oldPath, newPath) {
|
|
1133
|
+
const suffix = relative(oldPath, candidate);
|
|
1134
|
+
return suffix === "" ? newPath : resolve(newPath, suffix);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
class WorkspaceDocumentState {
|
|
1138
|
+
sendNotification;
|
|
1139
|
+
clearDiagnostics;
|
|
1140
|
+
openDocuments = new Map;
|
|
1141
|
+
openByUri = new Map;
|
|
1142
|
+
openPromises = new Map;
|
|
1143
|
+
now;
|
|
1144
|
+
versionlessPublishQuiescenceMs;
|
|
1145
|
+
constructor(sendNotification, clearDiagnostics, options = {}) {
|
|
1146
|
+
this.sendNotification = sendNotification;
|
|
1147
|
+
this.clearDiagnostics = clearDiagnostics;
|
|
1148
|
+
this.now = options.now ?? (() => Date.now());
|
|
1149
|
+
this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
1150
|
+
}
|
|
1151
|
+
async openFile(filePath) {
|
|
1152
|
+
const path = canonicalPath(filePath);
|
|
1153
|
+
const existingOpen = this.openPromises.get(path);
|
|
1154
|
+
if (existingOpen) {
|
|
1155
|
+
await existingOpen;
|
|
1156
|
+
return this.openFile(path);
|
|
1157
|
+
}
|
|
1158
|
+
const text = readFileSync(path, "utf-8");
|
|
1159
|
+
const existing = this.openDocuments.get(path);
|
|
1160
|
+
if (!existing)
|
|
1161
|
+
return this.openDocumentSingleFlight(path, text);
|
|
1162
|
+
if (existing.text === text)
|
|
1163
|
+
return;
|
|
1164
|
+
await this.changeDocument(existing, text);
|
|
1165
|
+
}
|
|
1166
|
+
getVersion(filePath) {
|
|
1167
|
+
return this.openDocuments.get(canonicalPath(filePath))?.version;
|
|
1168
|
+
}
|
|
1169
|
+
getStoredDiagnostics(uri) {
|
|
1170
|
+
const state = this.openByUri.get(uri);
|
|
1171
|
+
if (!state)
|
|
1172
|
+
return [];
|
|
1173
|
+
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
1174
|
+
}
|
|
1175
|
+
captureDiagnosticSnapshot(filePath) {
|
|
1176
|
+
const state = this.openDocuments.get(canonicalPath(filePath));
|
|
1177
|
+
if (!state)
|
|
1178
|
+
return null;
|
|
1179
|
+
return {
|
|
1180
|
+
path: state.path,
|
|
1181
|
+
uri: state.uri,
|
|
1182
|
+
version: state.version,
|
|
1183
|
+
documentGeneration: state.generation,
|
|
1184
|
+
publishGeneration: state.publishGeneration
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
isCurrentSnapshot(snapshot) {
|
|
1188
|
+
const state = this.openDocuments.get(snapshot.path);
|
|
1189
|
+
return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
|
|
1190
|
+
}
|
|
1191
|
+
getPullCache(snapshot) {
|
|
1192
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1193
|
+
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
|
|
1194
|
+
return null;
|
|
1195
|
+
return state.pullCache;
|
|
1196
|
+
}
|
|
1197
|
+
recordPullDiagnostics(snapshot, report) {
|
|
1198
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1199
|
+
if (!state)
|
|
1200
|
+
return;
|
|
1201
|
+
state.pullCache = {
|
|
1202
|
+
documentVersion: snapshot.version,
|
|
1203
|
+
diagnostics: [...report.diagnostics],
|
|
1204
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
recordPublishedDiagnostics(params) {
|
|
1208
|
+
const state = this.openByUri.get(params.uri);
|
|
1209
|
+
if (!state)
|
|
1210
|
+
return;
|
|
1211
|
+
state.publishGeneration += 1;
|
|
1212
|
+
state.lastPublish = {
|
|
1213
|
+
diagnostics: [...params.diagnostics],
|
|
1214
|
+
publishGeneration: state.publishGeneration,
|
|
1215
|
+
documentGenerationAtArrival: state.generation,
|
|
1216
|
+
arrivedAt: this.now(),
|
|
1217
|
+
...params.version === undefined ? {} : { version: params.version }
|
|
1218
|
+
};
|
|
1219
|
+
this.notifyWaiters(state);
|
|
1220
|
+
}
|
|
1221
|
+
resolvePushDiagnostics(snapshot) {
|
|
1222
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1223
|
+
if (!state?.lastPublish)
|
|
1224
|
+
return { status: "missing" };
|
|
1225
|
+
const publish = state.lastPublish;
|
|
1226
|
+
if (publish.version !== undefined) {
|
|
1227
|
+
return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
|
|
1228
|
+
}
|
|
1229
|
+
if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
|
|
1230
|
+
return { status: "missing" };
|
|
1231
|
+
const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
|
|
1232
|
+
const waitMs = Math.max(0, readyAt - this.now());
|
|
1233
|
+
return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
|
|
1234
|
+
}
|
|
1235
|
+
waitForDiagnosticsActivity(snapshot, timeoutMs) {
|
|
1236
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1237
|
+
if (!state || timeoutMs <= 0)
|
|
1238
|
+
return Promise.resolve();
|
|
1239
|
+
return new Promise((resolveActivity) => {
|
|
1240
|
+
let settled = false;
|
|
1241
|
+
const finish = () => {
|
|
1242
|
+
if (settled)
|
|
1243
|
+
return;
|
|
1244
|
+
settled = true;
|
|
1245
|
+
clearTimeout(timer);
|
|
1246
|
+
state.waiters.delete(finish);
|
|
1247
|
+
resolveActivity();
|
|
1248
|
+
};
|
|
1249
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1250
|
+
if (typeof timer.unref === "function")
|
|
1251
|
+
timer.unref();
|
|
1252
|
+
state.waiters.add(finish);
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
validateVersions(operations) {
|
|
1256
|
+
const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
|
|
1257
|
+
for (const operation of operations) {
|
|
1258
|
+
if (operation.kind === "text") {
|
|
1259
|
+
const current = versions.get(operation.path);
|
|
1260
|
+
if (operation.documentVersion !== null && current !== operation.documentVersion) {
|
|
1261
|
+
const observed = current === undefined ? "closed document" : `open document version ${current}`;
|
|
1262
|
+
return {
|
|
1263
|
+
changeIndex: operation.changeIndex,
|
|
1264
|
+
message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
if (current !== undefined)
|
|
1268
|
+
versions.set(operation.path, current + 1);
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (operation.kind === "rename") {
|
|
1272
|
+
const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
|
|
1273
|
+
for (const [path] of moved)
|
|
1274
|
+
versions.delete(path);
|
|
1275
|
+
for (const [path] of moved)
|
|
1276
|
+
versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (operation.kind === "delete") {
|
|
1280
|
+
for (const path of [...versions.keys()]) {
|
|
1281
|
+
if (isSameOrDescendant(path, operation.path))
|
|
1282
|
+
versions.delete(path);
|
|
1283
|
+
}
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
|
|
1287
|
+
versions.set(operation.path, 1);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return null;
|
|
1291
|
+
}
|
|
1292
|
+
async synchronize(delta) {
|
|
1293
|
+
const watched = [];
|
|
1294
|
+
for (const mutation of delta.operations)
|
|
1295
|
+
await this.synchronizeMutation(mutation, watched);
|
|
1296
|
+
for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
|
|
1297
|
+
await this.sendNotification("workspace/didChangeWatchedFiles", {
|
|
1298
|
+
changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async synchronizeMutation(mutation, watched) {
|
|
1303
|
+
if (mutation.kind === "text") {
|
|
1304
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1305
|
+
if (state)
|
|
1306
|
+
await this.changeDocument(state, mutation.afterText);
|
|
1307
|
+
else
|
|
1308
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (mutation.kind === "create") {
|
|
1312
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1313
|
+
if (state) {
|
|
1314
|
+
await this.closeDocument(state);
|
|
1315
|
+
await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
|
|
1316
|
+
} else {
|
|
1317
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (mutation.kind === "rename") {
|
|
1322
|
+
const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
|
|
1323
|
+
for (const state of moved)
|
|
1324
|
+
await this.closeDocument(state);
|
|
1325
|
+
for (const state of moved) {
|
|
1326
|
+
const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
|
|
1327
|
+
await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
|
|
1328
|
+
}
|
|
1329
|
+
if (moved.length === 0) {
|
|
1330
|
+
watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
|
|
1331
|
+
watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
|
|
1332
|
+
}
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
|
|
1336
|
+
for (const state of removed)
|
|
1337
|
+
await this.closeDocument(state);
|
|
1338
|
+
if (removed.length === 0)
|
|
1339
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
|
|
1340
|
+
}
|
|
1341
|
+
async openDocumentSingleFlight(path, text) {
|
|
1342
|
+
const existing = this.openPromises.get(path);
|
|
1343
|
+
if (existing)
|
|
1344
|
+
return existing;
|
|
1345
|
+
const open = (async () => {
|
|
1346
|
+
const state = {
|
|
1347
|
+
path,
|
|
1348
|
+
uri: pathToFileURL2(path).href,
|
|
1349
|
+
languageId: getLanguageId(effectiveExtension(path)),
|
|
1350
|
+
text,
|
|
1351
|
+
version: 1,
|
|
1352
|
+
generation: 1,
|
|
1353
|
+
publishGeneration: 0,
|
|
1354
|
+
waiters: new Set
|
|
1355
|
+
};
|
|
1356
|
+
this.openDocuments.set(path, state);
|
|
1357
|
+
this.openByUri.set(state.uri, state);
|
|
1358
|
+
this.notifyWaiters(state);
|
|
1359
|
+
await this.sendNotification("textDocument/didOpen", {
|
|
1360
|
+
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
|
|
1361
|
+
});
|
|
1362
|
+
})().finally(() => {
|
|
1363
|
+
this.openPromises.delete(path);
|
|
1364
|
+
});
|
|
1365
|
+
this.openPromises.set(path, open);
|
|
1366
|
+
return open;
|
|
1367
|
+
}
|
|
1368
|
+
async changeDocument(state, text) {
|
|
1369
|
+
state.text = text;
|
|
1370
|
+
state.version += 1;
|
|
1371
|
+
state.generation += 1;
|
|
1372
|
+
this.clearDiagnostics(state.uri);
|
|
1373
|
+
this.notifyWaiters(state);
|
|
1374
|
+
await this.sendNotification("textDocument/didChange", {
|
|
1375
|
+
textDocument: { uri: state.uri, version: state.version },
|
|
1376
|
+
contentChanges: [{ text }]
|
|
1377
|
+
});
|
|
1378
|
+
await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
|
|
1379
|
+
}
|
|
1380
|
+
async closeDocument(state) {
|
|
1381
|
+
this.openDocuments.delete(state.path);
|
|
1382
|
+
this.openByUri.delete(state.uri);
|
|
1383
|
+
this.clearDiagnostics(state.uri);
|
|
1384
|
+
this.notifyWaiters(state);
|
|
1385
|
+
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
1386
|
+
}
|
|
1387
|
+
notifyWaiters(state) {
|
|
1388
|
+
for (const waiter of [...state.waiters])
|
|
1389
|
+
waiter();
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
|
|
1394
|
+
var CONCURRENT_FAILURE_REASON_BY_PHASE = {
|
|
1395
|
+
applying: "workspace/applyEdit is already in progress for this workspace mutation",
|
|
1396
|
+
settled: "workspace/applyEdit was already handled for this workspace mutation"
|
|
1397
|
+
};
|
|
1398
|
+
function workspaceApplyEditConcurrentFailureReason(phase) {
|
|
1399
|
+
return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1403
|
+
import { existsSync as existsSync3, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1404
|
+
|
|
1405
|
+
// ../lsp-core/src/lsp/workspace-edit-path.ts
|
|
1406
|
+
import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync2 } from "node:fs";
|
|
1407
|
+
import { dirname, isAbsolute, relative as relative2, resolve as resolve2 } from "node:path";
|
|
1408
|
+
import { fileURLToPath } from "node:url";
|
|
1409
|
+
|
|
1410
|
+
class WorkspaceEditPathError extends Error {
|
|
1411
|
+
path;
|
|
1412
|
+
detail;
|
|
1413
|
+
name = "WorkspaceEditPathError";
|
|
1414
|
+
constructor(path, detail) {
|
|
1415
|
+
super(`${detail}: ${path}`);
|
|
1416
|
+
this.path = path;
|
|
1417
|
+
this.detail = detail;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
1421
|
+
const relativePath = relative2(workspaceRoot, filePath);
|
|
1422
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
1423
|
+
}
|
|
1424
|
+
function canonicalizeMissingPath(filePath) {
|
|
1425
|
+
let ancestor = filePath;
|
|
1426
|
+
while (!existsSync2(ancestor)) {
|
|
1427
|
+
const parent = dirname(ancestor);
|
|
1428
|
+
if (parent === ancestor)
|
|
1429
|
+
throw new WorkspaceEditPathError(filePath, "no existing ancestor");
|
|
1430
|
+
ancestor = parent;
|
|
1431
|
+
}
|
|
1432
|
+
return resolve2(realpathSync2(ancestor), relative2(ancestor, filePath));
|
|
1433
|
+
}
|
|
1434
|
+
function canonicalWorkspaceRoot(workspaceRoot) {
|
|
1435
|
+
try {
|
|
1436
|
+
const canonical = realpathSync2(resolve2(workspaceRoot));
|
|
1437
|
+
if (!lstatSync(canonical).isDirectory()) {
|
|
1438
|
+
return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
success: true,
|
|
1442
|
+
path: canonical,
|
|
1443
|
+
requestedPath: resolve2(workspaceRoot),
|
|
1444
|
+
followedSymbolicLink: existsSync2(resolve2(workspaceRoot)) && lstatSync(resolve2(workspaceRoot)).isSymbolicLink()
|
|
1445
|
+
};
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1448
|
+
return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
|
|
1452
|
+
let requestedPath;
|
|
1453
|
+
try {
|
|
1454
|
+
const parsed = new URL(uri);
|
|
1455
|
+
if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
|
|
1456
|
+
return { success: false, error: `non-file URI ${uri}` };
|
|
1457
|
+
}
|
|
1458
|
+
requestedPath = resolve2(fileURLToPath(parsed));
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1461
|
+
return { success: false, error: `non-file URI ${uri}: ${detail}` };
|
|
1462
|
+
}
|
|
1463
|
+
try {
|
|
1464
|
+
const canonical = existsSync2(requestedPath) ? realpathSync2(requestedPath) : canonicalizeMissingPath(requestedPath);
|
|
1465
|
+
if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
|
|
1466
|
+
return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
success: true,
|
|
1470
|
+
path: canonical,
|
|
1471
|
+
requestedPath,
|
|
1472
|
+
followedSymbolicLink: existsSync2(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
|
|
1473
|
+
};
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1476
|
+
return { success: false, error: `${requestedPath}: ${detail}` };
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function snapshotPath(path, includeChildren) {
|
|
1480
|
+
if (!existsSync2(path))
|
|
1481
|
+
return { kind: "missing" };
|
|
1482
|
+
const stats = lstatSync(path);
|
|
1483
|
+
if (stats.isFile())
|
|
1484
|
+
return { kind: "file", content: readFileSync2(path, "utf-8") };
|
|
1485
|
+
if (stats.isDirectory()) {
|
|
1486
|
+
return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
|
|
1487
|
+
}
|
|
1488
|
+
throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1492
|
+
var DEFAULT_IO = {
|
|
1493
|
+
writeFile(path, content) {
|
|
1494
|
+
writeFileSync(path, content, "utf-8");
|
|
1495
|
+
},
|
|
1496
|
+
rename(oldPath, newPath) {
|
|
1497
|
+
renameSync(oldPath, newPath);
|
|
1498
|
+
},
|
|
1499
|
+
remove(path, recursive) {
|
|
1500
|
+
rmSync(path, { recursive, force: false });
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
function snapshotsEqual(expected, actual) {
|
|
1504
|
+
if (expected.kind !== actual.kind)
|
|
1505
|
+
return false;
|
|
1506
|
+
if (expected.kind === "file" && actual.kind === "file")
|
|
1507
|
+
return expected.content === actual.content;
|
|
1508
|
+
if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
|
|
1509
|
+
return JSON.stringify(expected.children) === JSON.stringify(actual.children);
|
|
1510
|
+
}
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
1513
|
+
function liveSnapshot(path, expected) {
|
|
1514
|
+
return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
|
|
1515
|
+
}
|
|
1516
|
+
function firstOperationIndex(plan) {
|
|
1517
|
+
return plan.operations[0]?.changeIndex ?? 0;
|
|
1518
|
+
}
|
|
1519
|
+
function failedCommit(plan, failure) {
|
|
1520
|
+
const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
|
|
1521
|
+
return {
|
|
1522
|
+
result: {
|
|
1523
|
+
success: false,
|
|
1524
|
+
filesModified,
|
|
1525
|
+
totalEdits,
|
|
1526
|
+
errors: [`change ${changeIndex}: ${message}`],
|
|
1527
|
+
failedChange: changeIndex,
|
|
1528
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1529
|
+
},
|
|
1530
|
+
delta: mutationDelta(mutations),
|
|
1531
|
+
fingerprint: plan.fingerprint
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function verifySnapshots(plan) {
|
|
1535
|
+
for (const [path, expected] of plan.snapshots) {
|
|
1536
|
+
let actual;
|
|
1537
|
+
try {
|
|
1538
|
+
actual = liveSnapshot(path, expected);
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1541
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1542
|
+
return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
|
|
1543
|
+
}
|
|
1544
|
+
if (!snapshotsEqual(expected, actual)) {
|
|
1545
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1546
|
+
return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
function addModifiedPath(paths, path) {
|
|
1552
|
+
if (!paths.includes(path))
|
|
1553
|
+
paths.push(path);
|
|
1554
|
+
}
|
|
1555
|
+
function reportedPath(plan, path) {
|
|
1556
|
+
return plan.reportedPathByCanonical.get(path) ?? path;
|
|
1557
|
+
}
|
|
1558
|
+
function changedPathsForMutation(mutation) {
|
|
1559
|
+
return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
|
|
1560
|
+
}
|
|
1561
|
+
function mutationDelta(operations) {
|
|
1562
|
+
const changedPaths = new Set;
|
|
1563
|
+
for (const operation of operations) {
|
|
1564
|
+
for (const path of changedPathsForMutation(operation))
|
|
1565
|
+
changedPaths.add(path);
|
|
1566
|
+
}
|
|
1567
|
+
return { operations, changedPaths: [...changedPaths].sort() };
|
|
1568
|
+
}
|
|
1569
|
+
function resolveIo(overrides) {
|
|
1570
|
+
return {
|
|
1571
|
+
writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
|
|
1572
|
+
rename: overrides?.rename ?? DEFAULT_IO.rename,
|
|
1573
|
+
remove: overrides?.remove ?? DEFAULT_IO.remove
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function commitOperation(context, operation) {
|
|
1577
|
+
const { plan, io, accumulator } = context;
|
|
1578
|
+
if (operation.kind === "noop")
|
|
1579
|
+
return;
|
|
1580
|
+
if (operation.kind === "text") {
|
|
1581
|
+
io.writeFile(operation.path, operation.afterText);
|
|
1582
|
+
accumulator.mutations.push({
|
|
1583
|
+
kind: "text",
|
|
1584
|
+
path: operation.path,
|
|
1585
|
+
beforeText: operation.beforeText,
|
|
1586
|
+
afterText: operation.afterText
|
|
1587
|
+
});
|
|
1588
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1589
|
+
accumulator.totalEdits += operation.editCount;
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
if (operation.kind === "create") {
|
|
1593
|
+
io.writeFile(operation.path, "");
|
|
1594
|
+
accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
|
|
1595
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (operation.kind === "rename") {
|
|
1599
|
+
if (operation.replaceDestination) {
|
|
1600
|
+
const targetKind = existsSync3(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
|
|
1601
|
+
io.remove(operation.newPath, targetKind === "directory");
|
|
1602
|
+
accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
|
|
1603
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1604
|
+
}
|
|
1605
|
+
io.rename(operation.oldPath, operation.newPath);
|
|
1606
|
+
accumulator.mutations.push({
|
|
1607
|
+
kind: "rename",
|
|
1608
|
+
oldPath: operation.oldPath,
|
|
1609
|
+
newPath: operation.newPath,
|
|
1610
|
+
sourceKind: operation.sourceKind
|
|
1611
|
+
});
|
|
1612
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
io.remove(operation.path, operation.recursive);
|
|
1616
|
+
accumulator.mutations.push({
|
|
1617
|
+
kind: "delete",
|
|
1618
|
+
path: operation.path,
|
|
1619
|
+
targetKind: operation.targetKind
|
|
1620
|
+
});
|
|
1621
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1622
|
+
}
|
|
1623
|
+
function commitWorkspaceEditPlan(plan, options = {}) {
|
|
1624
|
+
if (options.signal?.aborted) {
|
|
1625
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
1626
|
+
}
|
|
1627
|
+
const stale = verifySnapshots(plan);
|
|
1628
|
+
if (stale)
|
|
1629
|
+
return stale;
|
|
1630
|
+
if (options.signal?.aborted) {
|
|
1631
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
1632
|
+
}
|
|
1633
|
+
const io = resolveIo(options.io);
|
|
1634
|
+
const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
|
|
1635
|
+
const context = { plan, io, accumulator };
|
|
1636
|
+
let lateAbort = false;
|
|
1637
|
+
for (const operation of plan.operations) {
|
|
1638
|
+
try {
|
|
1639
|
+
commitOperation(context, operation);
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1642
|
+
return failedCommit(plan, {
|
|
1643
|
+
message: `I/O failure during ${operation.kind}: ${detail}`,
|
|
1644
|
+
changeIndex: operation.changeIndex,
|
|
1645
|
+
mutations: accumulator.mutations,
|
|
1646
|
+
filesModified: accumulator.filesModified,
|
|
1647
|
+
totalEdits: accumulator.totalEdits,
|
|
1648
|
+
lateAbort: lateAbort || options.signal?.aborted === true
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
if (options.signal?.aborted)
|
|
1652
|
+
lateAbort = true;
|
|
1653
|
+
}
|
|
1654
|
+
const result = {
|
|
1655
|
+
success: true,
|
|
1656
|
+
filesModified: accumulator.filesModified,
|
|
1657
|
+
totalEdits: accumulator.totalEdits,
|
|
1658
|
+
errors: [],
|
|
1659
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1660
|
+
};
|
|
1661
|
+
return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
|
|
1665
|
+
import { createHash } from "node:crypto";
|
|
1666
|
+
function canonicalFingerprint(operations) {
|
|
1667
|
+
const canonical = operations.map((operation) => {
|
|
1668
|
+
switch (operation.kind) {
|
|
1669
|
+
case "text":
|
|
1670
|
+
return {
|
|
1671
|
+
kind: operation.kind,
|
|
1672
|
+
changeIndex: operation.changeIndex,
|
|
1673
|
+
path: operation.path,
|
|
1674
|
+
edits: operation.edits,
|
|
1675
|
+
version: operation.version
|
|
1676
|
+
};
|
|
1677
|
+
case "rename":
|
|
1678
|
+
return {
|
|
1679
|
+
kind: operation.kind,
|
|
1680
|
+
changeIndex: operation.changeIndex,
|
|
1681
|
+
oldPath: operation.oldPath,
|
|
1682
|
+
newPath: operation.newPath,
|
|
1683
|
+
overwrite: operation.overwrite,
|
|
1684
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
1685
|
+
};
|
|
1686
|
+
case "create":
|
|
1687
|
+
return {
|
|
1688
|
+
kind: operation.kind,
|
|
1689
|
+
changeIndex: operation.changeIndex,
|
|
1690
|
+
path: operation.path,
|
|
1691
|
+
overwrite: operation.overwrite,
|
|
1692
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
1693
|
+
};
|
|
1694
|
+
case "delete":
|
|
1695
|
+
return {
|
|
1696
|
+
kind: operation.kind,
|
|
1697
|
+
changeIndex: operation.changeIndex,
|
|
1698
|
+
path: operation.path,
|
|
1699
|
+
recursive: operation.recursive,
|
|
1700
|
+
ignoreIfNotExists: operation.ignoreIfNotExists
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// ../lsp-core/src/lsp/workspace-edit-types.ts
|
|
1708
|
+
class WorkspaceEditValidationError extends Error {
|
|
1709
|
+
changeIndex;
|
|
1710
|
+
detail;
|
|
1711
|
+
name = "WorkspaceEditValidationError";
|
|
1712
|
+
constructor(changeIndex, detail) {
|
|
1713
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
1714
|
+
this.changeIndex = changeIndex;
|
|
1715
|
+
this.detail = detail;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
|
|
1720
|
+
function isRecord2(value) {
|
|
1721
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1722
|
+
}
|
|
1723
|
+
function parsePosition(value) {
|
|
1724
|
+
if (!isRecord2(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
|
|
1725
|
+
return null;
|
|
1726
|
+
}
|
|
1727
|
+
return { line: value["line"], character: value["character"] };
|
|
1728
|
+
}
|
|
1729
|
+
function parseRange(value) {
|
|
1730
|
+
if (!isRecord2(value))
|
|
1731
|
+
return null;
|
|
1732
|
+
const start = parsePosition(value["start"]);
|
|
1733
|
+
const end = parsePosition(value["end"]);
|
|
1734
|
+
return start && end ? { start, end } : null;
|
|
1735
|
+
}
|
|
1736
|
+
function parseTextEdits(value, changeIndex) {
|
|
1737
|
+
if (!Array.isArray(value)) {
|
|
1738
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
|
|
1739
|
+
}
|
|
1740
|
+
const edits = [];
|
|
1741
|
+
for (const candidate of value) {
|
|
1742
|
+
if (!isRecord2(candidate) || typeof candidate["newText"] !== "string") {
|
|
1743
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
|
|
1744
|
+
}
|
|
1745
|
+
if ("annotationId" in candidate) {
|
|
1746
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
|
|
1747
|
+
}
|
|
1748
|
+
const range = parseRange(candidate["range"]);
|
|
1749
|
+
if (!range)
|
|
1750
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
|
|
1751
|
+
edits.push({ range, newText: candidate["newText"] });
|
|
1752
|
+
}
|
|
1753
|
+
return edits;
|
|
1754
|
+
}
|
|
1755
|
+
function parseBooleanOption(options, key, changeIndex) {
|
|
1756
|
+
const value = options[key];
|
|
1757
|
+
if (value === undefined)
|
|
1758
|
+
return false;
|
|
1759
|
+
if (typeof value !== "boolean") {
|
|
1760
|
+
throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
|
|
1761
|
+
}
|
|
1762
|
+
return value;
|
|
1763
|
+
}
|
|
1764
|
+
function parseOptions(value, allowed, changeIndex) {
|
|
1765
|
+
if (value === undefined)
|
|
1766
|
+
return {};
|
|
1767
|
+
if (!isRecord2(value))
|
|
1768
|
+
throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
|
|
1769
|
+
for (const key of Object.keys(value)) {
|
|
1770
|
+
if (!allowed.includes(key))
|
|
1771
|
+
throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
|
|
1772
|
+
}
|
|
1773
|
+
const parsed = {};
|
|
1774
|
+
for (const key of allowed)
|
|
1775
|
+
parsed[key] = parseBooleanOption(value, key, changeIndex);
|
|
1776
|
+
return parsed;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
|
|
1780
|
+
function parseResourceChange(input) {
|
|
1781
|
+
const kind = input.change["kind"];
|
|
1782
|
+
if (kind === "create" || kind === "delete") {
|
|
1783
|
+
parseSinglePathResource(input, kind);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
if (kind !== "rename") {
|
|
1787
|
+
throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
|
|
1788
|
+
}
|
|
1789
|
+
parseRename(input);
|
|
1790
|
+
}
|
|
1791
|
+
function parseSinglePathResource(input, kind) {
|
|
1792
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1793
|
+
if (typeof change["uri"] !== "string")
|
|
1794
|
+
throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
|
|
1795
|
+
const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
|
|
1796
|
+
if (!resolvedPath.success) {
|
|
1797
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (kind === "create") {
|
|
1801
|
+
const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
1802
|
+
target.operations.push({
|
|
1803
|
+
kind,
|
|
1804
|
+
changeIndex,
|
|
1805
|
+
path: resolvedPath.path,
|
|
1806
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1807
|
+
overwrite: options2["overwrite"] ?? false,
|
|
1808
|
+
ignoreIfExists: options2["ignoreIfExists"] ?? false,
|
|
1809
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
1810
|
+
});
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
|
|
1814
|
+
target.operations.push({
|
|
1815
|
+
kind,
|
|
1816
|
+
changeIndex,
|
|
1817
|
+
path: resolvedPath.path,
|
|
1818
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1819
|
+
recursive: options["recursive"] ?? false,
|
|
1820
|
+
ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
|
|
1821
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
function parseRename(input) {
|
|
1825
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1826
|
+
if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
|
|
1827
|
+
throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
|
|
1828
|
+
}
|
|
1829
|
+
const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
|
|
1830
|
+
const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
|
|
1831
|
+
if (!oldPath.success || !newPath.success) {
|
|
1832
|
+
target.failures.push({
|
|
1833
|
+
changeIndex,
|
|
1834
|
+
message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
|
|
1835
|
+
});
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
1839
|
+
target.operations.push({
|
|
1840
|
+
kind: "rename",
|
|
1841
|
+
changeIndex,
|
|
1842
|
+
oldPath: oldPath.path,
|
|
1843
|
+
newPath: newPath.path,
|
|
1844
|
+
reportedOldPath: oldPath.requestedPath,
|
|
1845
|
+
reportedNewPath: newPath.requestedPath,
|
|
1846
|
+
overwrite: options["overwrite"] ?? false,
|
|
1847
|
+
ignoreIfExists: options["ignoreIfExists"] ?? false,
|
|
1848
|
+
followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
// ../lsp-core/src/lsp/workspace-edit-parser.ts
|
|
1853
|
+
function failureResult(failures) {
|
|
1854
|
+
const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
|
|
1855
|
+
const first = sorted[0];
|
|
1856
|
+
return {
|
|
1857
|
+
success: false,
|
|
1858
|
+
filesModified: [],
|
|
1859
|
+
totalEdits: 0,
|
|
1860
|
+
errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
|
|
1861
|
+
...first ? { failedChange: first.changeIndex } : {}
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
function parseWorkspaceEdit(edit, workspaceRoot) {
|
|
1865
|
+
if (!isRecord2(edit))
|
|
1866
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
|
|
1867
|
+
if (edit["changeAnnotations"] !== undefined) {
|
|
1868
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
|
|
1869
|
+
}
|
|
1870
|
+
const hasChanges = edit["changes"] !== undefined;
|
|
1871
|
+
const hasDocumentChanges = edit["documentChanges"] !== undefined;
|
|
1872
|
+
if (hasChanges && hasDocumentChanges) {
|
|
1873
|
+
return {
|
|
1874
|
+
operations: [],
|
|
1875
|
+
failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
const target = { operations: [], failures: [] };
|
|
1879
|
+
if (hasChanges)
|
|
1880
|
+
return parseChanges(edit["changes"], workspaceRoot, target);
|
|
1881
|
+
return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
|
|
1882
|
+
}
|
|
1883
|
+
function parseChanges(value, workspaceRoot, target) {
|
|
1884
|
+
if (!isRecord2(value))
|
|
1885
|
+
return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
|
|
1886
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
1887
|
+
for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
|
|
1888
|
+
const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
|
|
1889
|
+
if (!resolvedPath.success) {
|
|
1890
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
try {
|
|
1894
|
+
target.operations.push({
|
|
1895
|
+
kind: "text",
|
|
1896
|
+
changeIndex,
|
|
1897
|
+
path: resolvedPath.path,
|
|
1898
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1899
|
+
edits: parseTextEdits(rawEdits, changeIndex),
|
|
1900
|
+
version: null
|
|
1901
|
+
});
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
1904
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
throw error;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
return target;
|
|
1911
|
+
}
|
|
1912
|
+
function parseDocumentChanges(value, workspaceRoot, target) {
|
|
1913
|
+
if (value === undefined)
|
|
1914
|
+
return target;
|
|
1915
|
+
if (!Array.isArray(value)) {
|
|
1916
|
+
return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
|
|
1917
|
+
}
|
|
1918
|
+
for (const [changeIndex, change] of value.entries()) {
|
|
1919
|
+
try {
|
|
1920
|
+
parseDocumentChange({ change, changeIndex, workspaceRoot, target });
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
1923
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
throw error;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return target;
|
|
1930
|
+
}
|
|
1931
|
+
function parseDocumentChange(input) {
|
|
1932
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1933
|
+
if (!isRecord2(change))
|
|
1934
|
+
throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
|
|
1935
|
+
if ("annotationId" in change) {
|
|
1936
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
|
|
1937
|
+
}
|
|
1938
|
+
if (typeof change["kind"] === "string") {
|
|
1939
|
+
parseResourceChange({ change, changeIndex, workspaceRoot, target });
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const identifier = change["textDocument"];
|
|
1943
|
+
if (!isRecord2(identifier) || typeof identifier["uri"] !== "string") {
|
|
1944
|
+
throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
|
|
1945
|
+
}
|
|
1946
|
+
const version = identifier["version"];
|
|
1947
|
+
if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
|
|
1948
|
+
throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
|
|
1949
|
+
}
|
|
1950
|
+
const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
|
|
1951
|
+
if (!resolvedPath.success) {
|
|
1952
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
target.operations.push({
|
|
1956
|
+
kind: "text",
|
|
1957
|
+
changeIndex,
|
|
1958
|
+
path: resolvedPath.path,
|
|
1959
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1960
|
+
edits: parseTextEdits(change["edits"], changeIndex),
|
|
1961
|
+
version
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
1966
|
+
import { dirname as dirname2, relative as relative3, resolve as resolve3 } from "node:path";
|
|
1967
|
+
|
|
1968
|
+
// ../lsp-core/src/lsp/workspace-edit-text.ts
|
|
1969
|
+
function comparePosition(left, right) {
|
|
1970
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
1971
|
+
}
|
|
1972
|
+
function positionsEqual(left, right) {
|
|
1973
|
+
return left.line === right.line && left.character === right.character;
|
|
1974
|
+
}
|
|
1975
|
+
function rangesEqual(left, right) {
|
|
1976
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
1977
|
+
}
|
|
1978
|
+
function isEmptyRange(range) {
|
|
1979
|
+
return positionsEqual(range.start, range.end);
|
|
1980
|
+
}
|
|
1981
|
+
function formatRange(range) {
|
|
1982
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
1983
|
+
}
|
|
1984
|
+
function validatePosition(position, label, context) {
|
|
1985
|
+
const { lines, changeIndex } = context;
|
|
1986
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
1987
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
1988
|
+
}
|
|
1989
|
+
if (position.line < 0 || position.character < 0) {
|
|
1990
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
1991
|
+
}
|
|
1992
|
+
const line = lines[position.line];
|
|
1993
|
+
if (line === undefined) {
|
|
1994
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
1995
|
+
}
|
|
1996
|
+
if (position.character > line.length) {
|
|
1997
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
function validateRange(range, lines, changeIndex) {
|
|
2001
|
+
const context = { lines, changeIndex };
|
|
2002
|
+
validatePosition(range.start, "start", context);
|
|
2003
|
+
validatePosition(range.end, "end", context);
|
|
2004
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
2005
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
function sortAndDeduplicate(edits) {
|
|
2009
|
+
const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
|
|
2010
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
2011
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
2012
|
+
});
|
|
2013
|
+
const unique = [];
|
|
2014
|
+
for (const entry of sorted) {
|
|
2015
|
+
const previous = unique.at(-1);
|
|
2016
|
+
if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
unique.push(entry.edit);
|
|
2020
|
+
}
|
|
2021
|
+
return unique;
|
|
2022
|
+
}
|
|
2023
|
+
function validateNoOverlap(edits, changeIndex) {
|
|
2024
|
+
for (let index = 0;index < edits.length - 1; index += 1) {
|
|
2025
|
+
const later = edits[index];
|
|
2026
|
+
const earlier = edits[index + 1];
|
|
2027
|
+
if (later === undefined || earlier === undefined)
|
|
2028
|
+
continue;
|
|
2029
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
2030
|
+
throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
function applyNormalizedTextEdits(content, edits) {
|
|
2035
|
+
const lines = content.split(`
|
|
2036
|
+
`);
|
|
2037
|
+
for (const edit of edits) {
|
|
2038
|
+
const { start, end } = edit.range;
|
|
2039
|
+
const startLine = lines[start.line];
|
|
2040
|
+
const endLine = lines[end.line];
|
|
2041
|
+
if (startLine === undefined || endLine === undefined)
|
|
2042
|
+
continue;
|
|
2043
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
2044
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
|
|
2045
|
+
`));
|
|
2046
|
+
}
|
|
2047
|
+
return lines.join(`
|
|
2048
|
+
`);
|
|
2049
|
+
}
|
|
2050
|
+
function normalizeTextEdits(content, edits, changeIndex) {
|
|
2051
|
+
const lines = content.split(`
|
|
2052
|
+
`);
|
|
2053
|
+
for (const edit of edits) {
|
|
2054
|
+
validateRange(edit.range, lines, changeIndex);
|
|
2055
|
+
}
|
|
2056
|
+
const normalized = sortAndDeduplicate(edits);
|
|
2057
|
+
validateNoOverlap(normalized, changeIndex);
|
|
2058
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2062
|
+
function isSameOrDescendant2(candidate, parent) {
|
|
2063
|
+
const relativePath = relative3(parent, candidate);
|
|
2064
|
+
return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
|
|
2065
|
+
}
|
|
2066
|
+
function removeVirtualSubtree(virtual, path) {
|
|
2067
|
+
for (const candidate of [...virtual.keys()]) {
|
|
2068
|
+
if (isSameOrDescendant2(candidate, path))
|
|
2069
|
+
virtual.delete(candidate);
|
|
2070
|
+
}
|
|
2071
|
+
virtual.set(path, { kind: "missing" });
|
|
2072
|
+
}
|
|
2073
|
+
function moveVirtualSubtree(virtual, oldPath, newPath) {
|
|
2074
|
+
const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
|
|
2075
|
+
removeVirtualSubtree(virtual, oldPath);
|
|
2076
|
+
removeVirtualSubtree(virtual, newPath);
|
|
2077
|
+
for (const [candidate, entry] of moved) {
|
|
2078
|
+
const suffix = relative3(oldPath, candidate);
|
|
2079
|
+
virtual.set(suffix === "" ? newPath : resolve3(newPath, suffix), entry);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
function virtualDirectoryHasChildren(virtual, path) {
|
|
2083
|
+
for (const [candidate, entry] of virtual) {
|
|
2084
|
+
if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
|
|
2085
|
+
return true;
|
|
2086
|
+
}
|
|
2087
|
+
return false;
|
|
2088
|
+
}
|
|
2089
|
+
function requireVirtualParent(virtual, path, changeIndex) {
|
|
2090
|
+
if (virtual.get(dirname2(path))?.kind !== "directory") {
|
|
2091
|
+
throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
function simulateOperations(parsed, snapshots) {
|
|
2095
|
+
const virtual = new Map(snapshots);
|
|
2096
|
+
const planned = [];
|
|
2097
|
+
const failures = [];
|
|
2098
|
+
for (const operation of parsed) {
|
|
2099
|
+
try {
|
|
2100
|
+
planned.push(simulateOperation(operation, virtual));
|
|
2101
|
+
} catch (error) {
|
|
2102
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2103
|
+
failures.push({ changeIndex: operation.changeIndex, message: error.detail });
|
|
2104
|
+
continue;
|
|
2105
|
+
}
|
|
2106
|
+
throw error;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return { operations: planned, failures };
|
|
2110
|
+
}
|
|
2111
|
+
function simulateOperation(operation, virtual) {
|
|
2112
|
+
switch (operation.kind) {
|
|
2113
|
+
case "text":
|
|
2114
|
+
return simulateText(operation, virtual);
|
|
2115
|
+
case "create":
|
|
2116
|
+
return simulateCreate(operation, virtual);
|
|
2117
|
+
case "rename":
|
|
2118
|
+
return simulateRename(operation, virtual);
|
|
2119
|
+
case "delete":
|
|
2120
|
+
return simulateDelete(operation, virtual);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
function rejectSymbolicLink(operation) {
|
|
2124
|
+
if (operation.followedSymbolicLink) {
|
|
2125
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
function simulateText(operation, virtual) {
|
|
2129
|
+
const entry = virtual.get(operation.path);
|
|
2130
|
+
if (entry?.kind !== "file")
|
|
2131
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
|
|
2132
|
+
const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
|
|
2133
|
+
virtual.set(operation.path, { kind: "file", content: normalized.text });
|
|
2134
|
+
return {
|
|
2135
|
+
kind: "text",
|
|
2136
|
+
changeIndex: operation.changeIndex,
|
|
2137
|
+
path: operation.path,
|
|
2138
|
+
beforeText: entry.content,
|
|
2139
|
+
afterText: normalized.text,
|
|
2140
|
+
editCount: normalized.edits.length,
|
|
2141
|
+
documentVersion: operation.version
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
function simulateCreate(operation, virtual) {
|
|
2145
|
+
rejectSymbolicLink(operation);
|
|
2146
|
+
requireVirtualParent(virtual, operation.path, operation.changeIndex);
|
|
2147
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2148
|
+
if (target.kind !== "missing") {
|
|
2149
|
+
if (operation.overwrite && target.kind === "file") {
|
|
2150
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2151
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
|
|
2152
|
+
}
|
|
2153
|
+
if (operation.ignoreIfExists)
|
|
2154
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2155
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
|
|
2156
|
+
}
|
|
2157
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2158
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
|
|
2159
|
+
}
|
|
2160
|
+
function simulateRename(operation, virtual) {
|
|
2161
|
+
rejectSymbolicLink(operation);
|
|
2162
|
+
const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
|
|
2163
|
+
if (source.kind === "missing") {
|
|
2164
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
|
|
2165
|
+
}
|
|
2166
|
+
if (operation.oldPath === operation.newPath)
|
|
2167
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2168
|
+
if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
|
|
2169
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
|
|
2170
|
+
}
|
|
2171
|
+
requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
|
|
2172
|
+
const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
|
|
2173
|
+
if (destination.kind !== "missing" && !operation.overwrite) {
|
|
2174
|
+
if (operation.ignoreIfExists)
|
|
2175
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2176
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
|
|
2177
|
+
}
|
|
2178
|
+
moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
|
|
2179
|
+
return {
|
|
2180
|
+
kind: "rename",
|
|
2181
|
+
changeIndex: operation.changeIndex,
|
|
2182
|
+
oldPath: operation.oldPath,
|
|
2183
|
+
newPath: operation.newPath,
|
|
2184
|
+
sourceKind: source.kind,
|
|
2185
|
+
replaceDestination: destination.kind !== "missing"
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
function simulateDelete(operation, virtual) {
|
|
2189
|
+
rejectSymbolicLink(operation);
|
|
2190
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2191
|
+
if (target.kind === "missing") {
|
|
2192
|
+
if (operation.ignoreIfNotExists)
|
|
2193
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2194
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
|
|
2195
|
+
}
|
|
2196
|
+
if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
|
|
2197
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
|
|
2198
|
+
}
|
|
2199
|
+
removeVirtualSubtree(virtual, operation.path);
|
|
2200
|
+
return {
|
|
2201
|
+
kind: "delete",
|
|
2202
|
+
changeIndex: operation.changeIndex,
|
|
2203
|
+
path: operation.path,
|
|
2204
|
+
targetKind: target.kind,
|
|
2205
|
+
recursive: operation.recursive
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// ../lsp-core/src/lsp/workspace-edit-snapshot.ts
|
|
2210
|
+
import { existsSync as existsSync4, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2211
|
+
import { dirname as dirname3, resolve as resolve4 } from "node:path";
|
|
2212
|
+
class WorkspaceSnapshotBuilder {
|
|
2213
|
+
workspaceRoot;
|
|
2214
|
+
snapshots = new Map;
|
|
2215
|
+
constructor(workspaceRoot) {
|
|
2216
|
+
this.workspaceRoot = workspaceRoot;
|
|
2217
|
+
}
|
|
2218
|
+
build(operations) {
|
|
2219
|
+
this.add(this.workspaceRoot, false);
|
|
2220
|
+
for (const operation of operations) {
|
|
2221
|
+
switch (operation.kind) {
|
|
2222
|
+
case "rename":
|
|
2223
|
+
this.add(operation.oldPath, true);
|
|
2224
|
+
this.add(operation.newPath, true);
|
|
2225
|
+
break;
|
|
2226
|
+
case "delete":
|
|
2227
|
+
this.add(operation.path, true);
|
|
2228
|
+
break;
|
|
2229
|
+
case "text":
|
|
2230
|
+
case "create":
|
|
2231
|
+
this.add(operation.path, false);
|
|
2232
|
+
break;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return this.snapshots;
|
|
2236
|
+
}
|
|
2237
|
+
add(path, includeChildren) {
|
|
2238
|
+
let candidate = path;
|
|
2239
|
+
while (true) {
|
|
2240
|
+
const existing = this.snapshots.get(candidate);
|
|
2241
|
+
if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
|
|
2242
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
|
|
2243
|
+
}
|
|
2244
|
+
if (candidate === this.workspaceRoot)
|
|
2245
|
+
break;
|
|
2246
|
+
candidate = dirname3(candidate);
|
|
2247
|
+
}
|
|
2248
|
+
if (!includeChildren || !existsSync4(path) || !lstatSync3(path).isDirectory())
|
|
2249
|
+
return;
|
|
2250
|
+
for (const child of readdirSync2(path))
|
|
2251
|
+
this.add(resolve4(path, child), true);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
function snapshotOperations(operations, workspaceRoot) {
|
|
2255
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// ../lsp-core/src/lsp/workspace-edit-plan.ts
|
|
2259
|
+
class PlanPathIndex {
|
|
2260
|
+
firstChangeByPath = new Map;
|
|
2261
|
+
reportedPathByCanonical = new Map;
|
|
2262
|
+
build(operations) {
|
|
2263
|
+
for (const operation of operations) {
|
|
2264
|
+
switch (operation.kind) {
|
|
2265
|
+
case "rename":
|
|
2266
|
+
this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
|
|
2267
|
+
this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
|
|
2268
|
+
break;
|
|
2269
|
+
case "text":
|
|
2270
|
+
case "create":
|
|
2271
|
+
case "delete":
|
|
2272
|
+
this.add(operation.path, operation.reportedPath, operation.changeIndex);
|
|
2273
|
+
break;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
add(path, reportedPath2, changeIndex) {
|
|
2278
|
+
if (!this.firstChangeByPath.has(path))
|
|
2279
|
+
this.firstChangeByPath.set(path, changeIndex);
|
|
2280
|
+
if (!this.reportedPathByCanonical.has(path))
|
|
2281
|
+
this.reportedPathByCanonical.set(path, reportedPath2);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
function fingerprintWorkspaceEdit(edit, workspaceRoot) {
|
|
2285
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2286
|
+
if (!root.success)
|
|
2287
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2288
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2289
|
+
if (parsed.failures.length > 0)
|
|
2290
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2291
|
+
return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
|
|
2292
|
+
}
|
|
2293
|
+
function planWorkspaceEdit(edit, workspaceRoot) {
|
|
2294
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2295
|
+
if (!root.success)
|
|
2296
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2297
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2298
|
+
if (parsed.failures.length > 0)
|
|
2299
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2300
|
+
let snapshots;
|
|
2301
|
+
try {
|
|
2302
|
+
snapshots = snapshotOperations(parsed.operations, root.path);
|
|
2303
|
+
} catch (error) {
|
|
2304
|
+
return {
|
|
2305
|
+
success: false,
|
|
2306
|
+
result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
const simulated = simulateOperations(parsed.operations, snapshots);
|
|
2310
|
+
if (simulated.failures.length > 0)
|
|
2311
|
+
return { success: false, result: failureResult(simulated.failures) };
|
|
2312
|
+
const paths = new PlanPathIndex;
|
|
2313
|
+
paths.build(parsed.operations);
|
|
2314
|
+
const plan = {
|
|
2315
|
+
workspaceRoot: root.path,
|
|
2316
|
+
operations: simulated.operations,
|
|
2317
|
+
snapshots,
|
|
2318
|
+
firstChangeByPath: paths.firstChangeByPath,
|
|
2319
|
+
reportedPathByCanonical: paths.reportedPathByCanonical,
|
|
2320
|
+
fingerprint: canonicalFingerprint(parsed.operations)
|
|
2321
|
+
};
|
|
2322
|
+
return { success: true, plan };
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// ../lsp-core/src/lsp/workspace-mutation-controller.ts
|
|
2326
|
+
function failure(message, failedChange, base) {
|
|
2327
|
+
return {
|
|
2328
|
+
success: false,
|
|
2329
|
+
filesModified: base?.filesModified ?? [],
|
|
2330
|
+
totalEdits: base?.totalEdits ?? 0,
|
|
2331
|
+
errors: [message],
|
|
2332
|
+
...failedChange === undefined ? {} : { failedChange },
|
|
2333
|
+
...base?.lateAbort ? { lateAbort: true } : {}
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
function responseFor(result) {
|
|
2337
|
+
if (result.success)
|
|
2338
|
+
return { applied: true };
|
|
2339
|
+
return {
|
|
2340
|
+
applied: false,
|
|
2341
|
+
failureReason: result.errors[0] ?? "workspace edit failed",
|
|
2342
|
+
...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
function isRecord3(value) {
|
|
2346
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
class WorkspaceMutationController {
|
|
2350
|
+
workspaceRoot;
|
|
2351
|
+
documents;
|
|
2352
|
+
activeLease = null;
|
|
2353
|
+
nextLeaseId = 1;
|
|
2354
|
+
io;
|
|
2355
|
+
constructor(workspaceRoot, documents) {
|
|
2356
|
+
this.workspaceRoot = workspaceRoot;
|
|
2357
|
+
this.documents = documents;
|
|
2358
|
+
}
|
|
2359
|
+
setIo(io) {
|
|
2360
|
+
this.io = io;
|
|
2361
|
+
}
|
|
2362
|
+
acquire(signal) {
|
|
2363
|
+
if (this.activeLease)
|
|
2364
|
+
return { success: false, result: failure("workspace mutation is already in progress") };
|
|
2365
|
+
if (signal?.aborted)
|
|
2366
|
+
return { success: false, result: failure("cancelled before mutating request") };
|
|
2367
|
+
const lease = {
|
|
2368
|
+
id: this.nextLeaseId,
|
|
2369
|
+
phase: "idle",
|
|
2370
|
+
...signal === undefined ? {} : { signal }
|
|
2371
|
+
};
|
|
2372
|
+
this.nextLeaseId += 1;
|
|
2373
|
+
this.activeLease = lease;
|
|
2374
|
+
return { success: true, lease };
|
|
2375
|
+
}
|
|
2376
|
+
release(lease) {
|
|
2377
|
+
if (this.activeLease?.id !== lease.id)
|
|
2378
|
+
return;
|
|
2379
|
+
this.activeLease.phase = "sealed";
|
|
2380
|
+
this.activeLease = null;
|
|
2381
|
+
}
|
|
2382
|
+
isBeforeCommit(lease) {
|
|
2383
|
+
return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
|
|
2384
|
+
}
|
|
2385
|
+
async handleApplyEdit(params) {
|
|
2386
|
+
const lease = this.activeLease;
|
|
2387
|
+
if (!lease)
|
|
2388
|
+
return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
|
|
2389
|
+
if (lease.phase !== "idle") {
|
|
2390
|
+
return {
|
|
2391
|
+
applied: false,
|
|
2392
|
+
failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
lease.phase = "applying";
|
|
2396
|
+
lease.applyCompletion = new Promise((resolve5) => {
|
|
2397
|
+
lease.resolveApply = resolve5;
|
|
2398
|
+
});
|
|
2399
|
+
const edit = isRecord3(params) ? params["edit"] : undefined;
|
|
2400
|
+
const record = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
|
|
2401
|
+
lease.serverApply = record;
|
|
2402
|
+
lease.phase = "settled";
|
|
2403
|
+
lease.resolveApply?.();
|
|
2404
|
+
return responseFor(record.result);
|
|
2405
|
+
}
|
|
2406
|
+
async reconcileRename(leaseToken, edit) {
|
|
2407
|
+
const lease = this.requireActiveLease(leaseToken);
|
|
2408
|
+
if (!lease)
|
|
2409
|
+
return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
|
|
2410
|
+
if (lease.phase === "applying")
|
|
2411
|
+
await lease.applyCompletion;
|
|
2412
|
+
if (lease.serverApply)
|
|
2413
|
+
return this.reconcileServerApply(lease.serverApply, edit);
|
|
2414
|
+
lease.phase = "sealed";
|
|
2415
|
+
if (!edit)
|
|
2416
|
+
return { edit, apply: failure("No edit provided") };
|
|
2417
|
+
const applied = await this.applyEdit(edit, lease);
|
|
2418
|
+
return { edit, apply: applied.result };
|
|
2419
|
+
}
|
|
2420
|
+
reconcileServerApply(record, edit) {
|
|
2421
|
+
if (!edit)
|
|
2422
|
+
return { edit, apply: record.result };
|
|
2423
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
|
|
2424
|
+
if (fingerprint.success && record.fingerprint !== null && fingerprint.fingerprint === record.fingerprint) {
|
|
2425
|
+
return { edit, apply: record.result };
|
|
2426
|
+
}
|
|
2427
|
+
return {
|
|
2428
|
+
edit,
|
|
2429
|
+
apply: failure("rename result conflicts with server-applied workspace edit", 0, record.result)
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
async applyEdit(edit, lease) {
|
|
2433
|
+
const planned = planWorkspaceEdit(edit, this.workspaceRoot);
|
|
2434
|
+
if (!planned.success)
|
|
2435
|
+
return { fingerprint: null, result: planned.result };
|
|
2436
|
+
const versionFailure = this.documents.validateVersions(planned.plan.operations);
|
|
2437
|
+
if (versionFailure) {
|
|
2438
|
+
return {
|
|
2439
|
+
fingerprint: planned.plan.fingerprint,
|
|
2440
|
+
result: failure(versionFailure.message, versionFailure.changeIndex)
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
const commit = commitWorkspaceEditPlan(planned.plan, {
|
|
2444
|
+
...lease.signal === undefined ? {} : { signal: lease.signal },
|
|
2445
|
+
...this.io === undefined ? {} : { io: this.io }
|
|
2446
|
+
});
|
|
2447
|
+
let result = commit.result;
|
|
2448
|
+
if (commit.delta.operations.length > 0) {
|
|
2449
|
+
try {
|
|
2450
|
+
await this.documents.synchronize(commit.delta);
|
|
2451
|
+
} catch (error) {
|
|
2452
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2453
|
+
result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
if (lease.signal?.aborted && !result.lateAbort)
|
|
2457
|
+
result = { ...result, lateAbort: true };
|
|
2458
|
+
return { fingerprint: planned.plan.fingerprint, result };
|
|
2459
|
+
}
|
|
2460
|
+
requireActiveLease(lease) {
|
|
2461
|
+
return this.activeLease?.id === lease.id ? this.activeLease : null;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
995
2465
|
// ../lsp-core/src/lsp/client.ts
|
|
996
|
-
var
|
|
997
|
-
var
|
|
2466
|
+
var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
|
|
2467
|
+
var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
998
2468
|
|
|
999
2469
|
class LspClient extends LspClientConnection {
|
|
1000
|
-
openedFiles = new Set;
|
|
1001
|
-
documentVersions = new Map;
|
|
1002
|
-
lastSyncedText = new Map;
|
|
1003
2470
|
diagnosticPullErrors = [];
|
|
2471
|
+
documents;
|
|
2472
|
+
workspaceMutations;
|
|
2473
|
+
diagnosticsFreshnessTimeoutMs;
|
|
2474
|
+
constructor(root, server, options = {}) {
|
|
2475
|
+
super(root, server, options);
|
|
2476
|
+
this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
|
|
2477
|
+
this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
|
|
2478
|
+
versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
|
|
2479
|
+
});
|
|
2480
|
+
this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
|
|
2481
|
+
this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
|
|
2482
|
+
}
|
|
1004
2483
|
getDiagnosticPullErrors() {
|
|
1005
2484
|
return this.diagnosticPullErrors;
|
|
1006
2485
|
}
|
|
1007
2486
|
async openFile(filePath) {
|
|
1008
|
-
const absPath =
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
textDocument: {
|
|
1017
|
-
uri,
|
|
1018
|
-
languageId,
|
|
1019
|
-
version,
|
|
1020
|
-
text
|
|
1021
|
-
}
|
|
1022
|
-
});
|
|
1023
|
-
this.openedFiles.add(absPath);
|
|
1024
|
-
this.documentVersions.set(uri, version);
|
|
1025
|
-
this.lastSyncedText.set(uri, text);
|
|
1026
|
-
await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
const prevText = this.lastSyncedText.get(uri);
|
|
1030
|
-
if (prevText === text) {
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
|
|
1034
|
-
this.documentVersions.set(uri, nextVersion);
|
|
1035
|
-
this.lastSyncedText.set(uri, text);
|
|
1036
|
-
await this.sendNotification("textDocument/didChange", {
|
|
1037
|
-
textDocument: { uri, version: nextVersion },
|
|
1038
|
-
contentChanges: [{ text }]
|
|
1039
|
-
});
|
|
1040
|
-
await this.sendNotification("textDocument/didSave", {
|
|
1041
|
-
textDocument: { uri },
|
|
1042
|
-
text
|
|
1043
|
-
});
|
|
2487
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2488
|
+
await this.documents.openFile(absPath);
|
|
2489
|
+
}
|
|
2490
|
+
getOpenDocumentVersion(filePath) {
|
|
2491
|
+
return this.documents.getVersion(this.resolveWorkspacePath(filePath));
|
|
2492
|
+
}
|
|
2493
|
+
getStoredDiagnostics(uri) {
|
|
2494
|
+
return [...this.documents.getStoredDiagnostics(uri)];
|
|
1044
2495
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
2496
|
+
setWorkspaceEditIo(io) {
|
|
2497
|
+
this.workspaceMutations.setIo(io);
|
|
2498
|
+
}
|
|
2499
|
+
handlePublishDiagnostics(params) {
|
|
2500
|
+
super.handlePublishDiagnostics(params);
|
|
2501
|
+
this.documents.recordPublishedDiagnostics(params);
|
|
2502
|
+
}
|
|
2503
|
+
async definition(filePath, line, character, signal) {
|
|
2504
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1047
2505
|
await this.openFile(absPath);
|
|
2506
|
+
const options = signal === undefined ? {} : { signal };
|
|
1048
2507
|
return this.sendRequest("textDocument/definition", {
|
|
1049
|
-
textDocument: { uri:
|
|
2508
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1050
2509
|
position: { line: line - 1, character }
|
|
1051
|
-
});
|
|
2510
|
+
}, options);
|
|
1052
2511
|
}
|
|
1053
|
-
async references(filePath, line, character, includeDeclaration = true) {
|
|
1054
|
-
const absPath =
|
|
2512
|
+
async references(filePath, line, character, includeDeclaration = true, signal) {
|
|
2513
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1055
2514
|
await this.openFile(absPath);
|
|
2515
|
+
const options = signal === undefined ? {} : { signal };
|
|
1056
2516
|
return this.sendRequest("textDocument/references", {
|
|
1057
|
-
textDocument: { uri:
|
|
2517
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1058
2518
|
position: { line: line - 1, character },
|
|
1059
2519
|
context: { includeDeclaration }
|
|
1060
|
-
});
|
|
2520
|
+
}, options);
|
|
1061
2521
|
}
|
|
1062
|
-
async documentSymbols(filePath) {
|
|
1063
|
-
const absPath =
|
|
2522
|
+
async documentSymbols(filePath, signal) {
|
|
2523
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1064
2524
|
await this.openFile(absPath);
|
|
2525
|
+
const options = signal === undefined ? {} : { signal };
|
|
1065
2526
|
return this.sendRequest("textDocument/documentSymbol", {
|
|
1066
|
-
textDocument: { uri:
|
|
1067
|
-
});
|
|
2527
|
+
textDocument: { uri: pathToFileURL3(absPath).href }
|
|
2528
|
+
}, options);
|
|
1068
2529
|
}
|
|
1069
|
-
async workspaceSymbols(query) {
|
|
1070
|
-
|
|
2530
|
+
async workspaceSymbols(query, signal) {
|
|
2531
|
+
const options = signal === undefined ? {} : { signal };
|
|
2532
|
+
return this.sendRequest("workspace/symbol", { query }, options);
|
|
1071
2533
|
}
|
|
1072
2534
|
isUnsupportedDiagnosticPullError(error) {
|
|
1073
2535
|
if (!(error instanceof Error))
|
|
@@ -1077,43 +2539,174 @@ class LspClient extends LspClientConnection {
|
|
|
1077
2539
|
return true;
|
|
1078
2540
|
return /unsupported|not supported|method not found|unknown request/i.test(error.message);
|
|
1079
2541
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
1087
|
-
textDocument: { uri }
|
|
1088
|
-
});
|
|
1089
|
-
if (result.items) {
|
|
1090
|
-
return { items: result.items };
|
|
2542
|
+
freshnessTimeout(absPath) {
|
|
2543
|
+
return {
|
|
2544
|
+
items: [],
|
|
2545
|
+
transientError: {
|
|
2546
|
+
kind: "freshness_timeout",
|
|
2547
|
+
message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
|
|
1091
2548
|
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
parseDiagnosticPullReport(value) {
|
|
2552
|
+
if (value.kind === "unchanged") {
|
|
2553
|
+
return {
|
|
2554
|
+
type: "unchanged",
|
|
2555
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
return {
|
|
2559
|
+
type: "full",
|
|
2560
|
+
diagnostics: value.items ?? [],
|
|
2561
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
async diagnostics(filePath, signal) {
|
|
2565
|
+
signal?.throwIfAborted();
|
|
2566
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2567
|
+
const uri = pathToFileURL3(absPath).href;
|
|
2568
|
+
await this.openFile(absPath);
|
|
2569
|
+
const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
|
|
2570
|
+
for (;; ) {
|
|
2571
|
+
signal?.throwIfAborted();
|
|
2572
|
+
const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
|
|
2573
|
+
if (!snapshot)
|
|
2574
|
+
return this.freshnessTimeout(absPath);
|
|
2575
|
+
const push = this.documents.resolvePushDiagnostics(snapshot);
|
|
2576
|
+
if (push.status === "ready")
|
|
2577
|
+
return { items: [...push.diagnostics] };
|
|
2578
|
+
let pushFallbackOnly = !this.isDiagnosticPullSupported();
|
|
2579
|
+
if (!pushFallbackOnly) {
|
|
2580
|
+
const cached = this.documents.getPullCache(snapshot);
|
|
2581
|
+
try {
|
|
2582
|
+
const remainingMs2 = deadlineAt - Date.now();
|
|
2583
|
+
if (remainingMs2 <= 0)
|
|
2584
|
+
return this.freshnessTimeout(absPath);
|
|
2585
|
+
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
2586
|
+
textDocument: { uri },
|
|
2587
|
+
...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
|
|
2588
|
+
}, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
|
|
2589
|
+
if (!this.documents.isCurrentSnapshot(snapshot))
|
|
2590
|
+
continue;
|
|
2591
|
+
const report = this.parseDiagnosticPullReport(result);
|
|
2592
|
+
if (report.type === "full") {
|
|
2593
|
+
this.documents.recordPullDiagnostics(snapshot, {
|
|
2594
|
+
kind: "full",
|
|
2595
|
+
diagnostics: report.diagnostics,
|
|
2596
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
2597
|
+
});
|
|
2598
|
+
return { items: [...report.diagnostics] };
|
|
2599
|
+
}
|
|
2600
|
+
if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
|
|
2601
|
+
return { items: [...cached.diagnostics] };
|
|
2602
|
+
}
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
if (this.isUnsupportedDiagnosticPullError(error)) {
|
|
2605
|
+
this.setDiagnosticPullSupported(false);
|
|
2606
|
+
pushFallbackOnly = true;
|
|
2607
|
+
} else if (error instanceof LspRequestTimeoutError) {
|
|
2608
|
+
pushFallbackOnly = true;
|
|
2609
|
+
} else {
|
|
2610
|
+
this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
2611
|
+
throw error;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
1095
2614
|
}
|
|
2615
|
+
if (!pushFallbackOnly)
|
|
2616
|
+
continue;
|
|
2617
|
+
const remainingMs = deadlineAt - Date.now();
|
|
2618
|
+
if (remainingMs <= 0)
|
|
2619
|
+
return this.freshnessTimeout(absPath);
|
|
2620
|
+
const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
|
|
2621
|
+
await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
|
|
1096
2622
|
}
|
|
1097
|
-
return { items: this.getStoredDiagnostics(uri) };
|
|
1098
2623
|
}
|
|
1099
|
-
async prepareRename(filePath, line, character) {
|
|
1100
|
-
const absPath =
|
|
2624
|
+
async prepareRename(filePath, line, character, signal) {
|
|
2625
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1101
2626
|
await this.openFile(absPath);
|
|
2627
|
+
const options = signal === undefined ? {} : { signal };
|
|
1102
2628
|
return this.sendRequest("textDocument/prepareRename", {
|
|
1103
|
-
textDocument: { uri:
|
|
2629
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1104
2630
|
position: { line: line - 1, character }
|
|
1105
|
-
});
|
|
2631
|
+
}, options);
|
|
1106
2632
|
}
|
|
1107
|
-
async rename(filePath, line, character, newName) {
|
|
1108
|
-
const absPath =
|
|
2633
|
+
async rename(filePath, line, character, newName, signal) {
|
|
2634
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1109
2635
|
await this.openFile(absPath);
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
2636
|
+
const acquired = this.workspaceMutations.acquire(signal);
|
|
2637
|
+
if (!acquired.success)
|
|
2638
|
+
return { edit: null, apply: acquired.result };
|
|
2639
|
+
const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
|
|
2640
|
+
try {
|
|
2641
|
+
const renameParams = {
|
|
2642
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
2643
|
+
position: { line: line - 1, character },
|
|
2644
|
+
newName
|
|
2645
|
+
};
|
|
2646
|
+
const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
|
|
2647
|
+
signal: preCommitSignal.signal
|
|
2648
|
+
});
|
|
2649
|
+
return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
|
|
2650
|
+
} finally {
|
|
2651
|
+
preCommitSignal?.dispose();
|
|
2652
|
+
this.workspaceMutations.release(acquired.lease);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
resolveWorkspacePath(filePath) {
|
|
2656
|
+
return resolve5(this.root, filePath);
|
|
1115
2657
|
}
|
|
1116
2658
|
}
|
|
2659
|
+
function waitForDiagnosticsActivity(wait, signal) {
|
|
2660
|
+
if (!signal)
|
|
2661
|
+
return wait;
|
|
2662
|
+
if (signal.aborted)
|
|
2663
|
+
return Promise.reject(abortError2(signal));
|
|
2664
|
+
return new Promise((resolve6, reject) => {
|
|
2665
|
+
const onAbort = () => {
|
|
2666
|
+
signal.removeEventListener("abort", onAbort);
|
|
2667
|
+
reject(abortError2(signal));
|
|
2668
|
+
};
|
|
2669
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2670
|
+
wait.then(() => {
|
|
2671
|
+
signal.removeEventListener("abort", onAbort);
|
|
2672
|
+
resolve6();
|
|
2673
|
+
}, (error) => {
|
|
2674
|
+
signal.removeEventListener("abort", onAbort);
|
|
2675
|
+
reject(error);
|
|
2676
|
+
});
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
function createPreCommitAbortSignal(source, isBeforeCommit) {
|
|
2680
|
+
if (!source)
|
|
2681
|
+
return;
|
|
2682
|
+
const controller = new AbortController;
|
|
2683
|
+
const onAbort = () => {
|
|
2684
|
+
if (isBeforeCommit() && !controller.signal.aborted)
|
|
2685
|
+
controller.abort(preCommitAbortReason(source));
|
|
2686
|
+
};
|
|
2687
|
+
if (source.aborted)
|
|
2688
|
+
onAbort();
|
|
2689
|
+
else
|
|
2690
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
2691
|
+
return {
|
|
2692
|
+
signal: controller.signal,
|
|
2693
|
+
dispose: () => source.removeEventListener("abort", onAbort)
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
function preCommitAbortReason(source) {
|
|
2697
|
+
const reason = source.reason;
|
|
2698
|
+
if (reason instanceof Error && reason.name !== "AbortError")
|
|
2699
|
+
return reason;
|
|
2700
|
+
return new Error("LSP request cancelled before workspace edit commit");
|
|
2701
|
+
}
|
|
2702
|
+
function abortError2(signal) {
|
|
2703
|
+
const reason = signal.reason;
|
|
2704
|
+
if (reason instanceof Error)
|
|
2705
|
+
return reason;
|
|
2706
|
+
const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
|
|
2707
|
+
error.name = "AbortError";
|
|
2708
|
+
return error;
|
|
2709
|
+
}
|
|
1117
2710
|
|
|
1118
2711
|
// ../lsp-core/src/lsp/process-signal-cleanup.ts
|
|
1119
2712
|
function installProcessSignalCleanup(cleanup) {
|
|
@@ -1144,7 +2737,7 @@ async function stopClientBestEffort(client) {
|
|
|
1144
2737
|
function awaitWithSignal(promise, signal) {
|
|
1145
2738
|
if (!signal)
|
|
1146
2739
|
return promise;
|
|
1147
|
-
return new Promise((
|
|
2740
|
+
return new Promise((resolve6, reject) => {
|
|
1148
2741
|
let settled = false;
|
|
1149
2742
|
const onAbort = () => {
|
|
1150
2743
|
if (settled)
|
|
@@ -1162,7 +2755,7 @@ function awaitWithSignal(promise, signal) {
|
|
|
1162
2755
|
return;
|
|
1163
2756
|
settled = true;
|
|
1164
2757
|
signal.removeEventListener("abort", onAbort);
|
|
1165
|
-
|
|
2758
|
+
resolve6(value);
|
|
1166
2759
|
}, (err) => {
|
|
1167
2760
|
if (settled)
|
|
1168
2761
|
return;
|
|
@@ -1428,9 +3021,6 @@ function errorResponse(id, code, message, data) {
|
|
|
1428
3021
|
function jsonRpcId(value) {
|
|
1429
3022
|
return typeof value === "string" || typeof value === "number" || value === null ? value : null;
|
|
1430
3023
|
}
|
|
1431
|
-
function messageFromError(error) {
|
|
1432
|
-
return error instanceof Error ? error.message : String(error);
|
|
1433
|
-
}
|
|
1434
3024
|
// ../mcp-stdio-core/src/transport.ts
|
|
1435
3025
|
var HEADER_SEPARATOR2 = Buffer.from(`\r
|
|
1436
3026
|
\r
|
|
@@ -1617,29 +3207,201 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
|
|
|
1617
3207
|
closed: () => isClosed
|
|
1618
3208
|
};
|
|
1619
3209
|
}
|
|
1620
|
-
// ../lsp-core/src/tools/diagnostics.ts
|
|
1621
|
-
import { resolve as resolve4 } from "node:path";
|
|
1622
|
-
|
|
1623
3210
|
// ../lsp-core/src/lsp/client-wrapper.ts
|
|
1624
|
-
import { existsSync as
|
|
1625
|
-
import { dirname as
|
|
3211
|
+
import { existsSync as existsSync9, statSync as statSync3 } from "node:fs";
|
|
3212
|
+
import { dirname as dirname6, join as join4, resolve as resolve7 } from "node:path";
|
|
1626
3213
|
|
|
1627
|
-
// ../lsp-core/src/
|
|
1628
|
-
import {
|
|
3214
|
+
// ../lsp-core/src/request-context.ts
|
|
3215
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3216
|
+
import { existsSync as existsSync5, realpathSync as realpathSync3, statSync as statSync2 } from "node:fs";
|
|
1629
3217
|
import { homedir } from "node:os";
|
|
1630
|
-
import { dirname, isAbsolute, join as join2 } from "node:path";
|
|
3218
|
+
import { basename as basename2, delimiter as delimiter2, dirname as dirname4, isAbsolute as isAbsolute2, join as join2, relative as relative4, resolve as resolve6 } from "node:path";
|
|
3219
|
+
|
|
3220
|
+
class LspRequestContextParseError extends Error {
|
|
3221
|
+
code;
|
|
3222
|
+
name = "LspRequestContextParseError";
|
|
3223
|
+
constructor(code, message) {
|
|
3224
|
+
super(message);
|
|
3225
|
+
this.code = code;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
class LspRequestContextUnavailableError extends Error {
|
|
3230
|
+
name = "LspRequestContextUnavailableError";
|
|
3231
|
+
constructor() {
|
|
3232
|
+
super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
var storage = new AsyncLocalStorage;
|
|
3236
|
+
var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
3237
|
+
var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
|
|
3238
|
+
function runWithRequestContext(context, fn) {
|
|
3239
|
+
return storage.run(context, fn);
|
|
3240
|
+
}
|
|
3241
|
+
function lspRequestContext() {
|
|
3242
|
+
const context = storage.getStore();
|
|
3243
|
+
if (!context)
|
|
3244
|
+
throw new LspRequestContextUnavailableError;
|
|
3245
|
+
return context;
|
|
3246
|
+
}
|
|
3247
|
+
function contextCwd() {
|
|
3248
|
+
return lspRequestContext().cwd;
|
|
3249
|
+
}
|
|
3250
|
+
function contextEnv(key) {
|
|
3251
|
+
const context = lspRequestContext();
|
|
3252
|
+
if (key === "LSP_TOOLS_MCP_PROJECT_CONFIG")
|
|
3253
|
+
return context.projectConfigPaths.join(delimiter2);
|
|
3254
|
+
if (key === "LSP_TOOLS_MCP_USER_CONFIG")
|
|
3255
|
+
return context.userConfigPath;
|
|
3256
|
+
if (key === "LSP_TOOLS_MCP_INSTALL_DECISIONS")
|
|
3257
|
+
return context.installDecisionsPath;
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
function createStandaloneMcpRequestContext(input = {}) {
|
|
3261
|
+
const env = input.env ?? process.env;
|
|
3262
|
+
const cwd = canonicalCwd(input.cwd ?? process.cwd());
|
|
3263
|
+
const home = input.homeDir ?? homedir();
|
|
3264
|
+
const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
|
|
3265
|
+
const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
|
|
3266
|
+
const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
|
|
3267
|
+
return parseLspRequestContext({
|
|
3268
|
+
cwd,
|
|
3269
|
+
projectConfigPaths,
|
|
3270
|
+
userConfigPath,
|
|
3271
|
+
installDecisionsPath,
|
|
3272
|
+
capabilities: { installDecisionTool: true }
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3275
|
+
function parseLspRequestContext(value) {
|
|
3276
|
+
if (!isRecord4(value)) {
|
|
3277
|
+
throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
|
|
3278
|
+
}
|
|
3279
|
+
rejectUnknownFields(value, CONTEXT_FIELDS, "context");
|
|
3280
|
+
const cwd = stringField(value, "cwd");
|
|
3281
|
+
const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
|
|
3282
|
+
const userConfigPath = stringField(value, "userConfigPath");
|
|
3283
|
+
const installDecisionsPath = stringField(value, "installDecisionsPath");
|
|
3284
|
+
const capabilities = capabilitiesField(value["capabilities"]);
|
|
3285
|
+
const canonical = canonicalCwd(cwd);
|
|
3286
|
+
for (const path of projectConfigPaths) {
|
|
3287
|
+
requireAbsolutePath(path, "projectConfigPaths");
|
|
3288
|
+
const projectPath = canonicalizeExistingOrNearestAncestor(path);
|
|
3289
|
+
if (!isPathInside(canonical, projectPath)) {
|
|
3290
|
+
throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
requireAbsolutePath(userConfigPath, "userConfigPath");
|
|
3294
|
+
requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
|
|
3295
|
+
return {
|
|
3296
|
+
cwd: canonical,
|
|
3297
|
+
projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
|
|
3298
|
+
userConfigPath,
|
|
3299
|
+
installDecisionsPath,
|
|
3300
|
+
capabilities
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3303
|
+
function translateProjectConfigEnv(value, cwd) {
|
|
3304
|
+
if (value === undefined || value.length === 0)
|
|
3305
|
+
return [join2(cwd, ".codex", "lsp-client.json")];
|
|
3306
|
+
return value.split(delimiter2).filter((entry) => entry.length > 0).map((entry) => isAbsolute2(entry) ? entry : join2(cwd, entry));
|
|
3307
|
+
}
|
|
3308
|
+
function translateHomeConfigEnv(value, home, fallback) {
|
|
3309
|
+
if (value === undefined || value.length === 0)
|
|
3310
|
+
return join2(home, fallback);
|
|
3311
|
+
return isAbsolute2(value) ? value : join2(home, value);
|
|
3312
|
+
}
|
|
3313
|
+
function canonicalCwd(cwd) {
|
|
3314
|
+
const resolved = resolve6(cwd);
|
|
3315
|
+
if (!existsSync5(resolved) || !statSync2(resolved).isDirectory()) {
|
|
3316
|
+
throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
3317
|
+
}
|
|
3318
|
+
return realpathSync3(resolved);
|
|
3319
|
+
}
|
|
3320
|
+
function canonicalizeExistingOrNearestAncestor(path) {
|
|
3321
|
+
let current = resolve6(path);
|
|
3322
|
+
const suffix = [];
|
|
3323
|
+
while (true) {
|
|
3324
|
+
try {
|
|
3325
|
+
const existing = realpathSync3(current);
|
|
3326
|
+
return suffix.length === 0 ? existing : join2(existing, ...suffix);
|
|
3327
|
+
} catch (error) {
|
|
3328
|
+
if (!isMissingPathError(error))
|
|
3329
|
+
throw error;
|
|
3330
|
+
const parent = dirname4(current);
|
|
3331
|
+
if (parent === current)
|
|
3332
|
+
throw error;
|
|
3333
|
+
suffix.unshift(basename2(current));
|
|
3334
|
+
current = parent;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
function capabilitiesField(value) {
|
|
3339
|
+
if (!isRecord4(value)) {
|
|
3340
|
+
throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
3341
|
+
}
|
|
3342
|
+
rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
|
|
3343
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
3344
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
3345
|
+
throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
|
|
3346
|
+
}
|
|
3347
|
+
return { installDecisionTool };
|
|
3348
|
+
}
|
|
3349
|
+
function stringField(value, field) {
|
|
3350
|
+
const fieldValue = value[field];
|
|
3351
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
3352
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
3353
|
+
}
|
|
3354
|
+
return fieldValue;
|
|
3355
|
+
}
|
|
3356
|
+
function stringArrayField(value, field) {
|
|
3357
|
+
const fieldValue = value[field];
|
|
3358
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
3359
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
|
|
3360
|
+
}
|
|
3361
|
+
return fieldValue;
|
|
3362
|
+
}
|
|
3363
|
+
function requireAbsolutePath(path, field) {
|
|
3364
|
+
if (!isAbsolute2(path)) {
|
|
3365
|
+
throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
function isPathInside(parent, child) {
|
|
3369
|
+
const childPath = resolve6(child);
|
|
3370
|
+
const relativePath = relative4(parent, childPath);
|
|
3371
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
|
|
3372
|
+
}
|
|
3373
|
+
function isMissingPathError(error) {
|
|
3374
|
+
const code = errorCode(error);
|
|
3375
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
3376
|
+
}
|
|
3377
|
+
function rejectUnknownFields(value, allowed, scope) {
|
|
3378
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
3379
|
+
if (unknown.length > 0) {
|
|
3380
|
+
throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
function isRecord4(value) {
|
|
3384
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3385
|
+
}
|
|
3386
|
+
function errorCode(error) {
|
|
3387
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
3388
|
+
return;
|
|
3389
|
+
const code = Reflect.get(error, "code");
|
|
3390
|
+
return typeof code === "string" ? code : undefined;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
// ../lsp-core/src/lsp/server-install-state.ts
|
|
3394
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3395
|
+
import { dirname as dirname5 } from "node:path";
|
|
1631
3396
|
function getInstallDecisionsPath() {
|
|
1632
|
-
|
|
1633
|
-
if (!override)
|
|
1634
|
-
return join2(homedir(), ".codex", "lsp-install-decisions.json");
|
|
1635
|
-
return isAbsolute(override) ? override : join2(homedir(), override);
|
|
3397
|
+
return lspRequestContext().installDecisionsPath;
|
|
1636
3398
|
}
|
|
1637
3399
|
function loadInstallDecisions() {
|
|
1638
3400
|
const path = getInstallDecisionsPath();
|
|
1639
|
-
if (!
|
|
3401
|
+
if (!existsSync6(path))
|
|
1640
3402
|
return {};
|
|
1641
3403
|
try {
|
|
1642
|
-
const parsed = JSON.parse(
|
|
3404
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1643
3405
|
return isInstallDecisions(parsed) ? parsed : {};
|
|
1644
3406
|
} catch {
|
|
1645
3407
|
return {};
|
|
@@ -1658,28 +3420,26 @@ function isInstallDecision(value) {
|
|
|
1658
3420
|
}
|
|
1659
3421
|
function writeInstallDecisions(decisions) {
|
|
1660
3422
|
const path = getInstallDecisionsPath();
|
|
1661
|
-
mkdirSync(
|
|
3423
|
+
mkdirSync(dirname5(path), { recursive: true });
|
|
1662
3424
|
const tmpPath = `${path}.tmp`;
|
|
1663
|
-
|
|
3425
|
+
writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
|
|
1664
3426
|
`, "utf8");
|
|
1665
|
-
|
|
3427
|
+
renameSync2(tmpPath, path);
|
|
1666
3428
|
}
|
|
1667
3429
|
function isInstallDecisions(value) {
|
|
1668
|
-
return
|
|
3430
|
+
return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
|
|
1669
3431
|
}
|
|
1670
3432
|
function isInstallDecisionRecord(value) {
|
|
1671
|
-
if (!
|
|
3433
|
+
if (!isRecord5(value))
|
|
1672
3434
|
return false;
|
|
1673
3435
|
return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
|
|
1674
3436
|
}
|
|
1675
|
-
function
|
|
3437
|
+
function isRecord5(value) {
|
|
1676
3438
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1677
3439
|
}
|
|
1678
3440
|
|
|
1679
3441
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1680
|
-
import { existsSync as
|
|
1681
|
-
import { homedir as homedir2 } from "node:os";
|
|
1682
|
-
import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
3442
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
1683
3443
|
|
|
1684
3444
|
// ../lsp-core/src/lsp/server-definitions.ts
|
|
1685
3445
|
var LSP_INSTALL_HINTS = {
|
|
@@ -1829,27 +3589,17 @@ var BUILTIN_SERVERS = {
|
|
|
1829
3589
|
};
|
|
1830
3590
|
|
|
1831
3591
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1832
|
-
function resolveProjectConfigPath(path) {
|
|
1833
|
-
return isAbsolute2(path) ? path : join3(contextCwd(), path);
|
|
1834
|
-
}
|
|
1835
3592
|
function getProjectConfigPaths() {
|
|
1836
|
-
|
|
1837
|
-
if (projectOverride) {
|
|
1838
|
-
return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
|
|
1839
|
-
}
|
|
1840
|
-
return [join3(contextCwd(), ".codex", "lsp-client.json")];
|
|
3593
|
+
return lspRequestContext().projectConfigPaths;
|
|
1841
3594
|
}
|
|
1842
3595
|
function getUserConfigPath() {
|
|
1843
|
-
|
|
1844
|
-
if (!userOverride)
|
|
1845
|
-
return join3(homedir2(), ".codex", "lsp-client.json");
|
|
1846
|
-
return isAbsolute2(userOverride) ? userOverride : join3(homedir2(), userOverride);
|
|
3596
|
+
return lspRequestContext().userConfigPath;
|
|
1847
3597
|
}
|
|
1848
3598
|
function loadJsonFile(path) {
|
|
1849
|
-
if (!
|
|
3599
|
+
if (!existsSync7(path))
|
|
1850
3600
|
return null;
|
|
1851
3601
|
try {
|
|
1852
|
-
const parsed = JSON.parse(
|
|
3602
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1853
3603
|
return isConfigJson(parsed) ? parsed : null;
|
|
1854
3604
|
} catch {
|
|
1855
3605
|
return null;
|
|
@@ -1988,16 +3738,16 @@ function applyOptionalServerFields(server2, entry) {
|
|
|
1988
3738
|
}
|
|
1989
3739
|
}
|
|
1990
3740
|
function isConfigJson(value) {
|
|
1991
|
-
if (!
|
|
3741
|
+
if (!isRecord6(value))
|
|
1992
3742
|
return false;
|
|
1993
3743
|
const lsp = value["lsp"];
|
|
1994
|
-
return lsp === undefined ||
|
|
3744
|
+
return lsp === undefined || isRecord6(lsp);
|
|
1995
3745
|
}
|
|
1996
3746
|
function parseLspEntry(value) {
|
|
1997
3747
|
return isLspEntry(value) ? value : null;
|
|
1998
3748
|
}
|
|
1999
3749
|
function isLspEntry(value) {
|
|
2000
|
-
if (!
|
|
3750
|
+
if (!isRecord6(value))
|
|
2001
3751
|
return false;
|
|
2002
3752
|
const disabled = value["disabled"];
|
|
2003
3753
|
const command = value["command"];
|
|
@@ -2005,15 +3755,15 @@ function isLspEntry(value) {
|
|
|
2005
3755
|
const priority = value["priority"];
|
|
2006
3756
|
const env = value["env"];
|
|
2007
3757
|
const initialization = value["initialization"];
|
|
2008
|
-
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 ||
|
|
3758
|
+
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));
|
|
2009
3759
|
}
|
|
2010
3760
|
function isStringArray(value) {
|
|
2011
3761
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2012
3762
|
}
|
|
2013
3763
|
function isStringRecord(value) {
|
|
2014
|
-
return
|
|
3764
|
+
return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
|
|
2015
3765
|
}
|
|
2016
|
-
function
|
|
3766
|
+
function isRecord6(value) {
|
|
2017
3767
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2018
3768
|
}
|
|
2019
3769
|
function getDisabledServerIds() {
|
|
@@ -2034,8 +3784,8 @@ function getDisabledServerIds() {
|
|
|
2034
3784
|
}
|
|
2035
3785
|
|
|
2036
3786
|
// ../lsp-core/src/lsp/server-installation.ts
|
|
2037
|
-
import { existsSync as
|
|
2038
|
-
import { delimiter as delimiter3, join as
|
|
3787
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
3788
|
+
import { delimiter as delimiter3, join as join3 } from "node:path";
|
|
2039
3789
|
function isServerInstalled(command, _workingDirectory) {
|
|
2040
3790
|
if (command.length === 0)
|
|
2041
3791
|
return false;
|
|
@@ -2043,7 +3793,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2043
3793
|
if (!cmd)
|
|
2044
3794
|
return false;
|
|
2045
3795
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
2046
|
-
if (
|
|
3796
|
+
if (existsSync8(cmd))
|
|
2047
3797
|
return true;
|
|
2048
3798
|
}
|
|
2049
3799
|
const isWindows = process.platform === "win32";
|
|
@@ -2064,7 +3814,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2064
3814
|
const paths = pathEnv.split(delimiter3);
|
|
2065
3815
|
for (const p of paths) {
|
|
2066
3816
|
for (const suffix of exts) {
|
|
2067
|
-
if (
|
|
3817
|
+
if (existsSync8(join3(p, cmd + suffix))) {
|
|
2068
3818
|
return true;
|
|
2069
3819
|
}
|
|
2070
3820
|
}
|
|
@@ -2163,39 +3913,50 @@ function getAllServers() {
|
|
|
2163
3913
|
var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
2164
3914
|
function isDirectoryPath(filePath) {
|
|
2165
3915
|
try {
|
|
2166
|
-
return
|
|
3916
|
+
return statSync3(filePath).isDirectory();
|
|
2167
3917
|
} catch {
|
|
2168
3918
|
return false;
|
|
2169
3919
|
}
|
|
2170
3920
|
}
|
|
2171
3921
|
function findWorkspaceRoot(filePath) {
|
|
2172
|
-
const abs =
|
|
3922
|
+
const abs = resolvePathInsideContext(filePath);
|
|
2173
3923
|
let dir = abs;
|
|
2174
3924
|
if (!isDirectoryPath(dir)) {
|
|
2175
|
-
dir =
|
|
3925
|
+
dir = dirname6(dir);
|
|
2176
3926
|
}
|
|
2177
3927
|
let prevDir = "";
|
|
2178
3928
|
while (dir !== prevDir) {
|
|
2179
3929
|
for (const marker of WORKSPACE_MARKERS) {
|
|
2180
|
-
if (
|
|
3930
|
+
if (existsSync9(join4(dir, marker))) {
|
|
2181
3931
|
return dir;
|
|
2182
3932
|
}
|
|
2183
3933
|
}
|
|
2184
3934
|
prevDir = dir;
|
|
2185
|
-
dir =
|
|
3935
|
+
dir = dirname6(dir);
|
|
3936
|
+
}
|
|
3937
|
+
return dirname6(abs);
|
|
3938
|
+
}
|
|
3939
|
+
function resolvePathInsideContext(filePath) {
|
|
3940
|
+
const cwd = contextCwd();
|
|
3941
|
+
const abs = resolve7(cwd, filePath);
|
|
3942
|
+
const canonical = canonicalizeExistingOrNearestAncestor(abs);
|
|
3943
|
+
if (!isPathInside(cwd, canonical)) {
|
|
3944
|
+
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
2186
3945
|
}
|
|
2187
|
-
return
|
|
3946
|
+
return canonical;
|
|
2188
3947
|
}
|
|
2189
3948
|
function formatServerLookupError(result) {
|
|
2190
3949
|
if (result.status === "not_installed") {
|
|
2191
3950
|
return formatNotInstalled(result);
|
|
2192
3951
|
}
|
|
3952
|
+
const context = lspRequestContext();
|
|
3953
|
+
const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
|
|
2193
3954
|
return [
|
|
2194
3955
|
`No LSP server configured for extension: ${result.extension}`,
|
|
2195
3956
|
"",
|
|
2196
3957
|
`Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
|
|
2197
3958
|
"",
|
|
2198
|
-
|
|
3959
|
+
`Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
|
|
2199
3960
|
" {",
|
|
2200
3961
|
' "lsp": {',
|
|
2201
3962
|
' "my-server": {',
|
|
@@ -2211,6 +3972,7 @@ function formatNotInstalled(result) {
|
|
|
2211
3972
|
const { server: server2, installHint } = result;
|
|
2212
3973
|
const extensions = server2.extensions.join(", ");
|
|
2213
3974
|
const decision = loadInstallDecision(server2.id)?.decision;
|
|
3975
|
+
const context = lspRequestContext();
|
|
2214
3976
|
if (decision === "declined") {
|
|
2215
3977
|
return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
|
|
2216
3978
|
}
|
|
@@ -2226,6 +3988,17 @@ function formatNotInstalled(result) {
|
|
|
2226
3988
|
"The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
|
|
2227
3989
|
` ${installHint}`
|
|
2228
3990
|
].join(`
|
|
3991
|
+
`);
|
|
3992
|
+
}
|
|
3993
|
+
if (!context.capabilities.installDecisionTool) {
|
|
3994
|
+
return [
|
|
3995
|
+
...header,
|
|
3996
|
+
"To install, run:",
|
|
3997
|
+
` ${installHint}`,
|
|
3998
|
+
"",
|
|
3999
|
+
"ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
|
|
4000
|
+
"Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
|
|
4001
|
+
].join(`
|
|
2229
4002
|
`);
|
|
2230
4003
|
}
|
|
2231
4004
|
return [
|
|
@@ -2237,6 +4010,7 @@ function formatNotInstalled(result) {
|
|
|
2237
4010
|
"- If the user agrees: run the install command above, then retry this tool.",
|
|
2238
4011
|
"- If the user declines, OR has NOT explicitly asked for LSP installation:",
|
|
2239
4012
|
` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
|
|
4013
|
+
` which writes to ${context.installDecisionsPath},`,
|
|
2240
4014
|
" then ignore this message and proceed WITHOUT LSP."
|
|
2241
4015
|
].join(`
|
|
2242
4016
|
`);
|
|
@@ -2250,14 +4024,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
|
|
|
2250
4024
|
"prepareRename"
|
|
2251
4025
|
]);
|
|
2252
4026
|
async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
2253
|
-
const absPath =
|
|
4027
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2254
4028
|
if (isDirectoryPath(absPath)) {
|
|
2255
4029
|
throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
|
|
2256
4030
|
}
|
|
2257
4031
|
const ext = effectiveExtension(absPath);
|
|
2258
4032
|
const result = findServerForExtension(ext);
|
|
2259
4033
|
if (result.status !== "found") {
|
|
2260
|
-
throw new LspServerLookupError(formatServerLookupError(result));
|
|
4034
|
+
throw new LspServerLookupError(formatServerLookupError(result), result);
|
|
2261
4035
|
}
|
|
2262
4036
|
const server2 = result.server;
|
|
2263
4037
|
const root = findWorkspaceRoot(absPath);
|
|
@@ -2285,11 +4059,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
|
2285
4059
|
}
|
|
2286
4060
|
|
|
2287
4061
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2288
|
-
import { existsSync as
|
|
2289
|
-
import { join as
|
|
4062
|
+
import { existsSync as existsSync10, lstatSync as lstatSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
4063
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
2290
4064
|
|
|
2291
4065
|
// ../lsp-core/src/lsp/formatters.ts
|
|
2292
|
-
import { fileURLToPath } from "node:url";
|
|
4066
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2293
4067
|
var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
2294
4068
|
error: 1,
|
|
2295
4069
|
warning: 2,
|
|
@@ -2297,7 +4071,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
|
2297
4071
|
hint: 4
|
|
2298
4072
|
};
|
|
2299
4073
|
function uriToPath(uri) {
|
|
2300
|
-
return
|
|
4074
|
+
return fileURLToPath2(uri);
|
|
2301
4075
|
}
|
|
2302
4076
|
function formatLocation(loc) {
|
|
2303
4077
|
if ("targetUri" in loc) {
|
|
@@ -2383,6 +4157,9 @@ function formatApplyResult(result) {
|
|
|
2383
4157
|
for (const file of result.filesModified) {
|
|
2384
4158
|
lines.push(` - ${file}`);
|
|
2385
4159
|
}
|
|
4160
|
+
if (result.lateAbort) {
|
|
4161
|
+
lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
|
|
4162
|
+
}
|
|
2386
4163
|
} else {
|
|
2387
4164
|
lines.push("Failed to apply some changes:");
|
|
2388
4165
|
for (const err of result.errors) {
|
|
@@ -2398,6 +4175,7 @@ function formatApplyResult(result) {
|
|
|
2398
4175
|
|
|
2399
4176
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2400
4177
|
var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
4178
|
+
var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
|
|
2401
4179
|
function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
2402
4180
|
const files = [];
|
|
2403
4181
|
function walk(currentDir) {
|
|
@@ -2405,17 +4183,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2405
4183
|
return;
|
|
2406
4184
|
let entries = [];
|
|
2407
4185
|
try {
|
|
2408
|
-
entries =
|
|
4186
|
+
entries = readdirSync3(currentDir);
|
|
2409
4187
|
} catch {
|
|
2410
4188
|
return;
|
|
2411
4189
|
}
|
|
2412
4190
|
for (const entry of entries) {
|
|
2413
4191
|
if (files.length >= maxFiles)
|
|
2414
4192
|
return;
|
|
2415
|
-
const fullPath =
|
|
4193
|
+
const fullPath = join5(currentDir, entry);
|
|
2416
4194
|
let stat;
|
|
2417
4195
|
try {
|
|
2418
|
-
stat =
|
|
4196
|
+
stat = lstatSync4(fullPath);
|
|
2419
4197
|
} catch {
|
|
2420
4198
|
continue;
|
|
2421
4199
|
}
|
|
@@ -2433,52 +4211,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2433
4211
|
walk(dir);
|
|
2434
4212
|
return files;
|
|
2435
4213
|
}
|
|
2436
|
-
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
|
|
4214
|
+
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
|
|
2437
4215
|
if (!extension.startsWith(".")) {
|
|
2438
4216
|
throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
|
|
2439
4217
|
}
|
|
2440
|
-
const absDir =
|
|
2441
|
-
if (!
|
|
4218
|
+
const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
|
|
4219
|
+
if (!existsSync10(absDir)) {
|
|
2442
4220
|
throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
|
|
2443
4221
|
}
|
|
2444
|
-
const serverResult = findServerForExtension(extension);
|
|
4222
|
+
const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
|
|
2445
4223
|
if (serverResult.status !== "found") {
|
|
2446
4224
|
throw new LspServerLookupError(formatServerLookupError(serverResult));
|
|
2447
4225
|
}
|
|
2448
4226
|
const server2 = serverResult.server;
|
|
2449
|
-
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
|
|
4227
|
+
const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
|
|
2450
4228
|
const wasCapped = allFiles.length > maxFiles;
|
|
2451
4229
|
const filesToProcess = allFiles.slice(0, maxFiles);
|
|
2452
4230
|
if (filesToProcess.length === 0) {
|
|
2453
|
-
|
|
4231
|
+
const output = [
|
|
2454
4232
|
`Directory: ${absDir}`,
|
|
2455
4233
|
`Extension: ${extension}`,
|
|
2456
4234
|
"Files scanned: 0",
|
|
2457
4235
|
`No files found with extension "${extension}".`
|
|
2458
4236
|
].join(`
|
|
2459
4237
|
`);
|
|
4238
|
+
return { output, totalDiagnostics: 0, fileFailures: [] };
|
|
2460
4239
|
}
|
|
2461
|
-
const root = findWorkspaceRoot(absDir);
|
|
2462
|
-
const manager2 = getLspManager();
|
|
4240
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
|
|
4241
|
+
const manager2 = options.manager ?? getLspManager();
|
|
2463
4242
|
const allDiagnostics = [];
|
|
2464
4243
|
const fileErrors = [];
|
|
4244
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
|
|
4245
|
+
options.signal?.throwIfAborted();
|
|
2465
4246
|
const client = await manager2.getClient(root, server2);
|
|
2466
4247
|
try {
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
file,
|
|
2478
|
-
|
|
2479
|
-
|
|
4248
|
+
let nextIndex = 0;
|
|
4249
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
|
|
4250
|
+
for (;; ) {
|
|
4251
|
+
if (options.signal?.aborted)
|
|
4252
|
+
return;
|
|
4253
|
+
const file = filesToProcess[nextIndex];
|
|
4254
|
+
nextIndex += 1;
|
|
4255
|
+
if (file === undefined)
|
|
4256
|
+
return;
|
|
4257
|
+
try {
|
|
4258
|
+
const result = await client.diagnostics(file, options.signal);
|
|
4259
|
+
const filtered = filterDiagnosticsBySeverity(result.items, severity);
|
|
4260
|
+
allDiagnostics.push(...filtered.map((diagnostic) => ({
|
|
4261
|
+
filePath: file,
|
|
4262
|
+
diagnostic
|
|
4263
|
+
})));
|
|
4264
|
+
} catch (e) {
|
|
4265
|
+
fileErrors.push({
|
|
4266
|
+
file,
|
|
4267
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4268
|
+
});
|
|
4269
|
+
}
|
|
2480
4270
|
}
|
|
2481
|
-
}
|
|
4271
|
+
});
|
|
4272
|
+
await Promise.all(workers);
|
|
2482
4273
|
} finally {
|
|
2483
4274
|
manager2.releaseClient(root, server2.id);
|
|
2484
4275
|
}
|
|
@@ -2506,13 +4297,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
|
|
|
2506
4297
|
lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
2507
4298
|
}
|
|
2508
4299
|
}
|
|
2509
|
-
return lines.join(`
|
|
2510
|
-
`);
|
|
4300
|
+
return { output: lines.join(`
|
|
4301
|
+
`), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
|
|
2511
4302
|
}
|
|
2512
4303
|
|
|
2513
4304
|
// ../lsp-core/src/lsp/infer-extension.ts
|
|
2514
|
-
import { lstatSync as
|
|
2515
|
-
import { join as
|
|
4305
|
+
import { lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
|
|
4306
|
+
import { join as join6 } from "node:path";
|
|
2516
4307
|
var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
2517
4308
|
var MAX_SCAN_ENTRIES = 500;
|
|
2518
4309
|
function inferExtensionFromDirectory(directory) {
|
|
@@ -2523,17 +4314,17 @@ function inferExtensionFromDirectory(directory) {
|
|
|
2523
4314
|
return;
|
|
2524
4315
|
let entries;
|
|
2525
4316
|
try {
|
|
2526
|
-
entries =
|
|
4317
|
+
entries = readdirSync4(dir);
|
|
2527
4318
|
} catch {
|
|
2528
4319
|
return;
|
|
2529
4320
|
}
|
|
2530
4321
|
for (const entry of entries) {
|
|
2531
4322
|
if (scanned >= MAX_SCAN_ENTRIES)
|
|
2532
4323
|
return;
|
|
2533
|
-
const fullPath =
|
|
4324
|
+
const fullPath = join6(dir, entry);
|
|
2534
4325
|
let stat;
|
|
2535
4326
|
try {
|
|
2536
|
-
stat =
|
|
4327
|
+
stat = lstatSync5(fullPath);
|
|
2537
4328
|
} catch {
|
|
2538
4329
|
continue;
|
|
2539
4330
|
}
|
|
@@ -2607,13 +4398,48 @@ function missingDependencyResult(error, details) {
|
|
|
2607
4398
|
details: {
|
|
2608
4399
|
...details,
|
|
2609
4400
|
error: message,
|
|
2610
|
-
errorKind: "missing_dependency"
|
|
4401
|
+
errorKind: "missing_dependency",
|
|
4402
|
+
...availabilityDetails(error)
|
|
2611
4403
|
}
|
|
2612
4404
|
};
|
|
2613
4405
|
}
|
|
4406
|
+
function availabilityDetails(error) {
|
|
4407
|
+
const availability = missingDependencyAvailability(error);
|
|
4408
|
+
return availability === null ? {} : { availability };
|
|
4409
|
+
}
|
|
4410
|
+
function missingDependencyAvailability(error) {
|
|
4411
|
+
if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
|
|
4412
|
+
return null;
|
|
4413
|
+
const context = lspRequestContext();
|
|
4414
|
+
switch (error.lookup.status) {
|
|
4415
|
+
case "not_configured":
|
|
4416
|
+
return {
|
|
4417
|
+
kind: "not_configured",
|
|
4418
|
+
extension: error.lookup.extension,
|
|
4419
|
+
availableServers: [...error.lookup.availableServers],
|
|
4420
|
+
projectConfigPaths: [...context.projectConfigPaths],
|
|
4421
|
+
userConfigPath: context.userConfigPath,
|
|
4422
|
+
installDecisionTool: context.capabilities.installDecisionTool
|
|
4423
|
+
};
|
|
4424
|
+
case "not_installed":
|
|
4425
|
+
return {
|
|
4426
|
+
kind: "not_installed",
|
|
4427
|
+
serverId: error.lookup.server.id,
|
|
4428
|
+
command: [...error.lookup.server.command],
|
|
4429
|
+
extensions: [...error.lookup.server.extensions],
|
|
4430
|
+
installHint: error.lookup.installHint,
|
|
4431
|
+
installDecisionTool: context.capabilities.installDecisionTool,
|
|
4432
|
+
installDecisionsPath: context.installDecisionsPath
|
|
4433
|
+
};
|
|
4434
|
+
default: {
|
|
4435
|
+
const exhaustive = error.lookup;
|
|
4436
|
+
return exhaustive;
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
2614
4440
|
|
|
2615
4441
|
// ../lsp-core/src/tools/parameters.ts
|
|
2616
|
-
function
|
|
4442
|
+
function isRecord7(value) {
|
|
2617
4443
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2618
4444
|
}
|
|
2619
4445
|
function requireString(params, key) {
|
|
@@ -2670,7 +4496,7 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2670
4496
|
const filePath = requireString(params, "filePath");
|
|
2671
4497
|
const severity = severityFilter(params);
|
|
2672
4498
|
try {
|
|
2673
|
-
const absPath =
|
|
4499
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2674
4500
|
if (isDirectoryPath(absPath)) {
|
|
2675
4501
|
const extension = inferExtensionFromDirectory(absPath);
|
|
2676
4502
|
if (!extension) {
|
|
@@ -2687,18 +4513,33 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2687
4513
|
};
|
|
2688
4514
|
return text(message, details3);
|
|
2689
4515
|
}
|
|
2690
|
-
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
|
|
4516
|
+
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
|
|
2691
4517
|
const details2 = {
|
|
2692
4518
|
filePath,
|
|
2693
4519
|
severity,
|
|
2694
4520
|
mode: "directory",
|
|
2695
4521
|
diagnostics: [],
|
|
4522
|
+
totalDiagnostics: output2.totalDiagnostics,
|
|
4523
|
+
truncated: false,
|
|
4524
|
+
fileFailures: [...output2.fileFailures]
|
|
4525
|
+
};
|
|
4526
|
+
return text(output2.output, details2);
|
|
4527
|
+
}
|
|
4528
|
+
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
|
|
4529
|
+
if (result.transientError) {
|
|
4530
|
+
const message = result.transientError.message;
|
|
4531
|
+
const details2 = {
|
|
4532
|
+
filePath,
|
|
4533
|
+
severity,
|
|
4534
|
+
mode: "file",
|
|
4535
|
+
diagnostics: [],
|
|
2696
4536
|
totalDiagnostics: 0,
|
|
2697
|
-
truncated: false
|
|
4537
|
+
truncated: false,
|
|
4538
|
+
error: message,
|
|
4539
|
+
errorKind: result.transientError.kind
|
|
2698
4540
|
};
|
|
2699
|
-
return text(
|
|
4541
|
+
return text(message, details2, true);
|
|
2700
4542
|
}
|
|
2701
|
-
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
|
|
2702
4543
|
const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
|
|
2703
4544
|
const total = diagnostics.length;
|
|
2704
4545
|
const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
|
|
@@ -2759,7 +4600,7 @@ async function executeLspGotoDefinition(params, signal) {
|
|
|
2759
4600
|
const line = requireNumber(params, "line");
|
|
2760
4601
|
const character = requireNumber(params, "character");
|
|
2761
4602
|
try {
|
|
2762
|
-
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
|
|
4603
|
+
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
|
|
2763
4604
|
const locations = !result ? [] : Array.isArray(result) ? result : [result];
|
|
2764
4605
|
const details = { filePath, line, character, locations };
|
|
2765
4606
|
if (locations.length === 0)
|
|
@@ -2784,7 +4625,7 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2784
4625
|
const character = requireNumber(params, "character");
|
|
2785
4626
|
const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
|
|
2786
4627
|
try {
|
|
2787
|
-
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
|
|
4628
|
+
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
|
|
2788
4629
|
const references = Array.isArray(result) ? result : [];
|
|
2789
4630
|
const total = references.length;
|
|
2790
4631
|
const truncated = total > DEFAULT_MAX_REFERENCES;
|
|
@@ -2820,181 +4661,13 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2820
4661
|
}
|
|
2821
4662
|
}
|
|
2822
4663
|
|
|
2823
|
-
// ../lsp-core/src/lsp/workspace-edit.ts
|
|
2824
|
-
import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2825
|
-
import { dirname as dirname3, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
|
|
2826
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2827
|
-
function errorMessage2(error) {
|
|
2828
|
-
return error instanceof Error ? error.message : String(error);
|
|
2829
|
-
}
|
|
2830
|
-
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
2831
|
-
const relativePath = relative(workspaceRoot, filePath);
|
|
2832
|
-
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
|
|
2833
|
-
}
|
|
2834
|
-
function realpathForValidation(filePath) {
|
|
2835
|
-
if (existsSync7(filePath))
|
|
2836
|
-
return realpathSync(filePath);
|
|
2837
|
-
const parent = dirname3(filePath);
|
|
2838
|
-
return resolve5(realpathSync(parent), relative(parent, filePath));
|
|
2839
|
-
}
|
|
2840
|
-
function uriToWorkspacePath(uri, workspaceRoot) {
|
|
2841
|
-
let filePath;
|
|
2842
|
-
try {
|
|
2843
|
-
filePath = fileURLToPath2(uri);
|
|
2844
|
-
} catch (error) {
|
|
2845
|
-
return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
|
|
2846
|
-
}
|
|
2847
|
-
let validatedPath;
|
|
2848
|
-
try {
|
|
2849
|
-
validatedPath = realpathForValidation(filePath);
|
|
2850
|
-
} catch (error) {
|
|
2851
|
-
return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
|
|
2852
|
-
}
|
|
2853
|
-
if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
|
|
2854
|
-
return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
|
|
2855
|
-
}
|
|
2856
|
-
return { success: true, path: filePath };
|
|
2857
|
-
}
|
|
2858
|
-
function applyTextEditsToFile(filePath, edits) {
|
|
2859
|
-
try {
|
|
2860
|
-
const content = readFileSync4(filePath, "utf-8");
|
|
2861
|
-
const lines = content.split(`
|
|
2862
|
-
`);
|
|
2863
|
-
const sortedEdits = [...edits].sort((a, b) => {
|
|
2864
|
-
if (b.range.start.line !== a.range.start.line) {
|
|
2865
|
-
return b.range.start.line - a.range.start.line;
|
|
2866
|
-
}
|
|
2867
|
-
return b.range.start.character - a.range.start.character;
|
|
2868
|
-
});
|
|
2869
|
-
for (const edit of sortedEdits) {
|
|
2870
|
-
const startLine = edit.range.start.line;
|
|
2871
|
-
const startChar = edit.range.start.character;
|
|
2872
|
-
const endLine = edit.range.end.line;
|
|
2873
|
-
const endChar = edit.range.end.character;
|
|
2874
|
-
if (startLine === endLine) {
|
|
2875
|
-
const line = lines[startLine] ?? "";
|
|
2876
|
-
lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
|
|
2877
|
-
} else {
|
|
2878
|
-
const firstLine = lines[startLine] ?? "";
|
|
2879
|
-
const lastLine = lines[endLine] ?? "";
|
|
2880
|
-
const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
|
|
2881
|
-
lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
|
|
2882
|
-
`));
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
writeFileSync2(filePath, lines.join(`
|
|
2886
|
-
`), "utf-8");
|
|
2887
|
-
return { success: true, editCount: edits.length };
|
|
2888
|
-
} catch (err) {
|
|
2889
|
-
return {
|
|
2890
|
-
success: false,
|
|
2891
|
-
editCount: 0,
|
|
2892
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2893
|
-
};
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
function applyWorkspaceEdit(edit, options = {}) {
|
|
2897
|
-
if (!edit) {
|
|
2898
|
-
return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
|
|
2899
|
-
}
|
|
2900
|
-
const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
|
|
2901
|
-
const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
|
|
2902
|
-
if (edit.changes) {
|
|
2903
|
-
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
2904
|
-
const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
|
|
2905
|
-
if (!validatedPath.success) {
|
|
2906
|
-
result.success = false;
|
|
2907
|
-
result.errors.push(validatedPath.error);
|
|
2908
|
-
continue;
|
|
2909
|
-
}
|
|
2910
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, edits);
|
|
2911
|
-
if (applyResult.success) {
|
|
2912
|
-
result.filesModified.push(validatedPath.path);
|
|
2913
|
-
result.totalEdits += applyResult.editCount;
|
|
2914
|
-
} else {
|
|
2915
|
-
result.success = false;
|
|
2916
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
if (edit.documentChanges) {
|
|
2921
|
-
for (const change of edit.documentChanges) {
|
|
2922
|
-
if (!("kind" in change)) {
|
|
2923
|
-
const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
|
|
2924
|
-
if (!validatedPath.success) {
|
|
2925
|
-
result.success = false;
|
|
2926
|
-
result.errors.push(validatedPath.error);
|
|
2927
|
-
continue;
|
|
2928
|
-
}
|
|
2929
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
|
|
2930
|
-
if (applyResult.success) {
|
|
2931
|
-
result.filesModified.push(validatedPath.path);
|
|
2932
|
-
result.totalEdits += applyResult.editCount;
|
|
2933
|
-
} else {
|
|
2934
|
-
result.success = false;
|
|
2935
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2936
|
-
}
|
|
2937
|
-
continue;
|
|
2938
|
-
}
|
|
2939
|
-
if (change.kind === "create") {
|
|
2940
|
-
try {
|
|
2941
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2942
|
-
if (!validatedPath.success) {
|
|
2943
|
-
result.success = false;
|
|
2944
|
-
result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
|
|
2945
|
-
continue;
|
|
2946
|
-
}
|
|
2947
|
-
writeFileSync2(validatedPath.path, "", "utf-8");
|
|
2948
|
-
result.filesModified.push(validatedPath.path);
|
|
2949
|
-
} catch (err) {
|
|
2950
|
-
result.success = false;
|
|
2951
|
-
result.errors.push(`Create ${change.uri}: ${String(err)}`);
|
|
2952
|
-
}
|
|
2953
|
-
} else if (change.kind === "rename") {
|
|
2954
|
-
try {
|
|
2955
|
-
const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
|
|
2956
|
-
const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
|
|
2957
|
-
if (!oldPath.success || !newPath.success) {
|
|
2958
|
-
const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
|
|
2959
|
-
result.success = false;
|
|
2960
|
-
result.errors.push(`Rename ${change.oldUri}: ${error}`);
|
|
2961
|
-
continue;
|
|
2962
|
-
}
|
|
2963
|
-
const content = readFileSync4(oldPath.path, "utf-8");
|
|
2964
|
-
writeFileSync2(newPath.path, content, "utf-8");
|
|
2965
|
-
unlinkSync(oldPath.path);
|
|
2966
|
-
result.filesModified.push(newPath.path);
|
|
2967
|
-
} catch (err) {
|
|
2968
|
-
result.success = false;
|
|
2969
|
-
result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
|
|
2970
|
-
}
|
|
2971
|
-
} else if (change.kind === "delete") {
|
|
2972
|
-
try {
|
|
2973
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2974
|
-
if (!validatedPath.success) {
|
|
2975
|
-
result.success = false;
|
|
2976
|
-
result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
|
|
2977
|
-
continue;
|
|
2978
|
-
}
|
|
2979
|
-
unlinkSync(validatedPath.path);
|
|
2980
|
-
result.filesModified.push(validatedPath.path);
|
|
2981
|
-
} catch (err) {
|
|
2982
|
-
result.success = false;
|
|
2983
|
-
result.errors.push(`Delete ${change.uri}: ${String(err)}`);
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
2988
|
-
return result;
|
|
2989
|
-
}
|
|
2990
|
-
|
|
2991
4664
|
// ../lsp-core/src/tools/rename.ts
|
|
2992
4665
|
async function executeLspPrepareRename(params, signal) {
|
|
2993
4666
|
const filePath = requireString(params, "filePath");
|
|
2994
4667
|
const line = requireNumber(params, "line");
|
|
2995
4668
|
const character = requireNumber(params, "character");
|
|
2996
4669
|
try {
|
|
2997
|
-
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
|
|
4670
|
+
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
|
|
2998
4671
|
const details = { filePath, line, character, result };
|
|
2999
4672
|
return text(formatPrepareRenameResult(result), details);
|
|
3000
4673
|
} catch (error) {
|
|
@@ -3015,13 +4688,9 @@ async function executeLspRename(params, signal) {
|
|
|
3015
4688
|
const character = requireNumber(params, "character");
|
|
3016
4689
|
const newName = requireString(params, "newName");
|
|
3017
4690
|
try {
|
|
3018
|
-
const
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
}), "rename", clientOptions(signal));
|
|
3022
|
-
const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
|
|
3023
|
-
const details = { filePath, line, character, newName, apply, edit: edit.edit };
|
|
3024
|
-
return text(formatApplyResult(apply), details, !apply.success);
|
|
4691
|
+
const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
|
|
4692
|
+
const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
|
|
4693
|
+
return text(formatApplyResult(result.apply), details, !result.apply.success);
|
|
3025
4694
|
} catch (error) {
|
|
3026
4695
|
const missingDependency = missingDependencyResult(error, {
|
|
3027
4696
|
filePath,
|
|
@@ -3096,10 +4765,10 @@ async function executeLspSymbols(params, signal) {
|
|
|
3096
4765
|
errorKind: "missing_query"
|
|
3097
4766
|
});
|
|
3098
4767
|
}
|
|
3099
|
-
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
|
|
4768
|
+
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
|
|
3100
4769
|
return formatSymbolsResult(filePath, scope, symbols2, limit, query);
|
|
3101
4770
|
}
|
|
3102
|
-
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
|
|
4771
|
+
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
|
|
3103
4772
|
return formatSymbolsResult(filePath, scope, symbols, limit);
|
|
3104
4773
|
} catch (error) {
|
|
3105
4774
|
const query = optionalString(params, "query");
|
|
@@ -3265,12 +4934,12 @@ function matchesToolName(tool, name) {
|
|
|
3265
4934
|
return tool.name === name || (tool.aliases?.includes(name) ?? false);
|
|
3266
4935
|
}
|
|
3267
4936
|
function coerceToolArguments(value) {
|
|
3268
|
-
return
|
|
4937
|
+
return isRecord7(value) ? value : {};
|
|
3269
4938
|
}
|
|
3270
4939
|
// ../lsp-core/src/mcp.ts
|
|
3271
4940
|
var SERVER_NAME = "lsp";
|
|
3272
4941
|
var SERVER_VERSION = "0.1.0";
|
|
3273
|
-
async function handleLspMcpRequest(input) {
|
|
4942
|
+
async function handleLspMcpRequest(input, options = {}) {
|
|
3274
4943
|
if (!isPlainRecord(input)) {
|
|
3275
4944
|
return errorResponse(null, -32600, "Invalid Request");
|
|
3276
4945
|
}
|
|
@@ -3292,33 +4961,37 @@ async function handleLspMcpRequest(input) {
|
|
|
3292
4961
|
return successResponse(id, { tools: LSP_MCP_TOOLS.map(describeTool) });
|
|
3293
4962
|
}
|
|
3294
4963
|
if (method === "tools/call") {
|
|
3295
|
-
return handleToolCall(id, input["params"]);
|
|
4964
|
+
return handleToolCall(id, input["params"], options.signal);
|
|
3296
4965
|
}
|
|
3297
4966
|
return errorResponse(id, -32601, `Method not found: ${String(method)}`);
|
|
3298
4967
|
}
|
|
3299
4968
|
async function runMcpStdioServer(input = process.stdin, output = process.stdout) {
|
|
4969
|
+
const requestContext = createStandaloneMcpRequestContext();
|
|
3300
4970
|
await runJsonRpcStdioServer({
|
|
3301
4971
|
input,
|
|
3302
4972
|
output,
|
|
3303
4973
|
idleTimeoutMs: 0,
|
|
3304
|
-
handler: handleLspMcpRequest,
|
|
4974
|
+
handler: (request) => runWithRequestContext(requestContext, () => handleLspMcpRequest(request)),
|
|
3305
4975
|
handlerOptions: undefined
|
|
3306
4976
|
});
|
|
3307
4977
|
}
|
|
3308
|
-
async function handleToolCall(id, params) {
|
|
4978
|
+
async function handleToolCall(id, params, signal) {
|
|
3309
4979
|
if (!isPlainRecord(params) || typeof params["name"] !== "string") {
|
|
3310
4980
|
return errorResponse(id, -32602, "tools/call requires params.name");
|
|
3311
4981
|
}
|
|
3312
4982
|
try {
|
|
3313
|
-
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
|
|
4983
|
+
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
|
|
3314
4984
|
return successResponse(id, {
|
|
3315
4985
|
content: result.content,
|
|
3316
4986
|
isError: result.isError ?? false,
|
|
3317
4987
|
details: result.details
|
|
3318
4988
|
});
|
|
3319
4989
|
} catch (error) {
|
|
4990
|
+
if (!(error instanceof Error)) {
|
|
4991
|
+
throw error;
|
|
4992
|
+
}
|
|
3320
4993
|
return successResponse(id, {
|
|
3321
|
-
content: [{ type: "text", text:
|
|
4994
|
+
content: [{ type: "text", text: error.message }],
|
|
3322
4995
|
isError: true
|
|
3323
4996
|
});
|
|
3324
4997
|
}
|