oh-my-opencode 4.17.1 → 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/SKILL.md +2 -0
- 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/get-local-version/types.d.ts +1 -1
- package/dist/cli/index.js +498 -165
- package/dist/cli-node/index.js +498 -165
- package/dist/index.js +425 -392
- package/dist/skills/frontend/SKILL.md +1 -1
- package/dist/skills/frontend/references/design/README.md +9 -0
- package/dist/skills/frontend/references/design/design-system-architecture.md +4 -2
- package/dist/skills/frontend/references/design/layout-skill.md +107 -0
- package/dist/skills/programming/SKILL.md +12 -2
- package/package.json +17 -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 +8 -6
- 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/directive.md +37 -10
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +37 -10
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +37 -10
- 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 +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +10 -9
- 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/frontend/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +9 -0
- package/packages/omo-codex/plugin/skills/frontend/references/design/design-system-architecture.md +4 -2
- package/packages/omo-codex/plugin/skills/frontend/references/design/layout-skill.md +107 -0
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +12 -2
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +37 -10
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +10 -9
- 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 +351 -74
- package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/README.md +9 -0
- package/packages/shared-skills/skills/frontend/references/design/design-system-architecture.md +4 -2
- package/packages/shared-skills/skills/frontend/references/design/layout-skill.md +107 -0
- package/packages/shared-skills/skills/programming/SKILL.md +12 -2
|
@@ -12,9 +12,6 @@ function errorResponse(id, code, message, data) {
|
|
|
12
12
|
function jsonRpcId(value) {
|
|
13
13
|
return typeof value === "string" || typeof value === "number" || value === null ? value : null;
|
|
14
14
|
}
|
|
15
|
-
function messageFromError(error) {
|
|
16
|
-
return error instanceof Error ? error.message : String(error);
|
|
17
|
-
}
|
|
18
15
|
// ../mcp-stdio-core/src/transport.ts
|
|
19
16
|
var HEADER_SEPARATOR = Buffer.from(`\r
|
|
20
17
|
\r
|
|
@@ -201,37 +198,197 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
|
|
|
201
198
|
closed: () => isClosed
|
|
202
199
|
};
|
|
203
200
|
}
|
|
204
|
-
// ../lsp-core/src/tools/diagnostics.ts
|
|
205
|
-
import { resolve as resolve4 } from "node:path";
|
|
206
|
-
|
|
207
201
|
// ../lsp-core/src/lsp/client-wrapper.ts
|
|
208
|
-
import { existsSync as
|
|
209
|
-
import { dirname as
|
|
202
|
+
import { existsSync as existsSync9, statSync as statSync3 } from "node:fs";
|
|
203
|
+
import { dirname as dirname6, join as join4, resolve as resolve7 } from "node:path";
|
|
210
204
|
|
|
211
205
|
// ../lsp-core/src/request-context.ts
|
|
212
206
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
207
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
208
|
+
import { homedir } from "node:os";
|
|
209
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
210
|
+
|
|
211
|
+
class LspRequestContextParseError extends Error {
|
|
212
|
+
code;
|
|
213
|
+
name = "LspRequestContextParseError";
|
|
214
|
+
constructor(code, message) {
|
|
215
|
+
super(message);
|
|
216
|
+
this.code = code;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
class LspRequestContextUnavailableError extends Error {
|
|
221
|
+
name = "LspRequestContextUnavailableError";
|
|
222
|
+
constructor() {
|
|
223
|
+
super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
213
226
|
var storage = new AsyncLocalStorage;
|
|
227
|
+
var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
228
|
+
var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
|
|
214
229
|
function runWithRequestContext(context, fn) {
|
|
215
230
|
return storage.run(context, fn);
|
|
216
231
|
}
|
|
232
|
+
function lspRequestContext() {
|
|
233
|
+
const context = storage.getStore();
|
|
234
|
+
if (!context)
|
|
235
|
+
throw new LspRequestContextUnavailableError;
|
|
236
|
+
return context;
|
|
237
|
+
}
|
|
217
238
|
function contextCwd() {
|
|
218
|
-
return
|
|
239
|
+
return lspRequestContext().cwd;
|
|
219
240
|
}
|
|
220
241
|
function contextEnv(key) {
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
return
|
|
224
|
-
|
|
242
|
+
const context = lspRequestContext();
|
|
243
|
+
if (key === "LSP_TOOLS_MCP_PROJECT_CONFIG")
|
|
244
|
+
return context.projectConfigPaths.join(delimiter);
|
|
245
|
+
if (key === "LSP_TOOLS_MCP_USER_CONFIG")
|
|
246
|
+
return context.userConfigPath;
|
|
247
|
+
if (key === "LSP_TOOLS_MCP_INSTALL_DECISIONS")
|
|
248
|
+
return context.installDecisionsPath;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
function createStandaloneMcpRequestContext(input = {}) {
|
|
252
|
+
const env = input.env ?? process.env;
|
|
253
|
+
const cwd = canonicalCwd(input.cwd ?? process.cwd());
|
|
254
|
+
const home = input.homeDir ?? homedir();
|
|
255
|
+
const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
|
|
256
|
+
const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
|
|
257
|
+
const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
|
|
258
|
+
return parseLspRequestContext({
|
|
259
|
+
cwd,
|
|
260
|
+
projectConfigPaths,
|
|
261
|
+
userConfigPath,
|
|
262
|
+
installDecisionsPath,
|
|
263
|
+
capabilities: { installDecisionTool: true }
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function parseLspRequestContext(value) {
|
|
267
|
+
if (!isRecord(value)) {
|
|
268
|
+
throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
|
|
269
|
+
}
|
|
270
|
+
rejectUnknownFields(value, CONTEXT_FIELDS, "context");
|
|
271
|
+
const cwd = stringField(value, "cwd");
|
|
272
|
+
const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
|
|
273
|
+
const userConfigPath = stringField(value, "userConfigPath");
|
|
274
|
+
const installDecisionsPath = stringField(value, "installDecisionsPath");
|
|
275
|
+
const capabilities = capabilitiesField(value["capabilities"]);
|
|
276
|
+
const canonical = canonicalCwd(cwd);
|
|
277
|
+
for (const path of projectConfigPaths) {
|
|
278
|
+
requireAbsolutePath(path, "projectConfigPaths");
|
|
279
|
+
const projectPath = canonicalizeExistingOrNearestAncestor(path);
|
|
280
|
+
if (!isPathInside(canonical, projectPath)) {
|
|
281
|
+
throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
requireAbsolutePath(userConfigPath, "userConfigPath");
|
|
285
|
+
requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
|
|
286
|
+
return {
|
|
287
|
+
cwd: canonical,
|
|
288
|
+
projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
|
|
289
|
+
userConfigPath,
|
|
290
|
+
installDecisionsPath,
|
|
291
|
+
capabilities
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function translateProjectConfigEnv(value, cwd) {
|
|
295
|
+
if (value === undefined || value.length === 0)
|
|
296
|
+
return [join(cwd, ".codex", "lsp-client.json")];
|
|
297
|
+
return value.split(delimiter).filter((entry) => entry.length > 0).map((entry) => isAbsolute(entry) ? entry : join(cwd, entry));
|
|
298
|
+
}
|
|
299
|
+
function translateHomeConfigEnv(value, home, fallback) {
|
|
300
|
+
if (value === undefined || value.length === 0)
|
|
301
|
+
return join(home, fallback);
|
|
302
|
+
return isAbsolute(value) ? value : join(home, value);
|
|
303
|
+
}
|
|
304
|
+
function canonicalCwd(cwd) {
|
|
305
|
+
const resolved = resolve(cwd);
|
|
306
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
307
|
+
throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
308
|
+
}
|
|
309
|
+
return realpathSync(resolved);
|
|
310
|
+
}
|
|
311
|
+
function canonicalizeExistingOrNearestAncestor(path) {
|
|
312
|
+
let current = resolve(path);
|
|
313
|
+
const suffix = [];
|
|
314
|
+
while (true) {
|
|
315
|
+
try {
|
|
316
|
+
const existing = realpathSync(current);
|
|
317
|
+
return suffix.length === 0 ? existing : join(existing, ...suffix);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (!isMissingPathError(error))
|
|
320
|
+
throw error;
|
|
321
|
+
const parent = dirname(current);
|
|
322
|
+
if (parent === current)
|
|
323
|
+
throw error;
|
|
324
|
+
suffix.unshift(basename(current));
|
|
325
|
+
current = parent;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function capabilitiesField(value) {
|
|
330
|
+
if (!isRecord(value)) {
|
|
331
|
+
throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
332
|
+
}
|
|
333
|
+
rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
|
|
334
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
335
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
336
|
+
throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
|
|
337
|
+
}
|
|
338
|
+
return { installDecisionTool };
|
|
339
|
+
}
|
|
340
|
+
function stringField(value, field) {
|
|
341
|
+
const fieldValue = value[field];
|
|
342
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
343
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
344
|
+
}
|
|
345
|
+
return fieldValue;
|
|
346
|
+
}
|
|
347
|
+
function stringArrayField(value, field) {
|
|
348
|
+
const fieldValue = value[field];
|
|
349
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
350
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
|
|
351
|
+
}
|
|
352
|
+
return fieldValue;
|
|
353
|
+
}
|
|
354
|
+
function requireAbsolutePath(path, field) {
|
|
355
|
+
if (!isAbsolute(path)) {
|
|
356
|
+
throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function isPathInside(parent, child) {
|
|
360
|
+
const childPath = resolve(child);
|
|
361
|
+
const relativePath = relative(parent, childPath);
|
|
362
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
363
|
+
}
|
|
364
|
+
function isMissingPathError(error) {
|
|
365
|
+
const code = errorCode(error);
|
|
366
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
367
|
+
}
|
|
368
|
+
function rejectUnknownFields(value, allowed, scope) {
|
|
369
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
370
|
+
if (unknown.length > 0) {
|
|
371
|
+
throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function isRecord(value) {
|
|
375
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
376
|
+
}
|
|
377
|
+
function errorCode(error) {
|
|
378
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
379
|
+
return;
|
|
380
|
+
const code = Reflect.get(error, "code");
|
|
381
|
+
return typeof code === "string" ? code : undefined;
|
|
225
382
|
}
|
|
226
383
|
|
|
227
384
|
// ../lsp-core/src/lsp/effective-extension.ts
|
|
228
|
-
import { basename, extname } from "node:path";
|
|
385
|
+
import { basename as basename2, extname } from "node:path";
|
|
229
386
|
var BASENAME_EXTENSIONS = {
|
|
230
387
|
Dockerfile: ".dockerfile",
|
|
231
388
|
Containerfile: ".dockerfile"
|
|
232
389
|
};
|
|
233
390
|
function effectiveExtension(filePath) {
|
|
234
|
-
return BASENAME_EXTENSIONS[
|
|
391
|
+
return BASENAME_EXTENSIONS[basename2(filePath)] ?? extname(filePath);
|
|
235
392
|
}
|
|
236
393
|
|
|
237
394
|
// ../lsp-core/src/lsp/errors.ts
|
|
@@ -281,7 +438,12 @@ class LspInvalidPathError extends Error {
|
|
|
281
438
|
}
|
|
282
439
|
|
|
283
440
|
class LspServerLookupError extends Error {
|
|
441
|
+
lookup;
|
|
284
442
|
name = "LspServerLookupError";
|
|
443
|
+
constructor(message, lookup) {
|
|
444
|
+
super(message);
|
|
445
|
+
this.lookup = lookup;
|
|
446
|
+
}
|
|
285
447
|
}
|
|
286
448
|
|
|
287
449
|
class LspServerInitializingError extends Error {
|
|
@@ -301,17 +463,18 @@ function isLspDeadConnectionError(err) {
|
|
|
301
463
|
}
|
|
302
464
|
|
|
303
465
|
// ../lsp-core/src/lsp/cleanup-errors.ts
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
466
|
+
function writeCleanupError(message) {
|
|
467
|
+
process.stderr.write(`${message}
|
|
468
|
+
`);
|
|
469
|
+
}
|
|
470
|
+
function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
|
|
307
471
|
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
-
|
|
472
|
+
logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
|
|
309
473
|
}
|
|
310
474
|
|
|
311
475
|
// ../lsp-core/src/lsp/client.ts
|
|
312
|
-
import {
|
|
313
|
-
import {
|
|
314
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
476
|
+
import { resolve as resolve6 } from "node:path";
|
|
477
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
315
478
|
|
|
316
479
|
// ../lsp-core/src/lsp/connection.ts
|
|
317
480
|
import { pathToFileURL } from "node:url";
|
|
@@ -375,28 +538,80 @@ class JsonRpcConnection {
|
|
|
375
538
|
onError(handler) {
|
|
376
539
|
this.errorHandlers.push(handler);
|
|
377
540
|
}
|
|
378
|
-
async sendRequest(method, params) {
|
|
541
|
+
async sendRequest(method, params, options = {}) {
|
|
379
542
|
if (this.disposed)
|
|
380
543
|
throw new Error("JSON-RPC connection is disposed");
|
|
381
544
|
const id = this.nextRequestId;
|
|
382
545
|
this.nextRequestId += 1;
|
|
546
|
+
const key = String(id);
|
|
383
547
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
384
|
-
|
|
385
|
-
|
|
548
|
+
let requestWritten = false;
|
|
549
|
+
let cancelAfterWrite = false;
|
|
550
|
+
let settled = false;
|
|
551
|
+
const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
552
|
+
const responsePromise = new Promise((resolve2, reject) => {
|
|
553
|
+
const cleanup = () => {
|
|
554
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
555
|
+
};
|
|
556
|
+
const settleCancel = () => {
|
|
557
|
+
if (settled)
|
|
558
|
+
return;
|
|
559
|
+
settled = true;
|
|
560
|
+
this.pendingRequests.delete(key);
|
|
561
|
+
cleanup();
|
|
562
|
+
const rejectCancelled = () => reject(abortError(options.signal));
|
|
563
|
+
if (!requestWritten) {
|
|
564
|
+
cancelAfterWrite = true;
|
|
565
|
+
rejectCancelled();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
writeCancel().then(rejectCancelled, (error) => {
|
|
569
|
+
this.emitError(toError(error));
|
|
570
|
+
rejectCancelled();
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
const onAbort = () => settleCancel();
|
|
574
|
+
this.pendingRequests.set(key, {
|
|
386
575
|
resolve(result) {
|
|
387
|
-
|
|
576
|
+
settled = true;
|
|
577
|
+
cleanup();
|
|
578
|
+
resolve2(result);
|
|
579
|
+
},
|
|
580
|
+
reject(error) {
|
|
581
|
+
settled = true;
|
|
582
|
+
cleanup();
|
|
583
|
+
reject(error);
|
|
388
584
|
},
|
|
389
|
-
|
|
585
|
+
cleanup
|
|
390
586
|
});
|
|
587
|
+
if (options.signal?.aborted) {
|
|
588
|
+
settleCancel();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
391
592
|
});
|
|
593
|
+
if (settled)
|
|
594
|
+
return responsePromise;
|
|
392
595
|
try {
|
|
393
596
|
await this.writeMessage(message);
|
|
597
|
+
requestWritten = true;
|
|
598
|
+
if (cancelAfterWrite)
|
|
599
|
+
await writeCancel();
|
|
394
600
|
} catch (error) {
|
|
395
|
-
|
|
601
|
+
if (settled)
|
|
602
|
+
return responsePromise;
|
|
603
|
+
const pending = this.pendingRequests.get(key);
|
|
604
|
+
if (pending) {
|
|
605
|
+
pending.cleanup();
|
|
606
|
+
this.pendingRequests.delete(key);
|
|
607
|
+
}
|
|
396
608
|
throw error;
|
|
397
609
|
}
|
|
398
610
|
return responsePromise;
|
|
399
611
|
}
|
|
612
|
+
pendingRequestCount() {
|
|
613
|
+
return this.pendingRequests.size;
|
|
614
|
+
}
|
|
400
615
|
async sendNotification(method, params) {
|
|
401
616
|
if (this.disposed)
|
|
402
617
|
return;
|
|
@@ -413,6 +628,7 @@ class JsonRpcConnection {
|
|
|
413
628
|
this.reader.off("error", this.handleStreamError);
|
|
414
629
|
this.writer.off("error", this.handleStreamError);
|
|
415
630
|
for (const pending of this.pendingRequests.values()) {
|
|
631
|
+
pending.cleanup();
|
|
416
632
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
417
633
|
}
|
|
418
634
|
this.pendingRequests.clear();
|
|
@@ -488,6 +704,7 @@ class JsonRpcConnection {
|
|
|
488
704
|
if (!pending)
|
|
489
705
|
return;
|
|
490
706
|
this.pendingRequests.delete(String(id));
|
|
707
|
+
pending.cleanup();
|
|
491
708
|
if ("error" in message) {
|
|
492
709
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
493
710
|
return;
|
|
@@ -501,7 +718,11 @@ class JsonRpcConnection {
|
|
|
501
718
|
try {
|
|
502
719
|
handler(params);
|
|
503
720
|
} catch (error) {
|
|
504
|
-
|
|
721
|
+
if (error instanceof Error) {
|
|
722
|
+
this.emitError(error);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
this.emitError(new Error(String(error)));
|
|
505
726
|
}
|
|
506
727
|
}
|
|
507
728
|
handleRequest(message) {
|
|
@@ -526,13 +747,13 @@ class JsonRpcConnection {
|
|
|
526
747
|
const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
527
748
|
\r
|
|
528
749
|
${body}`;
|
|
529
|
-
return new Promise((
|
|
750
|
+
return new Promise((resolve2, reject) => {
|
|
530
751
|
this.writer.write(payload, (error) => {
|
|
531
752
|
if (error) {
|
|
532
753
|
reject(error);
|
|
533
754
|
return;
|
|
534
755
|
}
|
|
535
|
-
|
|
756
|
+
resolve2();
|
|
536
757
|
});
|
|
537
758
|
});
|
|
538
759
|
}
|
|
@@ -542,6 +763,14 @@ ${body}`;
|
|
|
542
763
|
}
|
|
543
764
|
}
|
|
544
765
|
}
|
|
766
|
+
function abortError(signal) {
|
|
767
|
+
const reason = signal?.reason;
|
|
768
|
+
if (reason instanceof Error)
|
|
769
|
+
return reason;
|
|
770
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
771
|
+
error.name = "AbortError";
|
|
772
|
+
return error;
|
|
773
|
+
}
|
|
545
774
|
function parseContentLength2(headers) {
|
|
546
775
|
for (const line of headers.split(`\r
|
|
547
776
|
`)) {
|
|
@@ -581,8 +810,8 @@ function toError(error) {
|
|
|
581
810
|
|
|
582
811
|
// ../lsp-core/src/lsp/process.ts
|
|
583
812
|
import { spawn, spawnSync } from "node:child_process";
|
|
584
|
-
import { existsSync, statSync } from "node:fs";
|
|
585
|
-
import { delimiter, join } from "node:path";
|
|
813
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
|
|
814
|
+
import { delimiter as delimiter2, join as join2 } from "node:path";
|
|
586
815
|
function isMissingProcessError(error) {
|
|
587
816
|
if (!(error instanceof Error) || !("code" in error))
|
|
588
817
|
return false;
|
|
@@ -595,10 +824,10 @@ function reportKillError(context, error) {
|
|
|
595
824
|
}
|
|
596
825
|
function validateCwd(cwd) {
|
|
597
826
|
try {
|
|
598
|
-
if (!
|
|
827
|
+
if (!existsSync2(cwd)) {
|
|
599
828
|
return { valid: false, error: `Working directory does not exist: ${cwd}` };
|
|
600
829
|
}
|
|
601
|
-
const stats =
|
|
830
|
+
const stats = statSync2(cwd);
|
|
602
831
|
if (!stats.isDirectory()) {
|
|
603
832
|
return { valid: false, error: `Path is not a directory: ${cwd}` };
|
|
604
833
|
}
|
|
@@ -611,9 +840,9 @@ function validateCwd(cwd) {
|
|
|
611
840
|
}
|
|
612
841
|
}
|
|
613
842
|
function wrap(proc) {
|
|
614
|
-
const exitedPromise = new Promise((
|
|
615
|
-
proc.once("close", (code) =>
|
|
616
|
-
proc.once("error", () =>
|
|
843
|
+
const exitedPromise = new Promise((resolve2) => {
|
|
844
|
+
proc.once("close", (code) => resolve2(code ?? 0));
|
|
845
|
+
proc.once("error", () => resolve2(1));
|
|
617
846
|
});
|
|
618
847
|
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
619
848
|
throw new LspProcessSpawnError("Spawned process is missing one of stdin/stdout/stderr pipes");
|
|
@@ -667,7 +896,7 @@ function isWindowsShellShim(command) {
|
|
|
667
896
|
return lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat");
|
|
668
897
|
}
|
|
669
898
|
function splitPath(pathValue, platform) {
|
|
670
|
-
const separator = platform === "win32" ? ";" :
|
|
899
|
+
const separator = platform === "win32" ? ";" : delimiter2;
|
|
671
900
|
return pathValue.split(separator).filter(Boolean);
|
|
672
901
|
}
|
|
673
902
|
function getWindowsPathExtensions(env) {
|
|
@@ -682,8 +911,8 @@ function resolveWindowsCommand(command, env) {
|
|
|
682
911
|
const extensions = getWindowsPathExtensions(env);
|
|
683
912
|
for (const baseDirectory of baseDirectories) {
|
|
684
913
|
for (const extension of extensions) {
|
|
685
|
-
const candidate = baseDirectory ?
|
|
686
|
-
if (
|
|
914
|
+
const candidate = baseDirectory ? join2(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
|
|
915
|
+
if (existsSync2(candidate))
|
|
687
916
|
return candidate;
|
|
688
917
|
}
|
|
689
918
|
}
|
|
@@ -728,16 +957,16 @@ function spawnProcess(command, options) {
|
|
|
728
957
|
return wrap(proc);
|
|
729
958
|
}
|
|
730
959
|
|
|
731
|
-
// ../lsp-core/src/lsp/transport.ts
|
|
732
|
-
function
|
|
960
|
+
// ../lsp-core/src/lsp/transport-protocol.ts
|
|
961
|
+
function isRecord2(value) {
|
|
733
962
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
734
963
|
}
|
|
735
964
|
function parseConfigurationItems(params) {
|
|
736
|
-
if (!
|
|
965
|
+
if (!isRecord2(params) || !Array.isArray(params["items"]))
|
|
737
966
|
return [];
|
|
738
967
|
const items = [];
|
|
739
968
|
for (const item of params["items"]) {
|
|
740
|
-
if (!
|
|
969
|
+
if (!isRecord2(item))
|
|
741
970
|
continue;
|
|
742
971
|
const section = item["section"];
|
|
743
972
|
items.push(section === undefined || typeof section !== "string" ? {} : { section });
|
|
@@ -745,10 +974,35 @@ function parseConfigurationItems(params) {
|
|
|
745
974
|
return items;
|
|
746
975
|
}
|
|
747
976
|
function parseDiagnosticsParams(params) {
|
|
748
|
-
if (!
|
|
977
|
+
if (!isRecord2(params) || typeof params["uri"] !== "string")
|
|
749
978
|
return null;
|
|
750
979
|
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
751
|
-
|
|
980
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
981
|
+
return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
|
|
982
|
+
}
|
|
983
|
+
function createLspSpawnEnv(_root, input) {
|
|
984
|
+
return { ...input };
|
|
985
|
+
}
|
|
986
|
+
function isDiagnostic(value) {
|
|
987
|
+
return isRecord2(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
988
|
+
}
|
|
989
|
+
function isRange(value) {
|
|
990
|
+
return isRecord2(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
991
|
+
}
|
|
992
|
+
function isPosition(value) {
|
|
993
|
+
return isRecord2(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ../lsp-core/src/lsp/transport.ts
|
|
997
|
+
class LspClientNotStartedError extends Error {
|
|
998
|
+
serverId;
|
|
999
|
+
root;
|
|
1000
|
+
name = "LspClientNotStartedError";
|
|
1001
|
+
constructor(serverId, root) {
|
|
1002
|
+
super("LSP client not started");
|
|
1003
|
+
this.serverId = serverId;
|
|
1004
|
+
this.root = root;
|
|
1005
|
+
}
|
|
752
1006
|
}
|
|
753
1007
|
|
|
754
1008
|
class LspClientTransport {
|
|
@@ -761,6 +1015,8 @@ class LspClientTransport {
|
|
|
761
1015
|
diagnosticsStore = new Map;
|
|
762
1016
|
requestTimeoutMs;
|
|
763
1017
|
initializeTimeoutMs;
|
|
1018
|
+
workspaceApplyEditHandler = null;
|
|
1019
|
+
diagnosticPullSupported = false;
|
|
764
1020
|
constructor(root, server2, timeouts = {}) {
|
|
765
1021
|
this.root = root;
|
|
766
1022
|
this.server = server2;
|
|
@@ -773,6 +1029,21 @@ class LspClientTransport {
|
|
|
773
1029
|
command() {
|
|
774
1030
|
return [...this.server.command];
|
|
775
1031
|
}
|
|
1032
|
+
setWorkspaceApplyEditHandler(handler) {
|
|
1033
|
+
this.workspaceApplyEditHandler = handler;
|
|
1034
|
+
}
|
|
1035
|
+
hasWorkspaceApplyEditHandler() {
|
|
1036
|
+
return this.workspaceApplyEditHandler !== null;
|
|
1037
|
+
}
|
|
1038
|
+
setDiagnosticPullSupported(supported) {
|
|
1039
|
+
this.diagnosticPullSupported = supported;
|
|
1040
|
+
}
|
|
1041
|
+
isDiagnosticPullSupported() {
|
|
1042
|
+
return this.diagnosticPullSupported;
|
|
1043
|
+
}
|
|
1044
|
+
handlePublishDiagnostics(params) {
|
|
1045
|
+
this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
|
|
1046
|
+
}
|
|
776
1047
|
async start() {
|
|
777
1048
|
const env = createLspSpawnEnv(this.root, {
|
|
778
1049
|
...process.env,
|
|
@@ -783,7 +1054,6 @@ class LspClientTransport {
|
|
|
783
1054
|
env
|
|
784
1055
|
});
|
|
785
1056
|
this.startStderrReading();
|
|
786
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
787
1057
|
if (this.proc.exitCode !== null) {
|
|
788
1058
|
const stderr = this.stderrBuffer.join(`
|
|
789
1059
|
`);
|
|
@@ -793,7 +1063,7 @@ class LspClientTransport {
|
|
|
793
1063
|
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
794
1064
|
const diagnosticsParams = parseDiagnosticsParams(params);
|
|
795
1065
|
if (diagnosticsParams?.uri) {
|
|
796
|
-
this.
|
|
1066
|
+
this.handlePublishDiagnostics(diagnosticsParams);
|
|
797
1067
|
}
|
|
798
1068
|
});
|
|
799
1069
|
this.connection.onRequest("workspace/configuration", (params) => {
|
|
@@ -806,6 +1076,9 @@ class LspClientTransport {
|
|
|
806
1076
|
});
|
|
807
1077
|
this.connection.onRequest("client/registerCapability", () => null);
|
|
808
1078
|
this.connection.onRequest("window/workDoneProgress/create", () => null);
|
|
1079
|
+
if (this.workspaceApplyEditHandler) {
|
|
1080
|
+
this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
|
|
1081
|
+
}
|
|
809
1082
|
this.connection.onClose(() => {
|
|
810
1083
|
this.processExited = true;
|
|
811
1084
|
});
|
|
@@ -834,30 +1107,25 @@ class LspClientTransport {
|
|
|
834
1107
|
}
|
|
835
1108
|
async sendRequest(method, ...args) {
|
|
836
1109
|
if (!this.connection)
|
|
837
|
-
throw new
|
|
1110
|
+
throw new LspClientNotStartedError(this.server.id, this.root);
|
|
838
1111
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
839
1112
|
const stderrTail = this.stderrBuffer.slice(-10).join(`
|
|
840
1113
|
`);
|
|
841
1114
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
|
|
842
1115
|
}
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
1116
|
+
const options = args[1];
|
|
1117
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
1118
|
+
const timeoutController = new AbortController;
|
|
1119
|
+
const timeoutHandle = setTimeout(() => {
|
|
1120
|
+
const stderrTail = this.stderrBuffer.slice(-5).join(`
|
|
848
1121
|
`);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1122
|
+
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
1123
|
+
}, timeoutMs);
|
|
1124
|
+
const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
|
|
852
1125
|
try {
|
|
853
|
-
const
|
|
854
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
855
|
-
if (timeoutHandle !== null)
|
|
856
|
-
clearTimeout(timeoutHandle);
|
|
1126
|
+
const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
|
|
857
1127
|
return result;
|
|
858
1128
|
} catch (error) {
|
|
859
|
-
if (timeoutHandle !== null)
|
|
860
|
-
clearTimeout(timeoutHandle);
|
|
861
1129
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
862
1130
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
|
|
863
1131
|
`) || undefined);
|
|
@@ -866,6 +1134,9 @@ class LspClientTransport {
|
|
|
866
1134
|
throw new LspConnectionClosedError(this.server.id, this.root, error.message);
|
|
867
1135
|
}
|
|
868
1136
|
throw error;
|
|
1137
|
+
} finally {
|
|
1138
|
+
clearTimeout(timeoutHandle);
|
|
1139
|
+
combinedSignal.dispose();
|
|
869
1140
|
}
|
|
870
1141
|
}
|
|
871
1142
|
async sendNotification(method, ...args) {
|
|
@@ -894,17 +1165,17 @@ class LspClientTransport {
|
|
|
894
1165
|
try {
|
|
895
1166
|
await this.sendRequest("shutdown");
|
|
896
1167
|
} catch (error) {
|
|
897
|
-
reportBestEffortCleanupError("shutdown request", error);
|
|
1168
|
+
reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
|
|
898
1169
|
}
|
|
899
1170
|
try {
|
|
900
1171
|
await this.sendNotification("exit");
|
|
901
1172
|
} catch (error) {
|
|
902
|
-
reportBestEffortCleanupError("exit notification", error);
|
|
1173
|
+
reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
|
|
903
1174
|
}
|
|
904
1175
|
try {
|
|
905
1176
|
this.connection.dispose();
|
|
906
1177
|
} catch (error) {
|
|
907
|
-
reportBestEffortCleanupError("connection dispose", error);
|
|
1178
|
+
reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
|
|
908
1179
|
}
|
|
909
1180
|
this.connection = null;
|
|
910
1181
|
}
|
|
@@ -915,8 +1186,8 @@ class LspClientTransport {
|
|
|
915
1186
|
try {
|
|
916
1187
|
proc.kill();
|
|
917
1188
|
let timeoutId;
|
|
918
|
-
const timeoutPromise = new Promise((
|
|
919
|
-
timeoutId = setTimeout(
|
|
1189
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
1190
|
+
timeoutId = setTimeout(resolve2, STOP_HARD_KILL_TIMEOUT_MS);
|
|
920
1191
|
});
|
|
921
1192
|
await Promise.race([
|
|
922
1193
|
proc.exited.then(() => {
|
|
@@ -932,14 +1203,14 @@ class LspClientTransport {
|
|
|
932
1203
|
proc.kill("SIGKILL");
|
|
933
1204
|
await Promise.race([
|
|
934
1205
|
proc.exited,
|
|
935
|
-
new Promise((
|
|
1206
|
+
new Promise((resolve2) => setTimeout(resolve2, STOP_SIGKILL_GRACE_MS))
|
|
936
1207
|
]);
|
|
937
1208
|
} catch (error) {
|
|
938
|
-
reportBestEffortCleanupError("hard process kill", error);
|
|
1209
|
+
reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
|
|
939
1210
|
}
|
|
940
1211
|
}
|
|
941
1212
|
} catch (error) {
|
|
942
|
-
reportBestEffortCleanupError("process stop", error);
|
|
1213
|
+
reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
|
|
943
1214
|
}
|
|
944
1215
|
}
|
|
945
1216
|
this.processExited = true;
|
|
@@ -949,26 +1220,45 @@ class LspClientTransport {
|
|
|
949
1220
|
return this.diagnosticsStore.get(uri) ?? [];
|
|
950
1221
|
}
|
|
951
1222
|
}
|
|
952
|
-
function
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1223
|
+
function combineAbortSignals(primary, secondary) {
|
|
1224
|
+
const controller = new AbortController;
|
|
1225
|
+
const abortFrom = (signal) => {
|
|
1226
|
+
if (!controller.signal.aborted)
|
|
1227
|
+
controller.abort(signal.reason);
|
|
1228
|
+
};
|
|
1229
|
+
const onPrimaryAbort = () => {
|
|
1230
|
+
if (primary)
|
|
1231
|
+
abortFrom(primary);
|
|
1232
|
+
};
|
|
1233
|
+
const onSecondaryAbort = () => abortFrom(secondary);
|
|
1234
|
+
if (primary?.aborted)
|
|
1235
|
+
abortFrom(primary);
|
|
1236
|
+
else
|
|
1237
|
+
primary?.addEventListener("abort", onPrimaryAbort, { once: true });
|
|
1238
|
+
if (secondary.aborted)
|
|
1239
|
+
abortFrom(secondary);
|
|
1240
|
+
else
|
|
1241
|
+
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
|
|
1242
|
+
return {
|
|
1243
|
+
signal: controller.signal,
|
|
1244
|
+
dispose: () => {
|
|
1245
|
+
primary?.removeEventListener("abort", onPrimaryAbort);
|
|
1246
|
+
secondary.removeEventListener("abort", onSecondaryAbort);
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
963
1249
|
}
|
|
964
1250
|
|
|
965
1251
|
// ../lsp-core/src/lsp/connection.ts
|
|
966
|
-
|
|
1252
|
+
function supportsDiagnosticPull(capabilities) {
|
|
1253
|
+
if (capabilities === undefined)
|
|
1254
|
+
return false;
|
|
1255
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
1256
|
+
}
|
|
967
1257
|
|
|
968
1258
|
class LspClientConnection extends LspClientTransport {
|
|
969
1259
|
async initialize() {
|
|
970
1260
|
const rootUri = pathToFileURL(this.root).href;
|
|
971
|
-
await this.sendRequest("initialize", {
|
|
1261
|
+
const result = await this.sendRequest("initialize", {
|
|
972
1262
|
processId: process.pid,
|
|
973
1263
|
rootUri,
|
|
974
1264
|
rootPath: this.root,
|
|
@@ -982,8 +1272,7 @@ class LspClientConnection extends LspClientTransport {
|
|
|
982
1272
|
publishDiagnostics: {},
|
|
983
1273
|
rename: {
|
|
984
1274
|
prepareSupport: true,
|
|
985
|
-
prepareSupportDefaultBehavior: 1
|
|
986
|
-
honorsChangeAnnotations: true
|
|
1275
|
+
prepareSupportDefaultBehavior: 1
|
|
987
1276
|
},
|
|
988
1277
|
codeAction: {
|
|
989
1278
|
codeActionLiteralSupport: {
|
|
@@ -1012,22 +1301,28 @@ class LspClientConnection extends LspClientTransport {
|
|
|
1012
1301
|
symbol: {},
|
|
1013
1302
|
workspaceFolders: true,
|
|
1014
1303
|
configuration: true,
|
|
1015
|
-
applyEdit: true,
|
|
1304
|
+
...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
|
|
1016
1305
|
workspaceEdit: {
|
|
1017
|
-
documentChanges: true
|
|
1306
|
+
documentChanges: true,
|
|
1307
|
+
resourceOperations: ["create", "rename", "delete"]
|
|
1018
1308
|
}
|
|
1019
1309
|
}
|
|
1020
1310
|
},
|
|
1021
1311
|
initializationOptions: this.server.initialization
|
|
1022
1312
|
}, { timeoutMs: this.initializeTimeoutMs });
|
|
1313
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
1023
1314
|
await this.sendNotification("initialized");
|
|
1024
1315
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
1025
1316
|
settings: { json: { validate: { enable: true } } }
|
|
1026
1317
|
});
|
|
1027
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
1028
1318
|
}
|
|
1029
1319
|
}
|
|
1030
1320
|
|
|
1321
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1322
|
+
import { readFileSync, realpathSync as realpathSync2 } from "node:fs";
|
|
1323
|
+
import { relative as relative2, resolve as resolve2 } from "node:path";
|
|
1324
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1325
|
+
|
|
1031
1326
|
// ../lsp-core/src/lsp/language-mappings.ts
|
|
1032
1327
|
var SYMBOL_KIND_MAP = {
|
|
1033
1328
|
1: "File",
|
|
@@ -1200,82 +1495,1422 @@ function getLanguageId(ext) {
|
|
|
1200
1495
|
return EXT_TO_LANG[ext] ?? "plaintext";
|
|
1201
1496
|
}
|
|
1202
1497
|
|
|
1498
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1499
|
+
var WATCHED_FILE_BATCH_SIZE = 128;
|
|
1500
|
+
var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1501
|
+
function canonicalPath(filePath) {
|
|
1502
|
+
const absolute = resolve2(filePath);
|
|
1503
|
+
try {
|
|
1504
|
+
return realpathSync2(absolute);
|
|
1505
|
+
} catch {
|
|
1506
|
+
return absolute;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
function isSameOrDescendant(candidate, parent) {
|
|
1510
|
+
const suffix = relative2(parent, candidate);
|
|
1511
|
+
return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
|
|
1512
|
+
}
|
|
1513
|
+
function movedPath(candidate, oldPath, newPath) {
|
|
1514
|
+
const suffix = relative2(oldPath, candidate);
|
|
1515
|
+
return suffix === "" ? newPath : resolve2(newPath, suffix);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
class WorkspaceDocumentState {
|
|
1519
|
+
sendNotification;
|
|
1520
|
+
clearDiagnostics;
|
|
1521
|
+
openDocuments = new Map;
|
|
1522
|
+
openByUri = new Map;
|
|
1523
|
+
openPromises = new Map;
|
|
1524
|
+
now;
|
|
1525
|
+
versionlessPublishQuiescenceMs;
|
|
1526
|
+
constructor(sendNotification, clearDiagnostics, options = {}) {
|
|
1527
|
+
this.sendNotification = sendNotification;
|
|
1528
|
+
this.clearDiagnostics = clearDiagnostics;
|
|
1529
|
+
this.now = options.now ?? (() => Date.now());
|
|
1530
|
+
this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
1531
|
+
}
|
|
1532
|
+
async openFile(filePath) {
|
|
1533
|
+
const path = canonicalPath(filePath);
|
|
1534
|
+
const existingOpen = this.openPromises.get(path);
|
|
1535
|
+
if (existingOpen) {
|
|
1536
|
+
await existingOpen;
|
|
1537
|
+
return this.openFile(path);
|
|
1538
|
+
}
|
|
1539
|
+
const text = readFileSync(path, "utf-8");
|
|
1540
|
+
const existing = this.openDocuments.get(path);
|
|
1541
|
+
if (!existing)
|
|
1542
|
+
return this.openDocumentSingleFlight(path, text);
|
|
1543
|
+
if (existing.text === text)
|
|
1544
|
+
return;
|
|
1545
|
+
await this.changeDocument(existing, text);
|
|
1546
|
+
}
|
|
1547
|
+
getVersion(filePath) {
|
|
1548
|
+
return this.openDocuments.get(canonicalPath(filePath))?.version;
|
|
1549
|
+
}
|
|
1550
|
+
getStoredDiagnostics(uri) {
|
|
1551
|
+
const state = this.openByUri.get(uri);
|
|
1552
|
+
if (!state)
|
|
1553
|
+
return [];
|
|
1554
|
+
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
1555
|
+
}
|
|
1556
|
+
captureDiagnosticSnapshot(filePath) {
|
|
1557
|
+
const state = this.openDocuments.get(canonicalPath(filePath));
|
|
1558
|
+
if (!state)
|
|
1559
|
+
return null;
|
|
1560
|
+
return {
|
|
1561
|
+
path: state.path,
|
|
1562
|
+
uri: state.uri,
|
|
1563
|
+
version: state.version,
|
|
1564
|
+
documentGeneration: state.generation,
|
|
1565
|
+
publishGeneration: state.publishGeneration
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
isCurrentSnapshot(snapshot) {
|
|
1569
|
+
const state = this.openDocuments.get(snapshot.path);
|
|
1570
|
+
return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
|
|
1571
|
+
}
|
|
1572
|
+
getPullCache(snapshot) {
|
|
1573
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1574
|
+
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
|
|
1575
|
+
return null;
|
|
1576
|
+
return state.pullCache;
|
|
1577
|
+
}
|
|
1578
|
+
recordPullDiagnostics(snapshot, report) {
|
|
1579
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1580
|
+
if (!state)
|
|
1581
|
+
return;
|
|
1582
|
+
state.pullCache = {
|
|
1583
|
+
documentVersion: snapshot.version,
|
|
1584
|
+
diagnostics: [...report.diagnostics],
|
|
1585
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
recordPublishedDiagnostics(params) {
|
|
1589
|
+
const state = this.openByUri.get(params.uri);
|
|
1590
|
+
if (!state)
|
|
1591
|
+
return;
|
|
1592
|
+
state.publishGeneration += 1;
|
|
1593
|
+
state.lastPublish = {
|
|
1594
|
+
diagnostics: [...params.diagnostics],
|
|
1595
|
+
publishGeneration: state.publishGeneration,
|
|
1596
|
+
documentGenerationAtArrival: state.generation,
|
|
1597
|
+
arrivedAt: this.now(),
|
|
1598
|
+
...params.version === undefined ? {} : { version: params.version }
|
|
1599
|
+
};
|
|
1600
|
+
this.notifyWaiters(state);
|
|
1601
|
+
}
|
|
1602
|
+
resolvePushDiagnostics(snapshot) {
|
|
1603
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1604
|
+
if (!state?.lastPublish)
|
|
1605
|
+
return { status: "missing" };
|
|
1606
|
+
const publish = state.lastPublish;
|
|
1607
|
+
if (publish.version !== undefined) {
|
|
1608
|
+
return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
|
|
1609
|
+
}
|
|
1610
|
+
if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
|
|
1611
|
+
return { status: "missing" };
|
|
1612
|
+
const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
|
|
1613
|
+
const waitMs = Math.max(0, readyAt - this.now());
|
|
1614
|
+
return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
|
|
1615
|
+
}
|
|
1616
|
+
waitForDiagnosticsActivity(snapshot, timeoutMs) {
|
|
1617
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1618
|
+
if (!state || timeoutMs <= 0)
|
|
1619
|
+
return Promise.resolve();
|
|
1620
|
+
return new Promise((resolveActivity) => {
|
|
1621
|
+
let settled = false;
|
|
1622
|
+
const finish = () => {
|
|
1623
|
+
if (settled)
|
|
1624
|
+
return;
|
|
1625
|
+
settled = true;
|
|
1626
|
+
clearTimeout(timer);
|
|
1627
|
+
state.waiters.delete(finish);
|
|
1628
|
+
resolveActivity();
|
|
1629
|
+
};
|
|
1630
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1631
|
+
if (typeof timer.unref === "function")
|
|
1632
|
+
timer.unref();
|
|
1633
|
+
state.waiters.add(finish);
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
validateVersions(operations) {
|
|
1637
|
+
const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
|
|
1638
|
+
for (const operation of operations) {
|
|
1639
|
+
if (operation.kind === "text") {
|
|
1640
|
+
const current = versions.get(operation.path);
|
|
1641
|
+
if (operation.documentVersion !== null && current !== operation.documentVersion) {
|
|
1642
|
+
const observed = current === undefined ? "closed document" : `open document version ${current}`;
|
|
1643
|
+
return {
|
|
1644
|
+
changeIndex: operation.changeIndex,
|
|
1645
|
+
message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
if (current !== undefined)
|
|
1649
|
+
versions.set(operation.path, current + 1);
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
if (operation.kind === "rename") {
|
|
1653
|
+
const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
|
|
1654
|
+
for (const [path] of moved)
|
|
1655
|
+
versions.delete(path);
|
|
1656
|
+
for (const [path] of moved)
|
|
1657
|
+
versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (operation.kind === "delete") {
|
|
1661
|
+
for (const path of [...versions.keys()]) {
|
|
1662
|
+
if (isSameOrDescendant(path, operation.path))
|
|
1663
|
+
versions.delete(path);
|
|
1664
|
+
}
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
|
|
1668
|
+
versions.set(operation.path, 1);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1673
|
+
async synchronize(delta) {
|
|
1674
|
+
const watched = [];
|
|
1675
|
+
for (const mutation of delta.operations)
|
|
1676
|
+
await this.synchronizeMutation(mutation, watched);
|
|
1677
|
+
for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
|
|
1678
|
+
await this.sendNotification("workspace/didChangeWatchedFiles", {
|
|
1679
|
+
changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
async synchronizeMutation(mutation, watched) {
|
|
1684
|
+
if (mutation.kind === "text") {
|
|
1685
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1686
|
+
if (state)
|
|
1687
|
+
await this.changeDocument(state, mutation.afterText);
|
|
1688
|
+
else
|
|
1689
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (mutation.kind === "create") {
|
|
1693
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1694
|
+
if (state) {
|
|
1695
|
+
await this.closeDocument(state);
|
|
1696
|
+
await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
|
|
1697
|
+
} else {
|
|
1698
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
|
|
1699
|
+
}
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
if (mutation.kind === "rename") {
|
|
1703
|
+
const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
|
|
1704
|
+
for (const state of moved)
|
|
1705
|
+
await this.closeDocument(state);
|
|
1706
|
+
for (const state of moved) {
|
|
1707
|
+
const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
|
|
1708
|
+
await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
|
|
1709
|
+
}
|
|
1710
|
+
if (moved.length === 0) {
|
|
1711
|
+
watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
|
|
1712
|
+
watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
|
|
1713
|
+
}
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
|
|
1717
|
+
for (const state of removed)
|
|
1718
|
+
await this.closeDocument(state);
|
|
1719
|
+
if (removed.length === 0)
|
|
1720
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
|
|
1721
|
+
}
|
|
1722
|
+
async openDocumentSingleFlight(path, text) {
|
|
1723
|
+
const existing = this.openPromises.get(path);
|
|
1724
|
+
if (existing)
|
|
1725
|
+
return existing;
|
|
1726
|
+
const open = (async () => {
|
|
1727
|
+
const state = {
|
|
1728
|
+
path,
|
|
1729
|
+
uri: pathToFileURL2(path).href,
|
|
1730
|
+
languageId: getLanguageId(effectiveExtension(path)),
|
|
1731
|
+
text,
|
|
1732
|
+
version: 1,
|
|
1733
|
+
generation: 1,
|
|
1734
|
+
publishGeneration: 0,
|
|
1735
|
+
waiters: new Set
|
|
1736
|
+
};
|
|
1737
|
+
this.openDocuments.set(path, state);
|
|
1738
|
+
this.openByUri.set(state.uri, state);
|
|
1739
|
+
this.notifyWaiters(state);
|
|
1740
|
+
await this.sendNotification("textDocument/didOpen", {
|
|
1741
|
+
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
|
|
1742
|
+
});
|
|
1743
|
+
})().finally(() => {
|
|
1744
|
+
this.openPromises.delete(path);
|
|
1745
|
+
});
|
|
1746
|
+
this.openPromises.set(path, open);
|
|
1747
|
+
return open;
|
|
1748
|
+
}
|
|
1749
|
+
async changeDocument(state, text) {
|
|
1750
|
+
state.text = text;
|
|
1751
|
+
state.version += 1;
|
|
1752
|
+
state.generation += 1;
|
|
1753
|
+
this.clearDiagnostics(state.uri);
|
|
1754
|
+
this.notifyWaiters(state);
|
|
1755
|
+
await this.sendNotification("textDocument/didChange", {
|
|
1756
|
+
textDocument: { uri: state.uri, version: state.version },
|
|
1757
|
+
contentChanges: [{ text }]
|
|
1758
|
+
});
|
|
1759
|
+
await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
|
|
1760
|
+
}
|
|
1761
|
+
async closeDocument(state) {
|
|
1762
|
+
this.openDocuments.delete(state.path);
|
|
1763
|
+
this.openByUri.delete(state.uri);
|
|
1764
|
+
this.clearDiagnostics(state.uri);
|
|
1765
|
+
this.notifyWaiters(state);
|
|
1766
|
+
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
1767
|
+
}
|
|
1768
|
+
notifyWaiters(state) {
|
|
1769
|
+
for (const waiter of [...state.waiters])
|
|
1770
|
+
waiter();
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
|
|
1775
|
+
var CONCURRENT_FAILURE_REASON_BY_PHASE = {
|
|
1776
|
+
applying: "workspace/applyEdit is already in progress for this workspace mutation",
|
|
1777
|
+
settled: "workspace/applyEdit was already handled for this workspace mutation"
|
|
1778
|
+
};
|
|
1779
|
+
function workspaceApplyEditConcurrentFailureReason(phase) {
|
|
1780
|
+
return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1784
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1785
|
+
|
|
1786
|
+
// ../lsp-core/src/lsp/workspace-edit-path.ts
|
|
1787
|
+
import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync3 } from "node:fs";
|
|
1788
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3 } from "node:path";
|
|
1789
|
+
import { fileURLToPath } from "node:url";
|
|
1790
|
+
|
|
1791
|
+
class WorkspaceEditPathError extends Error {
|
|
1792
|
+
path;
|
|
1793
|
+
detail;
|
|
1794
|
+
name = "WorkspaceEditPathError";
|
|
1795
|
+
constructor(path, detail) {
|
|
1796
|
+
super(`${detail}: ${path}`);
|
|
1797
|
+
this.path = path;
|
|
1798
|
+
this.detail = detail;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
1802
|
+
const relativePath = relative3(workspaceRoot, filePath);
|
|
1803
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
|
|
1804
|
+
}
|
|
1805
|
+
function canonicalizeMissingPath(filePath) {
|
|
1806
|
+
let ancestor = filePath;
|
|
1807
|
+
while (!existsSync3(ancestor)) {
|
|
1808
|
+
const parent = dirname2(ancestor);
|
|
1809
|
+
if (parent === ancestor)
|
|
1810
|
+
throw new WorkspaceEditPathError(filePath, "no existing ancestor");
|
|
1811
|
+
ancestor = parent;
|
|
1812
|
+
}
|
|
1813
|
+
return resolve3(realpathSync3(ancestor), relative3(ancestor, filePath));
|
|
1814
|
+
}
|
|
1815
|
+
function canonicalWorkspaceRoot(workspaceRoot) {
|
|
1816
|
+
try {
|
|
1817
|
+
const canonical = realpathSync3(resolve3(workspaceRoot));
|
|
1818
|
+
if (!lstatSync(canonical).isDirectory()) {
|
|
1819
|
+
return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
|
|
1820
|
+
}
|
|
1821
|
+
return {
|
|
1822
|
+
success: true,
|
|
1823
|
+
path: canonical,
|
|
1824
|
+
requestedPath: resolve3(workspaceRoot),
|
|
1825
|
+
followedSymbolicLink: existsSync3(resolve3(workspaceRoot)) && lstatSync(resolve3(workspaceRoot)).isSymbolicLink()
|
|
1826
|
+
};
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1829
|
+
return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
|
|
1833
|
+
let requestedPath;
|
|
1834
|
+
try {
|
|
1835
|
+
const parsed = new URL(uri);
|
|
1836
|
+
if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
|
|
1837
|
+
return { success: false, error: `non-file URI ${uri}` };
|
|
1838
|
+
}
|
|
1839
|
+
requestedPath = resolve3(fileURLToPath(parsed));
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1842
|
+
return { success: false, error: `non-file URI ${uri}: ${detail}` };
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
const canonical = existsSync3(requestedPath) ? realpathSync3(requestedPath) : canonicalizeMissingPath(requestedPath);
|
|
1846
|
+
if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
|
|
1847
|
+
return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
|
|
1848
|
+
}
|
|
1849
|
+
return {
|
|
1850
|
+
success: true,
|
|
1851
|
+
path: canonical,
|
|
1852
|
+
requestedPath,
|
|
1853
|
+
followedSymbolicLink: existsSync3(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
|
|
1854
|
+
};
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1857
|
+
return { success: false, error: `${requestedPath}: ${detail}` };
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
function snapshotPath(path, includeChildren) {
|
|
1861
|
+
if (!existsSync3(path))
|
|
1862
|
+
return { kind: "missing" };
|
|
1863
|
+
const stats = lstatSync(path);
|
|
1864
|
+
if (stats.isFile())
|
|
1865
|
+
return { kind: "file", content: readFileSync2(path, "utf-8") };
|
|
1866
|
+
if (stats.isDirectory()) {
|
|
1867
|
+
return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
|
|
1868
|
+
}
|
|
1869
|
+
throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1873
|
+
var DEFAULT_IO = {
|
|
1874
|
+
writeFile(path, content) {
|
|
1875
|
+
writeFileSync(path, content, "utf-8");
|
|
1876
|
+
},
|
|
1877
|
+
rename(oldPath, newPath) {
|
|
1878
|
+
renameSync(oldPath, newPath);
|
|
1879
|
+
},
|
|
1880
|
+
remove(path, recursive) {
|
|
1881
|
+
rmSync(path, { recursive, force: false });
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
function snapshotsEqual(expected, actual) {
|
|
1885
|
+
if (expected.kind !== actual.kind)
|
|
1886
|
+
return false;
|
|
1887
|
+
if (expected.kind === "file" && actual.kind === "file")
|
|
1888
|
+
return expected.content === actual.content;
|
|
1889
|
+
if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
|
|
1890
|
+
return JSON.stringify(expected.children) === JSON.stringify(actual.children);
|
|
1891
|
+
}
|
|
1892
|
+
return true;
|
|
1893
|
+
}
|
|
1894
|
+
function liveSnapshot(path, expected) {
|
|
1895
|
+
return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
|
|
1896
|
+
}
|
|
1897
|
+
function firstOperationIndex(plan) {
|
|
1898
|
+
return plan.operations[0]?.changeIndex ?? 0;
|
|
1899
|
+
}
|
|
1900
|
+
function failedCommit(plan, failure) {
|
|
1901
|
+
const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
|
|
1902
|
+
return {
|
|
1903
|
+
result: {
|
|
1904
|
+
success: false,
|
|
1905
|
+
filesModified,
|
|
1906
|
+
totalEdits,
|
|
1907
|
+
errors: [`change ${changeIndex}: ${message}`],
|
|
1908
|
+
failedChange: changeIndex,
|
|
1909
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1910
|
+
},
|
|
1911
|
+
delta: mutationDelta(mutations),
|
|
1912
|
+
fingerprint: plan.fingerprint
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
function verifySnapshots(plan) {
|
|
1916
|
+
for (const [path, expected] of plan.snapshots) {
|
|
1917
|
+
let actual;
|
|
1918
|
+
try {
|
|
1919
|
+
actual = liveSnapshot(path, expected);
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1922
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1923
|
+
return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
|
|
1924
|
+
}
|
|
1925
|
+
if (!snapshotsEqual(expected, actual)) {
|
|
1926
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1927
|
+
return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return null;
|
|
1931
|
+
}
|
|
1932
|
+
function addModifiedPath(paths, path) {
|
|
1933
|
+
if (!paths.includes(path))
|
|
1934
|
+
paths.push(path);
|
|
1935
|
+
}
|
|
1936
|
+
function reportedPath(plan, path) {
|
|
1937
|
+
return plan.reportedPathByCanonical.get(path) ?? path;
|
|
1938
|
+
}
|
|
1939
|
+
function changedPathsForMutation(mutation) {
|
|
1940
|
+
return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
|
|
1941
|
+
}
|
|
1942
|
+
function mutationDelta(operations) {
|
|
1943
|
+
const changedPaths = new Set;
|
|
1944
|
+
for (const operation of operations) {
|
|
1945
|
+
for (const path of changedPathsForMutation(operation))
|
|
1946
|
+
changedPaths.add(path);
|
|
1947
|
+
}
|
|
1948
|
+
return { operations, changedPaths: [...changedPaths].sort() };
|
|
1949
|
+
}
|
|
1950
|
+
function resolveIo(overrides) {
|
|
1951
|
+
return {
|
|
1952
|
+
writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
|
|
1953
|
+
rename: overrides?.rename ?? DEFAULT_IO.rename,
|
|
1954
|
+
remove: overrides?.remove ?? DEFAULT_IO.remove
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
function commitOperation(context, operation) {
|
|
1958
|
+
const { plan, io, accumulator } = context;
|
|
1959
|
+
if (operation.kind === "noop")
|
|
1960
|
+
return;
|
|
1961
|
+
if (operation.kind === "text") {
|
|
1962
|
+
io.writeFile(operation.path, operation.afterText);
|
|
1963
|
+
accumulator.mutations.push({
|
|
1964
|
+
kind: "text",
|
|
1965
|
+
path: operation.path,
|
|
1966
|
+
beforeText: operation.beforeText,
|
|
1967
|
+
afterText: operation.afterText
|
|
1968
|
+
});
|
|
1969
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1970
|
+
accumulator.totalEdits += operation.editCount;
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
if (operation.kind === "create") {
|
|
1974
|
+
io.writeFile(operation.path, "");
|
|
1975
|
+
accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
|
|
1976
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
if (operation.kind === "rename") {
|
|
1980
|
+
if (operation.replaceDestination) {
|
|
1981
|
+
const targetKind = existsSync4(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
|
|
1982
|
+
io.remove(operation.newPath, targetKind === "directory");
|
|
1983
|
+
accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
|
|
1984
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1985
|
+
}
|
|
1986
|
+
io.rename(operation.oldPath, operation.newPath);
|
|
1987
|
+
accumulator.mutations.push({
|
|
1988
|
+
kind: "rename",
|
|
1989
|
+
oldPath: operation.oldPath,
|
|
1990
|
+
newPath: operation.newPath,
|
|
1991
|
+
sourceKind: operation.sourceKind
|
|
1992
|
+
});
|
|
1993
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
io.remove(operation.path, operation.recursive);
|
|
1997
|
+
accumulator.mutations.push({
|
|
1998
|
+
kind: "delete",
|
|
1999
|
+
path: operation.path,
|
|
2000
|
+
targetKind: operation.targetKind
|
|
2001
|
+
});
|
|
2002
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2003
|
+
}
|
|
2004
|
+
function commitWorkspaceEditPlan(plan, options = {}) {
|
|
2005
|
+
if (options.signal?.aborted) {
|
|
2006
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2007
|
+
}
|
|
2008
|
+
const stale = verifySnapshots(plan);
|
|
2009
|
+
if (stale)
|
|
2010
|
+
return stale;
|
|
2011
|
+
if (options.signal?.aborted) {
|
|
2012
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2013
|
+
}
|
|
2014
|
+
const io = resolveIo(options.io);
|
|
2015
|
+
const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
|
|
2016
|
+
const context = { plan, io, accumulator };
|
|
2017
|
+
let lateAbort = false;
|
|
2018
|
+
for (const operation of plan.operations) {
|
|
2019
|
+
try {
|
|
2020
|
+
commitOperation(context, operation);
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2023
|
+
return failedCommit(plan, {
|
|
2024
|
+
message: `I/O failure during ${operation.kind}: ${detail}`,
|
|
2025
|
+
changeIndex: operation.changeIndex,
|
|
2026
|
+
mutations: accumulator.mutations,
|
|
2027
|
+
filesModified: accumulator.filesModified,
|
|
2028
|
+
totalEdits: accumulator.totalEdits,
|
|
2029
|
+
lateAbort: lateAbort || options.signal?.aborted === true
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
if (options.signal?.aborted)
|
|
2033
|
+
lateAbort = true;
|
|
2034
|
+
}
|
|
2035
|
+
const result = {
|
|
2036
|
+
success: true,
|
|
2037
|
+
filesModified: accumulator.filesModified,
|
|
2038
|
+
totalEdits: accumulator.totalEdits,
|
|
2039
|
+
errors: [],
|
|
2040
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
2041
|
+
};
|
|
2042
|
+
return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
|
|
2046
|
+
import { createHash } from "node:crypto";
|
|
2047
|
+
function canonicalFingerprint(operations) {
|
|
2048
|
+
const canonical = operations.map((operation) => {
|
|
2049
|
+
switch (operation.kind) {
|
|
2050
|
+
case "text":
|
|
2051
|
+
return {
|
|
2052
|
+
kind: operation.kind,
|
|
2053
|
+
changeIndex: operation.changeIndex,
|
|
2054
|
+
path: operation.path,
|
|
2055
|
+
edits: operation.edits,
|
|
2056
|
+
version: operation.version
|
|
2057
|
+
};
|
|
2058
|
+
case "rename":
|
|
2059
|
+
return {
|
|
2060
|
+
kind: operation.kind,
|
|
2061
|
+
changeIndex: operation.changeIndex,
|
|
2062
|
+
oldPath: operation.oldPath,
|
|
2063
|
+
newPath: operation.newPath,
|
|
2064
|
+
overwrite: operation.overwrite,
|
|
2065
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2066
|
+
};
|
|
2067
|
+
case "create":
|
|
2068
|
+
return {
|
|
2069
|
+
kind: operation.kind,
|
|
2070
|
+
changeIndex: operation.changeIndex,
|
|
2071
|
+
path: operation.path,
|
|
2072
|
+
overwrite: operation.overwrite,
|
|
2073
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2074
|
+
};
|
|
2075
|
+
case "delete":
|
|
2076
|
+
return {
|
|
2077
|
+
kind: operation.kind,
|
|
2078
|
+
changeIndex: operation.changeIndex,
|
|
2079
|
+
path: operation.path,
|
|
2080
|
+
recursive: operation.recursive,
|
|
2081
|
+
ignoreIfNotExists: operation.ignoreIfNotExists
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// ../lsp-core/src/lsp/workspace-edit-types.ts
|
|
2089
|
+
class WorkspaceEditValidationError extends Error {
|
|
2090
|
+
changeIndex;
|
|
2091
|
+
detail;
|
|
2092
|
+
name = "WorkspaceEditValidationError";
|
|
2093
|
+
constructor(changeIndex, detail) {
|
|
2094
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
2095
|
+
this.changeIndex = changeIndex;
|
|
2096
|
+
this.detail = detail;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
|
|
2101
|
+
function isRecord3(value) {
|
|
2102
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2103
|
+
}
|
|
2104
|
+
function parsePosition(value) {
|
|
2105
|
+
if (!isRecord3(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
return { line: value["line"], character: value["character"] };
|
|
2109
|
+
}
|
|
2110
|
+
function parseRange(value) {
|
|
2111
|
+
if (!isRecord3(value))
|
|
2112
|
+
return null;
|
|
2113
|
+
const start = parsePosition(value["start"]);
|
|
2114
|
+
const end = parsePosition(value["end"]);
|
|
2115
|
+
return start && end ? { start, end } : null;
|
|
2116
|
+
}
|
|
2117
|
+
function parseTextEdits(value, changeIndex) {
|
|
2118
|
+
if (!Array.isArray(value)) {
|
|
2119
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
|
|
2120
|
+
}
|
|
2121
|
+
const edits = [];
|
|
2122
|
+
for (const candidate of value) {
|
|
2123
|
+
if (!isRecord3(candidate) || typeof candidate["newText"] !== "string") {
|
|
2124
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
|
|
2125
|
+
}
|
|
2126
|
+
if ("annotationId" in candidate) {
|
|
2127
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
|
|
2128
|
+
}
|
|
2129
|
+
const range = parseRange(candidate["range"]);
|
|
2130
|
+
if (!range)
|
|
2131
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
|
|
2132
|
+
edits.push({ range, newText: candidate["newText"] });
|
|
2133
|
+
}
|
|
2134
|
+
return edits;
|
|
2135
|
+
}
|
|
2136
|
+
function parseBooleanOption(options, key, changeIndex) {
|
|
2137
|
+
const value = options[key];
|
|
2138
|
+
if (value === undefined)
|
|
2139
|
+
return false;
|
|
2140
|
+
if (typeof value !== "boolean") {
|
|
2141
|
+
throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
|
|
2142
|
+
}
|
|
2143
|
+
return value;
|
|
2144
|
+
}
|
|
2145
|
+
function parseOptions(value, allowed, changeIndex) {
|
|
2146
|
+
if (value === undefined)
|
|
2147
|
+
return {};
|
|
2148
|
+
if (!isRecord3(value))
|
|
2149
|
+
throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
|
|
2150
|
+
for (const key of Object.keys(value)) {
|
|
2151
|
+
if (!allowed.includes(key))
|
|
2152
|
+
throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
|
|
2153
|
+
}
|
|
2154
|
+
const parsed = {};
|
|
2155
|
+
for (const key of allowed)
|
|
2156
|
+
parsed[key] = parseBooleanOption(value, key, changeIndex);
|
|
2157
|
+
return parsed;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
|
|
2161
|
+
function parseResourceChange(input) {
|
|
2162
|
+
const kind = input.change["kind"];
|
|
2163
|
+
if (kind === "create" || kind === "delete") {
|
|
2164
|
+
parseSinglePathResource(input, kind);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
if (kind !== "rename") {
|
|
2168
|
+
throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
|
|
2169
|
+
}
|
|
2170
|
+
parseRename(input);
|
|
2171
|
+
}
|
|
2172
|
+
function parseSinglePathResource(input, kind) {
|
|
2173
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2174
|
+
if (typeof change["uri"] !== "string")
|
|
2175
|
+
throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
|
|
2176
|
+
const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
|
|
2177
|
+
if (!resolvedPath.success) {
|
|
2178
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
if (kind === "create") {
|
|
2182
|
+
const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2183
|
+
target.operations.push({
|
|
2184
|
+
kind,
|
|
2185
|
+
changeIndex,
|
|
2186
|
+
path: resolvedPath.path,
|
|
2187
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2188
|
+
overwrite: options2["overwrite"] ?? false,
|
|
2189
|
+
ignoreIfExists: options2["ignoreIfExists"] ?? false,
|
|
2190
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2191
|
+
});
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
|
|
2195
|
+
target.operations.push({
|
|
2196
|
+
kind,
|
|
2197
|
+
changeIndex,
|
|
2198
|
+
path: resolvedPath.path,
|
|
2199
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2200
|
+
recursive: options["recursive"] ?? false,
|
|
2201
|
+
ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
|
|
2202
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
function parseRename(input) {
|
|
2206
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2207
|
+
if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
|
|
2208
|
+
throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
|
|
2209
|
+
}
|
|
2210
|
+
const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
|
|
2211
|
+
const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
|
|
2212
|
+
if (!oldPath.success || !newPath.success) {
|
|
2213
|
+
target.failures.push({
|
|
2214
|
+
changeIndex,
|
|
2215
|
+
message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
|
|
2216
|
+
});
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2220
|
+
target.operations.push({
|
|
2221
|
+
kind: "rename",
|
|
2222
|
+
changeIndex,
|
|
2223
|
+
oldPath: oldPath.path,
|
|
2224
|
+
newPath: newPath.path,
|
|
2225
|
+
reportedOldPath: oldPath.requestedPath,
|
|
2226
|
+
reportedNewPath: newPath.requestedPath,
|
|
2227
|
+
overwrite: options["overwrite"] ?? false,
|
|
2228
|
+
ignoreIfExists: options["ignoreIfExists"] ?? false,
|
|
2229
|
+
followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// ../lsp-core/src/lsp/workspace-edit-parser.ts
|
|
2234
|
+
function failureResult(failures) {
|
|
2235
|
+
const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
|
|
2236
|
+
const first = sorted[0];
|
|
2237
|
+
return {
|
|
2238
|
+
success: false,
|
|
2239
|
+
filesModified: [],
|
|
2240
|
+
totalEdits: 0,
|
|
2241
|
+
errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
|
|
2242
|
+
...first ? { failedChange: first.changeIndex } : {}
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
function parseWorkspaceEdit(edit, workspaceRoot) {
|
|
2246
|
+
if (!isRecord3(edit))
|
|
2247
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
|
|
2248
|
+
if (edit["changeAnnotations"] !== undefined) {
|
|
2249
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
|
|
2250
|
+
}
|
|
2251
|
+
const hasChanges = edit["changes"] !== undefined;
|
|
2252
|
+
const hasDocumentChanges = edit["documentChanges"] !== undefined;
|
|
2253
|
+
if (hasChanges && hasDocumentChanges) {
|
|
2254
|
+
return {
|
|
2255
|
+
operations: [],
|
|
2256
|
+
failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
const target = { operations: [], failures: [] };
|
|
2260
|
+
if (hasChanges)
|
|
2261
|
+
return parseChanges(edit["changes"], workspaceRoot, target);
|
|
2262
|
+
return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
|
|
2263
|
+
}
|
|
2264
|
+
function parseChanges(value, workspaceRoot, target) {
|
|
2265
|
+
if (!isRecord3(value))
|
|
2266
|
+
return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
|
|
2267
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
2268
|
+
for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
|
|
2269
|
+
const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
|
|
2270
|
+
if (!resolvedPath.success) {
|
|
2271
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
try {
|
|
2275
|
+
target.operations.push({
|
|
2276
|
+
kind: "text",
|
|
2277
|
+
changeIndex,
|
|
2278
|
+
path: resolvedPath.path,
|
|
2279
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2280
|
+
edits: parseTextEdits(rawEdits, changeIndex),
|
|
2281
|
+
version: null
|
|
2282
|
+
});
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2285
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2286
|
+
continue;
|
|
2287
|
+
}
|
|
2288
|
+
throw error;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return target;
|
|
2292
|
+
}
|
|
2293
|
+
function parseDocumentChanges(value, workspaceRoot, target) {
|
|
2294
|
+
if (value === undefined)
|
|
2295
|
+
return target;
|
|
2296
|
+
if (!Array.isArray(value)) {
|
|
2297
|
+
return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
|
|
2298
|
+
}
|
|
2299
|
+
for (const [changeIndex, change] of value.entries()) {
|
|
2300
|
+
try {
|
|
2301
|
+
parseDocumentChange({ change, changeIndex, workspaceRoot, target });
|
|
2302
|
+
} catch (error) {
|
|
2303
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2304
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
throw error;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
return target;
|
|
2311
|
+
}
|
|
2312
|
+
function parseDocumentChange(input) {
|
|
2313
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2314
|
+
if (!isRecord3(change))
|
|
2315
|
+
throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
|
|
2316
|
+
if ("annotationId" in change) {
|
|
2317
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
|
|
2318
|
+
}
|
|
2319
|
+
if (typeof change["kind"] === "string") {
|
|
2320
|
+
parseResourceChange({ change, changeIndex, workspaceRoot, target });
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const identifier = change["textDocument"];
|
|
2324
|
+
if (!isRecord3(identifier) || typeof identifier["uri"] !== "string") {
|
|
2325
|
+
throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
|
|
2326
|
+
}
|
|
2327
|
+
const version = identifier["version"];
|
|
2328
|
+
if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
|
|
2329
|
+
throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
|
|
2330
|
+
}
|
|
2331
|
+
const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
|
|
2332
|
+
if (!resolvedPath.success) {
|
|
2333
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
target.operations.push({
|
|
2337
|
+
kind: "text",
|
|
2338
|
+
changeIndex,
|
|
2339
|
+
path: resolvedPath.path,
|
|
2340
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2341
|
+
edits: parseTextEdits(change["edits"], changeIndex),
|
|
2342
|
+
version
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2347
|
+
import { dirname as dirname3, relative as relative4, resolve as resolve4 } from "node:path";
|
|
2348
|
+
|
|
2349
|
+
// ../lsp-core/src/lsp/workspace-edit-text.ts
|
|
2350
|
+
function comparePosition(left, right) {
|
|
2351
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
2352
|
+
}
|
|
2353
|
+
function positionsEqual(left, right) {
|
|
2354
|
+
return left.line === right.line && left.character === right.character;
|
|
2355
|
+
}
|
|
2356
|
+
function rangesEqual(left, right) {
|
|
2357
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
2358
|
+
}
|
|
2359
|
+
function isEmptyRange(range) {
|
|
2360
|
+
return positionsEqual(range.start, range.end);
|
|
2361
|
+
}
|
|
2362
|
+
function formatRange(range) {
|
|
2363
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
2364
|
+
}
|
|
2365
|
+
function validatePosition(position, label, context) {
|
|
2366
|
+
const { lines, changeIndex } = context;
|
|
2367
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
2368
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
2369
|
+
}
|
|
2370
|
+
if (position.line < 0 || position.character < 0) {
|
|
2371
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
2372
|
+
}
|
|
2373
|
+
const line = lines[position.line];
|
|
2374
|
+
if (line === undefined) {
|
|
2375
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
2376
|
+
}
|
|
2377
|
+
if (position.character > line.length) {
|
|
2378
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
function validateRange(range, lines, changeIndex) {
|
|
2382
|
+
const context = { lines, changeIndex };
|
|
2383
|
+
validatePosition(range.start, "start", context);
|
|
2384
|
+
validatePosition(range.end, "end", context);
|
|
2385
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
2386
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
function sortAndDeduplicate(edits) {
|
|
2390
|
+
const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
|
|
2391
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
2392
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
2393
|
+
});
|
|
2394
|
+
const unique = [];
|
|
2395
|
+
for (const entry of sorted) {
|
|
2396
|
+
const previous = unique.at(-1);
|
|
2397
|
+
if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
unique.push(entry.edit);
|
|
2401
|
+
}
|
|
2402
|
+
return unique;
|
|
2403
|
+
}
|
|
2404
|
+
function validateNoOverlap(edits, changeIndex) {
|
|
2405
|
+
for (let index = 0;index < edits.length - 1; index += 1) {
|
|
2406
|
+
const later = edits[index];
|
|
2407
|
+
const earlier = edits[index + 1];
|
|
2408
|
+
if (later === undefined || earlier === undefined)
|
|
2409
|
+
continue;
|
|
2410
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
2411
|
+
throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function applyNormalizedTextEdits(content, edits) {
|
|
2416
|
+
const lines = content.split(`
|
|
2417
|
+
`);
|
|
2418
|
+
for (const edit of edits) {
|
|
2419
|
+
const { start, end } = edit.range;
|
|
2420
|
+
const startLine = lines[start.line];
|
|
2421
|
+
const endLine = lines[end.line];
|
|
2422
|
+
if (startLine === undefined || endLine === undefined)
|
|
2423
|
+
continue;
|
|
2424
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
2425
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
|
|
2426
|
+
`));
|
|
2427
|
+
}
|
|
2428
|
+
return lines.join(`
|
|
2429
|
+
`);
|
|
2430
|
+
}
|
|
2431
|
+
function normalizeTextEdits(content, edits, changeIndex) {
|
|
2432
|
+
const lines = content.split(`
|
|
2433
|
+
`);
|
|
2434
|
+
for (const edit of edits) {
|
|
2435
|
+
validateRange(edit.range, lines, changeIndex);
|
|
2436
|
+
}
|
|
2437
|
+
const normalized = sortAndDeduplicate(edits);
|
|
2438
|
+
validateNoOverlap(normalized, changeIndex);
|
|
2439
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2443
|
+
function isSameOrDescendant2(candidate, parent) {
|
|
2444
|
+
const relativePath = relative4(parent, candidate);
|
|
2445
|
+
return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
|
|
2446
|
+
}
|
|
2447
|
+
function removeVirtualSubtree(virtual, path) {
|
|
2448
|
+
for (const candidate of [...virtual.keys()]) {
|
|
2449
|
+
if (isSameOrDescendant2(candidate, path))
|
|
2450
|
+
virtual.delete(candidate);
|
|
2451
|
+
}
|
|
2452
|
+
virtual.set(path, { kind: "missing" });
|
|
2453
|
+
}
|
|
2454
|
+
function moveVirtualSubtree(virtual, oldPath, newPath) {
|
|
2455
|
+
const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
|
|
2456
|
+
removeVirtualSubtree(virtual, oldPath);
|
|
2457
|
+
removeVirtualSubtree(virtual, newPath);
|
|
2458
|
+
for (const [candidate, entry] of moved) {
|
|
2459
|
+
const suffix = relative4(oldPath, candidate);
|
|
2460
|
+
virtual.set(suffix === "" ? newPath : resolve4(newPath, suffix), entry);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
function virtualDirectoryHasChildren(virtual, path) {
|
|
2464
|
+
for (const [candidate, entry] of virtual) {
|
|
2465
|
+
if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
|
|
2466
|
+
return true;
|
|
2467
|
+
}
|
|
2468
|
+
return false;
|
|
2469
|
+
}
|
|
2470
|
+
function requireVirtualParent(virtual, path, changeIndex) {
|
|
2471
|
+
if (virtual.get(dirname3(path))?.kind !== "directory") {
|
|
2472
|
+
throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
function simulateOperations(parsed, snapshots) {
|
|
2476
|
+
const virtual = new Map(snapshots);
|
|
2477
|
+
const planned = [];
|
|
2478
|
+
const failures = [];
|
|
2479
|
+
for (const operation of parsed) {
|
|
2480
|
+
try {
|
|
2481
|
+
planned.push(simulateOperation(operation, virtual));
|
|
2482
|
+
} catch (error) {
|
|
2483
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2484
|
+
failures.push({ changeIndex: operation.changeIndex, message: error.detail });
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
throw error;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
return { operations: planned, failures };
|
|
2491
|
+
}
|
|
2492
|
+
function simulateOperation(operation, virtual) {
|
|
2493
|
+
switch (operation.kind) {
|
|
2494
|
+
case "text":
|
|
2495
|
+
return simulateText(operation, virtual);
|
|
2496
|
+
case "create":
|
|
2497
|
+
return simulateCreate(operation, virtual);
|
|
2498
|
+
case "rename":
|
|
2499
|
+
return simulateRename(operation, virtual);
|
|
2500
|
+
case "delete":
|
|
2501
|
+
return simulateDelete(operation, virtual);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
function rejectSymbolicLink(operation) {
|
|
2505
|
+
if (operation.followedSymbolicLink) {
|
|
2506
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
function simulateText(operation, virtual) {
|
|
2510
|
+
const entry = virtual.get(operation.path);
|
|
2511
|
+
if (entry?.kind !== "file")
|
|
2512
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
|
|
2513
|
+
const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
|
|
2514
|
+
virtual.set(operation.path, { kind: "file", content: normalized.text });
|
|
2515
|
+
return {
|
|
2516
|
+
kind: "text",
|
|
2517
|
+
changeIndex: operation.changeIndex,
|
|
2518
|
+
path: operation.path,
|
|
2519
|
+
beforeText: entry.content,
|
|
2520
|
+
afterText: normalized.text,
|
|
2521
|
+
editCount: normalized.edits.length,
|
|
2522
|
+
documentVersion: operation.version
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
function simulateCreate(operation, virtual) {
|
|
2526
|
+
rejectSymbolicLink(operation);
|
|
2527
|
+
requireVirtualParent(virtual, operation.path, operation.changeIndex);
|
|
2528
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2529
|
+
if (target.kind !== "missing") {
|
|
2530
|
+
if (operation.overwrite && target.kind === "file") {
|
|
2531
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2532
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
|
|
2533
|
+
}
|
|
2534
|
+
if (operation.ignoreIfExists)
|
|
2535
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2536
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
|
|
2537
|
+
}
|
|
2538
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2539
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
|
|
2540
|
+
}
|
|
2541
|
+
function simulateRename(operation, virtual) {
|
|
2542
|
+
rejectSymbolicLink(operation);
|
|
2543
|
+
const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
|
|
2544
|
+
if (source.kind === "missing") {
|
|
2545
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
|
|
2546
|
+
}
|
|
2547
|
+
if (operation.oldPath === operation.newPath)
|
|
2548
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2549
|
+
if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
|
|
2550
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
|
|
2551
|
+
}
|
|
2552
|
+
requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
|
|
2553
|
+
const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
|
|
2554
|
+
if (destination.kind !== "missing" && !operation.overwrite) {
|
|
2555
|
+
if (operation.ignoreIfExists)
|
|
2556
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2557
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
|
|
2558
|
+
}
|
|
2559
|
+
moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
|
|
2560
|
+
return {
|
|
2561
|
+
kind: "rename",
|
|
2562
|
+
changeIndex: operation.changeIndex,
|
|
2563
|
+
oldPath: operation.oldPath,
|
|
2564
|
+
newPath: operation.newPath,
|
|
2565
|
+
sourceKind: source.kind,
|
|
2566
|
+
replaceDestination: destination.kind !== "missing"
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
function simulateDelete(operation, virtual) {
|
|
2570
|
+
rejectSymbolicLink(operation);
|
|
2571
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2572
|
+
if (target.kind === "missing") {
|
|
2573
|
+
if (operation.ignoreIfNotExists)
|
|
2574
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2575
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
|
|
2576
|
+
}
|
|
2577
|
+
if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
|
|
2578
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
|
|
2579
|
+
}
|
|
2580
|
+
removeVirtualSubtree(virtual, operation.path);
|
|
2581
|
+
return {
|
|
2582
|
+
kind: "delete",
|
|
2583
|
+
changeIndex: operation.changeIndex,
|
|
2584
|
+
path: operation.path,
|
|
2585
|
+
targetKind: target.kind,
|
|
2586
|
+
recursive: operation.recursive
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// ../lsp-core/src/lsp/workspace-edit-snapshot.ts
|
|
2591
|
+
import { existsSync as existsSync5, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2592
|
+
import { dirname as dirname4, resolve as resolve5 } from "node:path";
|
|
2593
|
+
class WorkspaceSnapshotBuilder {
|
|
2594
|
+
workspaceRoot;
|
|
2595
|
+
snapshots = new Map;
|
|
2596
|
+
constructor(workspaceRoot) {
|
|
2597
|
+
this.workspaceRoot = workspaceRoot;
|
|
2598
|
+
}
|
|
2599
|
+
build(operations) {
|
|
2600
|
+
this.add(this.workspaceRoot, false);
|
|
2601
|
+
for (const operation of operations) {
|
|
2602
|
+
switch (operation.kind) {
|
|
2603
|
+
case "rename":
|
|
2604
|
+
this.add(operation.oldPath, true);
|
|
2605
|
+
this.add(operation.newPath, true);
|
|
2606
|
+
break;
|
|
2607
|
+
case "delete":
|
|
2608
|
+
this.add(operation.path, true);
|
|
2609
|
+
break;
|
|
2610
|
+
case "text":
|
|
2611
|
+
case "create":
|
|
2612
|
+
this.add(operation.path, false);
|
|
2613
|
+
break;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
return this.snapshots;
|
|
2617
|
+
}
|
|
2618
|
+
add(path, includeChildren) {
|
|
2619
|
+
let candidate = path;
|
|
2620
|
+
while (true) {
|
|
2621
|
+
const existing = this.snapshots.get(candidate);
|
|
2622
|
+
if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
|
|
2623
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
|
|
2624
|
+
}
|
|
2625
|
+
if (candidate === this.workspaceRoot)
|
|
2626
|
+
break;
|
|
2627
|
+
candidate = dirname4(candidate);
|
|
2628
|
+
}
|
|
2629
|
+
if (!includeChildren || !existsSync5(path) || !lstatSync3(path).isDirectory())
|
|
2630
|
+
return;
|
|
2631
|
+
for (const child of readdirSync2(path))
|
|
2632
|
+
this.add(resolve5(path, child), true);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
function snapshotOperations(operations, workspaceRoot) {
|
|
2636
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// ../lsp-core/src/lsp/workspace-edit-plan.ts
|
|
2640
|
+
class PlanPathIndex {
|
|
2641
|
+
firstChangeByPath = new Map;
|
|
2642
|
+
reportedPathByCanonical = new Map;
|
|
2643
|
+
build(operations) {
|
|
2644
|
+
for (const operation of operations) {
|
|
2645
|
+
switch (operation.kind) {
|
|
2646
|
+
case "rename":
|
|
2647
|
+
this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
|
|
2648
|
+
this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
|
|
2649
|
+
break;
|
|
2650
|
+
case "text":
|
|
2651
|
+
case "create":
|
|
2652
|
+
case "delete":
|
|
2653
|
+
this.add(operation.path, operation.reportedPath, operation.changeIndex);
|
|
2654
|
+
break;
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
add(path, reportedPath2, changeIndex) {
|
|
2659
|
+
if (!this.firstChangeByPath.has(path))
|
|
2660
|
+
this.firstChangeByPath.set(path, changeIndex);
|
|
2661
|
+
if (!this.reportedPathByCanonical.has(path))
|
|
2662
|
+
this.reportedPathByCanonical.set(path, reportedPath2);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
function fingerprintWorkspaceEdit(edit, workspaceRoot) {
|
|
2666
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2667
|
+
if (!root.success)
|
|
2668
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2669
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2670
|
+
if (parsed.failures.length > 0)
|
|
2671
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2672
|
+
return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
|
|
2673
|
+
}
|
|
2674
|
+
function planWorkspaceEdit(edit, workspaceRoot) {
|
|
2675
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2676
|
+
if (!root.success)
|
|
2677
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2678
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2679
|
+
if (parsed.failures.length > 0)
|
|
2680
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2681
|
+
let snapshots;
|
|
2682
|
+
try {
|
|
2683
|
+
snapshots = snapshotOperations(parsed.operations, root.path);
|
|
2684
|
+
} catch (error) {
|
|
2685
|
+
return {
|
|
2686
|
+
success: false,
|
|
2687
|
+
result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
const simulated = simulateOperations(parsed.operations, snapshots);
|
|
2691
|
+
if (simulated.failures.length > 0)
|
|
2692
|
+
return { success: false, result: failureResult(simulated.failures) };
|
|
2693
|
+
const paths = new PlanPathIndex;
|
|
2694
|
+
paths.build(parsed.operations);
|
|
2695
|
+
const plan = {
|
|
2696
|
+
workspaceRoot: root.path,
|
|
2697
|
+
operations: simulated.operations,
|
|
2698
|
+
snapshots,
|
|
2699
|
+
firstChangeByPath: paths.firstChangeByPath,
|
|
2700
|
+
reportedPathByCanonical: paths.reportedPathByCanonical,
|
|
2701
|
+
fingerprint: canonicalFingerprint(parsed.operations)
|
|
2702
|
+
};
|
|
2703
|
+
return { success: true, plan };
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// ../lsp-core/src/lsp/workspace-mutation-controller.ts
|
|
2707
|
+
function failure(message, failedChange, base) {
|
|
2708
|
+
return {
|
|
2709
|
+
success: false,
|
|
2710
|
+
filesModified: base?.filesModified ?? [],
|
|
2711
|
+
totalEdits: base?.totalEdits ?? 0,
|
|
2712
|
+
errors: [message],
|
|
2713
|
+
...failedChange === undefined ? {} : { failedChange },
|
|
2714
|
+
...base?.lateAbort ? { lateAbort: true } : {}
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
function responseFor(result) {
|
|
2718
|
+
if (result.success)
|
|
2719
|
+
return { applied: true };
|
|
2720
|
+
return {
|
|
2721
|
+
applied: false,
|
|
2722
|
+
failureReason: result.errors[0] ?? "workspace edit failed",
|
|
2723
|
+
...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
function isRecord4(value) {
|
|
2727
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
class WorkspaceMutationController {
|
|
2731
|
+
workspaceRoot;
|
|
2732
|
+
documents;
|
|
2733
|
+
activeLease = null;
|
|
2734
|
+
nextLeaseId = 1;
|
|
2735
|
+
io;
|
|
2736
|
+
constructor(workspaceRoot, documents) {
|
|
2737
|
+
this.workspaceRoot = workspaceRoot;
|
|
2738
|
+
this.documents = documents;
|
|
2739
|
+
}
|
|
2740
|
+
setIo(io) {
|
|
2741
|
+
this.io = io;
|
|
2742
|
+
}
|
|
2743
|
+
acquire(signal) {
|
|
2744
|
+
if (this.activeLease)
|
|
2745
|
+
return { success: false, result: failure("workspace mutation is already in progress") };
|
|
2746
|
+
if (signal?.aborted)
|
|
2747
|
+
return { success: false, result: failure("cancelled before mutating request") };
|
|
2748
|
+
const lease = {
|
|
2749
|
+
id: this.nextLeaseId,
|
|
2750
|
+
phase: "idle",
|
|
2751
|
+
...signal === undefined ? {} : { signal }
|
|
2752
|
+
};
|
|
2753
|
+
this.nextLeaseId += 1;
|
|
2754
|
+
this.activeLease = lease;
|
|
2755
|
+
return { success: true, lease };
|
|
2756
|
+
}
|
|
2757
|
+
release(lease) {
|
|
2758
|
+
if (this.activeLease?.id !== lease.id)
|
|
2759
|
+
return;
|
|
2760
|
+
this.activeLease.phase = "sealed";
|
|
2761
|
+
this.activeLease = null;
|
|
2762
|
+
}
|
|
2763
|
+
isBeforeCommit(lease) {
|
|
2764
|
+
return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
|
|
2765
|
+
}
|
|
2766
|
+
async handleApplyEdit(params) {
|
|
2767
|
+
const lease = this.activeLease;
|
|
2768
|
+
if (!lease)
|
|
2769
|
+
return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
|
|
2770
|
+
if (lease.phase !== "idle") {
|
|
2771
|
+
return {
|
|
2772
|
+
applied: false,
|
|
2773
|
+
failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
lease.phase = "applying";
|
|
2777
|
+
lease.applyCompletion = new Promise((resolve6) => {
|
|
2778
|
+
lease.resolveApply = resolve6;
|
|
2779
|
+
});
|
|
2780
|
+
const edit = isRecord4(params) ? params["edit"] : undefined;
|
|
2781
|
+
const record2 = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
|
|
2782
|
+
lease.serverApply = record2;
|
|
2783
|
+
lease.phase = "settled";
|
|
2784
|
+
lease.resolveApply?.();
|
|
2785
|
+
return responseFor(record2.result);
|
|
2786
|
+
}
|
|
2787
|
+
async reconcileRename(leaseToken, edit) {
|
|
2788
|
+
const lease = this.requireActiveLease(leaseToken);
|
|
2789
|
+
if (!lease)
|
|
2790
|
+
return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
|
|
2791
|
+
if (lease.phase === "applying")
|
|
2792
|
+
await lease.applyCompletion;
|
|
2793
|
+
if (lease.serverApply)
|
|
2794
|
+
return this.reconcileServerApply(lease.serverApply, edit);
|
|
2795
|
+
lease.phase = "sealed";
|
|
2796
|
+
if (!edit)
|
|
2797
|
+
return { edit, apply: failure("No edit provided") };
|
|
2798
|
+
const applied = await this.applyEdit(edit, lease);
|
|
2799
|
+
return { edit, apply: applied.result };
|
|
2800
|
+
}
|
|
2801
|
+
reconcileServerApply(record2, edit) {
|
|
2802
|
+
if (!edit)
|
|
2803
|
+
return { edit, apply: record2.result };
|
|
2804
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
|
|
2805
|
+
if (fingerprint.success && record2.fingerprint !== null && fingerprint.fingerprint === record2.fingerprint) {
|
|
2806
|
+
return { edit, apply: record2.result };
|
|
2807
|
+
}
|
|
2808
|
+
return {
|
|
2809
|
+
edit,
|
|
2810
|
+
apply: failure("rename result conflicts with server-applied workspace edit", 0, record2.result)
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
async applyEdit(edit, lease) {
|
|
2814
|
+
const planned = planWorkspaceEdit(edit, this.workspaceRoot);
|
|
2815
|
+
if (!planned.success)
|
|
2816
|
+
return { fingerprint: null, result: planned.result };
|
|
2817
|
+
const versionFailure = this.documents.validateVersions(planned.plan.operations);
|
|
2818
|
+
if (versionFailure) {
|
|
2819
|
+
return {
|
|
2820
|
+
fingerprint: planned.plan.fingerprint,
|
|
2821
|
+
result: failure(versionFailure.message, versionFailure.changeIndex)
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
const commit = commitWorkspaceEditPlan(planned.plan, {
|
|
2825
|
+
...lease.signal === undefined ? {} : { signal: lease.signal },
|
|
2826
|
+
...this.io === undefined ? {} : { io: this.io }
|
|
2827
|
+
});
|
|
2828
|
+
let result = commit.result;
|
|
2829
|
+
if (commit.delta.operations.length > 0) {
|
|
2830
|
+
try {
|
|
2831
|
+
await this.documents.synchronize(commit.delta);
|
|
2832
|
+
} catch (error) {
|
|
2833
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2834
|
+
result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
if (lease.signal?.aborted && !result.lateAbort)
|
|
2838
|
+
result = { ...result, lateAbort: true };
|
|
2839
|
+
return { fingerprint: planned.plan.fingerprint, result };
|
|
2840
|
+
}
|
|
2841
|
+
requireActiveLease(lease) {
|
|
2842
|
+
return this.activeLease?.id === lease.id ? this.activeLease : null;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
|
|
1203
2846
|
// ../lsp-core/src/lsp/client.ts
|
|
1204
|
-
var
|
|
1205
|
-
var
|
|
2847
|
+
var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
|
|
2848
|
+
var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1206
2849
|
|
|
1207
2850
|
class LspClient extends LspClientConnection {
|
|
1208
|
-
openedFiles = new Set;
|
|
1209
|
-
documentVersions = new Map;
|
|
1210
|
-
lastSyncedText = new Map;
|
|
1211
2851
|
diagnosticPullErrors = [];
|
|
2852
|
+
documents;
|
|
2853
|
+
workspaceMutations;
|
|
2854
|
+
diagnosticsFreshnessTimeoutMs;
|
|
2855
|
+
constructor(root, server2, options = {}) {
|
|
2856
|
+
super(root, server2, options);
|
|
2857
|
+
this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
|
|
2858
|
+
this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
|
|
2859
|
+
versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
|
|
2860
|
+
});
|
|
2861
|
+
this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
|
|
2862
|
+
this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
|
|
2863
|
+
}
|
|
1212
2864
|
getDiagnosticPullErrors() {
|
|
1213
2865
|
return this.diagnosticPullErrors;
|
|
1214
2866
|
}
|
|
1215
2867
|
async openFile(filePath) {
|
|
1216
|
-
const absPath =
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
version,
|
|
1228
|
-
text
|
|
1229
|
-
}
|
|
1230
|
-
});
|
|
1231
|
-
this.openedFiles.add(absPath);
|
|
1232
|
-
this.documentVersions.set(uri, version);
|
|
1233
|
-
this.lastSyncedText.set(uri, text);
|
|
1234
|
-
await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
const prevText = this.lastSyncedText.get(uri);
|
|
1238
|
-
if (prevText === text) {
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
|
|
1242
|
-
this.documentVersions.set(uri, nextVersion);
|
|
1243
|
-
this.lastSyncedText.set(uri, text);
|
|
1244
|
-
await this.sendNotification("textDocument/didChange", {
|
|
1245
|
-
textDocument: { uri, version: nextVersion },
|
|
1246
|
-
contentChanges: [{ text }]
|
|
1247
|
-
});
|
|
1248
|
-
await this.sendNotification("textDocument/didSave", {
|
|
1249
|
-
textDocument: { uri },
|
|
1250
|
-
text
|
|
1251
|
-
});
|
|
2868
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2869
|
+
await this.documents.openFile(absPath);
|
|
2870
|
+
}
|
|
2871
|
+
getOpenDocumentVersion(filePath) {
|
|
2872
|
+
return this.documents.getVersion(this.resolveWorkspacePath(filePath));
|
|
2873
|
+
}
|
|
2874
|
+
getStoredDiagnostics(uri) {
|
|
2875
|
+
return [...this.documents.getStoredDiagnostics(uri)];
|
|
2876
|
+
}
|
|
2877
|
+
setWorkspaceEditIo(io) {
|
|
2878
|
+
this.workspaceMutations.setIo(io);
|
|
1252
2879
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
2880
|
+
handlePublishDiagnostics(params) {
|
|
2881
|
+
super.handlePublishDiagnostics(params);
|
|
2882
|
+
this.documents.recordPublishedDiagnostics(params);
|
|
2883
|
+
}
|
|
2884
|
+
async definition(filePath, line, character, signal) {
|
|
2885
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1255
2886
|
await this.openFile(absPath);
|
|
2887
|
+
const options = signal === undefined ? {} : { signal };
|
|
1256
2888
|
return this.sendRequest("textDocument/definition", {
|
|
1257
|
-
textDocument: { uri:
|
|
2889
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1258
2890
|
position: { line: line - 1, character }
|
|
1259
|
-
});
|
|
2891
|
+
}, options);
|
|
1260
2892
|
}
|
|
1261
|
-
async references(filePath, line, character, includeDeclaration = true) {
|
|
1262
|
-
const absPath =
|
|
2893
|
+
async references(filePath, line, character, includeDeclaration = true, signal) {
|
|
2894
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1263
2895
|
await this.openFile(absPath);
|
|
2896
|
+
const options = signal === undefined ? {} : { signal };
|
|
1264
2897
|
return this.sendRequest("textDocument/references", {
|
|
1265
|
-
textDocument: { uri:
|
|
2898
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1266
2899
|
position: { line: line - 1, character },
|
|
1267
2900
|
context: { includeDeclaration }
|
|
1268
|
-
});
|
|
2901
|
+
}, options);
|
|
1269
2902
|
}
|
|
1270
|
-
async documentSymbols(filePath) {
|
|
1271
|
-
const absPath =
|
|
2903
|
+
async documentSymbols(filePath, signal) {
|
|
2904
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1272
2905
|
await this.openFile(absPath);
|
|
2906
|
+
const options = signal === undefined ? {} : { signal };
|
|
1273
2907
|
return this.sendRequest("textDocument/documentSymbol", {
|
|
1274
|
-
textDocument: { uri:
|
|
1275
|
-
});
|
|
2908
|
+
textDocument: { uri: pathToFileURL3(absPath).href }
|
|
2909
|
+
}, options);
|
|
1276
2910
|
}
|
|
1277
|
-
async workspaceSymbols(query) {
|
|
1278
|
-
|
|
2911
|
+
async workspaceSymbols(query, signal) {
|
|
2912
|
+
const options = signal === undefined ? {} : { signal };
|
|
2913
|
+
return this.sendRequest("workspace/symbol", { query }, options);
|
|
1279
2914
|
}
|
|
1280
2915
|
isUnsupportedDiagnosticPullError(error) {
|
|
1281
2916
|
if (!(error instanceof Error))
|
|
@@ -1285,42 +2920,173 @@ class LspClient extends LspClientConnection {
|
|
|
1285
2920
|
return true;
|
|
1286
2921
|
return /unsupported|not supported|method not found|unknown request/i.test(error.message);
|
|
1287
2922
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
1295
|
-
textDocument: { uri }
|
|
1296
|
-
});
|
|
1297
|
-
if (result.items) {
|
|
1298
|
-
return { items: result.items };
|
|
2923
|
+
freshnessTimeout(absPath) {
|
|
2924
|
+
return {
|
|
2925
|
+
items: [],
|
|
2926
|
+
transientError: {
|
|
2927
|
+
kind: "freshness_timeout",
|
|
2928
|
+
message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
|
|
1299
2929
|
}
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
parseDiagnosticPullReport(value) {
|
|
2933
|
+
if (value.kind === "unchanged") {
|
|
2934
|
+
return {
|
|
2935
|
+
type: "unchanged",
|
|
2936
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2937
|
+
};
|
|
2938
|
+
}
|
|
2939
|
+
return {
|
|
2940
|
+
type: "full",
|
|
2941
|
+
diagnostics: value.items ?? [],
|
|
2942
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
async diagnostics(filePath, signal) {
|
|
2946
|
+
signal?.throwIfAborted();
|
|
2947
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2948
|
+
const uri = pathToFileURL3(absPath).href;
|
|
2949
|
+
await this.openFile(absPath);
|
|
2950
|
+
const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
|
|
2951
|
+
for (;; ) {
|
|
2952
|
+
signal?.throwIfAborted();
|
|
2953
|
+
const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
|
|
2954
|
+
if (!snapshot)
|
|
2955
|
+
return this.freshnessTimeout(absPath);
|
|
2956
|
+
const push = this.documents.resolvePushDiagnostics(snapshot);
|
|
2957
|
+
if (push.status === "ready")
|
|
2958
|
+
return { items: [...push.diagnostics] };
|
|
2959
|
+
let pushFallbackOnly = !this.isDiagnosticPullSupported();
|
|
2960
|
+
if (!pushFallbackOnly) {
|
|
2961
|
+
const cached = this.documents.getPullCache(snapshot);
|
|
2962
|
+
try {
|
|
2963
|
+
const remainingMs2 = deadlineAt - Date.now();
|
|
2964
|
+
if (remainingMs2 <= 0)
|
|
2965
|
+
return this.freshnessTimeout(absPath);
|
|
2966
|
+
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
2967
|
+
textDocument: { uri },
|
|
2968
|
+
...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
|
|
2969
|
+
}, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
|
|
2970
|
+
if (!this.documents.isCurrentSnapshot(snapshot))
|
|
2971
|
+
continue;
|
|
2972
|
+
const report = this.parseDiagnosticPullReport(result);
|
|
2973
|
+
if (report.type === "full") {
|
|
2974
|
+
this.documents.recordPullDiagnostics(snapshot, {
|
|
2975
|
+
kind: "full",
|
|
2976
|
+
diagnostics: report.diagnostics,
|
|
2977
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
2978
|
+
});
|
|
2979
|
+
return { items: [...report.diagnostics] };
|
|
2980
|
+
}
|
|
2981
|
+
if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
|
|
2982
|
+
return { items: [...cached.diagnostics] };
|
|
2983
|
+
}
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
if (this.isUnsupportedDiagnosticPullError(error)) {
|
|
2986
|
+
this.setDiagnosticPullSupported(false);
|
|
2987
|
+
pushFallbackOnly = true;
|
|
2988
|
+
} else if (error instanceof LspRequestTimeoutError) {
|
|
2989
|
+
pushFallbackOnly = true;
|
|
2990
|
+
} else {
|
|
2991
|
+
this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
2992
|
+
throw error;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
1303
2995
|
}
|
|
2996
|
+
if (!pushFallbackOnly)
|
|
2997
|
+
continue;
|
|
2998
|
+
const remainingMs = deadlineAt - Date.now();
|
|
2999
|
+
if (remainingMs <= 0)
|
|
3000
|
+
return this.freshnessTimeout(absPath);
|
|
3001
|
+
const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
|
|
3002
|
+
await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
|
|
1304
3003
|
}
|
|
1305
|
-
return { items: this.getStoredDiagnostics(uri) };
|
|
1306
3004
|
}
|
|
1307
|
-
async prepareRename(filePath, line, character) {
|
|
1308
|
-
const absPath =
|
|
3005
|
+
async prepareRename(filePath, line, character, signal) {
|
|
3006
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1309
3007
|
await this.openFile(absPath);
|
|
3008
|
+
const options = signal === undefined ? {} : { signal };
|
|
1310
3009
|
return this.sendRequest("textDocument/prepareRename", {
|
|
1311
|
-
textDocument: { uri:
|
|
3010
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
1312
3011
|
position: { line: line - 1, character }
|
|
1313
|
-
});
|
|
3012
|
+
}, options);
|
|
1314
3013
|
}
|
|
1315
|
-
async rename(filePath, line, character, newName) {
|
|
1316
|
-
const absPath =
|
|
3014
|
+
async rename(filePath, line, character, newName, signal) {
|
|
3015
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
1317
3016
|
await this.openFile(absPath);
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
3017
|
+
const acquired = this.workspaceMutations.acquire(signal);
|
|
3018
|
+
if (!acquired.success)
|
|
3019
|
+
return { edit: null, apply: acquired.result };
|
|
3020
|
+
const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
|
|
3021
|
+
try {
|
|
3022
|
+
const renameParams = {
|
|
3023
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
3024
|
+
position: { line: line - 1, character },
|
|
3025
|
+
newName
|
|
3026
|
+
};
|
|
3027
|
+
const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
|
|
3028
|
+
signal: preCommitSignal.signal
|
|
3029
|
+
});
|
|
3030
|
+
return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
|
|
3031
|
+
} finally {
|
|
3032
|
+
preCommitSignal?.dispose();
|
|
3033
|
+
this.workspaceMutations.release(acquired.lease);
|
|
3034
|
+
}
|
|
1323
3035
|
}
|
|
3036
|
+
resolveWorkspacePath(filePath) {
|
|
3037
|
+
return resolve6(this.root, filePath);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
function waitForDiagnosticsActivity(wait, signal) {
|
|
3041
|
+
if (!signal)
|
|
3042
|
+
return wait;
|
|
3043
|
+
if (signal.aborted)
|
|
3044
|
+
return Promise.reject(abortError2(signal));
|
|
3045
|
+
return new Promise((resolve7, reject) => {
|
|
3046
|
+
const onAbort = () => {
|
|
3047
|
+
signal.removeEventListener("abort", onAbort);
|
|
3048
|
+
reject(abortError2(signal));
|
|
3049
|
+
};
|
|
3050
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3051
|
+
wait.then(() => {
|
|
3052
|
+
signal.removeEventListener("abort", onAbort);
|
|
3053
|
+
resolve7();
|
|
3054
|
+
}, (error) => {
|
|
3055
|
+
signal.removeEventListener("abort", onAbort);
|
|
3056
|
+
reject(error);
|
|
3057
|
+
});
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
function createPreCommitAbortSignal(source, isBeforeCommit) {
|
|
3061
|
+
if (!source)
|
|
3062
|
+
return;
|
|
3063
|
+
const controller = new AbortController;
|
|
3064
|
+
const onAbort = () => {
|
|
3065
|
+
if (isBeforeCommit() && !controller.signal.aborted)
|
|
3066
|
+
controller.abort(preCommitAbortReason(source));
|
|
3067
|
+
};
|
|
3068
|
+
if (source.aborted)
|
|
3069
|
+
onAbort();
|
|
3070
|
+
else
|
|
3071
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
3072
|
+
return {
|
|
3073
|
+
signal: controller.signal,
|
|
3074
|
+
dispose: () => source.removeEventListener("abort", onAbort)
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
function preCommitAbortReason(source) {
|
|
3078
|
+
const reason = source.reason;
|
|
3079
|
+
if (reason instanceof Error && reason.name !== "AbortError")
|
|
3080
|
+
return reason;
|
|
3081
|
+
return new Error("LSP request cancelled before workspace edit commit");
|
|
3082
|
+
}
|
|
3083
|
+
function abortError2(signal) {
|
|
3084
|
+
const reason = signal.reason;
|
|
3085
|
+
if (reason instanceof Error)
|
|
3086
|
+
return reason;
|
|
3087
|
+
const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
|
|
3088
|
+
error.name = "AbortError";
|
|
3089
|
+
return error;
|
|
1324
3090
|
}
|
|
1325
3091
|
|
|
1326
3092
|
// ../lsp-core/src/lsp/process-signal-cleanup.ts
|
|
@@ -1352,7 +3118,7 @@ async function stopClientBestEffort(client) {
|
|
|
1352
3118
|
function awaitWithSignal(promise, signal) {
|
|
1353
3119
|
if (!signal)
|
|
1354
3120
|
return promise;
|
|
1355
|
-
return new Promise((
|
|
3121
|
+
return new Promise((resolve7, reject) => {
|
|
1356
3122
|
let settled = false;
|
|
1357
3123
|
const onAbort = () => {
|
|
1358
3124
|
if (settled)
|
|
@@ -1370,7 +3136,7 @@ function awaitWithSignal(promise, signal) {
|
|
|
1370
3136
|
return;
|
|
1371
3137
|
settled = true;
|
|
1372
3138
|
signal.removeEventListener("abort", onAbort);
|
|
1373
|
-
|
|
3139
|
+
resolve7(value);
|
|
1374
3140
|
}, (err) => {
|
|
1375
3141
|
if (settled)
|
|
1376
3142
|
return;
|
|
@@ -1624,21 +3390,17 @@ async function disposeDefaultLspManager() {
|
|
|
1624
3390
|
}
|
|
1625
3391
|
|
|
1626
3392
|
// ../lsp-core/src/lsp/server-install-state.ts
|
|
1627
|
-
import { existsSync as
|
|
1628
|
-
import {
|
|
1629
|
-
import { dirname, isAbsolute, join as join2 } from "node:path";
|
|
3393
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3394
|
+
import { dirname as dirname5 } from "node:path";
|
|
1630
3395
|
function getInstallDecisionsPath() {
|
|
1631
|
-
|
|
1632
|
-
if (!override)
|
|
1633
|
-
return join2(homedir(), ".codex", "lsp-install-decisions.json");
|
|
1634
|
-
return isAbsolute(override) ? override : join2(homedir(), override);
|
|
3396
|
+
return lspRequestContext().installDecisionsPath;
|
|
1635
3397
|
}
|
|
1636
3398
|
function loadInstallDecisions() {
|
|
1637
3399
|
const path = getInstallDecisionsPath();
|
|
1638
|
-
if (!
|
|
3400
|
+
if (!existsSync6(path))
|
|
1639
3401
|
return {};
|
|
1640
3402
|
try {
|
|
1641
|
-
const parsed = JSON.parse(
|
|
3403
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1642
3404
|
return isInstallDecisions(parsed) ? parsed : {};
|
|
1643
3405
|
} catch {
|
|
1644
3406
|
return {};
|
|
@@ -1657,28 +3419,26 @@ function isInstallDecision(value) {
|
|
|
1657
3419
|
}
|
|
1658
3420
|
function writeInstallDecisions(decisions) {
|
|
1659
3421
|
const path = getInstallDecisionsPath();
|
|
1660
|
-
mkdirSync(
|
|
3422
|
+
mkdirSync(dirname5(path), { recursive: true });
|
|
1661
3423
|
const tmpPath = `${path}.tmp`;
|
|
1662
|
-
|
|
3424
|
+
writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
|
|
1663
3425
|
`, "utf8");
|
|
1664
|
-
|
|
3426
|
+
renameSync2(tmpPath, path);
|
|
1665
3427
|
}
|
|
1666
3428
|
function isInstallDecisions(value) {
|
|
1667
|
-
return
|
|
3429
|
+
return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
|
|
1668
3430
|
}
|
|
1669
3431
|
function isInstallDecisionRecord(value) {
|
|
1670
|
-
if (!
|
|
3432
|
+
if (!isRecord5(value))
|
|
1671
3433
|
return false;
|
|
1672
3434
|
return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
|
|
1673
3435
|
}
|
|
1674
|
-
function
|
|
3436
|
+
function isRecord5(value) {
|
|
1675
3437
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1676
3438
|
}
|
|
1677
3439
|
|
|
1678
3440
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1679
|
-
import { existsSync as
|
|
1680
|
-
import { homedir as homedir2 } from "node:os";
|
|
1681
|
-
import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
3441
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
1682
3442
|
|
|
1683
3443
|
// ../lsp-core/src/lsp/server-definitions.ts
|
|
1684
3444
|
var LSP_INSTALL_HINTS = {
|
|
@@ -1828,27 +3588,17 @@ var BUILTIN_SERVERS = {
|
|
|
1828
3588
|
};
|
|
1829
3589
|
|
|
1830
3590
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1831
|
-
function resolveProjectConfigPath(path) {
|
|
1832
|
-
return isAbsolute2(path) ? path : join3(contextCwd(), path);
|
|
1833
|
-
}
|
|
1834
3591
|
function getProjectConfigPaths() {
|
|
1835
|
-
|
|
1836
|
-
if (projectOverride) {
|
|
1837
|
-
return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
|
|
1838
|
-
}
|
|
1839
|
-
return [join3(contextCwd(), ".codex", "lsp-client.json")];
|
|
3592
|
+
return lspRequestContext().projectConfigPaths;
|
|
1840
3593
|
}
|
|
1841
3594
|
function getUserConfigPath() {
|
|
1842
|
-
|
|
1843
|
-
if (!userOverride)
|
|
1844
|
-
return join3(homedir2(), ".codex", "lsp-client.json");
|
|
1845
|
-
return isAbsolute2(userOverride) ? userOverride : join3(homedir2(), userOverride);
|
|
3595
|
+
return lspRequestContext().userConfigPath;
|
|
1846
3596
|
}
|
|
1847
3597
|
function loadJsonFile(path) {
|
|
1848
|
-
if (!
|
|
3598
|
+
if (!existsSync7(path))
|
|
1849
3599
|
return null;
|
|
1850
3600
|
try {
|
|
1851
|
-
const parsed = JSON.parse(
|
|
3601
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1852
3602
|
return isConfigJson(parsed) ? parsed : null;
|
|
1853
3603
|
} catch {
|
|
1854
3604
|
return null;
|
|
@@ -1987,16 +3737,16 @@ function applyOptionalServerFields(server2, entry) {
|
|
|
1987
3737
|
}
|
|
1988
3738
|
}
|
|
1989
3739
|
function isConfigJson(value) {
|
|
1990
|
-
if (!
|
|
3740
|
+
if (!isRecord6(value))
|
|
1991
3741
|
return false;
|
|
1992
3742
|
const lsp = value["lsp"];
|
|
1993
|
-
return lsp === undefined ||
|
|
3743
|
+
return lsp === undefined || isRecord6(lsp);
|
|
1994
3744
|
}
|
|
1995
3745
|
function parseLspEntry(value) {
|
|
1996
3746
|
return isLspEntry(value) ? value : null;
|
|
1997
3747
|
}
|
|
1998
3748
|
function isLspEntry(value) {
|
|
1999
|
-
if (!
|
|
3749
|
+
if (!isRecord6(value))
|
|
2000
3750
|
return false;
|
|
2001
3751
|
const disabled = value["disabled"];
|
|
2002
3752
|
const command = value["command"];
|
|
@@ -2004,15 +3754,15 @@ function isLspEntry(value) {
|
|
|
2004
3754
|
const priority = value["priority"];
|
|
2005
3755
|
const env = value["env"];
|
|
2006
3756
|
const initialization = value["initialization"];
|
|
2007
|
-
return (disabled === undefined || typeof disabled === "boolean") && (command === undefined || isStringArray(command)) && (extensions === undefined || isStringArray(extensions)) && (priority === undefined || typeof priority === "number") && (env === undefined || isStringRecord(env)) && (initialization === undefined ||
|
|
3757
|
+
return (disabled === undefined || typeof disabled === "boolean") && (command === undefined || isStringArray(command)) && (extensions === undefined || isStringArray(extensions)) && (priority === undefined || typeof priority === "number") && (env === undefined || isStringRecord(env)) && (initialization === undefined || isRecord6(initialization));
|
|
2008
3758
|
}
|
|
2009
3759
|
function isStringArray(value) {
|
|
2010
3760
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2011
3761
|
}
|
|
2012
3762
|
function isStringRecord(value) {
|
|
2013
|
-
return
|
|
3763
|
+
return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
|
|
2014
3764
|
}
|
|
2015
|
-
function
|
|
3765
|
+
function isRecord6(value) {
|
|
2016
3766
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2017
3767
|
}
|
|
2018
3768
|
function getDisabledServerIds() {
|
|
@@ -2033,8 +3783,8 @@ function getDisabledServerIds() {
|
|
|
2033
3783
|
}
|
|
2034
3784
|
|
|
2035
3785
|
// ../lsp-core/src/lsp/server-installation.ts
|
|
2036
|
-
import { existsSync as
|
|
2037
|
-
import { delimiter as delimiter3, join as
|
|
3786
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
3787
|
+
import { delimiter as delimiter3, join as join3 } from "node:path";
|
|
2038
3788
|
function isServerInstalled(command, _workingDirectory) {
|
|
2039
3789
|
if (command.length === 0)
|
|
2040
3790
|
return false;
|
|
@@ -2042,7 +3792,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2042
3792
|
if (!cmd)
|
|
2043
3793
|
return false;
|
|
2044
3794
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
2045
|
-
if (
|
|
3795
|
+
if (existsSync8(cmd))
|
|
2046
3796
|
return true;
|
|
2047
3797
|
}
|
|
2048
3798
|
const isWindows = process.platform === "win32";
|
|
@@ -2063,7 +3813,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2063
3813
|
const paths = pathEnv.split(delimiter3);
|
|
2064
3814
|
for (const p of paths) {
|
|
2065
3815
|
for (const suffix of exts) {
|
|
2066
|
-
if (
|
|
3816
|
+
if (existsSync8(join3(p, cmd + suffix))) {
|
|
2067
3817
|
return true;
|
|
2068
3818
|
}
|
|
2069
3819
|
}
|
|
@@ -2162,39 +3912,50 @@ function getAllServers() {
|
|
|
2162
3912
|
var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
2163
3913
|
function isDirectoryPath(filePath) {
|
|
2164
3914
|
try {
|
|
2165
|
-
return
|
|
3915
|
+
return statSync3(filePath).isDirectory();
|
|
2166
3916
|
} catch {
|
|
2167
3917
|
return false;
|
|
2168
3918
|
}
|
|
2169
3919
|
}
|
|
2170
3920
|
function findWorkspaceRoot(filePath) {
|
|
2171
|
-
const abs =
|
|
3921
|
+
const abs = resolvePathInsideContext(filePath);
|
|
2172
3922
|
let dir = abs;
|
|
2173
3923
|
if (!isDirectoryPath(dir)) {
|
|
2174
|
-
dir =
|
|
3924
|
+
dir = dirname6(dir);
|
|
2175
3925
|
}
|
|
2176
3926
|
let prevDir = "";
|
|
2177
3927
|
while (dir !== prevDir) {
|
|
2178
3928
|
for (const marker of WORKSPACE_MARKERS) {
|
|
2179
|
-
if (
|
|
3929
|
+
if (existsSync9(join4(dir, marker))) {
|
|
2180
3930
|
return dir;
|
|
2181
3931
|
}
|
|
2182
3932
|
}
|
|
2183
3933
|
prevDir = dir;
|
|
2184
|
-
dir =
|
|
3934
|
+
dir = dirname6(dir);
|
|
3935
|
+
}
|
|
3936
|
+
return dirname6(abs);
|
|
3937
|
+
}
|
|
3938
|
+
function resolvePathInsideContext(filePath) {
|
|
3939
|
+
const cwd = contextCwd();
|
|
3940
|
+
const abs = resolve7(cwd, filePath);
|
|
3941
|
+
const canonical = canonicalizeExistingOrNearestAncestor(abs);
|
|
3942
|
+
if (!isPathInside(cwd, canonical)) {
|
|
3943
|
+
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
2185
3944
|
}
|
|
2186
|
-
return
|
|
3945
|
+
return canonical;
|
|
2187
3946
|
}
|
|
2188
3947
|
function formatServerLookupError(result) {
|
|
2189
3948
|
if (result.status === "not_installed") {
|
|
2190
3949
|
return formatNotInstalled(result);
|
|
2191
3950
|
}
|
|
3951
|
+
const context = lspRequestContext();
|
|
3952
|
+
const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
|
|
2192
3953
|
return [
|
|
2193
3954
|
`No LSP server configured for extension: ${result.extension}`,
|
|
2194
3955
|
"",
|
|
2195
3956
|
`Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
|
|
2196
3957
|
"",
|
|
2197
|
-
|
|
3958
|
+
`Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
|
|
2198
3959
|
" {",
|
|
2199
3960
|
' "lsp": {',
|
|
2200
3961
|
' "my-server": {',
|
|
@@ -2210,6 +3971,7 @@ function formatNotInstalled(result) {
|
|
|
2210
3971
|
const { server: server2, installHint } = result;
|
|
2211
3972
|
const extensions = server2.extensions.join(", ");
|
|
2212
3973
|
const decision = loadInstallDecision(server2.id)?.decision;
|
|
3974
|
+
const context = lspRequestContext();
|
|
2213
3975
|
if (decision === "declined") {
|
|
2214
3976
|
return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
|
|
2215
3977
|
}
|
|
@@ -2225,6 +3987,17 @@ function formatNotInstalled(result) {
|
|
|
2225
3987
|
"The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
|
|
2226
3988
|
` ${installHint}`
|
|
2227
3989
|
].join(`
|
|
3990
|
+
`);
|
|
3991
|
+
}
|
|
3992
|
+
if (!context.capabilities.installDecisionTool) {
|
|
3993
|
+
return [
|
|
3994
|
+
...header,
|
|
3995
|
+
"To install, run:",
|
|
3996
|
+
` ${installHint}`,
|
|
3997
|
+
"",
|
|
3998
|
+
"ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
|
|
3999
|
+
"Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
|
|
4000
|
+
].join(`
|
|
2228
4001
|
`);
|
|
2229
4002
|
}
|
|
2230
4003
|
return [
|
|
@@ -2236,6 +4009,7 @@ function formatNotInstalled(result) {
|
|
|
2236
4009
|
"- If the user agrees: run the install command above, then retry this tool.",
|
|
2237
4010
|
"- If the user declines, OR has NOT explicitly asked for LSP installation:",
|
|
2238
4011
|
` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
|
|
4012
|
+
` which writes to ${context.installDecisionsPath},`,
|
|
2239
4013
|
" then ignore this message and proceed WITHOUT LSP."
|
|
2240
4014
|
].join(`
|
|
2241
4015
|
`);
|
|
@@ -2249,14 +4023,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
|
|
|
2249
4023
|
"prepareRename"
|
|
2250
4024
|
]);
|
|
2251
4025
|
async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
2252
|
-
const absPath =
|
|
4026
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2253
4027
|
if (isDirectoryPath(absPath)) {
|
|
2254
4028
|
throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
|
|
2255
4029
|
}
|
|
2256
4030
|
const ext = effectiveExtension(absPath);
|
|
2257
4031
|
const result = findServerForExtension(ext);
|
|
2258
4032
|
if (result.status !== "found") {
|
|
2259
|
-
throw new LspServerLookupError(formatServerLookupError(result));
|
|
4033
|
+
throw new LspServerLookupError(formatServerLookupError(result), result);
|
|
2260
4034
|
}
|
|
2261
4035
|
const server2 = result.server;
|
|
2262
4036
|
const root = findWorkspaceRoot(absPath);
|
|
@@ -2284,11 +4058,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
|
2284
4058
|
}
|
|
2285
4059
|
|
|
2286
4060
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2287
|
-
import { existsSync as
|
|
2288
|
-
import { join as
|
|
4061
|
+
import { existsSync as existsSync10, lstatSync as lstatSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
4062
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
2289
4063
|
|
|
2290
4064
|
// ../lsp-core/src/lsp/formatters.ts
|
|
2291
|
-
import { fileURLToPath } from "node:url";
|
|
4065
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2292
4066
|
var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
2293
4067
|
error: 1,
|
|
2294
4068
|
warning: 2,
|
|
@@ -2296,7 +4070,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
|
2296
4070
|
hint: 4
|
|
2297
4071
|
};
|
|
2298
4072
|
function uriToPath(uri) {
|
|
2299
|
-
return
|
|
4073
|
+
return fileURLToPath2(uri);
|
|
2300
4074
|
}
|
|
2301
4075
|
function formatLocation(loc) {
|
|
2302
4076
|
if ("targetUri" in loc) {
|
|
@@ -2382,6 +4156,9 @@ function formatApplyResult(result) {
|
|
|
2382
4156
|
for (const file of result.filesModified) {
|
|
2383
4157
|
lines.push(` - ${file}`);
|
|
2384
4158
|
}
|
|
4159
|
+
if (result.lateAbort) {
|
|
4160
|
+
lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
|
|
4161
|
+
}
|
|
2385
4162
|
} else {
|
|
2386
4163
|
lines.push("Failed to apply some changes:");
|
|
2387
4164
|
for (const err of result.errors) {
|
|
@@ -2397,6 +4174,7 @@ function formatApplyResult(result) {
|
|
|
2397
4174
|
|
|
2398
4175
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2399
4176
|
var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
4177
|
+
var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
|
|
2400
4178
|
function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
2401
4179
|
const files = [];
|
|
2402
4180
|
function walk(currentDir) {
|
|
@@ -2404,17 +4182,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2404
4182
|
return;
|
|
2405
4183
|
let entries = [];
|
|
2406
4184
|
try {
|
|
2407
|
-
entries =
|
|
4185
|
+
entries = readdirSync3(currentDir);
|
|
2408
4186
|
} catch {
|
|
2409
4187
|
return;
|
|
2410
4188
|
}
|
|
2411
4189
|
for (const entry of entries) {
|
|
2412
4190
|
if (files.length >= maxFiles)
|
|
2413
4191
|
return;
|
|
2414
|
-
const fullPath =
|
|
4192
|
+
const fullPath = join5(currentDir, entry);
|
|
2415
4193
|
let stat;
|
|
2416
4194
|
try {
|
|
2417
|
-
stat =
|
|
4195
|
+
stat = lstatSync4(fullPath);
|
|
2418
4196
|
} catch {
|
|
2419
4197
|
continue;
|
|
2420
4198
|
}
|
|
@@ -2432,52 +4210,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2432
4210
|
walk(dir);
|
|
2433
4211
|
return files;
|
|
2434
4212
|
}
|
|
2435
|
-
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
|
|
4213
|
+
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
|
|
2436
4214
|
if (!extension.startsWith(".")) {
|
|
2437
4215
|
throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
|
|
2438
4216
|
}
|
|
2439
|
-
const absDir =
|
|
2440
|
-
if (!
|
|
4217
|
+
const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
|
|
4218
|
+
if (!existsSync10(absDir)) {
|
|
2441
4219
|
throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
|
|
2442
4220
|
}
|
|
2443
|
-
const serverResult = findServerForExtension(extension);
|
|
4221
|
+
const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
|
|
2444
4222
|
if (serverResult.status !== "found") {
|
|
2445
4223
|
throw new LspServerLookupError(formatServerLookupError(serverResult));
|
|
2446
4224
|
}
|
|
2447
4225
|
const server2 = serverResult.server;
|
|
2448
|
-
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
|
|
4226
|
+
const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
|
|
2449
4227
|
const wasCapped = allFiles.length > maxFiles;
|
|
2450
4228
|
const filesToProcess = allFiles.slice(0, maxFiles);
|
|
2451
4229
|
if (filesToProcess.length === 0) {
|
|
2452
|
-
|
|
4230
|
+
const output = [
|
|
2453
4231
|
`Directory: ${absDir}`,
|
|
2454
4232
|
`Extension: ${extension}`,
|
|
2455
4233
|
"Files scanned: 0",
|
|
2456
4234
|
`No files found with extension "${extension}".`
|
|
2457
4235
|
].join(`
|
|
2458
4236
|
`);
|
|
4237
|
+
return { output, totalDiagnostics: 0, fileFailures: [] };
|
|
2459
4238
|
}
|
|
2460
|
-
const root = findWorkspaceRoot(absDir);
|
|
2461
|
-
const manager = getLspManager();
|
|
4239
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
|
|
4240
|
+
const manager = options.manager ?? getLspManager();
|
|
2462
4241
|
const allDiagnostics = [];
|
|
2463
4242
|
const fileErrors = [];
|
|
4243
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
|
|
4244
|
+
options.signal?.throwIfAborted();
|
|
2464
4245
|
const client = await manager.getClient(root, server2);
|
|
2465
4246
|
try {
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
file,
|
|
2477
|
-
|
|
2478
|
-
|
|
4247
|
+
let nextIndex = 0;
|
|
4248
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
|
|
4249
|
+
for (;; ) {
|
|
4250
|
+
if (options.signal?.aborted)
|
|
4251
|
+
return;
|
|
4252
|
+
const file = filesToProcess[nextIndex];
|
|
4253
|
+
nextIndex += 1;
|
|
4254
|
+
if (file === undefined)
|
|
4255
|
+
return;
|
|
4256
|
+
try {
|
|
4257
|
+
const result = await client.diagnostics(file, options.signal);
|
|
4258
|
+
const filtered = filterDiagnosticsBySeverity(result.items, severity);
|
|
4259
|
+
allDiagnostics.push(...filtered.map((diagnostic) => ({
|
|
4260
|
+
filePath: file,
|
|
4261
|
+
diagnostic
|
|
4262
|
+
})));
|
|
4263
|
+
} catch (e) {
|
|
4264
|
+
fileErrors.push({
|
|
4265
|
+
file,
|
|
4266
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
2479
4269
|
}
|
|
2480
|
-
}
|
|
4270
|
+
});
|
|
4271
|
+
await Promise.all(workers);
|
|
2481
4272
|
} finally {
|
|
2482
4273
|
manager.releaseClient(root, server2.id);
|
|
2483
4274
|
}
|
|
@@ -2505,13 +4296,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
|
|
|
2505
4296
|
lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
2506
4297
|
}
|
|
2507
4298
|
}
|
|
2508
|
-
return lines.join(`
|
|
2509
|
-
`);
|
|
4299
|
+
return { output: lines.join(`
|
|
4300
|
+
`), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
|
|
2510
4301
|
}
|
|
2511
4302
|
|
|
2512
4303
|
// ../lsp-core/src/lsp/infer-extension.ts
|
|
2513
|
-
import { lstatSync as
|
|
2514
|
-
import { join as
|
|
4304
|
+
import { lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
|
|
4305
|
+
import { join as join6 } from "node:path";
|
|
2515
4306
|
var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
2516
4307
|
var MAX_SCAN_ENTRIES = 500;
|
|
2517
4308
|
function inferExtensionFromDirectory(directory) {
|
|
@@ -2522,17 +4313,17 @@ function inferExtensionFromDirectory(directory) {
|
|
|
2522
4313
|
return;
|
|
2523
4314
|
let entries;
|
|
2524
4315
|
try {
|
|
2525
|
-
entries =
|
|
4316
|
+
entries = readdirSync4(dir);
|
|
2526
4317
|
} catch {
|
|
2527
4318
|
return;
|
|
2528
4319
|
}
|
|
2529
4320
|
for (const entry of entries) {
|
|
2530
4321
|
if (scanned >= MAX_SCAN_ENTRIES)
|
|
2531
4322
|
return;
|
|
2532
|
-
const fullPath =
|
|
4323
|
+
const fullPath = join6(dir, entry);
|
|
2533
4324
|
let stat;
|
|
2534
4325
|
try {
|
|
2535
|
-
stat =
|
|
4326
|
+
stat = lstatSync5(fullPath);
|
|
2536
4327
|
} catch {
|
|
2537
4328
|
continue;
|
|
2538
4329
|
}
|
|
@@ -2606,13 +4397,48 @@ function missingDependencyResult(error, details) {
|
|
|
2606
4397
|
details: {
|
|
2607
4398
|
...details,
|
|
2608
4399
|
error: message,
|
|
2609
|
-
errorKind: "missing_dependency"
|
|
4400
|
+
errorKind: "missing_dependency",
|
|
4401
|
+
...availabilityDetails(error)
|
|
2610
4402
|
}
|
|
2611
4403
|
};
|
|
2612
4404
|
}
|
|
4405
|
+
function availabilityDetails(error) {
|
|
4406
|
+
const availability = missingDependencyAvailability(error);
|
|
4407
|
+
return availability === null ? {} : { availability };
|
|
4408
|
+
}
|
|
4409
|
+
function missingDependencyAvailability(error) {
|
|
4410
|
+
if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
|
|
4411
|
+
return null;
|
|
4412
|
+
const context = lspRequestContext();
|
|
4413
|
+
switch (error.lookup.status) {
|
|
4414
|
+
case "not_configured":
|
|
4415
|
+
return {
|
|
4416
|
+
kind: "not_configured",
|
|
4417
|
+
extension: error.lookup.extension,
|
|
4418
|
+
availableServers: [...error.lookup.availableServers],
|
|
4419
|
+
projectConfigPaths: [...context.projectConfigPaths],
|
|
4420
|
+
userConfigPath: context.userConfigPath,
|
|
4421
|
+
installDecisionTool: context.capabilities.installDecisionTool
|
|
4422
|
+
};
|
|
4423
|
+
case "not_installed":
|
|
4424
|
+
return {
|
|
4425
|
+
kind: "not_installed",
|
|
4426
|
+
serverId: error.lookup.server.id,
|
|
4427
|
+
command: [...error.lookup.server.command],
|
|
4428
|
+
extensions: [...error.lookup.server.extensions],
|
|
4429
|
+
installHint: error.lookup.installHint,
|
|
4430
|
+
installDecisionTool: context.capabilities.installDecisionTool,
|
|
4431
|
+
installDecisionsPath: context.installDecisionsPath
|
|
4432
|
+
};
|
|
4433
|
+
default: {
|
|
4434
|
+
const exhaustive = error.lookup;
|
|
4435
|
+
return exhaustive;
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
2613
4439
|
|
|
2614
4440
|
// ../lsp-core/src/tools/parameters.ts
|
|
2615
|
-
function
|
|
4441
|
+
function isRecord7(value) {
|
|
2616
4442
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2617
4443
|
}
|
|
2618
4444
|
function requireString(params, key) {
|
|
@@ -2669,7 +4495,7 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2669
4495
|
const filePath = requireString(params, "filePath");
|
|
2670
4496
|
const severity = severityFilter(params);
|
|
2671
4497
|
try {
|
|
2672
|
-
const absPath =
|
|
4498
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2673
4499
|
if (isDirectoryPath(absPath)) {
|
|
2674
4500
|
const extension = inferExtensionFromDirectory(absPath);
|
|
2675
4501
|
if (!extension) {
|
|
@@ -2686,18 +4512,33 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2686
4512
|
};
|
|
2687
4513
|
return text(message, details3);
|
|
2688
4514
|
}
|
|
2689
|
-
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
|
|
4515
|
+
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
|
|
2690
4516
|
const details2 = {
|
|
2691
4517
|
filePath,
|
|
2692
4518
|
severity,
|
|
2693
4519
|
mode: "directory",
|
|
2694
4520
|
diagnostics: [],
|
|
4521
|
+
totalDiagnostics: output2.totalDiagnostics,
|
|
4522
|
+
truncated: false,
|
|
4523
|
+
fileFailures: [...output2.fileFailures]
|
|
4524
|
+
};
|
|
4525
|
+
return text(output2.output, details2);
|
|
4526
|
+
}
|
|
4527
|
+
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
|
|
4528
|
+
if (result.transientError) {
|
|
4529
|
+
const message = result.transientError.message;
|
|
4530
|
+
const details2 = {
|
|
4531
|
+
filePath,
|
|
4532
|
+
severity,
|
|
4533
|
+
mode: "file",
|
|
4534
|
+
diagnostics: [],
|
|
2695
4535
|
totalDiagnostics: 0,
|
|
2696
|
-
truncated: false
|
|
4536
|
+
truncated: false,
|
|
4537
|
+
error: message,
|
|
4538
|
+
errorKind: result.transientError.kind
|
|
2697
4539
|
};
|
|
2698
|
-
return text(
|
|
4540
|
+
return text(message, details2, true);
|
|
2699
4541
|
}
|
|
2700
|
-
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
|
|
2701
4542
|
const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
|
|
2702
4543
|
const total = diagnostics.length;
|
|
2703
4544
|
const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
|
|
@@ -2758,7 +4599,7 @@ async function executeLspGotoDefinition(params, signal) {
|
|
|
2758
4599
|
const line = requireNumber(params, "line");
|
|
2759
4600
|
const character = requireNumber(params, "character");
|
|
2760
4601
|
try {
|
|
2761
|
-
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
|
|
4602
|
+
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
|
|
2762
4603
|
const locations = !result ? [] : Array.isArray(result) ? result : [result];
|
|
2763
4604
|
const details = { filePath, line, character, locations };
|
|
2764
4605
|
if (locations.length === 0)
|
|
@@ -2783,7 +4624,7 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2783
4624
|
const character = requireNumber(params, "character");
|
|
2784
4625
|
const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
|
|
2785
4626
|
try {
|
|
2786
|
-
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
|
|
4627
|
+
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
|
|
2787
4628
|
const references = Array.isArray(result) ? result : [];
|
|
2788
4629
|
const total = references.length;
|
|
2789
4630
|
const truncated = total > DEFAULT_MAX_REFERENCES;
|
|
@@ -2819,181 +4660,13 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2819
4660
|
}
|
|
2820
4661
|
}
|
|
2821
4662
|
|
|
2822
|
-
// ../lsp-core/src/lsp/workspace-edit.ts
|
|
2823
|
-
import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2824
|
-
import { dirname as dirname3, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
|
|
2825
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2826
|
-
function errorMessage2(error) {
|
|
2827
|
-
return error instanceof Error ? error.message : String(error);
|
|
2828
|
-
}
|
|
2829
|
-
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
2830
|
-
const relativePath = relative(workspaceRoot, filePath);
|
|
2831
|
-
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
|
|
2832
|
-
}
|
|
2833
|
-
function realpathForValidation(filePath) {
|
|
2834
|
-
if (existsSync7(filePath))
|
|
2835
|
-
return realpathSync(filePath);
|
|
2836
|
-
const parent = dirname3(filePath);
|
|
2837
|
-
return resolve5(realpathSync(parent), relative(parent, filePath));
|
|
2838
|
-
}
|
|
2839
|
-
function uriToWorkspacePath(uri, workspaceRoot) {
|
|
2840
|
-
let filePath;
|
|
2841
|
-
try {
|
|
2842
|
-
filePath = fileURLToPath2(uri);
|
|
2843
|
-
} catch (error) {
|
|
2844
|
-
return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
|
|
2845
|
-
}
|
|
2846
|
-
let validatedPath;
|
|
2847
|
-
try {
|
|
2848
|
-
validatedPath = realpathForValidation(filePath);
|
|
2849
|
-
} catch (error) {
|
|
2850
|
-
return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
|
|
2851
|
-
}
|
|
2852
|
-
if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
|
|
2853
|
-
return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
|
|
2854
|
-
}
|
|
2855
|
-
return { success: true, path: filePath };
|
|
2856
|
-
}
|
|
2857
|
-
function applyTextEditsToFile(filePath, edits) {
|
|
2858
|
-
try {
|
|
2859
|
-
const content = readFileSync4(filePath, "utf-8");
|
|
2860
|
-
const lines = content.split(`
|
|
2861
|
-
`);
|
|
2862
|
-
const sortedEdits = [...edits].sort((a, b) => {
|
|
2863
|
-
if (b.range.start.line !== a.range.start.line) {
|
|
2864
|
-
return b.range.start.line - a.range.start.line;
|
|
2865
|
-
}
|
|
2866
|
-
return b.range.start.character - a.range.start.character;
|
|
2867
|
-
});
|
|
2868
|
-
for (const edit of sortedEdits) {
|
|
2869
|
-
const startLine = edit.range.start.line;
|
|
2870
|
-
const startChar = edit.range.start.character;
|
|
2871
|
-
const endLine = edit.range.end.line;
|
|
2872
|
-
const endChar = edit.range.end.character;
|
|
2873
|
-
if (startLine === endLine) {
|
|
2874
|
-
const line = lines[startLine] ?? "";
|
|
2875
|
-
lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
|
|
2876
|
-
} else {
|
|
2877
|
-
const firstLine = lines[startLine] ?? "";
|
|
2878
|
-
const lastLine = lines[endLine] ?? "";
|
|
2879
|
-
const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
|
|
2880
|
-
lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
|
|
2881
|
-
`));
|
|
2882
|
-
}
|
|
2883
|
-
}
|
|
2884
|
-
writeFileSync2(filePath, lines.join(`
|
|
2885
|
-
`), "utf-8");
|
|
2886
|
-
return { success: true, editCount: edits.length };
|
|
2887
|
-
} catch (err) {
|
|
2888
|
-
return {
|
|
2889
|
-
success: false,
|
|
2890
|
-
editCount: 0,
|
|
2891
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2892
|
-
};
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
function applyWorkspaceEdit(edit, options = {}) {
|
|
2896
|
-
if (!edit) {
|
|
2897
|
-
return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
|
|
2898
|
-
}
|
|
2899
|
-
const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
|
|
2900
|
-
const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
|
|
2901
|
-
if (edit.changes) {
|
|
2902
|
-
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
2903
|
-
const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
|
|
2904
|
-
if (!validatedPath.success) {
|
|
2905
|
-
result.success = false;
|
|
2906
|
-
result.errors.push(validatedPath.error);
|
|
2907
|
-
continue;
|
|
2908
|
-
}
|
|
2909
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, edits);
|
|
2910
|
-
if (applyResult.success) {
|
|
2911
|
-
result.filesModified.push(validatedPath.path);
|
|
2912
|
-
result.totalEdits += applyResult.editCount;
|
|
2913
|
-
} else {
|
|
2914
|
-
result.success = false;
|
|
2915
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
if (edit.documentChanges) {
|
|
2920
|
-
for (const change of edit.documentChanges) {
|
|
2921
|
-
if (!("kind" in change)) {
|
|
2922
|
-
const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
|
|
2923
|
-
if (!validatedPath.success) {
|
|
2924
|
-
result.success = false;
|
|
2925
|
-
result.errors.push(validatedPath.error);
|
|
2926
|
-
continue;
|
|
2927
|
-
}
|
|
2928
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
|
|
2929
|
-
if (applyResult.success) {
|
|
2930
|
-
result.filesModified.push(validatedPath.path);
|
|
2931
|
-
result.totalEdits += applyResult.editCount;
|
|
2932
|
-
} else {
|
|
2933
|
-
result.success = false;
|
|
2934
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2935
|
-
}
|
|
2936
|
-
continue;
|
|
2937
|
-
}
|
|
2938
|
-
if (change.kind === "create") {
|
|
2939
|
-
try {
|
|
2940
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2941
|
-
if (!validatedPath.success) {
|
|
2942
|
-
result.success = false;
|
|
2943
|
-
result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
|
|
2944
|
-
continue;
|
|
2945
|
-
}
|
|
2946
|
-
writeFileSync2(validatedPath.path, "", "utf-8");
|
|
2947
|
-
result.filesModified.push(validatedPath.path);
|
|
2948
|
-
} catch (err) {
|
|
2949
|
-
result.success = false;
|
|
2950
|
-
result.errors.push(`Create ${change.uri}: ${String(err)}`);
|
|
2951
|
-
}
|
|
2952
|
-
} else if (change.kind === "rename") {
|
|
2953
|
-
try {
|
|
2954
|
-
const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
|
|
2955
|
-
const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
|
|
2956
|
-
if (!oldPath.success || !newPath.success) {
|
|
2957
|
-
const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
|
|
2958
|
-
result.success = false;
|
|
2959
|
-
result.errors.push(`Rename ${change.oldUri}: ${error}`);
|
|
2960
|
-
continue;
|
|
2961
|
-
}
|
|
2962
|
-
const content = readFileSync4(oldPath.path, "utf-8");
|
|
2963
|
-
writeFileSync2(newPath.path, content, "utf-8");
|
|
2964
|
-
unlinkSync(oldPath.path);
|
|
2965
|
-
result.filesModified.push(newPath.path);
|
|
2966
|
-
} catch (err) {
|
|
2967
|
-
result.success = false;
|
|
2968
|
-
result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
|
|
2969
|
-
}
|
|
2970
|
-
} else if (change.kind === "delete") {
|
|
2971
|
-
try {
|
|
2972
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2973
|
-
if (!validatedPath.success) {
|
|
2974
|
-
result.success = false;
|
|
2975
|
-
result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
|
|
2976
|
-
continue;
|
|
2977
|
-
}
|
|
2978
|
-
unlinkSync(validatedPath.path);
|
|
2979
|
-
result.filesModified.push(validatedPath.path);
|
|
2980
|
-
} catch (err) {
|
|
2981
|
-
result.success = false;
|
|
2982
|
-
result.errors.push(`Delete ${change.uri}: ${String(err)}`);
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
return result;
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
4663
|
// ../lsp-core/src/tools/rename.ts
|
|
2991
4664
|
async function executeLspPrepareRename(params, signal) {
|
|
2992
4665
|
const filePath = requireString(params, "filePath");
|
|
2993
4666
|
const line = requireNumber(params, "line");
|
|
2994
4667
|
const character = requireNumber(params, "character");
|
|
2995
4668
|
try {
|
|
2996
|
-
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
|
|
4669
|
+
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
|
|
2997
4670
|
const details = { filePath, line, character, result };
|
|
2998
4671
|
return text(formatPrepareRenameResult(result), details);
|
|
2999
4672
|
} catch (error) {
|
|
@@ -3014,13 +4687,9 @@ async function executeLspRename(params, signal) {
|
|
|
3014
4687
|
const character = requireNumber(params, "character");
|
|
3015
4688
|
const newName = requireString(params, "newName");
|
|
3016
4689
|
try {
|
|
3017
|
-
const
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
}), "rename", clientOptions(signal));
|
|
3021
|
-
const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
|
|
3022
|
-
const details = { filePath, line, character, newName, apply, edit: edit.edit };
|
|
3023
|
-
return text(formatApplyResult(apply), details, !apply.success);
|
|
4690
|
+
const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
|
|
4691
|
+
const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
|
|
4692
|
+
return text(formatApplyResult(result.apply), details, !result.apply.success);
|
|
3024
4693
|
} catch (error) {
|
|
3025
4694
|
const missingDependency = missingDependencyResult(error, {
|
|
3026
4695
|
filePath,
|
|
@@ -3095,10 +4764,10 @@ async function executeLspSymbols(params, signal) {
|
|
|
3095
4764
|
errorKind: "missing_query"
|
|
3096
4765
|
});
|
|
3097
4766
|
}
|
|
3098
|
-
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
|
|
4767
|
+
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
|
|
3099
4768
|
return formatSymbolsResult(filePath, scope, symbols2, limit, query);
|
|
3100
4769
|
}
|
|
3101
|
-
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
|
|
4770
|
+
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
|
|
3102
4771
|
return formatSymbolsResult(filePath, scope, symbols, limit);
|
|
3103
4772
|
} catch (error) {
|
|
3104
4773
|
const query = optionalString(params, "query");
|
|
@@ -3264,12 +4933,12 @@ function matchesToolName(tool, name) {
|
|
|
3264
4933
|
return tool.name === name || (tool.aliases?.includes(name) ?? false);
|
|
3265
4934
|
}
|
|
3266
4935
|
function coerceToolArguments(value) {
|
|
3267
|
-
return
|
|
4936
|
+
return isRecord7(value) ? value : {};
|
|
3268
4937
|
}
|
|
3269
4938
|
// ../lsp-core/src/mcp.ts
|
|
3270
4939
|
var SERVER_NAME = "lsp";
|
|
3271
4940
|
var SERVER_VERSION = "0.1.0";
|
|
3272
|
-
async function handleLspMcpRequest(input) {
|
|
4941
|
+
async function handleLspMcpRequest(input, options = {}) {
|
|
3273
4942
|
if (!isPlainRecord(input)) {
|
|
3274
4943
|
return errorResponse(null, -32600, "Invalid Request");
|
|
3275
4944
|
}
|
|
@@ -3291,33 +4960,37 @@ async function handleLspMcpRequest(input) {
|
|
|
3291
4960
|
return successResponse(id, { tools: LSP_MCP_TOOLS.map(describeTool) });
|
|
3292
4961
|
}
|
|
3293
4962
|
if (method === "tools/call") {
|
|
3294
|
-
return handleToolCall(id, input["params"]);
|
|
4963
|
+
return handleToolCall(id, input["params"], options.signal);
|
|
3295
4964
|
}
|
|
3296
4965
|
return errorResponse(id, -32601, `Method not found: ${String(method)}`);
|
|
3297
4966
|
}
|
|
3298
4967
|
async function runMcpStdioServer(input = process.stdin, output = process.stdout) {
|
|
4968
|
+
const requestContext = createStandaloneMcpRequestContext();
|
|
3299
4969
|
await runJsonRpcStdioServer({
|
|
3300
4970
|
input,
|
|
3301
4971
|
output,
|
|
3302
4972
|
idleTimeoutMs: 0,
|
|
3303
|
-
handler: handleLspMcpRequest,
|
|
4973
|
+
handler: (request) => runWithRequestContext(requestContext, () => handleLspMcpRequest(request)),
|
|
3304
4974
|
handlerOptions: undefined
|
|
3305
4975
|
});
|
|
3306
4976
|
}
|
|
3307
|
-
async function handleToolCall(id, params) {
|
|
4977
|
+
async function handleToolCall(id, params, signal) {
|
|
3308
4978
|
if (!isPlainRecord(params) || typeof params["name"] !== "string") {
|
|
3309
4979
|
return errorResponse(id, -32602, "tools/call requires params.name");
|
|
3310
4980
|
}
|
|
3311
4981
|
try {
|
|
3312
|
-
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
|
|
4982
|
+
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
|
|
3313
4983
|
return successResponse(id, {
|
|
3314
4984
|
content: result.content,
|
|
3315
4985
|
isError: result.isError ?? false,
|
|
3316
4986
|
details: result.details
|
|
3317
4987
|
});
|
|
3318
4988
|
} catch (error) {
|
|
4989
|
+
if (!(error instanceof Error)) {
|
|
4990
|
+
throw error;
|
|
4991
|
+
}
|
|
3319
4992
|
return successResponse(id, {
|
|
3320
|
-
content: [{ type: "text", text:
|
|
4993
|
+
content: [{ type: "text", text: error.message }],
|
|
3321
4994
|
isError: true
|
|
3322
4995
|
});
|
|
3323
4996
|
}
|