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
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { argv, stderr } from "node:process";
|
|
5
5
|
|
|
6
|
+
// src/proxy.ts
|
|
7
|
+
import { existsSync as existsSync11, realpathSync as realpathSync4 } from "node:fs";
|
|
8
|
+
import { basename as basename3, delimiter as delimiter4, dirname as dirname9, isAbsolute as isAbsolute4 } from "node:path";
|
|
9
|
+
|
|
6
10
|
// ../mcp-stdio-core/src/record.ts
|
|
7
11
|
function isPlainRecord(value) {
|
|
8
12
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -17,9 +21,6 @@ function errorResponse(id, code, message, data) {
|
|
|
17
21
|
function jsonRpcId(value) {
|
|
18
22
|
return typeof value === "string" || typeof value === "number" || value === null ? value : null;
|
|
19
23
|
}
|
|
20
|
-
function messageFromError(error) {
|
|
21
|
-
return error instanceof Error ? error.message : String(error);
|
|
22
|
-
}
|
|
23
24
|
// ../mcp-stdio-core/src/transport.ts
|
|
24
25
|
var HEADER_SEPARATOR = Buffer.from(`\r
|
|
25
26
|
\r
|
|
@@ -206,37 +207,187 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
|
|
|
206
207
|
closed: () => isClosed
|
|
207
208
|
};
|
|
208
209
|
}
|
|
209
|
-
// ../lsp-core/src/tools/diagnostics.ts
|
|
210
|
-
import { resolve as resolve4 } from "node:path";
|
|
211
|
-
|
|
212
210
|
// ../lsp-core/src/lsp/client-wrapper.ts
|
|
213
|
-
import { existsSync as
|
|
214
|
-
import { dirname as
|
|
211
|
+
import { existsSync as existsSync9, statSync as statSync3 } from "node:fs";
|
|
212
|
+
import { dirname as dirname6, join as join4, resolve as resolve7 } from "node:path";
|
|
215
213
|
|
|
216
214
|
// ../lsp-core/src/request-context.ts
|
|
217
215
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
216
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
217
|
+
import { homedir } from "node:os";
|
|
218
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
219
|
+
|
|
220
|
+
class LspRequestContextParseError extends Error {
|
|
221
|
+
code;
|
|
222
|
+
name = "LspRequestContextParseError";
|
|
223
|
+
constructor(code, message) {
|
|
224
|
+
super(message);
|
|
225
|
+
this.code = code;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class LspRequestContextUnavailableError extends Error {
|
|
230
|
+
name = "LspRequestContextUnavailableError";
|
|
231
|
+
constructor() {
|
|
232
|
+
super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
218
235
|
var storage = new AsyncLocalStorage;
|
|
236
|
+
var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
237
|
+
var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
|
|
219
238
|
function runWithRequestContext(context, fn) {
|
|
220
239
|
return storage.run(context, fn);
|
|
221
240
|
}
|
|
241
|
+
function lspRequestContext() {
|
|
242
|
+
const context = storage.getStore();
|
|
243
|
+
if (!context)
|
|
244
|
+
throw new LspRequestContextUnavailableError;
|
|
245
|
+
return context;
|
|
246
|
+
}
|
|
222
247
|
function contextCwd() {
|
|
223
|
-
return
|
|
248
|
+
return lspRequestContext().cwd;
|
|
249
|
+
}
|
|
250
|
+
function createStandaloneMcpRequestContext(input = {}) {
|
|
251
|
+
const env = input.env ?? process.env;
|
|
252
|
+
const cwd = canonicalCwd(input.cwd ?? process.cwd());
|
|
253
|
+
const home = input.homeDir ?? homedir();
|
|
254
|
+
const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
|
|
255
|
+
const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
|
|
256
|
+
const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
|
|
257
|
+
return parseLspRequestContext({
|
|
258
|
+
cwd,
|
|
259
|
+
projectConfigPaths,
|
|
260
|
+
userConfigPath,
|
|
261
|
+
installDecisionsPath,
|
|
262
|
+
capabilities: { installDecisionTool: true }
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function parseLspRequestContext(value) {
|
|
266
|
+
if (!isRecord(value)) {
|
|
267
|
+
throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
|
|
268
|
+
}
|
|
269
|
+
rejectUnknownFields(value, CONTEXT_FIELDS, "context");
|
|
270
|
+
const cwd = stringField(value, "cwd");
|
|
271
|
+
const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
|
|
272
|
+
const userConfigPath = stringField(value, "userConfigPath");
|
|
273
|
+
const installDecisionsPath = stringField(value, "installDecisionsPath");
|
|
274
|
+
const capabilities = capabilitiesField(value["capabilities"]);
|
|
275
|
+
const canonical = canonicalCwd(cwd);
|
|
276
|
+
for (const path of projectConfigPaths) {
|
|
277
|
+
requireAbsolutePath(path, "projectConfigPaths");
|
|
278
|
+
const projectPath = canonicalizeExistingOrNearestAncestor(path);
|
|
279
|
+
if (!isPathInside(canonical, projectPath)) {
|
|
280
|
+
throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
requireAbsolutePath(userConfigPath, "userConfigPath");
|
|
284
|
+
requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
|
|
285
|
+
return {
|
|
286
|
+
cwd: canonical,
|
|
287
|
+
projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
|
|
288
|
+
userConfigPath,
|
|
289
|
+
installDecisionsPath,
|
|
290
|
+
capabilities
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function translateProjectConfigEnv(value, cwd) {
|
|
294
|
+
if (value === undefined || value.length === 0)
|
|
295
|
+
return [join(cwd, ".codex", "lsp-client.json")];
|
|
296
|
+
return value.split(delimiter).filter((entry) => entry.length > 0).map((entry) => isAbsolute(entry) ? entry : join(cwd, entry));
|
|
297
|
+
}
|
|
298
|
+
function translateHomeConfigEnv(value, home, fallback) {
|
|
299
|
+
if (value === undefined || value.length === 0)
|
|
300
|
+
return join(home, fallback);
|
|
301
|
+
return isAbsolute(value) ? value : join(home, value);
|
|
302
|
+
}
|
|
303
|
+
function canonicalCwd(cwd) {
|
|
304
|
+
const resolved = resolve(cwd);
|
|
305
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
306
|
+
throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
307
|
+
}
|
|
308
|
+
return realpathSync(resolved);
|
|
309
|
+
}
|
|
310
|
+
function canonicalizeExistingOrNearestAncestor(path) {
|
|
311
|
+
let current = resolve(path);
|
|
312
|
+
const suffix = [];
|
|
313
|
+
while (true) {
|
|
314
|
+
try {
|
|
315
|
+
const existing = realpathSync(current);
|
|
316
|
+
return suffix.length === 0 ? existing : join(existing, ...suffix);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (!isMissingPathError(error))
|
|
319
|
+
throw error;
|
|
320
|
+
const parent = dirname(current);
|
|
321
|
+
if (parent === current)
|
|
322
|
+
throw error;
|
|
323
|
+
suffix.unshift(basename(current));
|
|
324
|
+
current = parent;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function capabilitiesField(value) {
|
|
329
|
+
if (!isRecord(value)) {
|
|
330
|
+
throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
331
|
+
}
|
|
332
|
+
rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
|
|
333
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
334
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
335
|
+
throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
|
|
336
|
+
}
|
|
337
|
+
return { installDecisionTool };
|
|
338
|
+
}
|
|
339
|
+
function stringField(value, field) {
|
|
340
|
+
const fieldValue = value[field];
|
|
341
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
342
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
343
|
+
}
|
|
344
|
+
return fieldValue;
|
|
345
|
+
}
|
|
346
|
+
function stringArrayField(value, field) {
|
|
347
|
+
const fieldValue = value[field];
|
|
348
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
349
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
|
|
350
|
+
}
|
|
351
|
+
return fieldValue;
|
|
352
|
+
}
|
|
353
|
+
function requireAbsolutePath(path, field) {
|
|
354
|
+
if (!isAbsolute(path)) {
|
|
355
|
+
throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
|
|
356
|
+
}
|
|
224
357
|
}
|
|
225
|
-
function
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
358
|
+
function isPathInside(parent, child) {
|
|
359
|
+
const childPath = resolve(child);
|
|
360
|
+
const relativePath = relative(parent, childPath);
|
|
361
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
362
|
+
}
|
|
363
|
+
function isMissingPathError(error) {
|
|
364
|
+
const code = errorCode(error);
|
|
365
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
366
|
+
}
|
|
367
|
+
function rejectUnknownFields(value, allowed, scope) {
|
|
368
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
369
|
+
if (unknown.length > 0) {
|
|
370
|
+
throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function isRecord(value) {
|
|
374
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
375
|
+
}
|
|
376
|
+
function errorCode(error) {
|
|
377
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
378
|
+
return;
|
|
379
|
+
const code = Reflect.get(error, "code");
|
|
380
|
+
return typeof code === "string" ? code : undefined;
|
|
230
381
|
}
|
|
231
382
|
|
|
232
383
|
// ../lsp-core/src/lsp/effective-extension.ts
|
|
233
|
-
import { basename, extname } from "node:path";
|
|
384
|
+
import { basename as basename2, extname } from "node:path";
|
|
234
385
|
var BASENAME_EXTENSIONS = {
|
|
235
386
|
Dockerfile: ".dockerfile",
|
|
236
387
|
Containerfile: ".dockerfile"
|
|
237
388
|
};
|
|
238
389
|
function effectiveExtension(filePath) {
|
|
239
|
-
return BASENAME_EXTENSIONS[
|
|
390
|
+
return BASENAME_EXTENSIONS[basename2(filePath)] ?? extname(filePath);
|
|
240
391
|
}
|
|
241
392
|
|
|
242
393
|
// ../lsp-core/src/lsp/errors.ts
|
|
@@ -286,7 +437,12 @@ class LspInvalidPathError extends Error {
|
|
|
286
437
|
}
|
|
287
438
|
|
|
288
439
|
class LspServerLookupError extends Error {
|
|
440
|
+
lookup;
|
|
289
441
|
name = "LspServerLookupError";
|
|
442
|
+
constructor(message, lookup) {
|
|
443
|
+
super(message);
|
|
444
|
+
this.lookup = lookup;
|
|
445
|
+
}
|
|
290
446
|
}
|
|
291
447
|
|
|
292
448
|
class LspServerInitializingError extends Error {
|
|
@@ -306,17 +462,18 @@ function isLspDeadConnectionError(err) {
|
|
|
306
462
|
}
|
|
307
463
|
|
|
308
464
|
// ../lsp-core/src/lsp/cleanup-errors.ts
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
|
|
465
|
+
function writeCleanupError(message) {
|
|
466
|
+
process.stderr.write(`${message}
|
|
467
|
+
`);
|
|
468
|
+
}
|
|
469
|
+
function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
|
|
312
470
|
const message = error instanceof Error ? error.message : String(error);
|
|
313
|
-
|
|
471
|
+
logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
|
|
314
472
|
}
|
|
315
473
|
|
|
316
474
|
// ../lsp-core/src/lsp/client.ts
|
|
317
|
-
import {
|
|
318
|
-
import {
|
|
319
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
475
|
+
import { resolve as resolve6 } from "node:path";
|
|
476
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
320
477
|
|
|
321
478
|
// ../lsp-core/src/lsp/connection.ts
|
|
322
479
|
import { pathToFileURL } from "node:url";
|
|
@@ -380,28 +537,80 @@ class JsonRpcConnection {
|
|
|
380
537
|
onError(handler) {
|
|
381
538
|
this.errorHandlers.push(handler);
|
|
382
539
|
}
|
|
383
|
-
async sendRequest(method, params) {
|
|
540
|
+
async sendRequest(method, params, options = {}) {
|
|
384
541
|
if (this.disposed)
|
|
385
542
|
throw new Error("JSON-RPC connection is disposed");
|
|
386
543
|
const id = this.nextRequestId;
|
|
387
544
|
this.nextRequestId += 1;
|
|
545
|
+
const key = String(id);
|
|
388
546
|
const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
|
|
389
|
-
|
|
390
|
-
|
|
547
|
+
let requestWritten = false;
|
|
548
|
+
let cancelAfterWrite = false;
|
|
549
|
+
let settled = false;
|
|
550
|
+
const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
|
|
551
|
+
const responsePromise = new Promise((resolve2, reject) => {
|
|
552
|
+
const cleanup = () => {
|
|
553
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
554
|
+
};
|
|
555
|
+
const settleCancel = () => {
|
|
556
|
+
if (settled)
|
|
557
|
+
return;
|
|
558
|
+
settled = true;
|
|
559
|
+
this.pendingRequests.delete(key);
|
|
560
|
+
cleanup();
|
|
561
|
+
const rejectCancelled = () => reject(abortError(options.signal));
|
|
562
|
+
if (!requestWritten) {
|
|
563
|
+
cancelAfterWrite = true;
|
|
564
|
+
rejectCancelled();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
writeCancel().then(rejectCancelled, (error) => {
|
|
568
|
+
this.emitError(toError(error));
|
|
569
|
+
rejectCancelled();
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
const onAbort = () => settleCancel();
|
|
573
|
+
this.pendingRequests.set(key, {
|
|
391
574
|
resolve(result) {
|
|
392
|
-
|
|
575
|
+
settled = true;
|
|
576
|
+
cleanup();
|
|
577
|
+
resolve2(result);
|
|
578
|
+
},
|
|
579
|
+
reject(error) {
|
|
580
|
+
settled = true;
|
|
581
|
+
cleanup();
|
|
582
|
+
reject(error);
|
|
393
583
|
},
|
|
394
|
-
|
|
584
|
+
cleanup
|
|
395
585
|
});
|
|
586
|
+
if (options.signal?.aborted) {
|
|
587
|
+
settleCancel();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
396
591
|
});
|
|
592
|
+
if (settled)
|
|
593
|
+
return responsePromise;
|
|
397
594
|
try {
|
|
398
595
|
await this.writeMessage(message);
|
|
596
|
+
requestWritten = true;
|
|
597
|
+
if (cancelAfterWrite)
|
|
598
|
+
await writeCancel();
|
|
399
599
|
} catch (error) {
|
|
400
|
-
|
|
600
|
+
if (settled)
|
|
601
|
+
return responsePromise;
|
|
602
|
+
const pending = this.pendingRequests.get(key);
|
|
603
|
+
if (pending) {
|
|
604
|
+
pending.cleanup();
|
|
605
|
+
this.pendingRequests.delete(key);
|
|
606
|
+
}
|
|
401
607
|
throw error;
|
|
402
608
|
}
|
|
403
609
|
return responsePromise;
|
|
404
610
|
}
|
|
611
|
+
pendingRequestCount() {
|
|
612
|
+
return this.pendingRequests.size;
|
|
613
|
+
}
|
|
405
614
|
async sendNotification(method, params) {
|
|
406
615
|
if (this.disposed)
|
|
407
616
|
return;
|
|
@@ -418,6 +627,7 @@ class JsonRpcConnection {
|
|
|
418
627
|
this.reader.off("error", this.handleStreamError);
|
|
419
628
|
this.writer.off("error", this.handleStreamError);
|
|
420
629
|
for (const pending of this.pendingRequests.values()) {
|
|
630
|
+
pending.cleanup();
|
|
421
631
|
pending.reject(new Error("JSON-RPC connection disposed"));
|
|
422
632
|
}
|
|
423
633
|
this.pendingRequests.clear();
|
|
@@ -493,6 +703,7 @@ class JsonRpcConnection {
|
|
|
493
703
|
if (!pending)
|
|
494
704
|
return;
|
|
495
705
|
this.pendingRequests.delete(String(id));
|
|
706
|
+
pending.cleanup();
|
|
496
707
|
if ("error" in message) {
|
|
497
708
|
pending.reject(jsonRpcErrorToError(message["error"]));
|
|
498
709
|
return;
|
|
@@ -506,7 +717,11 @@ class JsonRpcConnection {
|
|
|
506
717
|
try {
|
|
507
718
|
handler(params);
|
|
508
719
|
} catch (error) {
|
|
509
|
-
|
|
720
|
+
if (error instanceof Error) {
|
|
721
|
+
this.emitError(error);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
this.emitError(new Error(String(error)));
|
|
510
725
|
}
|
|
511
726
|
}
|
|
512
727
|
handleRequest(message) {
|
|
@@ -531,13 +746,13 @@ class JsonRpcConnection {
|
|
|
531
746
|
const payload = `Content-Length: ${Buffer.byteLength(body, "utf8")}\r
|
|
532
747
|
\r
|
|
533
748
|
${body}`;
|
|
534
|
-
return new Promise((
|
|
749
|
+
return new Promise((resolve2, reject) => {
|
|
535
750
|
this.writer.write(payload, (error) => {
|
|
536
751
|
if (error) {
|
|
537
752
|
reject(error);
|
|
538
753
|
return;
|
|
539
754
|
}
|
|
540
|
-
|
|
755
|
+
resolve2();
|
|
541
756
|
});
|
|
542
757
|
});
|
|
543
758
|
}
|
|
@@ -547,6 +762,14 @@ ${body}`;
|
|
|
547
762
|
}
|
|
548
763
|
}
|
|
549
764
|
}
|
|
765
|
+
function abortError(signal) {
|
|
766
|
+
const reason = signal?.reason;
|
|
767
|
+
if (reason instanceof Error)
|
|
768
|
+
return reason;
|
|
769
|
+
const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
|
|
770
|
+
error.name = "AbortError";
|
|
771
|
+
return error;
|
|
772
|
+
}
|
|
550
773
|
function parseContentLength2(headers) {
|
|
551
774
|
for (const line of headers.split(`\r
|
|
552
775
|
`)) {
|
|
@@ -586,8 +809,8 @@ function toError(error) {
|
|
|
586
809
|
|
|
587
810
|
// ../lsp-core/src/lsp/process.ts
|
|
588
811
|
import { spawn, spawnSync } from "node:child_process";
|
|
589
|
-
import { existsSync, statSync } from "node:fs";
|
|
590
|
-
import { delimiter, join } from "node:path";
|
|
812
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
|
|
813
|
+
import { delimiter as delimiter2, join as join2 } from "node:path";
|
|
591
814
|
function isMissingProcessError(error) {
|
|
592
815
|
if (!(error instanceof Error) || !("code" in error))
|
|
593
816
|
return false;
|
|
@@ -600,10 +823,10 @@ function reportKillError(context, error) {
|
|
|
600
823
|
}
|
|
601
824
|
function validateCwd(cwd) {
|
|
602
825
|
try {
|
|
603
|
-
if (!
|
|
826
|
+
if (!existsSync2(cwd)) {
|
|
604
827
|
return { valid: false, error: `Working directory does not exist: ${cwd}` };
|
|
605
828
|
}
|
|
606
|
-
const stats =
|
|
829
|
+
const stats = statSync2(cwd);
|
|
607
830
|
if (!stats.isDirectory()) {
|
|
608
831
|
return { valid: false, error: `Path is not a directory: ${cwd}` };
|
|
609
832
|
}
|
|
@@ -616,9 +839,9 @@ function validateCwd(cwd) {
|
|
|
616
839
|
}
|
|
617
840
|
}
|
|
618
841
|
function wrap(proc) {
|
|
619
|
-
const exitedPromise = new Promise((
|
|
620
|
-
proc.once("close", (code) =>
|
|
621
|
-
proc.once("error", () =>
|
|
842
|
+
const exitedPromise = new Promise((resolve2) => {
|
|
843
|
+
proc.once("close", (code) => resolve2(code ?? 0));
|
|
844
|
+
proc.once("error", () => resolve2(1));
|
|
622
845
|
});
|
|
623
846
|
if (!proc.stdin || !proc.stdout || !proc.stderr) {
|
|
624
847
|
throw new LspProcessSpawnError("Spawned process is missing one of stdin/stdout/stderr pipes");
|
|
@@ -672,7 +895,7 @@ function isWindowsShellShim(command) {
|
|
|
672
895
|
return lowerCommand.endsWith(".cmd") || lowerCommand.endsWith(".bat");
|
|
673
896
|
}
|
|
674
897
|
function splitPath(pathValue, platform) {
|
|
675
|
-
const separator = platform === "win32" ? ";" :
|
|
898
|
+
const separator = platform === "win32" ? ";" : delimiter2;
|
|
676
899
|
return pathValue.split(separator).filter(Boolean);
|
|
677
900
|
}
|
|
678
901
|
function getWindowsPathExtensions(env) {
|
|
@@ -687,8 +910,8 @@ function resolveWindowsCommand(command, env) {
|
|
|
687
910
|
const extensions = getWindowsPathExtensions(env);
|
|
688
911
|
for (const baseDirectory of baseDirectories) {
|
|
689
912
|
for (const extension of extensions) {
|
|
690
|
-
const candidate = baseDirectory ?
|
|
691
|
-
if (
|
|
913
|
+
const candidate = baseDirectory ? join2(baseDirectory, `${command}${extension}`) : `${command}${extension}`;
|
|
914
|
+
if (existsSync2(candidate))
|
|
692
915
|
return candidate;
|
|
693
916
|
}
|
|
694
917
|
}
|
|
@@ -733,16 +956,16 @@ function spawnProcess(command, options) {
|
|
|
733
956
|
return wrap(proc);
|
|
734
957
|
}
|
|
735
958
|
|
|
736
|
-
// ../lsp-core/src/lsp/transport.ts
|
|
737
|
-
function
|
|
959
|
+
// ../lsp-core/src/lsp/transport-protocol.ts
|
|
960
|
+
function isRecord2(value) {
|
|
738
961
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
739
962
|
}
|
|
740
963
|
function parseConfigurationItems(params) {
|
|
741
|
-
if (!
|
|
964
|
+
if (!isRecord2(params) || !Array.isArray(params["items"]))
|
|
742
965
|
return [];
|
|
743
966
|
const items = [];
|
|
744
967
|
for (const item of params["items"]) {
|
|
745
|
-
if (!
|
|
968
|
+
if (!isRecord2(item))
|
|
746
969
|
continue;
|
|
747
970
|
const section = item["section"];
|
|
748
971
|
items.push(section === undefined || typeof section !== "string" ? {} : { section });
|
|
@@ -750,10 +973,35 @@ function parseConfigurationItems(params) {
|
|
|
750
973
|
return items;
|
|
751
974
|
}
|
|
752
975
|
function parseDiagnosticsParams(params) {
|
|
753
|
-
if (!
|
|
976
|
+
if (!isRecord2(params) || typeof params["uri"] !== "string")
|
|
754
977
|
return null;
|
|
755
978
|
const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
|
|
756
|
-
|
|
979
|
+
const version = typeof params["version"] === "number" ? params["version"] : undefined;
|
|
980
|
+
return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
|
|
981
|
+
}
|
|
982
|
+
function createLspSpawnEnv(_root, input) {
|
|
983
|
+
return { ...input };
|
|
984
|
+
}
|
|
985
|
+
function isDiagnostic(value) {
|
|
986
|
+
return isRecord2(value) && isRange(value["range"]) && typeof value["message"] === "string";
|
|
987
|
+
}
|
|
988
|
+
function isRange(value) {
|
|
989
|
+
return isRecord2(value) && isPosition(value["start"]) && isPosition(value["end"]);
|
|
990
|
+
}
|
|
991
|
+
function isPosition(value) {
|
|
992
|
+
return isRecord2(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// ../lsp-core/src/lsp/transport.ts
|
|
996
|
+
class LspClientNotStartedError extends Error {
|
|
997
|
+
serverId;
|
|
998
|
+
root;
|
|
999
|
+
name = "LspClientNotStartedError";
|
|
1000
|
+
constructor(serverId, root) {
|
|
1001
|
+
super("LSP client not started");
|
|
1002
|
+
this.serverId = serverId;
|
|
1003
|
+
this.root = root;
|
|
1004
|
+
}
|
|
757
1005
|
}
|
|
758
1006
|
|
|
759
1007
|
class LspClientTransport {
|
|
@@ -766,6 +1014,8 @@ class LspClientTransport {
|
|
|
766
1014
|
diagnosticsStore = new Map;
|
|
767
1015
|
requestTimeoutMs;
|
|
768
1016
|
initializeTimeoutMs;
|
|
1017
|
+
workspaceApplyEditHandler = null;
|
|
1018
|
+
diagnosticPullSupported = false;
|
|
769
1019
|
constructor(root, server2, timeouts = {}) {
|
|
770
1020
|
this.root = root;
|
|
771
1021
|
this.server = server2;
|
|
@@ -778,6 +1028,21 @@ class LspClientTransport {
|
|
|
778
1028
|
command() {
|
|
779
1029
|
return [...this.server.command];
|
|
780
1030
|
}
|
|
1031
|
+
setWorkspaceApplyEditHandler(handler) {
|
|
1032
|
+
this.workspaceApplyEditHandler = handler;
|
|
1033
|
+
}
|
|
1034
|
+
hasWorkspaceApplyEditHandler() {
|
|
1035
|
+
return this.workspaceApplyEditHandler !== null;
|
|
1036
|
+
}
|
|
1037
|
+
setDiagnosticPullSupported(supported) {
|
|
1038
|
+
this.diagnosticPullSupported = supported;
|
|
1039
|
+
}
|
|
1040
|
+
isDiagnosticPullSupported() {
|
|
1041
|
+
return this.diagnosticPullSupported;
|
|
1042
|
+
}
|
|
1043
|
+
handlePublishDiagnostics(params) {
|
|
1044
|
+
this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
|
|
1045
|
+
}
|
|
781
1046
|
async start() {
|
|
782
1047
|
const env = createLspSpawnEnv(this.root, {
|
|
783
1048
|
...process.env,
|
|
@@ -788,7 +1053,6 @@ class LspClientTransport {
|
|
|
788
1053
|
env
|
|
789
1054
|
});
|
|
790
1055
|
this.startStderrReading();
|
|
791
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
792
1056
|
if (this.proc.exitCode !== null) {
|
|
793
1057
|
const stderr = this.stderrBuffer.join(`
|
|
794
1058
|
`);
|
|
@@ -798,7 +1062,7 @@ class LspClientTransport {
|
|
|
798
1062
|
this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
799
1063
|
const diagnosticsParams = parseDiagnosticsParams(params);
|
|
800
1064
|
if (diagnosticsParams?.uri) {
|
|
801
|
-
this.
|
|
1065
|
+
this.handlePublishDiagnostics(diagnosticsParams);
|
|
802
1066
|
}
|
|
803
1067
|
});
|
|
804
1068
|
this.connection.onRequest("workspace/configuration", (params) => {
|
|
@@ -811,6 +1075,9 @@ class LspClientTransport {
|
|
|
811
1075
|
});
|
|
812
1076
|
this.connection.onRequest("client/registerCapability", () => null);
|
|
813
1077
|
this.connection.onRequest("window/workDoneProgress/create", () => null);
|
|
1078
|
+
if (this.workspaceApplyEditHandler) {
|
|
1079
|
+
this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
|
|
1080
|
+
}
|
|
814
1081
|
this.connection.onClose(() => {
|
|
815
1082
|
this.processExited = true;
|
|
816
1083
|
});
|
|
@@ -839,30 +1106,25 @@ class LspClientTransport {
|
|
|
839
1106
|
}
|
|
840
1107
|
async sendRequest(method, ...args) {
|
|
841
1108
|
if (!this.connection)
|
|
842
|
-
throw new
|
|
1109
|
+
throw new LspClientNotStartedError(this.server.id, this.root);
|
|
843
1110
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
844
1111
|
const stderrTail = this.stderrBuffer.slice(-10).join(`
|
|
845
1112
|
`);
|
|
846
1113
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
|
|
847
1114
|
}
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
1115
|
+
const options = args[1];
|
|
1116
|
+
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
1117
|
+
const timeoutController = new AbortController;
|
|
1118
|
+
const timeoutHandle = setTimeout(() => {
|
|
1119
|
+
const stderrTail = this.stderrBuffer.slice(-5).join(`
|
|
853
1120
|
`);
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1121
|
+
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
1122
|
+
}, timeoutMs);
|
|
1123
|
+
const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
|
|
857
1124
|
try {
|
|
858
|
-
const
|
|
859
|
-
const result = await Promise.race([requestPromise, timeoutPromise]);
|
|
860
|
-
if (timeoutHandle !== null)
|
|
861
|
-
clearTimeout(timeoutHandle);
|
|
1125
|
+
const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
|
|
862
1126
|
return result;
|
|
863
1127
|
} catch (error) {
|
|
864
|
-
if (timeoutHandle !== null)
|
|
865
|
-
clearTimeout(timeoutHandle);
|
|
866
1128
|
if (this.processExited || this.proc && this.proc.exitCode !== null) {
|
|
867
1129
|
throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
|
|
868
1130
|
`) || undefined);
|
|
@@ -871,6 +1133,9 @@ class LspClientTransport {
|
|
|
871
1133
|
throw new LspConnectionClosedError(this.server.id, this.root, error.message);
|
|
872
1134
|
}
|
|
873
1135
|
throw error;
|
|
1136
|
+
} finally {
|
|
1137
|
+
clearTimeout(timeoutHandle);
|
|
1138
|
+
combinedSignal.dispose();
|
|
874
1139
|
}
|
|
875
1140
|
}
|
|
876
1141
|
async sendNotification(method, ...args) {
|
|
@@ -899,17 +1164,17 @@ class LspClientTransport {
|
|
|
899
1164
|
try {
|
|
900
1165
|
await this.sendRequest("shutdown");
|
|
901
1166
|
} catch (error) {
|
|
902
|
-
reportBestEffortCleanupError("shutdown request", error);
|
|
1167
|
+
reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
|
|
903
1168
|
}
|
|
904
1169
|
try {
|
|
905
1170
|
await this.sendNotification("exit");
|
|
906
1171
|
} catch (error) {
|
|
907
|
-
reportBestEffortCleanupError("exit notification", error);
|
|
1172
|
+
reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
|
|
908
1173
|
}
|
|
909
1174
|
try {
|
|
910
1175
|
this.connection.dispose();
|
|
911
1176
|
} catch (error) {
|
|
912
|
-
reportBestEffortCleanupError("connection dispose", error);
|
|
1177
|
+
reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
|
|
913
1178
|
}
|
|
914
1179
|
this.connection = null;
|
|
915
1180
|
}
|
|
@@ -920,8 +1185,8 @@ class LspClientTransport {
|
|
|
920
1185
|
try {
|
|
921
1186
|
proc.kill();
|
|
922
1187
|
let timeoutId;
|
|
923
|
-
const timeoutPromise = new Promise((
|
|
924
|
-
timeoutId = setTimeout(
|
|
1188
|
+
const timeoutPromise = new Promise((resolve2) => {
|
|
1189
|
+
timeoutId = setTimeout(resolve2, STOP_HARD_KILL_TIMEOUT_MS);
|
|
925
1190
|
});
|
|
926
1191
|
await Promise.race([
|
|
927
1192
|
proc.exited.then(() => {
|
|
@@ -937,14 +1202,14 @@ class LspClientTransport {
|
|
|
937
1202
|
proc.kill("SIGKILL");
|
|
938
1203
|
await Promise.race([
|
|
939
1204
|
proc.exited,
|
|
940
|
-
new Promise((
|
|
1205
|
+
new Promise((resolve2) => setTimeout(resolve2, STOP_SIGKILL_GRACE_MS))
|
|
941
1206
|
]);
|
|
942
1207
|
} catch (error) {
|
|
943
|
-
reportBestEffortCleanupError("hard process kill", error);
|
|
1208
|
+
reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
|
|
944
1209
|
}
|
|
945
1210
|
}
|
|
946
1211
|
} catch (error) {
|
|
947
|
-
reportBestEffortCleanupError("process stop", error);
|
|
1212
|
+
reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
|
|
948
1213
|
}
|
|
949
1214
|
}
|
|
950
1215
|
this.processExited = true;
|
|
@@ -954,26 +1219,45 @@ class LspClientTransport {
|
|
|
954
1219
|
return this.diagnosticsStore.get(uri) ?? [];
|
|
955
1220
|
}
|
|
956
1221
|
}
|
|
957
|
-
function
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1222
|
+
function combineAbortSignals(primary, secondary) {
|
|
1223
|
+
const controller = new AbortController;
|
|
1224
|
+
const abortFrom = (signal) => {
|
|
1225
|
+
if (!controller.signal.aborted)
|
|
1226
|
+
controller.abort(signal.reason);
|
|
1227
|
+
};
|
|
1228
|
+
const onPrimaryAbort = () => {
|
|
1229
|
+
if (primary)
|
|
1230
|
+
abortFrom(primary);
|
|
1231
|
+
};
|
|
1232
|
+
const onSecondaryAbort = () => abortFrom(secondary);
|
|
1233
|
+
if (primary?.aborted)
|
|
1234
|
+
abortFrom(primary);
|
|
1235
|
+
else
|
|
1236
|
+
primary?.addEventListener("abort", onPrimaryAbort, { once: true });
|
|
1237
|
+
if (secondary.aborted)
|
|
1238
|
+
abortFrom(secondary);
|
|
1239
|
+
else
|
|
1240
|
+
secondary.addEventListener("abort", onSecondaryAbort, { once: true });
|
|
1241
|
+
return {
|
|
1242
|
+
signal: controller.signal,
|
|
1243
|
+
dispose: () => {
|
|
1244
|
+
primary?.removeEventListener("abort", onPrimaryAbort);
|
|
1245
|
+
secondary.removeEventListener("abort", onSecondaryAbort);
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
968
1248
|
}
|
|
969
1249
|
|
|
970
1250
|
// ../lsp-core/src/lsp/connection.ts
|
|
971
|
-
|
|
1251
|
+
function supportsDiagnosticPull(capabilities) {
|
|
1252
|
+
if (capabilities === undefined)
|
|
1253
|
+
return false;
|
|
1254
|
+
return Object.hasOwn(capabilities, "diagnosticProvider");
|
|
1255
|
+
}
|
|
972
1256
|
|
|
973
1257
|
class LspClientConnection extends LspClientTransport {
|
|
974
1258
|
async initialize() {
|
|
975
1259
|
const rootUri = pathToFileURL(this.root).href;
|
|
976
|
-
await this.sendRequest("initialize", {
|
|
1260
|
+
const result = await this.sendRequest("initialize", {
|
|
977
1261
|
processId: process.pid,
|
|
978
1262
|
rootUri,
|
|
979
1263
|
rootPath: this.root,
|
|
@@ -987,8 +1271,7 @@ class LspClientConnection extends LspClientTransport {
|
|
|
987
1271
|
publishDiagnostics: {},
|
|
988
1272
|
rename: {
|
|
989
1273
|
prepareSupport: true,
|
|
990
|
-
prepareSupportDefaultBehavior: 1
|
|
991
|
-
honorsChangeAnnotations: true
|
|
1274
|
+
prepareSupportDefaultBehavior: 1
|
|
992
1275
|
},
|
|
993
1276
|
codeAction: {
|
|
994
1277
|
codeActionLiteralSupport: {
|
|
@@ -1017,22 +1300,28 @@ class LspClientConnection extends LspClientTransport {
|
|
|
1017
1300
|
symbol: {},
|
|
1018
1301
|
workspaceFolders: true,
|
|
1019
1302
|
configuration: true,
|
|
1020
|
-
applyEdit: true,
|
|
1303
|
+
...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
|
|
1021
1304
|
workspaceEdit: {
|
|
1022
|
-
documentChanges: true
|
|
1305
|
+
documentChanges: true,
|
|
1306
|
+
resourceOperations: ["create", "rename", "delete"]
|
|
1023
1307
|
}
|
|
1024
1308
|
}
|
|
1025
1309
|
},
|
|
1026
1310
|
initializationOptions: this.server.initialization
|
|
1027
1311
|
}, { timeoutMs: this.initializeTimeoutMs });
|
|
1312
|
+
this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
|
|
1028
1313
|
await this.sendNotification("initialized");
|
|
1029
1314
|
await this.sendNotification("workspace/didChangeConfiguration", {
|
|
1030
1315
|
settings: { json: { validate: { enable: true } } }
|
|
1031
1316
|
});
|
|
1032
|
-
await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
|
|
1033
1317
|
}
|
|
1034
1318
|
}
|
|
1035
1319
|
|
|
1320
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1321
|
+
import { readFileSync, realpathSync as realpathSync2 } from "node:fs";
|
|
1322
|
+
import { relative as relative2, resolve as resolve2 } from "node:path";
|
|
1323
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
1324
|
+
|
|
1036
1325
|
// ../lsp-core/src/lsp/language-mappings.ts
|
|
1037
1326
|
var SYMBOL_KIND_MAP = {
|
|
1038
1327
|
1: "File",
|
|
@@ -1205,164 +1494,1635 @@ function getLanguageId(ext) {
|
|
|
1205
1494
|
return EXT_TO_LANG[ext] ?? "plaintext";
|
|
1206
1495
|
}
|
|
1207
1496
|
|
|
1208
|
-
// ../lsp-core/src/lsp/
|
|
1209
|
-
var
|
|
1210
|
-
var
|
|
1497
|
+
// ../lsp-core/src/lsp/workspace-document-state.ts
|
|
1498
|
+
var WATCHED_FILE_BATCH_SIZE = 128;
|
|
1499
|
+
var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
1500
|
+
function canonicalPath(filePath) {
|
|
1501
|
+
const absolute = resolve2(filePath);
|
|
1502
|
+
try {
|
|
1503
|
+
return realpathSync2(absolute);
|
|
1504
|
+
} catch {
|
|
1505
|
+
return absolute;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
function isSameOrDescendant(candidate, parent) {
|
|
1509
|
+
const suffix = relative2(parent, candidate);
|
|
1510
|
+
return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
|
|
1511
|
+
}
|
|
1512
|
+
function movedPath(candidate, oldPath, newPath) {
|
|
1513
|
+
const suffix = relative2(oldPath, candidate);
|
|
1514
|
+
return suffix === "" ? newPath : resolve2(newPath, suffix);
|
|
1515
|
+
}
|
|
1211
1516
|
|
|
1212
|
-
class
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1517
|
+
class WorkspaceDocumentState {
|
|
1518
|
+
sendNotification;
|
|
1519
|
+
clearDiagnostics;
|
|
1520
|
+
openDocuments = new Map;
|
|
1521
|
+
openByUri = new Map;
|
|
1522
|
+
openPromises = new Map;
|
|
1523
|
+
now;
|
|
1524
|
+
versionlessPublishQuiescenceMs;
|
|
1525
|
+
constructor(sendNotification, clearDiagnostics, options = {}) {
|
|
1526
|
+
this.sendNotification = sendNotification;
|
|
1527
|
+
this.clearDiagnostics = clearDiagnostics;
|
|
1528
|
+
this.now = options.now ?? (() => Date.now());
|
|
1529
|
+
this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
1219
1530
|
}
|
|
1220
1531
|
async openFile(filePath) {
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const languageId = getLanguageId(ext);
|
|
1227
|
-
const version = 1;
|
|
1228
|
-
await this.sendNotification("textDocument/didOpen", {
|
|
1229
|
-
textDocument: {
|
|
1230
|
-
uri,
|
|
1231
|
-
languageId,
|
|
1232
|
-
version,
|
|
1233
|
-
text
|
|
1234
|
-
}
|
|
1235
|
-
});
|
|
1236
|
-
this.openedFiles.add(absPath);
|
|
1237
|
-
this.documentVersions.set(uri, version);
|
|
1238
|
-
this.lastSyncedText.set(uri, text);
|
|
1239
|
-
await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
|
|
1240
|
-
return;
|
|
1532
|
+
const path = canonicalPath(filePath);
|
|
1533
|
+
const existingOpen = this.openPromises.get(path);
|
|
1534
|
+
if (existingOpen) {
|
|
1535
|
+
await existingOpen;
|
|
1536
|
+
return this.openFile(path);
|
|
1241
1537
|
}
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1538
|
+
const text = readFileSync(path, "utf-8");
|
|
1539
|
+
const existing = this.openDocuments.get(path);
|
|
1540
|
+
if (!existing)
|
|
1541
|
+
return this.openDocumentSingleFlight(path, text);
|
|
1542
|
+
if (existing.text === text)
|
|
1244
1543
|
return;
|
|
1245
|
-
|
|
1246
|
-
const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
|
|
1247
|
-
this.documentVersions.set(uri, nextVersion);
|
|
1248
|
-
this.lastSyncedText.set(uri, text);
|
|
1249
|
-
await this.sendNotification("textDocument/didChange", {
|
|
1250
|
-
textDocument: { uri, version: nextVersion },
|
|
1251
|
-
contentChanges: [{ text }]
|
|
1252
|
-
});
|
|
1253
|
-
await this.sendNotification("textDocument/didSave", {
|
|
1254
|
-
textDocument: { uri },
|
|
1255
|
-
text
|
|
1256
|
-
});
|
|
1544
|
+
await this.changeDocument(existing, text);
|
|
1257
1545
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
await this.openFile(absPath);
|
|
1261
|
-
return this.sendRequest("textDocument/definition", {
|
|
1262
|
-
textDocument: { uri: pathToFileURL2(absPath).href },
|
|
1263
|
-
position: { line: line - 1, character }
|
|
1264
|
-
});
|
|
1546
|
+
getVersion(filePath) {
|
|
1547
|
+
return this.openDocuments.get(canonicalPath(filePath))?.version;
|
|
1265
1548
|
}
|
|
1266
|
-
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1549
|
+
getStoredDiagnostics(uri) {
|
|
1550
|
+
const state = this.openByUri.get(uri);
|
|
1551
|
+
if (!state)
|
|
1552
|
+
return [];
|
|
1553
|
+
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
1554
|
+
}
|
|
1555
|
+
captureDiagnosticSnapshot(filePath) {
|
|
1556
|
+
const state = this.openDocuments.get(canonicalPath(filePath));
|
|
1557
|
+
if (!state)
|
|
1558
|
+
return null;
|
|
1559
|
+
return {
|
|
1560
|
+
path: state.path,
|
|
1561
|
+
uri: state.uri,
|
|
1562
|
+
version: state.version,
|
|
1563
|
+
documentGeneration: state.generation,
|
|
1564
|
+
publishGeneration: state.publishGeneration
|
|
1565
|
+
};
|
|
1274
1566
|
}
|
|
1275
|
-
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
return this.sendRequest("textDocument/documentSymbol", {
|
|
1279
|
-
textDocument: { uri: pathToFileURL2(absPath).href }
|
|
1280
|
-
});
|
|
1567
|
+
isCurrentSnapshot(snapshot) {
|
|
1568
|
+
const state = this.openDocuments.get(snapshot.path);
|
|
1569
|
+
return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
|
|
1281
1570
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1571
|
+
getPullCache(snapshot) {
|
|
1572
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1573
|
+
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
|
|
1574
|
+
return null;
|
|
1575
|
+
return state.pullCache;
|
|
1284
1576
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1577
|
+
recordPullDiagnostics(snapshot, report) {
|
|
1578
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1579
|
+
if (!state)
|
|
1580
|
+
return;
|
|
1581
|
+
state.pullCache = {
|
|
1582
|
+
documentVersion: snapshot.version,
|
|
1583
|
+
diagnostics: [...report.diagnostics],
|
|
1584
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
1585
|
+
};
|
|
1292
1586
|
}
|
|
1293
|
-
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1587
|
+
recordPublishedDiagnostics(params) {
|
|
1588
|
+
const state = this.openByUri.get(params.uri);
|
|
1589
|
+
if (!state)
|
|
1590
|
+
return;
|
|
1591
|
+
state.publishGeneration += 1;
|
|
1592
|
+
state.lastPublish = {
|
|
1593
|
+
diagnostics: [...params.diagnostics],
|
|
1594
|
+
publishGeneration: state.publishGeneration,
|
|
1595
|
+
documentGenerationAtArrival: state.generation,
|
|
1596
|
+
arrivedAt: this.now(),
|
|
1597
|
+
...params.version === undefined ? {} : { version: params.version }
|
|
1598
|
+
};
|
|
1599
|
+
this.notifyWaiters(state);
|
|
1600
|
+
}
|
|
1601
|
+
resolvePushDiagnostics(snapshot) {
|
|
1602
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1603
|
+
if (!state?.lastPublish)
|
|
1604
|
+
return { status: "missing" };
|
|
1605
|
+
const publish = state.lastPublish;
|
|
1606
|
+
if (publish.version !== undefined) {
|
|
1607
|
+
return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
|
|
1608
|
+
}
|
|
1609
|
+
if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
|
|
1610
|
+
return { status: "missing" };
|
|
1611
|
+
const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
|
|
1612
|
+
const waitMs = Math.max(0, readyAt - this.now());
|
|
1613
|
+
return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
|
|
1614
|
+
}
|
|
1615
|
+
waitForDiagnosticsActivity(snapshot, timeoutMs) {
|
|
1616
|
+
const state = this.openByUri.get(snapshot.uri);
|
|
1617
|
+
if (!state || timeoutMs <= 0)
|
|
1618
|
+
return Promise.resolve();
|
|
1619
|
+
return new Promise((resolveActivity) => {
|
|
1620
|
+
let settled = false;
|
|
1621
|
+
const finish = () => {
|
|
1622
|
+
if (settled)
|
|
1623
|
+
return;
|
|
1624
|
+
settled = true;
|
|
1625
|
+
clearTimeout(timer);
|
|
1626
|
+
state.waiters.delete(finish);
|
|
1627
|
+
resolveActivity();
|
|
1628
|
+
};
|
|
1629
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
1630
|
+
if (typeof timer.unref === "function")
|
|
1631
|
+
timer.unref();
|
|
1632
|
+
state.waiters.add(finish);
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
validateVersions(operations) {
|
|
1636
|
+
const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
|
|
1637
|
+
for (const operation of operations) {
|
|
1638
|
+
if (operation.kind === "text") {
|
|
1639
|
+
const current = versions.get(operation.path);
|
|
1640
|
+
if (operation.documentVersion !== null && current !== operation.documentVersion) {
|
|
1641
|
+
const observed = current === undefined ? "closed document" : `open document version ${current}`;
|
|
1642
|
+
return {
|
|
1643
|
+
changeIndex: operation.changeIndex,
|
|
1644
|
+
message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
if (current !== undefined)
|
|
1648
|
+
versions.set(operation.path, current + 1);
|
|
1649
|
+
continue;
|
|
1304
1650
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1651
|
+
if (operation.kind === "rename") {
|
|
1652
|
+
const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
|
|
1653
|
+
for (const [path] of moved)
|
|
1654
|
+
versions.delete(path);
|
|
1655
|
+
for (const [path] of moved)
|
|
1656
|
+
versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (operation.kind === "delete") {
|
|
1660
|
+
for (const path of [...versions.keys()]) {
|
|
1661
|
+
if (isSameOrDescendant(path, operation.path))
|
|
1662
|
+
versions.delete(path);
|
|
1663
|
+
}
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
|
|
1667
|
+
versions.set(operation.path, 1);
|
|
1308
1668
|
}
|
|
1309
1669
|
}
|
|
1310
|
-
return
|
|
1670
|
+
return null;
|
|
1311
1671
|
}
|
|
1312
|
-
async
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1672
|
+
async synchronize(delta) {
|
|
1673
|
+
const watched = [];
|
|
1674
|
+
for (const mutation of delta.operations)
|
|
1675
|
+
await this.synchronizeMutation(mutation, watched);
|
|
1676
|
+
for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
|
|
1677
|
+
await this.sendNotification("workspace/didChangeWatchedFiles", {
|
|
1678
|
+
changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1319
1681
|
}
|
|
1320
|
-
async
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1682
|
+
async synchronizeMutation(mutation, watched) {
|
|
1683
|
+
if (mutation.kind === "text") {
|
|
1684
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1685
|
+
if (state)
|
|
1686
|
+
await this.changeDocument(state, mutation.afterText);
|
|
1687
|
+
else
|
|
1688
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
if (mutation.kind === "create") {
|
|
1692
|
+
const state = this.openDocuments.get(mutation.path);
|
|
1693
|
+
if (state) {
|
|
1694
|
+
await this.closeDocument(state);
|
|
1695
|
+
await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
|
|
1696
|
+
} else {
|
|
1697
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
|
|
1698
|
+
}
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
if (mutation.kind === "rename") {
|
|
1702
|
+
const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
|
|
1703
|
+
for (const state of moved)
|
|
1704
|
+
await this.closeDocument(state);
|
|
1705
|
+
for (const state of moved) {
|
|
1706
|
+
const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
|
|
1707
|
+
await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
|
|
1708
|
+
}
|
|
1709
|
+
if (moved.length === 0) {
|
|
1710
|
+
watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
|
|
1711
|
+
watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
|
|
1712
|
+
}
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
|
|
1716
|
+
for (const state of removed)
|
|
1717
|
+
await this.closeDocument(state);
|
|
1718
|
+
if (removed.length === 0)
|
|
1719
|
+
watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
|
|
1720
|
+
}
|
|
1721
|
+
async openDocumentSingleFlight(path, text) {
|
|
1722
|
+
const existing = this.openPromises.get(path);
|
|
1723
|
+
if (existing)
|
|
1724
|
+
return existing;
|
|
1725
|
+
const open = (async () => {
|
|
1726
|
+
const state = {
|
|
1727
|
+
path,
|
|
1728
|
+
uri: pathToFileURL2(path).href,
|
|
1729
|
+
languageId: getLanguageId(effectiveExtension(path)),
|
|
1730
|
+
text,
|
|
1731
|
+
version: 1,
|
|
1732
|
+
generation: 1,
|
|
1733
|
+
publishGeneration: 0,
|
|
1734
|
+
waiters: new Set
|
|
1735
|
+
};
|
|
1736
|
+
this.openDocuments.set(path, state);
|
|
1737
|
+
this.openByUri.set(state.uri, state);
|
|
1738
|
+
this.notifyWaiters(state);
|
|
1739
|
+
await this.sendNotification("textDocument/didOpen", {
|
|
1740
|
+
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
|
|
1741
|
+
});
|
|
1742
|
+
})().finally(() => {
|
|
1743
|
+
this.openPromises.delete(path);
|
|
1744
|
+
});
|
|
1745
|
+
this.openPromises.set(path, open);
|
|
1746
|
+
return open;
|
|
1747
|
+
}
|
|
1748
|
+
async changeDocument(state, text) {
|
|
1749
|
+
state.text = text;
|
|
1750
|
+
state.version += 1;
|
|
1751
|
+
state.generation += 1;
|
|
1752
|
+
this.clearDiagnostics(state.uri);
|
|
1753
|
+
this.notifyWaiters(state);
|
|
1754
|
+
await this.sendNotification("textDocument/didChange", {
|
|
1755
|
+
textDocument: { uri: state.uri, version: state.version },
|
|
1756
|
+
contentChanges: [{ text }]
|
|
1327
1757
|
});
|
|
1758
|
+
await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
|
|
1759
|
+
}
|
|
1760
|
+
async closeDocument(state) {
|
|
1761
|
+
this.openDocuments.delete(state.path);
|
|
1762
|
+
this.openByUri.delete(state.uri);
|
|
1763
|
+
this.clearDiagnostics(state.uri);
|
|
1764
|
+
this.notifyWaiters(state);
|
|
1765
|
+
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
1766
|
+
}
|
|
1767
|
+
notifyWaiters(state) {
|
|
1768
|
+
for (const waiter of [...state.waiters])
|
|
1769
|
+
waiter();
|
|
1328
1770
|
}
|
|
1329
1771
|
}
|
|
1330
1772
|
|
|
1331
|
-
// ../lsp-core/src/lsp/
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
};
|
|
1339
|
-
for (const signal of signals) {
|
|
1340
|
-
process.on(signal, handler);
|
|
1341
|
-
}
|
|
1342
|
-
return () => {
|
|
1343
|
-
for (const signal of signals) {
|
|
1344
|
-
process.removeListener(signal, handler);
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1773
|
+
// ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
|
|
1774
|
+
var CONCURRENT_FAILURE_REASON_BY_PHASE = {
|
|
1775
|
+
applying: "workspace/applyEdit is already in progress for this workspace mutation",
|
|
1776
|
+
settled: "workspace/applyEdit was already handled for this workspace mutation"
|
|
1777
|
+
};
|
|
1778
|
+
function workspaceApplyEditConcurrentFailureReason(phase) {
|
|
1779
|
+
return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
|
|
1347
1780
|
}
|
|
1348
1781
|
|
|
1349
|
-
// ../lsp-core/src/lsp/
|
|
1350
|
-
|
|
1782
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1783
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
1784
|
+
|
|
1785
|
+
// ../lsp-core/src/lsp/workspace-edit-path.ts
|
|
1786
|
+
import { existsSync as existsSync3, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync3 } from "node:fs";
|
|
1787
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve3 } from "node:path";
|
|
1788
|
+
import { fileURLToPath } from "node:url";
|
|
1789
|
+
|
|
1790
|
+
class WorkspaceEditPathError extends Error {
|
|
1791
|
+
path;
|
|
1792
|
+
detail;
|
|
1793
|
+
name = "WorkspaceEditPathError";
|
|
1794
|
+
constructor(path, detail) {
|
|
1795
|
+
super(`${detail}: ${path}`);
|
|
1796
|
+
this.path = path;
|
|
1797
|
+
this.detail = detail;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
1801
|
+
const relativePath = relative3(workspaceRoot, filePath);
|
|
1802
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
|
|
1803
|
+
}
|
|
1804
|
+
function canonicalizeMissingPath(filePath) {
|
|
1805
|
+
let ancestor = filePath;
|
|
1806
|
+
while (!existsSync3(ancestor)) {
|
|
1807
|
+
const parent = dirname2(ancestor);
|
|
1808
|
+
if (parent === ancestor)
|
|
1809
|
+
throw new WorkspaceEditPathError(filePath, "no existing ancestor");
|
|
1810
|
+
ancestor = parent;
|
|
1811
|
+
}
|
|
1812
|
+
return resolve3(realpathSync3(ancestor), relative3(ancestor, filePath));
|
|
1813
|
+
}
|
|
1814
|
+
function canonicalWorkspaceRoot(workspaceRoot) {
|
|
1351
1815
|
try {
|
|
1352
|
-
|
|
1816
|
+
const canonical = realpathSync3(resolve3(workspaceRoot));
|
|
1817
|
+
if (!lstatSync(canonical).isDirectory()) {
|
|
1818
|
+
return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
|
|
1819
|
+
}
|
|
1820
|
+
return {
|
|
1821
|
+
success: true,
|
|
1822
|
+
path: canonical,
|
|
1823
|
+
requestedPath: resolve3(workspaceRoot),
|
|
1824
|
+
followedSymbolicLink: existsSync3(resolve3(workspaceRoot)) && lstatSync(resolve3(workspaceRoot)).isSymbolicLink()
|
|
1825
|
+
};
|
|
1353
1826
|
} catch (error) {
|
|
1354
|
-
|
|
1827
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1828
|
+
return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
|
|
1355
1829
|
}
|
|
1356
1830
|
}
|
|
1357
|
-
function
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1831
|
+
function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
|
|
1832
|
+
let requestedPath;
|
|
1833
|
+
try {
|
|
1834
|
+
const parsed = new URL(uri);
|
|
1835
|
+
if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
|
|
1836
|
+
return { success: false, error: `non-file URI ${uri}` };
|
|
1837
|
+
}
|
|
1838
|
+
requestedPath = resolve3(fileURLToPath(parsed));
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1841
|
+
return { success: false, error: `non-file URI ${uri}: ${detail}` };
|
|
1842
|
+
}
|
|
1843
|
+
try {
|
|
1844
|
+
const canonical = existsSync3(requestedPath) ? realpathSync3(requestedPath) : canonicalizeMissingPath(requestedPath);
|
|
1845
|
+
if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
|
|
1846
|
+
return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
|
|
1847
|
+
}
|
|
1848
|
+
return {
|
|
1849
|
+
success: true,
|
|
1850
|
+
path: canonical,
|
|
1851
|
+
requestedPath,
|
|
1852
|
+
followedSymbolicLink: existsSync3(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
|
|
1853
|
+
};
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1856
|
+
return { success: false, error: `${requestedPath}: ${detail}` };
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
function snapshotPath(path, includeChildren) {
|
|
1860
|
+
if (!existsSync3(path))
|
|
1861
|
+
return { kind: "missing" };
|
|
1862
|
+
const stats = lstatSync(path);
|
|
1863
|
+
if (stats.isFile())
|
|
1864
|
+
return { kind: "file", content: readFileSync2(path, "utf-8") };
|
|
1865
|
+
if (stats.isDirectory()) {
|
|
1866
|
+
return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
|
|
1867
|
+
}
|
|
1868
|
+
throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// ../lsp-core/src/lsp/workspace-edit-commit.ts
|
|
1872
|
+
var DEFAULT_IO = {
|
|
1873
|
+
writeFile(path, content) {
|
|
1874
|
+
writeFileSync(path, content, "utf-8");
|
|
1875
|
+
},
|
|
1876
|
+
rename(oldPath, newPath) {
|
|
1877
|
+
renameSync(oldPath, newPath);
|
|
1878
|
+
},
|
|
1879
|
+
remove(path, recursive) {
|
|
1880
|
+
rmSync(path, { recursive, force: false });
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
function snapshotsEqual(expected, actual) {
|
|
1884
|
+
if (expected.kind !== actual.kind)
|
|
1885
|
+
return false;
|
|
1886
|
+
if (expected.kind === "file" && actual.kind === "file")
|
|
1887
|
+
return expected.content === actual.content;
|
|
1888
|
+
if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
|
|
1889
|
+
return JSON.stringify(expected.children) === JSON.stringify(actual.children);
|
|
1890
|
+
}
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
function liveSnapshot(path, expected) {
|
|
1894
|
+
return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
|
|
1895
|
+
}
|
|
1896
|
+
function firstOperationIndex(plan) {
|
|
1897
|
+
return plan.operations[0]?.changeIndex ?? 0;
|
|
1898
|
+
}
|
|
1899
|
+
function failedCommit(plan, failure) {
|
|
1900
|
+
const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
|
|
1901
|
+
return {
|
|
1902
|
+
result: {
|
|
1903
|
+
success: false,
|
|
1904
|
+
filesModified,
|
|
1905
|
+
totalEdits,
|
|
1906
|
+
errors: [`change ${changeIndex}: ${message}`],
|
|
1907
|
+
failedChange: changeIndex,
|
|
1908
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
1909
|
+
},
|
|
1910
|
+
delta: mutationDelta(mutations),
|
|
1911
|
+
fingerprint: plan.fingerprint
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
function verifySnapshots(plan) {
|
|
1915
|
+
for (const [path, expected] of plan.snapshots) {
|
|
1916
|
+
let actual;
|
|
1917
|
+
try {
|
|
1918
|
+
actual = liveSnapshot(path, expected);
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1921
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1922
|
+
return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
|
|
1923
|
+
}
|
|
1924
|
+
if (!snapshotsEqual(expected, actual)) {
|
|
1925
|
+
const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
|
|
1926
|
+
return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
function addModifiedPath(paths, path) {
|
|
1932
|
+
if (!paths.includes(path))
|
|
1933
|
+
paths.push(path);
|
|
1934
|
+
}
|
|
1935
|
+
function reportedPath(plan, path) {
|
|
1936
|
+
return plan.reportedPathByCanonical.get(path) ?? path;
|
|
1937
|
+
}
|
|
1938
|
+
function changedPathsForMutation(mutation) {
|
|
1939
|
+
return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
|
|
1940
|
+
}
|
|
1941
|
+
function mutationDelta(operations) {
|
|
1942
|
+
const changedPaths = new Set;
|
|
1943
|
+
for (const operation of operations) {
|
|
1944
|
+
for (const path of changedPathsForMutation(operation))
|
|
1945
|
+
changedPaths.add(path);
|
|
1946
|
+
}
|
|
1947
|
+
return { operations, changedPaths: [...changedPaths].sort() };
|
|
1948
|
+
}
|
|
1949
|
+
function resolveIo(overrides) {
|
|
1950
|
+
return {
|
|
1951
|
+
writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
|
|
1952
|
+
rename: overrides?.rename ?? DEFAULT_IO.rename,
|
|
1953
|
+
remove: overrides?.remove ?? DEFAULT_IO.remove
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
function commitOperation(context, operation) {
|
|
1957
|
+
const { plan, io, accumulator } = context;
|
|
1958
|
+
if (operation.kind === "noop")
|
|
1959
|
+
return;
|
|
1960
|
+
if (operation.kind === "text") {
|
|
1961
|
+
io.writeFile(operation.path, operation.afterText);
|
|
1962
|
+
accumulator.mutations.push({
|
|
1963
|
+
kind: "text",
|
|
1964
|
+
path: operation.path,
|
|
1965
|
+
beforeText: operation.beforeText,
|
|
1966
|
+
afterText: operation.afterText
|
|
1967
|
+
});
|
|
1968
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1969
|
+
accumulator.totalEdits += operation.editCount;
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
if (operation.kind === "create") {
|
|
1973
|
+
io.writeFile(operation.path, "");
|
|
1974
|
+
accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
|
|
1975
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (operation.kind === "rename") {
|
|
1979
|
+
if (operation.replaceDestination) {
|
|
1980
|
+
const targetKind = existsSync4(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
|
|
1981
|
+
io.remove(operation.newPath, targetKind === "directory");
|
|
1982
|
+
accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
|
|
1983
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1984
|
+
}
|
|
1985
|
+
io.rename(operation.oldPath, operation.newPath);
|
|
1986
|
+
accumulator.mutations.push({
|
|
1987
|
+
kind: "rename",
|
|
1988
|
+
oldPath: operation.oldPath,
|
|
1989
|
+
newPath: operation.newPath,
|
|
1990
|
+
sourceKind: operation.sourceKind
|
|
1991
|
+
});
|
|
1992
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
io.remove(operation.path, operation.recursive);
|
|
1996
|
+
accumulator.mutations.push({
|
|
1997
|
+
kind: "delete",
|
|
1998
|
+
path: operation.path,
|
|
1999
|
+
targetKind: operation.targetKind
|
|
2000
|
+
});
|
|
2001
|
+
addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
|
|
2002
|
+
}
|
|
2003
|
+
function commitWorkspaceEditPlan(plan, options = {}) {
|
|
2004
|
+
if (options.signal?.aborted) {
|
|
2005
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2006
|
+
}
|
|
2007
|
+
const stale = verifySnapshots(plan);
|
|
2008
|
+
if (stale)
|
|
2009
|
+
return stale;
|
|
2010
|
+
if (options.signal?.aborted) {
|
|
2011
|
+
return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
|
|
2012
|
+
}
|
|
2013
|
+
const io = resolveIo(options.io);
|
|
2014
|
+
const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
|
|
2015
|
+
const context = { plan, io, accumulator };
|
|
2016
|
+
let lateAbort = false;
|
|
2017
|
+
for (const operation of plan.operations) {
|
|
2018
|
+
try {
|
|
2019
|
+
commitOperation(context, operation);
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2022
|
+
return failedCommit(plan, {
|
|
2023
|
+
message: `I/O failure during ${operation.kind}: ${detail}`,
|
|
2024
|
+
changeIndex: operation.changeIndex,
|
|
2025
|
+
mutations: accumulator.mutations,
|
|
2026
|
+
filesModified: accumulator.filesModified,
|
|
2027
|
+
totalEdits: accumulator.totalEdits,
|
|
2028
|
+
lateAbort: lateAbort || options.signal?.aborted === true
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
if (options.signal?.aborted)
|
|
2032
|
+
lateAbort = true;
|
|
2033
|
+
}
|
|
2034
|
+
const result = {
|
|
2035
|
+
success: true,
|
|
2036
|
+
filesModified: accumulator.filesModified,
|
|
2037
|
+
totalEdits: accumulator.totalEdits,
|
|
2038
|
+
errors: [],
|
|
2039
|
+
...lateAbort ? { lateAbort: true } : {}
|
|
2040
|
+
};
|
|
2041
|
+
return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
|
|
2045
|
+
import { createHash } from "node:crypto";
|
|
2046
|
+
function canonicalFingerprint(operations) {
|
|
2047
|
+
const canonical = operations.map((operation) => {
|
|
2048
|
+
switch (operation.kind) {
|
|
2049
|
+
case "text":
|
|
2050
|
+
return {
|
|
2051
|
+
kind: operation.kind,
|
|
2052
|
+
changeIndex: operation.changeIndex,
|
|
2053
|
+
path: operation.path,
|
|
2054
|
+
edits: operation.edits,
|
|
2055
|
+
version: operation.version
|
|
2056
|
+
};
|
|
2057
|
+
case "rename":
|
|
2058
|
+
return {
|
|
2059
|
+
kind: operation.kind,
|
|
2060
|
+
changeIndex: operation.changeIndex,
|
|
2061
|
+
oldPath: operation.oldPath,
|
|
2062
|
+
newPath: operation.newPath,
|
|
2063
|
+
overwrite: operation.overwrite,
|
|
2064
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2065
|
+
};
|
|
2066
|
+
case "create":
|
|
2067
|
+
return {
|
|
2068
|
+
kind: operation.kind,
|
|
2069
|
+
changeIndex: operation.changeIndex,
|
|
2070
|
+
path: operation.path,
|
|
2071
|
+
overwrite: operation.overwrite,
|
|
2072
|
+
ignoreIfExists: operation.ignoreIfExists
|
|
2073
|
+
};
|
|
2074
|
+
case "delete":
|
|
2075
|
+
return {
|
|
2076
|
+
kind: operation.kind,
|
|
2077
|
+
changeIndex: operation.changeIndex,
|
|
2078
|
+
path: operation.path,
|
|
2079
|
+
recursive: operation.recursive,
|
|
2080
|
+
ignoreIfNotExists: operation.ignoreIfNotExists
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// ../lsp-core/src/lsp/workspace-edit-types.ts
|
|
2088
|
+
class WorkspaceEditValidationError extends Error {
|
|
2089
|
+
changeIndex;
|
|
2090
|
+
detail;
|
|
2091
|
+
name = "WorkspaceEditValidationError";
|
|
2092
|
+
constructor(changeIndex, detail) {
|
|
2093
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
2094
|
+
this.changeIndex = changeIndex;
|
|
2095
|
+
this.detail = detail;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
|
|
2100
|
+
function isRecord3(value) {
|
|
2101
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2102
|
+
}
|
|
2103
|
+
function parsePosition(value) {
|
|
2104
|
+
if (!isRecord3(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
|
|
2105
|
+
return null;
|
|
2106
|
+
}
|
|
2107
|
+
return { line: value["line"], character: value["character"] };
|
|
2108
|
+
}
|
|
2109
|
+
function parseRange(value) {
|
|
2110
|
+
if (!isRecord3(value))
|
|
2111
|
+
return null;
|
|
2112
|
+
const start = parsePosition(value["start"]);
|
|
2113
|
+
const end = parsePosition(value["end"]);
|
|
2114
|
+
return start && end ? { start, end } : null;
|
|
2115
|
+
}
|
|
2116
|
+
function parseTextEdits(value, changeIndex) {
|
|
2117
|
+
if (!Array.isArray(value)) {
|
|
2118
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
|
|
2119
|
+
}
|
|
2120
|
+
const edits = [];
|
|
2121
|
+
for (const candidate of value) {
|
|
2122
|
+
if (!isRecord3(candidate) || typeof candidate["newText"] !== "string") {
|
|
2123
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
|
|
2124
|
+
}
|
|
2125
|
+
if ("annotationId" in candidate) {
|
|
2126
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
|
|
2127
|
+
}
|
|
2128
|
+
const range = parseRange(candidate["range"]);
|
|
2129
|
+
if (!range)
|
|
2130
|
+
throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
|
|
2131
|
+
edits.push({ range, newText: candidate["newText"] });
|
|
2132
|
+
}
|
|
2133
|
+
return edits;
|
|
2134
|
+
}
|
|
2135
|
+
function parseBooleanOption(options, key, changeIndex) {
|
|
2136
|
+
const value = options[key];
|
|
2137
|
+
if (value === undefined)
|
|
2138
|
+
return false;
|
|
2139
|
+
if (typeof value !== "boolean") {
|
|
2140
|
+
throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
|
|
2141
|
+
}
|
|
2142
|
+
return value;
|
|
2143
|
+
}
|
|
2144
|
+
function parseOptions(value, allowed, changeIndex) {
|
|
2145
|
+
if (value === undefined)
|
|
2146
|
+
return {};
|
|
2147
|
+
if (!isRecord3(value))
|
|
2148
|
+
throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
|
|
2149
|
+
for (const key of Object.keys(value)) {
|
|
2150
|
+
if (!allowed.includes(key))
|
|
2151
|
+
throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
|
|
2152
|
+
}
|
|
2153
|
+
const parsed = {};
|
|
2154
|
+
for (const key of allowed)
|
|
2155
|
+
parsed[key] = parseBooleanOption(value, key, changeIndex);
|
|
2156
|
+
return parsed;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
|
|
2160
|
+
function parseResourceChange(input) {
|
|
2161
|
+
const kind = input.change["kind"];
|
|
2162
|
+
if (kind === "create" || kind === "delete") {
|
|
2163
|
+
parseSinglePathResource(input, kind);
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
if (kind !== "rename") {
|
|
2167
|
+
throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
|
|
2168
|
+
}
|
|
2169
|
+
parseRename(input);
|
|
2170
|
+
}
|
|
2171
|
+
function parseSinglePathResource(input, kind) {
|
|
2172
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2173
|
+
if (typeof change["uri"] !== "string")
|
|
2174
|
+
throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
|
|
2175
|
+
const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
|
|
2176
|
+
if (!resolvedPath.success) {
|
|
2177
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
if (kind === "create") {
|
|
2181
|
+
const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2182
|
+
target.operations.push({
|
|
2183
|
+
kind,
|
|
2184
|
+
changeIndex,
|
|
2185
|
+
path: resolvedPath.path,
|
|
2186
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2187
|
+
overwrite: options2["overwrite"] ?? false,
|
|
2188
|
+
ignoreIfExists: options2["ignoreIfExists"] ?? false,
|
|
2189
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2190
|
+
});
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
|
|
2194
|
+
target.operations.push({
|
|
2195
|
+
kind,
|
|
2196
|
+
changeIndex,
|
|
2197
|
+
path: resolvedPath.path,
|
|
2198
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2199
|
+
recursive: options["recursive"] ?? false,
|
|
2200
|
+
ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
|
|
2201
|
+
followedSymbolicLink: resolvedPath.followedSymbolicLink
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
function parseRename(input) {
|
|
2205
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2206
|
+
if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
|
|
2207
|
+
throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
|
|
2208
|
+
}
|
|
2209
|
+
const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
|
|
2210
|
+
const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
|
|
2211
|
+
if (!oldPath.success || !newPath.success) {
|
|
2212
|
+
target.failures.push({
|
|
2213
|
+
changeIndex,
|
|
2214
|
+
message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
|
|
2215
|
+
});
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
|
|
2219
|
+
target.operations.push({
|
|
2220
|
+
kind: "rename",
|
|
2221
|
+
changeIndex,
|
|
2222
|
+
oldPath: oldPath.path,
|
|
2223
|
+
newPath: newPath.path,
|
|
2224
|
+
reportedOldPath: oldPath.requestedPath,
|
|
2225
|
+
reportedNewPath: newPath.requestedPath,
|
|
2226
|
+
overwrite: options["overwrite"] ?? false,
|
|
2227
|
+
ignoreIfExists: options["ignoreIfExists"] ?? false,
|
|
2228
|
+
followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// ../lsp-core/src/lsp/workspace-edit-parser.ts
|
|
2233
|
+
function failureResult(failures) {
|
|
2234
|
+
const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
|
|
2235
|
+
const first = sorted[0];
|
|
2236
|
+
return {
|
|
2237
|
+
success: false,
|
|
2238
|
+
filesModified: [],
|
|
2239
|
+
totalEdits: 0,
|
|
2240
|
+
errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
|
|
2241
|
+
...first ? { failedChange: first.changeIndex } : {}
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
function parseWorkspaceEdit(edit, workspaceRoot) {
|
|
2245
|
+
if (!isRecord3(edit))
|
|
2246
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
|
|
2247
|
+
if (edit["changeAnnotations"] !== undefined) {
|
|
2248
|
+
return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
|
|
2249
|
+
}
|
|
2250
|
+
const hasChanges = edit["changes"] !== undefined;
|
|
2251
|
+
const hasDocumentChanges = edit["documentChanges"] !== undefined;
|
|
2252
|
+
if (hasChanges && hasDocumentChanges) {
|
|
2253
|
+
return {
|
|
2254
|
+
operations: [],
|
|
2255
|
+
failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
const target = { operations: [], failures: [] };
|
|
2259
|
+
if (hasChanges)
|
|
2260
|
+
return parseChanges(edit["changes"], workspaceRoot, target);
|
|
2261
|
+
return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
|
|
2262
|
+
}
|
|
2263
|
+
function parseChanges(value, workspaceRoot, target) {
|
|
2264
|
+
if (!isRecord3(value))
|
|
2265
|
+
return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
|
|
2266
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
2267
|
+
for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
|
|
2268
|
+
const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
|
|
2269
|
+
if (!resolvedPath.success) {
|
|
2270
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2271
|
+
continue;
|
|
2272
|
+
}
|
|
2273
|
+
try {
|
|
2274
|
+
target.operations.push({
|
|
2275
|
+
kind: "text",
|
|
2276
|
+
changeIndex,
|
|
2277
|
+
path: resolvedPath.path,
|
|
2278
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2279
|
+
edits: parseTextEdits(rawEdits, changeIndex),
|
|
2280
|
+
version: null
|
|
2281
|
+
});
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2284
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
throw error;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return target;
|
|
2291
|
+
}
|
|
2292
|
+
function parseDocumentChanges(value, workspaceRoot, target) {
|
|
2293
|
+
if (value === undefined)
|
|
2294
|
+
return target;
|
|
2295
|
+
if (!Array.isArray(value)) {
|
|
2296
|
+
return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
|
|
2297
|
+
}
|
|
2298
|
+
for (const [changeIndex, change] of value.entries()) {
|
|
2299
|
+
try {
|
|
2300
|
+
parseDocumentChange({ change, changeIndex, workspaceRoot, target });
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2303
|
+
target.failures.push({ changeIndex, message: error.detail });
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
throw error;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return target;
|
|
2310
|
+
}
|
|
2311
|
+
function parseDocumentChange(input) {
|
|
2312
|
+
const { change, changeIndex, workspaceRoot, target } = input;
|
|
2313
|
+
if (!isRecord3(change))
|
|
2314
|
+
throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
|
|
2315
|
+
if ("annotationId" in change) {
|
|
2316
|
+
throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
|
|
2317
|
+
}
|
|
2318
|
+
if (typeof change["kind"] === "string") {
|
|
2319
|
+
parseResourceChange({ change, changeIndex, workspaceRoot, target });
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
const identifier = change["textDocument"];
|
|
2323
|
+
if (!isRecord3(identifier) || typeof identifier["uri"] !== "string") {
|
|
2324
|
+
throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
|
|
2325
|
+
}
|
|
2326
|
+
const version = identifier["version"];
|
|
2327
|
+
if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
|
|
2328
|
+
throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
|
|
2329
|
+
}
|
|
2330
|
+
const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
|
|
2331
|
+
if (!resolvedPath.success) {
|
|
2332
|
+
target.failures.push({ changeIndex, message: resolvedPath.error });
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
target.operations.push({
|
|
2336
|
+
kind: "text",
|
|
2337
|
+
changeIndex,
|
|
2338
|
+
path: resolvedPath.path,
|
|
2339
|
+
reportedPath: resolvedPath.requestedPath,
|
|
2340
|
+
edits: parseTextEdits(change["edits"], changeIndex),
|
|
2341
|
+
version
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2346
|
+
import { dirname as dirname3, relative as relative4, resolve as resolve4 } from "node:path";
|
|
2347
|
+
|
|
2348
|
+
// ../lsp-core/src/lsp/workspace-edit-text.ts
|
|
2349
|
+
function comparePosition(left, right) {
|
|
2350
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
2351
|
+
}
|
|
2352
|
+
function positionsEqual(left, right) {
|
|
2353
|
+
return left.line === right.line && left.character === right.character;
|
|
2354
|
+
}
|
|
2355
|
+
function rangesEqual(left, right) {
|
|
2356
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
2357
|
+
}
|
|
2358
|
+
function isEmptyRange(range) {
|
|
2359
|
+
return positionsEqual(range.start, range.end);
|
|
2360
|
+
}
|
|
2361
|
+
function formatRange(range) {
|
|
2362
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
2363
|
+
}
|
|
2364
|
+
function validatePosition(position, label, context) {
|
|
2365
|
+
const { lines, changeIndex } = context;
|
|
2366
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
2367
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
2368
|
+
}
|
|
2369
|
+
if (position.line < 0 || position.character < 0) {
|
|
2370
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
2371
|
+
}
|
|
2372
|
+
const line = lines[position.line];
|
|
2373
|
+
if (line === undefined) {
|
|
2374
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
2375
|
+
}
|
|
2376
|
+
if (position.character > line.length) {
|
|
2377
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
function validateRange(range, lines, changeIndex) {
|
|
2381
|
+
const context = { lines, changeIndex };
|
|
2382
|
+
validatePosition(range.start, "start", context);
|
|
2383
|
+
validatePosition(range.end, "end", context);
|
|
2384
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
2385
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function sortAndDeduplicate(edits) {
|
|
2389
|
+
const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
|
|
2390
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
2391
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
2392
|
+
});
|
|
2393
|
+
const unique = [];
|
|
2394
|
+
for (const entry of sorted) {
|
|
2395
|
+
const previous = unique.at(-1);
|
|
2396
|
+
if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
unique.push(entry.edit);
|
|
2400
|
+
}
|
|
2401
|
+
return unique;
|
|
2402
|
+
}
|
|
2403
|
+
function validateNoOverlap(edits, changeIndex) {
|
|
2404
|
+
for (let index = 0;index < edits.length - 1; index += 1) {
|
|
2405
|
+
const later = edits[index];
|
|
2406
|
+
const earlier = edits[index + 1];
|
|
2407
|
+
if (later === undefined || earlier === undefined)
|
|
2408
|
+
continue;
|
|
2409
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
2410
|
+
throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
function applyNormalizedTextEdits(content, edits) {
|
|
2415
|
+
const lines = content.split(`
|
|
2416
|
+
`);
|
|
2417
|
+
for (const edit of edits) {
|
|
2418
|
+
const { start, end } = edit.range;
|
|
2419
|
+
const startLine = lines[start.line];
|
|
2420
|
+
const endLine = lines[end.line];
|
|
2421
|
+
if (startLine === undefined || endLine === undefined)
|
|
2422
|
+
continue;
|
|
2423
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
2424
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
|
|
2425
|
+
`));
|
|
2426
|
+
}
|
|
2427
|
+
return lines.join(`
|
|
2428
|
+
`);
|
|
2429
|
+
}
|
|
2430
|
+
function normalizeTextEdits(content, edits, changeIndex) {
|
|
2431
|
+
const lines = content.split(`
|
|
2432
|
+
`);
|
|
2433
|
+
for (const edit of edits) {
|
|
2434
|
+
validateRange(edit.range, lines, changeIndex);
|
|
2435
|
+
}
|
|
2436
|
+
const normalized = sortAndDeduplicate(edits);
|
|
2437
|
+
validateNoOverlap(normalized, changeIndex);
|
|
2438
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// ../lsp-core/src/lsp/workspace-edit-simulation.ts
|
|
2442
|
+
function isSameOrDescendant2(candidate, parent) {
|
|
2443
|
+
const relativePath = relative4(parent, candidate);
|
|
2444
|
+
return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
|
|
2445
|
+
}
|
|
2446
|
+
function removeVirtualSubtree(virtual, path) {
|
|
2447
|
+
for (const candidate of [...virtual.keys()]) {
|
|
2448
|
+
if (isSameOrDescendant2(candidate, path))
|
|
2449
|
+
virtual.delete(candidate);
|
|
2450
|
+
}
|
|
2451
|
+
virtual.set(path, { kind: "missing" });
|
|
2452
|
+
}
|
|
2453
|
+
function moveVirtualSubtree(virtual, oldPath, newPath) {
|
|
2454
|
+
const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
|
|
2455
|
+
removeVirtualSubtree(virtual, oldPath);
|
|
2456
|
+
removeVirtualSubtree(virtual, newPath);
|
|
2457
|
+
for (const [candidate, entry] of moved) {
|
|
2458
|
+
const suffix = relative4(oldPath, candidate);
|
|
2459
|
+
virtual.set(suffix === "" ? newPath : resolve4(newPath, suffix), entry);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
function virtualDirectoryHasChildren(virtual, path) {
|
|
2463
|
+
for (const [candidate, entry] of virtual) {
|
|
2464
|
+
if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
|
|
2465
|
+
return true;
|
|
2466
|
+
}
|
|
2467
|
+
return false;
|
|
2468
|
+
}
|
|
2469
|
+
function requireVirtualParent(virtual, path, changeIndex) {
|
|
2470
|
+
if (virtual.get(dirname3(path))?.kind !== "directory") {
|
|
2471
|
+
throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
function simulateOperations(parsed, snapshots) {
|
|
2475
|
+
const virtual = new Map(snapshots);
|
|
2476
|
+
const planned = [];
|
|
2477
|
+
const failures = [];
|
|
2478
|
+
for (const operation of parsed) {
|
|
2479
|
+
try {
|
|
2480
|
+
planned.push(simulateOperation(operation, virtual));
|
|
2481
|
+
} catch (error) {
|
|
2482
|
+
if (error instanceof WorkspaceEditValidationError) {
|
|
2483
|
+
failures.push({ changeIndex: operation.changeIndex, message: error.detail });
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
throw error;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
return { operations: planned, failures };
|
|
2490
|
+
}
|
|
2491
|
+
function simulateOperation(operation, virtual) {
|
|
2492
|
+
switch (operation.kind) {
|
|
2493
|
+
case "text":
|
|
2494
|
+
return simulateText(operation, virtual);
|
|
2495
|
+
case "create":
|
|
2496
|
+
return simulateCreate(operation, virtual);
|
|
2497
|
+
case "rename":
|
|
2498
|
+
return simulateRename(operation, virtual);
|
|
2499
|
+
case "delete":
|
|
2500
|
+
return simulateDelete(operation, virtual);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
function rejectSymbolicLink(operation) {
|
|
2504
|
+
if (operation.followedSymbolicLink) {
|
|
2505
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
function simulateText(operation, virtual) {
|
|
2509
|
+
const entry = virtual.get(operation.path);
|
|
2510
|
+
if (entry?.kind !== "file")
|
|
2511
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
|
|
2512
|
+
const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
|
|
2513
|
+
virtual.set(operation.path, { kind: "file", content: normalized.text });
|
|
2514
|
+
return {
|
|
2515
|
+
kind: "text",
|
|
2516
|
+
changeIndex: operation.changeIndex,
|
|
2517
|
+
path: operation.path,
|
|
2518
|
+
beforeText: entry.content,
|
|
2519
|
+
afterText: normalized.text,
|
|
2520
|
+
editCount: normalized.edits.length,
|
|
2521
|
+
documentVersion: operation.version
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
function simulateCreate(operation, virtual) {
|
|
2525
|
+
rejectSymbolicLink(operation);
|
|
2526
|
+
requireVirtualParent(virtual, operation.path, operation.changeIndex);
|
|
2527
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2528
|
+
if (target.kind !== "missing") {
|
|
2529
|
+
if (operation.overwrite && target.kind === "file") {
|
|
2530
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2531
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
|
|
2532
|
+
}
|
|
2533
|
+
if (operation.ignoreIfExists)
|
|
2534
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2535
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
|
|
2536
|
+
}
|
|
2537
|
+
virtual.set(operation.path, { kind: "file", content: "" });
|
|
2538
|
+
return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
|
|
2539
|
+
}
|
|
2540
|
+
function simulateRename(operation, virtual) {
|
|
2541
|
+
rejectSymbolicLink(operation);
|
|
2542
|
+
const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
|
|
2543
|
+
if (source.kind === "missing") {
|
|
2544
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
|
|
2545
|
+
}
|
|
2546
|
+
if (operation.oldPath === operation.newPath)
|
|
2547
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2548
|
+
if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
|
|
2549
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
|
|
2550
|
+
}
|
|
2551
|
+
requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
|
|
2552
|
+
const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
|
|
2553
|
+
if (destination.kind !== "missing" && !operation.overwrite) {
|
|
2554
|
+
if (operation.ignoreIfExists)
|
|
2555
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2556
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
|
|
2557
|
+
}
|
|
2558
|
+
moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
|
|
2559
|
+
return {
|
|
2560
|
+
kind: "rename",
|
|
2561
|
+
changeIndex: operation.changeIndex,
|
|
2562
|
+
oldPath: operation.oldPath,
|
|
2563
|
+
newPath: operation.newPath,
|
|
2564
|
+
sourceKind: source.kind,
|
|
2565
|
+
replaceDestination: destination.kind !== "missing"
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
function simulateDelete(operation, virtual) {
|
|
2569
|
+
rejectSymbolicLink(operation);
|
|
2570
|
+
const target = virtual.get(operation.path) ?? { kind: "missing" };
|
|
2571
|
+
if (target.kind === "missing") {
|
|
2572
|
+
if (operation.ignoreIfNotExists)
|
|
2573
|
+
return { kind: "noop", changeIndex: operation.changeIndex };
|
|
2574
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
|
|
2575
|
+
}
|
|
2576
|
+
if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
|
|
2577
|
+
throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
|
|
2578
|
+
}
|
|
2579
|
+
removeVirtualSubtree(virtual, operation.path);
|
|
2580
|
+
return {
|
|
2581
|
+
kind: "delete",
|
|
2582
|
+
changeIndex: operation.changeIndex,
|
|
2583
|
+
path: operation.path,
|
|
2584
|
+
targetKind: target.kind,
|
|
2585
|
+
recursive: operation.recursive
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// ../lsp-core/src/lsp/workspace-edit-snapshot.ts
|
|
2590
|
+
import { existsSync as existsSync5, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
|
|
2591
|
+
import { dirname as dirname4, resolve as resolve5 } from "node:path";
|
|
2592
|
+
class WorkspaceSnapshotBuilder {
|
|
2593
|
+
workspaceRoot;
|
|
2594
|
+
snapshots = new Map;
|
|
2595
|
+
constructor(workspaceRoot) {
|
|
2596
|
+
this.workspaceRoot = workspaceRoot;
|
|
2597
|
+
}
|
|
2598
|
+
build(operations) {
|
|
2599
|
+
this.add(this.workspaceRoot, false);
|
|
2600
|
+
for (const operation of operations) {
|
|
2601
|
+
switch (operation.kind) {
|
|
2602
|
+
case "rename":
|
|
2603
|
+
this.add(operation.oldPath, true);
|
|
2604
|
+
this.add(operation.newPath, true);
|
|
2605
|
+
break;
|
|
2606
|
+
case "delete":
|
|
2607
|
+
this.add(operation.path, true);
|
|
2608
|
+
break;
|
|
2609
|
+
case "text":
|
|
2610
|
+
case "create":
|
|
2611
|
+
this.add(operation.path, false);
|
|
2612
|
+
break;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
return this.snapshots;
|
|
2616
|
+
}
|
|
2617
|
+
add(path, includeChildren) {
|
|
2618
|
+
let candidate = path;
|
|
2619
|
+
while (true) {
|
|
2620
|
+
const existing = this.snapshots.get(candidate);
|
|
2621
|
+
if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
|
|
2622
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
|
|
2623
|
+
}
|
|
2624
|
+
if (candidate === this.workspaceRoot)
|
|
2625
|
+
break;
|
|
2626
|
+
candidate = dirname4(candidate);
|
|
2627
|
+
}
|
|
2628
|
+
if (!includeChildren || !existsSync5(path) || !lstatSync3(path).isDirectory())
|
|
2629
|
+
return;
|
|
2630
|
+
for (const child of readdirSync2(path))
|
|
2631
|
+
this.add(resolve5(path, child), true);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
function snapshotOperations(operations, workspaceRoot) {
|
|
2635
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// ../lsp-core/src/lsp/workspace-edit-plan.ts
|
|
2639
|
+
class PlanPathIndex {
|
|
2640
|
+
firstChangeByPath = new Map;
|
|
2641
|
+
reportedPathByCanonical = new Map;
|
|
2642
|
+
build(operations) {
|
|
2643
|
+
for (const operation of operations) {
|
|
2644
|
+
switch (operation.kind) {
|
|
2645
|
+
case "rename":
|
|
2646
|
+
this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
|
|
2647
|
+
this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
|
|
2648
|
+
break;
|
|
2649
|
+
case "text":
|
|
2650
|
+
case "create":
|
|
2651
|
+
case "delete":
|
|
2652
|
+
this.add(operation.path, operation.reportedPath, operation.changeIndex);
|
|
2653
|
+
break;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
add(path, reportedPath2, changeIndex) {
|
|
2658
|
+
if (!this.firstChangeByPath.has(path))
|
|
2659
|
+
this.firstChangeByPath.set(path, changeIndex);
|
|
2660
|
+
if (!this.reportedPathByCanonical.has(path))
|
|
2661
|
+
this.reportedPathByCanonical.set(path, reportedPath2);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
function fingerprintWorkspaceEdit(edit, workspaceRoot) {
|
|
2665
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2666
|
+
if (!root.success)
|
|
2667
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2668
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2669
|
+
if (parsed.failures.length > 0)
|
|
2670
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2671
|
+
return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
|
|
2672
|
+
}
|
|
2673
|
+
function planWorkspaceEdit(edit, workspaceRoot) {
|
|
2674
|
+
const root = canonicalWorkspaceRoot(workspaceRoot);
|
|
2675
|
+
if (!root.success)
|
|
2676
|
+
return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
|
|
2677
|
+
const parsed = parseWorkspaceEdit(edit, root.path);
|
|
2678
|
+
if (parsed.failures.length > 0)
|
|
2679
|
+
return { success: false, result: failureResult(parsed.failures) };
|
|
2680
|
+
let snapshots;
|
|
2681
|
+
try {
|
|
2682
|
+
snapshots = snapshotOperations(parsed.operations, root.path);
|
|
2683
|
+
} catch (error) {
|
|
2684
|
+
return {
|
|
2685
|
+
success: false,
|
|
2686
|
+
result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
const simulated = simulateOperations(parsed.operations, snapshots);
|
|
2690
|
+
if (simulated.failures.length > 0)
|
|
2691
|
+
return { success: false, result: failureResult(simulated.failures) };
|
|
2692
|
+
const paths = new PlanPathIndex;
|
|
2693
|
+
paths.build(parsed.operations);
|
|
2694
|
+
const plan = {
|
|
2695
|
+
workspaceRoot: root.path,
|
|
2696
|
+
operations: simulated.operations,
|
|
2697
|
+
snapshots,
|
|
2698
|
+
firstChangeByPath: paths.firstChangeByPath,
|
|
2699
|
+
reportedPathByCanonical: paths.reportedPathByCanonical,
|
|
2700
|
+
fingerprint: canonicalFingerprint(parsed.operations)
|
|
2701
|
+
};
|
|
2702
|
+
return { success: true, plan };
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// ../lsp-core/src/lsp/workspace-mutation-controller.ts
|
|
2706
|
+
function failure(message, failedChange, base) {
|
|
2707
|
+
return {
|
|
2708
|
+
success: false,
|
|
2709
|
+
filesModified: base?.filesModified ?? [],
|
|
2710
|
+
totalEdits: base?.totalEdits ?? 0,
|
|
2711
|
+
errors: [message],
|
|
2712
|
+
...failedChange === undefined ? {} : { failedChange },
|
|
2713
|
+
...base?.lateAbort ? { lateAbort: true } : {}
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
function responseFor(result) {
|
|
2717
|
+
if (result.success)
|
|
2718
|
+
return { applied: true };
|
|
2719
|
+
return {
|
|
2720
|
+
applied: false,
|
|
2721
|
+
failureReason: result.errors[0] ?? "workspace edit failed",
|
|
2722
|
+
...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
function isRecord4(value) {
|
|
2726
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
class WorkspaceMutationController {
|
|
2730
|
+
workspaceRoot;
|
|
2731
|
+
documents;
|
|
2732
|
+
activeLease = null;
|
|
2733
|
+
nextLeaseId = 1;
|
|
2734
|
+
io;
|
|
2735
|
+
constructor(workspaceRoot, documents) {
|
|
2736
|
+
this.workspaceRoot = workspaceRoot;
|
|
2737
|
+
this.documents = documents;
|
|
2738
|
+
}
|
|
2739
|
+
setIo(io) {
|
|
2740
|
+
this.io = io;
|
|
2741
|
+
}
|
|
2742
|
+
acquire(signal) {
|
|
2743
|
+
if (this.activeLease)
|
|
2744
|
+
return { success: false, result: failure("workspace mutation is already in progress") };
|
|
2745
|
+
if (signal?.aborted)
|
|
2746
|
+
return { success: false, result: failure("cancelled before mutating request") };
|
|
2747
|
+
const lease = {
|
|
2748
|
+
id: this.nextLeaseId,
|
|
2749
|
+
phase: "idle",
|
|
2750
|
+
...signal === undefined ? {} : { signal }
|
|
2751
|
+
};
|
|
2752
|
+
this.nextLeaseId += 1;
|
|
2753
|
+
this.activeLease = lease;
|
|
2754
|
+
return { success: true, lease };
|
|
2755
|
+
}
|
|
2756
|
+
release(lease) {
|
|
2757
|
+
if (this.activeLease?.id !== lease.id)
|
|
2758
|
+
return;
|
|
2759
|
+
this.activeLease.phase = "sealed";
|
|
2760
|
+
this.activeLease = null;
|
|
2761
|
+
}
|
|
2762
|
+
isBeforeCommit(lease) {
|
|
2763
|
+
return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
|
|
2764
|
+
}
|
|
2765
|
+
async handleApplyEdit(params) {
|
|
2766
|
+
const lease = this.activeLease;
|
|
2767
|
+
if (!lease)
|
|
2768
|
+
return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
|
|
2769
|
+
if (lease.phase !== "idle") {
|
|
2770
|
+
return {
|
|
2771
|
+
applied: false,
|
|
2772
|
+
failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
lease.phase = "applying";
|
|
2776
|
+
lease.applyCompletion = new Promise((resolve6) => {
|
|
2777
|
+
lease.resolveApply = resolve6;
|
|
2778
|
+
});
|
|
2779
|
+
const edit = isRecord4(params) ? params["edit"] : undefined;
|
|
2780
|
+
const record2 = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
|
|
2781
|
+
lease.serverApply = record2;
|
|
2782
|
+
lease.phase = "settled";
|
|
2783
|
+
lease.resolveApply?.();
|
|
2784
|
+
return responseFor(record2.result);
|
|
2785
|
+
}
|
|
2786
|
+
async reconcileRename(leaseToken, edit) {
|
|
2787
|
+
const lease = this.requireActiveLease(leaseToken);
|
|
2788
|
+
if (!lease)
|
|
2789
|
+
return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
|
|
2790
|
+
if (lease.phase === "applying")
|
|
2791
|
+
await lease.applyCompletion;
|
|
2792
|
+
if (lease.serverApply)
|
|
2793
|
+
return this.reconcileServerApply(lease.serverApply, edit);
|
|
2794
|
+
lease.phase = "sealed";
|
|
2795
|
+
if (!edit)
|
|
2796
|
+
return { edit, apply: failure("No edit provided") };
|
|
2797
|
+
const applied = await this.applyEdit(edit, lease);
|
|
2798
|
+
return { edit, apply: applied.result };
|
|
2799
|
+
}
|
|
2800
|
+
reconcileServerApply(record2, edit) {
|
|
2801
|
+
if (!edit)
|
|
2802
|
+
return { edit, apply: record2.result };
|
|
2803
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
|
|
2804
|
+
if (fingerprint.success && record2.fingerprint !== null && fingerprint.fingerprint === record2.fingerprint) {
|
|
2805
|
+
return { edit, apply: record2.result };
|
|
2806
|
+
}
|
|
2807
|
+
return {
|
|
2808
|
+
edit,
|
|
2809
|
+
apply: failure("rename result conflicts with server-applied workspace edit", 0, record2.result)
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
async applyEdit(edit, lease) {
|
|
2813
|
+
const planned = planWorkspaceEdit(edit, this.workspaceRoot);
|
|
2814
|
+
if (!planned.success)
|
|
2815
|
+
return { fingerprint: null, result: planned.result };
|
|
2816
|
+
const versionFailure = this.documents.validateVersions(planned.plan.operations);
|
|
2817
|
+
if (versionFailure) {
|
|
2818
|
+
return {
|
|
2819
|
+
fingerprint: planned.plan.fingerprint,
|
|
2820
|
+
result: failure(versionFailure.message, versionFailure.changeIndex)
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
const commit = commitWorkspaceEditPlan(planned.plan, {
|
|
2824
|
+
...lease.signal === undefined ? {} : { signal: lease.signal },
|
|
2825
|
+
...this.io === undefined ? {} : { io: this.io }
|
|
2826
|
+
});
|
|
2827
|
+
let result = commit.result;
|
|
2828
|
+
if (commit.delta.operations.length > 0) {
|
|
2829
|
+
try {
|
|
2830
|
+
await this.documents.synchronize(commit.delta);
|
|
2831
|
+
} catch (error) {
|
|
2832
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2833
|
+
result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
if (lease.signal?.aborted && !result.lateAbort)
|
|
2837
|
+
result = { ...result, lateAbort: true };
|
|
2838
|
+
return { fingerprint: planned.plan.fingerprint, result };
|
|
2839
|
+
}
|
|
2840
|
+
requireActiveLease(lease) {
|
|
2841
|
+
return this.activeLease?.id === lease.id ? this.activeLease : null;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
// ../lsp-core/src/lsp/client.ts
|
|
2846
|
+
var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
|
|
2847
|
+
var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
|
|
2848
|
+
|
|
2849
|
+
class LspClient extends LspClientConnection {
|
|
2850
|
+
diagnosticPullErrors = [];
|
|
2851
|
+
documents;
|
|
2852
|
+
workspaceMutations;
|
|
2853
|
+
diagnosticsFreshnessTimeoutMs;
|
|
2854
|
+
constructor(root, server2, options = {}) {
|
|
2855
|
+
super(root, server2, options);
|
|
2856
|
+
this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
|
|
2857
|
+
this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
|
|
2858
|
+
versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
|
|
2859
|
+
});
|
|
2860
|
+
this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
|
|
2861
|
+
this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
|
|
2862
|
+
}
|
|
2863
|
+
getDiagnosticPullErrors() {
|
|
2864
|
+
return this.diagnosticPullErrors;
|
|
2865
|
+
}
|
|
2866
|
+
async openFile(filePath) {
|
|
2867
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2868
|
+
await this.documents.openFile(absPath);
|
|
2869
|
+
}
|
|
2870
|
+
getOpenDocumentVersion(filePath) {
|
|
2871
|
+
return this.documents.getVersion(this.resolveWorkspacePath(filePath));
|
|
2872
|
+
}
|
|
2873
|
+
getStoredDiagnostics(uri) {
|
|
2874
|
+
return [...this.documents.getStoredDiagnostics(uri)];
|
|
2875
|
+
}
|
|
2876
|
+
setWorkspaceEditIo(io) {
|
|
2877
|
+
this.workspaceMutations.setIo(io);
|
|
2878
|
+
}
|
|
2879
|
+
handlePublishDiagnostics(params) {
|
|
2880
|
+
super.handlePublishDiagnostics(params);
|
|
2881
|
+
this.documents.recordPublishedDiagnostics(params);
|
|
2882
|
+
}
|
|
2883
|
+
async definition(filePath, line, character, signal) {
|
|
2884
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2885
|
+
await this.openFile(absPath);
|
|
2886
|
+
const options = signal === undefined ? {} : { signal };
|
|
2887
|
+
return this.sendRequest("textDocument/definition", {
|
|
2888
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
2889
|
+
position: { line: line - 1, character }
|
|
2890
|
+
}, options);
|
|
2891
|
+
}
|
|
2892
|
+
async references(filePath, line, character, includeDeclaration = true, signal) {
|
|
2893
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2894
|
+
await this.openFile(absPath);
|
|
2895
|
+
const options = signal === undefined ? {} : { signal };
|
|
2896
|
+
return this.sendRequest("textDocument/references", {
|
|
2897
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
2898
|
+
position: { line: line - 1, character },
|
|
2899
|
+
context: { includeDeclaration }
|
|
2900
|
+
}, options);
|
|
2901
|
+
}
|
|
2902
|
+
async documentSymbols(filePath, signal) {
|
|
2903
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2904
|
+
await this.openFile(absPath);
|
|
2905
|
+
const options = signal === undefined ? {} : { signal };
|
|
2906
|
+
return this.sendRequest("textDocument/documentSymbol", {
|
|
2907
|
+
textDocument: { uri: pathToFileURL3(absPath).href }
|
|
2908
|
+
}, options);
|
|
2909
|
+
}
|
|
2910
|
+
async workspaceSymbols(query, signal) {
|
|
2911
|
+
const options = signal === undefined ? {} : { signal };
|
|
2912
|
+
return this.sendRequest("workspace/symbol", { query }, options);
|
|
2913
|
+
}
|
|
2914
|
+
isUnsupportedDiagnosticPullError(error) {
|
|
2915
|
+
if (!(error instanceof Error))
|
|
2916
|
+
return false;
|
|
2917
|
+
const code = "code" in error && typeof error.code === "number" ? error.code : undefined;
|
|
2918
|
+
if (code === -32601)
|
|
2919
|
+
return true;
|
|
2920
|
+
return /unsupported|not supported|method not found|unknown request/i.test(error.message);
|
|
2921
|
+
}
|
|
2922
|
+
freshnessTimeout(absPath) {
|
|
2923
|
+
return {
|
|
2924
|
+
items: [],
|
|
2925
|
+
transientError: {
|
|
2926
|
+
kind: "freshness_timeout",
|
|
2927
|
+
message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
|
|
2928
|
+
}
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
parseDiagnosticPullReport(value) {
|
|
2932
|
+
if (value.kind === "unchanged") {
|
|
2933
|
+
return {
|
|
2934
|
+
type: "unchanged",
|
|
2935
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2936
|
+
};
|
|
2937
|
+
}
|
|
2938
|
+
return {
|
|
2939
|
+
type: "full",
|
|
2940
|
+
diagnostics: value.items ?? [],
|
|
2941
|
+
...value.resultId === undefined ? {} : { resultId: value.resultId }
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
async diagnostics(filePath, signal) {
|
|
2945
|
+
signal?.throwIfAborted();
|
|
2946
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
2947
|
+
const uri = pathToFileURL3(absPath).href;
|
|
2948
|
+
await this.openFile(absPath);
|
|
2949
|
+
const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
|
|
2950
|
+
for (;; ) {
|
|
2951
|
+
signal?.throwIfAborted();
|
|
2952
|
+
const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
|
|
2953
|
+
if (!snapshot)
|
|
2954
|
+
return this.freshnessTimeout(absPath);
|
|
2955
|
+
const push = this.documents.resolvePushDiagnostics(snapshot);
|
|
2956
|
+
if (push.status === "ready")
|
|
2957
|
+
return { items: [...push.diagnostics] };
|
|
2958
|
+
let pushFallbackOnly = !this.isDiagnosticPullSupported();
|
|
2959
|
+
if (!pushFallbackOnly) {
|
|
2960
|
+
const cached = this.documents.getPullCache(snapshot);
|
|
2961
|
+
try {
|
|
2962
|
+
const remainingMs2 = deadlineAt - Date.now();
|
|
2963
|
+
if (remainingMs2 <= 0)
|
|
2964
|
+
return this.freshnessTimeout(absPath);
|
|
2965
|
+
const result = await this.sendRequest("textDocument/diagnostic", {
|
|
2966
|
+
textDocument: { uri },
|
|
2967
|
+
...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
|
|
2968
|
+
}, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
|
|
2969
|
+
if (!this.documents.isCurrentSnapshot(snapshot))
|
|
2970
|
+
continue;
|
|
2971
|
+
const report = this.parseDiagnosticPullReport(result);
|
|
2972
|
+
if (report.type === "full") {
|
|
2973
|
+
this.documents.recordPullDiagnostics(snapshot, {
|
|
2974
|
+
kind: "full",
|
|
2975
|
+
diagnostics: report.diagnostics,
|
|
2976
|
+
...report.resultId === undefined ? {} : { resultId: report.resultId }
|
|
2977
|
+
});
|
|
2978
|
+
return { items: [...report.diagnostics] };
|
|
2979
|
+
}
|
|
2980
|
+
if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
|
|
2981
|
+
return { items: [...cached.diagnostics] };
|
|
2982
|
+
}
|
|
2983
|
+
} catch (error) {
|
|
2984
|
+
if (this.isUnsupportedDiagnosticPullError(error)) {
|
|
2985
|
+
this.setDiagnosticPullSupported(false);
|
|
2986
|
+
pushFallbackOnly = true;
|
|
2987
|
+
} else if (error instanceof LspRequestTimeoutError) {
|
|
2988
|
+
pushFallbackOnly = true;
|
|
2989
|
+
} else {
|
|
2990
|
+
this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
2991
|
+
throw error;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
if (!pushFallbackOnly)
|
|
2996
|
+
continue;
|
|
2997
|
+
const remainingMs = deadlineAt - Date.now();
|
|
2998
|
+
if (remainingMs <= 0)
|
|
2999
|
+
return this.freshnessTimeout(absPath);
|
|
3000
|
+
const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
|
|
3001
|
+
await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
async prepareRename(filePath, line, character, signal) {
|
|
3005
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
3006
|
+
await this.openFile(absPath);
|
|
3007
|
+
const options = signal === undefined ? {} : { signal };
|
|
3008
|
+
return this.sendRequest("textDocument/prepareRename", {
|
|
3009
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
3010
|
+
position: { line: line - 1, character }
|
|
3011
|
+
}, options);
|
|
3012
|
+
}
|
|
3013
|
+
async rename(filePath, line, character, newName, signal) {
|
|
3014
|
+
const absPath = this.resolveWorkspacePath(filePath);
|
|
3015
|
+
await this.openFile(absPath);
|
|
3016
|
+
const acquired = this.workspaceMutations.acquire(signal);
|
|
3017
|
+
if (!acquired.success)
|
|
3018
|
+
return { edit: null, apply: acquired.result };
|
|
3019
|
+
const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
|
|
3020
|
+
try {
|
|
3021
|
+
const renameParams = {
|
|
3022
|
+
textDocument: { uri: pathToFileURL3(absPath).href },
|
|
3023
|
+
position: { line: line - 1, character },
|
|
3024
|
+
newName
|
|
3025
|
+
};
|
|
3026
|
+
const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
|
|
3027
|
+
signal: preCommitSignal.signal
|
|
3028
|
+
});
|
|
3029
|
+
return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
|
|
3030
|
+
} finally {
|
|
3031
|
+
preCommitSignal?.dispose();
|
|
3032
|
+
this.workspaceMutations.release(acquired.lease);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
resolveWorkspacePath(filePath) {
|
|
3036
|
+
return resolve6(this.root, filePath);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
function waitForDiagnosticsActivity(wait, signal) {
|
|
3040
|
+
if (!signal)
|
|
3041
|
+
return wait;
|
|
3042
|
+
if (signal.aborted)
|
|
3043
|
+
return Promise.reject(abortError2(signal));
|
|
3044
|
+
return new Promise((resolve7, reject) => {
|
|
3045
|
+
const onAbort = () => {
|
|
3046
|
+
signal.removeEventListener("abort", onAbort);
|
|
3047
|
+
reject(abortError2(signal));
|
|
3048
|
+
};
|
|
3049
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3050
|
+
wait.then(() => {
|
|
3051
|
+
signal.removeEventListener("abort", onAbort);
|
|
3052
|
+
resolve7();
|
|
3053
|
+
}, (error) => {
|
|
3054
|
+
signal.removeEventListener("abort", onAbort);
|
|
3055
|
+
reject(error);
|
|
3056
|
+
});
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
function createPreCommitAbortSignal(source, isBeforeCommit) {
|
|
3060
|
+
if (!source)
|
|
3061
|
+
return;
|
|
3062
|
+
const controller = new AbortController;
|
|
3063
|
+
const onAbort = () => {
|
|
3064
|
+
if (isBeforeCommit() && !controller.signal.aborted)
|
|
3065
|
+
controller.abort(preCommitAbortReason(source));
|
|
3066
|
+
};
|
|
3067
|
+
if (source.aborted)
|
|
3068
|
+
onAbort();
|
|
3069
|
+
else
|
|
3070
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
3071
|
+
return {
|
|
3072
|
+
signal: controller.signal,
|
|
3073
|
+
dispose: () => source.removeEventListener("abort", onAbort)
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
function preCommitAbortReason(source) {
|
|
3077
|
+
const reason = source.reason;
|
|
3078
|
+
if (reason instanceof Error && reason.name !== "AbortError")
|
|
3079
|
+
return reason;
|
|
3080
|
+
return new Error("LSP request cancelled before workspace edit commit");
|
|
3081
|
+
}
|
|
3082
|
+
function abortError2(signal) {
|
|
3083
|
+
const reason = signal.reason;
|
|
3084
|
+
if (reason instanceof Error)
|
|
3085
|
+
return reason;
|
|
3086
|
+
const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
|
|
3087
|
+
error.name = "AbortError";
|
|
3088
|
+
return error;
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
// ../lsp-core/src/lsp/process-signal-cleanup.ts
|
|
3092
|
+
function installProcessSignalCleanup(cleanup) {
|
|
3093
|
+
const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM"];
|
|
3094
|
+
const handler = () => {
|
|
3095
|
+
cleanup().catch((error) => {
|
|
3096
|
+
reportBestEffortCleanupError("signal cleanup", error);
|
|
3097
|
+
});
|
|
3098
|
+
};
|
|
3099
|
+
for (const signal of signals) {
|
|
3100
|
+
process.on(signal, handler);
|
|
3101
|
+
}
|
|
3102
|
+
return () => {
|
|
3103
|
+
for (const signal of signals) {
|
|
3104
|
+
process.removeListener(signal, handler);
|
|
3105
|
+
}
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// ../lsp-core/src/lsp/manager.ts
|
|
3110
|
+
async function stopClientBestEffort(client) {
|
|
3111
|
+
try {
|
|
3112
|
+
await client.stop();
|
|
3113
|
+
} catch (error) {
|
|
3114
|
+
reportBestEffortCleanupError("client stop", error);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
function awaitWithSignal(promise, signal) {
|
|
3118
|
+
if (!signal)
|
|
3119
|
+
return promise;
|
|
3120
|
+
return new Promise((resolve7, reject) => {
|
|
3121
|
+
let settled = false;
|
|
3122
|
+
const onAbort = () => {
|
|
3123
|
+
if (settled)
|
|
3124
|
+
return;
|
|
3125
|
+
settled = true;
|
|
1366
3126
|
reject(new DOMException("Aborted", "AbortError"));
|
|
1367
3127
|
};
|
|
1368
3128
|
if (signal.aborted) {
|
|
@@ -1375,7 +3135,7 @@ function awaitWithSignal(promise, signal) {
|
|
|
1375
3135
|
return;
|
|
1376
3136
|
settled = true;
|
|
1377
3137
|
signal.removeEventListener("abort", onAbort);
|
|
1378
|
-
|
|
3138
|
+
resolve7(value);
|
|
1379
3139
|
}, (err) => {
|
|
1380
3140
|
if (settled)
|
|
1381
3141
|
return;
|
|
@@ -1629,21 +3389,17 @@ async function disposeDefaultLspManager() {
|
|
|
1629
3389
|
}
|
|
1630
3390
|
|
|
1631
3391
|
// ../lsp-core/src/lsp/server-install-state.ts
|
|
1632
|
-
import { existsSync as
|
|
1633
|
-
import {
|
|
1634
|
-
import { dirname, isAbsolute, join as join2 } from "node:path";
|
|
3392
|
+
import { existsSync as existsSync6, mkdirSync, readFileSync as readFileSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3393
|
+
import { dirname as dirname5 } from "node:path";
|
|
1635
3394
|
function getInstallDecisionsPath() {
|
|
1636
|
-
|
|
1637
|
-
if (!override)
|
|
1638
|
-
return join2(homedir(), ".codex", "lsp-install-decisions.json");
|
|
1639
|
-
return isAbsolute(override) ? override : join2(homedir(), override);
|
|
3395
|
+
return lspRequestContext().installDecisionsPath;
|
|
1640
3396
|
}
|
|
1641
3397
|
function loadInstallDecisions() {
|
|
1642
3398
|
const path = getInstallDecisionsPath();
|
|
1643
|
-
if (!
|
|
3399
|
+
if (!existsSync6(path))
|
|
1644
3400
|
return {};
|
|
1645
3401
|
try {
|
|
1646
|
-
const parsed = JSON.parse(
|
|
3402
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
1647
3403
|
return isInstallDecisions(parsed) ? parsed : {};
|
|
1648
3404
|
} catch {
|
|
1649
3405
|
return {};
|
|
@@ -1662,28 +3418,26 @@ function isInstallDecision(value) {
|
|
|
1662
3418
|
}
|
|
1663
3419
|
function writeInstallDecisions(decisions) {
|
|
1664
3420
|
const path = getInstallDecisionsPath();
|
|
1665
|
-
mkdirSync(
|
|
3421
|
+
mkdirSync(dirname5(path), { recursive: true });
|
|
1666
3422
|
const tmpPath = `${path}.tmp`;
|
|
1667
|
-
|
|
3423
|
+
writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
|
|
1668
3424
|
`, "utf8");
|
|
1669
|
-
|
|
3425
|
+
renameSync2(tmpPath, path);
|
|
1670
3426
|
}
|
|
1671
3427
|
function isInstallDecisions(value) {
|
|
1672
|
-
return
|
|
3428
|
+
return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
|
|
1673
3429
|
}
|
|
1674
3430
|
function isInstallDecisionRecord(value) {
|
|
1675
|
-
if (!
|
|
3431
|
+
if (!isRecord5(value))
|
|
1676
3432
|
return false;
|
|
1677
3433
|
return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
|
|
1678
3434
|
}
|
|
1679
|
-
function
|
|
3435
|
+
function isRecord5(value) {
|
|
1680
3436
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1681
3437
|
}
|
|
1682
3438
|
|
|
1683
3439
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1684
|
-
import { existsSync as
|
|
1685
|
-
import { homedir as homedir2 } from "node:os";
|
|
1686
|
-
import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
3440
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
1687
3441
|
|
|
1688
3442
|
// ../lsp-core/src/lsp/server-definitions.ts
|
|
1689
3443
|
var LSP_INSTALL_HINTS = {
|
|
@@ -1833,27 +3587,17 @@ var BUILTIN_SERVERS = {
|
|
|
1833
3587
|
};
|
|
1834
3588
|
|
|
1835
3589
|
// ../lsp-core/src/lsp/config-loader.ts
|
|
1836
|
-
function resolveProjectConfigPath(path) {
|
|
1837
|
-
return isAbsolute2(path) ? path : join3(contextCwd(), path);
|
|
1838
|
-
}
|
|
1839
3590
|
function getProjectConfigPaths() {
|
|
1840
|
-
|
|
1841
|
-
if (projectOverride) {
|
|
1842
|
-
return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
|
|
1843
|
-
}
|
|
1844
|
-
return [join3(contextCwd(), ".codex", "lsp-client.json")];
|
|
3591
|
+
return lspRequestContext().projectConfigPaths;
|
|
1845
3592
|
}
|
|
1846
3593
|
function getUserConfigPath() {
|
|
1847
|
-
|
|
1848
|
-
if (!userOverride)
|
|
1849
|
-
return join3(homedir2(), ".codex", "lsp-client.json");
|
|
1850
|
-
return isAbsolute2(userOverride) ? userOverride : join3(homedir2(), userOverride);
|
|
3594
|
+
return lspRequestContext().userConfigPath;
|
|
1851
3595
|
}
|
|
1852
3596
|
function loadJsonFile(path) {
|
|
1853
|
-
if (!
|
|
3597
|
+
if (!existsSync7(path))
|
|
1854
3598
|
return null;
|
|
1855
3599
|
try {
|
|
1856
|
-
const parsed = JSON.parse(
|
|
3600
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
1857
3601
|
return isConfigJson(parsed) ? parsed : null;
|
|
1858
3602
|
} catch {
|
|
1859
3603
|
return null;
|
|
@@ -1992,16 +3736,16 @@ function applyOptionalServerFields(server2, entry) {
|
|
|
1992
3736
|
}
|
|
1993
3737
|
}
|
|
1994
3738
|
function isConfigJson(value) {
|
|
1995
|
-
if (!
|
|
3739
|
+
if (!isRecord6(value))
|
|
1996
3740
|
return false;
|
|
1997
3741
|
const lsp = value["lsp"];
|
|
1998
|
-
return lsp === undefined ||
|
|
3742
|
+
return lsp === undefined || isRecord6(lsp);
|
|
1999
3743
|
}
|
|
2000
3744
|
function parseLspEntry(value) {
|
|
2001
3745
|
return isLspEntry(value) ? value : null;
|
|
2002
3746
|
}
|
|
2003
3747
|
function isLspEntry(value) {
|
|
2004
|
-
if (!
|
|
3748
|
+
if (!isRecord6(value))
|
|
2005
3749
|
return false;
|
|
2006
3750
|
const disabled = value["disabled"];
|
|
2007
3751
|
const command = value["command"];
|
|
@@ -2009,15 +3753,15 @@ function isLspEntry(value) {
|
|
|
2009
3753
|
const priority = value["priority"];
|
|
2010
3754
|
const env = value["env"];
|
|
2011
3755
|
const initialization = value["initialization"];
|
|
2012
|
-
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 ||
|
|
3756
|
+
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));
|
|
2013
3757
|
}
|
|
2014
3758
|
function isStringArray(value) {
|
|
2015
3759
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2016
3760
|
}
|
|
2017
3761
|
function isStringRecord(value) {
|
|
2018
|
-
return
|
|
3762
|
+
return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
|
|
2019
3763
|
}
|
|
2020
|
-
function
|
|
3764
|
+
function isRecord6(value) {
|
|
2021
3765
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2022
3766
|
}
|
|
2023
3767
|
function getDisabledServerIds() {
|
|
@@ -2038,8 +3782,8 @@ function getDisabledServerIds() {
|
|
|
2038
3782
|
}
|
|
2039
3783
|
|
|
2040
3784
|
// ../lsp-core/src/lsp/server-installation.ts
|
|
2041
|
-
import { existsSync as
|
|
2042
|
-
import { delimiter as delimiter3, join as
|
|
3785
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
3786
|
+
import { delimiter as delimiter3, join as join3 } from "node:path";
|
|
2043
3787
|
function isServerInstalled(command, _workingDirectory) {
|
|
2044
3788
|
if (command.length === 0)
|
|
2045
3789
|
return false;
|
|
@@ -2047,7 +3791,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2047
3791
|
if (!cmd)
|
|
2048
3792
|
return false;
|
|
2049
3793
|
if (cmd.includes("/") || cmd.includes("\\")) {
|
|
2050
|
-
if (
|
|
3794
|
+
if (existsSync8(cmd))
|
|
2051
3795
|
return true;
|
|
2052
3796
|
}
|
|
2053
3797
|
const isWindows = process.platform === "win32";
|
|
@@ -2068,7 +3812,7 @@ function isServerInstalled(command, _workingDirectory) {
|
|
|
2068
3812
|
const paths = pathEnv.split(delimiter3);
|
|
2069
3813
|
for (const p of paths) {
|
|
2070
3814
|
for (const suffix of exts) {
|
|
2071
|
-
if (
|
|
3815
|
+
if (existsSync8(join3(p, cmd + suffix))) {
|
|
2072
3816
|
return true;
|
|
2073
3817
|
}
|
|
2074
3818
|
}
|
|
@@ -2167,39 +3911,50 @@ function getAllServers() {
|
|
|
2167
3911
|
var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
2168
3912
|
function isDirectoryPath(filePath) {
|
|
2169
3913
|
try {
|
|
2170
|
-
return
|
|
3914
|
+
return statSync3(filePath).isDirectory();
|
|
2171
3915
|
} catch {
|
|
2172
3916
|
return false;
|
|
2173
3917
|
}
|
|
2174
3918
|
}
|
|
2175
3919
|
function findWorkspaceRoot(filePath) {
|
|
2176
|
-
const abs =
|
|
3920
|
+
const abs = resolvePathInsideContext(filePath);
|
|
2177
3921
|
let dir = abs;
|
|
2178
3922
|
if (!isDirectoryPath(dir)) {
|
|
2179
|
-
dir =
|
|
3923
|
+
dir = dirname6(dir);
|
|
2180
3924
|
}
|
|
2181
3925
|
let prevDir = "";
|
|
2182
3926
|
while (dir !== prevDir) {
|
|
2183
3927
|
for (const marker of WORKSPACE_MARKERS) {
|
|
2184
|
-
if (
|
|
3928
|
+
if (existsSync9(join4(dir, marker))) {
|
|
2185
3929
|
return dir;
|
|
2186
3930
|
}
|
|
2187
3931
|
}
|
|
2188
3932
|
prevDir = dir;
|
|
2189
|
-
dir =
|
|
3933
|
+
dir = dirname6(dir);
|
|
2190
3934
|
}
|
|
2191
|
-
return
|
|
3935
|
+
return dirname6(abs);
|
|
3936
|
+
}
|
|
3937
|
+
function resolvePathInsideContext(filePath) {
|
|
3938
|
+
const cwd = contextCwd();
|
|
3939
|
+
const abs = resolve7(cwd, filePath);
|
|
3940
|
+
const canonical = canonicalizeExistingOrNearestAncestor(abs);
|
|
3941
|
+
if (!isPathInside(cwd, canonical)) {
|
|
3942
|
+
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
3943
|
+
}
|
|
3944
|
+
return canonical;
|
|
2192
3945
|
}
|
|
2193
3946
|
function formatServerLookupError(result) {
|
|
2194
3947
|
if (result.status === "not_installed") {
|
|
2195
3948
|
return formatNotInstalled(result);
|
|
2196
3949
|
}
|
|
3950
|
+
const context = lspRequestContext();
|
|
3951
|
+
const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
|
|
2197
3952
|
return [
|
|
2198
3953
|
`No LSP server configured for extension: ${result.extension}`,
|
|
2199
3954
|
"",
|
|
2200
3955
|
`Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
|
|
2201
3956
|
"",
|
|
2202
|
-
|
|
3957
|
+
`Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
|
|
2203
3958
|
" {",
|
|
2204
3959
|
' "lsp": {',
|
|
2205
3960
|
' "my-server": {',
|
|
@@ -2215,6 +3970,7 @@ function formatNotInstalled(result) {
|
|
|
2215
3970
|
const { server: server2, installHint } = result;
|
|
2216
3971
|
const extensions = server2.extensions.join(", ");
|
|
2217
3972
|
const decision = loadInstallDecision(server2.id)?.decision;
|
|
3973
|
+
const context = lspRequestContext();
|
|
2218
3974
|
if (decision === "declined") {
|
|
2219
3975
|
return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
|
|
2220
3976
|
}
|
|
@@ -2230,6 +3986,17 @@ function formatNotInstalled(result) {
|
|
|
2230
3986
|
"The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
|
|
2231
3987
|
` ${installHint}`
|
|
2232
3988
|
].join(`
|
|
3989
|
+
`);
|
|
3990
|
+
}
|
|
3991
|
+
if (!context.capabilities.installDecisionTool) {
|
|
3992
|
+
return [
|
|
3993
|
+
...header,
|
|
3994
|
+
"To install, run:",
|
|
3995
|
+
` ${installHint}`,
|
|
3996
|
+
"",
|
|
3997
|
+
"ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
|
|
3998
|
+
"Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
|
|
3999
|
+
].join(`
|
|
2233
4000
|
`);
|
|
2234
4001
|
}
|
|
2235
4002
|
return [
|
|
@@ -2241,6 +4008,7 @@ function formatNotInstalled(result) {
|
|
|
2241
4008
|
"- If the user agrees: run the install command above, then retry this tool.",
|
|
2242
4009
|
"- If the user declines, OR has NOT explicitly asked for LSP installation:",
|
|
2243
4010
|
` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
|
|
4011
|
+
` which writes to ${context.installDecisionsPath},`,
|
|
2244
4012
|
" then ignore this message and proceed WITHOUT LSP."
|
|
2245
4013
|
].join(`
|
|
2246
4014
|
`);
|
|
@@ -2254,14 +4022,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
|
|
|
2254
4022
|
"prepareRename"
|
|
2255
4023
|
]);
|
|
2256
4024
|
async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
2257
|
-
const absPath =
|
|
4025
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2258
4026
|
if (isDirectoryPath(absPath)) {
|
|
2259
4027
|
throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
|
|
2260
4028
|
}
|
|
2261
4029
|
const ext = effectiveExtension(absPath);
|
|
2262
4030
|
const result = findServerForExtension(ext);
|
|
2263
4031
|
if (result.status !== "found") {
|
|
2264
|
-
throw new LspServerLookupError(formatServerLookupError(result));
|
|
4032
|
+
throw new LspServerLookupError(formatServerLookupError(result), result);
|
|
2265
4033
|
}
|
|
2266
4034
|
const server2 = result.server;
|
|
2267
4035
|
const root = findWorkspaceRoot(absPath);
|
|
@@ -2289,11 +4057,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
|
|
|
2289
4057
|
}
|
|
2290
4058
|
|
|
2291
4059
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2292
|
-
import { existsSync as
|
|
2293
|
-
import { join as
|
|
4060
|
+
import { existsSync as existsSync10, lstatSync as lstatSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
4061
|
+
import { join as join5, resolve as resolve8 } from "node:path";
|
|
2294
4062
|
|
|
2295
4063
|
// ../lsp-core/src/lsp/formatters.ts
|
|
2296
|
-
import { fileURLToPath } from "node:url";
|
|
4064
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2297
4065
|
var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
2298
4066
|
error: 1,
|
|
2299
4067
|
warning: 2,
|
|
@@ -2301,7 +4069,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
|
|
|
2301
4069
|
hint: 4
|
|
2302
4070
|
};
|
|
2303
4071
|
function uriToPath(uri) {
|
|
2304
|
-
return
|
|
4072
|
+
return fileURLToPath2(uri);
|
|
2305
4073
|
}
|
|
2306
4074
|
function formatLocation(loc) {
|
|
2307
4075
|
if ("targetUri" in loc) {
|
|
@@ -2387,6 +4155,9 @@ function formatApplyResult(result) {
|
|
|
2387
4155
|
for (const file of result.filesModified) {
|
|
2388
4156
|
lines.push(` - ${file}`);
|
|
2389
4157
|
}
|
|
4158
|
+
if (result.lateAbort) {
|
|
4159
|
+
lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
|
|
4160
|
+
}
|
|
2390
4161
|
} else {
|
|
2391
4162
|
lines.push("Failed to apply some changes:");
|
|
2392
4163
|
for (const err of result.errors) {
|
|
@@ -2402,6 +4173,7 @@ function formatApplyResult(result) {
|
|
|
2402
4173
|
|
|
2403
4174
|
// ../lsp-core/src/lsp/directory-diagnostics.ts
|
|
2404
4175
|
var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
4176
|
+
var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
|
|
2405
4177
|
function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
2406
4178
|
const files = [];
|
|
2407
4179
|
function walk(currentDir) {
|
|
@@ -2409,17 +4181,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2409
4181
|
return;
|
|
2410
4182
|
let entries = [];
|
|
2411
4183
|
try {
|
|
2412
|
-
entries =
|
|
4184
|
+
entries = readdirSync3(currentDir);
|
|
2413
4185
|
} catch {
|
|
2414
4186
|
return;
|
|
2415
4187
|
}
|
|
2416
4188
|
for (const entry of entries) {
|
|
2417
4189
|
if (files.length >= maxFiles)
|
|
2418
4190
|
return;
|
|
2419
|
-
const fullPath =
|
|
4191
|
+
const fullPath = join5(currentDir, entry);
|
|
2420
4192
|
let stat;
|
|
2421
4193
|
try {
|
|
2422
|
-
stat =
|
|
4194
|
+
stat = lstatSync4(fullPath);
|
|
2423
4195
|
} catch {
|
|
2424
4196
|
continue;
|
|
2425
4197
|
}
|
|
@@ -2437,52 +4209,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
|
|
|
2437
4209
|
walk(dir);
|
|
2438
4210
|
return files;
|
|
2439
4211
|
}
|
|
2440
|
-
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
|
|
4212
|
+
async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
|
|
2441
4213
|
if (!extension.startsWith(".")) {
|
|
2442
4214
|
throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
|
|
2443
4215
|
}
|
|
2444
|
-
const absDir =
|
|
2445
|
-
if (!
|
|
4216
|
+
const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
|
|
4217
|
+
if (!existsSync10(absDir)) {
|
|
2446
4218
|
throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
|
|
2447
4219
|
}
|
|
2448
|
-
const serverResult = findServerForExtension(extension);
|
|
4220
|
+
const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
|
|
2449
4221
|
if (serverResult.status !== "found") {
|
|
2450
4222
|
throw new LspServerLookupError(formatServerLookupError(serverResult));
|
|
2451
4223
|
}
|
|
2452
4224
|
const server2 = serverResult.server;
|
|
2453
|
-
const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
|
|
4225
|
+
const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
|
|
2454
4226
|
const wasCapped = allFiles.length > maxFiles;
|
|
2455
4227
|
const filesToProcess = allFiles.slice(0, maxFiles);
|
|
2456
4228
|
if (filesToProcess.length === 0) {
|
|
2457
|
-
|
|
4229
|
+
const output = [
|
|
2458
4230
|
`Directory: ${absDir}`,
|
|
2459
4231
|
`Extension: ${extension}`,
|
|
2460
4232
|
"Files scanned: 0",
|
|
2461
4233
|
`No files found with extension "${extension}".`
|
|
2462
4234
|
].join(`
|
|
2463
4235
|
`);
|
|
4236
|
+
return { output, totalDiagnostics: 0, fileFailures: [] };
|
|
2464
4237
|
}
|
|
2465
|
-
const root = findWorkspaceRoot(absDir);
|
|
2466
|
-
const manager = getLspManager();
|
|
4238
|
+
const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
|
|
4239
|
+
const manager = options.manager ?? getLspManager();
|
|
2467
4240
|
const allDiagnostics = [];
|
|
2468
4241
|
const fileErrors = [];
|
|
4242
|
+
const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
|
|
4243
|
+
options.signal?.throwIfAborted();
|
|
2469
4244
|
const client = await manager.getClient(root, server2);
|
|
2470
4245
|
try {
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
file,
|
|
2482
|
-
|
|
2483
|
-
|
|
4246
|
+
let nextIndex = 0;
|
|
4247
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
|
|
4248
|
+
for (;; ) {
|
|
4249
|
+
if (options.signal?.aborted)
|
|
4250
|
+
return;
|
|
4251
|
+
const file = filesToProcess[nextIndex];
|
|
4252
|
+
nextIndex += 1;
|
|
4253
|
+
if (file === undefined)
|
|
4254
|
+
return;
|
|
4255
|
+
try {
|
|
4256
|
+
const result = await client.diagnostics(file, options.signal);
|
|
4257
|
+
const filtered = filterDiagnosticsBySeverity(result.items, severity);
|
|
4258
|
+
allDiagnostics.push(...filtered.map((diagnostic) => ({
|
|
4259
|
+
filePath: file,
|
|
4260
|
+
diagnostic
|
|
4261
|
+
})));
|
|
4262
|
+
} catch (e) {
|
|
4263
|
+
fileErrors.push({
|
|
4264
|
+
file,
|
|
4265
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
2484
4268
|
}
|
|
2485
|
-
}
|
|
4269
|
+
});
|
|
4270
|
+
await Promise.all(workers);
|
|
2486
4271
|
} finally {
|
|
2487
4272
|
manager.releaseClient(root, server2.id);
|
|
2488
4273
|
}
|
|
@@ -2510,13 +4295,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
|
|
|
2510
4295
|
lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
2511
4296
|
}
|
|
2512
4297
|
}
|
|
2513
|
-
return lines.join(`
|
|
2514
|
-
`);
|
|
4298
|
+
return { output: lines.join(`
|
|
4299
|
+
`), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
|
|
2515
4300
|
}
|
|
2516
4301
|
|
|
2517
4302
|
// ../lsp-core/src/lsp/infer-extension.ts
|
|
2518
|
-
import { lstatSync as
|
|
2519
|
-
import { join as
|
|
4303
|
+
import { lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
|
|
4304
|
+
import { join as join6 } from "node:path";
|
|
2520
4305
|
var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
|
|
2521
4306
|
var MAX_SCAN_ENTRIES = 500;
|
|
2522
4307
|
function inferExtensionFromDirectory(directory) {
|
|
@@ -2527,17 +4312,17 @@ function inferExtensionFromDirectory(directory) {
|
|
|
2527
4312
|
return;
|
|
2528
4313
|
let entries;
|
|
2529
4314
|
try {
|
|
2530
|
-
entries =
|
|
4315
|
+
entries = readdirSync4(dir);
|
|
2531
4316
|
} catch {
|
|
2532
4317
|
return;
|
|
2533
4318
|
}
|
|
2534
4319
|
for (const entry of entries) {
|
|
2535
4320
|
if (scanned >= MAX_SCAN_ENTRIES)
|
|
2536
4321
|
return;
|
|
2537
|
-
const fullPath =
|
|
4322
|
+
const fullPath = join6(dir, entry);
|
|
2538
4323
|
let stat;
|
|
2539
4324
|
try {
|
|
2540
|
-
stat =
|
|
4325
|
+
stat = lstatSync5(fullPath);
|
|
2541
4326
|
} catch {
|
|
2542
4327
|
continue;
|
|
2543
4328
|
}
|
|
@@ -2611,13 +4396,48 @@ function missingDependencyResult(error, details) {
|
|
|
2611
4396
|
details: {
|
|
2612
4397
|
...details,
|
|
2613
4398
|
error: message,
|
|
2614
|
-
errorKind: "missing_dependency"
|
|
4399
|
+
errorKind: "missing_dependency",
|
|
4400
|
+
...availabilityDetails(error)
|
|
2615
4401
|
}
|
|
2616
4402
|
};
|
|
2617
4403
|
}
|
|
4404
|
+
function availabilityDetails(error) {
|
|
4405
|
+
const availability = missingDependencyAvailability(error);
|
|
4406
|
+
return availability === null ? {} : { availability };
|
|
4407
|
+
}
|
|
4408
|
+
function missingDependencyAvailability(error) {
|
|
4409
|
+
if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
|
|
4410
|
+
return null;
|
|
4411
|
+
const context = lspRequestContext();
|
|
4412
|
+
switch (error.lookup.status) {
|
|
4413
|
+
case "not_configured":
|
|
4414
|
+
return {
|
|
4415
|
+
kind: "not_configured",
|
|
4416
|
+
extension: error.lookup.extension,
|
|
4417
|
+
availableServers: [...error.lookup.availableServers],
|
|
4418
|
+
projectConfigPaths: [...context.projectConfigPaths],
|
|
4419
|
+
userConfigPath: context.userConfigPath,
|
|
4420
|
+
installDecisionTool: context.capabilities.installDecisionTool
|
|
4421
|
+
};
|
|
4422
|
+
case "not_installed":
|
|
4423
|
+
return {
|
|
4424
|
+
kind: "not_installed",
|
|
4425
|
+
serverId: error.lookup.server.id,
|
|
4426
|
+
command: [...error.lookup.server.command],
|
|
4427
|
+
extensions: [...error.lookup.server.extensions],
|
|
4428
|
+
installHint: error.lookup.installHint,
|
|
4429
|
+
installDecisionTool: context.capabilities.installDecisionTool,
|
|
4430
|
+
installDecisionsPath: context.installDecisionsPath
|
|
4431
|
+
};
|
|
4432
|
+
default: {
|
|
4433
|
+
const exhaustive = error.lookup;
|
|
4434
|
+
return exhaustive;
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
2618
4438
|
|
|
2619
4439
|
// ../lsp-core/src/tools/parameters.ts
|
|
2620
|
-
function
|
|
4440
|
+
function isRecord7(value) {
|
|
2621
4441
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2622
4442
|
}
|
|
2623
4443
|
function requireString(params, key) {
|
|
@@ -2674,7 +4494,7 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2674
4494
|
const filePath = requireString(params, "filePath");
|
|
2675
4495
|
const severity = severityFilter(params);
|
|
2676
4496
|
try {
|
|
2677
|
-
const absPath =
|
|
4497
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
2678
4498
|
if (isDirectoryPath(absPath)) {
|
|
2679
4499
|
const extension = inferExtensionFromDirectory(absPath);
|
|
2680
4500
|
if (!extension) {
|
|
@@ -2691,18 +4511,33 @@ async function executeLspDiagnostics(params, signal) {
|
|
|
2691
4511
|
};
|
|
2692
4512
|
return text(message, details3);
|
|
2693
4513
|
}
|
|
2694
|
-
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
|
|
4514
|
+
const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
|
|
2695
4515
|
const details2 = {
|
|
2696
4516
|
filePath,
|
|
2697
4517
|
severity,
|
|
2698
4518
|
mode: "directory",
|
|
2699
4519
|
diagnostics: [],
|
|
4520
|
+
totalDiagnostics: output2.totalDiagnostics,
|
|
4521
|
+
truncated: false,
|
|
4522
|
+
fileFailures: [...output2.fileFailures]
|
|
4523
|
+
};
|
|
4524
|
+
return text(output2.output, details2);
|
|
4525
|
+
}
|
|
4526
|
+
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
|
|
4527
|
+
if (result.transientError) {
|
|
4528
|
+
const message = result.transientError.message;
|
|
4529
|
+
const details2 = {
|
|
4530
|
+
filePath,
|
|
4531
|
+
severity,
|
|
4532
|
+
mode: "file",
|
|
4533
|
+
diagnostics: [],
|
|
2700
4534
|
totalDiagnostics: 0,
|
|
2701
|
-
truncated: false
|
|
4535
|
+
truncated: false,
|
|
4536
|
+
error: message,
|
|
4537
|
+
errorKind: result.transientError.kind
|
|
2702
4538
|
};
|
|
2703
|
-
return text(
|
|
4539
|
+
return text(message, details2, true);
|
|
2704
4540
|
}
|
|
2705
|
-
const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
|
|
2706
4541
|
const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
|
|
2707
4542
|
const total = diagnostics.length;
|
|
2708
4543
|
const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
|
|
@@ -2763,7 +4598,7 @@ async function executeLspGotoDefinition(params, signal) {
|
|
|
2763
4598
|
const line = requireNumber(params, "line");
|
|
2764
4599
|
const character = requireNumber(params, "character");
|
|
2765
4600
|
try {
|
|
2766
|
-
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
|
|
4601
|
+
const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
|
|
2767
4602
|
const locations = !result ? [] : Array.isArray(result) ? result : [result];
|
|
2768
4603
|
const details = { filePath, line, character, locations };
|
|
2769
4604
|
if (locations.length === 0)
|
|
@@ -2788,7 +4623,7 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2788
4623
|
const character = requireNumber(params, "character");
|
|
2789
4624
|
const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
|
|
2790
4625
|
try {
|
|
2791
|
-
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
|
|
4626
|
+
const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
|
|
2792
4627
|
const references = Array.isArray(result) ? result : [];
|
|
2793
4628
|
const total = references.length;
|
|
2794
4629
|
const truncated = total > DEFAULT_MAX_REFERENCES;
|
|
@@ -2824,181 +4659,13 @@ async function executeLspFindReferences(params, signal) {
|
|
|
2824
4659
|
}
|
|
2825
4660
|
}
|
|
2826
4661
|
|
|
2827
|
-
// ../lsp-core/src/lsp/workspace-edit.ts
|
|
2828
|
-
import { existsSync as existsSync7, readFileSync as readFileSync4, realpathSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2829
|
-
import { dirname as dirname3, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
|
|
2830
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2831
|
-
function errorMessage2(error) {
|
|
2832
|
-
return error instanceof Error ? error.message : String(error);
|
|
2833
|
-
}
|
|
2834
|
-
function isPathInsideWorkspace(filePath, workspaceRoot) {
|
|
2835
|
-
const relativePath = relative(workspaceRoot, filePath);
|
|
2836
|
-
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
|
|
2837
|
-
}
|
|
2838
|
-
function realpathForValidation(filePath) {
|
|
2839
|
-
if (existsSync7(filePath))
|
|
2840
|
-
return realpathSync(filePath);
|
|
2841
|
-
const parent = dirname3(filePath);
|
|
2842
|
-
return resolve5(realpathSync(parent), relative(parent, filePath));
|
|
2843
|
-
}
|
|
2844
|
-
function uriToWorkspacePath(uri, workspaceRoot) {
|
|
2845
|
-
let filePath;
|
|
2846
|
-
try {
|
|
2847
|
-
filePath = fileURLToPath2(uri);
|
|
2848
|
-
} catch (error) {
|
|
2849
|
-
return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
|
|
2850
|
-
}
|
|
2851
|
-
let validatedPath;
|
|
2852
|
-
try {
|
|
2853
|
-
validatedPath = realpathForValidation(filePath);
|
|
2854
|
-
} catch (error) {
|
|
2855
|
-
return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
|
|
2856
|
-
}
|
|
2857
|
-
if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
|
|
2858
|
-
return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
|
|
2859
|
-
}
|
|
2860
|
-
return { success: true, path: filePath };
|
|
2861
|
-
}
|
|
2862
|
-
function applyTextEditsToFile(filePath, edits) {
|
|
2863
|
-
try {
|
|
2864
|
-
const content = readFileSync4(filePath, "utf-8");
|
|
2865
|
-
const lines = content.split(`
|
|
2866
|
-
`);
|
|
2867
|
-
const sortedEdits = [...edits].sort((a, b) => {
|
|
2868
|
-
if (b.range.start.line !== a.range.start.line) {
|
|
2869
|
-
return b.range.start.line - a.range.start.line;
|
|
2870
|
-
}
|
|
2871
|
-
return b.range.start.character - a.range.start.character;
|
|
2872
|
-
});
|
|
2873
|
-
for (const edit of sortedEdits) {
|
|
2874
|
-
const startLine = edit.range.start.line;
|
|
2875
|
-
const startChar = edit.range.start.character;
|
|
2876
|
-
const endLine = edit.range.end.line;
|
|
2877
|
-
const endChar = edit.range.end.character;
|
|
2878
|
-
if (startLine === endLine) {
|
|
2879
|
-
const line = lines[startLine] ?? "";
|
|
2880
|
-
lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
|
|
2881
|
-
} else {
|
|
2882
|
-
const firstLine = lines[startLine] ?? "";
|
|
2883
|
-
const lastLine = lines[endLine] ?? "";
|
|
2884
|
-
const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
|
|
2885
|
-
lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
|
|
2886
|
-
`));
|
|
2887
|
-
}
|
|
2888
|
-
}
|
|
2889
|
-
writeFileSync2(filePath, lines.join(`
|
|
2890
|
-
`), "utf-8");
|
|
2891
|
-
return { success: true, editCount: edits.length };
|
|
2892
|
-
} catch (err) {
|
|
2893
|
-
return {
|
|
2894
|
-
success: false,
|
|
2895
|
-
editCount: 0,
|
|
2896
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2897
|
-
};
|
|
2898
|
-
}
|
|
2899
|
-
}
|
|
2900
|
-
function applyWorkspaceEdit(edit, options = {}) {
|
|
2901
|
-
if (!edit) {
|
|
2902
|
-
return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
|
|
2903
|
-
}
|
|
2904
|
-
const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
|
|
2905
|
-
const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
|
|
2906
|
-
if (edit.changes) {
|
|
2907
|
-
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
2908
|
-
const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
|
|
2909
|
-
if (!validatedPath.success) {
|
|
2910
|
-
result.success = false;
|
|
2911
|
-
result.errors.push(validatedPath.error);
|
|
2912
|
-
continue;
|
|
2913
|
-
}
|
|
2914
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, edits);
|
|
2915
|
-
if (applyResult.success) {
|
|
2916
|
-
result.filesModified.push(validatedPath.path);
|
|
2917
|
-
result.totalEdits += applyResult.editCount;
|
|
2918
|
-
} else {
|
|
2919
|
-
result.success = false;
|
|
2920
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
if (edit.documentChanges) {
|
|
2925
|
-
for (const change of edit.documentChanges) {
|
|
2926
|
-
if (!("kind" in change)) {
|
|
2927
|
-
const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
|
|
2928
|
-
if (!validatedPath.success) {
|
|
2929
|
-
result.success = false;
|
|
2930
|
-
result.errors.push(validatedPath.error);
|
|
2931
|
-
continue;
|
|
2932
|
-
}
|
|
2933
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
|
|
2934
|
-
if (applyResult.success) {
|
|
2935
|
-
result.filesModified.push(validatedPath.path);
|
|
2936
|
-
result.totalEdits += applyResult.editCount;
|
|
2937
|
-
} else {
|
|
2938
|
-
result.success = false;
|
|
2939
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
2940
|
-
}
|
|
2941
|
-
continue;
|
|
2942
|
-
}
|
|
2943
|
-
if (change.kind === "create") {
|
|
2944
|
-
try {
|
|
2945
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2946
|
-
if (!validatedPath.success) {
|
|
2947
|
-
result.success = false;
|
|
2948
|
-
result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
|
|
2949
|
-
continue;
|
|
2950
|
-
}
|
|
2951
|
-
writeFileSync2(validatedPath.path, "", "utf-8");
|
|
2952
|
-
result.filesModified.push(validatedPath.path);
|
|
2953
|
-
} catch (err) {
|
|
2954
|
-
result.success = false;
|
|
2955
|
-
result.errors.push(`Create ${change.uri}: ${String(err)}`);
|
|
2956
|
-
}
|
|
2957
|
-
} else if (change.kind === "rename") {
|
|
2958
|
-
try {
|
|
2959
|
-
const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
|
|
2960
|
-
const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
|
|
2961
|
-
if (!oldPath.success || !newPath.success) {
|
|
2962
|
-
const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
|
|
2963
|
-
result.success = false;
|
|
2964
|
-
result.errors.push(`Rename ${change.oldUri}: ${error}`);
|
|
2965
|
-
continue;
|
|
2966
|
-
}
|
|
2967
|
-
const content = readFileSync4(oldPath.path, "utf-8");
|
|
2968
|
-
writeFileSync2(newPath.path, content, "utf-8");
|
|
2969
|
-
unlinkSync(oldPath.path);
|
|
2970
|
-
result.filesModified.push(newPath.path);
|
|
2971
|
-
} catch (err) {
|
|
2972
|
-
result.success = false;
|
|
2973
|
-
result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
|
|
2974
|
-
}
|
|
2975
|
-
} else if (change.kind === "delete") {
|
|
2976
|
-
try {
|
|
2977
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
2978
|
-
if (!validatedPath.success) {
|
|
2979
|
-
result.success = false;
|
|
2980
|
-
result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
|
|
2981
|
-
continue;
|
|
2982
|
-
}
|
|
2983
|
-
unlinkSync(validatedPath.path);
|
|
2984
|
-
result.filesModified.push(validatedPath.path);
|
|
2985
|
-
} catch (err) {
|
|
2986
|
-
result.success = false;
|
|
2987
|
-
result.errors.push(`Delete ${change.uri}: ${String(err)}`);
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
return result;
|
|
2993
|
-
}
|
|
2994
|
-
|
|
2995
4662
|
// ../lsp-core/src/tools/rename.ts
|
|
2996
4663
|
async function executeLspPrepareRename(params, signal) {
|
|
2997
4664
|
const filePath = requireString(params, "filePath");
|
|
2998
4665
|
const line = requireNumber(params, "line");
|
|
2999
4666
|
const character = requireNumber(params, "character");
|
|
3000
4667
|
try {
|
|
3001
|
-
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
|
|
4668
|
+
const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
|
|
3002
4669
|
const details = { filePath, line, character, result };
|
|
3003
4670
|
return text(formatPrepareRenameResult(result), details);
|
|
3004
4671
|
} catch (error) {
|
|
@@ -3019,13 +4686,9 @@ async function executeLspRename(params, signal) {
|
|
|
3019
4686
|
const character = requireNumber(params, "character");
|
|
3020
4687
|
const newName = requireString(params, "newName");
|
|
3021
4688
|
try {
|
|
3022
|
-
const
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
}), "rename", clientOptions(signal));
|
|
3026
|
-
const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
|
|
3027
|
-
const details = { filePath, line, character, newName, apply, edit: edit.edit };
|
|
3028
|
-
return text(formatApplyResult(apply), details, !apply.success);
|
|
4689
|
+
const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
|
|
4690
|
+
const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
|
|
4691
|
+
return text(formatApplyResult(result.apply), details, !result.apply.success);
|
|
3029
4692
|
} catch (error) {
|
|
3030
4693
|
const missingDependency = missingDependencyResult(error, {
|
|
3031
4694
|
filePath,
|
|
@@ -3100,10 +4763,10 @@ async function executeLspSymbols(params, signal) {
|
|
|
3100
4763
|
errorKind: "missing_query"
|
|
3101
4764
|
});
|
|
3102
4765
|
}
|
|
3103
|
-
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
|
|
4766
|
+
const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
|
|
3104
4767
|
return formatSymbolsResult(filePath, scope, symbols2, limit, query);
|
|
3105
4768
|
}
|
|
3106
|
-
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
|
|
4769
|
+
const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
|
|
3107
4770
|
return formatSymbolsResult(filePath, scope, symbols, limit);
|
|
3108
4771
|
} catch (error) {
|
|
3109
4772
|
const query = optionalString(params, "query");
|
|
@@ -3269,12 +4932,12 @@ function matchesToolName(tool, name) {
|
|
|
3269
4932
|
return tool.name === name || (tool.aliases?.includes(name) ?? false);
|
|
3270
4933
|
}
|
|
3271
4934
|
function coerceToolArguments(value) {
|
|
3272
|
-
return
|
|
4935
|
+
return isRecord7(value) ? value : {};
|
|
3273
4936
|
}
|
|
3274
4937
|
// ../lsp-core/src/mcp.ts
|
|
3275
4938
|
var SERVER_NAME = "lsp";
|
|
3276
4939
|
var SERVER_VERSION = "0.1.0";
|
|
3277
|
-
async function handleLspMcpRequest(input) {
|
|
4940
|
+
async function handleLspMcpRequest(input, options = {}) {
|
|
3278
4941
|
if (!isPlainRecord(input)) {
|
|
3279
4942
|
return errorResponse(null, -32600, "Invalid Request");
|
|
3280
4943
|
}
|
|
@@ -3296,24 +4959,27 @@ async function handleLspMcpRequest(input) {
|
|
|
3296
4959
|
return successResponse(id, { tools: LSP_MCP_TOOLS.map(describeTool) });
|
|
3297
4960
|
}
|
|
3298
4961
|
if (method === "tools/call") {
|
|
3299
|
-
return handleToolCall(id, input["params"]);
|
|
4962
|
+
return handleToolCall(id, input["params"], options.signal);
|
|
3300
4963
|
}
|
|
3301
4964
|
return errorResponse(id, -32601, `Method not found: ${String(method)}`);
|
|
3302
4965
|
}
|
|
3303
|
-
async function handleToolCall(id, params) {
|
|
4966
|
+
async function handleToolCall(id, params, signal) {
|
|
3304
4967
|
if (!isPlainRecord(params) || typeof params["name"] !== "string") {
|
|
3305
4968
|
return errorResponse(id, -32602, "tools/call requires params.name");
|
|
3306
4969
|
}
|
|
3307
4970
|
try {
|
|
3308
|
-
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
|
|
4971
|
+
const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
|
|
3309
4972
|
return successResponse(id, {
|
|
3310
4973
|
content: result.content,
|
|
3311
4974
|
isError: result.isError ?? false,
|
|
3312
4975
|
details: result.details
|
|
3313
4976
|
});
|
|
3314
4977
|
} catch (error) {
|
|
4978
|
+
if (!(error instanceof Error)) {
|
|
4979
|
+
throw error;
|
|
4980
|
+
}
|
|
3315
4981
|
return successResponse(id, {
|
|
3316
|
-
content: [{ type: "text", text:
|
|
4982
|
+
content: [{ type: "text", text: error.message }],
|
|
3317
4983
|
isError: true
|
|
3318
4984
|
});
|
|
3319
4985
|
}
|
|
@@ -3334,78 +5000,409 @@ function requestedProtocolVersion(params) {
|
|
|
3334
5000
|
|
|
3335
5001
|
// src/daemon-client.ts
|
|
3336
5002
|
import { connect as connect2 } from "node:net";
|
|
5003
|
+
import { homedir as homedir3 } from "node:os";
|
|
5004
|
+
import { join as join7 } from "node:path";
|
|
3337
5005
|
|
|
3338
5006
|
// src/ensure-daemon.ts
|
|
3339
5007
|
import { spawn as spawn2 } from "node:child_process";
|
|
3340
|
-
import { closeSync as closeSync2,
|
|
5008
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync3, openSync as openSync2 } from "node:fs";
|
|
3341
5009
|
import { connect } from "node:net";
|
|
3342
|
-
import { dirname as
|
|
5010
|
+
import { dirname as dirname8 } from "node:path";
|
|
3343
5011
|
import { execPath } from "node:process";
|
|
3344
|
-
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
3345
5012
|
|
|
3346
|
-
// src/
|
|
3347
|
-
import {
|
|
3348
|
-
import {
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
5013
|
+
// src/ipc-protocol.ts
|
|
5014
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
5015
|
+
import {
|
|
5016
|
+
chmodSync,
|
|
5017
|
+
closeSync,
|
|
5018
|
+
constants,
|
|
5019
|
+
fchmodSync,
|
|
5020
|
+
fstatSync,
|
|
5021
|
+
lstatSync as lstatSync6,
|
|
5022
|
+
mkdirSync as mkdirSync2,
|
|
5023
|
+
openSync,
|
|
5024
|
+
readFileSync as readFileSync5,
|
|
5025
|
+
unlinkSync,
|
|
5026
|
+
writeSync
|
|
5027
|
+
} from "node:fs";
|
|
5028
|
+
import { dirname as dirname7 } from "node:path";
|
|
5029
|
+
var OMO_DAEMON_PROTOCOL_VERSION = 1;
|
|
5030
|
+
var AUTH_ERROR_CODE = -32001;
|
|
5031
|
+
var PROTOCOL_ERROR_CODE = -32002;
|
|
5032
|
+
var AUTH_TOKEN_BYTES = 32;
|
|
5033
|
+
|
|
5034
|
+
class UnsafePrivateDirectoryError extends Error {
|
|
5035
|
+
path;
|
|
5036
|
+
reason;
|
|
5037
|
+
name = "UnsafePrivateDirectoryError";
|
|
5038
|
+
code = "unsafe_private_directory";
|
|
5039
|
+
constructor(path, reason) {
|
|
5040
|
+
super(`unsafe private directory ${path}: ${reason}`);
|
|
5041
|
+
this.path = path;
|
|
5042
|
+
this.reason = reason;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
function authEnvelope(token) {
|
|
5046
|
+
return { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, token };
|
|
5047
|
+
}
|
|
5048
|
+
function readAuthToken(paths) {
|
|
3352
5049
|
try {
|
|
3353
|
-
|
|
3354
|
-
return
|
|
5050
|
+
const token = readFileSync5(paths.auth, "utf8").trim();
|
|
5051
|
+
return token.length > 0 ? token : null;
|
|
3355
5052
|
} catch (error) {
|
|
3356
|
-
|
|
5053
|
+
if (error instanceof Error)
|
|
5054
|
+
return null;
|
|
5055
|
+
throw error;
|
|
3357
5056
|
}
|
|
3358
5057
|
}
|
|
3359
|
-
function
|
|
5058
|
+
function readOrCreateAuthToken(paths) {
|
|
5059
|
+
const existing = readAuthToken(paths);
|
|
5060
|
+
if (existing)
|
|
5061
|
+
return existing;
|
|
5062
|
+
return createAuthToken(paths);
|
|
5063
|
+
}
|
|
5064
|
+
function rotateAuthToken(paths) {
|
|
3360
5065
|
try {
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
5066
|
+
unlinkSync(paths.auth);
|
|
5067
|
+
} catch (error) {
|
|
5068
|
+
if (!(error instanceof Error))
|
|
5069
|
+
throw error;
|
|
3365
5070
|
}
|
|
5071
|
+
return createAuthToken(paths);
|
|
3366
5072
|
}
|
|
3367
|
-
function
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
5073
|
+
function authenticateMessage(raw, expectedToken) {
|
|
5074
|
+
const id = jsonRpcId2(raw);
|
|
5075
|
+
if (!isPlainRecord(raw))
|
|
5076
|
+
return authError(id);
|
|
5077
|
+
const params = raw["params"];
|
|
5078
|
+
if (!isPlainRecord(params))
|
|
5079
|
+
return authError(id);
|
|
5080
|
+
const envelope = params["_omo"];
|
|
5081
|
+
if (!isPlainRecord(envelope))
|
|
5082
|
+
return authError(id);
|
|
5083
|
+
const protocolVersion = envelope["protocolVersion"];
|
|
5084
|
+
if (protocolVersion !== OMO_DAEMON_PROTOCOL_VERSION)
|
|
5085
|
+
return protocolError(id);
|
|
5086
|
+
const token = envelope["token"];
|
|
5087
|
+
if (typeof token !== "string" || !tokenMatches(token, expectedToken))
|
|
5088
|
+
return authError(id);
|
|
5089
|
+
const cleanParams = { ...params };
|
|
5090
|
+
delete cleanParams["_omo"];
|
|
5091
|
+
return { input: { ...raw, params: cleanParams }, id, method: typeof raw["method"] === "string" ? raw["method"] : undefined };
|
|
5092
|
+
}
|
|
5093
|
+
function isAuthErrorResponse(message) {
|
|
5094
|
+
if (!isPlainRecord(message))
|
|
5095
|
+
return false;
|
|
5096
|
+
const error = message["error"];
|
|
5097
|
+
if (!isPlainRecord(error))
|
|
5098
|
+
return false;
|
|
5099
|
+
const data = error["data"];
|
|
5100
|
+
return error["code"] === AUTH_ERROR_CODE && isPlainRecord(data) && data["code"] === "daemon_authentication_failed";
|
|
5101
|
+
}
|
|
5102
|
+
function writePrivateFile(path, data) {
|
|
5103
|
+
const fd = openSync(path, "w", 384);
|
|
5104
|
+
try {
|
|
5105
|
+
writeSync(fd, data);
|
|
5106
|
+
} finally {
|
|
5107
|
+
closeSync(fd);
|
|
3375
5108
|
}
|
|
3376
|
-
|
|
5109
|
+
setPrivateFileMode(path);
|
|
3377
5110
|
}
|
|
3378
|
-
function
|
|
5111
|
+
function ensurePrivateDirectory(path, options = {}) {
|
|
3379
5112
|
try {
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
5113
|
+
mkdirSync2(path, { recursive: true, mode: 448 });
|
|
5114
|
+
} catch (error) {
|
|
5115
|
+
if (errorCode2(error) !== "EEXIST")
|
|
5116
|
+
throw error;
|
|
5117
|
+
}
|
|
5118
|
+
if (process.platform === "win32")
|
|
5119
|
+
return;
|
|
5120
|
+
const before = validatePrivateDirectory(path, options);
|
|
5121
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
5122
|
+
try {
|
|
5123
|
+
fchmodSync(fd, 448);
|
|
5124
|
+
const after = validatePrivateDirectory(path, options);
|
|
5125
|
+
const openStats = fstatSync(fd);
|
|
5126
|
+
if (!sameDirectory(before, after) || !sameDirectory(before, openStats)) {
|
|
5127
|
+
throw new UnsafePrivateDirectoryError(path, "changed_during_chmod");
|
|
5128
|
+
}
|
|
5129
|
+
} finally {
|
|
3383
5130
|
closeSync(fd);
|
|
3384
|
-
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
5133
|
+
function setPrivateFileMode(path) {
|
|
5134
|
+
if (process.platform !== "win32")
|
|
5135
|
+
chmodSync(path, 384);
|
|
5136
|
+
}
|
|
5137
|
+
function createAuthToken(paths) {
|
|
5138
|
+
ensurePrivateDirectory(dirname7(paths.auth));
|
|
5139
|
+
const token = randomBytes(AUTH_TOKEN_BYTES).toString("base64url");
|
|
5140
|
+
let fd;
|
|
5141
|
+
try {
|
|
5142
|
+
fd = openSync(paths.auth, "wx", 384);
|
|
3385
5143
|
} catch (error) {
|
|
3386
|
-
if (error
|
|
3387
|
-
|
|
5144
|
+
if (errorCode2(error) === "EEXIST") {
|
|
5145
|
+
const existing = readAuthToken(paths);
|
|
5146
|
+
if (existing)
|
|
5147
|
+
return existing;
|
|
5148
|
+
}
|
|
3388
5149
|
throw error;
|
|
3389
5150
|
}
|
|
5151
|
+
try {
|
|
5152
|
+
writeSync(fd, `${token}
|
|
5153
|
+
`);
|
|
5154
|
+
} finally {
|
|
5155
|
+
closeSync(fd);
|
|
5156
|
+
}
|
|
5157
|
+
setPrivateFileMode(paths.auth);
|
|
5158
|
+
return token;
|
|
3390
5159
|
}
|
|
3391
|
-
function
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
5160
|
+
function errorCode2(error) {
|
|
5161
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
5162
|
+
return;
|
|
5163
|
+
const code = Reflect.get(error, "code");
|
|
5164
|
+
return typeof code === "string" ? code : undefined;
|
|
5165
|
+
}
|
|
5166
|
+
function validatePrivateDirectory(path, options) {
|
|
5167
|
+
const stats = options.lstat ? options.lstat(path) : lstatPrivateDirectory(path);
|
|
5168
|
+
if (stats.isSymbolicLink())
|
|
5169
|
+
throw new UnsafePrivateDirectoryError(path, "symlink");
|
|
5170
|
+
if (!stats.isDirectory())
|
|
5171
|
+
throw new UnsafePrivateDirectoryError(path, "not_directory");
|
|
5172
|
+
const currentUid = options.currentUid ? options.currentUid() : process.getuid?.();
|
|
5173
|
+
if (currentUid !== undefined && stats.uid !== currentUid) {
|
|
5174
|
+
throw new UnsafePrivateDirectoryError(path, "wrong_owner");
|
|
5175
|
+
}
|
|
5176
|
+
return stats;
|
|
5177
|
+
}
|
|
5178
|
+
function lstatPrivateDirectory(path) {
|
|
5179
|
+
return lstatSync6(path);
|
|
5180
|
+
}
|
|
5181
|
+
function sameDirectory(a, b) {
|
|
5182
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
5183
|
+
}
|
|
5184
|
+
function tokenMatches(candidate, expected) {
|
|
5185
|
+
const candidateBytes = Buffer.from(candidate);
|
|
5186
|
+
const expectedBytes = Buffer.from(expected);
|
|
5187
|
+
return candidateBytes.length === expectedBytes.length && timingSafeEqual(candidateBytes, expectedBytes);
|
|
5188
|
+
}
|
|
5189
|
+
function jsonRpcId2(raw) {
|
|
5190
|
+
if (!isPlainRecord(raw))
|
|
5191
|
+
return null;
|
|
5192
|
+
const id = raw["id"];
|
|
5193
|
+
return typeof id === "string" || typeof id === "number" || id === null ? id : null;
|
|
5194
|
+
}
|
|
5195
|
+
function authError(id) {
|
|
5196
|
+
return {
|
|
5197
|
+
jsonrpc: "2.0",
|
|
5198
|
+
id,
|
|
5199
|
+
error: { code: AUTH_ERROR_CODE, message: "daemon authentication failed", data: { code: "daemon_authentication_failed" } }
|
|
5200
|
+
};
|
|
5201
|
+
}
|
|
5202
|
+
function protocolError(id) {
|
|
5203
|
+
return {
|
|
5204
|
+
jsonrpc: "2.0",
|
|
5205
|
+
id,
|
|
5206
|
+
error: { code: PROTOCOL_ERROR_CODE, message: "daemon protocol mismatch", data: { code: "daemon_protocol_mismatch" } }
|
|
5207
|
+
};
|
|
3397
5208
|
}
|
|
3398
|
-
|
|
5209
|
+
|
|
5210
|
+
// src/paths.ts
|
|
5211
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5212
|
+
import { createRequire } from "node:module";
|
|
5213
|
+
import { homedir as homedir2, tmpdir, userInfo } from "node:os";
|
|
5214
|
+
import * as path from "node:path";
|
|
5215
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5216
|
+
|
|
5217
|
+
// src/runtime-contract.ts
|
|
5218
|
+
import { statSync as statSync4 } from "node:fs";
|
|
5219
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
5220
|
+
var OMO_LSP_DAEMON_DIR = "OMO_LSP_DAEMON_DIR";
|
|
5221
|
+
var OMO_LSP_DAEMON_CLI = "OMO_LSP_DAEMON_CLI";
|
|
5222
|
+
var OMO_LSP_DAEMON_VERSION = "OMO_LSP_DAEMON_VERSION";
|
|
5223
|
+
var DAEMON_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
|
|
5224
|
+
|
|
5225
|
+
class InvalidRuntimeOverrideError extends Error {
|
|
5226
|
+
code = "invalid_runtime_override";
|
|
5227
|
+
reason;
|
|
5228
|
+
constructor(reason, message) {
|
|
5229
|
+
super(message);
|
|
5230
|
+
this.name = "InvalidRuntimeOverrideError";
|
|
5231
|
+
this.reason = reason;
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
|
|
5235
|
+
class InvalidDaemonVersionError extends Error {
|
|
5236
|
+
code = "invalid_daemon_version";
|
|
5237
|
+
version;
|
|
5238
|
+
constructor(version) {
|
|
5239
|
+
super("LSP daemon version must match [A-Za-z0-9][A-Za-z0-9._+-]{0,127}");
|
|
5240
|
+
this.name = "InvalidDaemonVersionError";
|
|
5241
|
+
this.version = version;
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
function validateDaemonVersion(version) {
|
|
5245
|
+
if (!DAEMON_VERSION_PATTERN.test(version))
|
|
5246
|
+
throw new InvalidDaemonVersionError(version);
|
|
5247
|
+
return version;
|
|
5248
|
+
}
|
|
5249
|
+
function resolveDaemonRuntime(env, defaults) {
|
|
5250
|
+
const cliOverride = env[OMO_LSP_DAEMON_CLI];
|
|
5251
|
+
const versionOverride = env[OMO_LSP_DAEMON_VERSION];
|
|
5252
|
+
const hasCliOverride = cliOverride !== undefined;
|
|
5253
|
+
const hasVersionOverride = versionOverride !== undefined;
|
|
5254
|
+
if (hasCliOverride !== hasVersionOverride) {
|
|
5255
|
+
throw new InvalidRuntimeOverrideError("paired_values_required", `${OMO_LSP_DAEMON_CLI} and ${OMO_LSP_DAEMON_VERSION} must be set together`);
|
|
5256
|
+
}
|
|
5257
|
+
if (!hasCliOverride || !hasVersionOverride) {
|
|
5258
|
+
if (!isAbsolute3(defaults.cliPath)) {
|
|
5259
|
+
throw new InvalidRuntimeOverrideError("packaged_cli_must_be_absolute", "Packaged LSP daemon CLI path must be absolute");
|
|
5260
|
+
}
|
|
5261
|
+
return { cliPath: defaults.cliPath, version: validateDaemonVersion(defaults.version) };
|
|
5262
|
+
}
|
|
5263
|
+
if (!isAbsolute3(cliOverride)) {
|
|
5264
|
+
throw new InvalidRuntimeOverrideError("cli_must_be_absolute", `${OMO_LSP_DAEMON_CLI} must be an absolute path to an existing regular file`);
|
|
5265
|
+
}
|
|
5266
|
+
let cliStats;
|
|
3399
5267
|
try {
|
|
3400
|
-
|
|
3401
|
-
} catch (error) {
|
|
5268
|
+
cliStats = statSync4(cliOverride);
|
|
5269
|
+
} catch (error) {
|
|
5270
|
+
if (!(error instanceof Error))
|
|
5271
|
+
throw error;
|
|
5272
|
+
throw new InvalidRuntimeOverrideError("cli_not_found", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
|
|
5273
|
+
}
|
|
5274
|
+
if (!cliStats.isFile()) {
|
|
5275
|
+
throw new InvalidRuntimeOverrideError("cli_not_file", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
|
|
5276
|
+
}
|
|
5277
|
+
return { cliPath: cliOverride, version: validateDaemonVersion(versionOverride) };
|
|
5278
|
+
}
|
|
5279
|
+
|
|
5280
|
+
// src/paths.ts
|
|
5281
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
5282
|
+
var MAX_SOCKET_PATH_LENGTH = 100;
|
|
5283
|
+
|
|
5284
|
+
class InvalidDaemonDirectoryError extends Error {
|
|
5285
|
+
code = "invalid_daemon_directory";
|
|
5286
|
+
directory;
|
|
5287
|
+
constructor(directory) {
|
|
5288
|
+
super(`${OMO_LSP_DAEMON_DIR} must be an absolute path`);
|
|
5289
|
+
this.name = "InvalidDaemonDirectoryError";
|
|
5290
|
+
this.directory = directory;
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5293
|
+
function resolveDaemonVersion(requireFn = requireFromHere) {
|
|
5294
|
+
for (const candidate of ["./package.json", "../package.json"]) {
|
|
5295
|
+
let loaded;
|
|
5296
|
+
try {
|
|
5297
|
+
loaded = requireFn(candidate);
|
|
5298
|
+
} catch (error) {
|
|
5299
|
+
if (!(error instanceof Error))
|
|
5300
|
+
throw error;
|
|
5301
|
+
continue;
|
|
5302
|
+
}
|
|
5303
|
+
if (typeof loaded === "object" && loaded !== null && "version" in loaded) {
|
|
5304
|
+
const version = Reflect.get(loaded, "version");
|
|
5305
|
+
if (typeof version === "string")
|
|
5306
|
+
return validateDaemonVersion(version);
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
return "0";
|
|
5310
|
+
}
|
|
5311
|
+
function packagedRuntimeDefaults() {
|
|
5312
|
+
return {
|
|
5313
|
+
cliPath: fileURLToPath3(new URL("./cli.js", import.meta.url)),
|
|
5314
|
+
version: resolveDaemonVersion()
|
|
5315
|
+
};
|
|
5316
|
+
}
|
|
5317
|
+
function daemonBaseDir(env = process.env, platform = defaultDaemonPlatform()) {
|
|
5318
|
+
const override = env[OMO_LSP_DAEMON_DIR];
|
|
5319
|
+
if (override !== undefined) {
|
|
5320
|
+
if (!platform.path.isAbsolute(override))
|
|
5321
|
+
throw new InvalidDaemonDirectoryError(override);
|
|
5322
|
+
return platform.path.resolve(override);
|
|
5323
|
+
}
|
|
5324
|
+
return platform.path.resolve(platform.path.join(platform.homedir(), ".omo", "lsp-daemon"));
|
|
5325
|
+
}
|
|
5326
|
+
function daemonPaths(env = process.env, runtimeDefaults = packagedRuntimeDefaults(), platform = defaultDaemonPlatform()) {
|
|
5327
|
+
const runtime = resolveDaemonRuntime(env, runtimeDefaults);
|
|
5328
|
+
const baseDir = daemonBaseDir(env, platform);
|
|
5329
|
+
const dir = platform.path.resolve(platform.path.join(baseDir, `v${runtime.version}`));
|
|
5330
|
+
return {
|
|
5331
|
+
version: runtime.version,
|
|
5332
|
+
cliPath: runtime.cliPath,
|
|
5333
|
+
dir,
|
|
5334
|
+
socket: resolveSocketPath(dir, runtime.version, platform),
|
|
5335
|
+
lock: platform.path.join(dir, "daemon.lock"),
|
|
5336
|
+
pid: platform.path.join(dir, "daemon.pid"),
|
|
5337
|
+
auth: platform.path.join(dir, "daemon.auth"),
|
|
5338
|
+
endpoint: platform.path.join(dir, "daemon.endpoint"),
|
|
5339
|
+
owner: platform.path.join(dir, "daemon.owner"),
|
|
5340
|
+
log: platform.path.join(dir, "daemon.log")
|
|
5341
|
+
};
|
|
5342
|
+
}
|
|
5343
|
+
function defaultDaemonPlatform() {
|
|
5344
|
+
return {
|
|
5345
|
+
platform: process.platform,
|
|
5346
|
+
homedir: homedir2,
|
|
5347
|
+
tmpdir,
|
|
5348
|
+
getuid: () => typeof process.getuid === "function" ? process.getuid() : undefined,
|
|
5349
|
+
username: () => userInfo().username,
|
|
5350
|
+
path
|
|
5351
|
+
};
|
|
5352
|
+
}
|
|
5353
|
+
function resolveSocketPath(dir, version, platform) {
|
|
5354
|
+
const canonicalVersionDir = platform.path.resolve(dir);
|
|
5355
|
+
if (platform.platform === "win32") {
|
|
5356
|
+
const currentUserDiscriminator = `${platform.getuid() ?? "win"}:${platform.username()}:${platform.path.resolve(platform.homedir())}`;
|
|
5357
|
+
const digest = shortDigest(`${canonicalVersionDir}\x00${currentUserDiscriminator}`);
|
|
5358
|
+
return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
|
|
5359
|
+
}
|
|
5360
|
+
const natural = platform.path.join(canonicalVersionDir, "daemon.sock");
|
|
5361
|
+
if (natural.length < MAX_SOCKET_PATH_LENGTH)
|
|
5362
|
+
return natural;
|
|
5363
|
+
return platform.path.join(platform.tmpdir(), `omo-lsp-${version}-${shortDigest(canonicalVersionDir)}`, "daemon.sock");
|
|
5364
|
+
}
|
|
5365
|
+
function shortDigest(value) {
|
|
5366
|
+
return createHash2("sha256").update(value).digest("hex").slice(0, 16);
|
|
5367
|
+
}
|
|
5368
|
+
|
|
5369
|
+
// src/socket-jsonrpc.ts
|
|
5370
|
+
function encodeJsonLine(message) {
|
|
5371
|
+
return `${JSON.stringify(message)}
|
|
5372
|
+
`;
|
|
5373
|
+
}
|
|
5374
|
+
function createLineDecoder(onMessage, onParseError) {
|
|
5375
|
+
let buffer = "";
|
|
5376
|
+
return {
|
|
5377
|
+
push(chunk) {
|
|
5378
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
5379
|
+
let index = buffer.indexOf(`
|
|
5380
|
+
`);
|
|
5381
|
+
while (index !== -1) {
|
|
5382
|
+
const raw = buffer.slice(0, index).trim();
|
|
5383
|
+
buffer = buffer.slice(index + 1);
|
|
5384
|
+
if (raw.length > 0) {
|
|
5385
|
+
try {
|
|
5386
|
+
onMessage(JSON.parse(raw));
|
|
5387
|
+
} catch (error) {
|
|
5388
|
+
if (error instanceof Error) {
|
|
5389
|
+
onParseError?.(raw, error);
|
|
5390
|
+
} else {
|
|
5391
|
+
throw error;
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
5395
|
+
index = buffer.indexOf(`
|
|
5396
|
+
`);
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
};
|
|
3402
5400
|
}
|
|
3403
5401
|
|
|
3404
5402
|
// src/ensure-daemon.ts
|
|
3405
5403
|
var PROBE_TIMEOUT_MS = 500;
|
|
3406
5404
|
var DEFAULT_READY_TIMEOUT_MS = 5000;
|
|
3407
5405
|
var DEFAULT_POLL_INTERVAL_MS = 100;
|
|
3408
|
-
var CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
|
|
3409
5406
|
|
|
3410
5407
|
class DaemonUnreachableError extends Error {
|
|
3411
5408
|
constructor(socketPath) {
|
|
@@ -3416,58 +5413,59 @@ class DaemonUnreachableError extends Error {
|
|
|
3416
5413
|
async function ensureDaemonRunning(paths, deps = defaultEnsureDaemonDeps(), options = {}) {
|
|
3417
5414
|
const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
3418
5415
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
3419
|
-
if (await deps.probe(paths
|
|
3420
|
-
return;
|
|
3421
|
-
const lock = deps.acquireLock(paths.lock);
|
|
3422
|
-
if (!lock) {
|
|
3423
|
-
await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
|
|
5416
|
+
if (await deps.probe(paths))
|
|
3424
5417
|
return;
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
if (await deps.probe(paths.socket))
|
|
3428
|
-
return;
|
|
3429
|
-
deps.cleanupStaleSocket(paths.socket);
|
|
3430
|
-
deps.spawnDaemon(paths);
|
|
3431
|
-
await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
|
|
3432
|
-
} finally {
|
|
3433
|
-
lock.release();
|
|
3434
|
-
}
|
|
5418
|
+
deps.spawnDaemon(paths);
|
|
5419
|
+
await waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs);
|
|
3435
5420
|
}
|
|
3436
|
-
async function waitUntilReachable(
|
|
5421
|
+
async function waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs) {
|
|
3437
5422
|
const deadline = deps.now() + readyTimeoutMs;
|
|
3438
5423
|
for (;; ) {
|
|
3439
|
-
if (await deps.probe(
|
|
5424
|
+
if (await deps.probe(paths))
|
|
3440
5425
|
return;
|
|
3441
5426
|
if (deps.now() >= deadline)
|
|
3442
|
-
throw new DaemonUnreachableError(
|
|
5427
|
+
throw new DaemonUnreachableError(paths.socket);
|
|
3443
5428
|
await deps.sleep(pollIntervalMs);
|
|
3444
5429
|
}
|
|
3445
5430
|
}
|
|
3446
|
-
function
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
5431
|
+
async function probeDaemon(paths, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
5432
|
+
const token = readAuthToken(paths);
|
|
5433
|
+
if (!token)
|
|
5434
|
+
return false;
|
|
5435
|
+
return await pingDaemon(paths, token, timeoutMs) !== null;
|
|
5436
|
+
}
|
|
5437
|
+
function pingDaemon(paths, token, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
5438
|
+
return new Promise((resolve9) => {
|
|
5439
|
+
const socket = connect(paths.socket);
|
|
5440
|
+
let settled = false;
|
|
5441
|
+
const finish = (value) => {
|
|
5442
|
+
if (settled)
|
|
5443
|
+
return;
|
|
5444
|
+
settled = true;
|
|
3450
5445
|
socket.destroy();
|
|
3451
|
-
|
|
5446
|
+
resolve9(value);
|
|
3452
5447
|
};
|
|
3453
|
-
const timer = setTimeout(() => finish(
|
|
5448
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
3454
5449
|
timer.unref?.();
|
|
3455
|
-
|
|
5450
|
+
const decoder = createLineDecoder((message) => {
|
|
3456
5451
|
clearTimeout(timer);
|
|
3457
|
-
finish(
|
|
5452
|
+
finish(parsePingResponse(message));
|
|
5453
|
+
});
|
|
5454
|
+
socket.once("connect", () => {
|
|
5455
|
+
socket.write(encodeJsonLine({ jsonrpc: "2.0", id: 1, method: "omo/ping", params: { _omo: authEnvelope(token) } }));
|
|
3458
5456
|
});
|
|
5457
|
+
socket.on("data", (chunk) => decoder.push(chunk));
|
|
3459
5458
|
socket.once("error", () => {
|
|
3460
5459
|
clearTimeout(timer);
|
|
3461
|
-
finish(
|
|
5460
|
+
finish(null);
|
|
3462
5461
|
});
|
|
3463
5462
|
});
|
|
3464
5463
|
}
|
|
3465
5464
|
function spawnDaemonProcess(paths) {
|
|
3466
|
-
mkdirSync3(
|
|
5465
|
+
mkdirSync3(dirname8(paths.log), { recursive: true });
|
|
3467
5466
|
const logFd = openSync2(paths.log, "a");
|
|
3468
5467
|
try {
|
|
3469
|
-
const
|
|
3470
|
-
const child = spawn2(execPath, [cliPath, "daemon"], {
|
|
5468
|
+
const child = spawn2(execPath, [paths.cliPath, "daemon"], {
|
|
3471
5469
|
detached: true,
|
|
3472
5470
|
stdio: ["ignore", logFd, logFd]
|
|
3473
5471
|
});
|
|
@@ -3476,156 +5474,133 @@ function spawnDaemonProcess(paths) {
|
|
|
3476
5474
|
closeSync2(logFd);
|
|
3477
5475
|
}
|
|
3478
5476
|
}
|
|
3479
|
-
function resolveDaemonCliPath(env = process.env) {
|
|
3480
|
-
const override = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
|
|
3481
|
-
if (override)
|
|
3482
|
-
return override;
|
|
3483
|
-
return fileURLToPath3(new URL("./cli.js", import.meta.url));
|
|
3484
|
-
}
|
|
3485
5477
|
function defaultEnsureDaemonDeps() {
|
|
3486
5478
|
return {
|
|
3487
|
-
probe: (
|
|
3488
|
-
acquireLock: (lockPath) => tryAcquireLock(lockPath),
|
|
3489
|
-
cleanupStaleSocket: (socketPath) => {
|
|
3490
|
-
if (existsSync8(socketPath))
|
|
3491
|
-
unlinkQuietly(socketPath);
|
|
3492
|
-
},
|
|
5479
|
+
probe: (paths) => probeDaemon(paths),
|
|
3493
5480
|
spawnDaemon: (paths) => spawnDaemonProcess(paths),
|
|
3494
|
-
sleep: (ms) => new Promise((
|
|
3495
|
-
setTimeout(
|
|
5481
|
+
sleep: (ms) => new Promise((resolve9) => {
|
|
5482
|
+
setTimeout(resolve9, ms);
|
|
3496
5483
|
}),
|
|
3497
5484
|
now: () => Date.now()
|
|
3498
5485
|
};
|
|
3499
5486
|
}
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
const
|
|
3524
|
-
if (
|
|
3525
|
-
return
|
|
3526
|
-
const codexHome = env["CODEX_HOME"]?.trim();
|
|
3527
|
-
const home = codexHome && codexHome.length > 0 ? codexHome : join8(homedir3(), ".codex");
|
|
3528
|
-
return join8(home, "codex-lsp", "daemon");
|
|
3529
|
-
}
|
|
3530
|
-
function daemonPaths(env = process.env, version = resolveDaemonVersionFromEnv(env) ?? resolveDaemonVersion()) {
|
|
3531
|
-
const dir = join8(daemonBaseDir(env), `v${version}`);
|
|
3532
|
-
return {
|
|
3533
|
-
version,
|
|
3534
|
-
dir,
|
|
3535
|
-
socket: resolveSocketPath(dir, version),
|
|
3536
|
-
lock: join8(dir, "daemon.lock"),
|
|
3537
|
-
pid: join8(dir, "daemon.pid"),
|
|
3538
|
-
log: join8(dir, "daemon.log")
|
|
3539
|
-
};
|
|
3540
|
-
}
|
|
3541
|
-
function resolveDaemonVersionFromEnv(env = process.env) {
|
|
3542
|
-
const version = env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim();
|
|
3543
|
-
return version && version.length > 0 ? version : null;
|
|
3544
|
-
}
|
|
3545
|
-
function resolveSocketPath(dir, version) {
|
|
3546
|
-
const digest = createHash("sha256").update(dir).digest("hex").slice(0, 16);
|
|
3547
|
-
if (process.platform === "win32") {
|
|
3548
|
-
return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
|
|
5487
|
+
function parsePingResponse(message) {
|
|
5488
|
+
if (!message || typeof message !== "object" || Array.isArray(message))
|
|
5489
|
+
return null;
|
|
5490
|
+
const result = Reflect.get(message, "result");
|
|
5491
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
5492
|
+
return null;
|
|
5493
|
+
const pid = Reflect.get(result, "pid");
|
|
5494
|
+
const nonce = Reflect.get(result, "nonce");
|
|
5495
|
+
const startedAt = Reflect.get(result, "startedAt");
|
|
5496
|
+
const endpoint = Reflect.get(result, "endpoint");
|
|
5497
|
+
if (typeof pid !== "number" || typeof nonce !== "string" || typeof startedAt !== "string")
|
|
5498
|
+
return null;
|
|
5499
|
+
if (!endpoint || typeof endpoint !== "object" || Array.isArray(endpoint))
|
|
5500
|
+
return null;
|
|
5501
|
+
const path2 = Reflect.get(endpoint, "path");
|
|
5502
|
+
const kind = Reflect.get(endpoint, "kind");
|
|
5503
|
+
if (typeof path2 !== "string")
|
|
5504
|
+
return null;
|
|
5505
|
+
if (kind === "windows")
|
|
5506
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
|
|
5507
|
+
if (kind === "missing")
|
|
5508
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
|
|
5509
|
+
const dev = Reflect.get(endpoint, "dev");
|
|
5510
|
+
const ino = Reflect.get(endpoint, "ino");
|
|
5511
|
+
if (kind === "unix" && typeof dev === "number" && typeof ino === "number") {
|
|
5512
|
+
return { pid, nonce, startedAt, endpoint: { kind, path: path2, dev, ino } };
|
|
3549
5513
|
}
|
|
3550
|
-
|
|
3551
|
-
if (natural.length < MAX_SOCKET_PATH_LENGTH)
|
|
3552
|
-
return natural;
|
|
3553
|
-
return join8(tmpdir(), `omo-lsp-${version}-${digest}.sock`);
|
|
5514
|
+
return null;
|
|
3554
5515
|
}
|
|
3555
5516
|
|
|
3556
5517
|
// src/request-routing.ts
|
|
3557
5518
|
var CONTEXT_KEY = "_context";
|
|
5519
|
+
|
|
5520
|
+
class InvalidDaemonRequestError extends Error {
|
|
5521
|
+
name = "InvalidDaemonRequestError";
|
|
5522
|
+
}
|
|
3558
5523
|
function extractRequestContext(raw) {
|
|
3559
5524
|
if (!isPlainRecord(raw) || raw["method"] !== "tools/call")
|
|
3560
5525
|
return { input: raw, context: undefined };
|
|
3561
5526
|
const params = raw["params"];
|
|
3562
5527
|
if (!isPlainRecord(params))
|
|
3563
|
-
|
|
5528
|
+
throw new InvalidDaemonRequestError("Daemon tools/call params must be an object.");
|
|
3564
5529
|
const args = params["arguments"];
|
|
3565
5530
|
if (!isPlainRecord(args))
|
|
3566
|
-
|
|
5531
|
+
throw new InvalidDaemonRequestError("Daemon tools/call arguments must be an object.");
|
|
5532
|
+
if (!Object.hasOwn(args, CONTEXT_KEY)) {
|
|
5533
|
+
throw new InvalidDaemonRequestError("Daemon tools/call arguments must include _context.");
|
|
5534
|
+
}
|
|
3567
5535
|
const context = parseContext(args[CONTEXT_KEY]);
|
|
3568
|
-
if (!context)
|
|
3569
|
-
return { input: raw, context: undefined };
|
|
3570
5536
|
const cleanedArgs = { ...args };
|
|
3571
5537
|
delete cleanedArgs[CONTEXT_KEY];
|
|
3572
5538
|
const cleaned = { ...raw, params: { ...params, arguments: cleanedArgs } };
|
|
3573
5539
|
return { input: cleaned, context };
|
|
3574
5540
|
}
|
|
3575
|
-
function handleDaemonMessage(raw) {
|
|
3576
|
-
const
|
|
3577
|
-
if (
|
|
3578
|
-
return
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
if (
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
5541
|
+
function handleDaemonMessage(raw, state) {
|
|
5542
|
+
const authenticated = authenticateMessage(raw, state.token);
|
|
5543
|
+
if ("error" in authenticated)
|
|
5544
|
+
return Promise.resolve(authenticated);
|
|
5545
|
+
if (authenticated.method === "omo/ping") {
|
|
5546
|
+
return Promise.resolve({
|
|
5547
|
+
jsonrpc: "2.0",
|
|
5548
|
+
id: authenticated.id,
|
|
5549
|
+
result: { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, ...state.owner }
|
|
5550
|
+
});
|
|
5551
|
+
}
|
|
5552
|
+
if (authenticated.method === "$/cancelRequest") {
|
|
5553
|
+
const targetId = cancellationTargetId(authenticated.input);
|
|
5554
|
+
if (targetId !== undefined)
|
|
5555
|
+
state.activeRequests?.get(String(targetId))?.abort();
|
|
5556
|
+
return Promise.resolve(undefined);
|
|
5557
|
+
}
|
|
5558
|
+
let routed;
|
|
5559
|
+
try {
|
|
5560
|
+
routed = extractRequestContext(authenticated.input);
|
|
5561
|
+
} catch (error) {
|
|
5562
|
+
const message = error instanceof Error ? error.message : "invalid daemon request";
|
|
5563
|
+
return Promise.resolve({
|
|
5564
|
+
jsonrpc: "2.0",
|
|
5565
|
+
id: authenticated.id,
|
|
5566
|
+
error: { code: -32602, message, data: { code: "invalid_daemon_request" } }
|
|
5567
|
+
});
|
|
5568
|
+
}
|
|
5569
|
+
const { input, context } = routed;
|
|
5570
|
+
const key = routeRequestKey(authenticated.id);
|
|
5571
|
+
if (key === undefined || !state.activeRequests) {
|
|
5572
|
+
if (context)
|
|
5573
|
+
return runWithRequestContext(context, () => handleLspMcpRequest(input));
|
|
5574
|
+
return handleLspMcpRequest(input);
|
|
5575
|
+
}
|
|
5576
|
+
const controller = new AbortController;
|
|
5577
|
+
state.activeRequests.set(key, controller);
|
|
5578
|
+
const options = { signal: controller.signal };
|
|
5579
|
+
const run = context ? runWithRequestContext(context, () => handleLspMcpRequest(input, options)) : handleLspMcpRequest(input, options);
|
|
5580
|
+
return run.finally(() => {
|
|
5581
|
+
if (state.activeRequests?.get(key) === controller)
|
|
5582
|
+
state.activeRequests.delete(key);
|
|
5583
|
+
});
|
|
3592
5584
|
}
|
|
3593
|
-
function
|
|
3594
|
-
return
|
|
5585
|
+
function routeRequestKey(id) {
|
|
5586
|
+
return typeof id === "string" || typeof id === "number" ? String(id) : undefined;
|
|
3595
5587
|
}
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
5588
|
+
function cancellationTargetId(input) {
|
|
5589
|
+
const params = input["params"];
|
|
5590
|
+
if (!isPlainRecord(params))
|
|
5591
|
+
return;
|
|
5592
|
+
const id = params["id"];
|
|
5593
|
+
return typeof id === "string" || typeof id === "number" ? id : undefined;
|
|
3601
5594
|
}
|
|
3602
|
-
function
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
3607
|
-
let index = buffer.indexOf(`
|
|
3608
|
-
`);
|
|
3609
|
-
while (index !== -1) {
|
|
3610
|
-
const raw = buffer.slice(0, index).trim();
|
|
3611
|
-
buffer = buffer.slice(index + 1);
|
|
3612
|
-
if (raw.length > 0) {
|
|
3613
|
-
try {
|
|
3614
|
-
onMessage(JSON.parse(raw));
|
|
3615
|
-
} catch (error) {
|
|
3616
|
-
onParseError?.(raw, error);
|
|
3617
|
-
}
|
|
3618
|
-
}
|
|
3619
|
-
index = buffer.indexOf(`
|
|
3620
|
-
`);
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3623
|
-
};
|
|
5595
|
+
function parseContext(value) {
|
|
5596
|
+
if (!isPlainRecord(value))
|
|
5597
|
+
throw new InvalidDaemonRequestError("LSP request _context must be an object.");
|
|
5598
|
+
return parseLspRequestContext(value);
|
|
3624
5599
|
}
|
|
3625
5600
|
|
|
3626
5601
|
// src/daemon-client.ts
|
|
3627
5602
|
var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
3628
|
-
var
|
|
5603
|
+
var nextProxyRequestId = 1;
|
|
3629
5604
|
|
|
3630
5605
|
class DaemonRequestError extends Error {
|
|
3631
5606
|
requestWritten;
|
|
@@ -3635,44 +5610,70 @@ class DaemonRequestError extends Error {
|
|
|
3635
5610
|
this.requestWritten = requestWritten;
|
|
3636
5611
|
}
|
|
3637
5612
|
}
|
|
3638
|
-
|
|
5613
|
+
|
|
5614
|
+
class DaemonAuthenticationRejectedError extends DaemonRequestError {
|
|
5615
|
+
constructor() {
|
|
5616
|
+
super("daemon authentication failed before dispatch", true);
|
|
5617
|
+
this.name = "DaemonAuthenticationRejectedError";
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
class DaemonRequestCancelledError extends DaemonRequestError {
|
|
5622
|
+
constructor(requestWritten) {
|
|
5623
|
+
super("daemon request cancelled", requestWritten);
|
|
5624
|
+
this.name = "DaemonRequestCancelledError";
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
async function callToolViaDaemon(name, args, options) {
|
|
5628
|
+
const context = requireContext(options.context);
|
|
3639
5629
|
const paths = options.paths ?? daemonPaths();
|
|
3640
5630
|
const ensure = options.ensure ?? ensureDaemonRunning;
|
|
3641
5631
|
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
3642
|
-
const requestArgs = withContext(args,
|
|
5632
|
+
const requestArgs = withContext(args, context);
|
|
3643
5633
|
let lastError;
|
|
3644
|
-
|
|
5634
|
+
let authRefreshUsed = false;
|
|
5635
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
3645
5636
|
try {
|
|
3646
5637
|
await ensure(paths);
|
|
3647
|
-
|
|
5638
|
+
const token = readAuthToken(paths);
|
|
5639
|
+
if (!token)
|
|
5640
|
+
throw new DaemonRequestError("daemon auth token missing", false);
|
|
5641
|
+
const sendOptions = options.signal === undefined ? { timeoutMs } : { timeoutMs, signal: options.signal };
|
|
5642
|
+
return await sendToolCall(paths, token, name, requestArgs, sendOptions);
|
|
3648
5643
|
} catch (error) {
|
|
3649
5644
|
lastError = error;
|
|
3650
|
-
if (error instanceof
|
|
5645
|
+
if (error instanceof DaemonAuthenticationRejectedError && !authRefreshUsed) {
|
|
5646
|
+
authRefreshUsed = true;
|
|
5647
|
+
continue;
|
|
5648
|
+
}
|
|
5649
|
+
if (error instanceof DaemonRequestCancelledError)
|
|
5650
|
+
break;
|
|
5651
|
+
if (error instanceof DaemonRequestError && (error.requestWritten || !isRetryableTool(name)))
|
|
3651
5652
|
break;
|
|
3652
5653
|
}
|
|
3653
5654
|
}
|
|
3654
5655
|
return daemonUnreachableResult(paths, lastError);
|
|
3655
5656
|
}
|
|
3656
|
-
function callDiagnosticsViaDaemon(filePath, options
|
|
5657
|
+
function callDiagnosticsViaDaemon(filePath, options) {
|
|
3657
5658
|
return callToolViaDaemon("diagnostics", { filePath, severity: "error" }, options);
|
|
3658
5659
|
}
|
|
3659
|
-
var FORWARDED_ENV_KEYS = [
|
|
3660
|
-
"LSP_TOOLS_MCP_PROJECT_CONFIG",
|
|
3661
|
-
"LSP_TOOLS_MCP_USER_CONFIG",
|
|
3662
|
-
"LSP_TOOLS_MCP_INSTALL_DECISIONS"
|
|
3663
|
-
];
|
|
3664
5660
|
function currentRequestContext(env = process.env) {
|
|
3665
|
-
const
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
5661
|
+
const cwd = process.cwd();
|
|
5662
|
+
const home = env["HOME"] ?? homedir3();
|
|
5663
|
+
return parseLspRequestContext({
|
|
5664
|
+
cwd,
|
|
5665
|
+
projectConfigPaths: [join7(cwd, ".codex", "lsp-client.json")],
|
|
5666
|
+
userConfigPath: join7(home, ".codex", "lsp-client.json"),
|
|
5667
|
+
installDecisionsPath: join7(home, ".codex", "lsp-install-decisions.json"),
|
|
5668
|
+
capabilities: { installDecisionTool: true }
|
|
5669
|
+
});
|
|
5670
|
+
}
|
|
5671
|
+
function requireContext(context) {
|
|
5672
|
+
if (!context)
|
|
5673
|
+
throw new DaemonRequestError("daemon tool context is required", false);
|
|
5674
|
+
return parseLspRequestContext(context);
|
|
3672
5675
|
}
|
|
3673
5676
|
function withContext(args, context) {
|
|
3674
|
-
if (!context || context.cwd === undefined && context.env === undefined)
|
|
3675
|
-
return args;
|
|
3676
5677
|
return { ...args, [CONTEXT_KEY]: context };
|
|
3677
5678
|
}
|
|
3678
5679
|
function daemonUnreachableResult(paths, error) {
|
|
@@ -3686,39 +5687,84 @@ function daemonUnreachableResult(paths, error) {
|
|
|
3686
5687
|
`);
|
|
3687
5688
|
return { content: [{ type: "text", text: text2 }], isError: true };
|
|
3688
5689
|
}
|
|
3689
|
-
function sendToolCall(
|
|
3690
|
-
return new Promise((
|
|
3691
|
-
const socket = connect2(
|
|
5690
|
+
function sendToolCall(paths, token, name, args, options) {
|
|
5691
|
+
return new Promise((resolve9, reject) => {
|
|
5692
|
+
const socket = connect2(paths.socket);
|
|
5693
|
+
const requestId = allocateProxyRequestId();
|
|
3692
5694
|
let settled = false;
|
|
3693
5695
|
let requestWritten = false;
|
|
5696
|
+
let cancelAfterWrite = false;
|
|
5697
|
+
const cancelPayload = () => encodeJsonLine({
|
|
5698
|
+
jsonrpc: "2.0",
|
|
5699
|
+
method: "$/cancelRequest",
|
|
5700
|
+
params: { _omo: authEnvelope(token), id: requestId }
|
|
5701
|
+
});
|
|
3694
5702
|
const finish = (run) => {
|
|
3695
5703
|
if (settled)
|
|
3696
5704
|
return;
|
|
3697
5705
|
settled = true;
|
|
3698
5706
|
clearTimeout(timer);
|
|
3699
|
-
|
|
5707
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
5708
|
+
destroyAfterCancel();
|
|
3700
5709
|
run();
|
|
3701
5710
|
};
|
|
3702
|
-
const
|
|
5711
|
+
const sendCancel = () => {
|
|
5712
|
+
if (!requestWritten) {
|
|
5713
|
+
cancelAfterWrite = true;
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
if (!socket.writable)
|
|
5717
|
+
return;
|
|
5718
|
+
socket.write(cancelPayload());
|
|
5719
|
+
};
|
|
5720
|
+
const destroyAfterCancel = () => {
|
|
5721
|
+
socket.destroy();
|
|
5722
|
+
};
|
|
5723
|
+
const onAbort = () => {
|
|
5724
|
+
sendCancel();
|
|
5725
|
+
finish(() => reject(new DaemonRequestCancelledError(requestWritten)));
|
|
5726
|
+
};
|
|
5727
|
+
const timer = setTimeout(() => {
|
|
5728
|
+
sendCancel();
|
|
5729
|
+
finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten)));
|
|
5730
|
+
}, options.timeoutMs);
|
|
3703
5731
|
timer.unref();
|
|
5732
|
+
if (options.signal?.aborted) {
|
|
5733
|
+
onAbort();
|
|
5734
|
+
return;
|
|
5735
|
+
}
|
|
5736
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3704
5737
|
const decoder = createLineDecoder((message) => {
|
|
3705
|
-
|
|
5738
|
+
if (isAuthErrorResponse(message)) {
|
|
5739
|
+
finish(() => reject(new DaemonAuthenticationRejectedError));
|
|
5740
|
+
return;
|
|
5741
|
+
}
|
|
5742
|
+
const result = toToolResult(message, requestId);
|
|
3706
5743
|
if (result)
|
|
3707
|
-
finish(() =>
|
|
5744
|
+
finish(() => resolve9(result));
|
|
3708
5745
|
else
|
|
3709
5746
|
finish(() => reject(new DaemonRequestError("invalid daemon response", requestWritten)));
|
|
3710
5747
|
});
|
|
3711
5748
|
socket.once("connect", () => {
|
|
3712
|
-
|
|
3713
|
-
|
|
5749
|
+
const payload = encodeJsonLine({
|
|
5750
|
+
jsonrpc: "2.0",
|
|
5751
|
+
id: requestId,
|
|
5752
|
+
method: "tools/call",
|
|
5753
|
+
params: { _omo: authEnvelope(token), name, arguments: args }
|
|
5754
|
+
});
|
|
5755
|
+
socket.write(payload, () => {
|
|
5756
|
+
requestWritten = true;
|
|
5757
|
+
if (cancelAfterWrite && socket.writable)
|
|
5758
|
+
socket.write(cancelPayload());
|
|
5759
|
+
});
|
|
3714
5760
|
});
|
|
3715
5761
|
socket.on("data", (chunk) => decoder.push(chunk));
|
|
3716
5762
|
socket.once("error", (error) => finish(() => reject(new DaemonRequestError(error.message, requestWritten))));
|
|
3717
5763
|
socket.once("close", () => finish(() => reject(new DaemonRequestError("daemon connection closed", requestWritten))));
|
|
3718
5764
|
});
|
|
3719
5765
|
}
|
|
3720
|
-
function toToolResult(message) {
|
|
3721
|
-
if (!isPlainRecord(message) || message["id"] !==
|
|
5766
|
+
function toToolResult(message, requestId) {
|
|
5767
|
+
if (!isPlainRecord(message) || message["id"] !== requestId)
|
|
3722
5768
|
return null;
|
|
3723
5769
|
const result = message["result"];
|
|
3724
5770
|
if (!isPlainRecord(result) || !Array.isArray(result["content"]))
|
|
@@ -3729,6 +5775,16 @@ function toToolResult(message) {
|
|
|
3729
5775
|
details: result["details"]
|
|
3730
5776
|
};
|
|
3731
5777
|
}
|
|
5778
|
+
function allocateProxyRequestId() {
|
|
5779
|
+
const id = nextProxyRequestId;
|
|
5780
|
+
nextProxyRequestId += 1;
|
|
5781
|
+
if (nextProxyRequestId > Number.MAX_SAFE_INTEGER)
|
|
5782
|
+
nextProxyRequestId = 1;
|
|
5783
|
+
return id;
|
|
5784
|
+
}
|
|
5785
|
+
function isRetryableTool(name) {
|
|
5786
|
+
return name !== "rename" && name !== "lsp_rename";
|
|
5787
|
+
}
|
|
3732
5788
|
function errorText(error) {
|
|
3733
5789
|
return error instanceof Error ? error.message : String(error);
|
|
3734
5790
|
}
|
|
@@ -3738,13 +5794,21 @@ async function runMcpStdioProxy(options = {}) {
|
|
|
3738
5794
|
const input = options.input ?? process.stdin;
|
|
3739
5795
|
const output = options.output ?? process.stdout;
|
|
3740
5796
|
const paths = options.paths ?? daemonPaths();
|
|
3741
|
-
const
|
|
5797
|
+
const env = options.env ?? process.env;
|
|
5798
|
+
const cwd = options.cwd ?? inferOpenCodeProjectCwd(env["LSP_TOOLS_MCP_PROJECT_CONFIG"]);
|
|
5799
|
+
const contextEnv = cwd === undefined ? env : canonicalizeContextEnv(env);
|
|
5800
|
+
const contextInput = {
|
|
5801
|
+
env: contextEnv,
|
|
5802
|
+
...cwd === undefined ? {} : { cwd },
|
|
5803
|
+
...options.homeDir === undefined ? {} : { homeDir: options.homeDir }
|
|
5804
|
+
};
|
|
5805
|
+
const context = options.context ?? createStandaloneMcpRequestContext(contextInput);
|
|
3742
5806
|
const callOptions = { paths, context, ...options.ensure ? { ensure: options.ensure } : {} };
|
|
3743
5807
|
await runJsonRpcStdioServer({
|
|
3744
5808
|
input,
|
|
3745
5809
|
output,
|
|
3746
5810
|
idleTimeoutMs: 0,
|
|
3747
|
-
handler: handleProxyRequest,
|
|
5811
|
+
handler: (request, requestOptions) => runWithRequestContext(context, () => handleProxyRequest(request, requestOptions)),
|
|
3748
5812
|
handlerOptions: callOptions,
|
|
3749
5813
|
onHandlerError: (error) => {
|
|
3750
5814
|
process.stderr.write(`[lsp-daemon] proxy error: ${error instanceof Error ? error.message : String(error)}
|
|
@@ -3768,43 +5832,332 @@ function asToolCall(parsed) {
|
|
|
3768
5832
|
const args = params["arguments"];
|
|
3769
5833
|
return { id: jsonRpcId(parsed["id"]), name: params["name"], args: isPlainRecord(args) ? args : {} };
|
|
3770
5834
|
}
|
|
5835
|
+
function inferOpenCodeProjectCwd(projectConfigEnv) {
|
|
5836
|
+
if (!projectConfigEnv)
|
|
5837
|
+
return;
|
|
5838
|
+
for (const entry of projectConfigEnv.split(delimiter4)) {
|
|
5839
|
+
const projectRoot = projectRootFromOpenCodeConfigPath(entry);
|
|
5840
|
+
if (projectRoot)
|
|
5841
|
+
return projectRoot;
|
|
5842
|
+
}
|
|
5843
|
+
return;
|
|
5844
|
+
}
|
|
5845
|
+
function canonicalizeContextEnv(env) {
|
|
5846
|
+
return {
|
|
5847
|
+
...env,
|
|
5848
|
+
LSP_TOOLS_MCP_PROJECT_CONFIG: canonicalizePathList(env["LSP_TOOLS_MCP_PROJECT_CONFIG"]),
|
|
5849
|
+
LSP_TOOLS_MCP_USER_CONFIG: canonicalizePath(env["LSP_TOOLS_MCP_USER_CONFIG"]),
|
|
5850
|
+
LSP_TOOLS_MCP_INSTALL_DECISIONS: canonicalizePath(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"])
|
|
5851
|
+
};
|
|
5852
|
+
}
|
|
5853
|
+
function canonicalizePathList(value) {
|
|
5854
|
+
if (value === undefined)
|
|
5855
|
+
return;
|
|
5856
|
+
return value.split(delimiter4).map((entry) => canonicalizePath(entry) ?? entry).join(delimiter4);
|
|
5857
|
+
}
|
|
5858
|
+
function canonicalizePath(value) {
|
|
5859
|
+
if (value === undefined || !isAbsolute4(value) || !existsSync11(value))
|
|
5860
|
+
return value;
|
|
5861
|
+
return realpathSync4(value);
|
|
5862
|
+
}
|
|
5863
|
+
function projectRootFromOpenCodeConfigPath(path2) {
|
|
5864
|
+
if (basename3(path2) !== "lsp.json" && basename3(path2) !== "lsp-client.json")
|
|
5865
|
+
return;
|
|
5866
|
+
const configDir = dirname9(path2);
|
|
5867
|
+
const configDirName = basename3(configDir);
|
|
5868
|
+
if (configDirName !== ".opencode" && configDirName !== ".omo")
|
|
5869
|
+
return;
|
|
5870
|
+
return dirname9(configDir);
|
|
5871
|
+
}
|
|
3771
5872
|
|
|
3772
5873
|
// src/daemon-server.ts
|
|
3773
|
-
import {
|
|
5874
|
+
import { chmodSync as chmodSync2 } from "node:fs";
|
|
3774
5875
|
import { createServer as createServer2 } from "node:net";
|
|
3775
|
-
|
|
5876
|
+
|
|
5877
|
+
// src/ownership.ts
|
|
5878
|
+
import { randomUUID } from "node:crypto";
|
|
5879
|
+
import { existsSync as existsSync12, lstatSync as lstatSync7, readFileSync as readFileSync7, statSync as statSync5, unlinkSync as unlinkSync3 } from "node:fs";
|
|
5880
|
+
import { dirname as dirname11 } from "node:path";
|
|
5881
|
+
|
|
5882
|
+
// src/lock.ts
|
|
5883
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync4, openSync as openSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeSync as writeSync2 } from "node:fs";
|
|
5884
|
+
import { dirname as dirname10 } from "node:path";
|
|
5885
|
+
function isProcessAlive(pid) {
|
|
5886
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
5887
|
+
return false;
|
|
5888
|
+
try {
|
|
5889
|
+
process.kill(pid, 0);
|
|
5890
|
+
return true;
|
|
5891
|
+
} catch (error) {
|
|
5892
|
+
if (error instanceof Error)
|
|
5893
|
+
return errorCode3(error) === "EPERM";
|
|
5894
|
+
throw error;
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
function readLockPid(lockPath) {
|
|
5898
|
+
try {
|
|
5899
|
+
const pid = Number.parseInt(readFileSync6(lockPath, "utf8").trim(), 10);
|
|
5900
|
+
return Number.isInteger(pid) ? pid : null;
|
|
5901
|
+
} catch {
|
|
5902
|
+
return null;
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
function tryAcquireLock(lockPath, ownerPid = process.pid) {
|
|
5906
|
+
mkdirSync4(dirname10(lockPath), { recursive: true });
|
|
5907
|
+
for (let attempt = 0;attempt < 2; attempt += 1) {
|
|
5908
|
+
const handle = writeLockFile(lockPath, ownerPid);
|
|
5909
|
+
if (handle)
|
|
5910
|
+
return handle;
|
|
5911
|
+
if (!reapStaleLock(lockPath))
|
|
5912
|
+
return null;
|
|
5913
|
+
}
|
|
5914
|
+
return null;
|
|
5915
|
+
}
|
|
5916
|
+
function writeLockFile(lockPath, ownerPid) {
|
|
5917
|
+
try {
|
|
5918
|
+
const fd = openSync3(lockPath, "wx", 384);
|
|
5919
|
+
writeSync2(fd, `${ownerPid}
|
|
5920
|
+
`);
|
|
5921
|
+
closeSync3(fd);
|
|
5922
|
+
return { release: () => unlinkQuietly(lockPath) };
|
|
5923
|
+
} catch (error) {
|
|
5924
|
+
if (errorCode3(error) === "EEXIST")
|
|
5925
|
+
return null;
|
|
5926
|
+
throw error;
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
function reapStaleLock(lockPath) {
|
|
5930
|
+
const pid = readLockPid(lockPath);
|
|
5931
|
+
if (pid !== null && isProcessAlive(pid))
|
|
5932
|
+
return false;
|
|
5933
|
+
unlinkQuietly(lockPath);
|
|
5934
|
+
return true;
|
|
5935
|
+
}
|
|
5936
|
+
function unlinkQuietly(path2) {
|
|
5937
|
+
try {
|
|
5938
|
+
unlinkSync2(path2);
|
|
5939
|
+
} catch (error) {
|
|
5940
|
+
if (error instanceof Error)
|
|
5941
|
+
return;
|
|
5942
|
+
throw error;
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
function errorCode3(error) {
|
|
5946
|
+
if (!error || typeof error !== "object" || !("code" in error))
|
|
5947
|
+
return;
|
|
5948
|
+
const code = Reflect.get(error, "code");
|
|
5949
|
+
return typeof code === "string" ? code : undefined;
|
|
5950
|
+
}
|
|
5951
|
+
|
|
5952
|
+
// src/ownership.ts
|
|
5953
|
+
class DaemonAlreadyRunningError extends Error {
|
|
5954
|
+
name = "DaemonAlreadyRunningError";
|
|
5955
|
+
code = "daemon_already_running";
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5958
|
+
class DaemonStartupDeferredError extends Error {
|
|
5959
|
+
reason;
|
|
5960
|
+
name = "DaemonStartupDeferredError";
|
|
5961
|
+
code = "daemon_startup_deferred";
|
|
5962
|
+
constructor(reason) {
|
|
5963
|
+
super(`LSP daemon startup deferred: ${reason}`);
|
|
5964
|
+
this.reason = reason;
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
async function acquireStartupLease(paths, pingOwner) {
|
|
5968
|
+
ensureDaemonDirectories(paths);
|
|
5969
|
+
const lock = tryAcquireLock(paths.lock);
|
|
5970
|
+
if (!lock) {
|
|
5971
|
+
const token = readAuthToken(paths);
|
|
5972
|
+
if (token && await pingOwner(token))
|
|
5973
|
+
throw new DaemonAlreadyRunningError("LSP daemon already running");
|
|
5974
|
+
throw new DaemonStartupDeferredError("startup_lock_busy");
|
|
5975
|
+
}
|
|
5976
|
+
try {
|
|
5977
|
+
const token = await validateExistingOwner(paths, pingOwner);
|
|
5978
|
+
return { lock, token, owner: newOwner(paths) };
|
|
5979
|
+
} catch (error) {
|
|
5980
|
+
lock.release();
|
|
5981
|
+
throw error;
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
function ensureDaemonDirectories(paths) {
|
|
5985
|
+
ensurePrivateDirectory(paths.dir);
|
|
5986
|
+
if (process.platform !== "win32")
|
|
5987
|
+
ensurePrivateDirectory(dirname11(paths.socket));
|
|
5988
|
+
}
|
|
5989
|
+
function readDaemonOwner(paths) {
|
|
5990
|
+
try {
|
|
5991
|
+
return parseOwner(JSON.parse(readFileSync7(paths.owner, "utf8")));
|
|
5992
|
+
} catch (error) {
|
|
5993
|
+
if (error instanceof Error)
|
|
5994
|
+
return null;
|
|
5995
|
+
throw error;
|
|
5996
|
+
}
|
|
5997
|
+
}
|
|
5998
|
+
function writeDaemonOwner(paths, owner) {
|
|
5999
|
+
writePrivateFile(paths.pid, `${owner.pid}
|
|
6000
|
+
`);
|
|
6001
|
+
writePrivateFile(paths.endpoint, owner.endpoint.path);
|
|
6002
|
+
writePrivateFile(paths.owner, `${JSON.stringify(owner)}
|
|
6003
|
+
`);
|
|
6004
|
+
}
|
|
6005
|
+
function removeDaemonMetadataForOwner(paths, owner) {
|
|
6006
|
+
const current = readDaemonOwner(paths);
|
|
6007
|
+
if (!current || !sameOwner(current, owner))
|
|
6008
|
+
return;
|
|
6009
|
+
unlinkQuietly(paths.socket);
|
|
6010
|
+
unlinkQuietly(paths.pid);
|
|
6011
|
+
unlinkQuietly(paths.endpoint);
|
|
6012
|
+
unlinkQuietly(paths.owner);
|
|
6013
|
+
}
|
|
6014
|
+
function endpointIdentity(endpointPath) {
|
|
6015
|
+
if (process.platform === "win32")
|
|
6016
|
+
return { kind: "windows", path: endpointPath };
|
|
6017
|
+
try {
|
|
6018
|
+
const stats = statSync5(endpointPath);
|
|
6019
|
+
return { kind: "unix", path: endpointPath, dev: stats.dev, ino: stats.ino };
|
|
6020
|
+
} catch (error) {
|
|
6021
|
+
if (error instanceof Error)
|
|
6022
|
+
return { kind: "missing", path: endpointPath };
|
|
6023
|
+
throw error;
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
function sameEndpoint(a, b) {
|
|
6027
|
+
if (a.kind !== b.kind || a.path !== b.path)
|
|
6028
|
+
return false;
|
|
6029
|
+
switch (a.kind) {
|
|
6030
|
+
case "unix":
|
|
6031
|
+
return b.kind === "unix" && a.dev === b.dev && a.ino === b.ino;
|
|
6032
|
+
case "windows":
|
|
6033
|
+
return true;
|
|
6034
|
+
case "missing":
|
|
6035
|
+
return true;
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
6038
|
+
function sameOwner(a, b) {
|
|
6039
|
+
return a.pid === b.pid && a.nonce === b.nonce && sameEndpoint(a.endpoint, b.endpoint);
|
|
6040
|
+
}
|
|
6041
|
+
async function validateExistingOwner(paths, pingOwner) {
|
|
6042
|
+
let token = readOrCreateAuthToken(paths);
|
|
6043
|
+
for (let attempt = 0;attempt < 2; attempt += 1) {
|
|
6044
|
+
const owner = readDaemonOwner(paths);
|
|
6045
|
+
if (!owner)
|
|
6046
|
+
return token;
|
|
6047
|
+
const ping = await pingOwner(token);
|
|
6048
|
+
if (ping && owner.nonce === ping.nonce && sameEndpoint(owner.endpoint, ping.endpoint)) {
|
|
6049
|
+
throw new DaemonAlreadyRunningError("LSP daemon already running");
|
|
6050
|
+
}
|
|
6051
|
+
if (ping)
|
|
6052
|
+
continue;
|
|
6053
|
+
if (isProcessAlive(owner.pid))
|
|
6054
|
+
throw new DaemonStartupDeferredError("owner_pid_live_unreachable");
|
|
6055
|
+
const reread = readDaemonOwner(paths);
|
|
6056
|
+
const endpoint = endpointIdentity(owner.endpoint.path);
|
|
6057
|
+
if (!reread || reread.nonce !== owner.nonce || !sameEndpoint(endpoint, owner.endpoint)) {
|
|
6058
|
+
throw new DaemonStartupDeferredError("owner_changed_during_cleanup");
|
|
6059
|
+
}
|
|
6060
|
+
cleanupDeadOwner(paths, owner);
|
|
6061
|
+
token = rotateAuthToken(paths);
|
|
6062
|
+
return token;
|
|
6063
|
+
}
|
|
6064
|
+
throw new DaemonStartupDeferredError("reachable_owner_mismatch");
|
|
6065
|
+
}
|
|
6066
|
+
function cleanupDeadOwner(paths, owner) {
|
|
6067
|
+
if (process.platform !== "win32" && existsSync12(owner.endpoint.path)) {
|
|
6068
|
+
const stat = lstatSync7(owner.endpoint.path);
|
|
6069
|
+
if (stat.isSocket())
|
|
6070
|
+
unlinkSync3(owner.endpoint.path);
|
|
6071
|
+
}
|
|
6072
|
+
unlinkQuietly(paths.pid);
|
|
6073
|
+
unlinkQuietly(paths.endpoint);
|
|
6074
|
+
unlinkQuietly(paths.owner);
|
|
6075
|
+
}
|
|
6076
|
+
function newOwner(paths) {
|
|
6077
|
+
return {
|
|
6078
|
+
pid: process.pid,
|
|
6079
|
+
nonce: randomUUID(),
|
|
6080
|
+
startedAt: new Date().toISOString(),
|
|
6081
|
+
endpoint: endpointIdentity(paths.socket)
|
|
6082
|
+
};
|
|
6083
|
+
}
|
|
6084
|
+
function parseOwner(value) {
|
|
6085
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6086
|
+
return null;
|
|
6087
|
+
const pid = Reflect.get(value, "pid");
|
|
6088
|
+
const nonce = Reflect.get(value, "nonce");
|
|
6089
|
+
const startedAt = Reflect.get(value, "startedAt");
|
|
6090
|
+
const endpoint = parseEndpoint(Reflect.get(value, "endpoint"));
|
|
6091
|
+
if (typeof pid !== "number" || typeof nonce !== "string" || typeof startedAt !== "string" || !endpoint)
|
|
6092
|
+
return null;
|
|
6093
|
+
return { pid, nonce, startedAt, endpoint };
|
|
6094
|
+
}
|
|
6095
|
+
function parseEndpoint(value) {
|
|
6096
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6097
|
+
return null;
|
|
6098
|
+
const kind = Reflect.get(value, "kind");
|
|
6099
|
+
const path2 = Reflect.get(value, "path");
|
|
6100
|
+
if (typeof path2 !== "string")
|
|
6101
|
+
return null;
|
|
6102
|
+
if (kind === undefined)
|
|
6103
|
+
return endpointIdentity(path2);
|
|
6104
|
+
if (kind === "windows")
|
|
6105
|
+
return { kind, path: path2 };
|
|
6106
|
+
if (kind === "missing")
|
|
6107
|
+
return { kind, path: path2 };
|
|
6108
|
+
const dev = Reflect.get(value, "dev");
|
|
6109
|
+
const ino = Reflect.get(value, "ino");
|
|
6110
|
+
if (kind === "unix" && typeof dev === "number" && typeof ino === "number")
|
|
6111
|
+
return { kind, path: path2, dev, ino };
|
|
6112
|
+
return null;
|
|
6113
|
+
}
|
|
6114
|
+
|
|
6115
|
+
// src/daemon-server.ts
|
|
3776
6116
|
var DEFAULT_IDLE_SHUTDOWN_MS = 30 * 60000;
|
|
3777
6117
|
var DEFAULT_IDLE_CHECK_INTERVAL_MS = 60000;
|
|
3778
6118
|
async function startDaemonServer(paths, options = {}) {
|
|
3779
6119
|
const idleShutdownMs = options.idleShutdownMs ?? DEFAULT_IDLE_SHUTDOWN_MS;
|
|
3780
6120
|
const idleCheckIntervalMs = options.idleCheckIntervalMs ?? DEFAULT_IDLE_CHECK_INTERVAL_MS;
|
|
3781
|
-
|
|
3782
|
-
unlinkQuietly(paths.socket);
|
|
6121
|
+
const lease = await acquireStartupLease(paths, (token) => pingDaemon(paths, token));
|
|
3783
6122
|
const connections = new Set;
|
|
3784
6123
|
let lastActiveAt = Date.now();
|
|
3785
6124
|
const touch = () => {
|
|
3786
6125
|
lastActiveAt = Date.now();
|
|
3787
6126
|
};
|
|
6127
|
+
let routeOwner = lease.owner;
|
|
3788
6128
|
const server2 = createServer2((socket) => {
|
|
3789
6129
|
connections.add(socket);
|
|
6130
|
+
const activeRequests = new Map;
|
|
3790
6131
|
touch();
|
|
3791
6132
|
const decoder = createLineDecoder((message) => {
|
|
3792
6133
|
touch();
|
|
3793
|
-
respond(socket, message);
|
|
6134
|
+
respond(socket, message, lease.token, routeOwner, activeRequests);
|
|
3794
6135
|
});
|
|
3795
6136
|
socket.on("data", (chunk) => decoder.push(chunk));
|
|
3796
6137
|
socket.on("error", () => socket.destroy());
|
|
3797
6138
|
socket.on("close", () => {
|
|
6139
|
+
for (const controller of activeRequests.values())
|
|
6140
|
+
controller.abort();
|
|
6141
|
+
activeRequests.clear();
|
|
3798
6142
|
connections.delete(socket);
|
|
3799
6143
|
touch();
|
|
3800
6144
|
});
|
|
3801
6145
|
});
|
|
3802
6146
|
server2.on("error", (error) => logServerError(error));
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
6147
|
+
let owner;
|
|
6148
|
+
try {
|
|
6149
|
+
await listen(server2, paths.socket);
|
|
6150
|
+
if (process.platform !== "win32")
|
|
6151
|
+
chmodSync2(paths.socket, 384);
|
|
6152
|
+
owner = { ...lease.owner, endpoint: endpointIdentity(paths.socket) };
|
|
6153
|
+
routeOwner = owner;
|
|
6154
|
+
writeDaemonOwner(paths, owner);
|
|
6155
|
+
await assertSelfProbe(paths, lease.token, owner);
|
|
6156
|
+
} catch (error) {
|
|
6157
|
+
lease.lock.release();
|
|
6158
|
+
throw error;
|
|
6159
|
+
}
|
|
6160
|
+
lease.lock.release();
|
|
3808
6161
|
let closed = false;
|
|
3809
6162
|
const close = async () => {
|
|
3810
6163
|
if (closed)
|
|
@@ -3815,9 +6168,7 @@ async function startDaemonServer(paths, options = {}) {
|
|
|
3815
6168
|
socket.destroy();
|
|
3816
6169
|
connections.clear();
|
|
3817
6170
|
await closeServer(server2);
|
|
3818
|
-
|
|
3819
|
-
unlinkQuietly(paths.pid);
|
|
3820
|
-
unlinkQuietly(endpointPath);
|
|
6171
|
+
removeDaemonMetadataForOwner(paths, owner);
|
|
3821
6172
|
await disposeDefaultLspManager();
|
|
3822
6173
|
};
|
|
3823
6174
|
const idleTimer = setInterval(() => {
|
|
@@ -3839,27 +6190,37 @@ async function startDaemonServer(paths, options = {}) {
|
|
|
3839
6190
|
installSignalHandlers(close);
|
|
3840
6191
|
return { server: server2, close };
|
|
3841
6192
|
}
|
|
3842
|
-
async function respond(socket, message) {
|
|
6193
|
+
async function respond(socket, message, token, owner, activeRequests) {
|
|
3843
6194
|
try {
|
|
3844
|
-
const response = await handleDaemonMessage(message);
|
|
6195
|
+
const response = await handleDaemonMessage(message, { token, owner, activeRequests });
|
|
3845
6196
|
if (response && socket.writable)
|
|
3846
6197
|
socket.write(encodeJsonLine(response));
|
|
3847
6198
|
} catch (error) {
|
|
6199
|
+
if (!(error instanceof Error))
|
|
6200
|
+
throw error;
|
|
3848
6201
|
logServerError(error);
|
|
3849
6202
|
}
|
|
3850
6203
|
}
|
|
6204
|
+
async function assertSelfProbe(paths, token, owner) {
|
|
6205
|
+
setPrivateFileMode(paths.pid);
|
|
6206
|
+
setPrivateFileMode(paths.endpoint);
|
|
6207
|
+
setPrivateFileMode(paths.owner);
|
|
6208
|
+
const ping = await pingDaemon(paths, token);
|
|
6209
|
+
if (!ping || ping.nonce !== owner.nonce)
|
|
6210
|
+
throw new DaemonStartupDeferredError("self_probe_failed");
|
|
6211
|
+
}
|
|
3851
6212
|
function listen(server2, socketPath) {
|
|
3852
|
-
return new Promise((
|
|
6213
|
+
return new Promise((resolve9, reject) => {
|
|
3853
6214
|
const onError = (error) => reject(error);
|
|
3854
6215
|
server2.once("error", onError);
|
|
3855
6216
|
server2.listen(socketPath, () => {
|
|
3856
6217
|
server2.removeListener("error", onError);
|
|
3857
|
-
|
|
6218
|
+
resolve9();
|
|
3858
6219
|
});
|
|
3859
6220
|
});
|
|
3860
6221
|
}
|
|
3861
6222
|
function closeServer(server2) {
|
|
3862
|
-
return new Promise((
|
|
6223
|
+
return new Promise((resolve9) => server2.close(() => resolve9()));
|
|
3863
6224
|
}
|
|
3864
6225
|
function installSignalHandlers(close) {
|
|
3865
6226
|
const handler = () => {
|
|
@@ -3878,7 +6239,13 @@ function logServerError(error) {
|
|
|
3878
6239
|
async function runDaemon() {
|
|
3879
6240
|
process.on("uncaughtException", (error) => logNonFatal("uncaughtException", error));
|
|
3880
6241
|
process.on("unhandledRejection", (reason) => logNonFatal("unhandledRejection", reason));
|
|
3881
|
-
|
|
6242
|
+
try {
|
|
6243
|
+
await startDaemonServer(daemonPaths());
|
|
6244
|
+
} catch (error) {
|
|
6245
|
+
if (error instanceof DaemonAlreadyRunningError)
|
|
6246
|
+
return;
|
|
6247
|
+
throw error;
|
|
6248
|
+
}
|
|
3882
6249
|
}
|
|
3883
6250
|
function logNonFatal(kind, error) {
|
|
3884
6251
|
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|