From 8ffbcc2c81d1ff558618137204c36954797d7a6f Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 14 Oct 2026 13:12:43 -0400 Subject: [PATCH] fix(tui): tidy docked diff panel spacing (#55467) --- packages/tui/src/component/patch-diff.tsx | 24 ++++++++++++++---- .../feature-plugins/system/diff-viewer.tsx | 25 ++++++++++--------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/tui/src/component/patch-diff.tsx b/packages/tui/src/component/patch-diff.tsx index 68917c88418..34146b2c5af 100644 --- a/packages/tui/src/component/patch-diff.tsx +++ b/packages/tui/src/component/patch-diff.tsx @@ -13,7 +13,7 @@ import { } from "@opentui/core" import type { JSX } from "@opentui/solid" import { useRenderer } from "@opentui/solid" -import { batch, createMemo, createSignal, For, onCleanup, Show, splitProps } from "solid-js" +import { batch, createEffect, createMemo, createSignal, For, on, onCleanup, Show, splitProps } from "solid-js" import { splitAddedPatch, splitPatchHunks, type AddedPatchChunk } from "../util/diff" import { stringWidth } from "../util/string-width" @@ -31,10 +31,12 @@ type Props = Omit void scroll?: () => ScrollBoxRenderable | undefined + /** Extra gutter columns before the line numbers, painted in each row's color. */ + gutterPadding?: number } export function PatchDiff(props: Props) { - const [local, diffProps] = splitProps(props, ["diff", "hunkFg", "lineNumberBg", "ref", "scroll"]) + const [local, diffProps] = splitProps(props, ["diff", "hunkFg", "lineNumberBg", "ref", "scroll", "gutterPadding"]) const hunks = createMemo(() => splitPatchHunks(local.diff)) const chunks = createMemo(() => { if (!local.scroll) return @@ -70,20 +72,32 @@ export function PatchDiff(props: Props) { ...[...side.getLineSigns()].filter(([line]) => line >= 0).map(([, sign]) => stringWidth(sign.after ?? "")), ), ) + // A diff creates its gutters after it mounts, so wait a few frames for them to appear. + if (sides.length === 0 && attempt < 5) return syncGutters(attempt + 1) + if (sides.length === 0) return const maxDigits = Math.max(...digits) const maxAfter = Math.max(...after) - if (!maxDigits && attempt < 2) return syncGutters(attempt + 1) - if (!maxDigits) return const width = Math.max(maxDigits, minDigits()) sides.forEach((side) => { const index = sides.indexOf(side) const signs = new Map([...side.getLineSigns()].filter(([line]) => line >= 0)) - signs.set(-1, { after: " ".repeat(maxAfter + width - digits[index]) }) + // Line -1 never renders; its sign only sets the gutter's padding columns. + signs.set(-1, { + before: " ".repeat(local.gutterPadding ?? 0) || undefined, + after: " ".repeat(maxAfter + width - digits[index]), + }) side.setLineNumbers(lineNumbers[index]) side.setLineSigns(signs) }) }) } + createEffect( + on( + () => local.gutterPadding, + () => syncGutters(), + { defer: true }, + ), + ) const register = (index: number, node: DiffRenderable) => { nodes.set(index, node) onCleanup(() => nodes.delete(index)) diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx index 8b83a1171fd..7c1bae780d8 100644 --- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx +++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx @@ -326,7 +326,7 @@ export function DiffViewerContent(props: { // The side panel sits on the slot's raised surface; full screen keeps the base background. const surface = () => (docked() ? theme.background.raised.base : theme.background.base) // Cards are inset from the pane edges; the gaps between them run edge to edge. - const inset = () => (docked() ? 1 : 2) + const inset = () => (docked() ? 0 : 2) const width = () => props.panel?.width ?? dimensions().width const mode = () => props.mode const [fileTreeEnabled, setFileTreeEnabled] = createSignal(props.preferences?.tree ?? true) @@ -337,7 +337,7 @@ export function DiffViewerContent(props: { const fileTreeWidth = createMemo(() => Math.max(FILE_TREE_MIN_WIDTH, Math.min(FILE_TREE_MAX_WIDTH, Math.floor(width() / 4))), ) - const patchPaneWidth = createMemo(() => width() - (showFileTree() ? fileTreeWidth() : 0) - (docked() ? 2 : 4)) + const patchPaneWidth = createMemo(() => width() - (showFileTree() ? fileTreeWidth() : 0) - inset() * 2) const splitAvailable = createMemo(() => patchPaneWidth() >= MIN_SPLIT_WIDTH) const [viewOverride, setViewOverride] = createSignal(storedView(props.preferences?.view)) const view = createMemo(() => @@ -839,10 +839,10 @@ export function DiffViewerContent(props: { - + Loading diff… - + {!props.sourceBase && needsBase(mode()) ? "Could not load diff. Choose a base branch from Diff source, or select Uncommitted." @@ -895,14 +895,14 @@ export function DiffViewerContent(props: { - + Committed comparison unavailable without base metadata. Choose a base branch from Diff source. - + No changes to show @@ -1001,8 +1001,8 @@ export function DiffViewerContent(props: { height={FILE_HEADER_HEIGHT} zIndex={1} backgroundColor={background()} - paddingLeft={1} - paddingRight={docked() ? 0 : 1} + paddingLeft={docked() ? 2 : 1} + paddingRight={1} paddingBottom={1} > @@ -1070,6 +1070,7 @@ export function DiffViewerContent(props: { filetype={filetype(entry.file.file)} syntaxStyle={currentSyntax()} showLineNumbers={true} + gutterPadding={docked() ? 1 : 0} width="100%" wrapMode="char" fg={theme.text.base} @@ -1109,7 +1110,7 @@ export function DiffViewerContent(props: { backgroundColor={theme.background.base} customBorderChars={{ ...EmptyBorder, horizontal: "▀" }} /> - +