oh-my-opencode 4.18.0 → 4.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -0
- package/.agents/skills/work-with-pr/SKILL.md +16 -37
- package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/.opencode/skills/work-with-pr/SKILL.md +16 -37
- package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/dist/cli/index.js +457 -154
- package/dist/cli-node/index.js +457 -154
- package/dist/index.js +415 -388
- package/package.json +16 -16
- package/packages/lsp-core/package.json +4 -0
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
- package/packages/lsp-core/src/lsp/client.ts +262 -80
- package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
- package/packages/lsp-core/src/lsp/connection.ts +12 -6
- package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +104 -0
- package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
- package/packages/lsp-core/src/lsp/errors.ts +11 -0
- package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
- package/packages/lsp-core/src/lsp/formatters.ts +3 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
- package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
- package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
- package/packages/lsp-core/src/lsp/transport.ts +96 -70
- package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
- package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
- package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
- package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
- package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
- package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
- package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
- package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
- package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
- package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
- package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
- package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
- package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
- package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
- package/packages/lsp-core/src/mcp.ts +18 -7
- package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
- package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
- package/packages/lsp-core/src/post-edit/index.ts +1 -0
- package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
- package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
- package/packages/lsp-core/src/request-context.test.ts +171 -0
- package/packages/lsp-core/src/request-context.ts +222 -9
- package/packages/lsp-core/src/tool-surface.test.ts +4 -1
- package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
- package/packages/lsp-core/src/tools/navigation.ts +12 -12
- package/packages/lsp-core/src/tools/rename.ts +10 -15
- package/packages/lsp-core/src/tools/symbols.ts +11 -11
- package/packages/lsp-core/src/tools/types.ts +2 -1
- package/packages/lsp-daemon/dist/cli.js +3114 -747
- package/packages/lsp-daemon/dist/client.d.ts +105 -0
- package/packages/lsp-daemon/dist/client.js +5851 -0
- package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
- package/packages/lsp-daemon/dist/daemon-client.js +113 -30
- package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-server.js +40 -15
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +8 -7
- package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
- package/packages/lsp-daemon/dist/index.d.ts +2 -2
- package/packages/lsp-daemon/dist/index.js +2862 -754
- package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
- package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
- package/packages/lsp-daemon/dist/lock.js +14 -4
- package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
- package/packages/lsp-daemon/dist/ownership.js +168 -0
- package/packages/lsp-daemon/dist/paths.d.ts +33 -9
- package/packages/lsp-daemon/dist/paths.js +72 -33
- package/packages/lsp-daemon/dist/proxy.d.ts +3 -0
- package/packages/lsp-daemon/dist/proxy.js +54 -3
- package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
- package/packages/lsp-daemon/dist/request-routing.js +71 -22
- package/packages/lsp-daemon/dist/run-daemon.js +9 -2
- package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
- package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
- package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
- package/packages/lsp-daemon/package.json +12 -3
- package/packages/lsp-tools-mcp/dist/cli.js +2115 -442
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
- package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
- package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
- package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +19 -5
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +26 -14
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
- package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
- package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { LspClient } from "./client.js";
|
|
7
|
+
import { LspClientConnection } from "./connection.js";
|
|
8
|
+
import type { LspClientOptions } from "./client.js";
|
|
9
|
+
import type { LspClientTransport } from "./transport.js";
|
|
10
|
+
import type { Diagnostic, ResolvedServer, WorkspaceEdit } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const fixturePath = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "workspace-edit-server.mjs");
|
|
13
|
+
|
|
14
|
+
export interface RecordedEvent {
|
|
15
|
+
readonly type: string;
|
|
16
|
+
readonly id?: string | number;
|
|
17
|
+
readonly method?: string;
|
|
18
|
+
readonly params?: unknown;
|
|
19
|
+
readonly result?: unknown;
|
|
20
|
+
readonly error?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface WorkspaceEditTestContext {
|
|
24
|
+
readonly client: LspClient;
|
|
25
|
+
readonly workspace: string;
|
|
26
|
+
readonly source: string;
|
|
27
|
+
readonly destination: string;
|
|
28
|
+
readonly closed: string;
|
|
29
|
+
readonly created: string;
|
|
30
|
+
readonly events: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createWorkspaceEditTestHarness() {
|
|
34
|
+
const tempDirectories: string[] = [];
|
|
35
|
+
const clients: LspClientTransport[] = [];
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
async cleanup(): Promise<void> {
|
|
39
|
+
for (const client of clients.splice(0)) await client.stop();
|
|
40
|
+
for (const directory of tempDirectories.splice(0)) rmSync(directory, { recursive: true, force: true });
|
|
41
|
+
},
|
|
42
|
+
async makeClient(
|
|
43
|
+
scenario: Record<string, unknown>,
|
|
44
|
+
clientOptions: Partial<LspClientOptions> = {},
|
|
45
|
+
): Promise<WorkspaceEditTestContext> {
|
|
46
|
+
const fixture = createFixture(scenario, tempDirectories);
|
|
47
|
+
const client = new LspClient(fixture.workspace, fixture.server, {
|
|
48
|
+
requestTimeoutMs: 5_000,
|
|
49
|
+
initializeTimeoutMs: 5_000,
|
|
50
|
+
...clientOptions,
|
|
51
|
+
});
|
|
52
|
+
clients.push(client);
|
|
53
|
+
await client.start();
|
|
54
|
+
await client.initialize();
|
|
55
|
+
return { client, ...fixture.paths };
|
|
56
|
+
},
|
|
57
|
+
async makeBareConnection(
|
|
58
|
+
scenario: Record<string, unknown>,
|
|
59
|
+
): Promise<{ readonly connection: LspClientConnection; readonly events: string }> {
|
|
60
|
+
const fixture = createFixture(scenario, tempDirectories);
|
|
61
|
+
const connection = new LspClientConnection(fixture.workspace, fixture.server, {
|
|
62
|
+
requestTimeoutMs: 5_000,
|
|
63
|
+
initializeTimeoutMs: 5_000,
|
|
64
|
+
});
|
|
65
|
+
clients.push(connection);
|
|
66
|
+
await connection.start();
|
|
67
|
+
await connection.initialize();
|
|
68
|
+
return { connection, events: fixture.paths.events };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createFixture(scenario: Record<string, unknown>, tempDirectories: string[]) {
|
|
74
|
+
const workspace = mkdtempSync(join(tmpdir(), "lsp-apply-edit-"));
|
|
75
|
+
tempDirectories.push(workspace);
|
|
76
|
+
const source = join(workspace, "source.ts");
|
|
77
|
+
const destination = join(workspace, "destination.ts");
|
|
78
|
+
const closed = join(workspace, "closed.ts");
|
|
79
|
+
const created = join(workspace, "created.ts");
|
|
80
|
+
const scenarioPath = join(workspace, "scenario.json");
|
|
81
|
+
const events = join(workspace, "events.jsonl");
|
|
82
|
+
writeFileSync(source, "const before = 1;\n", "utf-8");
|
|
83
|
+
writeFileSync(closed, "const closed = 1;\n", "utf-8");
|
|
84
|
+
const uriByPlaceholder = new Map([
|
|
85
|
+
["file:///placeholder", pathToFileURL(source).href],
|
|
86
|
+
["file:///destination", pathToFileURL(destination).href],
|
|
87
|
+
["file:///closed", pathToFileURL(closed).href],
|
|
88
|
+
["file:///created", pathToFileURL(created).href],
|
|
89
|
+
]);
|
|
90
|
+
writeFileSync(scenarioPath, JSON.stringify(replacePlaceholderUris(scenario, uriByPlaceholder)), "utf-8");
|
|
91
|
+
writeFileSync(events, "", "utf-8");
|
|
92
|
+
const server: ResolvedServer = {
|
|
93
|
+
id: "workspace-edit-fixture",
|
|
94
|
+
command: [process.env["NODE_BINARY"] ?? "node", fixturePath, scenarioPath, events],
|
|
95
|
+
extensions: [".ts"],
|
|
96
|
+
priority: 0,
|
|
97
|
+
};
|
|
98
|
+
return { workspace, server, paths: { workspace, source, destination, closed, created, events } };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function replacePlaceholderUris(value: unknown, uriByPlaceholder: ReadonlyMap<string, string>): unknown {
|
|
102
|
+
if (Array.isArray(value)) return value.map((item) => replacePlaceholderUris(item, uriByPlaceholder));
|
|
103
|
+
if (typeof value !== "object" || value === null) {
|
|
104
|
+
return typeof value === "string" ? (uriByPlaceholder.get(value) ?? value) : value;
|
|
105
|
+
}
|
|
106
|
+
return Object.fromEntries(
|
|
107
|
+
Object.entries(value).map(([key, item]) => [key, replacePlaceholderUris(item, uriByPlaceholder)]),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface RenameTextEditTarget {
|
|
112
|
+
readonly version: number | null;
|
|
113
|
+
readonly uri?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renameTextEdit(before: string, after: string, target: RenameTextEditTarget): WorkspaceEdit {
|
|
117
|
+
const uri = target.uri ?? "file:///placeholder";
|
|
118
|
+
return {
|
|
119
|
+
documentChanges: [
|
|
120
|
+
{
|
|
121
|
+
textDocument: { uri, version: target.version },
|
|
122
|
+
edits: [
|
|
123
|
+
{
|
|
124
|
+
range: { start: { line: 0, character: 6 }, end: { line: 0, character: 6 + before.length } },
|
|
125
|
+
newText: after,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function diagnostic(message: string): Diagnostic {
|
|
134
|
+
return { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } }, message };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function readEvents(path: string): readonly RecordedEvent[] {
|
|
138
|
+
const events: RecordedEvent[] = [];
|
|
139
|
+
for (const line of readFileSync(path, "utf-8").split("\n")) {
|
|
140
|
+
if (!line) continue;
|
|
141
|
+
const value: unknown = JSON.parse(line);
|
|
142
|
+
if (isRecordedEvent(value)) events.push(value);
|
|
143
|
+
}
|
|
144
|
+
return events;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function waitForEventCount(
|
|
148
|
+
path: string,
|
|
149
|
+
predicate: (event: RecordedEvent) => boolean,
|
|
150
|
+
count: number,
|
|
151
|
+
): Promise<readonly RecordedEvent[]> {
|
|
152
|
+
const deadline = Date.now() + 2_000;
|
|
153
|
+
while (Date.now() < deadline) {
|
|
154
|
+
const matches = readEvents(path).filter(predicate);
|
|
155
|
+
if (matches.length >= count) return matches;
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
157
|
+
}
|
|
158
|
+
return readEvents(path).filter(predicate);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isRecordedEvent(value: unknown): value is RecordedEvent {
|
|
162
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
163
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createWorkspaceEditTestHarness,
|
|
8
|
+
diagnostic,
|
|
9
|
+
readEvents,
|
|
10
|
+
renameTextEdit,
|
|
11
|
+
} from "./workspace-apply-edit-test-support.js";
|
|
12
|
+
|
|
13
|
+
const harness = createWorkspaceEditTestHarness();
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await harness.cleanup();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("LspClient workspace/applyEdit", () => {
|
|
20
|
+
it("#given a server-applied edit returned again by rename #when rename completes #then one write is reused truthfully", async () => {
|
|
21
|
+
const context = await harness.makeClient({
|
|
22
|
+
renameSteps: [{ applyEdit: renameTextEdit("before", "after", { version: 1 }), renameResult: "same" }],
|
|
23
|
+
diagnostics: [diagnostic("fresh")],
|
|
24
|
+
});
|
|
25
|
+
let writes = 0;
|
|
26
|
+
context.client.setWorkspaceEditIo({
|
|
27
|
+
writeFile(path, content) {
|
|
28
|
+
writes += 1;
|
|
29
|
+
writeFileSync(path, content, "utf-8");
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
await context.client.openFile(context.source);
|
|
33
|
+
|
|
34
|
+
const result = await context.client.rename(context.source, 1, 6, "after");
|
|
35
|
+
|
|
36
|
+
expect(result.apply.success).toBe(true);
|
|
37
|
+
expect(result.apply.totalEdits).toBe(1);
|
|
38
|
+
expect(writes).toBe(1);
|
|
39
|
+
expect(readFileSync(context.source, "utf-8")).toBe("const after = 1;\n");
|
|
40
|
+
expect(context.client.getOpenDocumentVersion(context.source)).toBe(2);
|
|
41
|
+
expect(context.client.getStoredDiagnostics(pathToFileURL(realpathSync(context.source)).href)).toEqual([
|
|
42
|
+
diagnostic("fresh"),
|
|
43
|
+
]);
|
|
44
|
+
const responses = readEvents(context.events).filter(
|
|
45
|
+
(event) => event.type === "clientResponse" && event.method === "workspace/applyEdit",
|
|
46
|
+
);
|
|
47
|
+
expect(responses).toHaveLength(1);
|
|
48
|
+
expect(responses[0]?.result).toEqual({ applied: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("#given a server-applied edit and a null rename result #when rename completes #then the recorded result is reused", async () => {
|
|
52
|
+
const context = await harness.makeClient({
|
|
53
|
+
renameSteps: [{ applyEdit: renameTextEdit("before", "after", { version: 1 }), renameResult: "null" }],
|
|
54
|
+
});
|
|
55
|
+
await context.client.openFile(context.source);
|
|
56
|
+
|
|
57
|
+
const result = await context.client.rename(context.source, 1, 6, "after");
|
|
58
|
+
|
|
59
|
+
expect(result.edit).toBeNull();
|
|
60
|
+
expect(result.apply.success).toBe(true);
|
|
61
|
+
expect(readFileSync(context.source, "utf-8")).toBe("const after = 1;\n");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("#given two versioned server edits #when applied sequentially #then the synchronized version accepts the second edit", async () => {
|
|
65
|
+
const context = await harness.makeClient({
|
|
66
|
+
renameSteps: [
|
|
67
|
+
{ applyEdit: renameTextEdit("before", "first_", { version: 1 }), renameResult: "same" },
|
|
68
|
+
{ applyEdit: renameTextEdit("first_", "second", { version: 2 }), renameResult: "same" },
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
await context.client.openFile(context.source);
|
|
72
|
+
|
|
73
|
+
const first = await context.client.rename(context.source, 1, 6, "first_");
|
|
74
|
+
const second = await context.client.rename(context.source, 1, 6, "second");
|
|
75
|
+
|
|
76
|
+
expect(first.apply.success).toBe(true);
|
|
77
|
+
expect(second.apply.success).toBe(true);
|
|
78
|
+
expect(context.client.getOpenDocumentVersion(context.source)).toBe(3);
|
|
79
|
+
expect(readFileSync(context.source, "utf-8")).toBe("const second = 1;\n");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("#given a mismatched document version #when the server requests applyEdit #then mutation is rejected truthfully", async () => {
|
|
83
|
+
const context = await harness.makeClient({
|
|
84
|
+
renameSteps: [{ applyEdit: renameTextEdit("before", "after", { version: 9 }), renameResult: "same" }],
|
|
85
|
+
});
|
|
86
|
+
await context.client.openFile(context.source);
|
|
87
|
+
|
|
88
|
+
const result = await context.client.rename(context.source, 1, 6, "after");
|
|
89
|
+
|
|
90
|
+
expect(result.apply.success).toBe(false);
|
|
91
|
+
expect(result.apply.errors.join("\n")).toContain("document version");
|
|
92
|
+
expect(readFileSync(context.source, "utf-8")).toBe("const before = 1;\n");
|
|
93
|
+
const response = readEvents(context.events).find(
|
|
94
|
+
(event) => event.type === "clientResponse" && event.method === "workspace/applyEdit",
|
|
95
|
+
);
|
|
96
|
+
expect(response?.result).toEqual({
|
|
97
|
+
applied: false,
|
|
98
|
+
failureReason: expect.stringContaining("document version"),
|
|
99
|
+
failedChange: 0,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("#given a server-applied edit and a different rename result #when rename completes #then conflict is reported without a second mutation", async () => {
|
|
104
|
+
const context = await harness.makeClient({
|
|
105
|
+
renameSteps: [
|
|
106
|
+
{
|
|
107
|
+
applyEdit: renameTextEdit("before", "after", { version: 1 }),
|
|
108
|
+
renameResult: "different",
|
|
109
|
+
differentEdit: renameTextEdit("before", "other_", { version: 1 }),
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
let writes = 0;
|
|
114
|
+
context.client.setWorkspaceEditIo({
|
|
115
|
+
writeFile(path, content) {
|
|
116
|
+
writes += 1;
|
|
117
|
+
writeFileSync(path, content, "utf-8");
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
await context.client.openFile(context.source);
|
|
121
|
+
|
|
122
|
+
const result = await context.client.rename(context.source, 1, 6, "after");
|
|
123
|
+
|
|
124
|
+
expect(result.apply.success).toBe(false);
|
|
125
|
+
expect(result.apply.errors.join("\n")).toContain("conflicts with server-applied workspace edit");
|
|
126
|
+
expect(writes).toBe(1);
|
|
127
|
+
expect(readFileSync(context.source, "utf-8")).toBe("const after = 1;\n");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("#given a real commit I/O failure #when the server requests applyEdit #then both protocol and rename result report failure", async () => {
|
|
131
|
+
const context = await harness.makeClient({
|
|
132
|
+
renameSteps: [{ applyEdit: renameTextEdit("before", "after", { version: 1 }), renameResult: "same" }],
|
|
133
|
+
});
|
|
134
|
+
context.client.setWorkspaceEditIo({
|
|
135
|
+
writeFile() {
|
|
136
|
+
throw new InjectedWorkspaceIoError();
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
await context.client.openFile(context.source);
|
|
140
|
+
|
|
141
|
+
const result = await context.client.rename(context.source, 1, 6, "after");
|
|
142
|
+
|
|
143
|
+
expect(result.apply.success).toBe(false);
|
|
144
|
+
expect(result.apply.errors.join("\n")).toContain("I/O failure during text");
|
|
145
|
+
expect(readFileSync(context.source, "utf-8")).toBe("const before = 1;\n");
|
|
146
|
+
const response = readEvents(context.events).find(
|
|
147
|
+
(event) => event.type === "clientResponse" && event.method === "workspace/applyEdit",
|
|
148
|
+
);
|
|
149
|
+
expect(response?.result).toEqual({
|
|
150
|
+
applied: false,
|
|
151
|
+
failureReason: expect.stringContaining("I/O failure during text"),
|
|
152
|
+
failedChange: 0,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
class InjectedWorkspaceIoError extends Error {
|
|
158
|
+
override readonly name = "InjectedWorkspaceIoError";
|
|
159
|
+
|
|
160
|
+
constructor() {
|
|
161
|
+
super("injected workspace I/O failure");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { WorkspaceDocumentState } from "./workspace-document-state.js";
|
|
4
|
+
import type { WorkspaceMutation } from "./workspace-edit.js";
|
|
5
|
+
|
|
6
|
+
describe("WorkspaceDocumentState watched-file bounds", () => {
|
|
7
|
+
it("#given more closed-file mutations than one batch #when synchronized #then every notification stays bounded", async () => {
|
|
8
|
+
const notifications: unknown[] = [];
|
|
9
|
+
const documents = new WorkspaceDocumentState(
|
|
10
|
+
async (method, params) => {
|
|
11
|
+
if (method === "workspace/didChangeWatchedFiles") notifications.push(params);
|
|
12
|
+
},
|
|
13
|
+
() => {},
|
|
14
|
+
{ versionlessPublishQuiescenceMs: 0 },
|
|
15
|
+
);
|
|
16
|
+
const changedPaths = Array.from({ length: 129 }, (_, index) => `/workspace/file-${index}.ts`);
|
|
17
|
+
const operations: WorkspaceMutation[] = changedPaths.map((path) => ({
|
|
18
|
+
kind: "create",
|
|
19
|
+
path,
|
|
20
|
+
replaced: false,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
await documents.synchronize({ operations, changedPaths });
|
|
24
|
+
|
|
25
|
+
expect(notifications).toHaveLength(2);
|
|
26
|
+
expect(notificationChanges(notifications[0])).toHaveLength(128);
|
|
27
|
+
expect(notificationChanges(notifications[1])).toHaveLength(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("#given changed closed files #when synchronized #then sends bounded changed watched-file notifications", async () => {
|
|
31
|
+
const notifications: unknown[] = [];
|
|
32
|
+
const documents = new WorkspaceDocumentState(
|
|
33
|
+
async (method, params) => {
|
|
34
|
+
if (method === "workspace/didChangeWatchedFiles") notifications.push(params);
|
|
35
|
+
},
|
|
36
|
+
() => {},
|
|
37
|
+
{ versionlessPublishQuiescenceMs: 0 },
|
|
38
|
+
);
|
|
39
|
+
const changedPaths = Array.from({ length: 129 }, (_, index) => `/workspace/file-${index}.ts`);
|
|
40
|
+
const operations: WorkspaceMutation[] = changedPaths.map((path) => ({
|
|
41
|
+
kind: "text",
|
|
42
|
+
path,
|
|
43
|
+
beforeText: "before",
|
|
44
|
+
afterText: "after",
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
await documents.synchronize({ operations, changedPaths });
|
|
48
|
+
|
|
49
|
+
expect(notifications).toHaveLength(2);
|
|
50
|
+
expect(notificationChanges(notifications[0])).toHaveLength(128);
|
|
51
|
+
expect(notificationChanges(notifications[1])).toHaveLength(1);
|
|
52
|
+
expect(notificationTypes(notifications[0])).toEqual(Array.from({ length: 128 }, () => 2));
|
|
53
|
+
expect(notificationTypes(notifications[1])).toEqual([2]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function notificationChanges(value: unknown): readonly unknown[] {
|
|
58
|
+
if (typeof value !== "object" || value === null || !("changes" in value) || !Array.isArray(value.changes)) return [];
|
|
59
|
+
return value.changes;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function notificationTypes(value: unknown): readonly unknown[] {
|
|
63
|
+
return notificationChanges(value).map((change) => {
|
|
64
|
+
if (typeof change !== "object" || change === null || !("type" in change)) return undefined;
|
|
65
|
+
return change.type;
|
|
66
|
+
});
|
|
67
|
+
}
|