diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 27c5b90f..1429b69c 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -138,7 +138,10 @@ export function App({ () => availableThemes(bootstrap.customTheme), [bootstrap.customTheme], ); - const baseTheme = resolveTheme(themeId, detectedThemeMode ?? null, bootstrap.customTheme); + const baseTheme = useMemo( + () => resolveTheme(themeId, detectedThemeMode ?? null, bootstrap.customTheme), + [themeId, detectedThemeMode, bootstrap.customTheme], + ); const activeTheme = useMemo( () => bootstrap.input.options.transparentBackground diff --git a/src/ui/diff/pierre.test.ts b/src/ui/diff/pierre.test.ts index 2cb1f4af..9b115430 100644 --- a/src/ui/diff/pierre.test.ts +++ b/src/ui/diff/pierre.test.ts @@ -13,7 +13,7 @@ import { resolveSplitPaneWidths } from "./codeColumns"; import { renderCodeOnlyPlannedRowText, renderDecoratedPlannedRowText } from "./renderRows"; import { buildReviewRenderPlan } from "./reviewRenderPlan"; import { measureTextWidth } from "../lib/text"; -import { resolveTheme } from "../themes"; +import { TRANSPARENT_BACKGROUND, resolveTheme, withTransparentBackground } from "../themes"; function createDiffFile(): DiffFile { const metadata = parseDiffFromFile( @@ -146,6 +146,28 @@ describe("Pierre diff rows", () => { ).toBe(true); }); + test("keeps word-diff highlight backgrounds transparent in transparent mode", async () => { + const file = createDiffFile(); + const theme = withTransparentBackground(resolveTheme("midnight", null)); + const highlighted = await loadHighlightedDiff(file); + const rows = buildSplitRows(file, highlighted, theme); + const changedRow = rows.find( + (row) => + row.type === "split-line" && row.left.kind === "deletion" && row.right.kind === "addition", + ); + + expect(changedRow).toBeDefined(); + if (!changedRow || changedRow.type !== "split-line") { + throw new Error("Expected a split-line change row"); + } + + const removedWordSpan = changedRow.left.spans.find((span) => span.text.includes("41")); + const addedWordSpan = changedRow.right.spans.find((span) => span.text.includes("42")); + + expect(removedWordSpan?.bg).toBe(TRANSPARENT_BACKGROUND); + expect(addedWordSpan?.bg).toBe(TRANSPARENT_BACKGROUND); + }); + test("builds stacked rows with separate deletion and addition lines", () => { const file = createDiffFile(); const theme = resolveTheme("paper", null); diff --git a/src/ui/diff/pierre.ts b/src/ui/diff/pierre.ts index 7b4f3219..576018fd 100644 --- a/src/ui/diff/pierre.ts +++ b/src/ui/diff/pierre.ts @@ -11,7 +11,7 @@ import { formatHunkHeader } from "../../core/hunkHeader"; import type { DiffFile } from "../../core/types"; import { blendHex, hexColorDistance } from "../lib/color"; import { sanitizeTerminalLine } from "../../lib/terminalText"; -import type { AppTheme } from "../themes"; +import { TRANSPARENT_BACKGROUND, type AppTheme } from "../themes"; import { expandDiffTabs } from "./codeColumns"; const PIERRE_THEME = { @@ -238,6 +238,26 @@ function strengthenWordDiffBg(lineBg: string, signColor: string) { return strongestCandidate; } +/** Return whether a theme color can safely participate in RGB distance and blend math. */ +function isHexThemeColor(color: string) { + return /^#[0-9a-f]{6}$/i.test(color); +} + +/** Resolve one word-diff background without turning transparent surfaces into black blends. */ +function resolveWordDiffHighlightBg(contentBg: string, lineBg: string, signColor: string) { + if (contentBg === TRANSPARENT_BACKGROUND || lineBg === TRANSPARENT_BACKGROUND) { + return contentBg; + } + + if (!isHexThemeColor(contentBg) || !isHexThemeColor(lineBg)) { + return contentBg; + } + + return hexColorDistance(contentBg, lineBg) >= MIN_WORD_DIFF_BG_DISTANCE + ? contentBg + : strengthenWordDiffBg(lineBg, signColor); +} + /** Resolve the inline word-diff background, strengthening theme colors that are too subtle to see. */ function wordDiffHighlightBg(kind: SplitLineCell["kind"], theme: AppTheme) { const cacheKey = [ @@ -251,14 +271,16 @@ function wordDiffHighlightBg(kind: SplitLineCell["kind"], theme: AppTheme) { ].join(":"); let cached = wordDiffBackgroundCache.get(cacheKey); if (!cached) { - const addition = - hexColorDistance(theme.addedContentBg, theme.addedBg) >= MIN_WORD_DIFF_BG_DISTANCE - ? theme.addedContentBg - : strengthenWordDiffBg(theme.addedBg, theme.addedSignColor); - const deletion = - hexColorDistance(theme.removedContentBg, theme.removedBg) >= MIN_WORD_DIFF_BG_DISTANCE - ? theme.removedContentBg - : strengthenWordDiffBg(theme.removedBg, theme.removedSignColor); + const addition = resolveWordDiffHighlightBg( + theme.addedContentBg, + theme.addedBg, + theme.addedSignColor, + ); + const deletion = resolveWordDiffHighlightBg( + theme.removedContentBg, + theme.removedBg, + theme.removedSignColor, + ); cached = { addition,