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,53 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { snapshotPath } from "./workspace-edit-path.js";
|
|
5
|
+
import type { ParsedWorkspaceOperation } from "./workspace-edit-plan-types.js";
|
|
6
|
+
import type { WorkspaceSnapshotEntry } from "./workspace-edit-types.js";
|
|
7
|
+
|
|
8
|
+
class WorkspaceSnapshotBuilder {
|
|
9
|
+
private readonly snapshots = new Map<string, WorkspaceSnapshotEntry>();
|
|
10
|
+
|
|
11
|
+
constructor(private readonly workspaceRoot: string) {}
|
|
12
|
+
|
|
13
|
+
build(operations: readonly ParsedWorkspaceOperation[]): ReadonlyMap<string, WorkspaceSnapshotEntry> {
|
|
14
|
+
this.add(this.workspaceRoot, false);
|
|
15
|
+
for (const operation of operations) {
|
|
16
|
+
switch (operation.kind) {
|
|
17
|
+
case "rename":
|
|
18
|
+
this.add(operation.oldPath, true);
|
|
19
|
+
this.add(operation.newPath, true);
|
|
20
|
+
break;
|
|
21
|
+
case "delete":
|
|
22
|
+
this.add(operation.path, true);
|
|
23
|
+
break;
|
|
24
|
+
case "text":
|
|
25
|
+
case "create":
|
|
26
|
+
this.add(operation.path, false);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return this.snapshots;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private add(path: string, includeChildren: boolean): void {
|
|
34
|
+
let candidate = path;
|
|
35
|
+
while (true) {
|
|
36
|
+
const existing = this.snapshots.get(candidate);
|
|
37
|
+
if (existing === undefined || (includeChildren && existing.kind === "directory" && existing.children === undefined)) {
|
|
38
|
+
this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
|
|
39
|
+
}
|
|
40
|
+
if (candidate === this.workspaceRoot) break;
|
|
41
|
+
candidate = dirname(candidate);
|
|
42
|
+
}
|
|
43
|
+
if (!includeChildren || !existsSync(path) || !lstatSync(path).isDirectory()) return;
|
|
44
|
+
for (const child of readdirSync(path)) this.add(resolve(path, child), true);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function snapshotOperations(
|
|
49
|
+
operations: readonly ParsedWorkspaceOperation[],
|
|
50
|
+
workspaceRoot: string,
|
|
51
|
+
): ReadonlyMap<string, WorkspaceSnapshotEntry> {
|
|
52
|
+
return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
|
|
53
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Position, Range, TextEdit } from "./types.js";
|
|
2
|
+
import { type NormalizedTextEditResult, WorkspaceEditValidationError } from "./workspace-edit-types.js";
|
|
3
|
+
|
|
4
|
+
interface IndexedTextEdit {
|
|
5
|
+
readonly edit: TextEdit;
|
|
6
|
+
readonly index: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface PositionValidationContext {
|
|
10
|
+
readonly lines: readonly string[];
|
|
11
|
+
readonly changeIndex: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function comparePosition(left: Position, right: Position): number {
|
|
15
|
+
return left.line === right.line ? left.character - right.character : left.line - right.line;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function positionsEqual(left: Position, right: Position): boolean {
|
|
19
|
+
return left.line === right.line && left.character === right.character;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rangesEqual(left: Range, right: Range): boolean {
|
|
23
|
+
return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isEmptyRange(range: Range): boolean {
|
|
27
|
+
return positionsEqual(range.start, range.end);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatRange(range: Range): string {
|
|
31
|
+
return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function validatePosition(position: Position, label: string, context: PositionValidationContext): void {
|
|
35
|
+
const { lines, changeIndex } = context;
|
|
36
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
|
|
37
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
|
|
38
|
+
}
|
|
39
|
+
if (position.line < 0 || position.character < 0) {
|
|
40
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
|
|
41
|
+
}
|
|
42
|
+
const line = lines[position.line];
|
|
43
|
+
if (line === undefined) {
|
|
44
|
+
throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
|
|
45
|
+
}
|
|
46
|
+
if (position.character > line.length) {
|
|
47
|
+
throw new WorkspaceEditValidationError(
|
|
48
|
+
changeIndex,
|
|
49
|
+
`${label} character ${position.character} is outside line ${position.line}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateRange(range: Range, lines: readonly string[], changeIndex: number): void {
|
|
55
|
+
const context: PositionValidationContext = { lines, changeIndex };
|
|
56
|
+
validatePosition(range.start, "start", context);
|
|
57
|
+
validatePosition(range.end, "end", context);
|
|
58
|
+
if (comparePosition(range.start, range.end) > 0) {
|
|
59
|
+
throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sortAndDeduplicate(edits: readonly TextEdit[]): readonly TextEdit[] {
|
|
64
|
+
const sorted = edits
|
|
65
|
+
.map<IndexedTextEdit>((edit, index) => ({ edit, index }))
|
|
66
|
+
.sort((left, right) => {
|
|
67
|
+
const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
|
|
68
|
+
return positionOrder === 0 ? right.index - left.index : positionOrder;
|
|
69
|
+
});
|
|
70
|
+
const unique: TextEdit[] = [];
|
|
71
|
+
for (const entry of sorted) {
|
|
72
|
+
const previous = unique.at(-1);
|
|
73
|
+
if (
|
|
74
|
+
previous !== undefined &&
|
|
75
|
+
!isEmptyRange(entry.edit.range) &&
|
|
76
|
+
rangesEqual(previous.range, entry.edit.range) &&
|
|
77
|
+
previous.newText === entry.edit.newText
|
|
78
|
+
) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
unique.push(entry.edit);
|
|
82
|
+
}
|
|
83
|
+
return unique;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateNoOverlap(edits: readonly TextEdit[], changeIndex: number): void {
|
|
87
|
+
for (let index = 0; index < edits.length - 1; index += 1) {
|
|
88
|
+
const later = edits[index];
|
|
89
|
+
const earlier = edits[index + 1];
|
|
90
|
+
if (later === undefined || earlier === undefined) continue;
|
|
91
|
+
if (comparePosition(earlier.range.end, later.range.start) > 0) {
|
|
92
|
+
throw new WorkspaceEditValidationError(
|
|
93
|
+
changeIndex,
|
|
94
|
+
`overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function applyNormalizedTextEdits(content: string, edits: readonly TextEdit[]): string {
|
|
101
|
+
const lines = content.split("\n");
|
|
102
|
+
for (const edit of edits) {
|
|
103
|
+
const { start, end } = edit.range;
|
|
104
|
+
const startLine = lines[start.line];
|
|
105
|
+
const endLine = lines[end.line];
|
|
106
|
+
if (startLine === undefined || endLine === undefined) continue;
|
|
107
|
+
const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
|
|
108
|
+
lines.splice(start.line, end.line - start.line + 1, ...replacement.split("\n"));
|
|
109
|
+
}
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function normalizeTextEdits(
|
|
114
|
+
content: string,
|
|
115
|
+
edits: readonly TextEdit[],
|
|
116
|
+
changeIndex: number,
|
|
117
|
+
): NormalizedTextEditResult {
|
|
118
|
+
const lines = content.split("\n");
|
|
119
|
+
for (const edit of edits) {
|
|
120
|
+
validateRange(edit.range, lines, changeIndex);
|
|
121
|
+
}
|
|
122
|
+
const normalized = sortAndDeduplicate(edits);
|
|
123
|
+
validateNoOverlap(normalized, changeIndex);
|
|
124
|
+
return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
|
|
125
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { TextEdit, WorkspaceEdit } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface ApplyResult {
|
|
4
|
+
readonly success: boolean;
|
|
5
|
+
readonly filesModified: readonly string[];
|
|
6
|
+
readonly totalEdits: number;
|
|
7
|
+
readonly errors: readonly string[];
|
|
8
|
+
readonly failedChange?: number;
|
|
9
|
+
readonly lateAbort?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LspRenameResult {
|
|
13
|
+
readonly edit: WorkspaceEdit | null;
|
|
14
|
+
readonly apply: ApplyResult;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type WorkspaceSnapshotEntry =
|
|
18
|
+
| { readonly kind: "missing" }
|
|
19
|
+
| { readonly kind: "file"; readonly content: string }
|
|
20
|
+
| { readonly kind: "directory"; readonly children?: readonly string[] };
|
|
21
|
+
|
|
22
|
+
export type PlannedWorkspaceOperation =
|
|
23
|
+
| {
|
|
24
|
+
readonly kind: "text";
|
|
25
|
+
readonly changeIndex: number;
|
|
26
|
+
readonly path: string;
|
|
27
|
+
readonly beforeText: string;
|
|
28
|
+
readonly afterText: string;
|
|
29
|
+
readonly editCount: number;
|
|
30
|
+
readonly documentVersion: number | null;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
readonly kind: "create";
|
|
34
|
+
readonly changeIndex: number;
|
|
35
|
+
readonly path: string;
|
|
36
|
+
readonly replaced: boolean;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
readonly kind: "rename";
|
|
40
|
+
readonly changeIndex: number;
|
|
41
|
+
readonly oldPath: string;
|
|
42
|
+
readonly newPath: string;
|
|
43
|
+
readonly sourceKind: "file" | "directory";
|
|
44
|
+
readonly replaceDestination: boolean;
|
|
45
|
+
}
|
|
46
|
+
| {
|
|
47
|
+
readonly kind: "delete";
|
|
48
|
+
readonly changeIndex: number;
|
|
49
|
+
readonly path: string;
|
|
50
|
+
readonly targetKind: "file" | "directory";
|
|
51
|
+
readonly recursive: boolean;
|
|
52
|
+
}
|
|
53
|
+
| { readonly kind: "noop"; readonly changeIndex: number };
|
|
54
|
+
|
|
55
|
+
export interface WorkspaceEditPlan {
|
|
56
|
+
readonly workspaceRoot: string;
|
|
57
|
+
readonly operations: readonly PlannedWorkspaceOperation[];
|
|
58
|
+
readonly snapshots: ReadonlyMap<string, WorkspaceSnapshotEntry>;
|
|
59
|
+
readonly firstChangeByPath: ReadonlyMap<string, number>;
|
|
60
|
+
readonly reportedPathByCanonical: ReadonlyMap<string, string>;
|
|
61
|
+
readonly fingerprint: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type WorkspaceEditPlanResult =
|
|
65
|
+
| { readonly success: true; readonly plan: WorkspaceEditPlan }
|
|
66
|
+
| { readonly success: false; readonly result: ApplyResult };
|
|
67
|
+
|
|
68
|
+
export type WorkspaceMutation =
|
|
69
|
+
| {
|
|
70
|
+
readonly kind: "text";
|
|
71
|
+
readonly path: string;
|
|
72
|
+
readonly beforeText: string;
|
|
73
|
+
readonly afterText: string;
|
|
74
|
+
}
|
|
75
|
+
| { readonly kind: "create"; readonly path: string; readonly replaced: boolean }
|
|
76
|
+
| {
|
|
77
|
+
readonly kind: "rename";
|
|
78
|
+
readonly oldPath: string;
|
|
79
|
+
readonly newPath: string;
|
|
80
|
+
readonly sourceKind: "file" | "directory";
|
|
81
|
+
}
|
|
82
|
+
| { readonly kind: "delete"; readonly path: string; readonly targetKind: "file" | "directory" };
|
|
83
|
+
|
|
84
|
+
export interface WorkspaceMutationDelta {
|
|
85
|
+
readonly operations: readonly WorkspaceMutation[];
|
|
86
|
+
readonly changedPaths: readonly string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface WorkspaceEditCommit {
|
|
90
|
+
readonly result: ApplyResult;
|
|
91
|
+
readonly delta: WorkspaceMutationDelta;
|
|
92
|
+
readonly fingerprint: string | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface WorkspaceEditCommitIo {
|
|
96
|
+
readonly writeFile: (path: string, content: string) => void;
|
|
97
|
+
readonly rename: (oldPath: string, newPath: string) => void;
|
|
98
|
+
readonly remove: (path: string, recursive: boolean) => void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ApplyWorkspaceEditOptions {
|
|
102
|
+
readonly workspaceRoot?: string;
|
|
103
|
+
readonly signal?: AbortSignal;
|
|
104
|
+
readonly io?: Partial<WorkspaceEditCommitIo>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface NormalizedTextEditResult {
|
|
108
|
+
readonly edits: readonly TextEdit[];
|
|
109
|
+
readonly text: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class WorkspaceEditValidationError extends Error {
|
|
113
|
+
override readonly name = "WorkspaceEditValidationError";
|
|
114
|
+
|
|
115
|
+
constructor(
|
|
116
|
+
readonly changeIndex: number,
|
|
117
|
+
readonly detail: string,
|
|
118
|
+
) {
|
|
119
|
+
super(`change ${changeIndex}: ${detail}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
7
|
+
|
|
8
|
+
import { applyWorkspaceEdit } from "./workspace-edit.js";
|
|
9
|
+
|
|
10
|
+
const tempDirectories: string[] = [];
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
for (const directory of tempDirectories.splice(0)) {
|
|
14
|
+
rmSync(directory, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("applyWorkspaceEdit preserved behavior", () => {
|
|
19
|
+
it("#given a valid text edit #when applied #then the file and ApplyResult remain compatible", () => {
|
|
20
|
+
// given
|
|
21
|
+
const workspace = makeTempDirectory();
|
|
22
|
+
const source = join(workspace, "source.ts");
|
|
23
|
+
writeFileSync(source, "const before = 1;\n", "utf-8");
|
|
24
|
+
|
|
25
|
+
// when
|
|
26
|
+
const result = applyWorkspaceEdit(
|
|
27
|
+
{
|
|
28
|
+
changes: {
|
|
29
|
+
[pathToFileURL(source).href]: [
|
|
30
|
+
{
|
|
31
|
+
range: { start: { line: 0, character: 6 }, end: { line: 0, character: 12 } },
|
|
32
|
+
newText: "after",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{ workspaceRoot: workspace },
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// then
|
|
41
|
+
expect(result).toEqual({ success: true, filesModified: [source], totalEdits: 1, errors: [] });
|
|
42
|
+
expect(readFileSync(source, "utf-8")).toBe("const after = 1;\n");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("#given a resource rename #when applied #then content moves and the destination is reported", () => {
|
|
46
|
+
// given
|
|
47
|
+
const workspace = makeTempDirectory();
|
|
48
|
+
const source = join(workspace, "before.ts");
|
|
49
|
+
const destination = join(workspace, "after.ts");
|
|
50
|
+
writeFileSync(source, "export const value = 1;\n", "utf-8");
|
|
51
|
+
|
|
52
|
+
// when
|
|
53
|
+
const result = applyWorkspaceEdit(
|
|
54
|
+
{
|
|
55
|
+
documentChanges: [
|
|
56
|
+
{ kind: "rename", oldUri: pathToFileURL(source).href, newUri: pathToFileURL(destination).href },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{ workspaceRoot: workspace },
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// then
|
|
63
|
+
expect(result).toEqual({ success: true, filesModified: [destination], totalEdits: 0, errors: [] });
|
|
64
|
+
expect(existsSync(source)).toBe(false);
|
|
65
|
+
expect(readFileSync(destination, "utf-8")).toBe("export const value = 1;\n");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("#given an empty workspace edit #when applied #then it remains a successful no-op", () => {
|
|
69
|
+
// given
|
|
70
|
+
const workspace = makeTempDirectory();
|
|
71
|
+
|
|
72
|
+
// when
|
|
73
|
+
const result = applyWorkspaceEdit({}, { workspaceRoot: workspace });
|
|
74
|
+
|
|
75
|
+
// then
|
|
76
|
+
expect(result).toEqual({ success: true, filesModified: [], totalEdits: 0, errors: [] });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("#given no workspace edit #when applied #then the existing failure remains explicit", () => {
|
|
80
|
+
// given
|
|
81
|
+
const workspace = makeTempDirectory();
|
|
82
|
+
|
|
83
|
+
// when
|
|
84
|
+
const result = applyWorkspaceEdit(null, { workspaceRoot: workspace });
|
|
85
|
+
|
|
86
|
+
// then
|
|
87
|
+
expect(result).toEqual({ success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
function makeTempDirectory(): string {
|
|
92
|
+
const directory = mkdtempSync(join(tmpdir(), "lsp-workspace-characterization-"));
|
|
93
|
+
tempDirectories.push(directory);
|
|
94
|
+
return directory;
|
|
95
|
+
}
|
|
@@ -1,206 +1,55 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
1
|
import { contextCwd } from "../request-context.js";
|
|
6
|
-
import type {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let validatedPath: string;
|
|
53
|
-
try {
|
|
54
|
-
validatedPath = realpathForValidation(filePath);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
return { success: false, error: `${filePath}: ${errorMessage(error)}` };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
|
|
60
|
-
return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { success: true, path: filePath };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function applyTextEditsToFile(filePath: string, edits: TextEdit[]): FileApplyResult {
|
|
67
|
-
try {
|
|
68
|
-
const content = readFileSync(filePath, "utf-8");
|
|
69
|
-
const lines = content.split("\n");
|
|
70
|
-
|
|
71
|
-
const sortedEdits = [...edits].sort((a, b) => {
|
|
72
|
-
if (b.range.start.line !== a.range.start.line) {
|
|
73
|
-
return b.range.start.line - a.range.start.line;
|
|
74
|
-
}
|
|
75
|
-
return b.range.start.character - a.range.start.character;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
for (const edit of sortedEdits) {
|
|
79
|
-
const startLine = edit.range.start.line;
|
|
80
|
-
const startChar = edit.range.start.character;
|
|
81
|
-
const endLine = edit.range.end.line;
|
|
82
|
-
const endChar = edit.range.end.character;
|
|
83
|
-
|
|
84
|
-
if (startLine === endLine) {
|
|
85
|
-
const line = lines[startLine] ?? "";
|
|
86
|
-
lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
|
|
87
|
-
} else {
|
|
88
|
-
const firstLine = lines[startLine] ?? "";
|
|
89
|
-
const lastLine = lines[endLine] ?? "";
|
|
90
|
-
const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
|
|
91
|
-
lines.splice(startLine, endLine - startLine + 1, ...newContent.split("\n"));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
96
|
-
return { success: true, editCount: edits.length };
|
|
97
|
-
} catch (err) {
|
|
98
|
-
return {
|
|
99
|
-
success: false,
|
|
100
|
-
editCount: 0,
|
|
101
|
-
error: err instanceof Error ? err.message : String(err),
|
|
102
|
-
};
|
|
103
|
-
}
|
|
2
|
+
import type { WorkspaceEdit } from "./types.js";
|
|
3
|
+
import { commitWorkspaceEditPlan } from "./workspace-edit-commit.js";
|
|
4
|
+
import { fingerprintWorkspaceEdit, planWorkspaceEdit } from "./workspace-edit-plan.js";
|
|
5
|
+
import type {
|
|
6
|
+
ApplyResult,
|
|
7
|
+
ApplyWorkspaceEditOptions,
|
|
8
|
+
WorkspaceEditCommit,
|
|
9
|
+
WorkspaceMutationDelta,
|
|
10
|
+
} from "./workspace-edit-types.js";
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
ApplyResult,
|
|
14
|
+
ApplyWorkspaceEditOptions,
|
|
15
|
+
LspRenameResult,
|
|
16
|
+
PlannedWorkspaceOperation,
|
|
17
|
+
WorkspaceEditCommit,
|
|
18
|
+
WorkspaceEditCommitIo,
|
|
19
|
+
WorkspaceEditPlan,
|
|
20
|
+
WorkspaceEditPlanResult,
|
|
21
|
+
WorkspaceMutation,
|
|
22
|
+
WorkspaceMutationDelta,
|
|
23
|
+
} from "./workspace-edit-types.js";
|
|
24
|
+
export { commitWorkspaceEditPlan } from "./workspace-edit-commit.js";
|
|
25
|
+
export { fingerprintWorkspaceEdit, planWorkspaceEdit } from "./workspace-edit-plan.js";
|
|
26
|
+
|
|
27
|
+
const EMPTY_DELTA: WorkspaceMutationDelta = { operations: [], changedPaths: [] };
|
|
28
|
+
|
|
29
|
+
function noEditCommit(): WorkspaceEditCommit {
|
|
30
|
+
return {
|
|
31
|
+
result: { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] },
|
|
32
|
+
delta: EMPTY_DELTA,
|
|
33
|
+
fingerprint: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function applyWorkspaceEditDetailed(
|
|
38
|
+
edit: WorkspaceEdit | null,
|
|
39
|
+
options: ApplyWorkspaceEditOptions = {},
|
|
40
|
+
): WorkspaceEditCommit {
|
|
41
|
+
if (!edit) return noEditCommit();
|
|
42
|
+
const workspaceRoot = options.workspaceRoot ?? contextCwd();
|
|
43
|
+
const planned = planWorkspaceEdit(edit, workspaceRoot);
|
|
44
|
+
if (!planned.success) return { result: planned.result, delta: EMPTY_DELTA, fingerprint: null };
|
|
45
|
+
return commitWorkspaceEditPlan(planned.plan, options);
|
|
104
46
|
}
|
|
105
47
|
|
|
106
48
|
export function applyWorkspaceEdit(edit: WorkspaceEdit | null, options: ApplyWorkspaceEditOptions = {}): ApplyResult {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const result: ApplyResult = { success: true, filesModified: [], totalEdits: 0, errors: [] };
|
|
112
|
-
const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
|
|
113
|
-
|
|
114
|
-
if (edit.changes) {
|
|
115
|
-
for (const [uri, edits] of Object.entries(edit.changes)) {
|
|
116
|
-
const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
|
|
117
|
-
if (!validatedPath.success) {
|
|
118
|
-
result.success = false;
|
|
119
|
-
result.errors.push(validatedPath.error);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, edits);
|
|
123
|
-
|
|
124
|
-
if (applyResult.success) {
|
|
125
|
-
result.filesModified.push(validatedPath.path);
|
|
126
|
-
result.totalEdits += applyResult.editCount;
|
|
127
|
-
} else {
|
|
128
|
-
result.success = false;
|
|
129
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (edit.documentChanges) {
|
|
135
|
-
for (const change of edit.documentChanges) {
|
|
136
|
-
if (!("kind" in change)) {
|
|
137
|
-
const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
|
|
138
|
-
if (!validatedPath.success) {
|
|
139
|
-
result.success = false;
|
|
140
|
-
result.errors.push(validatedPath.error);
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
|
|
144
|
-
|
|
145
|
-
if (applyResult.success) {
|
|
146
|
-
result.filesModified.push(validatedPath.path);
|
|
147
|
-
result.totalEdits += applyResult.editCount;
|
|
148
|
-
} else {
|
|
149
|
-
result.success = false;
|
|
150
|
-
result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
|
|
151
|
-
}
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (change.kind === "create") {
|
|
156
|
-
try {
|
|
157
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
158
|
-
if (!validatedPath.success) {
|
|
159
|
-
result.success = false;
|
|
160
|
-
result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
writeFileSync(validatedPath.path, "", "utf-8");
|
|
164
|
-
result.filesModified.push(validatedPath.path);
|
|
165
|
-
} catch (err) {
|
|
166
|
-
result.success = false;
|
|
167
|
-
result.errors.push(`Create ${change.uri}: ${String(err)}`);
|
|
168
|
-
}
|
|
169
|
-
} else if (change.kind === "rename") {
|
|
170
|
-
try {
|
|
171
|
-
const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
|
|
172
|
-
const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
|
|
173
|
-
if (!oldPath.success || !newPath.success) {
|
|
174
|
-
const error = oldPath.success ? (newPath.success ? "invalid URI" : newPath.error) : oldPath.error;
|
|
175
|
-
result.success = false;
|
|
176
|
-
result.errors.push(`Rename ${change.oldUri}: ${error}`);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
const content = readFileSync(oldPath.path, "utf-8");
|
|
180
|
-
writeFileSync(newPath.path, content, "utf-8");
|
|
181
|
-
unlinkSync(oldPath.path);
|
|
182
|
-
result.filesModified.push(newPath.path);
|
|
183
|
-
} catch (err) {
|
|
184
|
-
result.success = false;
|
|
185
|
-
result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
|
|
186
|
-
}
|
|
187
|
-
} else if (change.kind === "delete") {
|
|
188
|
-
try {
|
|
189
|
-
const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
|
|
190
|
-
if (!validatedPath.success) {
|
|
191
|
-
result.success = false;
|
|
192
|
-
result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
unlinkSync(validatedPath.path);
|
|
196
|
-
result.filesModified.push(validatedPath.path);
|
|
197
|
-
} catch (err) {
|
|
198
|
-
result.success = false;
|
|
199
|
-
result.errors.push(`Delete ${change.uri}: ${String(err)}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
49
|
+
return applyWorkspaceEditDetailed(edit, options).result;
|
|
50
|
+
}
|
|
204
51
|
|
|
205
|
-
|
|
52
|
+
export function workspaceEditFingerprint(edit: WorkspaceEdit, workspaceRoot: string): string | null {
|
|
53
|
+
const fingerprint = fingerprintWorkspaceEdit(edit, workspaceRoot);
|
|
54
|
+
return fingerprint.success ? fingerprint.fingerprint : null;
|
|
206
55
|
}
|