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,31 +1,16 @@
|
|
|
1
1
|
// ../lsp-core/src/lsp/cleanup-errors.ts
|
|
2
|
-
function
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
function writeCleanupError(message) {
|
|
3
|
+
process.stderr.write(`${message}
|
|
4
|
+
`);
|
|
5
|
+
}
|
|
6
|
+
function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
|
|
5
7
|
const message = error instanceof Error ? error.message : String(error);
|
|
6
|
-
|
|
8
|
+
logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
// ../lsp-core/src/lsp/client.ts
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
13
|
-
|
|
14
|
-
// ../lsp-core/src/request-context.ts
|
|
15
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
16
|
-
var storage = new AsyncLocalStorage;
|
|
17
|
-
function runWithRequestContext(context, fn) {
|
|
18
|
-
return storage.run(context, fn);
|
|
19
|
-
}
|
|
20
|
-
function contextCwd() {
|
|
21
|
-
return storage.getStore()?.cwd ?? process.cwd();
|
|
22
|
-
}
|
|
23
|
-
function contextEnv(key) {
|
|
24
|
-
const store = storage.getStore();
|
|
25
|
-
if (store?.env)
|
|
26
|
-
return store.env[key];
|
|
27
|
-
return process.env[key];
|
|
28
|
-
}
|
|
12
|
+
import { resolve as resolve5 } from "node:path";
|
|
13
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
29
14
|
|
|
30
15
|
// ../lsp-core/src/lsp/connection.ts
|
|
31
16
|
import { pathToFileURL } from "node:url";
|
|
@@ -89,7 +74,12 @@ class LspInvalidPathError extends Error {
|
|
|
89
74
|
}
|
|
90
75
|
|
|
91
76
|
class LspServerLookupError extends Error {
|
|
77
|
+
lookup;
|
|
92
78
|
name = "LspServerLookupError";
|
|
79
|
+
constructor(message, lookup) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.lookup = lookup;
|
|
82
|
+
}
|
|
93
83
|
}
|
|
94
84
|
|
|
95
85
|
class LspServerInitializingError extends Error {
|
|
@@ -155,28 +145,80 @@ class JsonRpcConnection {
|
|
|
155
145
|
onError(handler) {
|
|
156
146
|
this.errorHandlers.push(handler);
|
|
157
147
|
}
|
|
158
|
-
async sendRequest(method, params) {
|
|
148
|
+
async sendRequest(method, params, options = {}) {
|
|
159
149
|
if (this.disposed)
|
|
160
150
|
throw new Error("JSON-RPC connection is disposed");
|
|
161
151
|
const id = this.nextRequestId;
|
|
162
152
|
this.nextRequestId += 1;
|
|
153
|
+
const key = String(id);
|
|
163
154
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
155
|
+
let requestWritten = false;
|
|
156
|
+
let cancelAfterWrite = false;
|
|
157
|
+
let settled = false;
|
|
158
|
+
const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
164
159
|
const responsePromise = new Promise((resolve, reject) => {
|
|
165
|
-
|
|
160
|
+
const cleanup = () => {
|
|
161
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
162
|
+
};
|
|
163
|
+
const settleCancel = () => {
|
|
164
|
+
if (settled)
|
|
165
|
+
return;
|
|
166
|
+
settled = true;
|
|
167
|
+
this.pendingRequests.delete(key);
|
|
168
|
+
cleanup();
|
|
169
|
+
const rejectCancelled = () => reject(abortError(options.signal));
|
|
170
|
+
if (!requestWritten) {
|
|
171
|
+
cancelAfterWrite = true;
|
|
172
|
+
rejectCancelled();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
writeCancel().then(rejectCancelled, (error) => {
|
|
176
|
+
this.emitError(toError(error));
|
|
177
|
+
rejectCancelled();
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
const onAbort = () => settleCancel();
|
|
181
|
+
this.pendingRequests.set(key, {
|
|
166
182
|
resolve(result) {
|
|
183
|
+
settled = true;
|
|
184
|
+
cleanup();
|
|
167
185
|
resolve(result);
|
|
168
186
|
},
|
|
169
|
-
reject
|
|
187
|
+
reject(error) {
|
|
188
|
+
settled = true;
|
|
189
|
+
cleanup();
|
|
190
|
+
reject(error);
|
|
191
|
+
},
|
|
192
|
+
cleanup
|
|
170
193
|
});
|
|
194
|
+
if (options.signal?.aborted) {
|
|
195
|
+
settleCancel();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
171
199
|
});
|
|
200
|
+
if (settled)
|
|
201
|
+
return responsePromise;
|
|
172
202
|
try {
|
|
173
203
|
await this.writeMessage(message);
|
|
204
|
+
requestWritten = true;
|
|
205
|
+
if (cancelAfterWrite)
|
|
206
|
+
await writeCancel();
|
|
174
207
|
} catch (error) {
|
|
175
|
-
|
|
208
|
+
if (settled)
|
|
209
|
+
return responsePromise;
|
|
210
|
+
const pending = this.pendingRequests.get(key);
|
|
211
|
+
if (pending) {
|
|
212
|
+
pending.cleanup();
|
|
213
|
+
this.pendingRequests.delete(key);
|
|
214
|
+
}
|
|
176
215
|
throw error;
|
|
177
216
|
}
|
|
178
217
|
return responsePromise;
|
|
179
218
|
}
|
|
219
|
+
pendingRequestCount() {
|
|
220
|
+
return this.pendingRequests.size;
|
|
221
|
+
}
|
|
180
222
|
async sendNotification(method, params) {
|
|
181
223
|
if (this.disposed)
|
|
182
224
|
return;
|
|
@@ -193,6 +235,7 @@ class JsonRpcConnection {
|
|
|
193
235
|
this.reader.off("error", this.handleStreamError);
|
|
194
236
|
this.writer.off("error", this.handleStreamError);
|
|
195
237
|
for (const pending of this.pendingRequests.values()) {
|
|
238
|
+
pending.cleanup();
|
|
196
239
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
197
240
|
}
|
|
198
241
|
this.pendingRequests.clear();
|
|
@@ -268,6 +311,7 @@ class JsonRpcConnection {
|
|
|
268
311
|
if (!pending)
|
|
269
312
|
return;
|
|
270
313
|
this.pendingRequests.delete(String(id));
|
|
314
|
+
pending.cleanup();
|
|
271
315
|
if ("error" in message) {
|
|
272
316
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
273
317
|
return;
|
|
@@ -281,7 +325,11 @@ class JsonRpcConnection {
|
|
|
281
325
|
try {
|
|
282
326
|
handler(params);
|
|
283
327
|
} catch (error) {
|
|
284
|
-
|
|
328
|
+
if (error instanceof Error) {
|
|
329
|
+
this.emitError(error);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.emitError(new Error(String(error)));
|
|
285
333
|
}
|
|
286
334
|
}
|
|
287
335
|
handleRequest(message) {
|
|
@@ -322,6 +370,14 @@ ${body}`;
|
|
|
322
370
|
}
|
|
323
371
|
}
|
|
324
372
|
}
|
|
373
|
+
function abortError(signal) {
|
|
374
|
+
const reason = signal?.reason;
|
|
375
|
+
if (reason instanceof Error)
|
|
376
|
+
return reason;
|
|
377
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
378
|
+
error.name = "AbortError";
|
|
379
|
+
return error;
|
|
380
|
+
}
|
|
325
381
|
function parseContentLength(headers) {
|
|
326
382
|
for (const line of headers.split(`\r
|
|
327
383
|
`)) {
|
|
@@ -508,7 +564,7 @@ function spawnProcess(command, options) {
|
|
|
508
564
|
return wrap(proc);
|
|
509
565
|
}
|
|
510
566
|
|
|
511
|
-
// ../lsp-core/src/lsp/transport.ts
|
|
567
|
+
// ../lsp-core/src/lsp/transport-protocol.ts
|
|
512
568
|
function isRecord(value) {
|
|
513
569
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
514
570
|
}
|
|
@@ -528,7 +584,32 @@ function parseDiagnosticsParams(params) {
|
|
|
528
584
|
if (!isRecord(params) || typeof params["uri"] !== "string")
|
|
529
585
|
return null;
|
|
530
586
|
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
531
|
-
|
|
587
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
588
|
+
return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
|
|
589
|
+
}
|
|
590
|
+
function createLspSpawnEnv(_root, input) {
|
|
591
|
+
return { ...input };
|
|
592
|
+
}
|
|
593
|
+
function isDiagnostic(value) {
|
|
594
|
+
return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
595
|
+
}
|
|
596
|
+
function isRange(value) {
|
|
597
|
+
return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
598
|
+
}
|
|
599
|
+
function isPosition(value) {
|
|
600
|
+
return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ../lsp-core/src/lsp/transport.ts
|
|
604
|
+
class LspClientNotStartedError extends Error {
|
|
605
|
+
serverId;
|
|
606
|
+
root;
|
|
607
|
+
name = "LspClientNotStartedError";
|
|
608
|
+
constructor(serverId, root) {
|
|
609
|
+
super("LSP client not started");
|
|
610
|
+
this.serverId = serverId;
|
|
611
|
+
this.root = root;
|
|
612
|
+
}
|
|
532
613
|
}
|
|
533
614
|
|
|
534
615
|
class LspClientTransport {
|
|
@@ -541,6 +622,8 @@ class LspClientTransport {
|
|
|
541
622
|
diagnosticsStore = new Map;
|
|
542
623
|
requestTimeoutMs;
|
|
543
624
|
initializeTimeoutMs;
|
|
625
|
+
workspaceApplyEditHandler = null;
|
|
626
|
+
diagnosticPullSupported = false;
|
|
544
627
|
constructor(root, server, timeouts = {}) {
|
|
545
628
|
this.root = root;
|
|
546
629
|
this.server = server;
|
|
@@ -553,6 +636,21 @@ class LspClientTransport {
|
|
|
553
636
|
command() {
|
|
554
637
|
return [...this.server.command];
|
|
555
638
|
}
|
|
639
|
+
setWorkspaceApplyEditHandler(handler) {
|
|
640
|
+
this.workspaceApplyEditHandler = handler;
|
|
641
|
+
}
|
|
642
|
+
hasWorkspaceApplyEditHandler() {
|
|
643
|
+
return this.workspaceApplyEditHandler !== null;
|
|
644
|
+
}
|
|
645
|
+
setDiagnosticPullSupported(supported) {
|
|
646
|
+
this.diagnosticPullSupported = supported;
|
|
647
|
+
}
|
|
648
|
+
isDiagnosticPullSupported() {
|
|
649
|
+
return this.diagnosticPullSupported;
|
|
650
|
+
}
|
|
651
|
+
handlePublishDiagnostics(params) {
|
|
652
|
+
this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
|
|
653
|
+
}
|
|
556
654
|
async start() {
|
|
557
655
|
const env = createLspSpawnEnv(this.root, {
|
|
558
656
|
...process.env,
|
|
@@ -563,7 +661,6 @@ class LspClientTransport {
|
|
|
563
661
|
env
|
|
564
662
|
});
|
|
565
663
|
this.startStderrReading();
|
|
566
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
567
664
|
if (this.proc.exitCode !== null) {
|
|
568
665
|
const stderr = this.stderrBuffer.join(`
|
|
569
666
|
`);
|
|
@@ -573,7 +670,7 @@ class LspClientTransport {
|
|
|
573
670
|
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
574
671
|
const diagnosticsParams = parseDiagnosticsParams(params);
|
|
575
672
|
if (diagnosticsParams?.uri) {
|
|
576
|
-
this.
|
|
673
|
+
this.handlePublishDiagnostics(diagnosticsParams);
|
|
577
674
|
}
|
|
578
675
|
});
|
|
579
676
|
this.connection.onRequest("workspace/configuration", (params) => {
|
|
@@ -586,6 +683,9 @@ class LspClientTransport {
|
|
|
586
683
|
});
|
|
587
684
|
this.connection.onRequest("client/registerCapability", () => null);
|
|
588
685
|
this.connection.onRequest("window/workDoneProgress/create", () => null);
|
|
686
|
+
if (this.workspaceApplyEditHandler) {
|
|
687
|
+
this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
|
|
688
|
+
}
|
|
589
689
|
this.connection.onClose(() => {
|
|
590
690
|
this.processExited = true;
|
|
591
691
|
});
|
|
@@ -614,30 +714,25 @@ class LspClientTransport {
|
|
|
614
714
|
}
|
|
615
715
|
async sendRequest(method, ...args) {
|
|
616
716
|
if (!this.connection)
|
|
617
|
-
throw new
|
|
717
|
+
throw new LspClientNotStartedError(this.server.id, this.root);
|
|
618
718
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
619
719
|
const stderrTail = this.stderrBuffer.slice(-10).join(`
|
|
620
720
|
`);
|
|
621
721
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
|
|
622
722
|
}
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
723
|
+
const options = args[1];
|
|
724
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
725
|
+
const timeoutController = new AbortController;
|
|
726
|
+
const timeoutHandle = setTimeout(() => {
|
|
727
|
+
const stderrTail = this.stderrBuffer.slice(-5).join(`
|
|
628
728
|
`);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
729
|
+
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
730
|
+
}, timeoutMs);
|
|
731
|
+
const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
|
|
632
732
|
try {
|
|
633
|
-
const
|
|
634
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
635
|
-
if (timeoutHandle !== null)
|
|
636
|
-
clearTimeout(timeoutHandle);
|
|
733
|
+
const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
|
|
637
734
|
return result;
|
|
638
735
|
} catch (error) {
|
|
639
|
-
if (timeoutHandle !== null)
|
|
640
|
-
clearTimeout(timeoutHandle);
|
|
641
736
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
642
737
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
|
|
643
738
|
`) || undefined);
|
|
@@ -646,6 +741,9 @@ class LspClientTransport {
|
|
|
646
741
|
throw new LspConnectionClosedError(this.server.id, this.root, error.message);
|
|
647
742
|
}
|
|
648
743
|
throw error;
|
|
744
|
+
} finally {
|
|
745
|
+
clearTimeout(timeoutHandle);
|
|
746
|
+
combinedSignal.dispose();
|
|
649
747
|
}
|
|
650
748
|
}
|
|
651
749
|
async sendNotification(method, ...args) {
|
|
@@ -674,17 +772,17 @@ class LspClientTransport {
|
|
|
674
772
|
try {
|
|
675
773
|
await this.sendRequest("shutdown");
|
|
676
774
|
} catch (error) {
|
|
677
|
-
reportBestEffortCleanupError("shutdown request", error);
|
|
775
|
+
reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
|
|
678
776
|
}
|
|
679
777
|
try {
|
|
680
778
|
await this.sendNotification("exit");
|
|
681
779
|
} catch (error) {
|
|
682
|
-
reportBestEffortCleanupError("exit notification", error);
|
|
780
|
+
reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
|
|
683
781
|
}
|
|
684
782
|
try {
|
|
685
783
|
this.connection.dispose();
|
|
686
784
|
} catch (error) {
|
|
687
|
-
reportBestEffortCleanupError("connection dispose", error);
|
|
785
|
+
reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
|
|
688
786
|
}
|
|
689
787
|
this.connection = null;
|
|
690
788
|
}
|
|
@@ -715,11 +813,11 @@ class LspClientTransport {
|
|
|
715
813
|
new Promise((resolve) => setTimeout(resolve, STOP_SIGKILL_GRACE_MS))
|
|
716
814
|
]);
|
|
717
815
|
} catch (error) {
|
|
718
|
-
reportBestEffortCleanupError("hard process kill", error);
|
|
816
|
+
reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
|
|
719
817
|
}
|
|
720
818
|
}
|
|
721
819
|
} catch (error) {
|
|
722
|
-
reportBestEffortCleanupError("process stop", error);
|
|
820
|
+
reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
|
|
723
821
|
}
|
|
724
822
|
}
|
|
725
823
|
this.processExited = true;
|
|
@@ -729,26 +827,45 @@ class LspClientTransport {
|
|
|
729
827
|
return this.diagnosticsStore.get(uri) ?? [];
|
|
730
828
|
}
|
|
731
829
|
}
|
|
732
|
-
function
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
830
|
+
function combineAbortSignals(primary, secondary) {
|
|
831
|
+
const controller = new AbortController;
|
|
832
|
+
const abortFrom = (signal) => {
|
|
833
|
+
if (!controller.signal.aborted)
|
|
834
|
+
controller.abort(signal.reason);
|
|
835
|
+
};
|
|
836
|
+
const onPrimaryAbort = () => {
|
|
837
|
+
if (primary)
|
|
838
|
+
abortFrom(primary);
|
|
839
|
+
};
|
|
840
|
+
const onSecondaryAbort = () => abortFrom(secondary);
|
|
841
|
+
if (primary?.aborted)
|
|
842
|
+
abortFrom(primary);
|
|
843
|
+
else
|
|
844
|
+
primary?.addEventListener("abort", onPrimaryAbort, { once: true });
|
|
845
|
+
if (secondary.aborted)
|
|
846
|
+
abortFrom(secondary);
|
|
847
|
+
else
|
|
848
|
+
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
|
|
849
|
+
return {
|
|
850
|
+
signal: controller.signal,
|
|
851
|
+
dispose: () => {
|
|
852
|
+
primary?.removeEventListener("abort", onPrimaryAbort);
|
|
853
|
+
secondary.removeEventListener("abort", onSecondaryAbort);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
743
856
|
}
|
|
744
857
|
|
|
745
858
|
// ../lsp-core/src/lsp/connection.ts
|
|
746
|
-
|
|
859
|
+
function supportsDiagnosticPull(capabilities) {
|
|
860
|
+
if (capabilities === undefined)
|
|
861
|
+
return false;
|
|
862
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
863
|
+
}
|
|
747
864
|
|
|
748
865
|
class LspClientConnection extends LspClientTransport {
|
|
749
866
|
async initialize() {
|
|
750
867
|
const rootUri = pathToFileURL(this.root).href;
|
|
751
|
-
await this.sendRequest("initialize", {
|
|
868
|
+
const result = await this.sendRequest("initialize", {
|
|
752
869
|
processId: process.pid,
|
|
753
870
|
rootUri,
|
|
754
871
|
rootPath: this.root,
|
|
@@ -762,8 +879,7 @@ class LspClientConnection extends LspClientTransport {
|
|
|
762
879
|
publishDiagnostics: {},
|
|
763
880
|
rename: {
|
|
764
881
|
prepareSupport: true,
|
|
765
|
-
prepareSupportDefaultBehavior: 1
|
|
766
|
-
honorsChangeAnnotations: true
|
|
882
|
+
prepareSupportDefaultBehavior: 1
|
|
767
883
|
},
|
|
768
884
|
codeAction: {
|
|
769
885
|
codeActionLiteralSupport: {
|
|
@@ -792,22 +908,28 @@ class LspClientConnection extends LspClientTransport {
|
|
|
792
908
|
symbol: {},
|
|
793
909
|
workspaceFolders: true,
|
|
794
910
|
configuration: true,
|
|
795
|
-
applyEdit: true,
|
|
911
|
+
...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
|
|
796
912
|
workspaceEdit: {
|
|
797
|
-
documentChanges: true
|
|
913
|
+
documentChanges: true,
|
|
914
|
+
resourceOperations: ["create", "rename", "delete"]
|
|
798
915
|
}
|
|
799
916
|
}
|
|
800
917
|
},
|
|
801
918
|
initializationOptions: this.server.initialization
|
|
802
919
|
}, { timeoutMs: this.initializeTimeoutMs });
|
|
920
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
803
921
|
await this.sendNotification("initialized");
|
|
804
922
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
805
923
|
settings: { json: { validate: { enable: true } } }
|
|
806
924
|
});
|
|
807
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
808
925
|
}
|
|
809
926
|
}
|
|
810
927
|
|
|
928
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
929
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
930
|
+
import { relative, resolve } from "node:path";
|
|
931
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
932
|
+
|
|
811
933
|
// ../lsp-core/src/lsp/effective-extension.ts
|
|
812
934
|
import { basename, extname } from "node:path";
|
|
813
935
|
var BASENAME_EXTENSIONS = {
|
|
@@ -990,82 +1112,1422 @@ function getLanguageId(ext) {
|
|
|
990
1112
|
return EXT_TO_LANG[ext] ?? "plaintext";
|
|
991
1113
|
}
|
|
992
1114
|
|
|
993
|
-
// ../lsp-core/src/lsp/
|
|
994
|
-
var
|
|
995
|
-
var
|
|
1115
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1116
|
+
var WATCHED_FILE_BATCH_SIZE = 128;
|
|
1117
|
+
var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1118
|
+
function canonicalPath(filePath) {
|
|
1119
|
+
const absolute = resolve(filePath);
|
|
1120
|
+
try {
|
|
1121
|
+
return realpathSync(absolute);
|
|
1122
|
+
} catch {
|
|
1123
|
+
return absolute;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function isSameOrDescendant(candidate, parent) {
|
|
1127
|
+
const suffix = relative(parent, candidate);
|
|
1128
|
+
return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
|
|
1129
|
+
}
|
|
1130
|
+
function movedPath(candidate, oldPath, newPath) {
|
|
1131
|
+
const suffix = relative(oldPath, candidate);
|
|
1132
|
+
return suffix === "" ? newPath : resolve(newPath, suffix);
|
|
1133
|
+
}
|
|
996
1134
|
|
|
997
|
-
class
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1135
|
+
class WorkspaceDocumentState {
|
|
1136
|
+
sendNotification;
|
|
1137
|
+
clearDiagnostics;
|
|
1138
|
+
openDocuments = new Map;
|
|
1139
|
+
openByUri = new Map;
|
|
1140
|
+
openPromises = new Map;
|
|
1141
|
+
now;
|
|
1142
|
+
versionlessPublishQuiescenceMs;
|
|
1143
|
+
constructor(sendNotification, clearDiagnostics, options = {}) {
|
|
1144
|
+
this.sendNotification = sendNotification;
|
|
1145
|
+
this.clearDiagnostics = clearDiagnostics;
|
|
1146
|
+
this.now = options.now ?? (() => Date.now());
|
|
1147
|
+
this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
1004
1148
|
}
|
|
1005
1149
|
async openFile(filePath) {
|
|
1006
|
-
const
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1150
|
+
const path = canonicalPath(filePath);
|
|
1151
|
+
const existingOpen = this.openPromises.get(path);
|
|
1152
|
+
if (existingOpen) {
|
|
1153
|
+
await existingOpen;
|
|
1154
|
+
return this.openFile(path);
|
|
1155
|
+
}
|
|
1156
|
+
const text = readFileSync(path, "utf-8");
|
|
1157
|
+
const existing = this.openDocuments.get(path);
|
|
1158
|
+
if (!existing)
|
|
1159
|
+
return this.openDocumentSingleFlight(path, text);
|
|
1160
|
+
if (existing.text === text)
|
|
1161
|
+
return;
|
|
1162
|
+
await this.changeDocument(existing, text);
|
|
1163
|
+
}
|
|
1164
|
+
getVersion(filePath) {
|
|
1165
|
+
return this.openDocuments.get(canonicalPath(filePath))?.version;
|
|
1166
|
+
}
|
|
1167
|
+
getStoredDiagnostics(uri) {
|
|
1168
|
+
const state = this.openByUri.get(uri);
|
|
1169
|
+
if (!state)
|
|
1170
|
+
return [];
|
|
1171
|
+
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
1172
|
+
}
|
|
1173
|
+
captureDiagnosticSnapshot(filePath) {
|
|
1174
|
+
const state = this.openDocuments.get(canonicalPath(filePath));
|
|
1175
|
+
if (!state)
|
|
1176
|
+
return null;
|
|
1177
|
+
return {
|
|
1178
|
+
path: state.path,
|
|
1179
|
+
uri: state.uri,
|
|
1180
|
+
version: state.version,
|
|
1181
|
+
documentGeneration: state.generation,
|
|
1182
|
+
publishGeneration: state.publishGeneration
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
isCurrentSnapshot(snapshot) {
|
|
1186
|
+
const state = this.openDocuments.get(snapshot.path);
|
|
1187
|
+
return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
|
|
1188
|
+
}
|
|
1189
|
+
getPullCache(snapshot) {
|
|
1190
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1191
|
+
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
|
|
1192
|
+
return null;
|
|
1193
|
+
return state.pullCache;
|
|
1194
|
+
}
|
|
1195
|
+
recordPullDiagnostics(snapshot, report) {
|
|
1196
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1197
|
+
if (!state)
|
|
1198
|
+
return;
|
|
1199
|
+
state.pullCache = {
|
|
1200
|
+
documentVersion: snapshot.version,
|
|
1201
|
+
diagnostics: [...report.diagnostics],
|
|
1202
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
recordPublishedDiagnostics(params) {
|
|
1206
|
+
const state = this.openByUri.get(params.uri);
|
|
1207
|
+
if (!state)
|
|
1208
|
+
return;
|
|
1209
|
+
state.publishGeneration += 1;
|
|
1210
|
+
state.lastPublish = {
|
|
1211
|
+
diagnostics: [...params.diagnostics],
|
|
1212
|
+
publishGeneration: state.publishGeneration,
|
|
1213
|
+
documentGenerationAtArrival: state.generation,
|
|
1214
|
+
arrivedAt: this.now(),
|
|
1215
|
+
...params.version === undefined ? {} : { version: params.version }
|
|
1216
|
+
};
|
|
1217
|
+
this.notifyWaiters(state);
|
|
1218
|
+
}
|
|
1219
|
+
resolvePushDiagnostics(snapshot) {
|
|
1220
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1221
|
+
if (!state?.lastPublish)
|
|
1222
|
+
return { status: "missing" };
|
|
1223
|
+
const publish = state.lastPublish;
|
|
1224
|
+
if (publish.version !== undefined) {
|
|
1225
|
+
return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
|
|
1226
|
+
}
|
|
1227
|
+
if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
|
|
1228
|
+
return { status: "missing" };
|
|
1229
|
+
const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
|
|
1230
|
+
const waitMs = Math.max(0, readyAt - this.now());
|
|
1231
|
+
return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
|
|
1232
|
+
}
|
|
1233
|
+
waitForDiagnosticsActivity(snapshot, timeoutMs) {
|
|
1234
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1235
|
+
if (!state || timeoutMs <= 0)
|
|
1236
|
+
return Promise.resolve();
|
|
1237
|
+
return new Promise((resolveActivity) => {
|
|
1238
|
+
let settled = false;
|
|
1239
|
+
const finish = () => {
|
|
1240
|
+
if (settled)
|
|
1241
|
+
return;
|
|
1242
|
+
settled = true;
|
|
1243
|
+
clearTimeout(timer);
|
|
1244
|
+
state.waiters.delete(finish);
|
|
1245
|
+
resolveActivity();
|
|
1246
|
+
};
|
|
1247
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1248
|
+
if (typeof timer.unref === "function")
|
|
1249
|
+
timer.unref();
|
|
1250
|
+
state.waiters.add(finish);
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
validateVersions(operations) {
|
|
1254
|
+
const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
|
|
1255
|
+
for (const operation of operations) {
|
|
1256
|
+
if (operation.kind === "text") {
|
|
1257
|
+
const current = versions.get(operation.path);
|
|
1258
|
+
if (operation.documentVersion !== null && current !== operation.documentVersion) {
|
|
1259
|
+
const observed = current === undefined ? "closed document" : `open document version ${current}`;
|
|
1260
|
+
return {
|
|
1261
|
+
changeIndex: operation.changeIndex,
|
|
1262
|
+
message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
if (current !== undefined)
|
|
1266
|
+
versions.set(operation.path, current + 1);
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (operation.kind === "rename") {
|
|
1270
|
+
const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
|
|
1271
|
+
for (const [path] of moved)
|
|
1272
|
+
versions.delete(path);
|
|
1273
|
+
for (const [path] of moved)
|
|
1274
|
+
versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (operation.kind === "delete") {
|
|
1278
|
+
for (const path of [...versions.keys()]) {
|
|
1279
|
+
if (isSameOrDescendant(path, operation.path))
|
|
1280
|
+
versions.delete(path);
|
|
1019
1281
|
}
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
|
|
1285
|
+
versions.set(operation.path, 1);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
async synchronize(delta) {
|
|
1291
|
+
const watched = [];
|
|
1292
|
+
for (const mutation of delta.operations)
|
|
1293
|
+
await this.synchronizeMutation(mutation, watched);
|
|
1294
|
+
for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
|
|
1295
|
+
await this.sendNotification("workspace/didChangeWatchedFiles", {
|
|
1296
|
+
changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
|
|
1020
1297
|
});
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
async synchronizeMutation(mutation, watched) {
|
|
1301
|
+
if (mutation.kind === "text") {
|
|
1302
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1303
|
+
if (state)
|
|
1304
|
+
await this.changeDocument(state, mutation.afterText);
|
|
1305
|
+
else
|
|
1306
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
|
|
1025
1307
|
return;
|
|
1026
1308
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1309
|
+
if (mutation.kind === "create") {
|
|
1310
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1311
|
+
if (state) {
|
|
1312
|
+
await this.closeDocument(state);
|
|
1313
|
+
await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
|
|
1314
|
+
} else {
|
|
1315
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
|
|
1316
|
+
}
|
|
1029
1317
|
return;
|
|
1030
1318
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1319
|
+
if (mutation.kind === "rename") {
|
|
1320
|
+
const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
|
|
1321
|
+
for (const state of moved)
|
|
1322
|
+
await this.closeDocument(state);
|
|
1323
|
+
for (const state of moved) {
|
|
1324
|
+
const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
|
|
1325
|
+
await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
|
|
1326
|
+
}
|
|
1327
|
+
if (moved.length === 0) {
|
|
1328
|
+
watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
|
|
1329
|
+
watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
|
|
1330
|
+
}
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
|
|
1334
|
+
for (const state of removed)
|
|
1335
|
+
await this.closeDocument(state);
|
|
1336
|
+
if (removed.length === 0)
|
|
1337
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
|
|
1338
|
+
}
|
|
1339
|
+
async openDocumentSingleFlight(path, text) {
|
|
1340
|
+
const existing = this.openPromises.get(path);
|
|
1341
|
+
if (existing)
|
|
1342
|
+
return existing;
|
|
1343
|
+
const open = (async () => {
|
|
1344
|
+
const state = {
|
|
1345
|
+
path,
|
|
1346
|
+
uri: pathToFileURL2(path).href,
|
|
1347
|
+
languageId: getLanguageId(effectiveExtension(path)),
|
|
1348
|
+
text,
|
|
1349
|
+
version: 1,
|
|
1350
|
+
generation: 1,
|
|
1351
|
+
publishGeneration: 0,
|
|
1352
|
+
waiters: new Set
|
|
1353
|
+
};
|
|
1354
|
+
this.openDocuments.set(path, state);
|
|
1355
|
+
this.openByUri.set(state.uri, state);
|
|
1356
|
+
this.notifyWaiters(state);
|
|
1357
|
+
await this.sendNotification("textDocument/didOpen", {
|
|
1358
|
+
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
|
|
1359
|
+
});
|
|
1360
|
+
})().finally(() => {
|
|
1361
|
+
this.openPromises.delete(path);
|
|
1362
|
+
});
|
|
1363
|
+
this.openPromises.set(path, open);
|
|
1364
|
+
return open;
|
|
1365
|
+
}
|
|
1366
|
+
async changeDocument(state, text) {
|
|
1367
|
+
state.text = text;
|
|
1368
|
+
state.version += 1;
|
|
1369
|
+
state.generation += 1;
|
|
1370
|
+
this.clearDiagnostics(state.uri);
|
|
1371
|
+
this.notifyWaiters(state);
|
|
1034
1372
|
await this.sendNotification("textDocument/didChange", {
|
|
1035
|
-
textDocument: { uri, version:
|
|
1373
|
+
textDocument: { uri: state.uri, version: state.version },
|
|
1036
1374
|
contentChanges: [{ text }]
|
|
1037
1375
|
});
|
|
1038
|
-
await this.sendNotification("textDocument/didSave", {
|
|
1039
|
-
|
|
1040
|
-
|
|
1376
|
+
await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
|
|
1377
|
+
}
|
|
1378
|
+
async closeDocument(state) {
|
|
1379
|
+
this.openDocuments.delete(state.path);
|
|
1380
|
+
this.openByUri.delete(state.uri);
|
|
1381
|
+
this.clearDiagnostics(state.uri);
|
|
1382
|
+
this.notifyWaiters(state);
|
|
1383
|
+
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
1384
|
+
}
|
|
1385
|
+
notifyWaiters(state) {
|
|
1386
|
+
for (const waiter of [...state.waiters])
|
|
1387
|
+
waiter();
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
|
|
1392
|
+
var CONCURRENT_FAILURE_REASON_BY_PHASE = {
|
|
1393
|
+
applying: "workspace/applyEdit is already in progress for this workspace mutation",
|
|
1394
|
+
settled: "workspace/applyEdit was already handled for this workspace mutation"
|
|
1395
|
+
};
|
|
1396
|
+
function workspaceApplyEditConcurrentFailureReason(phase) {
|
|
1397
|
+
return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1401
|
+
import { existsSync as existsSync3, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1402
|
+
|
|
1403
|
+
// ../lsp-core/src/lsp/workspace-edit-path.ts
|
|
1404
|
+
import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync2 } from "node:fs";
|
|
1405
|
+
import { dirname, isAbsolute, relative as relative2, resolve as resolve2 } from "node:path";
|
|
1406
|
+
import { fileURLToPath } from "node:url";
|
|
1407
|
+
|
|
1408
|
+
class WorkspaceEditPathError extends Error {
|
|
1409
|
+
path;
|
|
1410
|
+
detail;
|
|
1411
|
+
name = "WorkspaceEditPathError";
|
|
1412
|
+
constructor(path, detail) {
|
|
1413
|
+
super(`${detail}: ${path}`);
|
|
1414
|
+
this.path = path;
|
|
1415
|
+
this.detail = detail;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
1419
|
+
const relativePath = relative2(workspaceRoot, filePath);
|
|
1420
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
1421
|
+
}
|
|
1422
|
+
function canonicalizeMissingPath(filePath) {
|
|
1423
|
+
let ancestor = filePath;
|
|
1424
|
+
while (!existsSync2(ancestor)) {
|
|
1425
|
+
const parent = dirname(ancestor);
|
|
1426
|
+
if (parent === ancestor)
|
|
1427
|
+
throw new WorkspaceEditPathError(filePath, "no existing ancestor");
|
|
1428
|
+
ancestor = parent;
|
|
1429
|
+
}
|
|
1430
|
+
return resolve2(realpathSync2(ancestor), relative2(ancestor, filePath));
|
|
1431
|
+
}
|
|
1432
|
+
function canonicalWorkspaceRoot(workspaceRoot) {
|
|
1433
|
+
try {
|
|
1434
|
+
const canonical = realpathSync2(resolve2(workspaceRoot));
|
|
1435
|
+
if (!lstatSync(canonical).isDirectory()) {
|
|
1436
|
+
return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
|
|
1437
|
+
}
|
|
1438
|
+
return {
|
|
1439
|
+
success: true,
|
|
1440
|
+
path: canonical,
|
|
1441
|
+
requestedPath: resolve2(workspaceRoot),
|
|
1442
|
+
followedSymbolicLink: existsSync2(resolve2(workspaceRoot)) && lstatSync(resolve2(workspaceRoot)).isSymbolicLink()
|
|
1443
|
+
};
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1446
|
+
return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
|
|
1450
|
+
let requestedPath;
|
|
1451
|
+
try {
|
|
1452
|
+
const parsed = new URL(uri);
|
|
1453
|
+
if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
|
|
1454
|
+
return { success: false, error: `non-file URI ${uri}` };
|
|
1455
|
+
}
|
|
1456
|
+
requestedPath = resolve2(fileURLToPath(parsed));
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1459
|
+
return { success: false, error: `non-file URI ${uri}: ${detail}` };
|
|
1460
|
+
}
|
|
1461
|
+
try {
|
|
1462
|
+
const canonical = existsSync2(requestedPath) ? realpathSync2(requestedPath) : canonicalizeMissingPath(requestedPath);
|
|
1463
|
+
if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
|
|
1464
|
+
return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
|
|
1465
|
+
}
|
|
1466
|
+
return {
|
|
1467
|
+
success: true,
|
|
1468
|
+
path: canonical,
|
|
1469
|
+
requestedPath,
|
|
1470
|
+
followedSymbolicLink: existsSync2(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
|
|
1471
|
+
};
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1474
|
+
return { success: false, error: `${requestedPath}: ${detail}` };
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function snapshotPath(path, includeChildren) {
|
|
1478
|
+
if (!existsSync2(path))
|
|
1479
|
+
return { kind: "missing" };
|
|
1480
|
+
const stats = lstatSync(path);
|
|
1481
|
+
if (stats.isFile())
|
|
1482
|
+
return { kind: "file", content: readFileSync2(path, "utf-8") };
|
|
1483
|
+
if (stats.isDirectory()) {
|
|
1484
|
+
return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
|
|
1485
|
+
}
|
|
1486
|
+
throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1490
|
+
var DEFAULT_IO = {
|
|
1491
|
+
writeFile(path, content) {
|
|
1492
|
+
writeFileSync(path, content, "utf-8");
|
|
1493
|
+
},
|
|
1494
|
+
rename(oldPath, newPath) {
|
|
1495
|
+
renameSync(oldPath, newPath);
|
|
1496
|
+
},
|
|
1497
|
+
remove(path, recursive) {
|
|
1498
|
+
rmSync(path, { recursive, force: false });
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
function snapshotsEqual(expected, actual) {
|
|
1502
|
+
if (expected.kind !== actual.kind)
|
|
1503
|
+
return false;
|
|
1504
|
+
if (expected.kind === "file" && actual.kind === "file")
|
|
1505
|
+
return expected.content === actual.content;
|
|
1506
|
+
if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
|
|
1507
|
+
return JSON.stringify(expected.children) === JSON.stringify(actual.children);
|
|
1508
|
+
}
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
function liveSnapshot(path, expected) {
|
|
1512
|
+
return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
|
|
1513
|
+
}
|
|
1514
|
+
function firstOperationIndex(plan) {
|
|
1515
|
+
return plan.operations[0]?.changeIndex ?? 0;
|
|
1516
|
+
}
|
|
1517
|
+
function failedCommit(plan, failure) {
|
|
1518
|
+
const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
|
|
1519
|
+
return {
|
|
1520
|
+
result: {
|
|
1521
|
+
success: false,
|
|
1522
|
+
filesModified,
|
|
1523
|
+
totalEdits,
|
|
1524
|
+
errors: [`change ${changeIndex}: ${message}`],
|
|
1525
|
+
failedChange: changeIndex,
|
|
1526
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1527
|
+
},
|
|
1528
|
+
delta: mutationDelta(mutations),
|
|
1529
|
+
fingerprint: plan.fingerprint
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function verifySnapshots(plan) {
|
|
1533
|
+
for (const [path, expected] of plan.snapshots) {
|
|
1534
|
+
let actual;
|
|
1535
|
+
try {
|
|
1536
|
+
actual = liveSnapshot(path, expected);
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1539
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1540
|
+
return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
|
|
1541
|
+
}
|
|
1542
|
+
if (!snapshotsEqual(expected, actual)) {
|
|
1543
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1544
|
+
return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
function addModifiedPath(paths, path) {
|
|
1550
|
+
if (!paths.includes(path))
|
|
1551
|
+
paths.push(path);
|
|
1552
|
+
}
|
|
1553
|
+
function reportedPath(plan, path) {
|
|
1554
|
+
return plan.reportedPathByCanonical.get(path) ?? path;
|
|
1555
|
+
}
|
|
1556
|
+
function changedPathsForMutation(mutation) {
|
|
1557
|
+
return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
|
|
1558
|
+
}
|
|
1559
|
+
function mutationDelta(operations) {
|
|
1560
|
+
const changedPaths = new Set;
|
|
1561
|
+
for (const operation of operations) {
|
|
1562
|
+
for (const path of changedPathsForMutation(operation))
|
|
1563
|
+
changedPaths.add(path);
|
|
1564
|
+
}
|
|
1565
|
+
return { operations, changedPaths: [...changedPaths].sort() };
|
|
1566
|
+
}
|
|
1567
|
+
function resolveIo(overrides) {
|
|
1568
|
+
return {
|
|
1569
|
+
writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
|
|
1570
|
+
rename: overrides?.rename ?? DEFAULT_IO.rename,
|
|
1571
|
+
remove: overrides?.remove ?? DEFAULT_IO.remove
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
function commitOperation(context, operation) {
|
|
1575
|
+
const { plan, io, accumulator } = context;
|
|
1576
|
+
if (operation.kind === "noop")
|
|
1577
|
+
return;
|
|
1578
|
+
if (operation.kind === "text") {
|
|
1579
|
+
io.writeFile(operation.path, operation.afterText);
|
|
1580
|
+
accumulator.mutations.push({
|
|
1581
|
+
kind: "text",
|
|
1582
|
+
path: operation.path,
|
|
1583
|
+
beforeText: operation.beforeText,
|
|
1584
|
+
afterText: operation.afterText
|
|
1585
|
+
});
|
|
1586
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1587
|
+
accumulator.totalEdits += operation.editCount;
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (operation.kind === "create") {
|
|
1591
|
+
io.writeFile(operation.path, "");
|
|
1592
|
+
accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
|
|
1593
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (operation.kind === "rename") {
|
|
1597
|
+
if (operation.replaceDestination) {
|
|
1598
|
+
const targetKind = existsSync3(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
|
|
1599
|
+
io.remove(operation.newPath, targetKind === "directory");
|
|
1600
|
+
accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
|
|
1601
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1602
|
+
}
|
|
1603
|
+
io.rename(operation.oldPath, operation.newPath);
|
|
1604
|
+
accumulator.mutations.push({
|
|
1605
|
+
kind: "rename",
|
|
1606
|
+
oldPath: operation.oldPath,
|
|
1607
|
+
newPath: operation.newPath,
|
|
1608
|
+
sourceKind: operation.sourceKind
|
|
1041
1609
|
});
|
|
1610
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
io.remove(operation.path, operation.recursive);
|
|
1614
|
+
accumulator.mutations.push({
|
|
1615
|
+
kind: "delete",
|
|
1616
|
+
path: operation.path,
|
|
1617
|
+
targetKind: operation.targetKind
|
|
1618
|
+
});
|
|
1619
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1620
|
+
}
|
|
1621
|
+
function commitWorkspaceEditPlan(plan, options = {}) {
|
|
1622
|
+
if (options.signal?.aborted) {
|
|
1623
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
1624
|
+
}
|
|
1625
|
+
const stale = verifySnapshots(plan);
|
|
1626
|
+
if (stale)
|
|
1627
|
+
return stale;
|
|
1628
|
+
if (options.signal?.aborted) {
|
|
1629
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
1630
|
+
}
|
|
1631
|
+
const io = resolveIo(options.io);
|
|
1632
|
+
const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
|
|
1633
|
+
const context = { plan, io, accumulator };
|
|
1634
|
+
let lateAbort = false;
|
|
1635
|
+
for (const operation of plan.operations) {
|
|
1636
|
+
try {
|
|
1637
|
+
commitOperation(context, operation);
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1640
|
+
return failedCommit(plan, {
|
|
1641
|
+
message: `I/O failure during ${operation.kind}: ${detail}`,
|
|
1642
|
+
changeIndex: operation.changeIndex,
|
|
1643
|
+
mutations: accumulator.mutations,
|
|
1644
|
+
filesModified: accumulator.filesModified,
|
|
1645
|
+
totalEdits: accumulator.totalEdits,
|
|
1646
|
+
lateAbort: lateAbort || options.signal?.aborted === true
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
if (options.signal?.aborted)
|
|
1650
|
+
lateAbort = true;
|
|
1651
|
+
}
|
|
1652
|
+
const result = {
|
|
1653
|
+
success: true,
|
|
1654
|
+
filesModified: accumulator.filesModified,
|
|
1655
|
+
totalEdits: accumulator.totalEdits,
|
|
1656
|
+
errors: [],
|
|
1657
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1658
|
+
};
|
|
1659
|
+
return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
|
|
1663
|
+
import { createHash } from "node:crypto";
|
|
1664
|
+
function canonicalFingerprint(operations) {
|
|
1665
|
+
const canonical = operations.map((operation) => {
|
|
1666
|
+
switch (operation.kind) {
|
|
1667
|
+
case "text":
|
|
1668
|
+
return {
|
|
1669
|
+
kind: operation.kind,
|
|
1670
|
+
changeIndex: operation.changeIndex,
|
|
1671
|
+
path: operation.path,
|
|
1672
|
+
edits: operation.edits,
|
|
1673
|
+
version: operation.version
|
|
1674
|
+
};
|
|
1675
|
+
case "rename":
|
|
1676
|
+
return {
|
|
1677
|
+
kind: operation.kind,
|
|
1678
|
+
changeIndex: operation.changeIndex,
|
|
1679
|
+
oldPath: operation.oldPath,
|
|
1680
|
+
newPath: operation.newPath,
|
|
1681
|
+
overwrite: operation.overwrite,
|
|
1682
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
1683
|
+
};
|
|
1684
|
+
case "create":
|
|
1685
|
+
return {
|
|
1686
|
+
kind: operation.kind,
|
|
1687
|
+
changeIndex: operation.changeIndex,
|
|
1688
|
+
path: operation.path,
|
|
1689
|
+
overwrite: operation.overwrite,
|
|
1690
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
1691
|
+
};
|
|
1692
|
+
case "delete":
|
|
1693
|
+
return {
|
|
1694
|
+
kind: operation.kind,
|
|
1695
|
+
changeIndex: operation.changeIndex,
|
|
1696
|
+
path: operation.path,
|
|
1697
|
+
recursive: operation.recursive,
|
|
1698
|
+
ignoreIfNotExists: operation.ignoreIfNotExists
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// ../lsp-core/src/lsp/workspace-edit-types.ts
|
|
1706
|
+
class WorkspaceEditValidationError extends Error {
|
|
1707
|
+
changeIndex;
|
|
1708
|
+
detail;
|
|
1709
|
+
name = "WorkspaceEditValidationError";
|
|
1710
|
+
constructor(changeIndex, detail) {
|
|
1711
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
1712
|
+
this.changeIndex = changeIndex;
|
|
1713
|
+
this.detail = detail;
|
|
1042
1714
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
|
|
1718
|
+
function isRecord2(value) {
|
|
1719
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1720
|
+
}
|
|
1721
|
+
function parsePosition(value) {
|
|
1722
|
+
if (!isRecord2(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
return { line: value["line"], character: value["character"] };
|
|
1726
|
+
}
|
|
1727
|
+
function parseRange(value) {
|
|
1728
|
+
if (!isRecord2(value))
|
|
1729
|
+
return null;
|
|
1730
|
+
const start = parsePosition(value["start"]);
|
|
1731
|
+
const end = parsePosition(value["end"]);
|
|
1732
|
+
return start && end ? { start, end } : null;
|
|
1733
|
+
}
|
|
1734
|
+
function parseTextEdits(value, changeIndex) {
|
|
1735
|
+
if (!Array.isArray(value)) {
|
|
1736
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
|
|
1737
|
+
}
|
|
1738
|
+
const edits = [];
|
|
1739
|
+
for (const candidate of value) {
|
|
1740
|
+
if (!isRecord2(candidate) || typeof candidate["newText"] !== "string") {
|
|
1741
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
|
|
1742
|
+
}
|
|
1743
|
+
if ("annotationId" in candidate) {
|
|
1744
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
|
|
1745
|
+
}
|
|
1746
|
+
const range = parseRange(candidate["range"]);
|
|
1747
|
+
if (!range)
|
|
1748
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
|
|
1749
|
+
edits.push({ range, newText: candidate["newText"] });
|
|
1750
|
+
}
|
|
1751
|
+
return edits;
|
|
1752
|
+
}
|
|
1753
|
+
function parseBooleanOption(options, key, changeIndex) {
|
|
1754
|
+
const value = options[key];
|
|
1755
|
+
if (value === undefined)
|
|
1756
|
+
return false;
|
|
1757
|
+
if (typeof value !== "boolean") {
|
|
1758
|
+
throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
|
|
1759
|
+
}
|
|
1760
|
+
return value;
|
|
1761
|
+
}
|
|
1762
|
+
function parseOptions(value, allowed, changeIndex) {
|
|
1763
|
+
if (value === undefined)
|
|
1764
|
+
return {};
|
|
1765
|
+
if (!isRecord2(value))
|
|
1766
|
+
throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
|
|
1767
|
+
for (const key of Object.keys(value)) {
|
|
1768
|
+
if (!allowed.includes(key))
|
|
1769
|
+
throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
|
|
1770
|
+
}
|
|
1771
|
+
const parsed = {};
|
|
1772
|
+
for (const key of allowed)
|
|
1773
|
+
parsed[key] = parseBooleanOption(value, key, changeIndex);
|
|
1774
|
+
return parsed;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
|
|
1778
|
+
function parseResourceChange(input) {
|
|
1779
|
+
const kind = input.change["kind"];
|
|
1780
|
+
if (kind === "create" || kind === "delete") {
|
|
1781
|
+
parseSinglePathResource(input, kind);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (kind !== "rename") {
|
|
1785
|
+
throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
|
|
1786
|
+
}
|
|
1787
|
+
parseRename(input);
|
|
1788
|
+
}
|
|
1789
|
+
function parseSinglePathResource(input, kind) {
|
|
1790
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1791
|
+
if (typeof change["uri"] !== "string")
|
|
1792
|
+
throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
|
|
1793
|
+
const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
|
|
1794
|
+
if (!resolvedPath.success) {
|
|
1795
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (kind === "create") {
|
|
1799
|
+
const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
1800
|
+
target.operations.push({
|
|
1801
|
+
kind,
|
|
1802
|
+
changeIndex,
|
|
1803
|
+
path: resolvedPath.path,
|
|
1804
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1805
|
+
overwrite: options2["overwrite"] ?? false,
|
|
1806
|
+
ignoreIfExists: options2["ignoreIfExists"] ?? false,
|
|
1807
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
1808
|
+
});
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
|
|
1812
|
+
target.operations.push({
|
|
1813
|
+
kind,
|
|
1814
|
+
changeIndex,
|
|
1815
|
+
path: resolvedPath.path,
|
|
1816
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1817
|
+
recursive: options["recursive"] ?? false,
|
|
1818
|
+
ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
|
|
1819
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
function parseRename(input) {
|
|
1823
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1824
|
+
if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
|
|
1825
|
+
throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
|
|
1826
|
+
}
|
|
1827
|
+
const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
|
|
1828
|
+
const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
|
|
1829
|
+
if (!oldPath.success || !newPath.success) {
|
|
1830
|
+
target.failures.push({
|
|
1831
|
+
changeIndex,
|
|
1832
|
+
message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
|
|
1833
|
+
});
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
1837
|
+
target.operations.push({
|
|
1838
|
+
kind: "rename",
|
|
1839
|
+
changeIndex,
|
|
1840
|
+
oldPath: oldPath.path,
|
|
1841
|
+
newPath: newPath.path,
|
|
1842
|
+
reportedOldPath: oldPath.requestedPath,
|
|
1843
|
+
reportedNewPath: newPath.requestedPath,
|
|
1844
|
+
overwrite: options["overwrite"] ?? false,
|
|
1845
|
+
ignoreIfExists: options["ignoreIfExists"] ?? false,
|
|
1846
|
+
followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// ../lsp-core/src/lsp/workspace-edit-parser.ts
|
|
1851
|
+
function failureResult(failures) {
|
|
1852
|
+
const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
|
|
1853
|
+
const first = sorted[0];
|
|
1854
|
+
return {
|
|
1855
|
+
success: false,
|
|
1856
|
+
filesModified: [],
|
|
1857
|
+
totalEdits: 0,
|
|
1858
|
+
errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
|
|
1859
|
+
...first ? { failedChange: first.changeIndex } : {}
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
function parseWorkspaceEdit(edit, workspaceRoot) {
|
|
1863
|
+
if (!isRecord2(edit))
|
|
1864
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
|
|
1865
|
+
if (edit["changeAnnotations"] !== undefined) {
|
|
1866
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
|
|
1867
|
+
}
|
|
1868
|
+
const hasChanges = edit["changes"] !== undefined;
|
|
1869
|
+
const hasDocumentChanges = edit["documentChanges"] !== undefined;
|
|
1870
|
+
if (hasChanges && hasDocumentChanges) {
|
|
1871
|
+
return {
|
|
1872
|
+
operations: [],
|
|
1873
|
+
failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
const target = { operations: [], failures: [] };
|
|
1877
|
+
if (hasChanges)
|
|
1878
|
+
return parseChanges(edit["changes"], workspaceRoot, target);
|
|
1879
|
+
return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
|
|
1880
|
+
}
|
|
1881
|
+
function parseChanges(value, workspaceRoot, target) {
|
|
1882
|
+
if (!isRecord2(value))
|
|
1883
|
+
return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
|
|
1884
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
1885
|
+
for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
|
|
1886
|
+
const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
|
|
1887
|
+
if (!resolvedPath.success) {
|
|
1888
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
target.operations.push({
|
|
1893
|
+
kind: "text",
|
|
1894
|
+
changeIndex,
|
|
1895
|
+
path: resolvedPath.path,
|
|
1896
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1897
|
+
edits: parseTextEdits(rawEdits, changeIndex),
|
|
1898
|
+
version: null
|
|
1899
|
+
});
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
1902
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
throw error;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
return target;
|
|
1909
|
+
}
|
|
1910
|
+
function parseDocumentChanges(value, workspaceRoot, target) {
|
|
1911
|
+
if (value === undefined)
|
|
1912
|
+
return target;
|
|
1913
|
+
if (!Array.isArray(value)) {
|
|
1914
|
+
return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
|
|
1915
|
+
}
|
|
1916
|
+
for (const [changeIndex, change] of value.entries()) {
|
|
1917
|
+
try {
|
|
1918
|
+
parseDocumentChange({ change, changeIndex, workspaceRoot, target });
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
1921
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
throw error;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
return target;
|
|
1928
|
+
}
|
|
1929
|
+
function parseDocumentChange(input) {
|
|
1930
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
1931
|
+
if (!isRecord2(change))
|
|
1932
|
+
throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
|
|
1933
|
+
if ("annotationId" in change) {
|
|
1934
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
|
|
1935
|
+
}
|
|
1936
|
+
if (typeof change["kind"] === "string") {
|
|
1937
|
+
parseResourceChange({ change, changeIndex, workspaceRoot, target });
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const identifier = change["textDocument"];
|
|
1941
|
+
if (!isRecord2(identifier) || typeof identifier["uri"] !== "string") {
|
|
1942
|
+
throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
|
|
1943
|
+
}
|
|
1944
|
+
const version = identifier["version"];
|
|
1945
|
+
if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
|
|
1946
|
+
throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
|
|
1947
|
+
}
|
|
1948
|
+
const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
|
|
1949
|
+
if (!resolvedPath.success) {
|
|
1950
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
target.operations.push({
|
|
1954
|
+
kind: "text",
|
|
1955
|
+
changeIndex,
|
|
1956
|
+
path: resolvedPath.path,
|
|
1957
|
+
reportedPath: resolvedPath.requestedPath,
|
|
1958
|
+
edits: parseTextEdits(change["edits"], changeIndex),
|
|
1959
|
+
version
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
1964
|
+
import { dirname as dirname2, relative as relative3, resolve as resolve3 } from "node:path";
|
|
1965
|
+
|
|
1966
|
+
// ../lsp-core/src/lsp/workspace-edit-text.ts
|
|
1967
|
+
function comparePosition(left, right) {
|
|
1968
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
1969
|
+
}
|
|
1970
|
+
function positionsEqual(left, right) {
|
|
1971
|
+
return left.line === right.line && left.character === right.character;
|
|
1972
|
+
}
|
|
1973
|
+
function rangesEqual(left, right) {
|
|
1974
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
1975
|
+
}
|
|
1976
|
+
function isEmptyRange(range) {
|
|
1977
|
+
return positionsEqual(range.start, range.end);
|
|
1978
|
+
}
|
|
1979
|
+
function formatRange(range) {
|
|
1980
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
1981
|
+
}
|
|
1982
|
+
function validatePosition(position, label, context) {
|
|
1983
|
+
const { lines, changeIndex } = context;
|
|
1984
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
1985
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
1986
|
+
}
|
|
1987
|
+
if (position.line < 0 || position.character < 0) {
|
|
1988
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
1989
|
+
}
|
|
1990
|
+
const line = lines[position.line];
|
|
1991
|
+
if (line === undefined) {
|
|
1992
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
1993
|
+
}
|
|
1994
|
+
if (position.character > line.length) {
|
|
1995
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
function validateRange(range, lines, changeIndex) {
|
|
1999
|
+
const context = { lines, changeIndex };
|
|
2000
|
+
validatePosition(range.start, "start", context);
|
|
2001
|
+
validatePosition(range.end, "end", context);
|
|
2002
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
2003
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function sortAndDeduplicate(edits) {
|
|
2007
|
+
const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
|
|
2008
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
2009
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
2010
|
+
});
|
|
2011
|
+
const unique = [];
|
|
2012
|
+
for (const entry of sorted) {
|
|
2013
|
+
const previous = unique.at(-1);
|
|
2014
|
+
if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
unique.push(entry.edit);
|
|
2018
|
+
}
|
|
2019
|
+
return unique;
|
|
2020
|
+
}
|
|
2021
|
+
function validateNoOverlap(edits, changeIndex) {
|
|
2022
|
+
for (let index = 0;index < edits.length - 1; index += 1) {
|
|
2023
|
+
const later = edits[index];
|
|
2024
|
+
const earlier = edits[index + 1];
|
|
2025
|
+
if (later === undefined || earlier === undefined)
|
|
2026
|
+
continue;
|
|
2027
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
2028
|
+
throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
function applyNormalizedTextEdits(content, edits) {
|
|
2033
|
+
const lines = content.split(`
|
|
2034
|
+
`);
|
|
2035
|
+
for (const edit of edits) {
|
|
2036
|
+
const { start, end } = edit.range;
|
|
2037
|
+
const startLine = lines[start.line];
|
|
2038
|
+
const endLine = lines[end.line];
|
|
2039
|
+
if (startLine === undefined || endLine === undefined)
|
|
2040
|
+
continue;
|
|
2041
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
2042
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
|
|
2043
|
+
`));
|
|
2044
|
+
}
|
|
2045
|
+
return lines.join(`
|
|
2046
|
+
`);
|
|
2047
|
+
}
|
|
2048
|
+
function normalizeTextEdits(content, edits, changeIndex) {
|
|
2049
|
+
const lines = content.split(`
|
|
2050
|
+
`);
|
|
2051
|
+
for (const edit of edits) {
|
|
2052
|
+
validateRange(edit.range, lines, changeIndex);
|
|
2053
|
+
}
|
|
2054
|
+
const normalized = sortAndDeduplicate(edits);
|
|
2055
|
+
validateNoOverlap(normalized, changeIndex);
|
|
2056
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2060
|
+
function isSameOrDescendant2(candidate, parent) {
|
|
2061
|
+
const relativePath = relative3(parent, candidate);
|
|
2062
|
+
return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
|
|
2063
|
+
}
|
|
2064
|
+
function removeVirtualSubtree(virtual, path) {
|
|
2065
|
+
for (const candidate of [...virtual.keys()]) {
|
|
2066
|
+
if (isSameOrDescendant2(candidate, path))
|
|
2067
|
+
virtual.delete(candidate);
|
|
2068
|
+
}
|
|
2069
|
+
virtual.set(path, { kind: "missing" });
|
|
2070
|
+
}
|
|
2071
|
+
function moveVirtualSubtree(virtual, oldPath, newPath) {
|
|
2072
|
+
const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
|
|
2073
|
+
removeVirtualSubtree(virtual, oldPath);
|
|
2074
|
+
removeVirtualSubtree(virtual, newPath);
|
|
2075
|
+
for (const [candidate, entry] of moved) {
|
|
2076
|
+
const suffix = relative3(oldPath, candidate);
|
|
2077
|
+
virtual.set(suffix === "" ? newPath : resolve3(newPath, suffix), entry);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function virtualDirectoryHasChildren(virtual, path) {
|
|
2081
|
+
for (const [candidate, entry] of virtual) {
|
|
2082
|
+
if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
|
|
2083
|
+
return true;
|
|
2084
|
+
}
|
|
2085
|
+
return false;
|
|
2086
|
+
}
|
|
2087
|
+
function requireVirtualParent(virtual, path, changeIndex) {
|
|
2088
|
+
if (virtual.get(dirname2(path))?.kind !== "directory") {
|
|
2089
|
+
throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
function simulateOperations(parsed, snapshots) {
|
|
2093
|
+
const virtual = new Map(snapshots);
|
|
2094
|
+
const planned = [];
|
|
2095
|
+
const failures = [];
|
|
2096
|
+
for (const operation of parsed) {
|
|
2097
|
+
try {
|
|
2098
|
+
planned.push(simulateOperation(operation, virtual));
|
|
2099
|
+
} catch (error) {
|
|
2100
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2101
|
+
failures.push({ changeIndex: operation.changeIndex, message: error.detail });
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
throw error;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
return { operations: planned, failures };
|
|
2108
|
+
}
|
|
2109
|
+
function simulateOperation(operation, virtual) {
|
|
2110
|
+
switch (operation.kind) {
|
|
2111
|
+
case "text":
|
|
2112
|
+
return simulateText(operation, virtual);
|
|
2113
|
+
case "create":
|
|
2114
|
+
return simulateCreate(operation, virtual);
|
|
2115
|
+
case "rename":
|
|
2116
|
+
return simulateRename(operation, virtual);
|
|
2117
|
+
case "delete":
|
|
2118
|
+
return simulateDelete(operation, virtual);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
function rejectSymbolicLink(operation) {
|
|
2122
|
+
if (operation.followedSymbolicLink) {
|
|
2123
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
function simulateText(operation, virtual) {
|
|
2127
|
+
const entry = virtual.get(operation.path);
|
|
2128
|
+
if (entry?.kind !== "file")
|
|
2129
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
|
|
2130
|
+
const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
|
|
2131
|
+
virtual.set(operation.path, { kind: "file", content: normalized.text });
|
|
2132
|
+
return {
|
|
2133
|
+
kind: "text",
|
|
2134
|
+
changeIndex: operation.changeIndex,
|
|
2135
|
+
path: operation.path,
|
|
2136
|
+
beforeText: entry.content,
|
|
2137
|
+
afterText: normalized.text,
|
|
2138
|
+
editCount: normalized.edits.length,
|
|
2139
|
+
documentVersion: operation.version
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
function simulateCreate(operation, virtual) {
|
|
2143
|
+
rejectSymbolicLink(operation);
|
|
2144
|
+
requireVirtualParent(virtual, operation.path, operation.changeIndex);
|
|
2145
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2146
|
+
if (target.kind !== "missing") {
|
|
2147
|
+
if (operation.overwrite && target.kind === "file") {
|
|
2148
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2149
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
|
|
2150
|
+
}
|
|
2151
|
+
if (operation.ignoreIfExists)
|
|
2152
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2153
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
|
|
2154
|
+
}
|
|
2155
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2156
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
|
|
2157
|
+
}
|
|
2158
|
+
function simulateRename(operation, virtual) {
|
|
2159
|
+
rejectSymbolicLink(operation);
|
|
2160
|
+
const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
|
|
2161
|
+
if (source.kind === "missing") {
|
|
2162
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
|
|
2163
|
+
}
|
|
2164
|
+
if (operation.oldPath === operation.newPath)
|
|
2165
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2166
|
+
if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
|
|
2167
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
|
|
2168
|
+
}
|
|
2169
|
+
requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
|
|
2170
|
+
const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
|
|
2171
|
+
if (destination.kind !== "missing" && !operation.overwrite) {
|
|
2172
|
+
if (operation.ignoreIfExists)
|
|
2173
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2174
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
|
|
2175
|
+
}
|
|
2176
|
+
moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
|
|
2177
|
+
return {
|
|
2178
|
+
kind: "rename",
|
|
2179
|
+
changeIndex: operation.changeIndex,
|
|
2180
|
+
oldPath: operation.oldPath,
|
|
2181
|
+
newPath: operation.newPath,
|
|
2182
|
+
sourceKind: source.kind,
|
|
2183
|
+
replaceDestination: destination.kind !== "missing"
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function simulateDelete(operation, virtual) {
|
|
2187
|
+
rejectSymbolicLink(operation);
|
|
2188
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2189
|
+
if (target.kind === "missing") {
|
|
2190
|
+
if (operation.ignoreIfNotExists)
|
|
2191
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2192
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
|
|
2193
|
+
}
|
|
2194
|
+
if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
|
|
2195
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
|
|
2196
|
+
}
|
|
2197
|
+
removeVirtualSubtree(virtual, operation.path);
|
|
2198
|
+
return {
|
|
2199
|
+
kind: "delete",
|
|
2200
|
+
changeIndex: operation.changeIndex,
|
|
2201
|
+
path: operation.path,
|
|
2202
|
+
targetKind: target.kind,
|
|
2203
|
+
recursive: operation.recursive
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// ../lsp-core/src/lsp/workspace-edit-snapshot.ts
|
|
2208
|
+
import { existsSync as existsSync4, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2209
|
+
import { dirname as dirname3, resolve as resolve4 } from "node:path";
|
|
2210
|
+
class WorkspaceSnapshotBuilder {
|
|
2211
|
+
workspaceRoot;
|
|
2212
|
+
snapshots = new Map;
|
|
2213
|
+
constructor(workspaceRoot) {
|
|
2214
|
+
this.workspaceRoot = workspaceRoot;
|
|
2215
|
+
}
|
|
2216
|
+
build(operations) {
|
|
2217
|
+
this.add(this.workspaceRoot, false);
|
|
2218
|
+
for (const operation of operations) {
|
|
2219
|
+
switch (operation.kind) {
|
|
2220
|
+
case "rename":
|
|
2221
|
+
this.add(operation.oldPath, true);
|
|
2222
|
+
this.add(operation.newPath, true);
|
|
2223
|
+
break;
|
|
2224
|
+
case "delete":
|
|
2225
|
+
this.add(operation.path, true);
|
|
2226
|
+
break;
|
|
2227
|
+
case "text":
|
|
2228
|
+
case "create":
|
|
2229
|
+
this.add(operation.path, false);
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
return this.snapshots;
|
|
2234
|
+
}
|
|
2235
|
+
add(path, includeChildren) {
|
|
2236
|
+
let candidate = path;
|
|
2237
|
+
while (true) {
|
|
2238
|
+
const existing = this.snapshots.get(candidate);
|
|
2239
|
+
if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
|
|
2240
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
|
|
2241
|
+
}
|
|
2242
|
+
if (candidate === this.workspaceRoot)
|
|
2243
|
+
break;
|
|
2244
|
+
candidate = dirname3(candidate);
|
|
2245
|
+
}
|
|
2246
|
+
if (!includeChildren || !existsSync4(path) || !lstatSync3(path).isDirectory())
|
|
2247
|
+
return;
|
|
2248
|
+
for (const child of readdirSync2(path))
|
|
2249
|
+
this.add(resolve4(path, child), true);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
function snapshotOperations(operations, workspaceRoot) {
|
|
2253
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// ../lsp-core/src/lsp/workspace-edit-plan.ts
|
|
2257
|
+
class PlanPathIndex {
|
|
2258
|
+
firstChangeByPath = new Map;
|
|
2259
|
+
reportedPathByCanonical = new Map;
|
|
2260
|
+
build(operations) {
|
|
2261
|
+
for (const operation of operations) {
|
|
2262
|
+
switch (operation.kind) {
|
|
2263
|
+
case "rename":
|
|
2264
|
+
this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
|
|
2265
|
+
this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
|
|
2266
|
+
break;
|
|
2267
|
+
case "text":
|
|
2268
|
+
case "create":
|
|
2269
|
+
case "delete":
|
|
2270
|
+
this.add(operation.path, operation.reportedPath, operation.changeIndex);
|
|
2271
|
+
break;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
add(path, reportedPath2, changeIndex) {
|
|
2276
|
+
if (!this.firstChangeByPath.has(path))
|
|
2277
|
+
this.firstChangeByPath.set(path, changeIndex);
|
|
2278
|
+
if (!this.reportedPathByCanonical.has(path))
|
|
2279
|
+
this.reportedPathByCanonical.set(path, reportedPath2);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
function fingerprintWorkspaceEdit(edit, workspaceRoot) {
|
|
2283
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2284
|
+
if (!root.success)
|
|
2285
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2286
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2287
|
+
if (parsed.failures.length > 0)
|
|
2288
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2289
|
+
return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
|
|
2290
|
+
}
|
|
2291
|
+
function planWorkspaceEdit(edit, workspaceRoot) {
|
|
2292
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2293
|
+
if (!root.success)
|
|
2294
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2295
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2296
|
+
if (parsed.failures.length > 0)
|
|
2297
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2298
|
+
let snapshots;
|
|
2299
|
+
try {
|
|
2300
|
+
snapshots = snapshotOperations(parsed.operations, root.path);
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
return {
|
|
2303
|
+
success: false,
|
|
2304
|
+
result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
const simulated = simulateOperations(parsed.operations, snapshots);
|
|
2308
|
+
if (simulated.failures.length > 0)
|
|
2309
|
+
return { success: false, result: failureResult(simulated.failures) };
|
|
2310
|
+
const paths = new PlanPathIndex;
|
|
2311
|
+
paths.build(parsed.operations);
|
|
2312
|
+
const plan = {
|
|
2313
|
+
workspaceRoot: root.path,
|
|
2314
|
+
operations: simulated.operations,
|
|
2315
|
+
snapshots,
|
|
2316
|
+
firstChangeByPath: paths.firstChangeByPath,
|
|
2317
|
+
reportedPathByCanonical: paths.reportedPathByCanonical,
|
|
2318
|
+
fingerprint: canonicalFingerprint(parsed.operations)
|
|
2319
|
+
};
|
|
2320
|
+
return { success: true, plan };
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// ../lsp-core/src/lsp/workspace-mutation-controller.ts
|
|
2324
|
+
function failure(message, failedChange, base) {
|
|
2325
|
+
return {
|
|
2326
|
+
success: false,
|
|
2327
|
+
filesModified: base?.filesModified ?? [],
|
|
2328
|
+
totalEdits: base?.totalEdits ?? 0,
|
|
2329
|
+
errors: [message],
|
|
2330
|
+
...failedChange === undefined ? {} : { failedChange },
|
|
2331
|
+
...base?.lateAbort ? { lateAbort: true } : {}
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
function responseFor(result) {
|
|
2335
|
+
if (result.success)
|
|
2336
|
+
return { applied: true };
|
|
2337
|
+
return {
|
|
2338
|
+
applied: false,
|
|
2339
|
+
failureReason: result.errors[0] ?? "workspace edit failed",
|
|
2340
|
+
...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
function isRecord3(value) {
|
|
2344
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
class WorkspaceMutationController {
|
|
2348
|
+
workspaceRoot;
|
|
2349
|
+
documents;
|
|
2350
|
+
activeLease = null;
|
|
2351
|
+
nextLeaseId = 1;
|
|
2352
|
+
io;
|
|
2353
|
+
constructor(workspaceRoot, documents) {
|
|
2354
|
+
this.workspaceRoot = workspaceRoot;
|
|
2355
|
+
this.documents = documents;
|
|
2356
|
+
}
|
|
2357
|
+
setIo(io) {
|
|
2358
|
+
this.io = io;
|
|
2359
|
+
}
|
|
2360
|
+
acquire(signal) {
|
|
2361
|
+
if (this.activeLease)
|
|
2362
|
+
return { success: false, result: failure("workspace mutation is already in progress") };
|
|
2363
|
+
if (signal?.aborted)
|
|
2364
|
+
return { success: false, result: failure("cancelled before mutating request") };
|
|
2365
|
+
const lease = {
|
|
2366
|
+
id: this.nextLeaseId,
|
|
2367
|
+
phase: "idle",
|
|
2368
|
+
...signal === undefined ? {} : { signal }
|
|
2369
|
+
};
|
|
2370
|
+
this.nextLeaseId += 1;
|
|
2371
|
+
this.activeLease = lease;
|
|
2372
|
+
return { success: true, lease };
|
|
2373
|
+
}
|
|
2374
|
+
release(lease) {
|
|
2375
|
+
if (this.activeLease?.id !== lease.id)
|
|
2376
|
+
return;
|
|
2377
|
+
this.activeLease.phase = "sealed";
|
|
2378
|
+
this.activeLease = null;
|
|
2379
|
+
}
|
|
2380
|
+
isBeforeCommit(lease) {
|
|
2381
|
+
return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
|
|
2382
|
+
}
|
|
2383
|
+
async handleApplyEdit(params) {
|
|
2384
|
+
const lease = this.activeLease;
|
|
2385
|
+
if (!lease)
|
|
2386
|
+
return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
|
|
2387
|
+
if (lease.phase !== "idle") {
|
|
2388
|
+
return {
|
|
2389
|
+
applied: false,
|
|
2390
|
+
failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
lease.phase = "applying";
|
|
2394
|
+
lease.applyCompletion = new Promise((resolve5) => {
|
|
2395
|
+
lease.resolveApply = resolve5;
|
|
2396
|
+
});
|
|
2397
|
+
const edit = isRecord3(params) ? params["edit"] : undefined;
|
|
2398
|
+
const record = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
|
|
2399
|
+
lease.serverApply = record;
|
|
2400
|
+
lease.phase = "settled";
|
|
2401
|
+
lease.resolveApply?.();
|
|
2402
|
+
return responseFor(record.result);
|
|
2403
|
+
}
|
|
2404
|
+
async reconcileRename(leaseToken, edit) {
|
|
2405
|
+
const lease = this.requireActiveLease(leaseToken);
|
|
2406
|
+
if (!lease)
|
|
2407
|
+
return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
|
|
2408
|
+
if (lease.phase === "applying")
|
|
2409
|
+
await lease.applyCompletion;
|
|
2410
|
+
if (lease.serverApply)
|
|
2411
|
+
return this.reconcileServerApply(lease.serverApply, edit);
|
|
2412
|
+
lease.phase = "sealed";
|
|
2413
|
+
if (!edit)
|
|
2414
|
+
return { edit, apply: failure("No edit provided") };
|
|
2415
|
+
const applied = await this.applyEdit(edit, lease);
|
|
2416
|
+
return { edit, apply: applied.result };
|
|
2417
|
+
}
|
|
2418
|
+
reconcileServerApply(record, edit) {
|
|
2419
|
+
if (!edit)
|
|
2420
|
+
return { edit, apply: record.result };
|
|
2421
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
|
|
2422
|
+
if (fingerprint.success && record.fingerprint !== null && fingerprint.fingerprint === record.fingerprint) {
|
|
2423
|
+
return { edit, apply: record.result };
|
|
2424
|
+
}
|
|
2425
|
+
return {
|
|
2426
|
+
edit,
|
|
2427
|
+
apply: failure("rename result conflicts with server-applied workspace edit", 0, record.result)
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
async applyEdit(edit, lease) {
|
|
2431
|
+
const planned = planWorkspaceEdit(edit, this.workspaceRoot);
|
|
2432
|
+
if (!planned.success)
|
|
2433
|
+
return { fingerprint: null, result: planned.result };
|
|
2434
|
+
const versionFailure = this.documents.validateVersions(planned.plan.operations);
|
|
2435
|
+
if (versionFailure) {
|
|
2436
|
+
return {
|
|
2437
|
+
fingerprint: planned.plan.fingerprint,
|
|
2438
|
+
result: failure(versionFailure.message, versionFailure.changeIndex)
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
const commit = commitWorkspaceEditPlan(planned.plan, {
|
|
2442
|
+
...lease.signal === undefined ? {} : { signal: lease.signal },
|
|
2443
|
+
...this.io === undefined ? {} : { io: this.io }
|
|
2444
|
+
});
|
|
2445
|
+
let result = commit.result;
|
|
2446
|
+
if (commit.delta.operations.length > 0) {
|
|
2447
|
+
try {
|
|
2448
|
+
await this.documents.synchronize(commit.delta);
|
|
2449
|
+
} catch (error) {
|
|
2450
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2451
|
+
result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
if (lease.signal?.aborted && !result.lateAbort)
|
|
2455
|
+
result = { ...result, lateAbort: true };
|
|
2456
|
+
return { fingerprint: planned.plan.fingerprint, result };
|
|
2457
|
+
}
|
|
2458
|
+
requireActiveLease(lease) {
|
|
2459
|
+
return this.activeLease?.id === lease.id ? this.activeLease : null;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// ../lsp-core/src/lsp/client.ts
|
|
2464
|
+
var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
|
|
2465
|
+
var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
2466
|
+
|
|
2467
|
+
class LspClient extends LspClientConnection {
|
|
2468
|
+
diagnosticPullErrors = [];
|
|
2469
|
+
documents;
|
|
2470
|
+
workspaceMutations;
|
|
2471
|
+
diagnosticsFreshnessTimeoutMs;
|
|
2472
|
+
constructor(root, server, options = {}) {
|
|
2473
|
+
super(root, server, options);
|
|
2474
|
+
this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
|
|
2475
|
+
this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
|
|
2476
|
+
versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
|
|
2477
|
+
});
|
|
2478
|
+
this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
|
|
2479
|
+
this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
|
|
2480
|
+
}
|
|
2481
|
+
getDiagnosticPullErrors() {
|
|
2482
|
+
return this.diagnosticPullErrors;
|
|
2483
|
+
}
|
|
2484
|
+
async openFile(filePath) {
|
|
2485
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2486
|
+
await this.documents.openFile(absPath);
|
|
2487
|
+
}
|
|
2488
|
+
getOpenDocumentVersion(filePath) {
|
|
2489
|
+
return this.documents.getVersion(this.resolveWorkspacePath(filePath));
|
|
2490
|
+
}
|
|
2491
|
+
getStoredDiagnostics(uri) {
|
|
2492
|
+
return [...this.documents.getStoredDiagnostics(uri)];
|
|
2493
|
+
}
|
|
2494
|
+
setWorkspaceEditIo(io) {
|
|
2495
|
+
this.workspaceMutations.setIo(io);
|
|
2496
|
+
}
|
|
2497
|
+
handlePublishDiagnostics(params) {
|
|
2498
|
+
super.handlePublishDiagnostics(params);
|
|
2499
|
+
this.documents.recordPublishedDiagnostics(params);
|
|
2500
|
+
}
|
|
2501
|
+
async definition(filePath, line, character, signal) {
|
|
2502
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1045
2503
|
await this.openFile(absPath);
|
|
2504
|
+
const options = signal === undefined ? {} : { signal };
|
|
1046
2505
|
return this.sendRequest("textDocument/definition", {
|
|
1047
|
-
textDocument: { uri:
|
|
2506
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1048
2507
|
position: { line: line - 1, character }
|
|
1049
|
-
});
|
|
2508
|
+
}, options);
|
|
1050
2509
|
}
|
|
1051
|
-
async references(filePath, line, character, includeDeclaration = true) {
|
|
1052
|
-
const absPath =
|
|
2510
|
+
async references(filePath, line, character, includeDeclaration = true, signal) {
|
|
2511
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1053
2512
|
await this.openFile(absPath);
|
|
2513
|
+
const options = signal === undefined ? {} : { signal };
|
|
1054
2514
|
return this.sendRequest("textDocument/references", {
|
|
1055
|
-
textDocument: { uri:
|
|
2515
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1056
2516
|
position: { line: line - 1, character },
|
|
1057
2517
|
context: { includeDeclaration }
|
|
1058
|
-
});
|
|
2518
|
+
}, options);
|
|
1059
2519
|
}
|
|
1060
|
-
async documentSymbols(filePath) {
|
|
1061
|
-
const absPath =
|
|
2520
|
+
async documentSymbols(filePath, signal) {
|
|
2521
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1062
2522
|
await this.openFile(absPath);
|
|
2523
|
+
const options = signal === undefined ? {} : { signal };
|
|
1063
2524
|
return this.sendRequest("textDocument/documentSymbol", {
|
|
1064
|
-
textDocument: { uri:
|
|
1065
|
-
});
|
|
2525
|
+
textDocument: { uri: pathToFileURL3(absPath).href }
|
|
2526
|
+
}, options);
|
|
1066
2527
|
}
|
|
1067
|
-
async workspaceSymbols(query) {
|
|
1068
|
-
|
|
2528
|
+
async workspaceSymbols(query, signal) {
|
|
2529
|
+
const options = signal === undefined ? {} : { signal };
|
|
2530
|
+
return this.sendRequest("workspace/symbol", { query }, options);
|
|
1069
2531
|
}
|
|
1070
2532
|
isUnsupportedDiagnosticPullError(error) {
|
|
1071
2533
|
if (!(error instanceof Error))
|
|
@@ -1075,42 +2537,173 @@ class LspClient extends LspClientConnection {
|
|
|
1075
2537
|
return true;
|
|
1076
2538
|
return /unsupported|not supported|method not found|unknown request/i.test(error.message);
|
|
1077
2539
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
1085
|
-
textDocument: { uri }
|
|
1086
|
-
});
|
|
1087
|
-
if (result.items) {
|
|
1088
|
-
return { items: result.items };
|
|
2540
|
+
freshnessTimeout(absPath) {
|
|
2541
|
+
return {
|
|
2542
|
+
items: [],
|
|
2543
|
+
transientError: {
|
|
2544
|
+
kind: "freshness_timeout",
|
|
2545
|
+
message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
|
|
1089
2546
|
}
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
parseDiagnosticPullReport(value) {
|
|
2550
|
+
if (value.kind === "unchanged") {
|
|
2551
|
+
return {
|
|
2552
|
+
type: "unchanged",
|
|
2553
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
return {
|
|
2557
|
+
type: "full",
|
|
2558
|
+
diagnostics: value.items ?? [],
|
|
2559
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
async diagnostics(filePath, signal) {
|
|
2563
|
+
signal?.throwIfAborted();
|
|
2564
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2565
|
+
const uri = pathToFileURL3(absPath).href;
|
|
2566
|
+
await this.openFile(absPath);
|
|
2567
|
+
const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
|
|
2568
|
+
for (;; ) {
|
|
2569
|
+
signal?.throwIfAborted();
|
|
2570
|
+
const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
|
|
2571
|
+
if (!snapshot)
|
|
2572
|
+
return this.freshnessTimeout(absPath);
|
|
2573
|
+
const push = this.documents.resolvePushDiagnostics(snapshot);
|
|
2574
|
+
if (push.status === "ready")
|
|
2575
|
+
return { items: [...push.diagnostics] };
|
|
2576
|
+
let pushFallbackOnly = !this.isDiagnosticPullSupported();
|
|
2577
|
+
if (!pushFallbackOnly) {
|
|
2578
|
+
const cached = this.documents.getPullCache(snapshot);
|
|
2579
|
+
try {
|
|
2580
|
+
const remainingMs2 = deadlineAt - Date.now();
|
|
2581
|
+
if (remainingMs2 <= 0)
|
|
2582
|
+
return this.freshnessTimeout(absPath);
|
|
2583
|
+
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
2584
|
+
textDocument: { uri },
|
|
2585
|
+
...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
|
|
2586
|
+
}, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
|
|
2587
|
+
if (!this.documents.isCurrentSnapshot(snapshot))
|
|
2588
|
+
continue;
|
|
2589
|
+
const report = this.parseDiagnosticPullReport(result);
|
|
2590
|
+
if (report.type === "full") {
|
|
2591
|
+
this.documents.recordPullDiagnostics(snapshot, {
|
|
2592
|
+
kind: "full",
|
|
2593
|
+
diagnostics: report.diagnostics,
|
|
2594
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
2595
|
+
});
|
|
2596
|
+
return { items: [...report.diagnostics] };
|
|
2597
|
+
}
|
|
2598
|
+
if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
|
|
2599
|
+
return { items: [...cached.diagnostics] };
|
|
2600
|
+
}
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
if (this.isUnsupportedDiagnosticPullError(error)) {
|
|
2603
|
+
this.setDiagnosticPullSupported(false);
|
|
2604
|
+
pushFallbackOnly = true;
|
|
2605
|
+
} else if (error instanceof LspRequestTimeoutError) {
|
|
2606
|
+
pushFallbackOnly = true;
|
|
2607
|
+
} else {
|
|
2608
|
+
this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
2609
|
+
throw error;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
1093
2612
|
}
|
|
2613
|
+
if (!pushFallbackOnly)
|
|
2614
|
+
continue;
|
|
2615
|
+
const remainingMs = deadlineAt - Date.now();
|
|
2616
|
+
if (remainingMs <= 0)
|
|
2617
|
+
return this.freshnessTimeout(absPath);
|
|
2618
|
+
const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
|
|
2619
|
+
await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
|
|
1094
2620
|
}
|
|
1095
|
-
return { items: this.getStoredDiagnostics(uri) };
|
|
1096
2621
|
}
|
|
1097
|
-
async prepareRename(filePath, line, character) {
|
|
1098
|
-
const absPath =
|
|
2622
|
+
async prepareRename(filePath, line, character, signal) {
|
|
2623
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1099
2624
|
await this.openFile(absPath);
|
|
2625
|
+
const options = signal === undefined ? {} : { signal };
|
|
1100
2626
|
return this.sendRequest("textDocument/prepareRename", {
|
|
1101
|
-
textDocument: { uri:
|
|
2627
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1102
2628
|
position: { line: line - 1, character }
|
|
1103
|
-
});
|
|
2629
|
+
}, options);
|
|
1104
2630
|
}
|
|
1105
|
-
async rename(filePath, line, character, newName) {
|
|
1106
|
-
const absPath =
|
|
2631
|
+
async rename(filePath, line, character, newName, signal) {
|
|
2632
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1107
2633
|
await this.openFile(absPath);
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
2634
|
+
const acquired = this.workspaceMutations.acquire(signal);
|
|
2635
|
+
if (!acquired.success)
|
|
2636
|
+
return { edit: null, apply: acquired.result };
|
|
2637
|
+
const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
|
|
2638
|
+
try {
|
|
2639
|
+
const renameParams = {
|
|
2640
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
2641
|
+
position: { line: line - 1, character },
|
|
2642
|
+
newName
|
|
2643
|
+
};
|
|
2644
|
+
const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
|
|
2645
|
+
signal: preCommitSignal.signal
|
|
2646
|
+
});
|
|
2647
|
+
return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
|
|
2648
|
+
} finally {
|
|
2649
|
+
preCommitSignal?.dispose();
|
|
2650
|
+
this.workspaceMutations.release(acquired.lease);
|
|
2651
|
+
}
|
|
1113
2652
|
}
|
|
2653
|
+
resolveWorkspacePath(filePath) {
|
|
2654
|
+
return resolve5(this.root, filePath);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
function waitForDiagnosticsActivity(wait, signal) {
|
|
2658
|
+
if (!signal)
|
|
2659
|
+
return wait;
|
|
2660
|
+
if (signal.aborted)
|
|
2661
|
+
return Promise.reject(abortError2(signal));
|
|
2662
|
+
return new Promise((resolve6, reject) => {
|
|
2663
|
+
const onAbort = () => {
|
|
2664
|
+
signal.removeEventListener("abort", onAbort);
|
|
2665
|
+
reject(abortError2(signal));
|
|
2666
|
+
};
|
|
2667
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2668
|
+
wait.then(() => {
|
|
2669
|
+
signal.removeEventListener("abort", onAbort);
|
|
2670
|
+
resolve6();
|
|
2671
|
+
}, (error) => {
|
|
2672
|
+
signal.removeEventListener("abort", onAbort);
|
|
2673
|
+
reject(error);
|
|
2674
|
+
});
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
function createPreCommitAbortSignal(source, isBeforeCommit) {
|
|
2678
|
+
if (!source)
|
|
2679
|
+
return;
|
|
2680
|
+
const controller = new AbortController;
|
|
2681
|
+
const onAbort = () => {
|
|
2682
|
+
if (isBeforeCommit() && !controller.signal.aborted)
|
|
2683
|
+
controller.abort(preCommitAbortReason(source));
|
|
2684
|
+
};
|
|
2685
|
+
if (source.aborted)
|
|
2686
|
+
onAbort();
|
|
2687
|
+
else
|
|
2688
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
2689
|
+
return {
|
|
2690
|
+
signal: controller.signal,
|
|
2691
|
+
dispose: () => source.removeEventListener("abort", onAbort)
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
function preCommitAbortReason(source) {
|
|
2695
|
+
const reason = source.reason;
|
|
2696
|
+
if (reason instanceof Error && reason.name !== "AbortError")
|
|
2697
|
+
return reason;
|
|
2698
|
+
return new Error("LSP request cancelled before workspace edit commit");
|
|
2699
|
+
}
|
|
2700
|
+
function abortError2(signal) {
|
|
2701
|
+
const reason = signal.reason;
|
|
2702
|
+
if (reason instanceof Error)
|
|
2703
|
+
return reason;
|
|
2704
|
+
const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
|
|
2705
|
+
error.name = "AbortError";
|
|
2706
|
+
return error;
|
|
1114
2707
|
}
|
|
1115
2708
|
|
|
1116
2709
|
// ../lsp-core/src/lsp/process-signal-cleanup.ts
|
|
@@ -1142,7 +2735,7 @@ async function stopClientBestEffort(client) {
|
|
|
1142
2735
|
function awaitWithSignal(promise, signal) {
|
|
1143
2736
|
if (!signal)
|
|
1144
2737
|
return promise;
|
|
1145
|
-
return new Promise((
|
|
2738
|
+
return new Promise((resolve6, reject) => {
|
|
1146
2739
|
let settled = false;
|
|
1147
2740
|
const onAbort = () => {
|
|
1148
2741
|
if (settled)
|
|
@@ -1160,7 +2753,7 @@ function awaitWithSignal(promise, signal) {
|
|
|
1160
2753
|
return;
|
|
1161
2754
|
settled = true;
|
|
1162
2755
|
signal.removeEventListener("abort", onAbort);
|
|
1163
|
-
|
|
2756
|
+
resolve6(value);
|
|
1164
2757
|
}, (err) => {
|
|
1165
2758
|
if (settled)
|
|
1166
2759
|
return;
|