@cueloop/client 0.1.0-alpha.2 → 0.1.0-alpha.21
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/package.json +17 -11
- package/src/App.autoclose.test.tsx +109 -0
- package/src/App.diff.test.tsx +5 -18
- package/src/App.export.test.tsx +8 -13
- package/src/App.plan-review.test.tsx +293 -0
- package/src/App.readonly.test.tsx +6 -21
- package/src/App.rendering.test.tsx +104 -0
- package/src/App.review-panel.test.tsx +97 -0
- package/src/App.submit-confirm.test.tsx +165 -0
- package/src/App.test.tsx +20 -58
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +208 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +75 -0
- package/src/components/AnnotationCard.tsx +189 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +38 -0
- package/src/components/CodeBlock.stories.tsx +25 -0
- package/src/components/CodeBlock.tsx +68 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +49 -0
- package/src/components/ComposeBar.stories.tsx +18 -0
- package/src/components/ComposeBar.tsx +54 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +117 -0
- package/src/components/DiffSheet.stories.tsx +44 -0
- package/src/components/DiffSheet.tsx +243 -0
- package/src/components/InboxList.stories.tsx +18 -0
- package/src/components/InboxList.tsx +42 -0
- package/src/components/PlanSheet.stories.tsx +81 -0
- package/src/components/PlanSheet.tsx +309 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +116 -0
- package/src/components/ReviewRail.stories.tsx +103 -0
- package/src/components/ReviewRail.tsx +171 -0
- package/src/components/SettingsDialog.stories.tsx +63 -0
- package/src/components/SettingsDialog.tsx +132 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +92 -0
- package/src/components/WalkWizard.stories.tsx +89 -0
- package/src/components/WalkWizard.tsx +146 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1459 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +44 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +59 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +53 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +22 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +97 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/stories-app.tsx +95 -0
- package/src/components/stories.test.tsx +51 -0
- package/src/components/story-fixtures.ts +90 -0
- package/src/components/story.ts +66 -0
- package/src/components/syntax-highlight.ts +57 -0
- package/src/components/theme-context.stories.tsx +41 -0
- package/src/components/theme-context.tsx +30 -0
- package/src/components/truncate-text.ts +6 -0
- package/src/config.test.ts +105 -30
- package/src/config.ts +91 -16
- package/src/editor.test.ts +110 -0
- package/src/editor.ts +101 -13
- package/src/herdr.test.ts +37 -0
- package/src/herdr.ts +21 -0
- package/src/integrations.test.ts +29 -0
- package/src/integrations.ts +28 -0
- package/src/key-bindings.test.ts +120 -0
- package/src/key-bindings.ts +353 -0
- package/src/keymap.test.ts +347 -0
- package/src/keymap.ts +229 -0
- package/src/review-panel.test.ts +111 -0
- package/src/review-panel.ts +76 -0
- package/src/run.ts +9 -5
- package/src/serve.test.ts +6 -6
- package/src/serve.ts +7 -7
- package/src/session-controller.ts +512 -0
- package/src/test-support.ts +87 -0
- package/src/theme.ts +23 -1
- package/src/view-diff.test.ts +8 -8
- package/src/view-diff.ts +22 -22
- package/src/view-plan.test.ts +179 -0
- package/src/view-plan.ts +362 -0
- package/src/walk.test.ts +110 -0
- package/src/walk.ts +65 -0
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
package/src/walk.test.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Pure walk projections: file steps, resume index, viewed counts, notes. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import type { Annotation } from "@cueloop/schema";
|
|
5
|
+
import { diffRows } from "./view-diff";
|
|
6
|
+
import { firstUnviewedIndex, noteForFile, viewedCount, walkFiles, WALK_PREVIEW_ROWS } from "./walk";
|
|
7
|
+
|
|
8
|
+
const PATCH = `diff --git a/src/a.ts b/src/a.ts
|
|
9
|
+
index 111..222 100644
|
|
10
|
+
--- a/src/a.ts
|
|
11
|
+
+++ b/src/a.ts
|
|
12
|
+
@@ -1,3 +1,4 @@
|
|
13
|
+
export const a = 1;
|
|
14
|
+
-export const b = 2;
|
|
15
|
+
+export const b = 3;
|
|
16
|
+
+export const c = 4;
|
|
17
|
+
diff --git a/src/b.ts b/src/b.ts
|
|
18
|
+
index 333..444 100644
|
|
19
|
+
--- a/src/b.ts
|
|
20
|
+
+++ b/src/b.ts
|
|
21
|
+
@@ -1,2 +1,2 @@
|
|
22
|
+
// header
|
|
23
|
+
-old line
|
|
24
|
+
+new line
|
|
25
|
+
diff --git a/src/c.ts b/src/c.ts
|
|
26
|
+
index 555..666 100644
|
|
27
|
+
--- a/src/c.ts
|
|
28
|
+
+++ b/src/c.ts
|
|
29
|
+
@@ -1,1 +1,8 @@
|
|
30
|
+
-gone
|
|
31
|
+
+one
|
|
32
|
+
+two
|
|
33
|
+
+three
|
|
34
|
+
+four
|
|
35
|
+
+five
|
|
36
|
+
+six
|
|
37
|
+
+seven
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const rows = diffRows(PATCH);
|
|
41
|
+
const files = walkFiles(rows);
|
|
42
|
+
|
|
43
|
+
describe("walkFiles", () => {
|
|
44
|
+
test("one step per changed file, in patch order, with counted stats", () => {
|
|
45
|
+
expect(files.map((file) => file.path)).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
|
|
46
|
+
expect(files[0]).toMatchObject({ added: 2, removed: 1 });
|
|
47
|
+
expect(files[1]).toMatchObject({ added: 1, removed: 1 });
|
|
48
|
+
expect(files[2]).toMatchObject({ added: 7, removed: 1 });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("previews carry signed changed lines and cap at the preview budget", () => {
|
|
52
|
+
expect(files[0]!.preview).toEqual([
|
|
53
|
+
{ sign: "-", text: "export const b = 2;" },
|
|
54
|
+
{ sign: "+", text: "export const b = 3;" },
|
|
55
|
+
{ sign: "+", text: "export const c = 4;" },
|
|
56
|
+
]);
|
|
57
|
+
expect(files[2]!.preview.length).toBe(WALK_PREVIEW_ROWS);
|
|
58
|
+
expect(files[2]!.preview[0]).toEqual({ sign: "-", text: "gone" });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("firstUnviewedIndex", () => {
|
|
63
|
+
test("starts at the first file when nothing is viewed", () => {
|
|
64
|
+
expect(firstUnviewedIndex(files, new Set())).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("skips viewed files - a resumed review lands on its first gap", () => {
|
|
68
|
+
expect(firstUnviewedIndex(files, new Set(["src/a.ts"]))).toBe(1);
|
|
69
|
+
expect(firstUnviewedIndex(files, new Set(["src/a.ts", "src/b.ts"]))).toBe(2);
|
|
70
|
+
// a viewed file later in the list does not block the earlier gap
|
|
71
|
+
expect(firstUnviewedIndex(files, new Set(["src/c.ts"]))).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("all viewed resumes on the end card, not the first file", () => {
|
|
75
|
+
expect(firstUnviewedIndex(files, new Set(["src/a.ts", "src/b.ts", "src/c.ts"]))).toBe(files.length);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("viewedCount", () => {
|
|
80
|
+
test("counts only paths present in the current file set", () => {
|
|
81
|
+
expect(viewedCount(files, new Set(["src/a.ts", "src/gone-in-rev-2.ts"]))).toBe(1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("noteForFile", () => {
|
|
86
|
+
const note = (path: string, body: string): Annotation => ({
|
|
87
|
+
id: `a_${path}`,
|
|
88
|
+
kind: "note",
|
|
89
|
+
anchor: { quote: path, prefix: "", suffix: "" },
|
|
90
|
+
body,
|
|
91
|
+
createdAt: "now",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("finds the note anchored at the file path", () => {
|
|
95
|
+
const annotations = [note("src/a.ts", "Renames b and adds c."), note("src/b.ts", "Swaps the line.")];
|
|
96
|
+
expect(noteForFile(annotations, "src/b.ts")).toBe("Swaps the line.");
|
|
97
|
+
expect(noteForFile(annotations, "src/c.ts")).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("ignores non-note annotations that happen to quote the path", () => {
|
|
101
|
+
const comment: Annotation = {
|
|
102
|
+
id: "a_1",
|
|
103
|
+
kind: "comment",
|
|
104
|
+
anchor: { quote: "src/a.ts", prefix: "", suffix: "" },
|
|
105
|
+
body: "not a note",
|
|
106
|
+
createdAt: "now",
|
|
107
|
+
};
|
|
108
|
+
expect(noteForFile([comment], "src/a.ts")).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
package/src/walk.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guided-walk projections: pure helpers over the diff row model - no
|
|
3
|
+
* IO, no React. The walk steps through the changed files in patch order;
|
|
4
|
+
* which files were seen lives on the session record (viewedPaths), which
|
|
5
|
+
* file is on screen is view state in the session controller.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Annotation } from "@cueloop/schema";
|
|
9
|
+
import type { DiffRow } from "./view-diff";
|
|
10
|
+
|
|
11
|
+
/** One wizard step: a changed file with its stats and a short diff preview. */
|
|
12
|
+
export interface WalkFile {
|
|
13
|
+
path: string;
|
|
14
|
+
added: number;
|
|
15
|
+
removed: number;
|
|
16
|
+
/** The first changed lines, signed (+/-), for the card's diff preview. */
|
|
17
|
+
preview: { sign: "+" | "-"; text: string }[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Preview lines per card: enough to recognize the change, never a full diff. */
|
|
21
|
+
export const WALK_PREVIEW_ROWS = 5;
|
|
22
|
+
|
|
23
|
+
/** Collapse the diff rows into one walk step per changed file, in patch order. */
|
|
24
|
+
export function walkFiles(rows: DiffRow[]): WalkFile[] {
|
|
25
|
+
const files: WalkFile[] = [];
|
|
26
|
+
const byPath = new Map<string, WalkFile>();
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
let file = byPath.get(row.file);
|
|
29
|
+
if (!file) {
|
|
30
|
+
file = { path: row.file, added: 0, removed: 0, preview: [] };
|
|
31
|
+
byPath.set(row.file, file);
|
|
32
|
+
files.push(file);
|
|
33
|
+
}
|
|
34
|
+
if (row.kind !== "add" && row.kind !== "del") continue;
|
|
35
|
+
if (row.kind === "add") file.added += 1;
|
|
36
|
+
else file.removed += 1;
|
|
37
|
+
if (file.preview.length < WALK_PREVIEW_ROWS) {
|
|
38
|
+
file.preview.push({ sign: row.kind === "add" ? "+" : "-", text: row.text.replace(/\n$/, "") });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Where the walk resumes: the first file not yet marked viewed. All files
|
|
46
|
+
* viewed = the end card (index === files.length), so a finished walk reopens
|
|
47
|
+
* on its summary instead of restarting.
|
|
48
|
+
*/
|
|
49
|
+
export function firstUnviewedIndex(files: WalkFile[], viewedPaths: ReadonlySet<string>): number {
|
|
50
|
+
const index = files.findIndex((file) => !viewedPaths.has(file.path));
|
|
51
|
+
return index === -1 ? files.length : index;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Viewed count against the CURRENT file set - stale paths never inflate it. */
|
|
55
|
+
export function viewedCount(files: WalkFile[], viewedPaths: ReadonlySet<string>): number {
|
|
56
|
+
return files.filter((file) => viewedPaths.has(file.path)).length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The agent's note for a file: an annotation with kind "note" whose anchor
|
|
61
|
+
* quote is the file path (the note contract in the shared review core).
|
|
62
|
+
*/
|
|
63
|
+
export function noteForFile(annotations: Annotation[], path: string): string | undefined {
|
|
64
|
+
return annotations.find((annotation) => annotation.kind === "note" && annotation.anchor.quote === path)?.body;
|
|
65
|
+
}
|
package/src/view.test.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
blockRuns,
|
|
4
|
-
buildDisplay,
|
|
5
|
-
cutBlock,
|
|
6
|
-
displayText,
|
|
7
|
-
marksByDisplay,
|
|
8
|
-
overlayMarks,
|
|
9
|
-
restoreBlock,
|
|
10
|
-
restoreLine,
|
|
11
|
-
spanKey,
|
|
12
|
-
startSpan,
|
|
13
|
-
wrapRuns,
|
|
14
|
-
} from "./view";
|
|
15
|
-
import { parseBlocks, type Annotation } from "@cueloop/schema";
|
|
16
|
-
|
|
17
|
-
const BASE = `# Plan
|
|
18
|
-
|
|
19
|
-
## Context
|
|
20
|
-
|
|
21
|
-
The daemon persists sessions to disk atomically.
|
|
22
|
-
|
|
23
|
-
- first item
|
|
24
|
-
- second item
|
|
25
|
-
`;
|
|
26
|
-
|
|
27
|
-
describe("buildDisplay reconciliation", () => {
|
|
28
|
-
test("no working copy: everything is same", () => {
|
|
29
|
-
const d = buildDisplay(BASE);
|
|
30
|
-
expect(d.every((x) => x.type === "same")).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("reworded block pairs as mod (similar)", () => {
|
|
34
|
-
const working = BASE.replace("persists sessions to disk", "writes sessions to disk");
|
|
35
|
-
const d = buildDisplay(BASE, working);
|
|
36
|
-
const mod = d.find((x) => x.type === "mod")!;
|
|
37
|
-
expect(mod.base!.text).toContain("persists");
|
|
38
|
-
expect(mod.work!.text).toContain("writes");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("unrelated cut + insert do NOT merge into a mod", () => {
|
|
42
|
-
const working = BASE.replace("- second item\n", "") + "\nA totally new closing note.\n";
|
|
43
|
-
const d = buildDisplay(BASE, working);
|
|
44
|
-
expect(d.some((x) => x.type === "del" && x.base!.text === "second item")).toBe(true);
|
|
45
|
-
expect(d.some((x) => x.type === "add" && x.work!.text.includes("closing note"))).toBe(true);
|
|
46
|
-
expect(d.some((x) => x.type === "mod")).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("ordered list items get running numbers", () => {
|
|
50
|
-
const d = buildDisplay("1. one\n2. two\n3. three\n");
|
|
51
|
-
expect(d.map((x) => x.oliNum)).toEqual([1, 2, 3]);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe("blockRuns + overlayMarks", () => {
|
|
56
|
-
test("mod blocks emit word-diff runs that reconstruct both sides", () => {
|
|
57
|
-
const d = buildDisplay("plain old text here\n", "plain new text here\n")[0]!;
|
|
58
|
-
const runs = blockRuns(d, true);
|
|
59
|
-
const oldSide = runs.filter((r) => r.role !== "ins").map((r) => r.text).join("");
|
|
60
|
-
const newSide = runs.filter((r) => r.role !== "del").map((r) => r.text).join("");
|
|
61
|
-
expect(oldSide).toBe("plain old text here");
|
|
62
|
-
expect(newSide).toBe("plain new text here");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("marks bind to working-text offsets and skip del runs", () => {
|
|
66
|
-
const d = buildDisplay("old words linger\n", "new words linger\n")[0]!;
|
|
67
|
-
const runs = blockRuns(d, true);
|
|
68
|
-
const marked = overlayMarks(runs, [{ start: 4, end: 9, role: "mark-comment" }]); // "words"
|
|
69
|
-
const markedText = marked.filter((r) => r.role === "mark-comment").map((r) => r.text).join("");
|
|
70
|
-
expect(markedText).toBe("words");
|
|
71
|
-
// del run text untouched
|
|
72
|
-
expect(marked.some((r) => r.role === "del" && r.text === "old")).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("marks never override ins emphasis", () => {
|
|
76
|
-
const d = buildDisplay("keep\n", "keep added\n")[0]!;
|
|
77
|
-
const runs = overlayMarks(blockRuns(d, true), [{ start: 0, end: 10, role: "mark-comment" }]);
|
|
78
|
-
expect(runs.some((r) => r.role === "ins")).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("marksByDisplay", () => {
|
|
83
|
-
test("annotations resolve into display coordinates", () => {
|
|
84
|
-
const working = BASE.replace("first item", "first item edited beyond recognition of it");
|
|
85
|
-
const display = buildDisplay(BASE, working);
|
|
86
|
-
const ann: Annotation = {
|
|
87
|
-
id: "a1",
|
|
88
|
-
kind: "comment",
|
|
89
|
-
anchor: { quote: "atomically", prefix: "to disk ", suffix: "." },
|
|
90
|
-
body: "define atomically",
|
|
91
|
-
createdAt: "",
|
|
92
|
-
};
|
|
93
|
-
const map = marksByDisplay([ann], display);
|
|
94
|
-
const [dispIdx, marks] = [...map.entries()][0]!;
|
|
95
|
-
expect(displayText(display[dispIdx]!)).toContain("atomically");
|
|
96
|
-
expect(marks[0]!.annotationId).toBe("a1");
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test("orphaned annotations produce no marks", () => {
|
|
100
|
-
const display = buildDisplay(BASE);
|
|
101
|
-
const ann: Annotation = {
|
|
102
|
-
id: "a1",
|
|
103
|
-
kind: "comment",
|
|
104
|
-
anchor: { quote: "text that is gone", prefix: "", suffix: "" },
|
|
105
|
-
body: "",
|
|
106
|
-
createdAt: "",
|
|
107
|
-
};
|
|
108
|
-
expect(marksByDisplay([ann], display).size).toBe(0);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("wrapRuns", () => {
|
|
113
|
-
test("wraps at word boundaries within width", () => {
|
|
114
|
-
const lines = wrapRuns([{ text: "alpha beta gamma delta", role: "plain", start: 0 }], 11);
|
|
115
|
-
const rendered = lines.map((l) => l.map((r) => r.text).join(""));
|
|
116
|
-
expect(rendered.every((l) => l.length <= 11)).toBe(true);
|
|
117
|
-
expect(rendered.join(" ").replace(/\s+/g, " ").trim()).toBe("alpha beta gamma delta");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("hard-splits words longer than the width", () => {
|
|
121
|
-
const lines = wrapRuns([{ text: "abcdefghij", role: "plain", start: 0 }], 4);
|
|
122
|
-
expect(lines.length).toBeGreaterThan(1);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("newlines force line breaks (code blocks)", () => {
|
|
126
|
-
const lines = wrapRuns([{ text: "line1\nline2", role: "plain", start: 0 }], 80);
|
|
127
|
-
expect(lines.length).toBe(2);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe("span mode", () => {
|
|
132
|
-
const text = "one two three four";
|
|
133
|
-
test("v selects the first word; l grows; w slides; $ to end", () => {
|
|
134
|
-
let s = startSpan(0, text)!;
|
|
135
|
-
expect(text.slice(s.start, s.end)).toBe("one");
|
|
136
|
-
s = spanKey(s, "l", text);
|
|
137
|
-
expect(text.slice(s.start, s.end)).toBe("one two");
|
|
138
|
-
s = spanKey(s, "w", text);
|
|
139
|
-
expect(text.slice(s.start, s.end)).toBe("two three");
|
|
140
|
-
s = spanKey(s, "$", text);
|
|
141
|
-
expect(text.slice(s.start, s.end)).toBe("two three four");
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("cut / restore round-trip", () => {
|
|
146
|
-
test("cut removes the block lines; restore puts them back", () => {
|
|
147
|
-
const blocks = parseBlocks(BASE);
|
|
148
|
-
const li = blocks.find((b) => b.text === "second item")!;
|
|
149
|
-
const cut = cutBlock(BASE, li);
|
|
150
|
-
expect(cut).not.toContain("second item");
|
|
151
|
-
const display = buildDisplay(BASE, cut);
|
|
152
|
-
const delIdx = display.findIndex((d) => d.type === "del");
|
|
153
|
-
const line = restoreLine(display, delIdx, cut.split("\n").length);
|
|
154
|
-
const restored = restoreBlock(cut, "- second item", line);
|
|
155
|
-
expect(buildDisplay(BASE, restored).every((d) => d.type === "same")).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
});
|
package/src/view.ts
DELETED
|
Binary file
|