fix: keep selected hunks fully visible when they fit#108
fix: keep selected hunks fully visible when they fit#108benvinegar wants to merge 3 commits intomainfrom
Conversation
Greptile SummaryThis PR improves hunk navigation in the diff pane so the full visible extent of a selected hunk is kept on screen, rather than just snapping its anchor row into view. It does this by deriving hunk bounds ( Key changes:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant DiffPane
participant sectionHeights
participant plannedReviewRows
participant PierreDiffView
participant hunkScroll
DiffPane->>sectionHeights: measureDiffSectionMetrics(file, layout, ...)
sectionHeights->>plannedReviewRows: measurePlannedHunkBounds(plannedRows, options)
plannedReviewRows-->>sectionHeights: { bodyHeight, hunkAnchorRows, hunkBounds }
sectionHeights-->>DiffPane: DiffSectionMetrics (with hunkBounds)
DiffPane->>DiffPane: selectedEstimatedHunkBounds = hunkBounds.get(clampedHunkIndex) + sectionTop
DiffPane->>PierreDiffView: render(plannedRows with id-wrapped boxes)
PierreDiffView-->>DiffPane: mounted DOM with reviewRowId on each box
DiffPane->>DiffPane: useLayoutEffect fires
DiffPane->>DiffPane: findDescendantById(startRowId) → renderedTop
DiffPane->>DiffPane: findDescendantById(endRowId) → renderedBottom
DiffPane->>hunkScroll: computeHunkRevealScrollTop({ hunkTop, hunkHeight, preferredTopPadding, viewportHeight })
hunkScroll-->>DiffPane: scrollTop (keeps full hunk visible)
DiffPane->>DiffPane: scrollBox.scrollTo(scrollTop)
Reviews (1): Last reviewed commit: "fix: keep selected hunks fully visible w..." | Re-trigger Greptile |
src/ui/lib/sectionHeights.ts
Outdated
| export interface DiffSectionHunkBounds { | ||
| top: number; | ||
| height: number; | ||
| startRowId: string; | ||
| endRowId: string; | ||
| } | ||
|
|
||
| export interface DiffSectionMetrics { | ||
| bodyHeight: number; | ||
| hunkAnchorRows: Map<number, number>; | ||
| hunkBounds: Map<number, DiffSectionHunkBounds>; |
There was a problem hiding this comment.
Duplicate interface definition
DiffSectionHunkBounds defined here (lines 8–13) is structurally identical to PlannedHunkBounds exported from src/ui/diff/plannedReviewRows.ts (lines 12–17). Both have the exact same four fields (top, height, startRowId, endRowId). TypeScript's structural typing makes the assignment work silently today, but these two types can drift independently in the future.
Consider re-exporting PlannedHunkBounds from plannedReviewRows.ts and using it directly in the DiffSectionMetrics interface here, removing DiffSectionHunkBounds entirely:
import type { PlannedHunkBounds } from "../diff/plannedReviewRows";
export interface DiffSectionMetrics {
bodyHeight: number;
hunkAnchorRows: Map<number, number>;
hunkBounds: Map<number, PlannedHunkBounds>;
}There was a problem hiding this comment.
Good catch. I removed the duplicate DiffSectionHunkBounds interface and now use PlannedHunkBounds directly from plannedReviewRows.ts so the bounds shape stays defined in one place.
Responded by pi using unknown-model.
src/ui/components/panes/DiffPane.tsx
Outdated
| const hunkTop = renderedTop ?? selectedEstimatedHunkBounds.top; | ||
| const hunkHeight = | ||
| renderedTop !== null && renderedBottom !== null | ||
| ? Math.max(0, renderedBottom - renderedTop) | ||
| : selectedEstimatedHunkBounds.height; |
There was a problem hiding this comment.
Partial-render fallback uses estimated height
When startRow is found in the rendered tree but endRow is not (or vice versa), hunkHeight falls back silently to selectedEstimatedHunkBounds.height:
const hunkHeight =
renderedTop !== null && renderedBottom !== null
? Math.max(0, renderedBottom - renderedTop)
: selectedEstimatedHunkBounds.height;If only one boundary is resolved (e.g. the start row has already rendered but the end row hasn't been mounted yet during the retry window), hunkTop is the live rendered position but hunkHeight is the estimated height from the plan. For a hunk whose last row contains wrapped lines or multi-row inline notes whose actual height differs from the estimate, this mixed measurement can produce a scroll target that still cuts off the bottom of the hunk on the first settle pass, until the next retry corrects it.
Consider using the estimated bounds (selectedEstimatedHunkBounds.top + selectedEstimatedHunkBounds.height) as an atomic unit — either use both live values or neither — to avoid combining a live hunkTop with a stale hunkHeight.
There was a problem hiding this comment.
Agreed. I changed the fallback to use the rendered bounds only when both boundaries are mounted; otherwise it falls back to the planned top and height together so we do not mix a live top with an estimated height.
Responded by pi using unknown-model.
065ce55 to
cb73fae
Compare
cb73fae to
07d7716
Compare
Summary
Testing