diff --git a/src/ui/components/panes/DiffPane.tsx b/src/ui/components/panes/DiffPane.tsx index 26c1f3b..9026196 100644 --- a/src/ui/components/panes/DiffPane.tsx +++ b/src/ui/components/panes/DiffPane.tsx @@ -10,6 +10,7 @@ import { } from "react"; import type { DiffFile, LayoutMode } from "../../../core/types"; import type { VisibleAgentNote } from "../../lib/agentAnnotations"; +import { computeHunkRevealScrollTop } from "../../lib/hunkScroll"; import { measureDiffSectionMetrics } from "../../lib/sectionHeights"; import { diffHunkId, diffSectionId } from "../../lib/ids"; import type { AppTheme } from "../../themes"; @@ -193,7 +194,14 @@ export function DiffPane({ return next; } - for (const fileId of visibleViewportFileIds) { + const fileIdsToMeasure = new Set(visibleViewportFileIds); + // Always measure the selected file with its real note rows so hunk navigation can compute + // accurate bounds even before the file scrolls into the visible viewport. + if (selectedFileId) { + fileIdsToMeasure.add(selectedFileId); + } + + for (const fileId of fileIdsToMeasure) { const visibleNotes = allAgentNotesByFile.get(fileId); if (visibleNotes && visibleNotes.length > 0) { next.set(fileId, visibleNotes); @@ -201,7 +209,7 @@ export function DiffPane({ } return next; - }, [allAgentNotesByFile, showAgentNotes, visibleViewportFileIds]); + }, [allAgentNotesByFile, selectedFileId, showAgentNotes, visibleViewportFileIds]); const sectionMetrics = useMemo( () => @@ -278,43 +286,52 @@ export function DiffPane({ ? diffHunkId(selectedFile.id, selectedHunkIndex) : diffSectionId(selectedFile.id) : null; - const selectedEstimatedScrollTop = useMemo(() => { - if (!selectedFile || selectedFileIndex < 0) { + const selectedEstimatedHunkBounds = useMemo(() => { + if (!selectedFile || selectedFileIndex < 0 || selectedFile.metadata.hunks.length === 0) { return null; } - let top = 0; + // Convert the selected hunk's file-local bounds into absolute scrollbox coordinates by adding + // the accumulated section chrome and earlier file heights. + let sectionTop = 0; for (let index = 0; index < selectedFileIndex; index += 1) { - top += (index > 0 ? 1 : 0) + 1 + (estimatedBodyHeights[index] ?? 0); + sectionTop += (index > 0 ? 1 : 0) + 1 + (estimatedBodyHeights[index] ?? 0); } if (selectedFileIndex > 0) { - top += 1; + sectionTop += 1; } - top += 1; + sectionTop += 1; - if (selectedFile.metadata.hunks.length > 0) { - const clampedHunkIndex = Math.max( - 0, - Math.min(selectedHunkIndex, selectedFile.metadata.hunks.length - 1), - ); - top += sectionMetrics[selectedFileIndex]?.hunkAnchorRows.get(clampedHunkIndex) ?? 0; + const clampedHunkIndex = Math.max( + 0, + Math.min(selectedHunkIndex, selectedFile.metadata.hunks.length - 1), + ); + const hunkBounds = sectionMetrics[selectedFileIndex]?.hunkBounds.get(clampedHunkIndex); + if (!hunkBounds) { + return null; } - return top; + return { + top: sectionTop + hunkBounds.top, + height: hunkBounds.height, + startRowId: hunkBounds.startRowId, + endRowId: hunkBounds.endRowId, + }; }, [estimatedBodyHeights, sectionMetrics, selectedFile, selectedFileIndex, selectedHunkIndex]); - // Track the previous selected anchor to detect actual selection changes + // Track the previous selected anchor to detect actual selection changes. const prevSelectedAnchorIdRef = useRef(null); useLayoutEffect(() => { - if (!selectedAnchorId) { + if (!selectedAnchorId && !selectedEstimatedHunkBounds) { prevSelectedAnchorIdRef.current = null; return; } - // Only auto-scroll when the selection actually changes, not when metrics update during scrolling + // Only auto-scroll when the selection actually changes, not when metrics update during + // scrolling or when the selected section refines its measured bounds. const isSelectionChange = prevSelectedAnchorIdRef.current !== selectedAnchorId; prevSelectedAnchorIdRef.current = selectedAnchorId; @@ -328,15 +345,43 @@ export function DiffPane({ return; } - // In the common no-wrap/no-note path we can estimate the selected hunk row and keep it - // comfortably below the top edge instead of merely making it barely visible. - if (!wrapLines && visibleAgentNotesByFile.size === 0 && selectedEstimatedScrollTop !== null) { - const topPaddingRows = Math.max(2, Math.floor(scrollViewport.height * 0.25)); - scrollBox.scrollTo(Math.max(0, selectedEstimatedScrollTop - topPaddingRows)); + const viewportHeight = Math.max(scrollViewport.height, scrollBox.viewport.height ?? 0); + const preferredTopPadding = Math.max(2, Math.floor(viewportHeight * 0.25)); + + if (selectedEstimatedHunkBounds) { + const viewportTop = scrollBox.viewport.y; + const currentScrollTop = scrollBox.scrollTop; + const startRow = scrollBox.content.findDescendantById( + selectedEstimatedHunkBounds.startRowId, + ); + const endRow = scrollBox.content.findDescendantById(selectedEstimatedHunkBounds.endRowId); + + // Prefer exact mounted bounds when both edges are available. If only one edge has mounted + // so far, fall back to the planned bounds as one atomic estimate instead of mixing sources. + const renderedTop = startRow ? currentScrollTop + (startRow.y - viewportTop) : null; + const renderedBottom = endRow + ? currentScrollTop + (endRow.y + endRow.height - viewportTop) + : null; + const renderedBoundsReady = renderedTop !== null && renderedBottom !== null; + const hunkTop = renderedBoundsReady ? renderedTop : selectedEstimatedHunkBounds.top; + const hunkHeight = renderedBoundsReady + ? Math.max(0, renderedBottom - renderedTop) + : selectedEstimatedHunkBounds.height; + + scrollBox.scrollTo( + computeHunkRevealScrollTop({ + hunkTop, + hunkHeight, + preferredTopPadding, + viewportHeight, + }), + ); return; } - scrollBox.scrollChildIntoView(selectedAnchorId); + if (selectedAnchorId) { + scrollBox.scrollChildIntoView(selectedAnchorId); + } }; // Run after this pane renders the selected section/hunk, then retry briefly while layout settles. @@ -346,14 +391,7 @@ export function DiffPane({ return () => { timeouts.forEach((timeout) => clearTimeout(timeout)); }; - }, [ - scrollRef, - scrollViewport.height, - selectedAnchorId, - selectedEstimatedScrollTop, - visibleAgentNotesByFile.size, - wrapLines, - ]); + }, [scrollRef, scrollViewport.height, selectedAnchorId, selectedEstimatedHunkBounds]); // Configure scroll step size to scroll exactly 1 line per step useEffect(() => { diff --git a/src/ui/diff/PierreDiffView.tsx b/src/ui/diff/PierreDiffView.tsx index f5fbed7..3e4b408 100644 --- a/src/ui/diff/PierreDiffView.tsx +++ b/src/ui/diff/PierreDiffView.tsx @@ -2,8 +2,10 @@ import { useMemo } from "react"; import type { DiffFile, LayoutMode } from "../../core/types"; import { AgentInlineNote, AgentInlineNoteGuideCap } from "../components/panes/AgentInlineNote"; import type { VisibleAgentNote } from "../lib/agentAnnotations"; +import { reviewRowId } from "../lib/ids"; import type { AppTheme } from "../themes"; import { buildSplitRows, buildStackRows } from "./pierre"; +import { plannedReviewRowVisible } from "./plannedReviewRows"; import { buildReviewRenderPlan } from "./reviewRenderPlan"; import { diffMessage, DiffRowView, findMaxLineNumber, fitText } from "./renderRows"; import { useHighlightedDiff } from "./useHighlightedDiff"; @@ -92,51 +94,63 @@ export function PierreDiffView({ const content = ( {plannedRows.map((plannedRow) => { + // Mirror the same visibility/id decisions used by the scroll-bound helpers so the mounted + // tree can be measured by hunk later. + const rowId = reviewRowId(plannedRow.key); + const visible = plannedReviewRowVisible(plannedRow, { + showHunkHeaders, + layout, + width, + }); + + if (!visible) { + return null; + } + if (plannedRow.kind === "inline-note") { return ( - + + + ); } if (plannedRow.kind === "note-guide-cap") { return ( - + + + ); } return ( - + + + ); })} diff --git a/src/ui/diff/plannedReviewRows.ts b/src/ui/diff/plannedReviewRows.ts new file mode 100644 index 0000000..b82b275 --- /dev/null +++ b/src/ui/diff/plannedReviewRows.ts @@ -0,0 +1,114 @@ +import type { LayoutMode } from "../../core/types"; +import { measureAgentInlineNoteHeight } from "../components/panes/AgentInlineNote"; +import { reviewRowId } from "../lib/ids"; +import type { PlannedReviewRow } from "./reviewRenderPlan"; + +/** Layout inputs needed to turn one planned review row into concrete terminal height. */ +export interface PlannedReviewRowLayoutOptions { + showHunkHeaders: boolean; + layout: Exclude; + width: number; +} + +/** + * Visible bounds for one hunk within a file section body. + * + * The row ids let DiffPane upgrade from planned measurements to exact mounted measurements later. + */ +export interface PlannedHunkBounds { + top: number; + height: number; + startRowId: string; + endRowId: string; +} + +/** Return whether this planned row should count toward a hunk's own visible extent. */ +function rowContributesToHunkBounds(row: PlannedReviewRow) { + // Collapsed gap rows belong between hunks, so they affect total section height but not a hunk's + // own visible extent. + return !(row.kind === "diff-row" && row.row.type === "collapsed"); +} + +/** Measure how many terminal rows one planned review row will occupy once rendered. */ +export function plannedReviewRowHeight( + row: PlannedReviewRow, + { showHunkHeaders, layout, width }: PlannedReviewRowLayoutOptions, +) { + if (row.kind === "inline-note") { + return measureAgentInlineNoteHeight({ + annotation: row.annotation, + anchorSide: row.anchorSide, + layout, + width, + }); + } + + if (row.kind === "note-guide-cap") { + return 1; + } + + if (row.row.type === "hunk-header") { + return showHunkHeaders ? 1 : 0; + } + + return 1; +} + +/** Check whether a planned row will produce any visible output at all. */ +export function plannedReviewRowVisible( + row: PlannedReviewRow, + options: PlannedReviewRowLayoutOptions, +) { + return plannedReviewRowHeight(row, options) > 0; +} + +/** + * Walk one file's planned rows and derive both section metrics and hunk-local bounds. + * + * `top` is measured in section-body rows, so callers can add the file section offset later. + */ +export function measurePlannedHunkBounds( + plannedRows: PlannedReviewRow[], + options: PlannedReviewRowLayoutOptions, +) { + const hunkAnchorRows = new Map(); + const hunkBounds = new Map(); + let bodyHeight = 0; + + for (const row of plannedRows) { + if (row.kind === "diff-row" && row.anchorId && !hunkAnchorRows.has(row.hunkIndex)) { + // Track the renderer's anchor row separately from the full hunk bounds so navigation can + // still target the same semantic row when headers are hidden. + hunkAnchorRows.set(row.hunkIndex, bodyHeight); + } + + const rowHeight = plannedReviewRowHeight(row, options); + + if (rowHeight > 0 && rowContributesToHunkBounds(row)) { + const rowId = reviewRowId(row.key); + const existingBounds = hunkBounds.get(row.hunkIndex); + + if (existingBounds) { + // Extend the current hunk through the latest visible row that belongs to it. + existingBounds.endRowId = rowId; + existingBounds.height += rowHeight; + } else { + // Seed the first visible row for this hunk; later rows will widen the bounds. + hunkBounds.set(row.hunkIndex, { + top: bodyHeight, + height: rowHeight, + startRowId: rowId, + endRowId: rowId, + }); + } + } + + bodyHeight += rowHeight; + } + + return { + bodyHeight, + hunkAnchorRows, + hunkBounds, + }; +} diff --git a/src/ui/lib/hunkScroll.ts b/src/ui/lib/hunkScroll.ts new file mode 100644 index 0000000..7057cd2 --- /dev/null +++ b/src/ui/lib/hunkScroll.ts @@ -0,0 +1,35 @@ +/** + * Pick a scroll target that keeps the selected hunk readable. + * + * If the whole hunk fits, keep all of it in view. Otherwise bias toward showing the top of the + * hunk with a little breathing room. + */ +export function computeHunkRevealScrollTop({ + hunkTop, + hunkHeight, + preferredTopPadding, + viewportHeight, +}: { + hunkTop: number; + hunkHeight: number; + preferredTopPadding: number; + viewportHeight: number; +}) { + const clampedTop = Math.max(0, hunkTop); + const clampedHeight = Math.max(0, hunkHeight); + const clampedViewportHeight = Math.max(0, viewportHeight); + const desiredTop = Math.max(0, clampedTop - Math.max(0, preferredTopPadding)); + + if (clampedViewportHeight === 0) { + return desiredTop; + } + + if (clampedHeight <= clampedViewportHeight) { + // Preserve the preferred top padding when possible, but never at the cost of clipping the end + // of a hunk that would otherwise fit completely on screen. + const minimumTopForFullHunk = Math.max(0, clampedTop + clampedHeight - clampedViewportHeight); + return Math.max(desiredTop, minimumTopForFullHunk); + } + + return desiredTop; +} diff --git a/src/ui/lib/ids.ts b/src/ui/lib/ids.ts index 0f0bb29..657228d 100644 --- a/src/ui/lib/ids.ts +++ b/src/ui/lib/ids.ts @@ -12,3 +12,8 @@ export function diffSectionId(fileId: string) { export function diffHunkId(fileId: string, hunkIndex: number) { return `diff-hunk:${fileId}:${hunkIndex}`; } + +/** Build the stable id for one presentational review row in the main diff stream. */ +export function reviewRowId(rowKey: string) { + return `review-row:${rowKey}`; +} diff --git a/src/ui/lib/sectionHeights.ts b/src/ui/lib/sectionHeights.ts index 86139e2..1567b51 100644 --- a/src/ui/lib/sectionHeights.ts +++ b/src/ui/lib/sectionHeights.ts @@ -1,13 +1,15 @@ import type { DiffFile, LayoutMode } from "../../core/types"; -import { measureAgentInlineNoteHeight } from "../components/panes/AgentInlineNote"; import { buildSplitRows, buildStackRows } from "../diff/pierre"; -import { buildReviewRenderPlan, type PlannedReviewRow } from "../diff/reviewRenderPlan"; +import { measurePlannedHunkBounds, type PlannedHunkBounds } from "../diff/plannedReviewRows"; +import { buildReviewRenderPlan } from "../diff/reviewRenderPlan"; import type { VisibleAgentNote } from "./agentAnnotations"; import type { AppTheme } from "../themes"; +/** Cached placeholder sizing and hunk navigation metrics for one file section. */ export interface DiffSectionMetrics { bodyHeight: number; hunkAnchorRows: Map; + hunkBounds: Map; } const NOTE_AWARE_SECTION_METRICS_CACHE = new WeakMap< @@ -15,6 +17,7 @@ const NOTE_AWARE_SECTION_METRICS_CACHE = new WeakMap< Map >(); +/** Build the same planned rows the renderer will consume, but without requiring mounted UI. */ function buildBasePlannedRows( file: DiffFile, layout: Exclude, @@ -34,35 +37,9 @@ function buildBasePlannedRows( }); } -function plannedRowHeight( - row: PlannedReviewRow, - showHunkHeaders: boolean, - layout: Exclude, - width: number, -) { - if (row.kind === "inline-note") { - return measureAgentInlineNoteHeight({ - annotation: row.annotation, - anchorSide: row.anchorSide, - layout, - width, - }); - } - - if (row.kind === "note-guide-cap") { - return 1; - } - - if (row.row.type === "hunk-header") { - return showHunkHeaders ? 1 : 0; - } - - return 1; -} - /** * Measure one file section from the same render plan used by PierreDiffView. - * This drives the no-wrap/no-note windowing path, where every visible planned row is one terminal row. + * This drives the windowed review stream and keeps scrolling and rendering aligned. */ export function measureDiffSectionMetrics( file: DiffFile, @@ -76,6 +53,7 @@ export function measureDiffSectionMetrics( return { bodyHeight: 1, hunkAnchorRows: new Map(), + hunkBounds: new Map(), }; } @@ -89,21 +67,13 @@ export function measureDiffSectionMetrics( } const plannedRows = buildBasePlannedRows(file, layout, showHunkHeaders, theme, visibleAgentNotes); - const hunkAnchorRows = new Map(); - let bodyHeight = 0; - - for (const row of plannedRows) { - if (row.kind === "diff-row" && row.anchorId && !hunkAnchorRows.has(row.hunkIndex)) { - hunkAnchorRows.set(row.hunkIndex, bodyHeight); - } - - bodyHeight += plannedRowHeight(row, showHunkHeaders, layout, width); - } - - const metrics = { - bodyHeight, - hunkAnchorRows, - }; + // Reuse the same bounds pass as the live renderer so placeholder sizing and navigation math stay + // in lock-step with what the user actually sees. + const metrics = measurePlannedHunkBounds(plannedRows, { + showHunkHeaders, + layout, + width, + }); if (visibleAgentNotes.length > 0) { const cachedByNotes = NOTE_AWARE_SECTION_METRICS_CACHE.get(visibleAgentNotes) ?? new Map(); diff --git a/test/ui-components.test.tsx b/test/ui-components.test.tsx index eff30ad..9331e33 100644 --- a/test/ui-components.test.tsx +++ b/test/ui-components.test.tsx @@ -130,6 +130,37 @@ function createMultiHunkDiffFile(id: string, path: string) { return createDiffFile(id, path, before, after); } +function createViewportSizedBottomHunkDiffFile(id: string, path: string) { + const beforeLines = Array.from( + { length: 20 }, + (_, index) => `export const line${index + 1} = ${index + 1};`, + ); + const afterLines = [...beforeLines]; + + afterLines[1] = "export const line2 = 200;"; + afterLines[13] = "export const line14 = 1400;"; + afterLines[14] = "export const line15 = 1500;"; + afterLines[15] = "export const line16 = 1600;"; + + return createDiffFile(id, path, lines(...beforeLines), lines(...afterLines)); +} + +function createWrappedViewportSizedBottomHunkDiffFile(id: string, path: string) { + const beforeLines = Array.from( + { length: 20 }, + (_, index) => `export const line${index + 1} = ${index + 1};`, + ); + const afterLines = [...beforeLines]; + + afterLines[1] = "export const line2 = 200;"; + afterLines[13] = + "export const line14 = 'this is a long wrapped replacement for line 14 in the selected hunk';"; + afterLines[14] = + "export const line15 = 'this is a long wrapped replacement for line 15 in the selected hunk';"; + + return createDiffFile(id, path, lines(...beforeLines), lines(...afterLines)); +} + function createDiffPaneProps( files: DiffFile[], theme = resolveTheme("midnight", null), @@ -436,6 +467,128 @@ describe("UI components", () => { } }); + test("DiffPane keeps a viewport-sized selected hunk fully visible when it fits", async () => { + const theme = resolveTheme("midnight", null); + const props = createDiffPaneProps( + [createViewportSizedBottomHunkDiffFile("target", "target.ts")], + theme, + { + diffContentWidth: 96, + headerLabelWidth: 48, + selectedFileId: "target", + selectedHunkIndex: 1, + separatorWidth: 92, + showHunkHeaders: false, + width: 100, + }, + ); + const setup = await testRender(, { + width: 104, + height: 12, + }); + + try { + await settleDiffPane(setup); + const frame = setup.captureCharFrame(); + + expect(frame).toContain("export const line11 = 11;"); + expect(frame).toContain("14 - export const line14 = 14;"); + expect(frame).toContain("14 + export const line14 = 1400;"); + expect(frame).toContain("16 - export const line16 = 16;"); + expect(frame).toContain("16 + export const line16 = 1600;"); + expect(frame).toContain("export const line19 = 19;"); + expect(frame).not.toContain("2 - export const line2 = 2;"); + expect(frame).not.toContain("2 + export const line2 = 200;"); + } finally { + await act(async () => { + setup.renderer.destroy(); + }); + } + }); + + test("DiffPane keeps a selected wrapped hunk fully visible when it fits", async () => { + const theme = resolveTheme("midnight", null); + const props = createDiffPaneProps( + [createWrappedViewportSizedBottomHunkDiffFile("target", "target.ts")], + theme, + { + diffContentWidth: 76, + headerLabelWidth: 40, + selectedFileId: "target", + selectedHunkIndex: 1, + separatorWidth: 72, + showHunkHeaders: false, + width: 80, + wrapLines: true, + }, + ); + const setup = await testRender(, { + width: 84, + height: 16, + }); + + try { + await settleDiffPane(setup); + const frame = setup.captureCharFrame(); + + expect(frame).toContain("11 export const line11 = 11;"); + expect(frame).toContain("14 + export const line14 = 'this is a"); + expect(frame).toContain("15 + export const line15 = 'this is a"); + expect(frame).toContain("18 export const line18 = 18;"); + expect(frame).not.toContain("2 - export const line2 = 2;"); + expect(frame).not.toContain("2 + export const line2 = 200;"); + } finally { + await act(async () => { + setup.renderer.destroy(); + }); + } + }); + + test("DiffPane keeps a selected hunk with inline notes fully visible when it fits", async () => { + const theme = resolveTheme("midnight", null); + const file = createViewportSizedBottomHunkDiffFile("target", "target.ts"); + file.agent = { + path: file.path, + summary: "target note", + annotations: [ + { + newRange: [14, 16], + summary: "Keep the selected hunk visible with its note.", + }, + ], + }; + const props = createDiffPaneProps([file], theme, { + diffContentWidth: 96, + headerLabelWidth: 48, + selectedFileId: "target", + selectedHunkIndex: 1, + separatorWidth: 92, + showAgentNotes: true, + showHunkHeaders: false, + width: 100, + }); + const setup = await testRender(, { + width: 104, + height: 20, + }); + + try { + await settleDiffPane(setup); + const frame = setup.captureCharFrame(); + + expect(frame).toContain("Keep the selected hunk visible with its note."); + expect(frame).toContain("11 export const line11 = 11;"); + expect(frame).toContain("16 + export const line16 = 1600;"); + expect(frame).toContain("export const line19 = 19;"); + expect(frame).not.toContain("2 - export const line2 = 2;"); + expect(frame).not.toContain("2 + export const line2 = 200;"); + } finally { + await act(async () => { + setup.renderer.destroy(); + }); + } + }); + test("AgentCard removes top and bottom padding while keeping the footer inside the frame", async () => { const theme = resolveTheme("midnight", null); const frame = await captureFrame( diff --git a/test/ui-lib.test.ts b/test/ui-lib.test.ts index 76b8062..d42ea21 100644 --- a/test/ui-lib.test.ts +++ b/test/ui-lib.test.ts @@ -15,6 +15,7 @@ import { } from "../src/ui/lib/agentPopover"; import { buildAppMenus } from "../src/ui/lib/appMenus"; import { fitText, padText } from "../src/ui/lib/text"; +import { computeHunkRevealScrollTop } from "../src/ui/lib/hunkScroll"; import { estimateDiffBodyRows, measureDiffSectionMetrics } from "../src/ui/lib/sectionHeights"; import { resizeSidebarWidth } from "../src/ui/lib/sidebar"; import { resolveTheme } from "../src/ui/themes"; @@ -254,6 +255,10 @@ describe("ui helpers", () => { expect(metrics.hunkAnchorRows.get(0)).toBe(1); expect(metrics.hunkAnchorRows.get(1)).toBe(3); expect(metrics.hunkAnchorRows.get(1)).toBeGreaterThan(metrics.hunkAnchorRows.get(0) ?? -1); + expect(metrics.hunkBounds.get(0)?.top).toBe(1); + expect(metrics.hunkBounds.get(0)?.height).toBe(1); + expect(metrics.hunkBounds.get(1)?.top).toBe(3); + expect(metrics.hunkBounds.get(1)?.height).toBe(1); }); test("measureDiffSectionMetrics includes visible inline note rows in split mode", () => { @@ -282,6 +287,25 @@ describe("ui helpers", () => { expect(noteMetrics.hunkAnchorRows.get(0)).toBe(baseMetrics.hunkAnchorRows.get(0)); }); + test("computeHunkRevealScrollTop keeps a hunk fully visible when it fits", () => { + expect( + computeHunkRevealScrollTop({ + hunkTop: 20, + hunkHeight: 10, + preferredTopPadding: 4, + viewportHeight: 12, + }), + ).toBe(18); + expect( + computeHunkRevealScrollTop({ + hunkTop: 20, + hunkHeight: 10, + preferredTopPadding: 4, + viewportHeight: 16, + }), + ).toBe(16); + }); + test("resolveTheme falls back by requested id and renderer mode while lazily exposing syntax styles", () => { const midnight = resolveTheme("midnight", null); const missingLight = resolveTheme("missing", "light");