From bfa2cc17cb90a245485786b46bfc3cda2125fa6b Mon Sep 17 00:00:00 2001 From: Max Syabro Date: Thu, 18 Jun 2026 14:29:49 +0700 Subject: [PATCH] FIX: make word emphasis readable on light themes Use lighter word-emphasis backgrounds when the configured Shiki theme is light, keep existing dark-theme background colors, and remove the forced bold style from word emphasis. Add and update regression tests for the rendered emphasis sequences. --- src/diff/index.spec.ts | 45 ++++++++++++++++----------- src/diff/word/emphasis-golden.spec.ts | 4 +-- src/diff/word/line-emphasis.ts | 11 +++++-- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/diff/index.spec.ts b/src/diff/index.spec.ts index 9c342a4..a717f5b 100644 --- a/src/diff/index.spec.ts +++ b/src/diff/index.spec.ts @@ -127,15 +127,24 @@ test("word emphasis pairs the most similar lines inside change blocks", () => { "-1 const trimmed = line.trim();\n+1 const safeLine = escapeControlChars(line);\n+2 const trimmed = safeLine.trim();"; const rendered = renderSyntaxHighlightedDiff(diff, undefined, testTheme(), 3).split("\n"); assert.doesNotMatch(rendered[1] ?? "", /\x1b\[48;2;64;132;82m/); - assert.match(rendered[2] ?? "", /\x1b\[48;2;64;132;82m\x1b\[1msafeLine/); - assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70m\x1b\[1mline/); + assert.match(rendered[2] ?? "", /\x1b\[48;2;64;132;82msafeLine/); + assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70mline/); +}); + +test("word emphasis uses readable backgrounds with light syntax themes", () => { + setCodePreviewSettings({ ...codePreviewSettings, shikiTheme: "github-light" }); + const diff = "-1 const value = oldValue;\n+1 const value = newValue;"; + const rendered = renderSyntaxHighlightedDiff(diff, undefined, testTheme(), 2); + assert.match(rendered, /\x1b\[48;2;216;182;182mold/); + assert.match(rendered, /\x1b\[48;2;194;209;194mnew/); + assert.doesNotMatch(rendered, /\x1b\[48;2;(?:148;62;70|64;132;82)m/); }); test("word emphasis marks low-overlap one-to-one changed pairs instead of skipping them", () => { const diff = "-1 out.push(pair.removed, pair.added);\n+1 block.push(next);"; const rendered = renderSyntaxHighlightedDiff(diff, undefined, testTheme(), 2).split("\n"); - assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70m\x1b\[1mout/); - assert.match(rendered[1] ?? "", /\x1b\[48;2;64;132;82m\x1b\[1mblock/); + assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70mout/); + assert.match(rendered[1] ?? "", /\x1b\[48;2;64;132;82mblock/); }); test("word emphasis uses compound identifier parts when pairing changed lines", () => { @@ -145,8 +154,8 @@ test("word emphasis uses compound identifier parts when pairing changed lines", "+2 return renderPreview(input);", ].join("\n"); const rendered = renderSyntaxHighlightedDiff(diff, undefined, testTheme(), 3).split("\n"); - assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70m\x1b\[1mread/); - assert.match(rendered[1] ?? "", /\x1b\[48;2;64;132;82m\x1b\[1medit/); + assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70mread/); + assert.match(rendered[1] ?? "", /\x1b\[48;2;64;132;82medit/); assert.doesNotMatch(rendered[2] ?? "", /\x1b\[48;2;64;132;82m/); }); @@ -195,10 +204,10 @@ test("word emphasis pairs high-confidence reordered lines", () => { "+2 const alphaResult = computeAlpha(next);", ].join("\n"); const rendered = renderSyntaxHighlightedDiff(diff, undefined, testTheme(), 4).split("\n"); - assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70m\x1b\[1minput/); - assert.match(rendered[1] ?? "", /\x1b\[48;2;148;62;70m\x1b\[1mprevious/); - assert.match(rendered[2] ?? "", /\x1b\[48;2;64;132;82m\x1b\[1mcurrent/); - assert.match(rendered[3] ?? "", /\x1b\[48;2;64;132;82m\x1b\[1mnext/); + assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70minput/); + assert.match(rendered[1] ?? "", /\x1b\[48;2;148;62;70mprevious/); + assert.match(rendered[2] ?? "", /\x1b\[48;2;64;132;82mcurrent/); + assert.match(rendered[3] ?? "", /\x1b\[48;2;64;132;82mnext/); }); test("word emphasis narrows compound identifier changes to changed segments", () => { @@ -269,9 +278,9 @@ test("word emphasis skips low-confidence positional pairs inside larger blocks", "+2 renderCompletelyDifferentScreen();", ].join("\n"); const rendered = renderSyntaxHighlightedDiff(diff, undefined, testTheme(), 4).split("\n"); - assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70m\x1b\[1mitems/); + assert.match(rendered[0] ?? "", /\x1b\[48;2;148;62;70mitems/); assert.doesNotMatch(rendered[1] ?? "", /\x1b\[48;2;148;62;70m/); - assert.match(rendered[2] ?? "", /\x1b\[48;2;64;132;82m\x1b\[1mnext/); + assert.match(rendered[2] ?? "", /\x1b\[48;2;64;132;82mnext/); assert.doesNotMatch(rendered[3] ?? "", /\x1b\[48;2;64;132;82m/); }); @@ -306,7 +315,7 @@ test("smart word emphasis suppresses low-signal wrapper syntax", () => { setCodePreviewSettings({ ...codePreviewSettings, wordEmphasis: "all" }); const all = renderSyntaxHighlightedDiff(diff, undefined, testTheme(), 2); - assert.match(all, /\x1b\[48;2;148;62;70m\x1b\[1m\.map/); + assert.match(all, /\x1b\[48;2;148;62;70m\.map/); }); test("word emphasis can be disabled", () => { @@ -331,8 +340,8 @@ test("word emphasis ranges stay aligned when indentation changes", () => { const diff = "-1 \tconst next = parseDiffLine(lines[i + 1]!);\n+1 \t\tconst next = parseDiffLine(lines[end]!);"; const rendered = renderSyntaxHighlightedDiff(diff, undefined, testTheme(), 2).split("\n"); - assert.match(rendered[0] ?? "", /lines\[\x1b\[48;2;148;62;70m\x1b\[1mi \+ 1/); - assert.match(rendered[1] ?? "", /lines\[\x1b\[48;2;64;132;82m\x1b\[1mend/); + assert.match(rendered[0] ?? "", /lines\[\x1b\[48;2;148;62;70mi \+ 1/); + assert.match(rendered[1] ?? "", /lines\[\x1b\[48;2;64;132;82mend/); }); test("word emphasis highlights long shared lines with appended text", () => { @@ -342,7 +351,7 @@ test("word emphasis highlights long shared lines with appended text", () => { assert.doesNotMatch(rendered[0] ?? "", /\x1b\[48;2;148;62;70m/); assert.match( rendered[1] ?? "", - /\x1b\[48;2;64;132;82m\x1b\[1m\. Project settings override global settings/, + /\x1b\[48;2;64;132;82m\. Project settings override global settings/, ); }); @@ -358,8 +367,8 @@ test("word emphasis is applied synchronously for large changed lines", () => { () => invalidations++, ); assert.equal(invalidations, 0); - assert.match(rendered, /\x1b\[48;2;148;62;70m\x1b\[1mold/); - assert.match(rendered, /\x1b\[48;2;64;132;82m\x1b\[1mnew/); + assert.match(rendered, /\x1b\[48;2;148;62;70mold/); + assert.match(rendered, /\x1b\[48;2;64;132;82mnew/); }); test("word range emphasis returns changed spans for unrelated token-heavy lines", () => { diff --git a/src/diff/word/emphasis-golden.spec.ts b/src/diff/word/emphasis-golden.spec.ts index 979fae7..f6224c4 100644 --- a/src/diff/word/emphasis-golden.spec.ts +++ b/src/diff/word/emphasis-golden.spec.ts @@ -5,8 +5,8 @@ import { codePreviewSettings, setCodePreviewSettings } from "../../settings/inde import { stripAnsi, testTheme } from "../../testing/render"; import { wordEmphasisGoldenCases } from "./fixtures/emphasis-golden"; -const WORD_EMPHASIS_OPEN = /\x1b\[48;2;(?:64;132;82|148;62;70)m\x1b\[1m/g; -const WORD_EMPHASIS_CLOSE = "\x1b[22m\x1b[49m"; +const WORD_EMPHASIS_OPEN = /\x1b\[48;2;(?:64;132;82|148;62;70)m/g; +const WORD_EMPHASIS_CLOSE = "\x1b[49m"; let previousCodePreviewSettings = { ...codePreviewSettings }; diff --git a/src/diff/word/line-emphasis.ts b/src/diff/word/line-emphasis.ts index 8c6c31e..8ff7812 100644 --- a/src/diff/word/line-emphasis.ts +++ b/src/diff/word/line-emphasis.ts @@ -1,9 +1,13 @@ +import { bundledThemesInfo } from "shiki"; +import { codePreviewSettings } from "../../settings/index"; import type { DiffWordEmphasis } from "../../settings/types"; import { injectVisibleRanges } from "../../shared/terminal-text"; import type { ParsedDiffLine } from "../parse"; import { analyzeChangedLineBlock } from "./change-block"; import { shouldEmphasizeChangedPair } from "./emphasis"; +const shikiThemeTypes = new Map(bundledThemesInfo.map((theme) => [theme.id, theme.type])); + export function changedLineEmphasis( block: ParsedDiffLine[], wordEmphasis: DiffWordEmphasis, @@ -31,7 +35,7 @@ export function emphasizeChangedSpans( line.slice(0, codeStart) + injectVisibleRanges(line.slice(codeStart), ranges, { open: wordEmphasis(kind), - close: "\x1b[22m\x1b[49m", + close: "\x1b[49m", reopenAfterSgr: (sequence) => sequence === "\x1b[39m" || sequence === "\x1b[22m", }) ); @@ -50,5 +54,8 @@ function findCodeStart(line: string): number { } function wordEmphasis(kind: "add" | "remove"): string { - return kind === "add" ? "\x1b[48;2;64;132;82m\x1b[1m" : "\x1b[48;2;148;62;70m\x1b[1m"; + if (shikiThemeTypes.get(codePreviewSettings.shikiTheme) === "light") { + return kind === "add" ? "\x1b[48;2;194;209;194m" : "\x1b[48;2;216;182;182m"; + } + return kind === "add" ? "\x1b[48;2;64;132;82m" : "\x1b[48;2;148;62;70m"; }