@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.90
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +42 -34
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +259 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +330 -161
- package/src/App.prototype-markdown.test.tsx +128 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +264 -0
- package/src/App.test.tsx +402 -126
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1402 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +774 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +577 -0
- package/src/app-view-model.ts +168 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/client-extension-registry.test.tsx +61 -0
- package/src/client-extension-registry.ts +159 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.extensions.test.tsx +191 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +362 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +150 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +149 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +906 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/ExtensionZone.stories.tsx +28 -0
- package/src/components/ExtensionZone.tsx +145 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +75 -0
- package/src/components/ProjectTreeView.tsx +125 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +951 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +525 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +49 -0
- package/src/components/WelcomePlayground.tsx +139 -0
- package/src/components/WorkspacePanels.stories.tsx +36 -0
- package/src/components/WorkspacePanels.tsx +116 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2264 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +59 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +579 -34
- package/src/config.ts +533 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +28 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +724 -0
- package/src/intent-dispatch.ts +690 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +456 -115
- package/src/keymap.ts +315 -81
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +67 -0
- package/src/launch-layout.ts +47 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +81 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +85 -12
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +219 -0
- package/src/theme.ts +88 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-code-syntax.ts +45 -0
- package/src/thread-controller.curate.test.ts +476 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +2050 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +153 -0
- package/src/use-annotation-surface.tsx +975 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-repo-changes.ts +87 -0
- package/src/use-settings-dialog.tsx +364 -0
- package/src/use-terminal-virtualizer.ts +259 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +279 -9
- package/src/view-diff.ts +460 -24
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +545 -0
- package/src/view-plan.ts +1019 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +99 -0
- package/src/workbench-snapshot.ts +59 -0
- package/src/session-controller.ts +0 -398
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -364
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
blockRuns,
|
|
4
|
+
buildDisplay,
|
|
5
|
+
displayText,
|
|
6
|
+
marksByDisplay,
|
|
7
|
+
nextWorkBlock,
|
|
8
|
+
overlayMarks,
|
|
9
|
+
renderedOffsetAtOrAfter,
|
|
10
|
+
renderedOffsetAtOrBefore,
|
|
11
|
+
renderedOffsetFor,
|
|
12
|
+
renderedSpanToWork,
|
|
13
|
+
renderedStyleRuns,
|
|
14
|
+
renderedText,
|
|
15
|
+
safeLinkHref,
|
|
16
|
+
spanFromRange,
|
|
17
|
+
spanKey,
|
|
18
|
+
startSpan,
|
|
19
|
+
styledRunsFor,
|
|
20
|
+
workRangeForRendered,
|
|
21
|
+
} from "./view-plan";
|
|
22
|
+
import {
|
|
23
|
+
applyTextCuts,
|
|
24
|
+
cutBlock,
|
|
25
|
+
mergeTextCut,
|
|
26
|
+
parseBlocks,
|
|
27
|
+
restoreBlock,
|
|
28
|
+
restoreLine,
|
|
29
|
+
type Annotation,
|
|
30
|
+
} from "@cueloop/schema";
|
|
31
|
+
|
|
32
|
+
const BASE = `# Plan
|
|
33
|
+
|
|
34
|
+
## Context
|
|
35
|
+
|
|
36
|
+
The daemon persists sessions to disk atomically.
|
|
37
|
+
|
|
38
|
+
- first item
|
|
39
|
+
- second item
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
describe("buildDisplay reconciliation", () => {
|
|
43
|
+
test("no working copy: everything is same", () => {
|
|
44
|
+
// Act
|
|
45
|
+
const display = buildDisplay(BASE);
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
expect(display.every((block) => block.type === "same")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("reworded block pairs as mod (similar)", () => {
|
|
52
|
+
// Arrange
|
|
53
|
+
const working = BASE.replace("persists sessions to disk", "writes sessions to disk");
|
|
54
|
+
|
|
55
|
+
// Act
|
|
56
|
+
const display = buildDisplay(BASE, working);
|
|
57
|
+
|
|
58
|
+
// Assert
|
|
59
|
+
const mod = display.find((block) => block.type === "mod")!;
|
|
60
|
+
|
|
61
|
+
expect(mod.base!.text).toContain("persists");
|
|
62
|
+
expect(mod.work!.text).toContain("writes");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("a partial Cut stays one modified block even when only a short prefix survives", () => {
|
|
66
|
+
const base = "A seeded plan so cueloop dev always has a thread to open.";
|
|
67
|
+
const display = buildDisplay(base, "A seed");
|
|
68
|
+
|
|
69
|
+
expect(display).toHaveLength(1);
|
|
70
|
+
expect(display[0]!.type).toBe("mod");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("unrelated cut + insert do NOT merge into a mod", () => {
|
|
74
|
+
// Arrange
|
|
75
|
+
const working = BASE.replace("- second item\n", "") + "\nA totally new closing note.\n";
|
|
76
|
+
|
|
77
|
+
// Act
|
|
78
|
+
const display = buildDisplay(BASE, working);
|
|
79
|
+
|
|
80
|
+
// Assert
|
|
81
|
+
expect(
|
|
82
|
+
display.some((block) => block.type === "del" && block.base!.text === "second item"),
|
|
83
|
+
).toBe(true);
|
|
84
|
+
expect(
|
|
85
|
+
display.some((block) => block.type === "add" && block.work!.text.includes("closing note")),
|
|
86
|
+
).toBe(true);
|
|
87
|
+
expect(display.some((block) => block.type === "mod")).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("ordered list items get running numbers", () => {
|
|
91
|
+
// Act
|
|
92
|
+
const display = buildDisplay("1. one\n2. two\n3. three\n");
|
|
93
|
+
|
|
94
|
+
// Assert
|
|
95
|
+
expect(display.map((block) => block.orderedItemNumber)).toEqual([1, 2, 3]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("blockRuns + overlayMarks", () => {
|
|
100
|
+
test("a partial-word Cut leaves surviving characters plain instead of inserted", () => {
|
|
101
|
+
const base = "It lands under unin Threads then in the sidebar rather than a project.";
|
|
102
|
+
const firstStart = base.indexOf("under");
|
|
103
|
+
const firstEnd = firstStart + "under".length;
|
|
104
|
+
const secondStart = base.indexOf("Threads");
|
|
105
|
+
const secondEnd = secondStart + "Threads".length;
|
|
106
|
+
const cuts = mergeTextCut(
|
|
107
|
+
base,
|
|
108
|
+
mergeTextCut(base, [], firstStart, firstEnd),
|
|
109
|
+
secondStart,
|
|
110
|
+
secondEnd,
|
|
111
|
+
);
|
|
112
|
+
const [block] = buildDisplay(base, applyTextCuts(base, cuts), cuts);
|
|
113
|
+
const runs = blockRuns(block!, true);
|
|
114
|
+
|
|
115
|
+
expect(runs.filter((run) => run.role === "del").map((run) => run.text)).toEqual([
|
|
116
|
+
"under",
|
|
117
|
+
"Threads",
|
|
118
|
+
]);
|
|
119
|
+
expect(
|
|
120
|
+
runs
|
|
121
|
+
.filter((run) => run.role !== "del")
|
|
122
|
+
.map((run) => run.text)
|
|
123
|
+
.join(""),
|
|
124
|
+
).toBe("It lands unin then in the sidebar rather than a project.");
|
|
125
|
+
expect(runs.some((run) => run.role === "ins")).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a cross-block Cut keeps every survivor plain and strikes only selected characters", () => {
|
|
129
|
+
const base = "Keep before remove this.\n\n- remove that keep after";
|
|
130
|
+
const cuts = mergeTextCut(base, [], 12, 40);
|
|
131
|
+
const display = buildDisplay(base, applyTextCuts(base, cuts), cuts);
|
|
132
|
+
const runs = display.flatMap((block) => blockRuns(block, true));
|
|
133
|
+
|
|
134
|
+
expect(runs.filter((run) => run.role === "ins")).toEqual([]);
|
|
135
|
+
expect(
|
|
136
|
+
runs
|
|
137
|
+
.filter((run) => run.role === "del")
|
|
138
|
+
.map((run) => run.text)
|
|
139
|
+
.join(""),
|
|
140
|
+
).toBe(base.slice(12, 40).replace("\n\n- ", ""));
|
|
141
|
+
expect(
|
|
142
|
+
runs
|
|
143
|
+
.filter((run) => run.role !== "del")
|
|
144
|
+
.map((run) => run.text)
|
|
145
|
+
.join(""),
|
|
146
|
+
).toBe("Keep before keep after");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("mod blocks emit word-diff runs that reconstruct both sides", () => {
|
|
150
|
+
// Arrange
|
|
151
|
+
const block = buildDisplay("plain old text here\n", "plain new text here\n")[0]!;
|
|
152
|
+
|
|
153
|
+
// Act
|
|
154
|
+
const runs = blockRuns(block, true);
|
|
155
|
+
|
|
156
|
+
// Assert
|
|
157
|
+
const oldSide = runs
|
|
158
|
+
.filter((run) => run.role !== "ins")
|
|
159
|
+
.map((run) => run.text)
|
|
160
|
+
.join("");
|
|
161
|
+
const newSide = runs
|
|
162
|
+
.filter((run) => run.role !== "del")
|
|
163
|
+
.map((run) => run.text)
|
|
164
|
+
.join("");
|
|
165
|
+
|
|
166
|
+
expect(oldSide).toBe("plain old text here");
|
|
167
|
+
expect(newSide).toBe("plain new text here");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("marks bind to working-text offsets and skip del runs", () => {
|
|
171
|
+
// Arrange
|
|
172
|
+
const block = buildDisplay("old words linger\n", "new words linger\n")[0]!;
|
|
173
|
+
const runs = blockRuns(block, true);
|
|
174
|
+
|
|
175
|
+
// Act
|
|
176
|
+
const marked = overlayMarks(runs, [{ start: 4, end: 9, role: "mark-comment" }]); // "words"
|
|
177
|
+
|
|
178
|
+
// Assert
|
|
179
|
+
const markedText = marked
|
|
180
|
+
.filter((run) => run.role === "mark-comment")
|
|
181
|
+
.map((run) => run.text)
|
|
182
|
+
.join("");
|
|
183
|
+
|
|
184
|
+
expect(markedText).toBe("words");
|
|
185
|
+
// del run text untouched
|
|
186
|
+
expect(marked.some((run) => run.role === "del" && run.text === "old")).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("marks never override ins emphasis", () => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const block = buildDisplay("keep\n", "keep added\n")[0]!;
|
|
192
|
+
|
|
193
|
+
// Act
|
|
194
|
+
const runs = overlayMarks(blockRuns(block, true), [
|
|
195
|
+
{ start: 0, end: 10, role: "mark-comment" },
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
// Assert
|
|
199
|
+
expect(runs.some((run) => run.role === "ins")).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("M4: link hrefs are scheme-validated for OSC 8 (#236)", () => {
|
|
204
|
+
test("trusts http(s) and mailto, drops everything else", () => {
|
|
205
|
+
// Assert
|
|
206
|
+
expect(safeLinkHref("https://example.com")).toBe("https://example.com");
|
|
207
|
+
expect(safeLinkHref("http://x")).toBe("http://x");
|
|
208
|
+
expect(safeLinkHref("mailto:a@b.c")).toBe("mailto:a@b.c");
|
|
209
|
+
expect(safeLinkHref("javascript:alert(1)")).toBeUndefined();
|
|
210
|
+
expect(safeLinkHref("./relative/path")).toBeUndefined();
|
|
211
|
+
expect(safeLinkHref(undefined)).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("a control byte or space in the target drops the link", () => {
|
|
215
|
+
// Assert - ESC and BEL could splice into the OSC 8 escape stream
|
|
216
|
+
expect(safeLinkHref("https://x\x1b]8;;evil\x07")).toBeUndefined();
|
|
217
|
+
expect(safeLinkHref("https://x\x07")).toBeUndefined();
|
|
218
|
+
expect(safeLinkHref("https://x y")).toBeUndefined();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("blockRuns carries the link href through to the link run", () => {
|
|
222
|
+
// Act
|
|
223
|
+
const block = buildDisplay("see [docs](https://example.com) now\n")[0]!;
|
|
224
|
+
const link = blockRuns(block, true).find((run) => run.role === "link")!;
|
|
225
|
+
|
|
226
|
+
// Assert
|
|
227
|
+
expect(link.text).toBe("docs");
|
|
228
|
+
expect(link.href).toBe("https://example.com");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("M3: inline emphasis composes with word-diff (#235)", () => {
|
|
233
|
+
test("context emphasis survives; added text keeps the green diff role; markers concealed", () => {
|
|
234
|
+
// Arrange - "**this**" is unchanged (context, stays emphasized); "safe"->"sound" is the edit
|
|
235
|
+
const block = buildDisplay("keep **this** safe\n", "keep **this** sound\n")[0]!;
|
|
236
|
+
|
|
237
|
+
// Act
|
|
238
|
+
const runs = blockRuns(block, true);
|
|
239
|
+
|
|
240
|
+
// Assert - no marker text renders anywhere
|
|
241
|
+
expect(runs.every((run) => !run.text.includes("*"))).toBe(true);
|
|
242
|
+
// the unchanged emphasized word keeps its strong role (context, not diffed)
|
|
243
|
+
expect(runs.find((run) => run.text === "this")!.role).toBe("strong");
|
|
244
|
+
// the added word is green (ins wins over any emphasis), the removed one opaque
|
|
245
|
+
expect(runs.find((run) => run.text.includes("sound"))!.role).toBe("ins");
|
|
246
|
+
expect(runs.some((run) => run.role === "del" && run.start === null)).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("markers inside added text conceal while the run stays a positioned ins", () => {
|
|
250
|
+
// Arrange - an emphasized word is added into an otherwise unchanged line
|
|
251
|
+
const block = buildDisplay("keep it safe\n", "keep it **very** safe\n")[0]!;
|
|
252
|
+
|
|
253
|
+
// Act
|
|
254
|
+
const runs = blockRuns(block, true);
|
|
255
|
+
const added = runs.find((run) => run.text === "very")!;
|
|
256
|
+
|
|
257
|
+
// Assert - concealed markers, green role, and an exact work offset back into work text
|
|
258
|
+
expect(added.role).toBe("ins");
|
|
259
|
+
expect(added.start).not.toBeNull();
|
|
260
|
+
expect(block.work!.text.slice(added.start!, added.start! + "very".length)).toBe("very");
|
|
261
|
+
expect(runs.every((run) => !run.text.includes("*"))).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("an anchor over an emphasized context word round-trips to its work offsets", () => {
|
|
265
|
+
// Arrange
|
|
266
|
+
const block = buildDisplay("hold **steady** now\n", "hold **steady** later\n")[0]!;
|
|
267
|
+
const runs = blockRuns(block, true);
|
|
268
|
+
const work = block.work!.text;
|
|
269
|
+
const start = work.indexOf("steady");
|
|
270
|
+
|
|
271
|
+
// Act
|
|
272
|
+
const rendered = renderedOffsetFor(runs, start);
|
|
273
|
+
|
|
274
|
+
// Assert
|
|
275
|
+
expect(rendered).not.toBeNull();
|
|
276
|
+
expect(workRangeForRendered(runs, rendered!, rendered! + "steady".length)).toEqual({
|
|
277
|
+
start,
|
|
278
|
+
end: start + "steady".length,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("marksByDisplay", () => {
|
|
284
|
+
test("annotations resolve into display coordinates", () => {
|
|
285
|
+
// Arrange
|
|
286
|
+
const working = BASE.replace("first item", "first item edited beyond recognition of it");
|
|
287
|
+
const display = buildDisplay(BASE, working);
|
|
288
|
+
const ann: Annotation = {
|
|
289
|
+
id: "a1",
|
|
290
|
+
kind: "comment",
|
|
291
|
+
anchor: { quote: "atomically", prefix: "to disk ", suffix: "." },
|
|
292
|
+
body: "define atomically",
|
|
293
|
+
createdAt: "",
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Act
|
|
297
|
+
const map = marksByDisplay([ann], display);
|
|
298
|
+
|
|
299
|
+
// Assert
|
|
300
|
+
const [displayIndex, marks] = [...map.entries()][0]!;
|
|
301
|
+
|
|
302
|
+
expect(displayText(display[displayIndex]!)).toContain("atomically");
|
|
303
|
+
expect(marks[0]!.annotationId).toBe("a1");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("orphaned annotations produce no marks", () => {
|
|
307
|
+
// Arrange
|
|
308
|
+
const display = buildDisplay(BASE);
|
|
309
|
+
const ann: Annotation = {
|
|
310
|
+
id: "a1",
|
|
311
|
+
kind: "comment",
|
|
312
|
+
anchor: { quote: "text that is gone", prefix: "", suffix: "" },
|
|
313
|
+
body: "",
|
|
314
|
+
createdAt: "",
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Assert
|
|
318
|
+
expect(marksByDisplay([ann], display).size).toBe(0);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("rendered/work offset mapping", () => {
|
|
323
|
+
test("plain blocks map 1:1 in both directions", () => {
|
|
324
|
+
// Arrange
|
|
325
|
+
const block = buildDisplay("alpha beta gamma\n")[0]!;
|
|
326
|
+
const runs = blockRuns(block, true);
|
|
327
|
+
|
|
328
|
+
// Assert
|
|
329
|
+
expect(renderedOffsetFor(runs, 6)).toBe(6);
|
|
330
|
+
expect(workRangeForRendered(runs, 6, 10)).toEqual({ start: 6, end: 10 });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("inline del runs shift rendered offsets in word-diffed blocks", () => {
|
|
334
|
+
// Arrange
|
|
335
|
+
// rendered: "old new words linger" - "old " is a del run without offsets
|
|
336
|
+
const block = buildDisplay("old words linger\n", "new words linger\n")[0]!;
|
|
337
|
+
const runs = blockRuns(block, true);
|
|
338
|
+
const workText = displayText(block);
|
|
339
|
+
const wordsAt = workText.indexOf("words");
|
|
340
|
+
|
|
341
|
+
// Act
|
|
342
|
+
const rendered = renderedOffsetFor(runs, wordsAt)!;
|
|
343
|
+
|
|
344
|
+
// Assert
|
|
345
|
+
const renderedString = runs.map((run) => run.text).join("");
|
|
346
|
+
|
|
347
|
+
expect(renderedString.slice(rendered, rendered + "words".length)).toBe("words");
|
|
348
|
+
// reading a rendered selection over "words" recovers the work range
|
|
349
|
+
expect(workRangeForRendered(runs, rendered, rendered + "words".length)).toEqual({
|
|
350
|
+
start: wordsAt,
|
|
351
|
+
end: wordsAt + "words".length,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("a rendered selection spanning a del run keeps only positioned text", () => {
|
|
356
|
+
// Arrange
|
|
357
|
+
const block = buildDisplay("old words\n", "new words\n")[0]!;
|
|
358
|
+
const runs = blockRuns(block, true);
|
|
359
|
+
const renderedString = runs.map((run) => run.text).join("");
|
|
360
|
+
|
|
361
|
+
// Act
|
|
362
|
+
// select everything rendered: the work range is the whole work text
|
|
363
|
+
const range = workRangeForRendered(runs, 0, renderedString.length)!;
|
|
364
|
+
|
|
365
|
+
// Assert
|
|
366
|
+
expect(displayText(block).slice(range.start, range.end)).toBe(displayText(block));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("offsets outside any positioned run map to null", () => {
|
|
370
|
+
// Arrange
|
|
371
|
+
const runs = blockRuns(buildDisplay("short\n")[0]!, true);
|
|
372
|
+
|
|
373
|
+
// Assert
|
|
374
|
+
expect(renderedOffsetFor(runs, 99)).toBeNull();
|
|
375
|
+
expect(workRangeForRendered(runs, 90, 99)).toBeNull();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("a span starting/ending on concealed markers snaps to the visible word", () => {
|
|
379
|
+
// Arrange - the word span over "**very**" covers the markers, which have
|
|
380
|
+
// no rendered cells; the clamped lookups land on "very" itself
|
|
381
|
+
const text = "keep it **very** safe\n";
|
|
382
|
+
const runs = blockRuns(buildDisplay(text)[0]!, true);
|
|
383
|
+
const spanStart = text.indexOf("**very**");
|
|
384
|
+
const spanEnd = spanStart + "**very**".length;
|
|
385
|
+
|
|
386
|
+
// Act
|
|
387
|
+
const renderedStart = renderedOffsetAtOrAfter(runs, spanStart)!;
|
|
388
|
+
const renderedEnd = renderedOffsetAtOrBefore(runs, spanEnd - 1)!;
|
|
389
|
+
|
|
390
|
+
// Assert
|
|
391
|
+
const renderedString = runs.map((run) => run.text).join("");
|
|
392
|
+
|
|
393
|
+
expect(renderedString.slice(renderedStart, renderedEnd + 1)).toBe("very");
|
|
394
|
+
// exact hits behave like renderedOffsetFor
|
|
395
|
+
expect(renderedOffsetAtOrAfter(runs, 0)).toBe(0);
|
|
396
|
+
expect(renderedOffsetAtOrBefore(runs, 0)).toBe(0);
|
|
397
|
+
// nothing positioned beyond the text end
|
|
398
|
+
expect(renderedOffsetAtOrAfter(runs, 99)).toBeNull();
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("span mode", () => {
|
|
403
|
+
const text = "one two three four";
|
|
404
|
+
|
|
405
|
+
test("v selects the first word; l grows; w slides; $ to end", () => {
|
|
406
|
+
// Act
|
|
407
|
+
let span = startSpan(0, text)!;
|
|
408
|
+
|
|
409
|
+
// Assert
|
|
410
|
+
expect(text.slice(span.start, span.end)).toBe("one");
|
|
411
|
+
|
|
412
|
+
// Act
|
|
413
|
+
span = spanKey(span, "l", text);
|
|
414
|
+
|
|
415
|
+
// Assert
|
|
416
|
+
expect(text.slice(span.start, span.end)).toBe("one two");
|
|
417
|
+
|
|
418
|
+
// Act
|
|
419
|
+
span = spanKey(span, "w", text);
|
|
420
|
+
|
|
421
|
+
// Assert
|
|
422
|
+
expect(text.slice(span.start, span.end)).toBe("two three");
|
|
423
|
+
|
|
424
|
+
// Act
|
|
425
|
+
span = spanKey(span, "$", text);
|
|
426
|
+
|
|
427
|
+
// Assert
|
|
428
|
+
expect(text.slice(span.start, span.end)).toBe("two three four");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("spanFromRange snaps a mid-word mouse drag to whole words", () => {
|
|
432
|
+
// Arrange - a drag from inside "two" to inside "three"
|
|
433
|
+
const dragStart = text.indexOf("two") + 1;
|
|
434
|
+
const dragEnd = text.indexOf("three") + 2;
|
|
435
|
+
|
|
436
|
+
// Act
|
|
437
|
+
const span = spanFromRange(0, text, dragStart, dragEnd)!;
|
|
438
|
+
|
|
439
|
+
// Assert - snaps outward to cover both touched words, ready for w/b/l/h
|
|
440
|
+
expect(text.slice(span.start, span.end)).toBe("two three");
|
|
441
|
+
expect(spanKey(span, "l", text)).toMatchObject({ wordEnd: 3 });
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("spanFromRange returns null when the range covers no word", () => {
|
|
445
|
+
// Assert - a range past the last word has nothing to select
|
|
446
|
+
expect(spanFromRange(0, text, text.length, text.length)).toBeNull();
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
describe("cut / restore round-trip", () => {
|
|
451
|
+
test("cut removes the block lines; restore puts them back", () => {
|
|
452
|
+
// Arrange
|
|
453
|
+
const blocks = parseBlocks(BASE);
|
|
454
|
+
const listItem = blocks.find((candidate) => candidate.text === "second item")!;
|
|
455
|
+
|
|
456
|
+
// Act
|
|
457
|
+
const cut = cutBlock(BASE, listItem);
|
|
458
|
+
|
|
459
|
+
// Assert
|
|
460
|
+
expect(cut).not.toContain("second item");
|
|
461
|
+
|
|
462
|
+
// Arrange
|
|
463
|
+
const display = buildDisplay(BASE, cut);
|
|
464
|
+
const deletedIndex = display.findIndex((block) => block.type === "del");
|
|
465
|
+
const line = restoreLine(nextWorkBlock(display, deletedIndex), cut.split("\n").length);
|
|
466
|
+
|
|
467
|
+
// Act
|
|
468
|
+
// undefined = the working copy round-tripped back to the submitted revision
|
|
469
|
+
const restored = restoreBlock(BASE, cut, listItem, line);
|
|
470
|
+
|
|
471
|
+
// Assert
|
|
472
|
+
expect(restored).toBeUndefined();
|
|
473
|
+
expect(buildDisplay(BASE, restored).every((block) => block.type === "same")).toBe(true);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe("rendered-text projection (markers concealed)", () => {
|
|
478
|
+
const linkedBlock = () => buildDisplay("Use the `flag` and see [docs](https://x.dev).")[0]!;
|
|
479
|
+
|
|
480
|
+
test("renderedText drops inline markers, keeping visible characters", () => {
|
|
481
|
+
expect(renderedText(linkedBlock())).toBe("Use the flag and see docs.");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("renderedText leaves a table block's source verbatim", () => {
|
|
485
|
+
const table = buildDisplay("| a | b |\n| --- | --- |\n| 1 | 2 |")[0]!;
|
|
486
|
+
|
|
487
|
+
expect(table.kind).toBe("table");
|
|
488
|
+
expect(renderedText(table)).toBe("| a | b |\n| --- | --- |\n| 1 | 2 |");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("renderedSpanToWork maps a rendered link label back to its work offsets", () => {
|
|
492
|
+
const display = buildDisplay("Use the `flag` and see [docs](https://x.dev).");
|
|
493
|
+
// "docs" sits at rendered offset 21 ("Use the flag and see ".length)
|
|
494
|
+
const work = renderedSpanToWork(display, 0, 0, 21, 25);
|
|
495
|
+
const workText = display[0]!.work!.text;
|
|
496
|
+
|
|
497
|
+
expect(workText.slice(work.start, work.end)).toBe("docs");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("renderedSpanToWork maps rendered inline code back to the backtick content", () => {
|
|
501
|
+
const display = buildDisplay("Use the `flag` and see docs.");
|
|
502
|
+
// "flag" sits at rendered offset 8 ("Use the ".length)
|
|
503
|
+
const work = renderedSpanToWork(display, 0, 0, 8, 12);
|
|
504
|
+
|
|
505
|
+
expect(display[0]!.work!.text.slice(work.start, work.end)).toBe("flag");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("styledRunsFor colors a link run and marks the covered stretch", () => {
|
|
509
|
+
const linkRow = buildDisplay("see [docs](https://x.dev) now")[0]!;
|
|
510
|
+
const roleRuns = renderedStyleRuns(linkRow);
|
|
511
|
+
// "see docs now"; mark the whole "docs" label (rendered offsets 4..8)
|
|
512
|
+
const runs = styledRunsFor(roleRuns, { start: 0, end: renderedText(linkRow).length }, [
|
|
513
|
+
{ start: 4, end: 8 },
|
|
514
|
+
]);
|
|
515
|
+
const link = runs.find((run) => run.role === "link")!;
|
|
516
|
+
|
|
517
|
+
expect(link.text).toBe("docs");
|
|
518
|
+
expect(link.marked).toBe(true);
|
|
519
|
+
expect(runs.find((run) => run.text === "see ")!.marked).toBe(false);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("syntax tokens split painted code runs without moving annotation marks", () => {
|
|
523
|
+
const roles = [{ text: "export value", role: "plain" as const, start: 0 }];
|
|
524
|
+
const runs = styledRunsFor(
|
|
525
|
+
roles,
|
|
526
|
+
{ start: 0, end: 12 },
|
|
527
|
+
[{ start: 5, end: 9 }],
|
|
528
|
+
[{ start: 0, end: 6, group: "keyword" }],
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(runs.map((run) => run.text).join("")).toBe("export value");
|
|
532
|
+
expect(
|
|
533
|
+
runs
|
|
534
|
+
.filter((run) => run.syntaxGroup === "keyword")
|
|
535
|
+
.map((run) => run.text)
|
|
536
|
+
.join(""),
|
|
537
|
+
).toBe("export");
|
|
538
|
+
expect(
|
|
539
|
+
runs
|
|
540
|
+
.filter((run) => run.marked)
|
|
541
|
+
.map((run) => run.text)
|
|
542
|
+
.join(""),
|
|
543
|
+
).toBe("t va");
|
|
544
|
+
});
|
|
545
|
+
});
|