@cueloop/client 0.1.0-alpha.102 → 0.1.0-alpha.104
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/annotations/annotation-palette.ts +1 -0
- package/src/annotations/components/AnnotationCards.tsx +4 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
- package/src/annotations/discussions.ts +1 -0
- package/src/annotations/thread-selection.ts +6 -0
- package/src/annotations/use-annotation-surface.tsx +97 -0
- package/src/app/App.deleted-scroll.test.tsx +3 -0
- package/src/app/App.harness-settings.test.tsx +77 -0
- package/src/app/App.plan-render.test.tsx +2 -0
- package/src/app/App.plan-review.test.tsx +1 -0
- package/src/app/App.readonly.test.tsx +63 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +94 -12
- package/src/app/App.visual.test.tsx +1 -0
- package/src/app/App.walk.test.tsx +1 -0
- package/src/app/App.workbench.test.tsx +1 -0
- package/src/app/app-screens.tsx +3 -2
- package/src/app/app-view-model.ts +11 -1
- package/src/app/components/WorkspacePanels.tsx +1 -0
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/intent-dispatch.ts +26 -0
- package/src/app/ready-signal.ts +2 -0
- package/src/app/run.ts +2 -0
- package/src/app/serve.test.ts +4 -0
- package/src/diff/components/AnnotatableFileView.tsx +1 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +25 -0
- package/src/diff/components/diff-content-view-layout.ts +3 -0
- package/src/diff/diff-fold.ts +4 -0
- package/src/diff/diff-hunk-curate.ts +3 -0
- package/src/diff/diff-intraline.ts +6 -0
- package/src/diff/diff-syntax.ts +8 -0
- package/src/diff/split-diff.ts +6 -0
- package/src/diff/view-diff.ts +18 -0
- package/src/diff/walk.ts +3 -0
- package/src/index.ts +1 -0
- package/src/integrations/attribution.ts +2 -0
- package/src/integrations/client-extension-registry.ts +4 -0
- package/src/integrations/clipboard.ts +1 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
- package/src/integrations/components/ShareDialog.tsx +28 -0
- package/src/integrations/editor.ts +3 -0
- package/src/integrations/github-identity.test.ts +1 -0
- package/src/integrations/github-identity.ts +1 -0
- package/src/integrations/herdr.ts +2 -0
- package/src/integrations/pasted-image.ts +2 -0
- package/src/integrations/share.ts +46 -1
- package/src/keyboard/components/HandleChipsInput.tsx +4 -0
- package/src/keyboard/components/SlashComposer.tsx +3 -0
- package/src/keyboard/key-bindings.ts +3 -0
- package/src/keyboard/keymap.ts +51 -0
- package/src/keyboard/skills.ts +2 -0
- package/src/keyboard/slash-palette.ts +8 -0
- package/src/keyboard/thread-chords.ts +5 -0
- package/src/keyboard/use-shared-keyboard.ts +1 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
- package/src/markdown/components/ThreadView.tsx +13 -0
- package/src/markdown/mark-runs.ts +4 -0
- package/src/markdown/markdown-editor-syntax-style.ts +1 -0
- package/src/markdown/markdown-grid.ts +6 -0
- package/src/markdown/markdown-highlight.ts +1 -0
- package/src/markdown/thread-code-syntax.ts +3 -0
- package/src/markdown/view-plan.ts +52 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/prototype/components/PrototypeContentView.tsx +17 -0
- package/src/prototype/prototype-browser.ts +11 -0
- package/src/settings/components/SettingsDialog.tsx +3 -0
- package/src/settings/config.test.ts +79 -1
- package/src/settings/config.ts +91 -1
- package/src/settings/use-settings-dialog.test.tsx +172 -0
- package/src/settings/use-settings-dialog.tsx +115 -1
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -0
- package/src/stories/stories.test.tsx +4 -0
- package/src/stories/story.ts +6 -0
- package/src/terminal/ansi-screen.ts +5 -0
- package/src/terminal/components/terminal-pane.ts +12 -0
- package/src/terminal/ghostty-terminal.ts +10 -0
- package/src/terminal/kitty-image.ts +3 -0
- package/src/terminal/pty.ts +9 -0
- package/src/testing/test-support.test.ts +2 -0
- package/src/testing/test-support.ts +16 -0
- package/src/thread/agent-continuation.test.ts +2 -0
- package/src/thread/agent-transcript.ts +7 -0
- package/src/thread/components/AgentConfigControls.test.tsx +41 -0
- package/src/thread/components/AgentConfigControls.tsx +8 -1
- package/src/thread/components/AgentThreadPane.test.tsx +91 -3
- package/src/thread/components/AgentThreadPane.tsx +61 -0
- package/src/thread/components/ThreadTree.tsx +1 -0
- package/src/thread/components/thread-tree.test.ts +1 -0
- package/src/thread/thread-controller.curate.test.ts +2 -0
- package/src/thread/thread-controller.share.test.ts +47 -2
- package/src/thread/thread-controller.ts +129 -4
- package/src/thread/tree-view.ts +4 -0
- package/src/thread/use-thread-agent.test.tsx +1 -0
- package/src/thread/use-thread-agent.ts +5 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
- package/src/ui/components/OverlayScrollbar.tsx +5 -0
- package/src/ui/components/PromptDialog.tsx +2 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/ScrollArea.tsx +2 -0
- package/src/ui/components/primitives/Button.tsx +1 -0
- package/src/ui/components/primitives/Tabs.tsx +1 -0
- package/src/ui/components/primitives/Tree.tsx +4 -0
- package/src/ui/components/primitives/tree-model.ts +7 -0
- package/src/ui/components/syntax-highlight.ts +5 -0
- package/src/ui/components/truncate-title.ts +1 -0
- package/src/ui/use-frame-measure.ts +3 -0
- package/src/ui/use-terminal-virtualizer.ts +6 -0
- package/src/workbench/components/ChangesColumn.tsx +7 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
- package/src/workbench/components/EditorGrid.tsx +8 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +2 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +8 -0
- package/src/workbench/components/TreePane.tsx +2 -0
- package/src/workbench/components/editor-grid.ts +3 -0
- package/src/workbench/components/file-tree.ts +2 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
- package/src/workbench/use-remember-layout.ts +2 -0
- package/src/workbench/use-repo-changes.ts +2 -0
|
@@ -125,7 +125,9 @@ function isLandableRow(rows: DiffRow[], index: number): boolean {
|
|
|
125
125
|
const row = rows[index];
|
|
126
126
|
|
|
127
127
|
if (isCodeRow(row)) return true;
|
|
128
|
+
|
|
128
129
|
if (row?.kind !== "file") return false;
|
|
130
|
+
|
|
129
131
|
const next = rows[index + 1];
|
|
130
132
|
|
|
131
133
|
return next === undefined || next.kind === "file";
|
|
@@ -138,6 +140,7 @@ function nextCodeRowIndex(rows: DiffRow[], from: number, to: IntentOfType<"move"
|
|
|
138
140
|
|
|
139
141
|
return from;
|
|
140
142
|
}
|
|
143
|
+
|
|
141
144
|
if (to === "bottom") {
|
|
142
145
|
for (let index = rows.length - 1; index >= 0; index--) {
|
|
143
146
|
if (isLandableRow(rows, index)) return index;
|
|
@@ -145,6 +148,7 @@ function nextCodeRowIndex(rows: DiffRow[], from: number, to: IntentOfType<"move"
|
|
|
145
148
|
|
|
146
149
|
return from;
|
|
147
150
|
}
|
|
151
|
+
|
|
148
152
|
const step = to === "down" ? 1 : -1;
|
|
149
153
|
|
|
150
154
|
for (let index = from + step; index >= 0 && index < rows.length; index += step) {
|
|
@@ -160,6 +164,7 @@ function handleMove(intent: IntentOfType<"move">, deps: IntentDispatchDeps): voi
|
|
|
160
164
|
|
|
161
165
|
return;
|
|
162
166
|
}
|
|
167
|
+
|
|
163
168
|
const navigableCount = deps.display.length;
|
|
164
169
|
|
|
165
170
|
if (intent.to === "down") deps.setCursor((current) => Math.min(navigableCount - 1, current + 1));
|
|
@@ -206,6 +211,7 @@ function handleOpenRename(_intent: IntentOfType<"openRename">, deps: IntentDispa
|
|
|
206
211
|
|
|
207
212
|
return;
|
|
208
213
|
}
|
|
214
|
+
|
|
209
215
|
deps.setMode({
|
|
210
216
|
type: "rename",
|
|
211
217
|
authorId: focused.author,
|
|
@@ -225,6 +231,7 @@ function handleConfirmDialog(
|
|
|
225
231
|
else if (mode.type === "nameSelf") controller.setSelfName(mode.text.trim());
|
|
226
232
|
else if (mode.type === "renameSelf") deps.setLocalIdentityName(mode.text.trim());
|
|
227
233
|
else if (mode.type === "treePrompt") confirmTreePrompt(mode, deps);
|
|
234
|
+
|
|
228
235
|
deps.setMode({ type: "normal" });
|
|
229
236
|
}
|
|
230
237
|
|
|
@@ -232,6 +239,7 @@ function handleStartSpan(_intent: IntentOfType<"startSpan">, deps: IntentDispatc
|
|
|
232
239
|
const block = deps.display[deps.cursor];
|
|
233
240
|
|
|
234
241
|
if (!block?.work) return;
|
|
242
|
+
|
|
235
243
|
const span = startSpan(deps.cursor, displayText(block));
|
|
236
244
|
|
|
237
245
|
if (span) deps.setMode({ type: "span", span });
|
|
@@ -301,6 +309,7 @@ function handlePickSpanAction(
|
|
|
301
309
|
const { mode, session, controller } = deps;
|
|
302
310
|
|
|
303
311
|
if (mode.type !== "spanActions") return;
|
|
312
|
+
|
|
304
313
|
const action = deps.quickActions[intent.index ?? mode.index];
|
|
305
314
|
|
|
306
315
|
if (session && action) {
|
|
@@ -315,6 +324,7 @@ function handlePickSpanAction(
|
|
|
315
324
|
|
|
316
325
|
if (annotationId) deps.setFocusedAnnotationId(annotationId);
|
|
317
326
|
}
|
|
327
|
+
|
|
318
328
|
deps.setMode({ type: "normal" });
|
|
319
329
|
}
|
|
320
330
|
|
|
@@ -322,6 +332,7 @@ function handleOpenCompose(intent: IntentOfType<"openCompose">, deps: IntentDisp
|
|
|
322
332
|
const { mode } = deps;
|
|
323
333
|
|
|
324
334
|
deps.liveInput.current = "";
|
|
335
|
+
|
|
325
336
|
if (intent.from === "span" && mode.type === "span") {
|
|
326
337
|
deps.setMode({
|
|
327
338
|
type: "compose",
|
|
@@ -362,6 +373,7 @@ function handleOpenSubmit(_intent: IntentOfType<"openSubmit">, deps: IntentDispa
|
|
|
362
373
|
const { session } = deps;
|
|
363
374
|
|
|
364
375
|
if (!session) return;
|
|
376
|
+
|
|
365
377
|
deps.liveInput.current = "";
|
|
366
378
|
deps.setMode({ type: "submit", message: deps.defaultMessage, summary: "" });
|
|
367
379
|
}
|
|
@@ -387,6 +399,7 @@ function handleFoldFile(_intent: IntentOfType<"foldFile">, deps: IntentDispatchD
|
|
|
387
399
|
const file = deps.rows[deps.cursor]?.file;
|
|
388
400
|
|
|
389
401
|
if (!file || deps.controller.isFileCollapsed(file)) return;
|
|
402
|
+
|
|
390
403
|
deps.controller.setFileCollapsed(file, true);
|
|
391
404
|
// the file band is unchanged in index by its own collapse, so land the cursor on it
|
|
392
405
|
const headerIndex = deps.controller
|
|
@@ -400,6 +413,7 @@ function handleUnfoldFile(_intent: IntentOfType<"unfoldFile">, deps: IntentDispa
|
|
|
400
413
|
const file = deps.rows[deps.cursor]?.file;
|
|
401
414
|
|
|
402
415
|
if (!file || !deps.controller.isFileCollapsed(file)) return;
|
|
416
|
+
|
|
403
417
|
deps.controller.setFileCollapsed(file, false);
|
|
404
418
|
// drop the cursor onto the file's first code line now that its body is back
|
|
405
419
|
const firstCode = deps.controller.rows().findIndex((row) => row.file === file && isCodeRow(row));
|
|
@@ -423,6 +437,7 @@ function handleRestoreCuration(
|
|
|
423
437
|
const items = deps.controller.curationItems();
|
|
424
438
|
|
|
425
439
|
if (!items.length) return;
|
|
440
|
+
|
|
426
441
|
const targetId = deps.selectedCurationId ?? items[items.length - 1]!.id;
|
|
427
442
|
|
|
428
443
|
deps.controller.restoreCuration(targetId);
|
|
@@ -447,6 +462,7 @@ function handleAnnotationCycle(
|
|
|
447
462
|
);
|
|
448
463
|
|
|
449
464
|
if (!annotations.length) return;
|
|
465
|
+
|
|
450
466
|
const focusedIndex = annotations.findIndex(
|
|
451
467
|
(annotation) => annotation.id === deps.focusedAnnotationId,
|
|
452
468
|
);
|
|
@@ -500,11 +516,14 @@ function handleSaveCompose(_intent: IntentOfType<"saveCompose">, deps: IntentDis
|
|
|
500
516
|
|
|
501
517
|
if (mode.type === "railEdit") {
|
|
502
518
|
if (session && body) controller.updateAnnotation(mode.id, body);
|
|
519
|
+
|
|
503
520
|
deps.setMode({ type: "normal" });
|
|
504
521
|
|
|
505
522
|
return;
|
|
506
523
|
}
|
|
524
|
+
|
|
507
525
|
if (mode.type !== "compose") return;
|
|
526
|
+
|
|
508
527
|
if (session && body) {
|
|
509
528
|
const annotationId = controller.annotate(
|
|
510
529
|
mode.kind,
|
|
@@ -516,6 +535,7 @@ function handleSaveCompose(_intent: IntentOfType<"saveCompose">, deps: IntentDis
|
|
|
516
535
|
|
|
517
536
|
if (annotationId) deps.setFocusedAnnotationId(annotationId);
|
|
518
537
|
}
|
|
538
|
+
|
|
519
539
|
deps.setMode({ type: "normal" });
|
|
520
540
|
}
|
|
521
541
|
|
|
@@ -526,6 +546,7 @@ function handleSubmitMessage(
|
|
|
526
546
|
const { mode } = deps;
|
|
527
547
|
|
|
528
548
|
if (mode.type === "submit") deps.controller.submit(mode.message, deps.liveInput.current);
|
|
549
|
+
|
|
529
550
|
deps.setMode({ type: "normal" });
|
|
530
551
|
}
|
|
531
552
|
|
|
@@ -533,6 +554,7 @@ function handleCycleMessage(intent: IntentOfType<"cycleMessage">, deps: IntentDi
|
|
|
533
554
|
const { mode } = deps;
|
|
534
555
|
|
|
535
556
|
if (mode.type !== "submit") return;
|
|
557
|
+
|
|
536
558
|
const messageIndex =
|
|
537
559
|
(MESSAGE_OUTCOMES.indexOf(mode.message) + intent.direction + MESSAGE_OUTCOMES.length) %
|
|
538
560
|
MESSAGE_OUTCOMES.length;
|
|
@@ -582,6 +604,7 @@ function handleTreeMove(intent: IntentOfType<"treeMove">, deps: IntentDispatchDe
|
|
|
582
604
|
const rows = deps.controller.treeRows();
|
|
583
605
|
|
|
584
606
|
if (rows.length === 0) return;
|
|
607
|
+
|
|
585
608
|
const current = selectedTreeIndex(deps, rows);
|
|
586
609
|
const next = Math.min(rows.length - 1, Math.max(0, current + intent.direction));
|
|
587
610
|
|
|
@@ -593,9 +616,12 @@ function handleTreeGo(_intent: IntentOfType<"treeGo">, deps: IntentDispatchDeps)
|
|
|
593
616
|
const row = rows[selectedTreeIndex(deps, rows)];
|
|
594
617
|
|
|
595
618
|
if (!row) return deps.controller.setStatus("select an entry in the tree first");
|
|
619
|
+
|
|
596
620
|
if (row.isCurrentTip) return deps.controller.setStatus("already at the tip");
|
|
621
|
+
|
|
597
622
|
// a switch needs no summary; a move back may leave one, so it asks
|
|
598
623
|
if (row.tips.length > 0) return deps.controller.goToEntry(row.entryId);
|
|
624
|
+
|
|
599
625
|
deps.setMode({ type: "treePrompt", ask: "navigate", entryId: row.entryId, text: "" });
|
|
600
626
|
}
|
|
601
627
|
|
package/src/app/ready-signal.ts
CHANGED
|
@@ -23,6 +23,7 @@ function announceReady(): void {
|
|
|
23
23
|
const path = process.env[READY_FILE_ENV];
|
|
24
24
|
|
|
25
25
|
if (!path) return;
|
|
26
|
+
|
|
26
27
|
writeFileSync(path, "ready\n");
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -37,6 +38,7 @@ export function useReadySignal(ready: boolean, onReady?: () => void): void {
|
|
|
37
38
|
|
|
38
39
|
useEffect(() => {
|
|
39
40
|
if (!ready || fired.current) return;
|
|
41
|
+
|
|
40
42
|
const emit = (): void => {
|
|
41
43
|
fired.current = true;
|
|
42
44
|
announceReady();
|
package/src/app/run.ts
CHANGED
|
@@ -57,6 +57,7 @@ export async function runClient(options: RunClientOptions): Promise<number> {
|
|
|
57
57
|
let exited = false;
|
|
58
58
|
const shutdown = async (code: number): Promise<void> => {
|
|
59
59
|
if (exited) return;
|
|
60
|
+
|
|
60
61
|
exited = true;
|
|
61
62
|
renderer.destroy();
|
|
62
63
|
// close the warm prototype Chromium before exit so it never orphans; the
|
|
@@ -101,6 +102,7 @@ export async function runClient(options: RunClientOptions): Promise<number> {
|
|
|
101
102
|
.waitForThemeMode(THEME_QUERY_TIMEOUT_MS)
|
|
102
103
|
.then((mode) => {
|
|
103
104
|
perfMark("themeQuery");
|
|
105
|
+
|
|
104
106
|
// the query can answer after a fast quit; never render onto a destroyed renderer
|
|
105
107
|
if (!exited && mode && mode !== "dark") renderApp(mode);
|
|
106
108
|
})
|
package/src/app/serve.test.ts
CHANGED
|
@@ -49,9 +49,11 @@ function sshCapture(
|
|
|
49
49
|
let done = false;
|
|
50
50
|
const finish = (err?: Error) => {
|
|
51
51
|
if (done) return;
|
|
52
|
+
|
|
52
53
|
done = true;
|
|
53
54
|
clearTimeout(timer);
|
|
54
55
|
conn.end();
|
|
56
|
+
|
|
55
57
|
if (err) reject(err);
|
|
56
58
|
else resolve(out);
|
|
57
59
|
};
|
|
@@ -61,8 +63,10 @@ function sshCapture(
|
|
|
61
63
|
.on("ready", () => {
|
|
62
64
|
conn.shell({ term: "xterm-256color", cols: 100, rows: 30 }, (err, stream) => {
|
|
63
65
|
if (err) return finish(err);
|
|
66
|
+
|
|
64
67
|
stream.on("data", (chunk: Buffer) => {
|
|
65
68
|
out += chunk.toString("utf8");
|
|
69
|
+
|
|
66
70
|
if (until(out)) finish();
|
|
67
71
|
});
|
|
68
72
|
stream.on("close", () => finish(new Error(`stream closed early; captured:\n${out}`)));
|
|
@@ -17,6 +17,7 @@ const noop = (): void => {};
|
|
|
17
17
|
/** Find a renderable by id anywhere in the tree (getRenderable is not recursive). */
|
|
18
18
|
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
19
19
|
if (node.id === id) return node;
|
|
20
|
+
|
|
20
21
|
for (const child of node.getChildren()) {
|
|
21
22
|
const found = findById(child, id);
|
|
22
23
|
|
|
@@ -81,6 +82,7 @@ test("walking the caret down past wrapped discussion cards keeps it on screen an
|
|
|
81
82
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
82
83
|
|
|
83
84
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
85
|
+
|
|
84
86
|
const scrollbox = found;
|
|
85
87
|
const caretCell = annotationPaletteFor(DARK).caretCell;
|
|
86
88
|
|
|
@@ -156,6 +158,7 @@ test("wheel input accelerates a continuous gesture and settles back to precise s
|
|
|
156
158
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
157
159
|
|
|
158
160
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
161
|
+
|
|
159
162
|
const scrollbox = found;
|
|
160
163
|
const acceleration = scrollbox.scrollAcceleration;
|
|
161
164
|
const first = acceleration.tick(1000);
|
|
@@ -196,6 +199,7 @@ test("wheel scrolling up keeps the caret on the bottom visible code row", async
|
|
|
196
199
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
197
200
|
|
|
198
201
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
202
|
+
|
|
199
203
|
const scrollbox = found;
|
|
200
204
|
|
|
201
205
|
for (const key of Array.from({ length: 30 }, () => "down" as const)) {
|
|
@@ -270,6 +274,7 @@ test("rapid wheel scrolling through a large wrapped diff does not loop React upd
|
|
|
270
274
|
for (const [index, direction] of Array.from({ length: 240 }, () => "down" as const).entries()) {
|
|
271
275
|
// eslint-disable-next-line no-await-in-loop
|
|
272
276
|
await setup.mockMouse.scroll(20, 5, direction);
|
|
277
|
+
|
|
273
278
|
if (index % 12 === 0) {
|
|
274
279
|
// eslint-disable-next-line no-await-in-loop
|
|
275
280
|
await settle(setup);
|
|
@@ -315,6 +320,7 @@ test("walking long wrapped code lines advances the viewport one visual row per k
|
|
|
315
320
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
316
321
|
|
|
317
322
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
323
|
+
|
|
318
324
|
const scrollbox = found;
|
|
319
325
|
const steps: number[] = [];
|
|
320
326
|
const caretCell = annotationPaletteFor(DARK).caretCell;
|
|
@@ -410,6 +416,7 @@ test("Up keeps the caret on the first screen row while wrapped lines scroll", as
|
|
|
410
416
|
const found = findById(setup.renderer.root, "diff-scroll");
|
|
411
417
|
|
|
412
418
|
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
419
|
+
|
|
413
420
|
const scrollbox = found;
|
|
414
421
|
|
|
415
422
|
for (let index = 0; index < 40; index++) await press(setup, "down");
|
|
@@ -423,10 +430,12 @@ test("Up keeps the caret on the first screen row while wrapped lines scroll", as
|
|
|
423
430
|
const top = scrollbox.scrollTop;
|
|
424
431
|
|
|
425
432
|
expect(previousTop - top).toBeLessThanOrEqual(1);
|
|
433
|
+
|
|
426
434
|
if (top < previousTop) {
|
|
427
435
|
expect(marker).toBe(0);
|
|
428
436
|
scrolled = true;
|
|
429
437
|
}
|
|
438
|
+
|
|
430
439
|
previousTop = top;
|
|
431
440
|
}
|
|
432
441
|
expect(scrolled).toBe(true);
|
|
@@ -72,6 +72,7 @@ test("renders tree-sitter colors, with the changed word in the diff color on top
|
|
|
72
72
|
for (const line of setup.captureSpans().lines) {
|
|
73
73
|
for (const span of line.spans) {
|
|
74
74
|
if (span.text.includes("250")) changedColors.add(hex(span.fg));
|
|
75
|
+
|
|
75
76
|
if (span.text.includes("100")) changedColors.add(hex(span.fg));
|
|
76
77
|
}
|
|
77
78
|
}
|
|
@@ -56,6 +56,7 @@ function lineNumberWidth(rows: DiffRow[]): number {
|
|
|
56
56
|
|
|
57
57
|
for (const row of rows) {
|
|
58
58
|
if (row.oldLine !== undefined) width = Math.max(width, String(row.oldLine).length);
|
|
59
|
+
|
|
59
60
|
if (row.newLine !== undefined) width = Math.max(width, String(row.newLine).length);
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -339,10 +340,12 @@ function splitLayout(splitRows: SplitRow[], rows: DiffRow[], textWidth: number):
|
|
|
339
340
|
|
|
340
341
|
return { index, height: row ? headerHeight(row) : 1 };
|
|
341
342
|
}
|
|
343
|
+
|
|
342
344
|
const sideHeight = (line: SplitLine | undefined): number =>
|
|
343
345
|
line ? codeRowHeight(line.row, textWidth) : 0;
|
|
344
346
|
|
|
345
347
|
if (pair.left) itemOfRow[pair.left.rowIndex] = index;
|
|
348
|
+
|
|
346
349
|
if (pair.right) itemOfRow[pair.right.rowIndex] = index;
|
|
347
350
|
|
|
348
351
|
return { index, height: Math.max(1, sideHeight(pair.left), sideHeight(pair.right)) };
|
|
@@ -401,6 +404,7 @@ function codeLineSpans(
|
|
|
401
404
|
) {
|
|
402
405
|
previous.text += character;
|
|
403
406
|
} else spans.push({ text: character, fg, bg, attributes });
|
|
407
|
+
|
|
404
408
|
column++;
|
|
405
409
|
}
|
|
406
410
|
}
|
|
@@ -465,12 +469,14 @@ function emphasisBackgroundColumns(
|
|
|
465
469
|
const columns: Array<string | undefined> = Array.from({ length: text.length }, () => undefined);
|
|
466
470
|
|
|
467
471
|
if (!intraline || !emphasisBackground) return columns;
|
|
472
|
+
|
|
468
473
|
let offset = 0;
|
|
469
474
|
|
|
470
475
|
for (const run of intraline) {
|
|
471
476
|
if (run.changed)
|
|
472
477
|
for (let index = 0; index < run.text.length && offset + index < text.length; index++)
|
|
473
478
|
columns[offset + index] = emphasisBackground;
|
|
479
|
+
|
|
474
480
|
offset += run.text.length;
|
|
475
481
|
}
|
|
476
482
|
|
|
@@ -655,6 +661,7 @@ export function DiffContentView({
|
|
|
655
661
|
const scrollbox = scrollRef.current;
|
|
656
662
|
|
|
657
663
|
if (!scrollbox) return false;
|
|
664
|
+
|
|
658
665
|
let distance: number;
|
|
659
666
|
|
|
660
667
|
if (targetY !== undefined) {
|
|
@@ -664,10 +671,12 @@ export function DiffContentView({
|
|
|
664
671
|
distance = direction === 1 ? targetY - bottom : top - targetY;
|
|
665
672
|
} else {
|
|
666
673
|
if (from === to) return false;
|
|
674
|
+
|
|
667
675
|
const item = layout.itemOfRow[to];
|
|
668
676
|
const target = item === undefined ? undefined : virtual.startOfIndex(item);
|
|
669
677
|
|
|
670
678
|
if (target === undefined) return false;
|
|
679
|
+
|
|
671
680
|
const top = scrollbox.scrollTop;
|
|
672
681
|
const bottom = top + scrollbox.viewport.height - 1;
|
|
673
682
|
|
|
@@ -675,8 +684,10 @@ export function DiffContentView({
|
|
|
675
684
|
}
|
|
676
685
|
|
|
677
686
|
if (distance <= 0) return false;
|
|
687
|
+
|
|
678
688
|
// Let the caret commit before scrolling the next visual row into view.
|
|
679
689
|
if (distance === 1) return false;
|
|
690
|
+
|
|
680
691
|
scrollbox.scrollBy(direction);
|
|
681
692
|
|
|
682
693
|
return distance > 1;
|
|
@@ -689,15 +700,18 @@ export function DiffContentView({
|
|
|
689
700
|
|
|
690
701
|
useEffect(() => {
|
|
691
702
|
if (revealItem === undefined || (!surface.compose && !surface.focusedDiscussion)) return;
|
|
703
|
+
|
|
692
704
|
virtual.scrollToIndex(revealItem, surface.compose ? "end" : "auto");
|
|
693
705
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
694
706
|
}, [surface.revealBlockIndex, surface.compose, surface.focusedDiscussion]);
|
|
695
707
|
|
|
696
708
|
useLayoutEffect(() => {
|
|
697
709
|
if (revealItem === undefined || surface.compose || surface.focusedDiscussion) return;
|
|
710
|
+
|
|
698
711
|
const scrollbox = scrollRef.current;
|
|
699
712
|
|
|
700
713
|
if (!scrollbox) return;
|
|
714
|
+
|
|
701
715
|
const headY = surface.headVisualY();
|
|
702
716
|
|
|
703
717
|
if (headY !== undefined) {
|
|
@@ -709,9 +723,11 @@ export function DiffContentView({
|
|
|
709
723
|
|
|
710
724
|
return;
|
|
711
725
|
}
|
|
726
|
+
|
|
712
727
|
const start = virtual.startOfIndex(revealItem);
|
|
713
728
|
|
|
714
729
|
if (start === undefined) return;
|
|
730
|
+
|
|
715
731
|
const top = scrollbox.scrollTop;
|
|
716
732
|
const bottom = top + scrollbox.viewport.height - 1;
|
|
717
733
|
|
|
@@ -779,6 +795,7 @@ export function DiffContentView({
|
|
|
779
795
|
</>
|
|
780
796
|
);
|
|
781
797
|
}
|
|
798
|
+
|
|
782
799
|
if (split) {
|
|
783
800
|
return (
|
|
784
801
|
<>
|
|
@@ -862,6 +879,7 @@ export function DiffContentView({
|
|
|
862
879
|
</text>
|
|
863
880
|
</box>,
|
|
864
881
|
);
|
|
882
|
+
|
|
865
883
|
// cards render on the side that owns the row, so a shared context row's card is not duplicated
|
|
866
884
|
if (paintMarks) cards.push(...surface.cardsAfterLine(rowIndex, line, isLastLine));
|
|
867
885
|
}
|
|
@@ -895,6 +913,7 @@ export function DiffContentView({
|
|
|
895
913
|
const row = rows[rowIndex]!;
|
|
896
914
|
|
|
897
915
|
if (!isCodeRow(row)) return headerNode(row, rowIndex);
|
|
916
|
+
|
|
898
917
|
const { lines, cards } = codeRowLines(row, rowIndex, textWidth, `row-${rowIndex}`);
|
|
899
918
|
|
|
900
919
|
return (
|
|
@@ -919,6 +938,7 @@ export function DiffContentView({
|
|
|
919
938
|
|
|
920
939
|
return { node: filler };
|
|
921
940
|
}
|
|
941
|
+
|
|
922
942
|
// the change side (right: additions and context) owns the caret and marks; the left carries them
|
|
923
943
|
// only on its deletions, so a context row shared by both columns is not marked twice
|
|
924
944
|
const paintMarks = side === "right" || line.kind === "del";
|
|
@@ -951,6 +971,7 @@ export function DiffContentView({
|
|
|
951
971
|
rowIndex,
|
|
952
972
|
);
|
|
953
973
|
}
|
|
974
|
+
|
|
954
975
|
const left = splitSide(pair.left, "left", textWidth, `pair-${pairIndex}-left`);
|
|
955
976
|
const right = splitSide(pair.right, "right", textWidth, `pair-${pairIndex}-right`);
|
|
956
977
|
// both sides answer to the pair's id, so a reveal of either base row lands here
|
|
@@ -993,6 +1014,7 @@ export function DiffContentView({
|
|
|
993
1014
|
if (firstItem.start > 0) {
|
|
994
1015
|
materialized.push(<box key="spacer-above" style={{ height: firstItem.start }} />);
|
|
995
1016
|
}
|
|
1017
|
+
|
|
996
1018
|
for (const item of virtual.items) {
|
|
997
1019
|
// the wrapper is what gets measured, so a row's cards count toward its height
|
|
998
1020
|
materialized.push(
|
|
@@ -1001,6 +1023,7 @@ export function DiffContentView({
|
|
|
1001
1023
|
</box>,
|
|
1002
1024
|
);
|
|
1003
1025
|
}
|
|
1026
|
+
|
|
1004
1027
|
if (below > 0) materialized.push(<box key="spacer-below" style={{ height: below }} />);
|
|
1005
1028
|
}
|
|
1006
1029
|
|
|
@@ -1020,7 +1043,9 @@ export function DiffContentView({
|
|
|
1020
1043
|
const scrollbox = scrollRef.current;
|
|
1021
1044
|
|
|
1022
1045
|
if (!scrollbox || !event.scroll || event.modifiers.shift) return;
|
|
1046
|
+
|
|
1023
1047
|
if (event.scroll.direction !== "up" && event.scroll.direction !== "down") return;
|
|
1048
|
+
|
|
1024
1049
|
const before = scrollbox.scrollTop;
|
|
1025
1050
|
const followCaret = surface.prepareViewportScroll();
|
|
1026
1051
|
|
|
@@ -52,11 +52,13 @@ function changedColumns(length: number, intralineRuns: IntralineRun[] | undefine
|
|
|
52
52
|
const changed = Array.from({ length }, () => false);
|
|
53
53
|
|
|
54
54
|
if (!intralineRuns) return changed;
|
|
55
|
+
|
|
55
56
|
let offset = 0;
|
|
56
57
|
|
|
57
58
|
for (const run of intralineRuns) {
|
|
58
59
|
if (run.changed)
|
|
59
60
|
for (let index = 0; index < run.text.length; index++) changed[offset + index] = true;
|
|
61
|
+
|
|
60
62
|
offset += run.text.length;
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -75,6 +77,7 @@ function syntaxColorColumns(
|
|
|
75
77
|
const color = colorForSyntaxGroup(span.group, tokens);
|
|
76
78
|
|
|
77
79
|
if (!color) continue;
|
|
80
|
+
|
|
78
81
|
for (let column = span.start; column < span.end && column < length; column++) {
|
|
79
82
|
colors[column] = color;
|
|
80
83
|
}
|
package/src/diff/diff-fold.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function weaveFullFileRows(
|
|
|
55
55
|
fillTo(row.newLine);
|
|
56
56
|
woven.push(row);
|
|
57
57
|
newCursor = row.newLine + 1;
|
|
58
|
+
|
|
58
59
|
if (row.kind === "ctx") oldCursor = (row.oldLine ?? oldCursor) + 1;
|
|
59
60
|
} else {
|
|
60
61
|
// a deletion consumes an old line only; it keeps its place before the additions
|
|
@@ -90,6 +91,7 @@ export function applyFold(
|
|
|
90
91
|
files: readonly FileContents[] | undefined,
|
|
91
92
|
): DiffRow[] {
|
|
92
93
|
if (collapsed.size === 0 && expanded.size === 0) return base;
|
|
94
|
+
|
|
93
95
|
const contentsByPath = new Map((files ?? []).map((file) => [file.path, file]));
|
|
94
96
|
const out: DiffRow[] = [];
|
|
95
97
|
let index = 0;
|
|
@@ -102,6 +104,7 @@ export function applyFold(
|
|
|
102
104
|
index += 1;
|
|
103
105
|
continue;
|
|
104
106
|
}
|
|
107
|
+
|
|
105
108
|
out.push(row);
|
|
106
109
|
const file = row.file;
|
|
107
110
|
let end = index + 1;
|
|
@@ -119,6 +122,7 @@ export function applyFold(
|
|
|
119
122
|
} else {
|
|
120
123
|
out.push(...body);
|
|
121
124
|
}
|
|
125
|
+
|
|
122
126
|
index = end;
|
|
123
127
|
}
|
|
124
128
|
|
|
@@ -65,6 +65,7 @@ function changeSideLine(
|
|
|
65
65
|
): { side: DiffSide; lineNumber: number } | null {
|
|
66
66
|
if (row.kind === "add" && row.newLine !== undefined)
|
|
67
67
|
return { side: "addition", lineNumber: row.newLine };
|
|
68
|
+
|
|
68
69
|
if (row.kind === "del" && row.oldLine !== undefined)
|
|
69
70
|
return { side: "deletion", lineNumber: row.oldLine };
|
|
70
71
|
|
|
@@ -104,6 +105,7 @@ export function changeRejectionForRow(
|
|
|
104
105
|
const change = changeSideLine(row);
|
|
105
106
|
|
|
106
107
|
if (!change) return null;
|
|
108
|
+
|
|
107
109
|
const located = locateLine(model, change.side, change.lineNumber);
|
|
108
110
|
|
|
109
111
|
if (!located || located.changeIndex === undefined) return null;
|
|
@@ -139,6 +141,7 @@ export function isRowRejected(
|
|
|
139
141
|
const change = changeSideLine(row);
|
|
140
142
|
|
|
141
143
|
if (!change) return false;
|
|
144
|
+
|
|
142
145
|
const located = locateLine(model, change.side, change.lineNumber);
|
|
143
146
|
|
|
144
147
|
if (!located || located.changeIndex === undefined) return false;
|
|
@@ -53,6 +53,7 @@ function intralineBlocks(rows: DiffRow[]): IntralineBlock[] {
|
|
|
53
53
|
index++;
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
|
+
|
|
56
57
|
const deletionStart = index;
|
|
57
58
|
|
|
58
59
|
while (index < rows.length && rows[index]!.kind === "del") index++;
|
|
@@ -127,6 +128,7 @@ export function createIntralineResolver(rows: DiffRow[]): IntralineResolver {
|
|
|
127
128
|
const blockIndex = blockContaining(blocks, rowIndex);
|
|
128
129
|
|
|
129
130
|
if (blockIndex === undefined) return undefined;
|
|
131
|
+
|
|
130
132
|
if (!computed.has(blockIndex)) {
|
|
131
133
|
computed.add(blockIndex);
|
|
132
134
|
for (const [row, runs] of blockRunEntries(rows, blocks[blockIndex]!))
|
|
@@ -161,6 +163,7 @@ function lineWordSetCache(): (text: string) => ReadonlySet<string> {
|
|
|
161
163
|
const cached = cache.get(text);
|
|
162
164
|
|
|
163
165
|
if (cached !== undefined) return cached;
|
|
166
|
+
|
|
164
167
|
const words = new Set(text.toLowerCase().split(/\s+/).filter(Boolean));
|
|
165
168
|
|
|
166
169
|
cache.set(text, words);
|
|
@@ -180,6 +183,7 @@ function linesSimilar(
|
|
|
180
183
|
const newWords = wordsOf(newText);
|
|
181
184
|
|
|
182
185
|
if (oldWords.size === 0 && newWords.size === 0) return true;
|
|
186
|
+
|
|
183
187
|
let intersection = 0;
|
|
184
188
|
|
|
185
189
|
for (const word of oldWords) if (newWords.has(word)) intersection++;
|
|
@@ -198,6 +202,7 @@ function linesSimilar(
|
|
|
198
202
|
*/
|
|
199
203
|
function alignedPairs(deletionTexts: string[], additionTexts: string[]): Array<[number, number]> {
|
|
200
204
|
if (deletionTexts.length === 1 && additionTexts.length === 1) return [[0, 0]];
|
|
205
|
+
|
|
201
206
|
const pairs: Array<[number, number]> = [];
|
|
202
207
|
let deletionOffset = 0;
|
|
203
208
|
let additionOffset = 0;
|
|
@@ -227,6 +232,7 @@ function sideRuns(changes: WordChange[], side: "added" | "removed"): IntralineRu
|
|
|
227
232
|
|
|
228
233
|
for (const change of changes) {
|
|
229
234
|
if (change.kind !== "common" && change.kind !== side) continue;
|
|
235
|
+
|
|
230
236
|
const changed = change.kind === side;
|
|
231
237
|
const last = runs[runs.length - 1];
|
|
232
238
|
|
package/src/diff/diff-syntax.ts
CHANGED
|
@@ -47,9 +47,11 @@ export function highlightJobs(rows: DiffRow[]): HighlightJob[] {
|
|
|
47
47
|
if (filetype) {
|
|
48
48
|
if (oldLines.length && (hasChange || newLines.length === 0))
|
|
49
49
|
jobs.push({ filetype, source: oldLines.join("\n"), rowIndexByLine: oldRowIndexByLine });
|
|
50
|
+
|
|
50
51
|
if (newLines.length)
|
|
51
52
|
jobs.push({ filetype, source: newLines.join("\n"), rowIndexByLine: newRowIndexByLine });
|
|
52
53
|
}
|
|
54
|
+
|
|
53
55
|
oldLines = [];
|
|
54
56
|
newLines = [];
|
|
55
57
|
oldRowIndexByLine = [];
|
|
@@ -70,16 +72,19 @@ export function highlightJobs(rows: DiffRow[]): HighlightJob[] {
|
|
|
70
72
|
flush();
|
|
71
73
|
continue;
|
|
72
74
|
}
|
|
75
|
+
|
|
73
76
|
const text = stripTrailingNewline(row.text);
|
|
74
77
|
|
|
75
78
|
if (row.kind === "ctx" || row.kind === "del") {
|
|
76
79
|
oldLines.push(text);
|
|
77
80
|
oldRowIndexByLine.push(rowIndex);
|
|
78
81
|
}
|
|
82
|
+
|
|
79
83
|
if (row.kind === "ctx" || row.kind === "add") {
|
|
80
84
|
newLines.push(text);
|
|
81
85
|
newRowIndexByLine.push(rowIndex);
|
|
82
86
|
}
|
|
87
|
+
|
|
83
88
|
if (row.kind === "add" || row.kind === "del") hasChange = true;
|
|
84
89
|
}
|
|
85
90
|
flush();
|
|
@@ -145,6 +150,7 @@ export function spansByLine(source: string, highlights: SimpleHighlight[]): Synt
|
|
|
145
150
|
const group = chars[column];
|
|
146
151
|
|
|
147
152
|
if (group === undefined) continue;
|
|
153
|
+
|
|
148
154
|
const previous = spans[spans.length - 1];
|
|
149
155
|
|
|
150
156
|
if (previous && previous.group === group && previous.end === column)
|
|
@@ -166,6 +172,7 @@ export async function highlightDiffRows(rows: DiffRow[]): Promise<Map<number, Sy
|
|
|
166
172
|
const jobs = highlightJobs(rows);
|
|
167
173
|
|
|
168
174
|
if (!jobs.length) return spansByRow;
|
|
175
|
+
|
|
169
176
|
const client = getTreeSitterClient();
|
|
170
177
|
|
|
171
178
|
await client.initialize();
|
|
@@ -173,6 +180,7 @@ export async function highlightDiffRows(rows: DiffRow[]): Promise<Map<number, Sy
|
|
|
173
180
|
const result = await client.highlightOnce(job.source, job.filetype);
|
|
174
181
|
|
|
175
182
|
if (!result.highlights) continue;
|
|
183
|
+
|
|
176
184
|
const lineSpans = spansByLine(job.source, result.highlights);
|
|
177
185
|
|
|
178
186
|
job.rowIndexByLine.forEach((rowIndex, line) => {
|