Skip to content

Commit 2f61098

Browse files
committed
refactor(review): tighten expansion geometry polish
1 parent 6686128 commit 2f61098

5 files changed

Lines changed: 46 additions & 105 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ All notable user-visible changes to Hunk are documented in this file.
66

77
### Added
88

9-
- Added inline expansion for collapsed unchanged file content. Click an unchanged-context row (`▾ N unchanged lines` when expandable, otherwise the static `··· N unchanged lines ···` form) or press `e` while a hunk is selected to reveal surrounding and trailing file lines without leaving the review. The affordance is shown only for input modes that have reachable source content (`hunk diff`, `show`, `stash show`, file-pair `diff` and `difftool`, untracked files); raw `hunk patch` input still renders as before. Failed and in-flight loads surface a one-line status ("Loading…", "Could not load N unchanged lines") on the gap row. Expanded context rows are plain text in this release; syntax highlighting will follow.
9+
- Added inline expansion for collapsed unchanged file content. Click an unchanged-context row (`▾ N unchanged lines` when expandable, otherwise the static `··· N unchanged lines ···` form) or press `e` while a hunk is selected to reveal surrounding and trailing file lines without leaving the review. The affordance is shown only for input modes that have reachable source content (`hunk diff`, `show`, `stash show`, file-pair `diff` and `difftool`, untracked files); raw `hunk patch` input still renders as before. Failed and in-flight loads surface a one-line status ("Loading…", "Could not load N unchanged lines") on the gap row. Expanded context rows use the same syntax highlighting as the surrounding diff.
1010

1111
### Changed
1212

src/core/fileSource.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export type FileSourceSide = "old" | "new";
1717
export interface FileSourceFetcher {
1818
/**
1919
* Returns the file's full source text on the requested side, or `null` when
20-
* the side is not reachable (deleted side, missing path, git error). Never
21-
* rejects: callers may safely fire-and-forget the promise without handling
22-
* rejection.
20+
* the side is not reachable (deleted side, missing path, git error). Built-in
21+
* fetchers resolve `null` instead of rejecting, but UI callers still handle
22+
* custom fetcher rejection defensively.
2323
*/
2424
getFullText(side: FileSourceSide): Promise<string | null>;
2525
}

