Skip to content

Commit 3cd541c

Browse files
Pin the current file header while scrolling the review pane (#141)
Co-authored-by: Ben Vinegar <ben@benv.ca>
1 parent fcb5fd0 commit 3cd541c

11 files changed

Lines changed: 717 additions & 245 deletions

src/ui/App.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,26 @@ export function App({
121121
const [resizeStartWidth, setResizeStartWidth] = useState<number | null>(null);
122122
const [selectedFileId, setSelectedFileId] = useState(bootstrap.changeset.files[0]?.id ?? "");
123123
const [selectedHunkIndex, setSelectedHunkIndex] = useState(0);
124+
const [selectedFileTopAlignRequestId, setSelectedFileTopAlignRequestId] = useState(0);
124125
const [scrollToNote, setScrollToNote] = useState(false);
125126
const deferredFilter = useDeferredValue(filter);
126127

127128
const pagerMode = Boolean(bootstrap.input.options.pager);
128129
const activeTheme = resolveTheme(themeId, renderer.themeMode);
129130

130-
const jumpToFile = useCallback((fileId: string, nextHunkIndex = 0) => {
131-
filesScrollRef.current?.scrollChildIntoView(fileRowId(fileId));
132-
setSelectedFileId(fileId);
133-
setSelectedHunkIndex(nextHunkIndex);
134-
setScrollToNote(false);
135-
}, []);
131+
const jumpToFile = useCallback(
132+
(fileId: string, nextHunkIndex = 0, options?: { alignFileHeaderTop?: boolean }) => {
133+
filesScrollRef.current?.scrollChildIntoView(fileRowId(fileId));
134+
setSelectedFileId(fileId);
135+
setSelectedHunkIndex(nextHunkIndex);
136+
setScrollToNote(false);
137+
138+
if (options?.alignFileHeaderTop) {
139+
setSelectedFileTopAlignRequestId((current) => current + 1);
140+
}
141+
},
142+
[],
143+
);
136144

137145
const jumpToAnnotatedHunk = useCallback((fileId: string, nextHunkIndex = 0) => {
138146
filesScrollRef.current?.scrollChildIntoView(fileRowId(fileId));
@@ -948,7 +956,7 @@ export function App({
948956
width={clampedFilesPaneWidth}
949957
onSelectFile={(fileId) => {
950958
setFocusArea("files");
951-
jumpToFile(fileId);
959+
jumpToFile(fileId, 0, { alignFileHeaderTop: true });
952960
}}
953961
/>
954962

@@ -982,6 +990,7 @@ export function App({
982990
showHunkHeaders={showHunkHeaders}
983991
wrapLines={wrapLines}
984992
wrapToggleScrollTop={wrapToggleScrollTopRef.current}
993+
selectedFileTopAlignRequestId={selectedFileTopAlignRequestId}
985994
theme={activeTheme}
986995
width={diffPaneWidth}
987996
onOpenAgentNotesAtHunk={openAgentNotesAtHunk}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { DiffFile } from "../../../core/types";
2+
import { fileLabelParts } from "../../lib/files";
3+
import { fitText } from "../../lib/text";
4+
import type { AppTheme } from "../../themes";
5+
6+
interface DiffFileHeaderRowProps {
7+
file: DiffFile;
8+
headerLabelWidth: number;
9+
headerStatsWidth: number;
10+
theme: AppTheme;
11+
onSelect?: () => void;
12+
}
13+
14+
/** Render one file header row in the review stream or sticky overlay. */
15+
export function DiffFileHeaderRow({
16+
file,
17+
headerLabelWidth,
18+
headerStatsWidth,
19+
theme,
20+
onSelect,
21+
}: DiffFileHeaderRowProps) {
22+
const additionsText = `+${file.stats.additions}`;
23+
const deletionsText = `-${file.stats.deletions}`;
24+
const { filename, stateLabel } = fileLabelParts(file);
25+
26+
return (
27+
<box
28+
style={{
29+
width: "100%",
30+
height: 1,
31+
flexShrink: 0,
32+
flexDirection: "row",
33+
justifyContent: "space-between",
34+
paddingLeft: 1,
35+
paddingRight: 1,
36+
backgroundColor: theme.panel,
37+
}}
38+
onMouseUp={onSelect}
39+
>
40+
{/* Clicking the file header jumps the main stream selection without collapsing to a single-file view. */}
41+
<box style={{ flexDirection: "row" }}>
42+
<text fg={theme.text}>
43+
{fitText(filename, Math.max(1, headerLabelWidth - (stateLabel?.length ?? 0)))}
44+
</text>
45+
{stateLabel && <text fg={theme.muted}>{stateLabel}</text>}
46+
</box>
47+
<box
48+
style={{
49+
width: headerStatsWidth,
50+
height: 1,
51+
flexDirection: "row",
52+
justifyContent: "flex-end",
53+
}}
54+
>
55+
<text fg={theme.badgeAdded}>{additionsText}</text>
56+
<text fg={theme.muted}> </text>
57+
<text fg={theme.badgeRemoved}>{deletionsText}</text>
58+
</box>
59+
</box>
60+
);
61+
}

0 commit comments

Comments
 (0)