From 71a86995c679ddadc26b4f062edcb8dccda445fa Mon Sep 17 00:00:00 2001 From: James Long <17031+jlongster@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:38:17 +0000 Subject: [PATCH] fix(tui): preserve line number alignment --- packages/tui/src/component/patch-diff.tsx | 25 +++++++++++++++---- .../tui/test/component/patch-diff.test.tsx | 3 +++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/tui/src/component/patch-diff.tsx b/packages/tui/src/component/patch-diff.tsx index e1bc371702..dd12258c37 100644 --- a/packages/tui/src/component/patch-diff.tsx +++ b/packages/tui/src/component/patch-diff.tsx @@ -3,6 +3,7 @@ import { DiffRenderable, LineNumberRenderable, type ColorInput } from "@opentui/ import type { JSX } from "@opentui/solid" import { createMemo, For, Show, splitProps } from "solid-js" import { splitPatchHunks } from "../util/diff" +import { stringWidth } from "../util/string-width" type Props = Omit & { diff: string @@ -19,12 +20,26 @@ export function PatchDiff(props: Props) { const sides = [...nodes] .filter((item) => !item.isDestroyed) .flatMap((item) => item.getChildren().filter((side) => side instanceof LineNumberRenderable)) - const max = Math.max(...sides.flatMap((side) => [...side.getLineNumbers().values()])) - if (!max && attempt < 2) return syncGutters(attempt + 1) - if (!max) return + const lineNumbers = sides.map((side) => new Map([...side.getLineNumbers()].filter(([line]) => line >= 0))) + const digits = lineNumbers.map((numbers) => Math.max(0, ...numbers.values()).toString().length) + const after = sides.map((side) => + Math.max( + 0, + ...[...side.getLineSigns()] + .filter(([line]) => line >= 0) + .map(([, sign]) => stringWidth(sign.after ?? "")), + ), + ) + const maxDigits = Math.max(...digits) + const maxAfter = Math.max(...after) + if (!maxDigits && attempt < 2) return syncGutters(attempt + 1) + if (!maxDigits) return sides.forEach((side) => { - side.setLineNumbers(new Map([...side.getLineNumbers(), [-1, max]])) - side.setLineSigns(new Map([...side.getLineSigns(), [-1, { after: " " }]])) + const index = sides.indexOf(side) + const signs = new Map([...side.getLineSigns()].filter(([line]) => line >= 0)) + signs.set(-1, { after: " ".repeat(maxAfter + maxDigits - digits[index]) }) + side.setLineNumbers(lineNumbers[index]) + side.setLineSigns(signs) }) }) } diff --git a/packages/tui/test/component/patch-diff.test.tsx b/packages/tui/test/component/patch-diff.test.tsx index 9c0c85d41c..87690c66b7 100644 --- a/packages/tui/test/component/patch-diff.test.tsx +++ b/packages/tui/test/component/patch-diff.test.tsx @@ -46,6 +46,8 @@ test("renders separate diff nodes with a full-width hunk row", async () => { const frame = app.captureCharFrame() const headerRow = frame.split("\n").findIndex((line) => line.includes("@@ -20,3 +20,3 @@")) const header = frame.split("\n")[headerRow] + const firstLine = frame.split("\n").find((line) => line.includes("const first")) ?? "" + const secondLine = frame.split("\n").find((line) => line.includes("const second")) ?? "" const background = parseColor("#222222") expect(header?.startsWith("@@ -20,3 +20,3 @@")).toBe(true) @@ -66,6 +68,7 @@ test("renders separate diff nodes with a full-width hunk row", async () => { expect(diffs).toHaveLength(2) expect(gutters[0].width).toBeGreaterThan(0) expect(new Set(gutters.map((gutter) => gutter.width)).size).toBe(1) + expect(firstLine.search(/\d/)).toBe(secondLine.search(/\d/)) }) function findDiffs(root: Renderable): DiffRenderable[] {