Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/core/constants.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 3 additions & 2 deletions src/core/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 11 additions & 7 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -101,11 +102,14 @@ function AppShell({
options?: { resetShell?: boolean; sourcePath?: string },
) => Promise<ReloadedSessionResult>;
}) {
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();
Expand All @@ -125,7 +129,7 @@ function AppShell({
const [showHelp, setShowHelp] = useState(false);
const [focusArea, setFocusArea] = useState<FocusArea>("files");
const [filter, setFilter] = useState("");
const [filesPaneWidth, setFilesPaneWidth] = useState(34);
const [filesPaneWidth, setFilesPaneWidth] = useState<number>(SIDEBAR_DEFAULT_WIDTH);
const [resizeDragOriginX, setResizeDragOriginX] = useState<number | null>(null);
const [resizeStartWidth, setResizeStartWidth] = useState<number | null>(null);
const [selectedFileId, setSelectedFileId] = useState(bootstrap.changeset.files[0]?.id ?? "");
Expand Down Expand Up @@ -463,7 +467,7 @@ function AppShell({
}
};

const interval = setInterval(pollForChanges, 250);
const interval = setInterval(pollForChanges, WATCH_POLL_INTERVAL_MS);
return () => {
cancelled = true;
clearInterval(interval);
Expand Down
12 changes: 8 additions & 4 deletions src/ui/components/panes/DiffPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand Down
3 changes: 2 additions & 1 deletion src/ui/components/scrollbar/VerticalScrollbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
38 changes: 18 additions & 20 deletions src/ui/diff/pierre.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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;
Expand All @@ -38,6 +44,7 @@ function pierreRenderOptions(appearance: AppTheme["appearance"]) {
}

type HighlightOptions = ReturnType<typeof getHighlighterOptions>;
type Highlighter = Awaited<ReturnType<typeof getSharedHighlighter>>;

const highlighterOptionsByKey = new Map<string, HighlightOptions>();
let queuedHighlightWork = Promise.resolve();
Expand Down Expand Up @@ -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. */
Expand All @@ -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;
}
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -240,7 +238,7 @@ function makeSplitCell(
rawLine: string | undefined,
highlightedLine: HastNode | undefined,
theme: AppTheme,
) {
): SplitLineCell {
if (kind === "empty") {
return {
kind,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -349,7 +347,7 @@ function trailingCollapsedLines(metadata: FileDiffMetadata) {
async function prepareHighlighter(
language: string | undefined,
appearance: AppTheme["appearance"],
) {
): Promise<Highlighter> {
const resolvedLanguage = language ?? "text";
const cacheKey = `${appearance}:${resolvedLanguage}`;
const options =
Expand All @@ -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<HighlightedDiffCode> {
const queued = queuedHighlightWork.then(
() =>
new Promise<HighlightedDiffCode>((resolve, reject) => {
Expand Down
Loading