From 2e28bb90a00ad415d453a2ec868faa78679602f0 Mon Sep 17 00:00:00 2001 From: Miguel Solorio Date: Wed, 23 Jul 2025 15:39:22 -0700 Subject: [PATCH] Update diff colors (#4747) Co-authored-by: Jacob Richman --- packages/cli/src/ui/colors.ts | 6 +++ .../cli/src/ui/components/ThemeDialog.tsx | 17 ++++--- .../components/messages/DiffRenderer.test.tsx | 46 ++++++++--------- .../ui/components/messages/DiffRenderer.tsx | 50 +++++++++++++------ packages/cli/src/ui/themes/ansi-light.ts | 2 + packages/cli/src/ui/themes/ansi.ts | 2 + packages/cli/src/ui/themes/atom-one-dark.ts | 2 + packages/cli/src/ui/themes/ayu-light.ts | 4 +- packages/cli/src/ui/themes/ayu.ts | 4 +- packages/cli/src/ui/themes/dracula.ts | 2 + packages/cli/src/ui/themes/github-dark.ts | 2 + packages/cli/src/ui/themes/github-light.ts | 2 + packages/cli/src/ui/themes/googlecode.ts | 2 + packages/cli/src/ui/themes/no-color.ts | 2 + .../cli/src/ui/themes/shades-of-purple.ts | 2 + .../cli/src/ui/themes/theme-manager.test.ts | 10 ++-- packages/cli/src/ui/themes/theme.ts | 14 +++++- packages/cli/src/ui/themes/xcode.ts | 2 + packages/cli/src/ui/utils/CodeColorizer.tsx | 45 +++++++++++++---- 19 files changed, 155 insertions(+), 61 deletions(-) diff --git a/packages/cli/src/ui/colors.ts b/packages/cli/src/ui/colors.ts index bb8451cc..f87055e4 100644 --- a/packages/cli/src/ui/colors.ts +++ b/packages/cli/src/ui/colors.ts @@ -38,6 +38,12 @@ export const Colors: ColorsTheme = { get AccentRed() { return themeManager.getActiveTheme().colors.AccentRed; }, + get DiffAdded() { + return themeManager.getActiveTheme().colors.DiffAdded; + }, + get DiffRemoved() { + return themeManager.getActiveTheme().colors.DiffRemoved; + }, get Comment() { return themeManager.getActiveTheme().colors.Comment; }, diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx index a6a16b8c..7c38bb4b 100644 --- a/packages/cli/src/ui/components/ThemeDialog.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.tsx @@ -279,18 +279,23 @@ export function ThemeDialog({ > {colorizeCode( `# function --def fibonacci(n): -- a, b = 0, 1 -- for _ in range(n): -- a, b = b, a + b -- return a`, +def fibonacci(n): + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a`, 'python', codeBlockHeight, colorizeCodeWidth, )} { @@ -253,35 +253,35 @@ index 123..789 100644 { terminalWidth: 80, height: undefined, - expected: `1 console.log('first hunk'); -2 - const oldVar = 1; -2 + const newVar = 1; -3 console.log('end of first hunk'); + expected: ` 1 console.log('first hunk'); + 2 - const oldVar = 1; + 2 + const newVar = 1; + 3 console.log('end of first hunk'); ════════════════════════════════════════════════════════════════════════════════ -20 console.log('second hunk'); -21 - const anotherOld = 'test'; -21 + const anotherNew = 'test'; -22 console.log('end of second hunk');`, +20 console.log('second hunk'); +21 - const anotherOld = 'test'; +21 + const anotherNew = 'test'; +22 console.log('end of second hunk');`, }, { terminalWidth: 80, height: 6, expected: `... first 4 lines hidden ... ════════════════════════════════════════════════════════════════════════════════ -20 console.log('second hunk'); -21 - const anotherOld = 'test'; -21 + const anotherNew = 'test'; -22 console.log('end of second hunk');`, +20 console.log('second hunk'); +21 - const anotherOld = 'test'; +21 + const anotherNew = 'test'; +22 console.log('end of second hunk');`, }, { terminalWidth: 30, height: 6, expected: `... first 10 lines hidden ... - 'test'; -21 + const anotherNew = - 'test'; -22 console.log('end of - second hunk');`, + ; +21 + const anotherNew = 'test' + ; +22 console.log('end of + second hunk');`, }, ])( 'with terminalWidth $terminalWidth and height $height', @@ -329,11 +329,11 @@ fileDiff Index: file.txt ); const output = lastFrame(); - expect(output).toEqual(`1 - const oldVar = 1; -1 + const newVar = 1; + expect(output).toEqual(` 1 - const oldVar = 1; + 1 + const newVar = 1; ════════════════════════════════════════════════════════════════════════════════ -20 - const anotherOld = 'test'; -20 + const anotherNew = 'test';`); +20 - const anotherOld = 'test'; +20 + const anotherNew = 'test';`); }); it('should correctly render a new file with no file extension correctly', () => { diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx index db402517..7f130b3f 100644 --- a/packages/cli/src/ui/components/messages/DiffRenderer.tsx +++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import crypto from 'crypto'; -import { colorizeCode } from '../../utils/CodeColorizer.js'; +import { colorizeCode, colorizeLine } from '../../utils/CodeColorizer.js'; import { MaxSizedBox } from '../shared/MaxSizedBox.js'; interface DiffLine { @@ -157,7 +157,6 @@ export const DiffRenderer: React.FC = ({ tabWidth, availableTerminalHeight, terminalWidth, - theme, ); } @@ -170,7 +169,6 @@ const renderDiffContent = ( tabWidth = DEFAULT_TAB_WIDTH, availableTerminalHeight: number | undefined, terminalWidth: number, - theme?: import('../../themes/theme.js').Theme, ) => { // 1. Normalize whitespace (replace tabs with spaces) *before* further processing const normalizedLines = parsedLines.map((line) => ({ @@ -191,6 +189,18 @@ const renderDiffContent = ( ); } + const maxLineNumber = Math.max( + 0, + ...displayableLines.map((l) => l.oldLine ?? 0), + ...displayableLines.map((l) => l.newLine ?? 0), + ); + const gutterWidth = Math.max(1, maxLineNumber.toString().length); + + const fileExtension = filename?.split('.').pop() || null; + const language = fileExtension + ? getLanguageFromExtension(fileExtension) + : null; + // Calculate the minimum indentation across all displayable lines let baseIndentation = Infinity; // Start high to find the minimum for (const line of displayableLines) { @@ -237,27 +247,25 @@ const renderDiffContent = ( ) { acc.push( - {'═'.repeat(terminalWidth)} + + {'═'.repeat(terminalWidth)} + , ); } const lineKey = `diff-line-${index}`; let gutterNumStr = ''; - let color: string | undefined = undefined; let prefixSymbol = ' '; - let dim = false; switch (line.type) { case 'add': gutterNumStr = (line.newLine ?? '').toString(); - color = theme?.colors?.AccentGreen || 'green'; prefixSymbol = '+'; lastLineNumber = line.newLine ?? null; break; case 'del': gutterNumStr = (line.oldLine ?? '').toString(); - color = theme?.colors?.AccentRed || 'red'; prefixSymbol = '-'; // For deletions, update lastLineNumber based on oldLine if it's advancing. // This helps manage gaps correctly if there are multiple consecutive deletions @@ -268,7 +276,6 @@ const renderDiffContent = ( break; case 'context': gutterNumStr = (line.newLine ?? '').toString(); - dim = true; prefixSymbol = ' '; lastLineNumber = line.newLine ?? null; break; @@ -280,13 +287,26 @@ const renderDiffContent = ( acc.push( - {gutterNumStr.padEnd(4)} - - {prefixSymbol}{' '} - - - {displayContent} + + {gutterNumStr.padStart(gutterWidth)}{' '} + {line.type === 'context' ? ( + <> + {prefixSymbol} + + {colorizeLine(displayContent, language)} + + + ) : ( + + {prefixSymbol} {colorizeLine(displayContent, language)} + + )} , ); return acc; diff --git a/packages/cli/src/ui/themes/ansi-light.ts b/packages/cli/src/ui/themes/ansi-light.ts index 4a798539..00f9bbcc 100644 --- a/packages/cli/src/ui/themes/ansi-light.ts +++ b/packages/cli/src/ui/themes/ansi-light.ts @@ -17,6 +17,8 @@ const ansiLightColors: ColorsTheme = { AccentGreen: 'green', AccentYellow: 'orange', AccentRed: 'red', + DiffAdded: '#E5F2E5', + DiffRemoved: '#FFE5E5', Comment: 'gray', Gray: 'gray', GradientColors: ['blue', 'green'], diff --git a/packages/cli/src/ui/themes/ansi.ts b/packages/cli/src/ui/themes/ansi.ts index 4ef69454..2afc135c 100644 --- a/packages/cli/src/ui/themes/ansi.ts +++ b/packages/cli/src/ui/themes/ansi.ts @@ -17,6 +17,8 @@ const ansiColors: ColorsTheme = { AccentGreen: 'green', AccentYellow: 'yellow', AccentRed: 'red', + DiffAdded: '#003300', + DiffRemoved: '#4D0000', Comment: 'gray', Gray: 'gray', GradientColors: ['cyan', 'green'], diff --git a/packages/cli/src/ui/themes/atom-one-dark.ts b/packages/cli/src/ui/themes/atom-one-dark.ts index 951b8898..5545971e 100644 --- a/packages/cli/src/ui/themes/atom-one-dark.ts +++ b/packages/cli/src/ui/themes/atom-one-dark.ts @@ -17,6 +17,8 @@ const atomOneDarkColors: ColorsTheme = { AccentGreen: '#98c379', AccentYellow: '#e6c07b', AccentRed: '#e06c75', + DiffAdded: '#39544E', + DiffRemoved: '#562B2F', Comment: '#5c6370', Gray: '#5c6370', GradientColors: ['#61aeee', '#98c379'], diff --git a/packages/cli/src/ui/themes/ayu-light.ts b/packages/cli/src/ui/themes/ayu-light.ts index 45004107..8410cfb2 100644 --- a/packages/cli/src/ui/themes/ayu-light.ts +++ b/packages/cli/src/ui/themes/ayu-light.ts @@ -17,8 +17,10 @@ const ayuLightColors: ColorsTheme = { AccentGreen: '#86b300', AccentYellow: '#f2ae49', AccentRed: '#f07171', + DiffAdded: '#C6EAD8', + DiffRemoved: '#FFCCCC', Comment: '#ABADB1', - Gray: '#CCCFD3', + Gray: '#a6aaaf', GradientColors: ['#399ee6', '#86b300'], }; diff --git a/packages/cli/src/ui/themes/ayu.ts b/packages/cli/src/ui/themes/ayu.ts index a5cfc7db..1d1fc7d0 100644 --- a/packages/cli/src/ui/themes/ayu.ts +++ b/packages/cli/src/ui/themes/ayu.ts @@ -17,8 +17,10 @@ const ayuDarkColors: ColorsTheme = { AccentGreen: '#AAD94C', AccentYellow: '#FFB454', AccentRed: '#F26D78', + DiffAdded: '#293022', + DiffRemoved: '#3D1215', Comment: '#646A71', - Gray: '##3D4149', + Gray: '#3D4149', GradientColors: ['#FFB454', '#F26D78'], }; diff --git a/packages/cli/src/ui/themes/dracula.ts b/packages/cli/src/ui/themes/dracula.ts index d754deed..e746d8e8 100644 --- a/packages/cli/src/ui/themes/dracula.ts +++ b/packages/cli/src/ui/themes/dracula.ts @@ -17,6 +17,8 @@ const draculaColors: ColorsTheme = { AccentGreen: '#50fa7b', AccentYellow: '#f1fa8c', AccentRed: '#ff5555', + DiffAdded: '#11431d', + DiffRemoved: '#6e1818', Comment: '#6272a4', Gray: '#6272a4', GradientColors: ['#ff79c6', '#8be9fd'], diff --git a/packages/cli/src/ui/themes/github-dark.ts b/packages/cli/src/ui/themes/github-dark.ts index f6912821..e93c8c6a 100644 --- a/packages/cli/src/ui/themes/github-dark.ts +++ b/packages/cli/src/ui/themes/github-dark.ts @@ -17,6 +17,8 @@ const githubDarkColors: ColorsTheme = { AccentGreen: '#85E89D', AccentYellow: '#FFAB70', AccentRed: '#F97583', + DiffAdded: '#3C4636', + DiffRemoved: '#502125', Comment: '#6A737D', Gray: '#6A737D', GradientColors: ['#79B8FF', '#85E89D'], diff --git a/packages/cli/src/ui/themes/github-light.ts b/packages/cli/src/ui/themes/github-light.ts index f1393e70..dcb4bbf0 100644 --- a/packages/cli/src/ui/themes/github-light.ts +++ b/packages/cli/src/ui/themes/github-light.ts @@ -17,6 +17,8 @@ const githubLightColors: ColorsTheme = { AccentGreen: '#008080', AccentYellow: '#990073', AccentRed: '#d14', + DiffAdded: '#C6EAD8', + DiffRemoved: '#FFCCCC', Comment: '#998', Gray: '#999', GradientColors: ['#458', '#008080'], diff --git a/packages/cli/src/ui/themes/googlecode.ts b/packages/cli/src/ui/themes/googlecode.ts index 77551284..38b719a3 100644 --- a/packages/cli/src/ui/themes/googlecode.ts +++ b/packages/cli/src/ui/themes/googlecode.ts @@ -17,6 +17,8 @@ const googleCodeColors: ColorsTheme = { AccentGreen: '#080', AccentYellow: '#660', AccentRed: '#800', + DiffAdded: '#C6EAD8', + DiffRemoved: '#FEDEDE', Comment: '#5f6368', Gray: lightTheme.Gray, GradientColors: ['#066', '#606'], diff --git a/packages/cli/src/ui/themes/no-color.ts b/packages/cli/src/ui/themes/no-color.ts index d726e14c..a6efb454 100644 --- a/packages/cli/src/ui/themes/no-color.ts +++ b/packages/cli/src/ui/themes/no-color.ts @@ -17,6 +17,8 @@ const noColorColorsTheme: ColorsTheme = { AccentGreen: '', AccentYellow: '', AccentRed: '', + DiffAdded: '', + DiffRemoved: '', Comment: '', Gray: '', }; diff --git a/packages/cli/src/ui/themes/shades-of-purple.ts b/packages/cli/src/ui/themes/shades-of-purple.ts index 8b467e75..6e20240f 100644 --- a/packages/cli/src/ui/themes/shades-of-purple.ts +++ b/packages/cli/src/ui/themes/shades-of-purple.ts @@ -22,6 +22,8 @@ const shadesOfPurpleColors: ColorsTheme = { AccentGreen: '#A5FF90', // Strings and many others AccentYellow: '#fad000', // Title, main yellow AccentRed: '#ff628c', // Error/deletion accent + DiffAdded: '#383E45', + DiffRemoved: '#572244', Comment: '#B362FF', // Comment color (same as AccentPurple) Gray: '#726c86', // Gray color GradientColors: ['#4d21fc', '#847ace', '#ff628c'], diff --git a/packages/cli/src/ui/themes/theme-manager.test.ts b/packages/cli/src/ui/themes/theme-manager.test.ts index 5bb48167..6f9565a5 100644 --- a/packages/cli/src/ui/themes/theme-manager.test.ts +++ b/packages/cli/src/ui/themes/theme-manager.test.ts @@ -23,10 +23,12 @@ const validCustomTheme: CustomTheme = { AccentPurple: '#8B5CF6', AccentCyan: '#06B6D4', AccentGreen: '#3CA84B', - AccentYellow: '#D5A40A', - AccentRed: '#DD4C4C', - Comment: '#008000', - Gray: '#B7BECC', + AccentYellow: 'yellow', + AccentRed: 'red', + DiffAdded: 'green', + DiffRemoved: 'red', + Comment: 'gray', + Gray: 'gray', }; describe('ThemeManager', () => { diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts index 9758357b..3955014f 100644 --- a/packages/cli/src/ui/themes/theme.ts +++ b/packages/cli/src/ui/themes/theme.ts @@ -20,6 +20,8 @@ export interface ColorsTheme { AccentGreen: string; AccentYellow: string; AccentRed: string; + DiffAdded: string; + DiffRemoved: string; Comment: string; Gray: string; GradientColors?: string[]; @@ -41,8 +43,10 @@ export const lightTheme: ColorsTheme = { AccentGreen: '#3CA84B', AccentYellow: '#D5A40A', AccentRed: '#DD4C4C', + DiffAdded: '#C6EAD8', + DiffRemoved: '#FFCCCC', Comment: '#008000', - Gray: '#B7BECC', + Gray: '#97a0b0', GradientColors: ['#4796E4', '#847ACE', '#C3677F'], }; @@ -57,6 +61,8 @@ export const darkTheme: ColorsTheme = { AccentGreen: '#A6E3A1', AccentYellow: '#F9E2AF', AccentRed: '#F38BA8', + DiffAdded: '#28350B', + DiffRemoved: '#430000', Comment: '#6C7086', Gray: '#6C7086', GradientColors: ['#4796E4', '#847ACE', '#C3677F'], @@ -73,6 +79,8 @@ export const ansiTheme: ColorsTheme = { AccentGreen: 'green', AccentYellow: 'yellow', AccentRed: 'red', + DiffAdded: 'green', + DiffRemoved: 'red', Comment: 'gray', Gray: 'gray', }; @@ -328,6 +336,8 @@ export function validateCustomTheme(customTheme: Partial): { 'AccentGreen', 'AccentYellow', 'AccentRed', + 'DiffAdded', + 'DiffRemoved', 'Comment', 'Gray', ]; @@ -352,6 +362,8 @@ export function validateCustomTheme(customTheme: Partial): { 'AccentGreen', 'AccentYellow', 'AccentRed', + 'DiffAdded', + 'DiffRemoved', 'Comment', 'Gray', ]; diff --git a/packages/cli/src/ui/themes/xcode.ts b/packages/cli/src/ui/themes/xcode.ts index dfdd4b8e..690d2386 100644 --- a/packages/cli/src/ui/themes/xcode.ts +++ b/packages/cli/src/ui/themes/xcode.ts @@ -17,6 +17,8 @@ const xcodeColors: ColorsTheme = { AccentGreen: '#007400', AccentYellow: '#836C28', AccentRed: '#c41a16', + DiffAdded: '#C6EAD8', + DiffRemoved: '#FEDEDE', Comment: '#007400', Gray: '#c0c0c0', GradientColors: ['#1c00cf', '#007400'], diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx index 38dc49d4..58b32c7e 100644 --- a/packages/cli/src/ui/utils/CodeColorizer.tsx +++ b/packages/cli/src/ui/utils/CodeColorizer.tsx @@ -88,6 +88,34 @@ function renderHastNode( return null; } +function highlightAndRenderLine( + line: string, + language: string | null, + theme: Theme, +): React.ReactNode { + try { + const getHighlightedLine = () => + !language || !lowlight.registered(language) + ? lowlight.highlightAuto(line) + : lowlight.highlight(language, line); + + const renderedNode = renderHastNode(getHighlightedLine(), theme, undefined); + + return renderedNode !== null ? renderedNode : line; + } catch (_error) { + return line; + } +} + +export function colorizeLine( + line: string, + language: string | null, + theme?: Theme, +): React.ReactNode { + const activeTheme = theme || themeManager.getActiveTheme(); + return highlightAndRenderLine(line, language, activeTheme); +} + /** * Renders syntax-highlighted code for Ink applications using a selected theme. * @@ -123,11 +151,6 @@ export function colorizeCode( } } - const getHighlightedLines = (line: string) => - !language || !lowlight.registered(language) - ? lowlight.highlightAuto(line) - : lowlight.highlight(language, line); - return ( {lines.map((line, index) => { - const renderedNode = renderHastNode( - getHighlightedLines(line), + const contentToRender = highlightAndRenderLine( + line, + language, activeTheme, - undefined, ); - const contentToRender = renderedNode !== null ? renderedNode : line; return ( - {`${String(index + 1 + hiddenLinesCount).padStart(padWidth, ' ')} `} + {`${String(index + 1 + hiddenLinesCount).padStart( + padWidth, + ' ', + )} `} {contentToRender}