src/ui/diff/plannedReviewRows.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,25 @@ export interface PlannedHunkBounds extends VerticalBounds {
2525
export type PlannedSectionGeometry = SectionGeometry<PlannedHunkBounds>;
2626

2727
/** Return whether this planned row should count toward a hunk's own visible extent. */
28-
function rowContributesToHunkBounds(row: PlannedReviewRow) {
28+
export function plannedReviewRowContributesToHunkBounds(row: PlannedReviewRow) {
29+
if (row.kind !== "diff-row") {
30+
return true;
31+
}
32+
2933
// Collapsed gap rows sit outside hunk bodies, so they affect total section height but not a
30-
// hunk's own visible extent.
31-
return !(row.kind === "diff-row" && row.row.type === "collapsed");
34+
// hunk's own visible extent. Expanded rows share that property: they fill a gap, not a hunk.
35+
if (row.row.type === "collapsed") {
36+
return false;
37+
}
38+
39+
if (
40+
(row.row.type === "split-line" || row.row.type === "stack-line") &&
41+
row.row.isExpansionRow === true
42+
) {
43+
return false;
44+
}
45+
46+
return true;
3247
}
3348

3449
/** Measure how many terminal rows one planned review row will occupy once rendered. */
@@ -86,7 +101,7 @@ export function measurePlannedSectionGeometry(
86101

87102
const rowHeight = plannedReviewRowHeight(row, options);
88103

89-
if (rowHeight > 0 && rowContributesToHunkBounds(row)) {
104+
if (rowHeight > 0 && plannedReviewRowContributesToHunkBounds(row)) {
90105
const rowId = reviewRowId(row.key);
91106
const existingBounds = hunkBounds.get(row.hunkIndex);
92107

src/ui/hooks/useReviewController.test.tsx

Lines changed: 18 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -128,29 +128,15 @@ async function renderReviewController(initialFiles: DiffFile[]) {
128128

129129
describe("useReviewController", () => {
130130
test("reselects the first visible file when filtering hides the current selection", async () => {
131-
const controllerRef: { current: ReviewController | null } = { current: null };
132-
const setup = await testRender(
133-
<ReviewControllerHarness
134-
initialFiles={[
135-
createDiffFile(
136-
"alpha",
137-
"alpha.ts",
138-
"export const alpha = 1;\n",
139-
"export const alpha = 2;\n",
140-
),
141-
createDiffFile(
142-
"beta",
143-
"beta.ts",
144-
"export const beta = 1;\n",
145-
"export const betaValue = 2;\n",
146-
),
147-
]}
148-
onController={(nextController) => {
149-
controllerRef.current = nextController;
150-
}}
151-
/>,
152-
{ width: 80, height: 4 },
153-
);
131+
const { controllerRef, setup } = await renderReviewController([
132+
createDiffFile("alpha", "alpha.ts", "export const alpha = 1;\n", "export const alpha = 2;\n"),
133+
createDiffFile(
134+
"beta",
135+
"beta.ts",
136+
"export const beta = 1;\n",
137+
"export const betaValue = 2;\n",
138+
),
139+
]);
154140

155141
try {
156142
await flush(setup);
@@ -175,20 +161,9 @@ describe("useReviewController", () => {
175161
});
176162

177163
test("clamps the selected hunk index when files update under a soft reload", async () => {
178-
const controllerRef: { current: ReviewController | null } = { current: null };
179-
const setFilesRef: { current: ((nextFiles: DiffFile[]) => void) | null } = { current: null };
180-
const setup = await testRender(
181-
<ReviewControllerHarness
182-
initialFiles={[createTwoHunkFile()]}
183-
onController={(nextController) => {
184-
controllerRef.current = nextController;
185-
}}
186-
onSetFiles={(nextSetFiles) => {
187-
setFilesRef.current = nextSetFiles;
188-
}}
189-
/>,
190-
{ width: 80, height: 4 },
191-
);
164+
const { controllerRef, setFilesRef, setup } = await renderReviewController([
165+
createTwoHunkFile(),
166+
]);
192167

193168
try {
194169
await flush(setup);
@@ -215,24 +190,10 @@ describe("useReviewController", () => {
215190
});
216191

217192
test("live comment mutations update annotated navigation without remounting the app", async () => {
218-
const controllerRef: { current: ReviewController | null } = { current: null };
219-
const setup = await testRender(
220-
<ReviewControllerHarness
221-
initialFiles={[
222-
createDiffFile(
223-
"alpha",
224-
"alpha.ts",
225-
"export const alpha = 1;\n",
226-
"export const alpha = 2;\n",
227-
),
228-
createDiffFile("beta", "beta.ts", "export const beta = 1;\n", "export const beta = 2;\n"),
229-
]}
230-
onController={(nextController) => {
231-
controllerRef.current = nextController;
232-
}}
233-
/>,
234-
{ width: 80, height: 4 },
235-
);
193+
const { controllerRef, setup } = await renderReviewController([
194+
createDiffFile("alpha", "alpha.ts", "export const alpha = 1;\n", "export const alpha = 2;\n"),
195+
createDiffFile("beta", "beta.ts", "export const beta = 1;\n", "export const beta = 2;\n"),
196+
]);
236197

237198
try {
238199
await flush(setup);
@@ -287,16 +248,7 @@ describe("useReviewController", () => {
287248
});
288249

289250
test("batch live comments validate together and reveal the first applied hunk", async () => {
290-
const controllerRef: { current: ReviewController | null } = { current: null };
291-
const setup = await testRender(
292-
<ReviewControllerHarness
293-
initialFiles={[createTwoHunkFile()]}
294-
onController={(nextController) => {
295-
controllerRef.current = nextController;
296-
}}
297-
/>,
298-
{ width: 80, height: 4 },
299-
);
251+
const { controllerRef, setup } = await renderReviewController([createTwoHunkFile()]);
300252

301253
try {
302254
await flush(setup);
@@ -336,16 +288,7 @@ describe("useReviewController", () => {
336288
});
337289

338290
test("batch live comments do not mutate state when any target is invalid", async () => {
339-
const controllerRef: { current: ReviewController | null } = { current: null };
340-
const setup = await testRender(
341-
<ReviewControllerHarness
342-
initialFiles={[createTwoHunkFile()]}
343-
onController={(nextController) => {
344-
controllerRef.current = nextController;
345-
}}
346-
/>,
347-
{ width: 80, height: 4 },
348-
);
291+
const { controllerRef, setup } = await renderReviewController([createTwoHunkFile()]);
349292

350293
try {
351294
await flush(setup);

src/ui/lib/diffSectionGeometry.ts

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { findMaxLineNumber, findMaxLineNumberInRows } from "../diff/codeColumns"
44
import { expandCollapsedRows, type FileSourceStatus } from "../diff/expandCollapsedRows";
55
import { buildSplitRows, buildStackRows } from "../diff/pierre";
66
import { measureRenderedRowHeight } from "../diff/renderRows";
7-
import type { PlannedHunkBounds } from "../diff/plannedReviewRows";
7+
import {
8+
plannedReviewRowContributesToHunkBounds,
9+
type PlannedHunkBounds,
10+
} from "../diff/plannedReviewRows";
811
import { buildReviewRenderPlan, type PlannedReviewRow } from "../diff/reviewRenderPlan";
912
import type { SectionGeometry, VerticalBounds } from "./diffSpatial";
1013
import { reviewRowId } from "./ids";
@@ -117,26 +120,6 @@ function plannedRowHeight(
117120
);
118121
}
119122

120-
/** Return whether a measured review row should count toward the visible extent of its hunk. */
121-
function rowContributesToHunkBounds(row: PlannedReviewRow) {
122-
if (row.kind !== "diff-row") {
123-
return true;
124-
}
125-
// Collapsed gap rows sit outside hunk bodies, so they affect total section height but should not
126-
// make a selected hunk look taller than the rows that actually belong to it. Expanded rows share
127-
// that property: they fill the gap but still belong to the gap, not to either neighbor.
128-
if (row.row.type === "collapsed") {
129-
return false;
130-
}
131-
if (
132-
(row.row.type === "split-line" || row.row.type === "stack-line") &&
133-
row.row.isExpansionRow === true
134-
) {
135-
return false;
136-
}
137-
return true;
138-
}
139-
140123
/** Measure one file section from the same render plan used by PierreDiffView. */
141124
export function measureDiffSectionGeometry(
142125
file: DiffFile,
@@ -226,7 +209,7 @@ export function measureDiffSectionGeometry(
226209
}
227210
}
228211

229-
if (height > 0 && rowContributesToHunkBounds(row)) {
212+
if (height > 0 && plannedReviewRowContributesToHunkBounds(row)) {
230213
const rowId = reviewRowId(row.key);
231214
const existingBounds = hunkBounds.get(row.hunkIndex);
232215

0 commit comments

Comments
 (0)