Skip to content

Commit 95d7e20

Browse files
committed
fix: let the diff pane own active-hunk scrolling
1 parent d5cfa1f commit 95d7e20

2 files changed

Lines changed: 29 additions & 31 deletions

File tree

src/ui/App.tsx

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { MouseButton, type KeyEvent, type MouseEvent as TuiMouseEvent, type ScrollBoxRenderable } from "@opentui/core";
22
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
3-
import { Suspense, lazy, startTransition, useDeferredValue, useEffect, useLayoutEffect, useRef, useState } from "react";
3+
import { Suspense, lazy, startTransition, useDeferredValue, useEffect, useRef, useState } from "react";
44
import type { AppBootstrap, LayoutMode } from "../core/types";
55
import { MenuBar } from "./components/chrome/MenuBar";
66
import { MENU_ORDER, buildMenuSpecs, menuWidth, nextMenuItemIndex, type MenuEntry, type MenuId } from "./components/chrome/menu";
@@ -133,29 +133,13 @@ export function App({ bootstrap, onQuit = () => process.exit(0) }: { bootstrap:
133133
setSelectedHunkIndex((current) => clamp(current, 0, maxIndex));
134134
}, [selectedFile]);
135135

136-
useLayoutEffect(() => {
136+
useEffect(() => {
137137
if (!selectedFile) {
138138
return;
139139
}
140140

141-
const scrollSelectionIntoView = () => {
142-
filesScrollRef.current?.scrollChildIntoView(fileRowId(selectedFile.id));
143-
if (selectedFile.metadata.hunks[selectedHunkIndex]) {
144-
diffScrollRef.current?.scrollChildIntoView(diffHunkId(selectedFile.id, selectedHunkIndex));
145-
return;
146-
}
147-
148-
diffScrollRef.current?.scrollChildIntoView(diffSectionId(selectedFile.id));
149-
};
150-
151-
// Selection changes can race with section windowing, so retry briefly while the new target mounts.
152-
scrollSelectionIntoView();
153-
const retryDelays = [0, 16, 48];
154-
const timeouts = retryDelays.map((delay) => setTimeout(scrollSelectionIntoView, delay));
155-
return () => {
156-
timeouts.forEach((timeout) => clearTimeout(timeout));
157-
};
158-
}, [selectedFile, selectedHunkIndex]);
141+
filesScrollRef.current?.scrollChildIntoView(fileRowId(selectedFile.id));
142+
}, [selectedFile]);
159143

160144
useEffect(() => {
161145
// Dismissed notes are hunk-local, so reset them when the review focus moves.
@@ -170,23 +154,13 @@ export function App({ bootstrap, onQuit = () => process.exit(0) }: { bootstrap:
170154
}
171155

172156
filesScrollRef.current?.scrollChildIntoView(fileRowId(nextCursor.fileId));
173-
diffScrollRef.current?.scrollChildIntoView(diffHunkId(nextCursor.fileId, nextCursor.hunkIndex));
174-
175157
setSelectedFileId(nextCursor.fileId);
176158
setSelectedHunkIndex(nextCursor.hunkIndex);
177159
};
178160

179161
/** Jump the review stream to a file and optionally a specific hunk within it. */
180162
const jumpToFile = (fileId: string, nextHunkIndex = 0) => {
181163
filesScrollRef.current?.scrollChildIntoView(fileRowId(fileId));
182-
const nextFile =
183-
filteredFiles.find((file) => file.id === fileId) ?? bootstrap.changeset.files.find((file) => file.id === fileId);
184-
if (nextFile?.metadata.hunks[nextHunkIndex]) {
185-
diffScrollRef.current?.scrollChildIntoView(diffHunkId(fileId, nextHunkIndex));
186-
} else {
187-
diffScrollRef.current?.scrollChildIntoView(diffSectionId(fileId));
188-
}
189-
190164
setSelectedFileId(fileId);
191165
setSelectedHunkIndex(nextHunkIndex);
192166
};

src/ui/components/panes/DiffPane.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { ScrollBoxRenderable } from "@opentui/core";
2-
import { useCallback, useEffect, useMemo, useState, type RefObject } from "react";
2+
import { useCallback, useEffect, useLayoutEffect, useMemo, useState, type RefObject } from "react";
33
import type { AgentAnnotation, DiffFile, LayoutMode } from "../../../core/types";
44
import type { VisibleAgentNote } from "../../lib/agentAnnotations";
55
import { estimateDiffBodyRows } from "../../lib/sectionHeights";
6+
import { diffHunkId, diffSectionId } from "../../lib/ids";
67
import type { AppTheme } from "../../themes";
78
import { DiffSection } from "./DiffSection";
89
import { DiffSectionPlaceholder } from "./DiffSectionPlaceholder";
@@ -184,6 +185,29 @@ export function DiffPane({
184185
return next;
185186
}, [adjacentPrefetchFileIds, selectedFileId, visibleViewportFileIds, windowingEnabled]);
186187

188+
const selectedFile = selectedFileId ? files.find((file) => file.id === selectedFileId) : undefined;
189+
const selectedAnchorId = selectedFile
190+
? (selectedFile.metadata.hunks[selectedHunkIndex] ? diffHunkId(selectedFile.id, selectedHunkIndex) : diffSectionId(selectedFile.id))
191+
: null;
192+
193+
useLayoutEffect(() => {
194+
if (!selectedAnchorId) {
195+
return;
196+
}
197+
198+
const scrollSelectionIntoView = () => {
199+
scrollRef.current?.scrollChildIntoView(selectedAnchorId);
200+
};
201+
202+
// Run after this pane renders the selected section/hunk, then retry briefly while layout settles.
203+
scrollSelectionIntoView();
204+
const retryDelays = [0, 16, 48];
205+
const timeouts = retryDelays.map((delay) => setTimeout(scrollSelectionIntoView, delay));
206+
return () => {
207+
timeouts.forEach((timeout) => clearTimeout(timeout));
208+
};
209+
}, [selectedAnchorId, scrollRef]);
210+
187211
return (
188212
<box
189213
style={{

0 commit comments

Comments
 (0)