@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,78 @@
|
|
|
1
|
+
/** Per-file collapse and full-file weave over the flat diff rows. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import type { DiffRow } from "./view-diff";
|
|
5
|
+
import { applyFold, weaveFullFileRows } from "./diff-fold";
|
|
6
|
+
|
|
7
|
+
function row(kind: DiffRow["kind"], text: string, extra: Partial<DiffRow> = {}): DiffRow {
|
|
8
|
+
return { kind, text, file: "a.ts", ...extra };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// a two-line change in a four-line file: line 2 swapped, lines 1/3/4 unchanged
|
|
12
|
+
const base: DiffRow[] = [
|
|
13
|
+
row("file", "a.ts"),
|
|
14
|
+
row("hunk", "@@ -1,3 +1,3 @@"),
|
|
15
|
+
row("ctx", "line one", { oldLine: 1, newLine: 1 }),
|
|
16
|
+
row("del", "old two", { oldLine: 2 }),
|
|
17
|
+
row("add", "new two", { newLine: 2 }),
|
|
18
|
+
row("ctx", "line three", { oldLine: 3, newLine: 3 }),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const contents = {
|
|
22
|
+
path: "a.ts",
|
|
23
|
+
oldContents: "line one\nold two\nline three\nline four\n",
|
|
24
|
+
newContents: "line one\nnew two\nline three\nline four\n",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("applyFold", () => {
|
|
28
|
+
test("with no fold state it returns the base rows unchanged", () => {
|
|
29
|
+
expect(applyFold(base, new Set(), new Set(), [contents])).toBe(base);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a collapsed file keeps only its file band", () => {
|
|
33
|
+
// Act
|
|
34
|
+
const folded = applyFold(base, new Set(["a.ts"]), new Set(), [contents]);
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
expect(folded).toHaveLength(1);
|
|
38
|
+
expect(folded[0]).toEqual(row("file", "a.ts"));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("an expanded file weaves the unchanged tail line the hunk never showed", () => {
|
|
42
|
+
// Act
|
|
43
|
+
const folded = applyFold(base, new Set(), new Set(["a.ts"]), [contents]);
|
|
44
|
+
|
|
45
|
+
// Assert - the hunk header is gone and "line four" (outside the hunk) now shows
|
|
46
|
+
expect(folded.some((r) => r.kind === "hunk")).toBe(false);
|
|
47
|
+
const four = folded.find((r) => r.text === "line four");
|
|
48
|
+
|
|
49
|
+
expect(four).toEqual(row("ctx", "line four", { oldLine: 4, newLine: 4 }));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("expand is a no-op without the file's full contents", () => {
|
|
53
|
+
// Act - no contents entry, so the file keeps its hunks
|
|
54
|
+
const folded = applyFold(base, new Set(), new Set(["a.ts"]), undefined);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(folded).toEqual(base);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("weaveFullFileRows", () => {
|
|
62
|
+
test("keeps the change rows and fills every unchanged line as context in order", () => {
|
|
63
|
+
// Arrange - the file's code rows (no headers)
|
|
64
|
+
const codeRows = base.slice(2);
|
|
65
|
+
|
|
66
|
+
// Act
|
|
67
|
+
const woven = weaveFullFileRows("a.ts", codeRows, contents.newContents);
|
|
68
|
+
|
|
69
|
+
// Assert - full file: ctx1, del, add, ctx3, ctx4
|
|
70
|
+
expect(woven.map((r) => `${r.kind}:${r.text}`)).toEqual([
|
|
71
|
+
"ctx:line one",
|
|
72
|
+
"del:old two",
|
|
73
|
+
"add:new two",
|
|
74
|
+
"ctx:line three",
|
|
75
|
+
"ctx:line four",
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/diff-fold.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-file fold state for the diff sheet, applied over the flat diff rows so the
|
|
3
|
+
* cursor, curation, and rendering all read one array. A collapsed file shows only
|
|
4
|
+
* its file band; an expanded file shows its whole contents with the diff woven in
|
|
5
|
+
* (unchanged lines become context rows), an "expand all lines" view.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DiffRow } from "./view-diff";
|
|
9
|
+
|
|
10
|
+
/** The full old/new contents a diff carries per file (curatable diffs do). */
|
|
11
|
+
export interface FileContents {
|
|
12
|
+
path: string;
|
|
13
|
+
newContents: string;
|
|
14
|
+
oldContents: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Split file contents into lines, dropping the single trailing newline the file carries. */
|
|
18
|
+
function contentLines(contents: string): string[] {
|
|
19
|
+
if (contents === "") return [];
|
|
20
|
+
|
|
21
|
+
return contents.replace(/\n$/, "").split("\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Weave a file's hunk rows into its full contents: unchanged lines between and
|
|
26
|
+
* around the hunks become context rows, so the whole file renders with the diff
|
|
27
|
+
* inline. `codeRows` are the file's ctx/add/del rows in order (no headers).
|
|
28
|
+
*/
|
|
29
|
+
export function weaveFullFileRows(
|
|
30
|
+
fileName: string,
|
|
31
|
+
codeRows: DiffRow[],
|
|
32
|
+
newContents: string,
|
|
33
|
+
): DiffRow[] {
|
|
34
|
+
const newLines = contentLines(newContents);
|
|
35
|
+
const woven: DiffRow[] = [];
|
|
36
|
+
// the next unchanged old/new line numbers to emit while filling gaps between hunks
|
|
37
|
+
let newCursor = 1;
|
|
38
|
+
let oldCursor = 1;
|
|
39
|
+
const fillTo = (newLine: number): void => {
|
|
40
|
+
while (newCursor < newLine) {
|
|
41
|
+
woven.push({
|
|
42
|
+
kind: "ctx",
|
|
43
|
+
text: newLines[newCursor - 1] ?? "",
|
|
44
|
+
file: fileName,
|
|
45
|
+
oldLine: oldCursor,
|
|
46
|
+
newLine: newCursor,
|
|
47
|
+
});
|
|
48
|
+
newCursor += 1;
|
|
49
|
+
oldCursor += 1;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (const row of codeRows) {
|
|
54
|
+
if (row.newLine !== undefined) {
|
|
55
|
+
fillTo(row.newLine);
|
|
56
|
+
woven.push(row);
|
|
57
|
+
newCursor = row.newLine + 1;
|
|
58
|
+
if (row.kind === "ctx") oldCursor = (row.oldLine ?? oldCursor) + 1;
|
|
59
|
+
} else {
|
|
60
|
+
// a deletion consumes an old line only; it keeps its place before the additions
|
|
61
|
+
woven.push(row);
|
|
62
|
+
oldCursor = (row.oldLine ?? oldCursor) + 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// the unchanged tail after the last hunk
|
|
66
|
+
while (newCursor <= newLines.length) {
|
|
67
|
+
woven.push({
|
|
68
|
+
kind: "ctx",
|
|
69
|
+
text: newLines[newCursor - 1] ?? "",
|
|
70
|
+
file: fileName,
|
|
71
|
+
oldLine: oldCursor,
|
|
72
|
+
newLine: newCursor,
|
|
73
|
+
});
|
|
74
|
+
newCursor += 1;
|
|
75
|
+
oldCursor += 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return woven;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Rebuild the diff rows with per-file fold state. A collapsed file keeps only its
|
|
83
|
+
* file band; an expanded file with known contents shows its whole file woven; every
|
|
84
|
+
* other file keeps its hunks. Row order and the file bands themselves are preserved.
|
|
85
|
+
*/
|
|
86
|
+
export function applyFold(
|
|
87
|
+
base: DiffRow[],
|
|
88
|
+
collapsed: ReadonlySet<string>,
|
|
89
|
+
expanded: ReadonlySet<string>,
|
|
90
|
+
files: readonly FileContents[] | undefined,
|
|
91
|
+
): DiffRow[] {
|
|
92
|
+
if (collapsed.size === 0 && expanded.size === 0) return base;
|
|
93
|
+
const contentsByPath = new Map((files ?? []).map((file) => [file.path, file]));
|
|
94
|
+
const out: DiffRow[] = [];
|
|
95
|
+
let index = 0;
|
|
96
|
+
|
|
97
|
+
while (index < base.length) {
|
|
98
|
+
const row = base[index]!;
|
|
99
|
+
|
|
100
|
+
if (row.kind !== "file") {
|
|
101
|
+
out.push(row);
|
|
102
|
+
index += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
out.push(row);
|
|
106
|
+
const file = row.file;
|
|
107
|
+
let end = index + 1;
|
|
108
|
+
|
|
109
|
+
while (end < base.length && base[end]!.kind !== "file") end += 1;
|
|
110
|
+
const body = base.slice(index + 1, end);
|
|
111
|
+
const contents = contentsByPath.get(file);
|
|
112
|
+
|
|
113
|
+
if (collapsed.has(file)) {
|
|
114
|
+
// only the file band; the body is hidden
|
|
115
|
+
} else if (expanded.has(file) && contents) {
|
|
116
|
+
const codeRows = body.filter((bodyRow) => bodyRow.kind !== "hunk");
|
|
117
|
+
|
|
118
|
+
out.push(...weaveFullFileRows(file, codeRows, contents.newContents));
|
|
119
|
+
} else {
|
|
120
|
+
out.push(...body);
|
|
121
|
+
}
|
|
122
|
+
index = end;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { unifiedDiffText, type DiffFileContents } from "@cueloop/schema";
|
|
3
|
+
import {
|
|
4
|
+
changeRejectionForRow,
|
|
5
|
+
curateDiff,
|
|
6
|
+
hunkRejectionForRow,
|
|
7
|
+
isRowRejected,
|
|
8
|
+
locateLine,
|
|
9
|
+
parseFileDiff,
|
|
10
|
+
type HunkRejection,
|
|
11
|
+
} from "./diff-hunk-curate";
|
|
12
|
+
import type { DiffRow } from "./view-diff";
|
|
13
|
+
|
|
14
|
+
const OLD = "line1\nline2\nline3\nline4\nline5\n";
|
|
15
|
+
// Two changes in one hunk: line2 modified, then a pure addition after line4.
|
|
16
|
+
const NEW = "line1\nCHANGED2\nline3\nline4\nADDED\nline5\n";
|
|
17
|
+
const FILE: DiffFileContents = {
|
|
18
|
+
path: "src/a.ts",
|
|
19
|
+
oldContents: OLD,
|
|
20
|
+
newContents: NEW,
|
|
21
|
+
status: "modified",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Two changes far apart, so the model splits into two hunks.
|
|
25
|
+
const MULTI_OLD = "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\n";
|
|
26
|
+
const MULTI_NEW = "a\nB\nc\nd\ne\nf\ng\nh\ni\nj\nK\nl\n";
|
|
27
|
+
const MULTI: DiffFileContents = {
|
|
28
|
+
path: "src/b.ts",
|
|
29
|
+
oldContents: MULTI_OLD,
|
|
30
|
+
newContents: MULTI_NEW,
|
|
31
|
+
status: "modified",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("curateDiff", () => {
|
|
35
|
+
test("no rejections reproduces the full per-file diff", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const rejections: HunkRejection[] = [];
|
|
38
|
+
|
|
39
|
+
// Act
|
|
40
|
+
const patch = curateDiff([FILE], rejections);
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
43
|
+
expect(patch).toBe(unifiedDiffText(OLD, NEW, FILE.path)!);
|
|
44
|
+
expect(patch).toContain("+CHANGED2");
|
|
45
|
+
expect(patch).toContain("+ADDED");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("whole-hunk reject drops every change in that hunk", () => {
|
|
49
|
+
// Arrange
|
|
50
|
+
const rejections: HunkRejection[] = [{ path: FILE.path, hunkIndex: 0 }];
|
|
51
|
+
|
|
52
|
+
// Act
|
|
53
|
+
const patch = curateDiff([FILE], rejections);
|
|
54
|
+
|
|
55
|
+
// Assert - the only hunk was reverted, so nothing is left to apply
|
|
56
|
+
expect(patch).toBe("");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("change-level reject keeps the other accepted change exactly", () => {
|
|
60
|
+
// Arrange - reject the line2 modification (hunkContent index 1), keep ADDED
|
|
61
|
+
const rejections: HunkRejection[] = [{ path: FILE.path, hunkIndex: 0, changeIndex: 1 }];
|
|
62
|
+
const expectedNew = "line1\nline2\nline3\nline4\nADDED\nline5\n";
|
|
63
|
+
|
|
64
|
+
// Act
|
|
65
|
+
const patch = curateDiff([FILE], rejections);
|
|
66
|
+
|
|
67
|
+
// Assert
|
|
68
|
+
expect(patch).toBe(unifiedDiffText(OLD, expectedNew, FILE.path)!);
|
|
69
|
+
expect(patch).not.toContain("CHANGED2");
|
|
70
|
+
expect(patch).toContain("+ADDED");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("change-level reject of the addition keeps the modification", () => {
|
|
74
|
+
// Arrange - reject the pure addition (hunkContent index 3), keep CHANGED2
|
|
75
|
+
const rejections: HunkRejection[] = [{ path: FILE.path, hunkIndex: 0, changeIndex: 3 }];
|
|
76
|
+
const expectedNew = "line1\nCHANGED2\nline3\nline4\nline5\n";
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
const patch = curateDiff([FILE], rejections);
|
|
80
|
+
|
|
81
|
+
// Assert
|
|
82
|
+
expect(patch).toBe(unifiedDiffText(OLD, expectedNew, FILE.path)!);
|
|
83
|
+
expect(patch).toContain("+CHANGED2");
|
|
84
|
+
expect(patch).not.toContain("ADDED");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("rejecting every change in a file yields an empty patch", () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
const rejections: HunkRejection[] = [
|
|
90
|
+
{ path: FILE.path, hunkIndex: 0, changeIndex: 1 },
|
|
91
|
+
{ path: FILE.path, hunkIndex: 0, changeIndex: 3 },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
// Act
|
|
95
|
+
const patch = curateDiff([FILE], rejections);
|
|
96
|
+
|
|
97
|
+
// Assert
|
|
98
|
+
expect(patch).toBe("");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("whole-hunk reject supersedes a change reject for the same hunk", () => {
|
|
102
|
+
// Arrange - a redundant change reject alongside the whole-hunk reject
|
|
103
|
+
const rejections: HunkRejection[] = [
|
|
104
|
+
{ path: FILE.path, hunkIndex: 0 },
|
|
105
|
+
{ path: FILE.path, hunkIndex: 0, changeIndex: 1 },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// Act
|
|
109
|
+
const patch = curateDiff([FILE], rejections);
|
|
110
|
+
|
|
111
|
+
// Assert
|
|
112
|
+
expect(patch).toBe("");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("multi-file curation keeps each file's accepted changes independent", () => {
|
|
116
|
+
// Arrange - reject file A's addition; leave file B whole
|
|
117
|
+
const rejections: HunkRejection[] = [{ path: FILE.path, hunkIndex: 0, changeIndex: 3 }];
|
|
118
|
+
|
|
119
|
+
// Act
|
|
120
|
+
const patch = curateDiff([FILE, MULTI], rejections);
|
|
121
|
+
|
|
122
|
+
// Assert
|
|
123
|
+
expect(patch).toContain("src/a.ts");
|
|
124
|
+
expect(patch).toContain("src/b.ts");
|
|
125
|
+
expect(patch).toContain("+CHANGED2");
|
|
126
|
+
expect(patch).not.toContain("+ADDED");
|
|
127
|
+
expect(patch).toContain("+B");
|
|
128
|
+
expect(patch).toContain("+K");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("rejecting one of two hunks reverts only that hunk", () => {
|
|
132
|
+
// Arrange - reject the first hunk (B), keep the second (K)
|
|
133
|
+
const rejections: HunkRejection[] = [{ path: MULTI.path, hunkIndex: 0 }];
|
|
134
|
+
const expectedNew = "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nK\nl\n";
|
|
135
|
+
|
|
136
|
+
// Act
|
|
137
|
+
const patch = curateDiff([MULTI], rejections);
|
|
138
|
+
|
|
139
|
+
// Assert
|
|
140
|
+
expect(patch).toBe(unifiedDiffText(MULTI_OLD, expectedNew, MULTI.path)!);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("locateLine and row mapping", () => {
|
|
145
|
+
test("locateLine resolves a modified new line to its change block", () => {
|
|
146
|
+
// Arrange
|
|
147
|
+
const model = parseFileDiff(FILE);
|
|
148
|
+
|
|
149
|
+
// Act - CHANGED2 is new line 2
|
|
150
|
+
const located = locateLine(model, "addition", 2);
|
|
151
|
+
|
|
152
|
+
// Assert
|
|
153
|
+
expect(located).toEqual({ hunkIndex: 0, changeIndex: 1 });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("locateLine resolves the pure addition to its change block", () => {
|
|
157
|
+
// Arrange
|
|
158
|
+
const model = parseFileDiff(FILE);
|
|
159
|
+
|
|
160
|
+
// Act - ADDED is new line 5
|
|
161
|
+
const located = locateLine(model, "addition", 5);
|
|
162
|
+
|
|
163
|
+
// Assert
|
|
164
|
+
expect(located).toEqual({ hunkIndex: 0, changeIndex: 3 });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("locateLine resolves a context line to a hunk with no change index", () => {
|
|
168
|
+
// Arrange
|
|
169
|
+
const model = parseFileDiff(FILE);
|
|
170
|
+
|
|
171
|
+
// Act - line1 is unchanged context
|
|
172
|
+
const located = locateLine(model, "addition", 1);
|
|
173
|
+
|
|
174
|
+
// Assert
|
|
175
|
+
expect(located).toEqual({ hunkIndex: 0, changeIndex: undefined });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("changeRejectionForRow targets the change under an addition row", () => {
|
|
179
|
+
// Arrange
|
|
180
|
+
const model = parseFileDiff(FILE);
|
|
181
|
+
const row: DiffRow = { kind: "add", text: "CHANGED2\n", file: FILE.path, newLine: 2 };
|
|
182
|
+
|
|
183
|
+
// Act
|
|
184
|
+
const rejection = changeRejectionForRow(FILE.path, model, row);
|
|
185
|
+
|
|
186
|
+
// Assert
|
|
187
|
+
expect(rejection).toEqual({ path: FILE.path, hunkIndex: 0, changeIndex: 1 });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("changeRejectionForRow targets the change under a deletion row", () => {
|
|
191
|
+
// Arrange
|
|
192
|
+
const model = parseFileDiff(FILE);
|
|
193
|
+
const row: DiffRow = { kind: "del", text: "line2\n", file: FILE.path, oldLine: 2 };
|
|
194
|
+
|
|
195
|
+
// Act
|
|
196
|
+
const rejection = changeRejectionForRow(FILE.path, model, row);
|
|
197
|
+
|
|
198
|
+
// Assert
|
|
199
|
+
expect(rejection).toEqual({ path: FILE.path, hunkIndex: 0, changeIndex: 1 });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("changeRejectionForRow returns null on a context row", () => {
|
|
203
|
+
// Arrange
|
|
204
|
+
const model = parseFileDiff(FILE);
|
|
205
|
+
const row: DiffRow = { kind: "ctx", text: "line1\n", file: FILE.path, oldLine: 1, newLine: 1 };
|
|
206
|
+
|
|
207
|
+
// Act
|
|
208
|
+
const rejection = changeRejectionForRow(FILE.path, model, row);
|
|
209
|
+
|
|
210
|
+
// Assert
|
|
211
|
+
expect(rejection).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("hunkRejectionForRow targets the enclosing hunk of a context row", () => {
|
|
215
|
+
// Arrange
|
|
216
|
+
const model = parseFileDiff(FILE);
|
|
217
|
+
const row: DiffRow = { kind: "ctx", text: "line1\n", file: FILE.path, oldLine: 1, newLine: 1 };
|
|
218
|
+
|
|
219
|
+
// Act
|
|
220
|
+
const rejection = hunkRejectionForRow(FILE.path, model, row);
|
|
221
|
+
|
|
222
|
+
// Assert
|
|
223
|
+
expect(rejection).toEqual({ path: FILE.path, hunkIndex: 0 });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("hunkRejectionForRow resolves the second hunk in a multi-hunk file", () => {
|
|
227
|
+
// Arrange
|
|
228
|
+
const model = parseFileDiff(MULTI);
|
|
229
|
+
const row: DiffRow = { kind: "add", text: "K\n", file: MULTI.path, newLine: 11 };
|
|
230
|
+
|
|
231
|
+
// Act
|
|
232
|
+
const rejection = hunkRejectionForRow(MULTI.path, model, row);
|
|
233
|
+
|
|
234
|
+
// Assert
|
|
235
|
+
expect(rejection).toEqual({ path: MULTI.path, hunkIndex: 1 });
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("isRowRejected", () => {
|
|
240
|
+
test("a change row is rejected when its whole hunk is rejected", () => {
|
|
241
|
+
// Arrange
|
|
242
|
+
const model = parseFileDiff(FILE);
|
|
243
|
+
const row: DiffRow = { kind: "add", text: "ADDED\n", file: FILE.path, newLine: 5 };
|
|
244
|
+
const rejections: HunkRejection[] = [{ path: FILE.path, hunkIndex: 0 }];
|
|
245
|
+
|
|
246
|
+
// Act
|
|
247
|
+
const rejected = isRowRejected(FILE.path, model, row, rejections);
|
|
248
|
+
|
|
249
|
+
// Assert
|
|
250
|
+
expect(rejected).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("a change row is rejected only by its own change-level decision", () => {
|
|
254
|
+
// Arrange
|
|
255
|
+
const model = parseFileDiff(FILE);
|
|
256
|
+
const addedRow: DiffRow = { kind: "add", text: "ADDED\n", file: FILE.path, newLine: 5 };
|
|
257
|
+
const changedRow: DiffRow = { kind: "add", text: "CHANGED2\n", file: FILE.path, newLine: 2 };
|
|
258
|
+
const rejections: HunkRejection[] = [{ path: FILE.path, hunkIndex: 0, changeIndex: 3 }];
|
|
259
|
+
|
|
260
|
+
// Act + Assert - only the addition's change is dropped
|
|
261
|
+
expect(isRowRejected(FILE.path, model, addedRow, rejections)).toBe(true);
|
|
262
|
+
expect(isRowRejected(FILE.path, model, changedRow, rejections)).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("a context row is never marked rejected", () => {
|
|
266
|
+
// Arrange
|
|
267
|
+
const model = parseFileDiff(FILE);
|
|
268
|
+
const row: DiffRow = { kind: "ctx", text: "line1\n", file: FILE.path, oldLine: 1, newLine: 1 };
|
|
269
|
+
const rejections: HunkRejection[] = [{ path: FILE.path, hunkIndex: 0 }];
|
|
270
|
+
|
|
271
|
+
// Act
|
|
272
|
+
const rejected = isRowRejected(FILE.path, model, row, rejections);
|
|
273
|
+
|
|
274
|
+
// Assert
|
|
275
|
+
expect(rejected).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("curateDiff file-state headers", () => {
|
|
280
|
+
test("an accepted created file points its old side at /dev/null", () => {
|
|
281
|
+
// Arrange - a new file has empty old contents
|
|
282
|
+
const created: DiffFileContents = {
|
|
283
|
+
path: "src/new.ts",
|
|
284
|
+
oldContents: "",
|
|
285
|
+
newContents: "one\ntwo\n",
|
|
286
|
+
status: "added",
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Act
|
|
290
|
+
const patch = curateDiff([created], []);
|
|
291
|
+
|
|
292
|
+
// Assert
|
|
293
|
+
expect(patch).toContain("--- /dev/null");
|
|
294
|
+
expect(patch).toContain("+++ b/src/new.ts");
|
|
295
|
+
expect(patch).toContain("+one");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("an accepted deleted file points its new side at /dev/null", () => {
|
|
299
|
+
// Arrange - a deleted file has empty new contents
|
|
300
|
+
const deleted: DiffFileContents = {
|
|
301
|
+
path: "src/gone.ts",
|
|
302
|
+
oldContents: "one\ntwo\n",
|
|
303
|
+
newContents: "",
|
|
304
|
+
status: "deleted",
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Act
|
|
308
|
+
const patch = curateDiff([deleted], []);
|
|
309
|
+
|
|
310
|
+
// Assert
|
|
311
|
+
expect(patch).toContain("--- a/src/gone.ts");
|
|
312
|
+
expect(patch).toContain("+++ /dev/null");
|
|
313
|
+
expect(patch).toContain("-one");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff hunk curation: turn a set of reject decisions over a working-tree diff
|
|
3
|
+
* into the curated unified-diff patch of the accepted changes only. Each file
|
|
4
|
+
* is re-parsed from its FULL old/new contents (isPartial: false), so
|
|
5
|
+
* `diffAcceptRejectHunk` can revert a whole hunk or a single change and the
|
|
6
|
+
* serialized patch keeps exact line numbers and stays applyable.
|
|
7
|
+
*
|
|
8
|
+
* A rejection is addressed by parse-model coordinates, never by rendered-row
|
|
9
|
+
* position: `hunkIndex` indexes `model.hunks`, and `changeIndex` indexes that
|
|
10
|
+
* hunk's `hunkContent` (context and change blocks combined - the same space
|
|
11
|
+
* `diffAcceptRejectHunk`'s config consumes). A cursor row is mapped to those
|
|
12
|
+
* coordinates by its absolute file line number, so it stays correct even when
|
|
13
|
+
* the stored git patch and the model split hunks differently.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { FileDiffMetadata } from "@pierre/diffs";
|
|
17
|
+
import type { HunkRejection } from "@cueloop/schema";
|
|
18
|
+
import type { DiffRow } from "./view-diff";
|
|
19
|
+
|
|
20
|
+
export { curateDiff, parseFileDiff } from "@cueloop/daemon/curate";
|
|
21
|
+
export type { HunkRejection } from "@cueloop/schema";
|
|
22
|
+
|
|
23
|
+
/** Which side of the diff a line number addresses. */
|
|
24
|
+
type DiffSide = "addition" | "deletion";
|
|
25
|
+
|
|
26
|
+
/** A line resolved onto its owning hunk, and its change block when it is one. */
|
|
27
|
+
export interface LocatedLine {
|
|
28
|
+
hunkIndex: number;
|
|
29
|
+
/** Present only when the line sits inside a change block (an add/del line). */
|
|
30
|
+
changeIndex?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a 1-based file line on one side to its hunk and, when the line is
|
|
35
|
+
* part of a change block, that block's `hunkContent` index.
|
|
36
|
+
*/
|
|
37
|
+
export function locateLine(
|
|
38
|
+
model: FileDiffMetadata,
|
|
39
|
+
side: DiffSide,
|
|
40
|
+
lineNumber: number,
|
|
41
|
+
): LocatedLine | null {
|
|
42
|
+
const index = lineNumber - 1;
|
|
43
|
+
|
|
44
|
+
for (const [hunkIndex, hunk] of model.hunks.entries()) {
|
|
45
|
+
for (const [contentIndex, content] of hunk.hunkContent.entries()) {
|
|
46
|
+
const base = side === "addition" ? content.additionLineIndex : content.deletionLineIndex;
|
|
47
|
+
const count =
|
|
48
|
+
content.type === "context"
|
|
49
|
+
? content.lines
|
|
50
|
+
: side === "addition"
|
|
51
|
+
? content.additions
|
|
52
|
+
: content.deletions;
|
|
53
|
+
|
|
54
|
+
if (count > 0 && index >= base && index < base + count) {
|
|
55
|
+
return { hunkIndex, changeIndex: content.type === "change" ? contentIndex : undefined };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The side and line number a change row addresses; null for headers/context. */
|
|
64
|
+
function changeSideLine(
|
|
65
|
+
row: Pick<DiffRow, "kind" | "oldLine" | "newLine">,
|
|
66
|
+
): { side: DiffSide; lineNumber: number } | null {
|
|
67
|
+
if (row.kind === "add" && row.newLine !== undefined)
|
|
68
|
+
return { side: "addition", lineNumber: row.newLine };
|
|
69
|
+
if (row.kind === "del" && row.oldLine !== undefined)
|
|
70
|
+
return { side: "deletion", lineNumber: row.oldLine };
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The whole-hunk rejection for the row under the cursor, or null when the row
|
|
77
|
+
* is not inside a hunk. Context rows resolve through their new-file line.
|
|
78
|
+
*/
|
|
79
|
+
export function hunkRejectionForRow(
|
|
80
|
+
path: string,
|
|
81
|
+
model: FileDiffMetadata,
|
|
82
|
+
row: Pick<DiffRow, "kind" | "oldLine" | "newLine">,
|
|
83
|
+
): HunkRejection | null {
|
|
84
|
+
const change = changeSideLine(row);
|
|
85
|
+
const located = change
|
|
86
|
+
? locateLine(model, change.side, change.lineNumber)
|
|
87
|
+
: row.newLine !== undefined
|
|
88
|
+
? locateLine(model, "addition", row.newLine)
|
|
89
|
+
: row.oldLine !== undefined
|
|
90
|
+
? locateLine(model, "deletion", row.oldLine)
|
|
91
|
+
: null;
|
|
92
|
+
|
|
93
|
+
return located ? { path, hunkIndex: located.hunkIndex } : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The change-level rejection for the row under the cursor, or null when the row
|
|
98
|
+
* carries no change (a header or context line).
|
|
99
|
+
*/
|
|
100
|
+
export function changeRejectionForRow(
|
|
101
|
+
path: string,
|
|
102
|
+
model: FileDiffMetadata,
|
|
103
|
+
row: Pick<DiffRow, "kind" | "oldLine" | "newLine">,
|
|
104
|
+
): HunkRejection | null {
|
|
105
|
+
const change = changeSideLine(row);
|
|
106
|
+
|
|
107
|
+
if (!change) return null;
|
|
108
|
+
const located = locateLine(model, change.side, change.lineNumber);
|
|
109
|
+
|
|
110
|
+
if (!located || located.changeIndex === undefined) return null;
|
|
111
|
+
|
|
112
|
+
return { path, hunkIndex: located.hunkIndex, changeIndex: located.changeIndex };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether two rejections address the same target. */
|
|
116
|
+
export function sameRejection(left: HunkRejection, right: HunkRejection): boolean {
|
|
117
|
+
return (
|
|
118
|
+
left.path === right.path &&
|
|
119
|
+
left.hunkIndex === right.hunkIndex &&
|
|
120
|
+
left.changeIndex === right.changeIndex
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Whether `rejection` drops the whole hunk that `target` sits in. */
|
|
125
|
+
export function rejectsWholeHunk(rejection: HunkRejection, target: HunkRejection): boolean {
|
|
126
|
+
return (
|
|
127
|
+
rejection.path === target.path &&
|
|
128
|
+
rejection.hunkIndex === target.hunkIndex &&
|
|
129
|
+
rejection.changeIndex === undefined
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Whether a change row is dropped by the current decisions (for dimming). */
|
|
134
|
+
export function isRowRejected(
|
|
135
|
+
path: string,
|
|
136
|
+
model: FileDiffMetadata,
|
|
137
|
+
row: Pick<DiffRow, "kind" | "oldLine" | "newLine">,
|
|
138
|
+
rejections: HunkRejection[],
|
|
139
|
+
): boolean {
|
|
140
|
+
const change = changeSideLine(row);
|
|
141
|
+
|
|
142
|
+
if (!change) return false;
|
|
143
|
+
const located = locateLine(model, change.side, change.lineNumber);
|
|
144
|
+
|
|
145
|
+
if (!located || located.changeIndex === undefined) return false;
|
|
146
|
+
|
|
147
|
+
return rejections.some(
|
|
148
|
+
(rejection) =>
|
|
149
|
+
rejection.path === path &&
|
|
150
|
+
rejection.hunkIndex === located.hunkIndex &&
|
|
151
|
+
(rejection.changeIndex === undefined || rejection.changeIndex === located.changeIndex),
|
|
152
|
+
);
|
|
153
|
+
}
|