@cueloop/client 0.1.0-alpha.59 → 0.1.0-alpha.60
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 +7 -0
- package/src/App.diff.test.tsx +11 -0
- package/src/App.export.test.tsx +5 -0
- package/src/App.plan-review.test.tsx +42 -0
- package/src/App.prototype.test.tsx +11 -0
- package/src/App.readonly.test.tsx +10 -0
- package/src/App.rendering.test.tsx +4 -0
- package/src/App.review-panel.test.tsx +7 -0
- package/src/App.submit-confirm.test.tsx +11 -0
- package/src/App.test.tsx +13 -0
- package/src/App.toast.test.tsx +2 -0
- package/src/App.tsx +27 -0
- package/src/App.walk.test.tsx +10 -0
- package/src/app-key-state.ts +1 -0
- package/src/app-screens.tsx +4 -0
- package/src/app-view-model.ts +21 -0
- package/src/attribution.ts +4 -0
- package/src/clipboard.ts +3 -0
- package/src/components/AnnotationCard.tsx +9 -0
- package/src/components/Breadcrumb.tsx +1 -0
- package/src/components/CodeBlock.tsx +1 -0
- package/src/components/CompletionOverlay.tsx +1 -0
- package/src/components/ConfirmCard.tsx +2 -0
- package/src/components/ConfirmDialog.tsx +2 -0
- package/src/components/DiffSheet.scroll.test.tsx +12 -0
- package/src/components/DiffSheet.syntax.test.tsx +3 -0
- package/src/components/DiffSheet.tsx +19 -0
- package/src/components/InboxList.tsx +2 -0
- package/src/components/KeybindsDialog.tsx +1 -0
- package/src/components/MarkerPopover.tsx +1 -0
- package/src/components/MenuBar.tsx +1 -0
- package/src/components/PlanSheet.tsx +26 -0
- package/src/components/PromptDialog.tsx +2 -0
- package/src/components/PrototypeSheet.tsx +27 -0
- package/src/components/ReviewPanel.tsx +2 -0
- package/src/components/ReviewRail.tsx +1 -0
- package/src/components/SettingsDialog.tsx +6 -0
- package/src/components/SettingsRows.tsx +4 -0
- package/src/components/Toast.tsx +1 -0
- package/src/components/agent-launcher.test.tsx +1 -0
- package/src/components/agent-launcher.tsx +2 -0
- package/src/components/diff-sheet-layout.test.ts +1 -0
- package/src/components/diff-sheet-layout.ts +17 -0
- package/src/components/plan-sheet-run-style.ts +4 -0
- package/src/components/primitives/Button.tsx +1 -0
- package/src/components/primitives/Card.tsx +1 -0
- package/src/components/primitives/Dialog.tsx +2 -0
- package/src/components/primitives/StatusBar.tsx +1 -0
- package/src/components/primitives/Tabs.tsx +4 -0
- package/src/components/quick-actions-editor.tsx +2 -0
- package/src/components/stories-app.tsx +1 -0
- package/src/components/stories.test.tsx +4 -0
- package/src/components/story.ts +6 -0
- package/src/components/syntax-highlight.ts +10 -0
- package/src/components/terminal-pane.ts +12 -0
- package/src/components/theme-context.tsx +1 -0
- package/src/components/truncate-text.ts +1 -0
- package/src/config.test.ts +18 -0
- package/src/config.ts +27 -0
- package/src/diff-hunk-curate.ts +20 -0
- package/src/diff-intraline.test.ts +1 -0
- package/src/diff-intraline.ts +14 -0
- package/src/diff-syntax.ts +22 -0
- package/src/editor.test.ts +6 -0
- package/src/editor.ts +12 -0
- package/src/ghostty-terminal.test.ts +2 -0
- package/src/ghostty-terminal.ts +8 -0
- package/src/herdr.test.ts +2 -0
- package/src/herdr.ts +3 -0
- package/src/integrations.ts +2 -0
- package/src/intent-dispatch.test.ts +6 -0
- package/src/intent-dispatch.ts +37 -0
- package/src/key-bindings.test.ts +4 -0
- package/src/key-bindings.ts +22 -0
- package/src/keymap.test.ts +13 -0
- package/src/keymap.ts +43 -0
- package/src/kitty-image.ts +9 -0
- package/src/prototype-browser.test.ts +1 -0
- package/src/prototype-browser.ts +22 -0
- package/src/pty.test.ts +3 -0
- package/src/pty.ts +15 -0
- package/src/run.ts +1 -0
- package/src/serve.test.ts +1 -0
- package/src/serve.ts +3 -0
- package/src/session-controller.curate.test.ts +7 -0
- package/src/session-controller.share.test.ts +11 -0
- package/src/session-controller.ts +79 -0
- package/src/share.ts +7 -0
- package/src/test-support.test.ts +5 -0
- package/src/test-support.ts +10 -0
- package/src/theme-presets.ts +1 -0
- package/src/use-settings-dialog.tsx +12 -0
- package/src/view-diff.test.ts +3 -0
- package/src/view-diff.ts +5 -0
- package/src/view-plan.test.ts +7 -0
- package/src/view-plan.ts +59 -0
- package/src/walk.ts +4 -0
package/src/view-diff.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface DiffRow {
|
|
|
20
20
|
export function diffRows(patchText: string): DiffRow[] {
|
|
21
21
|
const rows: DiffRow[] = [];
|
|
22
22
|
const patches = parsePatchFiles(patchText);
|
|
23
|
+
|
|
23
24
|
for (const patch of patches) {
|
|
24
25
|
for (const file of patch.files) {
|
|
25
26
|
rows.push({ kind: "file", text: fileLabel(file), file: file.name });
|
|
@@ -27,6 +28,7 @@ export function diffRows(patchText: string): DiffRow[] {
|
|
|
27
28
|
rows.push({ kind: "hunk", text: hunk.hunkSpecs ?? "@@", file: file.name });
|
|
28
29
|
let oldLine = hunk.deletionStart;
|
|
29
30
|
let newLine = hunk.additionStart;
|
|
31
|
+
|
|
30
32
|
for (const segment of hunk.hunkContent) {
|
|
31
33
|
if (segment.type === "context") {
|
|
32
34
|
for (let lineOffset = 0; lineOffset < segment.lines; lineOffset++) {
|
|
@@ -60,6 +62,7 @@ export function diffRows(patchText: string): DiffRow[] {
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
|
|
63
66
|
return rows;
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -77,6 +80,7 @@ export function diffRowAnchor(
|
|
|
77
80
|
const row = rows[rowIndex]!;
|
|
78
81
|
const prev = rows[rowIndex - 1];
|
|
79
82
|
const next = rows[rowIndex + 1];
|
|
83
|
+
|
|
80
84
|
return {
|
|
81
85
|
quote: row.text,
|
|
82
86
|
prefix:
|
|
@@ -93,5 +97,6 @@ export function diffRowAnchor(
|
|
|
93
97
|
/** Location label for the rail and feedback: file:newLine (or old for del). */
|
|
94
98
|
export function diffRowLocation(row: DiffRow): string {
|
|
95
99
|
const line = row.newLine ?? row.oldLine;
|
|
100
|
+
|
|
96
101
|
return line !== undefined ? `${row.file}:${line}` : row.file;
|
|
97
102
|
}
|
package/src/view-plan.test.ts
CHANGED
|
@@ -45,6 +45,7 @@ describe("buildDisplay reconciliation", () => {
|
|
|
45
45
|
|
|
46
46
|
// Assert
|
|
47
47
|
const mod = display.find((block) => block.type === "mod")!;
|
|
48
|
+
|
|
48
49
|
expect(mod.base!.text).toContain("persists");
|
|
49
50
|
expect(mod.work!.text).toContain("writes");
|
|
50
51
|
});
|
|
@@ -92,6 +93,7 @@ describe("blockRuns + overlayMarks", () => {
|
|
|
92
93
|
.filter((run) => run.role !== "del")
|
|
93
94
|
.map((run) => run.text)
|
|
94
95
|
.join("");
|
|
96
|
+
|
|
95
97
|
expect(oldSide).toBe("plain old text here");
|
|
96
98
|
expect(newSide).toBe("plain new text here");
|
|
97
99
|
});
|
|
@@ -109,6 +111,7 @@ describe("blockRuns + overlayMarks", () => {
|
|
|
109
111
|
.filter((run) => run.role === "mark-comment")
|
|
110
112
|
.map((run) => run.text)
|
|
111
113
|
.join("");
|
|
114
|
+
|
|
112
115
|
expect(markedText).toBe("words");
|
|
113
116
|
// del run text untouched
|
|
114
117
|
expect(marked.some((run) => run.role === "del" && run.text === "old")).toBe(true);
|
|
@@ -226,6 +229,7 @@ describe("marksByDisplay", () => {
|
|
|
226
229
|
|
|
227
230
|
// Assert
|
|
228
231
|
const [displayIndex, marks] = [...map.entries()][0]!;
|
|
232
|
+
|
|
229
233
|
expect(displayText(display[displayIndex]!)).toContain("atomically");
|
|
230
234
|
expect(marks[0]!.annotationId).toBe("a1");
|
|
231
235
|
});
|
|
@@ -270,6 +274,7 @@ describe("rendered/work offset mapping", () => {
|
|
|
270
274
|
|
|
271
275
|
// Assert
|
|
272
276
|
const renderedText = runs.map((run) => run.text).join("");
|
|
277
|
+
|
|
273
278
|
expect(renderedText.slice(rendered, rendered + "words".length)).toBe("words");
|
|
274
279
|
// reading a rendered selection over "words" recovers the work range
|
|
275
280
|
expect(workRangeForRendered(runs, rendered, rendered + "words".length)).toEqual({
|
|
@@ -315,6 +320,7 @@ describe("rendered/work offset mapping", () => {
|
|
|
315
320
|
|
|
316
321
|
// Assert
|
|
317
322
|
const renderedText = runs.map((run) => run.text).join("");
|
|
323
|
+
|
|
318
324
|
expect(renderedText.slice(renderedStart, renderedEnd + 1)).toBe("very");
|
|
319
325
|
// exact hits behave like renderedOffsetFor
|
|
320
326
|
expect(renderedOffsetAtOrAfter(runs, 0)).toBe(0);
|
|
@@ -326,6 +332,7 @@ describe("rendered/work offset mapping", () => {
|
|
|
326
332
|
|
|
327
333
|
describe("span mode", () => {
|
|
328
334
|
const text = "one two three four";
|
|
335
|
+
|
|
329
336
|
test("v selects the first word; l grows; w slides; $ to end", () => {
|
|
330
337
|
// Act
|
|
331
338
|
let span = startSpan(0, text)!;
|
package/src/view-plan.ts
CHANGED
|
@@ -43,14 +43,17 @@ function similar(baseBlock: Block, workBlock: Block): boolean {
|
|
|
43
43
|
const baseWords = words(baseBlock.text);
|
|
44
44
|
const workWords = words(workBlock.text);
|
|
45
45
|
let intersection = 0;
|
|
46
|
+
|
|
46
47
|
for (const word of baseWords) if (workWords.has(word)) intersection++;
|
|
47
48
|
const union = baseWords.size + workWords.size - intersection;
|
|
49
|
+
|
|
48
50
|
return union === 0 ? true : intersection / union >= 0.3;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/** Reconcile base blocks vs working blocks into the display list. */
|
|
52
54
|
export function buildDisplay(baseContent: string, workingContent?: string): DisplayBlock[] {
|
|
53
55
|
const baseBlocks = parseBlocks(baseContent);
|
|
56
|
+
|
|
54
57
|
if (workingContent === undefined) {
|
|
55
58
|
const display: DisplayBlock[] = baseBlocks.map((block) => ({
|
|
56
59
|
type: "same",
|
|
@@ -58,7 +61,9 @@ export function buildDisplay(baseContent: string, workingContent?: string): Disp
|
|
|
58
61
|
work: block,
|
|
59
62
|
base: block,
|
|
60
63
|
}));
|
|
64
|
+
|
|
61
65
|
numberOrderedListItems(display);
|
|
66
|
+
|
|
62
67
|
return display;
|
|
63
68
|
}
|
|
64
69
|
const workBlocks = parseBlocks(workingContent);
|
|
@@ -70,8 +75,10 @@ export function buildDisplay(baseContent: string, workingContent?: string): Disp
|
|
|
70
75
|
);
|
|
71
76
|
const display: DisplayBlock[] = [];
|
|
72
77
|
let opIndex = 0;
|
|
78
|
+
|
|
73
79
|
while (opIndex < ops.length) {
|
|
74
80
|
const op = ops[opIndex]!;
|
|
81
|
+
|
|
75
82
|
if (op.kind === "ctx") {
|
|
76
83
|
display.push({ type: "same", kind: op.newValue!.kind, work: op.newValue, base: op.oldValue });
|
|
77
84
|
opIndex++;
|
|
@@ -79,14 +86,17 @@ export function buildDisplay(baseContent: string, workingContent?: string): Disp
|
|
|
79
86
|
}
|
|
80
87
|
const deletedBlocks: Block[] = [];
|
|
81
88
|
const addedBlocks: Block[] = [];
|
|
89
|
+
|
|
82
90
|
while (opIndex < ops.length && ops[opIndex]!.kind !== "ctx") {
|
|
83
91
|
const changeOp = ops[opIndex]!;
|
|
92
|
+
|
|
84
93
|
if (changeOp.kind === "del") deletedBlocks.push(changeOp.oldValue!);
|
|
85
94
|
else addedBlocks.push(changeOp.newValue!);
|
|
86
95
|
opIndex++;
|
|
87
96
|
}
|
|
88
97
|
let deletedIndex = 0;
|
|
89
98
|
let addedIndex = 0;
|
|
99
|
+
|
|
90
100
|
while (deletedIndex < deletedBlocks.length && addedIndex < addedBlocks.length) {
|
|
91
101
|
if (similar(deletedBlocks[deletedIndex]!, addedBlocks[addedIndex]!)) {
|
|
92
102
|
display.push({
|
|
@@ -129,11 +139,13 @@ export function buildDisplay(baseContent: string, workingContent?: string): Disp
|
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
141
|
numberOrderedListItems(display);
|
|
142
|
+
|
|
132
143
|
return display;
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
function numberOrderedListItems(display: DisplayBlock[]): void {
|
|
136
147
|
let runLength = 0;
|
|
148
|
+
|
|
137
149
|
for (const block of display) {
|
|
138
150
|
if (block.kind === "oli") block.orderedItemNumber = ++runLength;
|
|
139
151
|
else runLength = 0;
|
|
@@ -188,14 +200,18 @@ export function marksByDisplay(
|
|
|
188
200
|
.filter((entry) => entry.block.work !== undefined);
|
|
189
201
|
const workBlocks = workEntries.map((entry) => entry.block.work!);
|
|
190
202
|
const marksByIndex = new Map<number, Mark[]>();
|
|
203
|
+
|
|
191
204
|
for (const annotation of annotations) {
|
|
192
205
|
// an addressed annotation keeps its record but paints no highlight
|
|
193
206
|
if (isAddressed(annotation)) continue;
|
|
194
207
|
const resolved = resolveAnchor(annotation.anchor, workBlocks);
|
|
208
|
+
|
|
195
209
|
if (!resolved) continue;
|
|
196
210
|
const entry = workEntries[resolved.blockIndex];
|
|
211
|
+
|
|
197
212
|
if (!entry) continue;
|
|
198
213
|
const marks = marksByIndex.get(entry.displayIndex) ?? [];
|
|
214
|
+
|
|
199
215
|
marks.push({
|
|
200
216
|
start: resolved.start,
|
|
201
217
|
end: resolved.end,
|
|
@@ -204,16 +220,19 @@ export function marksByDisplay(
|
|
|
204
220
|
});
|
|
205
221
|
marksByIndex.set(entry.displayIndex, marks);
|
|
206
222
|
}
|
|
223
|
+
|
|
207
224
|
return marksByIndex;
|
|
208
225
|
}
|
|
209
226
|
|
|
210
227
|
/** Base runs for a display block: plain text, or word-diff for mod blocks. */
|
|
211
228
|
export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
|
|
212
229
|
const literal = block.kind === "code" || block.kind === "hr";
|
|
230
|
+
|
|
213
231
|
if (block.type === "mod" && markup) {
|
|
214
232
|
const changes = wordLevelChanges(block.base!.text, block.work!.text);
|
|
215
233
|
const runs: StyleRun[] = [];
|
|
216
234
|
let workOffset = 0;
|
|
235
|
+
|
|
217
236
|
for (const change of changes) {
|
|
218
237
|
if (change.kind === "removed") {
|
|
219
238
|
runs.push({ text: change.text, role: "del", start: null });
|
|
@@ -225,19 +244,23 @@ export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
|
|
|
225
244
|
const segment = literal
|
|
226
245
|
? [{ text: change.text, role: "plain" as const, start: workOffset }]
|
|
227
246
|
: inlineStyleRuns(change.text, workOffset);
|
|
247
|
+
|
|
228
248
|
for (const run of segment) {
|
|
229
249
|
runs.push(change.kind === "added" ? { ...run, role: "ins" } : run);
|
|
230
250
|
}
|
|
231
251
|
workOffset += change.text.length;
|
|
232
252
|
}
|
|
253
|
+
|
|
233
254
|
return runs;
|
|
234
255
|
}
|
|
235
256
|
const text = displayText(block);
|
|
257
|
+
|
|
236
258
|
// Prose carries inline markup; code fences and rules are literal. Concealed
|
|
237
259
|
// markers are dropped, so every rendered cell still maps to a work offset.
|
|
238
260
|
if (markup && !literal) {
|
|
239
261
|
return inlineStyleRuns(text, 0);
|
|
240
262
|
}
|
|
263
|
+
|
|
241
264
|
return [{ text, role: "plain", start: 0 }];
|
|
242
265
|
}
|
|
243
266
|
|
|
@@ -248,6 +271,7 @@ export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
|
|
|
248
271
|
export function safeLinkHref(href: string | undefined): string | undefined {
|
|
249
272
|
if (href === undefined) return undefined;
|
|
250
273
|
if (!/^(https?:|mailto:)/i.test(href)) return undefined;
|
|
274
|
+
|
|
251
275
|
return /^[\x21-\x7e]+$/.test(href) ? href : undefined;
|
|
252
276
|
}
|
|
253
277
|
|
|
@@ -258,6 +282,7 @@ export function safeLinkHref(href: string | undefined): string | undefined {
|
|
|
258
282
|
*/
|
|
259
283
|
export function inlineStyleRuns(text: string, base: number): StyleRun[] {
|
|
260
284
|
const runs: StyleRun[] = [];
|
|
285
|
+
|
|
261
286
|
for (const run of inlineRuns(text)) {
|
|
262
287
|
if (run.role === "marker" || run.start === null) continue;
|
|
263
288
|
runs.push({
|
|
@@ -267,6 +292,7 @@ export function inlineStyleRuns(text: string, base: number): StyleRun[] {
|
|
|
267
292
|
...(run.href !== undefined ? { href: run.href } : {}),
|
|
268
293
|
});
|
|
269
294
|
}
|
|
295
|
+
|
|
270
296
|
return runs;
|
|
271
297
|
}
|
|
272
298
|
|
|
@@ -274,15 +300,18 @@ export function inlineStyleRuns(text: string, base: number): StyleRun[] {
|
|
|
274
300
|
export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
|
|
275
301
|
if (!marks.length) return runs;
|
|
276
302
|
const splitRuns: StyleRun[] = [];
|
|
303
|
+
|
|
277
304
|
for (const run of runs) {
|
|
278
305
|
if (run.start === null) {
|
|
279
306
|
splitRuns.push(run);
|
|
280
307
|
continue;
|
|
281
308
|
}
|
|
282
309
|
const bounds = new Set<number>([0, run.text.length]);
|
|
310
|
+
|
|
283
311
|
for (const mark of marks) {
|
|
284
312
|
const boundStart = Math.max(0, mark.start - run.start);
|
|
285
313
|
const boundEnd = Math.min(run.text.length, mark.end - run.start);
|
|
314
|
+
|
|
286
315
|
if (boundStart < boundEnd) {
|
|
287
316
|
bounds.add(boundStart);
|
|
288
317
|
bounds.add(boundEnd);
|
|
@@ -290,14 +319,17 @@ export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
|
|
|
290
319
|
}
|
|
291
320
|
const runStart = run.start;
|
|
292
321
|
const boundaries = [...bounds].sort((left, right) => left - right);
|
|
322
|
+
|
|
293
323
|
for (let boundaryIndex = 0; boundaryIndex < boundaries.length - 1; boundaryIndex++) {
|
|
294
324
|
const sliceStart = boundaries[boundaryIndex]!;
|
|
295
325
|
const sliceEnd = boundaries[boundaryIndex + 1]!;
|
|
326
|
+
|
|
296
327
|
if (sliceStart >= sliceEnd) continue;
|
|
297
328
|
const absoluteStart = runStart + sliceStart;
|
|
298
329
|
const mark = marks.find(
|
|
299
330
|
(candidate) => candidate.start <= absoluteStart && runStart + sliceEnd <= candidate.end,
|
|
300
331
|
);
|
|
332
|
+
|
|
301
333
|
splitRuns.push({
|
|
302
334
|
text: run.text.slice(sliceStart, sliceEnd),
|
|
303
335
|
// a mark never overrides ins/del emphasis; it rides on plain text
|
|
@@ -307,6 +339,7 @@ export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
|
|
|
307
339
|
});
|
|
308
340
|
}
|
|
309
341
|
}
|
|
342
|
+
|
|
310
343
|
return splitRuns;
|
|
311
344
|
}
|
|
312
345
|
|
|
@@ -319,12 +352,14 @@ export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
|
|
|
319
352
|
*/
|
|
320
353
|
export function renderedOffsetFor(runs: StyleRun[], workOffset: number): number | null {
|
|
321
354
|
let rendered = 0;
|
|
355
|
+
|
|
322
356
|
for (const run of runs) {
|
|
323
357
|
if (run.start !== null && workOffset >= run.start && workOffset < run.start + run.text.length) {
|
|
324
358
|
return rendered + (workOffset - run.start);
|
|
325
359
|
}
|
|
326
360
|
rendered += run.text.length;
|
|
327
361
|
}
|
|
362
|
+
|
|
328
363
|
return null;
|
|
329
364
|
}
|
|
330
365
|
|
|
@@ -336,12 +371,14 @@ export function renderedOffsetFor(runs: StyleRun[], workOffset: number): number
|
|
|
336
371
|
*/
|
|
337
372
|
export function renderedOffsetAtOrAfter(runs: StyleRun[], workOffset: number): number | null {
|
|
338
373
|
let rendered = 0;
|
|
374
|
+
|
|
339
375
|
for (const run of runs) {
|
|
340
376
|
if (run.start !== null && run.start + run.text.length > workOffset) {
|
|
341
377
|
return rendered + Math.max(0, workOffset - run.start);
|
|
342
378
|
}
|
|
343
379
|
rendered += run.text.length;
|
|
344
380
|
}
|
|
381
|
+
|
|
345
382
|
return null;
|
|
346
383
|
}
|
|
347
384
|
|
|
@@ -352,12 +389,14 @@ export function renderedOffsetAtOrAfter(runs: StyleRun[], workOffset: number): n
|
|
|
352
389
|
export function renderedOffsetAtOrBefore(runs: StyleRun[], workOffset: number): number | null {
|
|
353
390
|
let rendered = 0;
|
|
354
391
|
let best: number | null = null;
|
|
392
|
+
|
|
355
393
|
for (const run of runs) {
|
|
356
394
|
if (run.start !== null && run.start <= workOffset) {
|
|
357
395
|
best = rendered + Math.min(workOffset - run.start, run.text.length - 1);
|
|
358
396
|
}
|
|
359
397
|
rendered += run.text.length;
|
|
360
398
|
}
|
|
399
|
+
|
|
361
400
|
return best;
|
|
362
401
|
}
|
|
363
402
|
|
|
@@ -373,22 +412,27 @@ export function workRangeForRendered(
|
|
|
373
412
|
): { start: number; end: number } | null {
|
|
374
413
|
let rendered = 0;
|
|
375
414
|
let range: { start: number; end: number } | null = null;
|
|
415
|
+
|
|
376
416
|
for (const run of runs) {
|
|
377
417
|
const runRenderedStart = rendered;
|
|
378
418
|
const runRenderedEnd = rendered + run.text.length;
|
|
419
|
+
|
|
379
420
|
rendered = runRenderedEnd;
|
|
380
421
|
if (run.start === null) continue;
|
|
381
422
|
const overlapStart = Math.max(renderedStart, runRenderedStart);
|
|
382
423
|
const overlapEnd = Math.min(renderedEnd, runRenderedEnd);
|
|
424
|
+
|
|
383
425
|
if (overlapEnd <= overlapStart) continue;
|
|
384
426
|
const workStart = run.start + (overlapStart - runRenderedStart);
|
|
385
427
|
const workEnd = run.start + (overlapEnd - runRenderedStart);
|
|
428
|
+
|
|
386
429
|
if (!range) range = { start: workStart, end: workEnd };
|
|
387
430
|
else {
|
|
388
431
|
range.start = Math.min(range.start, workStart);
|
|
389
432
|
range.end = Math.max(range.end, workEnd);
|
|
390
433
|
}
|
|
391
434
|
}
|
|
435
|
+
|
|
392
436
|
return range;
|
|
393
437
|
}
|
|
394
438
|
|
|
@@ -400,10 +444,12 @@ export function revisionDelta(
|
|
|
400
444
|
const ops = lcsDiff(previousContent.split("\n"), nextContent.split("\n"));
|
|
401
445
|
let added = 0;
|
|
402
446
|
let removed = 0;
|
|
447
|
+
|
|
403
448
|
for (const op of ops) {
|
|
404
449
|
if (op.kind === "add") added++;
|
|
405
450
|
else if (op.kind === "del") removed++;
|
|
406
451
|
}
|
|
452
|
+
|
|
407
453
|
return { added, removed };
|
|
408
454
|
}
|
|
409
455
|
|
|
@@ -421,14 +467,18 @@ export function wordRanges(text: string): [number, number][] {
|
|
|
421
467
|
const ranges: [number, number][] = [];
|
|
422
468
|
const wordPattern = /\S+/g;
|
|
423
469
|
let match: RegExpExecArray | null;
|
|
470
|
+
|
|
424
471
|
while ((match = wordPattern.exec(text)))
|
|
425
472
|
ranges.push([match.index, match.index + match[0]!.length]);
|
|
473
|
+
|
|
426
474
|
return ranges;
|
|
427
475
|
}
|
|
428
476
|
|
|
429
477
|
export function startSpan(displayIndex: number, text: string): SpanState | null {
|
|
430
478
|
const words = wordRanges(text);
|
|
479
|
+
|
|
431
480
|
if (!words.length) return null;
|
|
481
|
+
|
|
432
482
|
return { displayIndex, wordIndex: 0, wordEnd: 0, start: words[0]![0], end: words[0]![1] };
|
|
433
483
|
}
|
|
434
484
|
|
|
@@ -445,11 +495,14 @@ export function spanFromRange(
|
|
|
445
495
|
): SpanState | null {
|
|
446
496
|
const words = wordRanges(text);
|
|
447
497
|
const wordIndex = words.findIndex((word) => word[1] > start);
|
|
498
|
+
|
|
448
499
|
if (wordIndex === -1) return null;
|
|
449
500
|
let wordEnd = wordIndex;
|
|
501
|
+
|
|
450
502
|
for (let index = wordIndex; index < words.length && words[index]![0] < end; index++) {
|
|
451
503
|
wordEnd = index;
|
|
452
504
|
}
|
|
505
|
+
|
|
453
506
|
return {
|
|
454
507
|
displayIndex,
|
|
455
508
|
wordIndex,
|
|
@@ -463,14 +516,17 @@ export function spanKey(span: SpanState, key: string, text: string): SpanState {
|
|
|
463
516
|
const words = wordRanges(text);
|
|
464
517
|
const nextSpan = { ...span };
|
|
465
518
|
const width = nextSpan.wordEnd - nextSpan.wordIndex;
|
|
519
|
+
|
|
466
520
|
if (key === "l") nextSpan.wordEnd = Math.min(words.length - 1, nextSpan.wordEnd + 1);
|
|
467
521
|
else if (key === "h") nextSpan.wordEnd = Math.max(nextSpan.wordIndex, nextSpan.wordEnd - 1);
|
|
468
522
|
else if (key === "w") {
|
|
469
523
|
const slidTo = Math.min(words.length - 1 - width, nextSpan.wordIndex + 1);
|
|
524
|
+
|
|
470
525
|
nextSpan.wordIndex = slidTo;
|
|
471
526
|
nextSpan.wordEnd = slidTo + width;
|
|
472
527
|
} else if (key === "b") {
|
|
473
528
|
const slidTo = Math.max(0, nextSpan.wordIndex - 1);
|
|
529
|
+
|
|
474
530
|
nextSpan.wordIndex = slidTo;
|
|
475
531
|
nextSpan.wordEnd = slidTo + width;
|
|
476
532
|
} else if (key === "$") nextSpan.wordEnd = words.length - 1;
|
|
@@ -480,6 +536,7 @@ export function spanKey(span: SpanState, key: string, text: string): SpanState {
|
|
|
480
536
|
} else return span;
|
|
481
537
|
nextSpan.start = words[nextSpan.wordIndex]![0];
|
|
482
538
|
nextSpan.end = words[nextSpan.wordEnd]![1];
|
|
539
|
+
|
|
483
540
|
return nextSpan;
|
|
484
541
|
}
|
|
485
542
|
|
|
@@ -489,7 +546,9 @@ export function spanKey(span: SpanState, key: string, text: string): SpanState {
|
|
|
489
546
|
export function nextWorkBlock(display: DisplayBlock[], displayIndex: number): Block | undefined {
|
|
490
547
|
for (let candidateIndex = displayIndex + 1; candidateIndex < display.length; candidateIndex++) {
|
|
491
548
|
const workBlock = display[candidateIndex]!.work;
|
|
549
|
+
|
|
492
550
|
if (workBlock) return workBlock;
|
|
493
551
|
}
|
|
552
|
+
|
|
494
553
|
return undefined;
|
|
495
554
|
}
|
package/src/walk.ts
CHANGED
|
@@ -24,8 +24,10 @@ export const WALK_PREVIEW_ROWS = 5;
|
|
|
24
24
|
export function walkFiles(rows: DiffRow[]): WalkFile[] {
|
|
25
25
|
const files: WalkFile[] = [];
|
|
26
26
|
const byPath = new Map<string, WalkFile>();
|
|
27
|
+
|
|
27
28
|
for (const row of rows) {
|
|
28
29
|
let file = byPath.get(row.file);
|
|
30
|
+
|
|
29
31
|
if (!file) {
|
|
30
32
|
file = { path: row.file, added: 0, removed: 0, preview: [] };
|
|
31
33
|
byPath.set(row.file, file);
|
|
@@ -41,6 +43,7 @@ export function walkFiles(rows: DiffRow[]): WalkFile[] {
|
|
|
41
43
|
});
|
|
42
44
|
}
|
|
43
45
|
}
|
|
46
|
+
|
|
44
47
|
return files;
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -51,6 +54,7 @@ export function walkFiles(rows: DiffRow[]): WalkFile[] {
|
|
|
51
54
|
*/
|
|
52
55
|
export function firstUnviewedIndex(files: WalkFile[], viewedPaths: ReadonlySet<string>): number {
|
|
53
56
|
const index = files.findIndex((file) => !viewedPaths.has(file.path));
|
|
57
|
+
|
|
54
58
|
return index === -1 ? files.length : index;
|
|
55
59
|
}
|
|
56
60
|
|