@cueloop/client 0.1.0-alpha.21 → 0.1.0-alpha.23
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 +5 -5
- package/src/App.autoclose.test.tsx +29 -5
- package/src/App.diff.test.tsx +20 -0
- package/src/App.export.test.tsx +12 -8
- package/src/App.plan-review.test.tsx +140 -7
- package/src/App.readonly.test.tsx +19 -5
- package/src/App.rendering.test.tsx +15 -0
- package/src/App.review-panel.test.tsx +29 -6
- package/src/App.submit-confirm.test.tsx +57 -6
- package/src/App.test.tsx +62 -5
- package/src/App.tsx +77 -183
- package/src/App.walk.test.tsx +63 -8
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +26 -0
- package/src/components/PlanSheet.stories.tsx +1 -1
- package/src/components/PlanSheet.tsx +13 -6
- package/src/components/ReviewRail.tsx +16 -4
- package/src/components/stories.test.tsx +5 -0
- package/src/config.test.ts +62 -0
- package/src/config.ts +1 -0
- package/src/editor.test.ts +24 -0
- package/src/herdr.test.ts +4 -0
- package/src/index.ts +2 -0
- package/src/integrations.test.ts +12 -0
- package/src/intent-dispatch.test.ts +213 -0
- package/src/intent-dispatch.ts +270 -0
- package/src/key-bindings.test.ts +27 -5
- package/src/key-bindings.ts +11 -1
- package/src/keymap.test.ts +97 -5
- package/src/keymap.ts +25 -2
- package/src/serve.test.ts +7 -0
- package/src/session-controller.ts +22 -3
- package/src/share.ts +47 -0
- package/src/view-diff.test.ts +11 -0
- package/src/view-diff.ts +6 -6
- package/src/view-plan.test.ts +81 -0
- package/src/view-plan.ts +3 -0
- package/src/walk.test.ts +6 -0
package/src/view-plan.test.ts
CHANGED
|
@@ -25,36 +25,57 @@ The daemon persists sessions to disk atomically.
|
|
|
25
25
|
|
|
26
26
|
describe("buildDisplay reconciliation", () => {
|
|
27
27
|
test("no working copy: everything is same", () => {
|
|
28
|
+
// Act
|
|
28
29
|
const display = buildDisplay(BASE);
|
|
30
|
+
|
|
31
|
+
// Assert
|
|
29
32
|
expect(display.every((block) => block.type === "same")).toBe(true);
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
test("reworded block pairs as mod (similar)", () => {
|
|
36
|
+
// Arrange
|
|
33
37
|
const working = BASE.replace("persists sessions to disk", "writes sessions to disk");
|
|
38
|
+
|
|
39
|
+
// Act
|
|
34
40
|
const display = buildDisplay(BASE, working);
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
35
43
|
const mod = display.find((block) => block.type === "mod")!;
|
|
36
44
|
expect(mod.base!.text).toContain("persists");
|
|
37
45
|
expect(mod.work!.text).toContain("writes");
|
|
38
46
|
});
|
|
39
47
|
|
|
40
48
|
test("unrelated cut + insert do NOT merge into a mod", () => {
|
|
49
|
+
// Arrange
|
|
41
50
|
const working = BASE.replace("- second item\n", "") + "\nA totally new closing note.\n";
|
|
51
|
+
|
|
52
|
+
// Act
|
|
42
53
|
const display = buildDisplay(BASE, working);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
43
56
|
expect(display.some((block) => block.type === "del" && block.base!.text === "second item")).toBe(true);
|
|
44
57
|
expect(display.some((block) => block.type === "add" && block.work!.text.includes("closing note"))).toBe(true);
|
|
45
58
|
expect(display.some((block) => block.type === "mod")).toBe(false);
|
|
46
59
|
});
|
|
47
60
|
|
|
48
61
|
test("ordered list items get running numbers", () => {
|
|
62
|
+
// Act
|
|
49
63
|
const display = buildDisplay("1. one\n2. two\n3. three\n");
|
|
64
|
+
|
|
65
|
+
// Assert
|
|
50
66
|
expect(display.map((block) => block.orderedItemNumber)).toEqual([1, 2, 3]);
|
|
51
67
|
});
|
|
52
68
|
});
|
|
53
69
|
|
|
54
70
|
describe("blockRuns + overlayMarks", () => {
|
|
55
71
|
test("mod blocks emit word-diff runs that reconstruct both sides", () => {
|
|
72
|
+
// Arrange
|
|
56
73
|
const block = buildDisplay("plain old text here\n", "plain new text here\n")[0]!;
|
|
74
|
+
|
|
75
|
+
// Act
|
|
57
76
|
const runs = blockRuns(block, true);
|
|
77
|
+
|
|
78
|
+
// Assert
|
|
58
79
|
const oldSide = runs.filter((run) => run.role !== "ins").map((run) => run.text).join("");
|
|
59
80
|
const newSide = runs.filter((run) => run.role !== "del").map((run) => run.text).join("");
|
|
60
81
|
expect(oldSide).toBe("plain old text here");
|
|
@@ -62,9 +83,14 @@ describe("blockRuns + overlayMarks", () => {
|
|
|
62
83
|
});
|
|
63
84
|
|
|
64
85
|
test("marks bind to working-text offsets and skip del runs", () => {
|
|
86
|
+
// Arrange
|
|
65
87
|
const block = buildDisplay("old words linger\n", "new words linger\n")[0]!;
|
|
66
88
|
const runs = blockRuns(block, true);
|
|
89
|
+
|
|
90
|
+
// Act
|
|
67
91
|
const marked = overlayMarks(runs, [{ start: 4, end: 9, role: "mark-comment" }]); // "words"
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
68
94
|
const markedText = marked.filter((run) => run.role === "mark-comment").map((run) => run.text).join("");
|
|
69
95
|
expect(markedText).toBe("words");
|
|
70
96
|
// del run text untouched
|
|
@@ -72,14 +98,20 @@ describe("blockRuns + overlayMarks", () => {
|
|
|
72
98
|
});
|
|
73
99
|
|
|
74
100
|
test("marks never override ins emphasis", () => {
|
|
101
|
+
// Arrange
|
|
75
102
|
const block = buildDisplay("keep\n", "keep added\n")[0]!;
|
|
103
|
+
|
|
104
|
+
// Act
|
|
76
105
|
const runs = overlayMarks(blockRuns(block, true), [{ start: 0, end: 10, role: "mark-comment" }]);
|
|
106
|
+
|
|
107
|
+
// Assert
|
|
77
108
|
expect(runs.some((run) => run.role === "ins")).toBe(true);
|
|
78
109
|
});
|
|
79
110
|
});
|
|
80
111
|
|
|
81
112
|
describe("marksByDisplay", () => {
|
|
82
113
|
test("annotations resolve into display coordinates", () => {
|
|
114
|
+
// Arrange
|
|
83
115
|
const working = BASE.replace("first item", "first item edited beyond recognition of it");
|
|
84
116
|
const display = buildDisplay(BASE, working);
|
|
85
117
|
const ann: Annotation = {
|
|
@@ -89,13 +121,18 @@ describe("marksByDisplay", () => {
|
|
|
89
121
|
body: "define atomically",
|
|
90
122
|
createdAt: "",
|
|
91
123
|
};
|
|
124
|
+
|
|
125
|
+
// Act
|
|
92
126
|
const map = marksByDisplay([ann], display);
|
|
127
|
+
|
|
128
|
+
// Assert
|
|
93
129
|
const [displayIndex, marks] = [...map.entries()][0]!;
|
|
94
130
|
expect(displayText(display[displayIndex]!)).toContain("atomically");
|
|
95
131
|
expect(marks[0]!.annotationId).toBe("a1");
|
|
96
132
|
});
|
|
97
133
|
|
|
98
134
|
test("orphaned annotations produce no marks", () => {
|
|
135
|
+
// Arrange
|
|
99
136
|
const display = buildDisplay(BASE);
|
|
100
137
|
const ann: Annotation = {
|
|
101
138
|
id: "a1",
|
|
@@ -104,25 +141,35 @@ describe("marksByDisplay", () => {
|
|
|
104
141
|
body: "",
|
|
105
142
|
createdAt: "",
|
|
106
143
|
};
|
|
144
|
+
|
|
145
|
+
// Assert
|
|
107
146
|
expect(marksByDisplay([ann], display).size).toBe(0);
|
|
108
147
|
});
|
|
109
148
|
});
|
|
110
149
|
|
|
111
150
|
describe("rendered/work offset mapping", () => {
|
|
112
151
|
test("plain blocks map 1:1 in both directions", () => {
|
|
152
|
+
// Arrange
|
|
113
153
|
const block = buildDisplay("alpha beta gamma\n")[0]!;
|
|
114
154
|
const runs = blockRuns(block, true);
|
|
155
|
+
|
|
156
|
+
// Assert
|
|
115
157
|
expect(renderedOffsetFor(runs, 6)).toBe(6);
|
|
116
158
|
expect(workRangeForRendered(runs, 6, 10)).toEqual({ start: 6, end: 10 });
|
|
117
159
|
});
|
|
118
160
|
|
|
119
161
|
test("inline del runs shift rendered offsets in word-diffed blocks", () => {
|
|
162
|
+
// Arrange
|
|
120
163
|
// rendered: "old new words linger" - "old " is a del run without offsets
|
|
121
164
|
const block = buildDisplay("old words linger\n", "new words linger\n")[0]!;
|
|
122
165
|
const runs = blockRuns(block, true);
|
|
123
166
|
const workText = displayText(block);
|
|
124
167
|
const wordsAt = workText.indexOf("words");
|
|
168
|
+
|
|
169
|
+
// Act
|
|
125
170
|
const rendered = renderedOffsetFor(runs, wordsAt)!;
|
|
171
|
+
|
|
172
|
+
// Assert
|
|
126
173
|
const renderedText = runs.map((run) => run.text).join("");
|
|
127
174
|
expect(renderedText.slice(rendered, rendered + "words".length)).toBe("words");
|
|
128
175
|
// reading a rendered selection over "words" recovers the work range
|
|
@@ -133,16 +180,24 @@ describe("rendered/work offset mapping", () => {
|
|
|
133
180
|
});
|
|
134
181
|
|
|
135
182
|
test("a rendered selection spanning a del run keeps only positioned text", () => {
|
|
183
|
+
// Arrange
|
|
136
184
|
const block = buildDisplay("old words\n", "new words\n")[0]!;
|
|
137
185
|
const runs = blockRuns(block, true);
|
|
138
186
|
const renderedText = runs.map((run) => run.text).join("");
|
|
187
|
+
|
|
188
|
+
// Act
|
|
139
189
|
// select everything rendered: the work range is the whole work text
|
|
140
190
|
const range = workRangeForRendered(runs, 0, renderedText.length)!;
|
|
191
|
+
|
|
192
|
+
// Assert
|
|
141
193
|
expect(displayText(block).slice(range.start, range.end)).toBe(displayText(block));
|
|
142
194
|
});
|
|
143
195
|
|
|
144
196
|
test("offsets outside any positioned run map to null", () => {
|
|
197
|
+
// Arrange
|
|
145
198
|
const runs = blockRuns(buildDisplay("short\n")[0]!, true);
|
|
199
|
+
|
|
200
|
+
// Assert
|
|
146
201
|
expect(renderedOffsetFor(runs, 99)).toBeNull();
|
|
147
202
|
expect(workRangeForRendered(runs, 90, 99)).toBeNull();
|
|
148
203
|
});
|
|
@@ -151,28 +206,54 @@ describe("rendered/work offset mapping", () => {
|
|
|
151
206
|
describe("span mode", () => {
|
|
152
207
|
const text = "one two three four";
|
|
153
208
|
test("v selects the first word; l grows; w slides; $ to end", () => {
|
|
209
|
+
// Act
|
|
154
210
|
let span = startSpan(0, text)!;
|
|
211
|
+
|
|
212
|
+
// Assert
|
|
155
213
|
expect(text.slice(span.start, span.end)).toBe("one");
|
|
214
|
+
|
|
215
|
+
// Act
|
|
156
216
|
span = spanKey(span, "l", text);
|
|
217
|
+
|
|
218
|
+
// Assert
|
|
157
219
|
expect(text.slice(span.start, span.end)).toBe("one two");
|
|
220
|
+
|
|
221
|
+
// Act
|
|
158
222
|
span = spanKey(span, "w", text);
|
|
223
|
+
|
|
224
|
+
// Assert
|
|
159
225
|
expect(text.slice(span.start, span.end)).toBe("two three");
|
|
226
|
+
|
|
227
|
+
// Act
|
|
160
228
|
span = spanKey(span, "$", text);
|
|
229
|
+
|
|
230
|
+
// Assert
|
|
161
231
|
expect(text.slice(span.start, span.end)).toBe("two three four");
|
|
162
232
|
});
|
|
163
233
|
});
|
|
164
234
|
|
|
165
235
|
describe("cut / restore round-trip", () => {
|
|
166
236
|
test("cut removes the block lines; restore puts them back", () => {
|
|
237
|
+
// Arrange
|
|
167
238
|
const blocks = parseBlocks(BASE);
|
|
168
239
|
const listItem = blocks.find((candidate) => candidate.text === "second item")!;
|
|
240
|
+
|
|
241
|
+
// Act
|
|
169
242
|
const cut = cutBlock(BASE, listItem);
|
|
243
|
+
|
|
244
|
+
// Assert
|
|
170
245
|
expect(cut).not.toContain("second item");
|
|
246
|
+
|
|
247
|
+
// Arrange
|
|
171
248
|
const display = buildDisplay(BASE, cut);
|
|
172
249
|
const deletedIndex = display.findIndex((block) => block.type === "del");
|
|
173
250
|
const line = restoreLine(nextWorkBlock(display, deletedIndex), cut.split("\n").length);
|
|
251
|
+
|
|
252
|
+
// Act
|
|
174
253
|
// undefined = the working copy round-tripped back to the submitted revision
|
|
175
254
|
const restored = restoreBlock(BASE, cut, listItem, line);
|
|
255
|
+
|
|
256
|
+
// Assert
|
|
176
257
|
expect(restored).toBeUndefined();
|
|
177
258
|
expect(buildDisplay(BASE, restored).every((block) => block.type === "same")).toBe(true);
|
|
178
259
|
});
|
package/src/view-plan.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
isAddressed,
|
|
11
12
|
lcsDiff,
|
|
12
13
|
parseBlocks,
|
|
13
14
|
resolveAnchor,
|
|
@@ -161,6 +162,8 @@ export function marksByDisplay(
|
|
|
161
162
|
const workBlocks = workEntries.map((entry) => entry.block.work!);
|
|
162
163
|
const marksByIndex = new Map<number, Mark[]>();
|
|
163
164
|
for (const annotation of annotations) {
|
|
165
|
+
// an addressed annotation keeps its record but paints no highlight
|
|
166
|
+
if (isAddressed(annotation)) continue;
|
|
164
167
|
const resolved = resolveAnchor(annotation.anchor, workBlocks);
|
|
165
168
|
if (!resolved) continue;
|
|
166
169
|
const entry = workEntries[resolved.blockIndex];
|
package/src/walk.test.ts
CHANGED
|
@@ -92,12 +92,16 @@ describe("noteForFile", () => {
|
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
test("finds the note anchored at the file path", () => {
|
|
95
|
+
// Arrange
|
|
95
96
|
const annotations = [note("src/a.ts", "Renames b and adds c."), note("src/b.ts", "Swaps the line.")];
|
|
97
|
+
|
|
98
|
+
// Assert
|
|
96
99
|
expect(noteForFile(annotations, "src/b.ts")).toBe("Swaps the line.");
|
|
97
100
|
expect(noteForFile(annotations, "src/c.ts")).toBeUndefined();
|
|
98
101
|
});
|
|
99
102
|
|
|
100
103
|
test("ignores non-note annotations that happen to quote the path", () => {
|
|
104
|
+
// Arrange
|
|
101
105
|
const comment: Annotation = {
|
|
102
106
|
id: "a_1",
|
|
103
107
|
kind: "comment",
|
|
@@ -105,6 +109,8 @@ describe("noteForFile", () => {
|
|
|
105
109
|
body: "not a note",
|
|
106
110
|
createdAt: "now",
|
|
107
111
|
};
|
|
112
|
+
|
|
113
|
+
// Assert
|
|
108
114
|
expect(noteForFile([comment], "src/a.ts")).toBeUndefined();
|
|
109
115
|
});
|
|
110
116
|
});
|