From 43c0840aed5f082045b9d57f7aa227375bdae4ae Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Sun, 29 Mar 2026 21:58:21 -0400 Subject: [PATCH] refactor: centralize shared render constants --- src/core/constants.ts | 34 +++++++++++++++++ src/core/loaders.ts | 5 ++- src/ui/App.tsx | 18 +++++---- src/ui/components/panes/DiffPane.tsx | 12 ++++-- .../scrollbar/VerticalScrollbar.tsx | 3 +- src/ui/diff/pierre.ts | 38 +++++++++---------- 6 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 src/core/constants.ts diff --git a/src/core/constants.ts b/src/core/constants.ts new file mode 100644 index 0000000..3df7030 --- /dev/null +++ b/src/core/constants.ts @@ -0,0 +1,34 @@ +export const DIFF_CONTEXT_LINES = 3; +export const TOKENIZE_MAX_LINE_LENGTH = 1_000; +export const DEFAULT_TAB_SIZE = 2; +export const HIGHLIGHTER_PREFERRED = "shiki-wasm" as const; +export const WATCH_POLL_INTERVAL_MS = 250; + +export const UI_LAYOUT_CONSTANTS = { + BODY_PADDING: 2, + DIFF_MIN_WIDTH: 48, + DIVIDER_HIT_WIDTH: 5, + DIVIDER_WIDTH: 1, + FILES_MIN_WIDTH: 22, + SIDEBAR_DEFAULT_WIDTH: 34, +} as const; + +export const UI_SCROLL_CONSTANTS = { + DIFF_SELECTION_MIN_TOP_PADDING: 2, + SCROLLBAR_HIDE_DELAY_MS: 2_000, + SCROLL_RESTORE_RETRY_DELAYS_MS: [0, 16, 48], + VIEWPORT_OVERSCAN_ROWS: 8, +} as const; + +export const THEME_CONSTANTS = { + RESERVED_PIERRE_TOKEN_COLORS: { + dark: { + "#ff6762": "keyword", + "#5ecc71": "string", + }, + light: { + "#d52c36": "keyword", + "#199f43": "string", + }, + }, +} as const; diff --git a/src/core/loaders.ts b/src/core/loaders.ts index 6cc4042..ad9975c 100644 --- a/src/core/loaders.ts +++ b/src/core/loaders.ts @@ -7,6 +7,7 @@ import { } from "@pierre/diffs"; import { createTwoFilesPatch } from "diff"; import { resolve as resolvePath } from "node:path"; +import { DIFF_CONTEXT_LINES } from "./constants"; import { findAgentFileContext, loadAgentContext } from "./agent"; import { buildGitDiffArgs, @@ -356,9 +357,9 @@ async function loadFileDiffChangeset( cacheKey: `${rightPath}:right`, }; - const metadata = parseDiffFromFile(oldFile, newFile, { context: 3 }, true); + const metadata = parseDiffFromFile(oldFile, newFile, { context: DIFF_CONTEXT_LINES }, true); const patch = createTwoFilesPatch(displayPath, displayPath, leftText, rightText, "", "", { - context: 3, + context: DIFF_CONTEXT_LINES, }); return { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index bb010e5..85faaa2 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -16,6 +16,7 @@ import { useState, useRef, } from "react"; +import { UI_LAYOUT_CONSTANTS, WATCH_POLL_INTERVAL_MS } from "../core/constants"; import { resolveConfiguredCliInput } from "../core/config"; import { loadAppBootstrap } from "../core/loaders"; import { resolveRuntimeCliInput } from "../core/terminal"; @@ -101,11 +102,14 @@ function AppShell({ options?: { resetShell?: boolean; sourcePath?: string }, ) => Promise; }) { - const FILES_MIN_WIDTH = 22; - const DIFF_MIN_WIDTH = 48; - const BODY_PADDING = 2; - const DIVIDER_WIDTH = 1; - const DIVIDER_HIT_WIDTH = 5; + const { + BODY_PADDING, + DIFF_MIN_WIDTH, + DIVIDER_HIT_WIDTH, + DIVIDER_WIDTH, + FILES_MIN_WIDTH, + SIDEBAR_DEFAULT_WIDTH, + } = UI_LAYOUT_CONSTANTS; const renderer = useRenderer(); const terminal = useTerminalDimensions(); @@ -125,7 +129,7 @@ function AppShell({ const [showHelp, setShowHelp] = useState(false); const [focusArea, setFocusArea] = useState("files"); const [filter, setFilter] = useState(""); - const [filesPaneWidth, setFilesPaneWidth] = useState(34); + const [filesPaneWidth, setFilesPaneWidth] = useState(SIDEBAR_DEFAULT_WIDTH); const [resizeDragOriginX, setResizeDragOriginX] = useState(null); const [resizeStartWidth, setResizeStartWidth] = useState(null); const [selectedFileId, setSelectedFileId] = useState(bootstrap.changeset.files[0]?.id ?? ""); @@ -463,7 +467,7 @@ function AppShell({ } }; - const interval = setInterval(pollForChanges, 250); + const interval = setInterval(pollForChanges, WATCH_POLL_INTERVAL_MS); return () => { cancelled = true; clearInterval(interval); diff --git a/src/ui/components/panes/DiffPane.tsx b/src/ui/components/panes/DiffPane.tsx index 03b83d8..131068d 100644 --- a/src/ui/components/panes/DiffPane.tsx +++ b/src/ui/components/panes/DiffPane.tsx @@ -8,6 +8,7 @@ import { useState, type RefObject, } from "react"; +import { UI_SCROLL_CONSTANTS } from "../../../core/constants"; import type { DiffFile, LayoutMode } from "../../../core/types"; import type { VisibleAgentNote } from "../../lib/agentAnnotations"; import { computeHunkRevealScrollTop } from "../../lib/hunkScroll"; @@ -285,7 +286,7 @@ export function DiffPane({ ); const visibleViewportFileIds = useMemo(() => { - const overscanRows = 8; + const overscanRows = UI_SCROLL_CONSTANTS.VIEWPORT_OVERSCAN_ROWS; const minVisibleY = Math.max(0, scrollViewport.top - overscanRows); const maxVisibleY = scrollViewport.top + scrollViewport.height + overscanRows; let offsetY = 0; @@ -507,7 +508,7 @@ export function DiffPane({ suppressNextSelectionAutoScrollRef.current = true; // Retry across a couple of repaint cycles so the restored top-row anchor sticks // after wrapped row heights and viewport culling settle. - const retryDelays = [0, 16, 48]; + const retryDelays = UI_SCROLL_CONSTANTS.SCROLL_RESTORE_RETRY_DELAYS_MS; const timeouts = retryDelays.map((delay) => setTimeout(restoreViewportAnchor, delay)); previousWrapLinesRef.current = wrapLines; @@ -552,7 +553,10 @@ export function DiffPane({ } const viewportHeight = Math.max(scrollViewport.height, scrollBox.viewport.height ?? 0); - const preferredTopPadding = Math.max(2, Math.floor(viewportHeight * 0.25)); + const preferredTopPadding = Math.max( + UI_SCROLL_CONSTANTS.DIFF_SELECTION_MIN_TOP_PADDING, + Math.floor(viewportHeight * 0.25), + ); // When navigating comment-to-comment, scroll the inline note card near the viewport top // instead of positioning the entire hunk. Uses the same reveal function so the padding @@ -608,7 +612,7 @@ export function DiffPane({ // Run after this pane renders the selected section/hunk, then retry briefly while layout // settles across a couple of repaint cycles. scrollSelectionIntoView(); - const retryDelays = [0, 16, 48]; + const retryDelays = UI_SCROLL_CONSTANTS.SCROLL_RESTORE_RETRY_DELAYS_MS; const timeouts = retryDelays.map((delay) => setTimeout(scrollSelectionIntoView, delay)); return () => { timeouts.forEach((timeout) => clearTimeout(timeout)); diff --git a/src/ui/components/scrollbar/VerticalScrollbar.tsx b/src/ui/components/scrollbar/VerticalScrollbar.tsx index ae2edce..b62d870 100644 --- a/src/ui/components/scrollbar/VerticalScrollbar.tsx +++ b/src/ui/components/scrollbar/VerticalScrollbar.tsx @@ -8,9 +8,10 @@ import { useState, type RefObject, } from "react"; +import { UI_SCROLL_CONSTANTS } from "../../../core/constants"; import type { AppTheme } from "../../themes"; -const HIDE_DELAY_MS = 2000; +const HIDE_DELAY_MS = UI_SCROLL_CONSTANTS.SCROLLBAR_HIDE_DELAY_MS; const SCROLLBAR_WIDTH = 1; const MIN_THUMB_HEIGHT = 2; diff --git a/src/ui/diff/pierre.ts b/src/ui/diff/pierre.ts index 1fa7b4d..0df3acb 100644 --- a/src/ui/diff/pierre.ts +++ b/src/ui/diff/pierre.ts @@ -6,6 +6,12 @@ import { type FileDiffMetadata, type Hunk, } from "@pierre/diffs"; +import { + DEFAULT_TAB_SIZE, + HIGHLIGHTER_PREFERRED, + THEME_CONSTANTS, + TOKENIZE_MAX_LINE_LENGTH, +} from "../../core/constants"; import type { DiffFile } from "../../core/types"; import type { AppTheme } from "../themes"; @@ -22,12 +28,12 @@ function pierreThemeName(appearance: AppTheme["appearance"]) { const PIERRE_RENDER_OPTIONS_BY_APPEARANCE = { light: { theme: pierreThemeName("light"), - tokenizeMaxLineLength: 1_000, + tokenizeMaxLineLength: TOKENIZE_MAX_LINE_LENGTH, lineDiffType: "word-alt" as const, }, dark: { theme: pierreThemeName("dark"), - tokenizeMaxLineLength: 1_000, + tokenizeMaxLineLength: TOKENIZE_MAX_LINE_LENGTH, lineDiffType: "word-alt" as const, }, } as const; @@ -38,6 +44,7 @@ function pierreRenderOptions(appearance: AppTheme["appearance"]) { } type HighlightOptions = ReturnType; +type Highlighter = Awaited>; const highlighterOptionsByKey = new Map(); let queuedHighlightWork = Promise.resolve(); @@ -108,7 +115,7 @@ export type DiffRow = /** Replace tabs with fixed spaces so terminal cell widths stay predictable. */ function tabify(text: string) { - return text.replaceAll("\t", " "); + return text.replaceAll("\t", " ".repeat(DEFAULT_TAB_SIZE)); } /** Parse an inline CSS style string from Pierre's highlighted HAST output. */ @@ -134,19 +141,10 @@ function parseStyleValue(styleValue: unknown) { return styles; } -const RESERVED_PIERRE_TOKEN_COLORS = { - dark: { - "#ff6762": "keyword", - "#5ecc71": "string", - }, - light: { - "#d52c36": "keyword", - "#199f43": "string", - }, -} as const; +const RESERVED_PIERRE_TOKEN_COLORS = THEME_CONSTANTS.RESERVED_PIERRE_TOKEN_COLORS; /** Remap Pierre token hues that collide with diff add/remove semantics into theme-safe syntax colors. */ -function normalizeHighlightedColor(color: string | undefined, theme: AppTheme) { +function normalizeHighlightedColor(color: string | undefined, theme: AppTheme): string | undefined { if (!color) { return color; } @@ -184,7 +182,7 @@ function flattenHighlightedLine( theme: AppTheme, emphasisBg: string, fallbackText: string, -) { +): RenderSpan[] { const spans: RenderSpan[] = []; const colorVariable = theme.appearance === "light" ? "--diffs-token-light" : "--diffs-token-dark"; @@ -240,7 +238,7 @@ function makeSplitCell( rawLine: string | undefined, highlightedLine: HastNode | undefined, theme: AppTheme, -) { +): SplitLineCell { if (kind === "empty") { return { kind, @@ -284,7 +282,7 @@ function makeStackCell( rawLine: string | undefined, highlightedLine: HastNode | undefined, theme: AppTheme, -) { +): StackLineCell { const fallbackText = cleanDiffLine(rawLine); // Startup renders often build rows before highlighted HAST exists, so keep that plain-text path cheap. @@ -349,7 +347,7 @@ function trailingCollapsedLines(metadata: FileDiffMetadata) { async function prepareHighlighter( language: string | undefined, appearance: AppTheme["appearance"], -) { +): Promise { const resolvedLanguage = language ?? "text"; const cacheKey = `${appearance}:${resolvedLanguage}`; const options = @@ -364,12 +362,12 @@ async function prepareHighlighter( return getSharedHighlighter({ ...options, - preferredHighlighter: "shiki-wasm", + preferredHighlighter: HIGHLIGHTER_PREFERRED, }); } /** Queue highlight rendering so startup work stays serialized in request order. */ -function queueHighlightedDiff(run: () => HighlightedDiffCode) { +function queueHighlightedDiff(run: () => HighlightedDiffCode): Promise { const queued = queuedHighlightWork.then( () => new Promise((resolve, reject) => {