Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wrapped-moved-line-tint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Git's `color.moved` highlights are no longer lost when `wrap_lines` is on.
4 changes: 2 additions & 2 deletions src/ui/diff/renderRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ function buildWrappedSplitCell(
prefixWidth: number,
theme: AppTheme,
) {
const palette = splitCellPalette(cell.kind, theme);
const palette = splitCellPalette(cell.kind, theme, cell.moveKind);
const { gutterWidth, contentWidth } = resolveSplitCellGeometry(
width,
lineNumberDigits,
Expand Down Expand Up @@ -758,7 +758,7 @@ function buildWrappedStackCell(
prefixWidth: number,
theme: AppTheme,
) {
const palette = stackCellPalette(cell.kind, theme);
const palette = stackCellPalette(cell.kind, theme, cell.moveKind);
const { gutterWidth, contentWidth } = resolveStackCellGeometry(
width,
lineNumberDigits,
Expand Down
39 changes: 39 additions & 0 deletions test/pty/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,44 @@ export function createPtyHarness() {
return { dir };
}

/**
* Build a block that moves between two files, with Git's move detection enabled.
*
* `plainAddition` is an ordinary added line in the same diff, so tests can tell a moved tint
* from the added tint instead of only asserting that some tint was painted.
*/
function createMovedLinesRepoFixture() {
const movedBlock = [
"MOVED BLOCK ALPHA",
"MOVED BLOCK BRAVO",
"MOVED BLOCK CHARLIE",
"MOVED BLOCK DELTA",
];
const plainAddition = "brand new destination line";
const fixture = createGitRepoFixture([
{
path: "source.txt",
before: ["source header one", "source header two", ...movedBlock, "source footer"].join(
"\n",
),
after: ["source header one", "source header two", "source footer"].join("\n"),
},
{
path: "destination.txt",
before: ["destination header one", "destination header two"].join("\n"),
after: [
"destination header one",
"destination header two",
...movedBlock,
plainAddition,
].join("\n"),
},
]);

runGit(["config", "diff.colorMoved", "zebra"], fixture.dir);
return { ...fixture, movedBlock, plainAddition };
}

/** Build the long-path fixture used to verify narrow file-header layout. */
function createNarrowHeaderTestRepoFixture() {
return createGitRepoFixture([
Expand Down Expand Up @@ -1039,6 +1077,7 @@ end
createRepoExtensionFixture,
createLinkedWorktreeWatchFixture,
createLongWrapFilePair,
createMovedLinesRepoFixture,
createMultiFilePagerPatchFixture,
createMultiHunkFilePair,
createNarrowHeaderTestRepoFixture,
Expand Down
65 changes: 65 additions & 0 deletions test/pty/moved-lines.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { afterEach, describe, expect, setDefaultTimeout, test } from "bun:test";
import { DEFAULT_DARK_THEME_ID, resolveTheme } from "../../src/ui/themes";
import { createPtyHarness } from "./harness";

const harness = createPtyHarness();
const theme = resolveTheme(DEFAULT_DARK_THEME_ID, "dark");

setDefaultTimeout(20_000);

afterEach(() => {
harness.cleanup();
});

/**
* Moved-line tinting is asserted through the rendered screen rather than at the row model or
* the palette helper because both of those stayed correct while the wrapped renderer painted
* moved rows as ordinary additions and deletions. Wrapping and layout are covered as a matrix
* for the same reason: each combination reaches the palette through its own cell builder.
*/
describe("PTY moved-line coloring", () => {
for (const layout of ["stack", "split"] as const) {
for (const wrap of ["--wrap", "--no-wrap"] as const) {
test(`tints moved rows apart from ordinary added rows in ${layout} with ${wrap}`, async () => {
const fixture = harness.createMovedLinesRepoFixture();
const session = await harness.launchHunk({
args: ["diff", "--mode", layout, wrap],
cwd: fixture.dir,
cols: 160,
rows: 40,
});

try {
await session.waitForText(/MOVED BLOCK ALPHA/, { timeout: 15_000 });

const movedTinted = await session.text({
immediate: true,
only: { background: theme.movedAddedBg },
});
const addedTinted = await session.text({
immediate: true,
only: { background: theme.addedBg },
});
const removedTinted = await session.text({
immediate: true,
only: { background: theme.removedBg },
});

// Both sides of the move carry the moved tint: the deletion in source.txt and the
// addition in destination.txt.
for (const line of fixture.movedBlock) {
expect(movedTinted).toContain(line);
}
expect(addedTinted).not.toContain("MOVED BLOCK");
expect(removedTinted).not.toContain("MOVED BLOCK");

// A genuinely new line in the same diff still paints as an ordinary addition.
expect(addedTinted).toContain(fixture.plainAddition);
expect(movedTinted).not.toContain(fixture.plainAddition);
} finally {
session.close();
}
});
}
}
});
Loading