@cueloop/client 0.1.0-alpha.9 → 0.1.0-alpha.91
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 +1406 -1060
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +787 -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 +29 -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 +78 -0
- package/src/launch-layout.ts +62 -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,238 @@
|
|
|
1
|
+
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import * as intraline from "./diff-intraline";
|
|
3
|
+
import {
|
|
4
|
+
createIntralineResolver,
|
|
5
|
+
intralineRunsByRow,
|
|
6
|
+
wordLevelChanges,
|
|
7
|
+
type IntralineRun,
|
|
8
|
+
} from "./diff-intraline";
|
|
9
|
+
import type { DiffRow } from "./view-diff";
|
|
10
|
+
|
|
11
|
+
function line(kind: DiffRow["kind"], text: string): DiffRow {
|
|
12
|
+
return { kind, text, file: "a.ts" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function joinText(runs: IntralineRun[]): string {
|
|
16
|
+
return runs.map((run) => run.text).join("");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function changedText(runs: IntralineRun[]): string {
|
|
20
|
+
return runs
|
|
21
|
+
.filter((run) => run.changed)
|
|
22
|
+
.map((run) => run.text)
|
|
23
|
+
.join("");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("wordLevelChanges", () => {
|
|
27
|
+
test("marks only the changed word and stays whitespace-lossless", () => {
|
|
28
|
+
// Arrange
|
|
29
|
+
const oldText = "const foo = 1";
|
|
30
|
+
const newText = "const foo = 2";
|
|
31
|
+
|
|
32
|
+
// Act
|
|
33
|
+
const changes = wordLevelChanges(oldText, newText);
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(
|
|
37
|
+
changes
|
|
38
|
+
.filter((change) => change.kind === "common")
|
|
39
|
+
.map((change) => change.text)
|
|
40
|
+
.join(""),
|
|
41
|
+
).toBe("const foo = ");
|
|
42
|
+
expect(
|
|
43
|
+
changes
|
|
44
|
+
.filter((change) => change.kind === "removed")
|
|
45
|
+
.map((change) => change.text)
|
|
46
|
+
.join(""),
|
|
47
|
+
).toBe("1");
|
|
48
|
+
expect(
|
|
49
|
+
changes
|
|
50
|
+
.filter((change) => change.kind === "added")
|
|
51
|
+
.map((change) => change.text)
|
|
52
|
+
.join(""),
|
|
53
|
+
).toBe("2");
|
|
54
|
+
// old text reconstructs from common + removed, new text from common + added
|
|
55
|
+
expect(
|
|
56
|
+
changes
|
|
57
|
+
.filter((change) => change.kind !== "added")
|
|
58
|
+
.map((change) => change.text)
|
|
59
|
+
.join(""),
|
|
60
|
+
).toBe(oldText);
|
|
61
|
+
expect(
|
|
62
|
+
changes
|
|
63
|
+
.filter((change) => change.kind !== "removed")
|
|
64
|
+
.map((change) => change.text)
|
|
65
|
+
.join(""),
|
|
66
|
+
).toBe(newText);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("intralineRunsByRow", () => {
|
|
71
|
+
test("pairs a deletion with its addition and highlights the changed word on each side", () => {
|
|
72
|
+
// Arrange
|
|
73
|
+
const rows = [
|
|
74
|
+
line("ctx", "unchanged"),
|
|
75
|
+
line("del", "const foo = 1"),
|
|
76
|
+
line("add", "const foo = 2"),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// Act
|
|
80
|
+
const runsByRow = intralineRunsByRow(rows);
|
|
81
|
+
|
|
82
|
+
// Assert
|
|
83
|
+
const deletionRuns = runsByRow.get(1)!;
|
|
84
|
+
const additionRuns = runsByRow.get(2)!;
|
|
85
|
+
|
|
86
|
+
expect(joinText(deletionRuns)).toBe("const foo = 1");
|
|
87
|
+
expect(joinText(additionRuns)).toBe("const foo = 2");
|
|
88
|
+
expect(changedText(deletionRuns)).toBe("1");
|
|
89
|
+
expect(changedText(additionRuns)).toBe("2");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("omits context rows and rows with no counterpart", () => {
|
|
93
|
+
// Arrange
|
|
94
|
+
const rows = [line("ctx", "keep me"), line("add", "brand new line")];
|
|
95
|
+
|
|
96
|
+
// Act
|
|
97
|
+
const runsByRow = intralineRunsByRow(rows);
|
|
98
|
+
|
|
99
|
+
// Assert
|
|
100
|
+
expect(runsByRow.size).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("pairs only min(deletions, additions); the surplus stays a plain row", () => {
|
|
104
|
+
// Arrange - two deletions, one addition
|
|
105
|
+
const rows = [line("del", "alpha one"), line("del", "beta two"), line("add", "alpha ONE")];
|
|
106
|
+
|
|
107
|
+
// Act
|
|
108
|
+
const runsByRow = intralineRunsByRow(rows);
|
|
109
|
+
|
|
110
|
+
// Assert
|
|
111
|
+
expect(runsByRow.has(0)).toBe(true); // first deletion pairs with the addition
|
|
112
|
+
expect(runsByRow.has(1)).toBe(false); // second deletion has no counterpart
|
|
113
|
+
expect(runsByRow.has(2)).toBe(true);
|
|
114
|
+
expect(changedText(runsByRow.get(0)!)).toBe("one");
|
|
115
|
+
expect(changedText(runsByRow.get(2)!)).toBe("ONE");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a fully rewritten single line marks the whole line changed on both sides", () => {
|
|
119
|
+
// Arrange - a lone deletion opposite a lone addition is unambiguously one edit
|
|
120
|
+
const rows = [line("del", "aaa"), line("add", "zzz")];
|
|
121
|
+
|
|
122
|
+
// Act
|
|
123
|
+
const runsByRow = intralineRunsByRow(rows);
|
|
124
|
+
|
|
125
|
+
// Assert
|
|
126
|
+
expect(changedText(runsByRow.get(0)!)).toBe("aaa");
|
|
127
|
+
expect(changedText(runsByRow.get(1)!)).toBe("zzz");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("in a shifted block, alignment matches the real counterpart, not by position", () => {
|
|
131
|
+
// Arrange - "beta gamma" is modified and an unrelated line sits at each end;
|
|
132
|
+
// positional pairing would word-diff "alpha" against "beta gamma delta"
|
|
133
|
+
const rows = [
|
|
134
|
+
line("del", "alpha"),
|
|
135
|
+
line("del", "beta gamma"),
|
|
136
|
+
line("add", "beta gamma delta"),
|
|
137
|
+
line("add", "zulu"),
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// Act
|
|
141
|
+
const runsByRow = intralineRunsByRow(rows);
|
|
142
|
+
|
|
143
|
+
// Assert
|
|
144
|
+
// the modified pair is found and word-diffed
|
|
145
|
+
expect(runsByRow.has(1)).toBe(true); // "beta gamma"
|
|
146
|
+
expect(runsByRow.has(2)).toBe(true); // "beta gamma delta"
|
|
147
|
+
expect(changedText(runsByRow.get(2)!).trim()).toBe("delta");
|
|
148
|
+
// the genuinely unrelated lines stay whole-line
|
|
149
|
+
expect(runsByRow.has(0)).toBe(false); // "alpha"
|
|
150
|
+
expect(runsByRow.has(3)).toBe(false); // "zulu"
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("in a multi-line block, related positional pairs still highlight the changed word", () => {
|
|
154
|
+
// Arrange - a same-shape 2x2 edit: each row lines up with its counterpart
|
|
155
|
+
const rows = [
|
|
156
|
+
line("del", "one two"),
|
|
157
|
+
line("del", "three four"),
|
|
158
|
+
line("add", "one TWO"),
|
|
159
|
+
line("add", "three FOUR"),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// Act
|
|
163
|
+
const runsByRow = intralineRunsByRow(rows);
|
|
164
|
+
|
|
165
|
+
// Assert
|
|
166
|
+
expect(changedText(runsByRow.get(0)!)).toBe("two");
|
|
167
|
+
expect(changedText(runsByRow.get(1)!)).toBe("four");
|
|
168
|
+
expect(changedText(runsByRow.get(2)!)).toBe("TWO");
|
|
169
|
+
expect(changedText(runsByRow.get(3)!)).toBe("FOUR");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("createIntralineResolver", () => {
|
|
174
|
+
test("resolves each row to the same runs the eager pass would, on demand", () => {
|
|
175
|
+
// Arrange - context around two separate change blocks
|
|
176
|
+
const rows = [
|
|
177
|
+
line("ctx", "header"),
|
|
178
|
+
line("del", "one two"),
|
|
179
|
+
line("add", "one TWO"),
|
|
180
|
+
line("ctx", "middle"),
|
|
181
|
+
line("del", "alpha beta"),
|
|
182
|
+
line("add", "alpha GAMMA"),
|
|
183
|
+
line("ctx", "footer"),
|
|
184
|
+
];
|
|
185
|
+
const eager = intralineRunsByRow(rows);
|
|
186
|
+
|
|
187
|
+
// Act
|
|
188
|
+
const resolver = createIntralineResolver(rows);
|
|
189
|
+
|
|
190
|
+
// Assert - lazy row-by-row output matches the eager map, including undefined for context rows
|
|
191
|
+
for (let index = 0; index < rows.length; index++) {
|
|
192
|
+
expect(resolver.runsForRow(index)).toEqual(eager.get(index));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a second call for the same row returns the cached runs", () => {
|
|
197
|
+
const rows = [line("del", "one two"), line("add", "one TWO")];
|
|
198
|
+
const resolver = createIntralineResolver(rows);
|
|
199
|
+
|
|
200
|
+
expect(resolver.runsForRow(0)).toBe(resolver.runsForRow(0));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// The performance guard: a virtualized diff must word-diff only the change blocks whose rows are on
|
|
204
|
+
// screen. This is deterministic (a call count, not a wall-clock), so it fails on any machine if a
|
|
205
|
+
// future change makes the resolver eager again - the regression the nav-latency benchmark would show.
|
|
206
|
+
test("word-diffs only the accessed block, not the whole diff", () => {
|
|
207
|
+
// Arrange - 50 independent change blocks, each a single del/add pair separated by context
|
|
208
|
+
const rows: DiffRow[] = [];
|
|
209
|
+
|
|
210
|
+
for (let block = 0; block < 50; block++) {
|
|
211
|
+
rows.push(
|
|
212
|
+
line("ctx", `ctx ${block}`),
|
|
213
|
+
line("del", `old ${block}`),
|
|
214
|
+
line("add", `new ${block}`),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const spy = spyOn(intraline, "wordLevelChanges");
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// Act - the eager pass word-diffs every block
|
|
221
|
+
spy.mockClear();
|
|
222
|
+
intralineRunsByRow(rows);
|
|
223
|
+
const eagerCalls = spy.mock.calls.length;
|
|
224
|
+
|
|
225
|
+
// Act - the resolver word-diffs only the one block whose row is asked for
|
|
226
|
+
spy.mockClear();
|
|
227
|
+
const resolver = createIntralineResolver(rows);
|
|
228
|
+
resolver.runsForRow(1); // a row in block 0
|
|
229
|
+
const oneBlockCalls = spy.mock.calls.length;
|
|
230
|
+
|
|
231
|
+
// Assert - eager pays for all 50 blocks; lazy pays for one
|
|
232
|
+
expect(eagerCalls).toBe(50);
|
|
233
|
+
expect(oneBlockCalls).toBe(1);
|
|
234
|
+
} finally {
|
|
235
|
+
spy.mockRestore();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intra-line word diff: the char-precise "what changed within this line" engine
|
|
3
|
+
* shared by the plan tracked-changes view and the diff review sheet. Backed by
|
|
4
|
+
* jsdiff diffWordsWithSpace, so it is punctuation-aware and whitespace-lossless:
|
|
5
|
+
* the run values always concatenate back to the exact inputs, which keeps quote
|
|
6
|
+
* anchors intact. Supersedes the coarse LCS wordDiff.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { diffWordsWithSpace } from "diff";
|
|
10
|
+
import { lcsDiff } from "@cueloop/schema";
|
|
11
|
+
import type { DiffRow } from "./view-diff";
|
|
12
|
+
|
|
13
|
+
export type WordChangeKind = "common" | "added" | "removed";
|
|
14
|
+
|
|
15
|
+
/** One word-level change between two strings; text is verbatim (lossless). */
|
|
16
|
+
export interface WordChange {
|
|
17
|
+
text: string;
|
|
18
|
+
kind: WordChangeKind;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Word-level changes between two strings, whitespace-lossless and ordered. */
|
|
22
|
+
export function wordLevelChanges(oldText: string, newText: string): WordChange[] {
|
|
23
|
+
return diffWordsWithSpace(oldText, newText).map((change) => ({
|
|
24
|
+
text: change.value,
|
|
25
|
+
kind: change.added ? "added" : change.removed ? "removed" : "common",
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One rendered segment of a diff row: changed marks the intra-line change. */
|
|
30
|
+
export interface IntralineRun {
|
|
31
|
+
text: string;
|
|
32
|
+
changed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stripTrailingNewline(text: string): string {
|
|
36
|
+
return text.replace(/\n$/, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A change block: a run of deletion rows immediately followed by addition rows. */
|
|
40
|
+
interface IntralineBlock {
|
|
41
|
+
deletionStart: number;
|
|
42
|
+
additionStart: number;
|
|
43
|
+
end: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The change blocks in row order, found in one cheap pass (no word diff yet). */
|
|
47
|
+
function intralineBlocks(rows: DiffRow[]): IntralineBlock[] {
|
|
48
|
+
const blocks: IntralineBlock[] = [];
|
|
49
|
+
let index = 0;
|
|
50
|
+
|
|
51
|
+
while (index < rows.length) {
|
|
52
|
+
if (rows[index]!.kind !== "del") {
|
|
53
|
+
index++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const deletionStart = index;
|
|
57
|
+
|
|
58
|
+
while (index < rows.length && rows[index]!.kind === "del") index++;
|
|
59
|
+
const additionStart = index;
|
|
60
|
+
|
|
61
|
+
while (index < rows.length && rows[index]!.kind === "add") index++;
|
|
62
|
+
blocks.push({ deletionStart, additionStart, end: index });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return blocks;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Word-diff one change block into [rowIndex, runs] pairs; unmatched rows get no entry. */
|
|
69
|
+
function blockRunEntries(rows: DiffRow[], block: IntralineBlock): Array<[number, IntralineRun[]]> {
|
|
70
|
+
const deletionTexts = rows
|
|
71
|
+
.slice(block.deletionStart, block.additionStart)
|
|
72
|
+
.map((row) => stripTrailingNewline(row.text));
|
|
73
|
+
const additionTexts = rows
|
|
74
|
+
.slice(block.additionStart, block.end)
|
|
75
|
+
.map((row) => stripTrailingNewline(row.text));
|
|
76
|
+
const entries: Array<[number, IntralineRun[]]> = [];
|
|
77
|
+
|
|
78
|
+
for (const [deletionOffset, additionOffset] of alignedPairs(deletionTexts, additionTexts)) {
|
|
79
|
+
const changes = wordLevelChanges(
|
|
80
|
+
deletionTexts[deletionOffset]!,
|
|
81
|
+
additionTexts[additionOffset]!,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
entries.push([block.deletionStart + deletionOffset, sideRuns(changes, "removed")]);
|
|
85
|
+
entries.push([block.additionStart + additionOffset, sideRuns(changes, "added")]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The change block whose rows cover `rowIndex`, by binary search over the ordered blocks. */
|
|
92
|
+
function blockContaining(blocks: IntralineBlock[], rowIndex: number): number | undefined {
|
|
93
|
+
let low = 0;
|
|
94
|
+
let high = blocks.length - 1;
|
|
95
|
+
|
|
96
|
+
while (low <= high) {
|
|
97
|
+
const mid = (low + high) >> 1;
|
|
98
|
+
const block = blocks[mid]!;
|
|
99
|
+
|
|
100
|
+
if (rowIndex < block.deletionStart) high = mid - 1;
|
|
101
|
+
else if (rowIndex >= block.end) low = mid + 1;
|
|
102
|
+
else return mid;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A row's intra-line runs, resolved on demand. */
|
|
109
|
+
export interface IntralineResolver {
|
|
110
|
+
runsForRow(rowIndex: number): IntralineRun[] | undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Char-precise intra-line runs for modified diff rows, computed lazily. The change blocks are found
|
|
115
|
+
* up front in one cheap pass; a block is word-diffed only when a row inside it is first asked for,
|
|
116
|
+
* so a virtualized diff pays for the rows on screen, not for the whole file. Within a change block -
|
|
117
|
+
* deletions immediately followed by additions - lines align to their real counterpart (alignedPairs)
|
|
118
|
+
* and each matched pair is word-diffed; unmatched rows get no runs, so the caller renders those whole.
|
|
119
|
+
*/
|
|
120
|
+
export function createIntralineResolver(rows: DiffRow[]): IntralineResolver {
|
|
121
|
+
const blocks = intralineBlocks(rows);
|
|
122
|
+
const computed = new Set<number>();
|
|
123
|
+
const runsByRow = new Map<number, IntralineRun[]>();
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
runsForRow(rowIndex) {
|
|
127
|
+
const blockIndex = blockContaining(blocks, rowIndex);
|
|
128
|
+
|
|
129
|
+
if (blockIndex === undefined) return undefined;
|
|
130
|
+
if (!computed.has(blockIndex)) {
|
|
131
|
+
computed.add(blockIndex);
|
|
132
|
+
for (const [row, runs] of blockRunEntries(rows, blocks[blockIndex]!))
|
|
133
|
+
runsByRow.set(row, runs);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return runsByRow.get(rowIndex);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Every modified row's intra-line runs, eagerly. Prefer createIntralineResolver on a hot path. */
|
|
142
|
+
export function intralineRunsByRow(rows: DiffRow[]): Map<number, IntralineRun[]> {
|
|
143
|
+
const runsByRow = new Map<number, IntralineRun[]>();
|
|
144
|
+
|
|
145
|
+
for (const block of intralineBlocks(rows)) {
|
|
146
|
+
for (const [row, runs] of blockRunEntries(rows, block)) runsByRow.set(row, runs);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return runsByRow;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Lines this similar (word-set overlap) are treated as the same line edited. */
|
|
153
|
+
const SIMILAR_MIN_WORD_OVERLAP = 0.3;
|
|
154
|
+
|
|
155
|
+
/** Lower-cased word set of a line for the similarity gate, cached per distinct line within a block so
|
|
156
|
+
* the LCS grid tokenizes each line once instead of once per comparison. */
|
|
157
|
+
function lineWordSetCache(): (text: string) => ReadonlySet<string> {
|
|
158
|
+
const cache = new Map<string, ReadonlySet<string>>();
|
|
159
|
+
|
|
160
|
+
return (text) => {
|
|
161
|
+
const cached = cache.get(text);
|
|
162
|
+
if (cached !== undefined) return cached;
|
|
163
|
+
const words = new Set(text.toLowerCase().split(/\s+/).filter(Boolean));
|
|
164
|
+
cache.set(text, words);
|
|
165
|
+
|
|
166
|
+
return words;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Whether two lines are the same line edited, by case-insensitive word-set
|
|
171
|
+
* overlap - the matching gate, distinct from the case-sensitive word diff. */
|
|
172
|
+
function linesSimilar(
|
|
173
|
+
wordsOf: (text: string) => ReadonlySet<string>,
|
|
174
|
+
oldText: string,
|
|
175
|
+
newText: string,
|
|
176
|
+
): boolean {
|
|
177
|
+
const oldWords = wordsOf(oldText);
|
|
178
|
+
const newWords = wordsOf(newText);
|
|
179
|
+
|
|
180
|
+
if (oldWords.size === 0 && newWords.size === 0) return true;
|
|
181
|
+
let intersection = 0;
|
|
182
|
+
|
|
183
|
+
for (const word of oldWords) if (newWords.has(word)) intersection++;
|
|
184
|
+
const union = oldWords.size + newWords.size - intersection;
|
|
185
|
+
|
|
186
|
+
return union === 0 ? true : intersection / union >= SIMILAR_MIN_WORD_OVERLAP;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Pair each modified line with the addition it became. A lone deletion opposite
|
|
191
|
+
* a lone addition is one edit (always paired). Otherwise align the lines by
|
|
192
|
+
* similarity, order-preserving (LCS over `linesSimilar`), so an inserted, removed,
|
|
193
|
+
* or shifted line finds its real counterpart instead of mispairing by position -
|
|
194
|
+
* unmatched lines get no pair and render whole-line. Returns [deletionOffset,
|
|
195
|
+
* additionOffset] pairs into the block.
|
|
196
|
+
*/
|
|
197
|
+
function alignedPairs(deletionTexts: string[], additionTexts: string[]): Array<[number, number]> {
|
|
198
|
+
if (deletionTexts.length === 1 && additionTexts.length === 1) return [[0, 0]];
|
|
199
|
+
const pairs: Array<[number, number]> = [];
|
|
200
|
+
let deletionOffset = 0;
|
|
201
|
+
let additionOffset = 0;
|
|
202
|
+
const wordsOf = lineWordSetCache();
|
|
203
|
+
|
|
204
|
+
for (const op of lcsDiff(deletionTexts, additionTexts, (deletionText, additionText) =>
|
|
205
|
+
linesSimilar(wordsOf, deletionText, additionText),
|
|
206
|
+
)) {
|
|
207
|
+
if (op.kind === "ctx") {
|
|
208
|
+
pairs.push([deletionOffset, additionOffset]);
|
|
209
|
+
deletionOffset++;
|
|
210
|
+
additionOffset++;
|
|
211
|
+
} else if (op.kind === "del") {
|
|
212
|
+
deletionOffset++;
|
|
213
|
+
} else {
|
|
214
|
+
additionOffset++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return pairs;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Runs for one side of a word diff: common plus this side's changes, adjacent
|
|
222
|
+
* runs of equal changed-ness coalesced so highlighting stays as few spans. */
|
|
223
|
+
function sideRuns(changes: WordChange[], side: "added" | "removed"): IntralineRun[] {
|
|
224
|
+
const runs: IntralineRun[] = [];
|
|
225
|
+
|
|
226
|
+
for (const change of changes) {
|
|
227
|
+
if (change.kind !== "common" && change.kind !== side) continue;
|
|
228
|
+
const changed = change.kind === side;
|
|
229
|
+
const last = runs[runs.length - 1];
|
|
230
|
+
|
|
231
|
+
if (last && last.changed === changed) last.text += change.text;
|
|
232
|
+
else runs.push({ text: change.text, changed });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return runs;
|
|
236
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { highlightJobs, spansByLine } from "./diff-syntax";
|
|
3
|
+
import type { DiffRow } from "./view-diff";
|
|
4
|
+
import type { SimpleHighlight } from "@opentui/core";
|
|
5
|
+
|
|
6
|
+
function row(kind: DiffRow["kind"], text: string, file = "src/x.ts"): DiffRow {
|
|
7
|
+
return { kind, text, file };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("spansByLine", () => {
|
|
11
|
+
test("maps absolute offsets to line-relative spans", () => {
|
|
12
|
+
// Arrange
|
|
13
|
+
const source = "const x = 1;\nfoo";
|
|
14
|
+
const highlights: SimpleHighlight[] = [
|
|
15
|
+
[0, 5, "keyword"],
|
|
16
|
+
[6, 7, "variable"],
|
|
17
|
+
[13, 16, "variable"],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
const lineSpans = spansByLine(source, highlights);
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(lineSpans[0]).toEqual([
|
|
25
|
+
{ start: 0, end: 5, group: "keyword" },
|
|
26
|
+
{ start: 6, end: 7, group: "variable" },
|
|
27
|
+
]);
|
|
28
|
+
expect(lineSpans[1]).toEqual([{ start: 0, end: 3, group: "variable" }]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("spreads a capture that crosses newlines onto every line it covers", () => {
|
|
32
|
+
// Arrange - one capture spanning both lines, as a block comment would
|
|
33
|
+
const source = "aa\nbb";
|
|
34
|
+
const highlights: SimpleHighlight[] = [[0, 5, "comment"]];
|
|
35
|
+
|
|
36
|
+
// Act
|
|
37
|
+
const lineSpans = spansByLine(source, highlights);
|
|
38
|
+
|
|
39
|
+
// Assert
|
|
40
|
+
expect(lineSpans[0]).toEqual([{ start: 0, end: 2, group: "comment" }]);
|
|
41
|
+
expect(lineSpans[1]).toEqual([{ start: 0, end: 2, group: "comment" }]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a capture ending past the source clamps to its end instead of walking forever", () => {
|
|
45
|
+
// Arrange - offsets past the string, as byte offsets over a multi-byte line report
|
|
46
|
+
const source = "a│b\ncc";
|
|
47
|
+
const highlights: SimpleHighlight[] = [
|
|
48
|
+
[0, 9, "string"],
|
|
49
|
+
[12, 20, "comment"],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Act
|
|
53
|
+
const lineSpans = spansByLine(source, highlights);
|
|
54
|
+
|
|
55
|
+
// Assert - the covered text is colored to the end and the second capture paints nothing
|
|
56
|
+
expect(lineSpans[0]).toEqual([{ start: 0, end: 3, group: "string" }]);
|
|
57
|
+
expect(lineSpans[1]).toEqual([{ start: 0, end: 2, group: "string" }]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("resolves overlapping captures with later winning", () => {
|
|
61
|
+
// Arrange
|
|
62
|
+
const source = "foo";
|
|
63
|
+
const highlights: SimpleHighlight[] = [
|
|
64
|
+
[0, 3, "variable"],
|
|
65
|
+
[0, 3, "type"],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
const lineSpans = spansByLine(source, highlights);
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
expect(lineSpans[0]).toEqual([{ start: 0, end: 3, group: "type" }]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("highlightJobs", () => {
|
|
77
|
+
test("reconstructs old and new sides of a hunk with row indices", () => {
|
|
78
|
+
// Arrange
|
|
79
|
+
const rows = [
|
|
80
|
+
row("file", "src/x.ts", "src/x.ts"),
|
|
81
|
+
row("hunk", "@@ -1,2 +1,2 @@"),
|
|
82
|
+
row("ctx", "const a = 1;"),
|
|
83
|
+
row("del", "const b = 2;"),
|
|
84
|
+
row("add", "const b = 3;"),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// Act
|
|
88
|
+
const jobs = highlightJobs(rows);
|
|
89
|
+
|
|
90
|
+
// Assert
|
|
91
|
+
// old side = context + deletion; new side = context + addition
|
|
92
|
+
expect(jobs).toEqual([
|
|
93
|
+
{ filetype: "typescript", source: "const a = 1;\nconst b = 2;", rowIndexByLine: [2, 3] },
|
|
94
|
+
{ filetype: "typescript", source: "const a = 1;\nconst b = 3;", rowIndexByLine: [2, 4] },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("skips files with no known filetype", () => {
|
|
99
|
+
// Arrange
|
|
100
|
+
const rows = [
|
|
101
|
+
row("file", "notes/thing.unknownext", "notes/thing.unknownext"),
|
|
102
|
+
row("hunk", "@@"),
|
|
103
|
+
row("add", "whatever"),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// Act
|
|
107
|
+
const jobs = highlightJobs(rows);
|
|
108
|
+
|
|
109
|
+
// Assert
|
|
110
|
+
expect(jobs).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
});
|