Update diff colors (#4747)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Miguel Solorio 2025-07-23 15:39:22 -07:00 committed by GitHub
parent e21b5c95aa
commit 2e28bb90a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 155 additions and 61 deletions

View File

@ -38,6 +38,12 @@ export const Colors: ColorsTheme = {
get AccentRed() { get AccentRed() {
return themeManager.getActiveTheme().colors.AccentRed; return themeManager.getActiveTheme().colors.AccentRed;
}, },
get DiffAdded() {
return themeManager.getActiveTheme().colors.DiffAdded;
},
get DiffRemoved() {
return themeManager.getActiveTheme().colors.DiffRemoved;
},
get Comment() { get Comment() {
return themeManager.getActiveTheme().colors.Comment; return themeManager.getActiveTheme().colors.Comment;
}, },

View File

@ -279,18 +279,23 @@ export function ThemeDialog({
> >
{colorizeCode( {colorizeCode(
`# function `# function
-def fibonacci(n): def fibonacci(n):
- a, b = 0, 1 a, b = 0, 1
- for _ in range(n): for _ in range(n):
- a, b = b, a + b a, b = b, a + b
- return a`, return a`,
'python', 'python',
codeBlockHeight, codeBlockHeight,
colorizeCodeWidth, colorizeCodeWidth,
)} )}
<Box marginTop={1} /> <Box marginTop={1} />
<DiffRenderer <DiffRenderer
diffContent={`--- a/old_file.txt\n+++ b/new_file.txt\n@@ -1,6 +1,7 @@\n # function\n-def fibonacci(n):\n- a, b = 0, 1\n- for _ in range(n):\n- a, b = b, a + b\n- return a\n+def fibonacci(n):\n+ a, b = 0, 1\n+ for _ in range(n):\n+ a, b = b, a + b\n+ return a\n+\n+print(fibonacci(10))\n`} diffContent={`--- a/util.py
+++ b/util.py
@@ -1,2 +1,2 @@
- print("Hello, " + name)
+ print(f"Hello, {name}!")
`}
availableTerminalHeight={diffHeight} availableTerminalHeight={diffHeight}
terminalWidth={colorizeCodeWidth} terminalWidth={colorizeCodeWidth}
theme={previewTheme} theme={previewTheme}

View File

@ -130,8 +130,8 @@ index 0000001..0000002 100644
); );
const output = lastFrame(); const output = lastFrame();
const lines = output!.split('\n'); const lines = output!.split('\n');
expect(lines[0]).toBe('1 - old line'); expect(lines[0]).toBe('1 - old line');
expect(lines[1]).toBe('1 + new line'); expect(lines[1]).toBe('1 + new line');
}); });
it('should handle diff with only header and no changes', () => { it('should handle diff with only header and no changes', () => {
@ -253,35 +253,35 @@ index 123..789 100644
{ {
terminalWidth: 80, terminalWidth: 80,
height: undefined, height: undefined,
expected: `1 console.log('first hunk'); expected: ` 1 console.log('first hunk');
2 - const oldVar = 1; 2 - const oldVar = 1;
2 + const newVar = 1; 2 + const newVar = 1;
3 console.log('end of first hunk'); 3 console.log('end of first hunk');
20 console.log('second hunk'); 20 console.log('second hunk');
21 - const anotherOld = 'test'; 21 - const anotherOld = 'test';
21 + const anotherNew = 'test'; 21 + const anotherNew = 'test';
22 console.log('end of second hunk');`, 22 console.log('end of second hunk');`,
}, },
{ {
terminalWidth: 80, terminalWidth: 80,
height: 6, height: 6,
expected: `... first 4 lines hidden ... expected: `... first 4 lines hidden ...
20 console.log('second hunk'); 20 console.log('second hunk');
21 - const anotherOld = 'test'; 21 - const anotherOld = 'test';
21 + const anotherNew = 'test'; 21 + const anotherNew = 'test';
22 console.log('end of second hunk');`, 22 console.log('end of second hunk');`,
}, },
{ {
terminalWidth: 30, terminalWidth: 30,
height: 6, height: 6,
expected: `... first 10 lines hidden ... expected: `... first 10 lines hidden ...
'test'; ;
21 + const anotherNew = 21 + const anotherNew = 'test'
'test'; ;
22 console.log('end of 22 console.log('end of
second hunk');`, second hunk');`,
}, },
])( ])(
'with terminalWidth $terminalWidth and height $height', 'with terminalWidth $terminalWidth and height $height',
@ -329,11 +329,11 @@ fileDiff Index: file.txt
); );
const output = lastFrame(); const output = lastFrame();
expect(output).toEqual(`1 - const oldVar = 1; expect(output).toEqual(` 1 - const oldVar = 1;
1 + const newVar = 1; 1 + const newVar = 1;
20 - const anotherOld = 'test'; 20 - const anotherOld = 'test';
20 + const anotherNew = 'test';`); 20 + const anotherNew = 'test';`);
}); });
it('should correctly render a new file with no file extension correctly', () => { it('should correctly render a new file with no file extension correctly', () => {

View File

@ -8,7 +8,7 @@ import React from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { Colors } from '../../colors.js'; import { Colors } from '../../colors.js';
import crypto from 'crypto'; import crypto from 'crypto';
import { colorizeCode } from '../../utils/CodeColorizer.js'; import { colorizeCode, colorizeLine } from '../../utils/CodeColorizer.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js'; import { MaxSizedBox } from '../shared/MaxSizedBox.js';
interface DiffLine { interface DiffLine {
@ -157,7 +157,6 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
tabWidth, tabWidth,
availableTerminalHeight, availableTerminalHeight,
terminalWidth, terminalWidth,
theme,
); );
} }
@ -170,7 +169,6 @@ const renderDiffContent = (
tabWidth = DEFAULT_TAB_WIDTH, tabWidth = DEFAULT_TAB_WIDTH,
availableTerminalHeight: number | undefined, availableTerminalHeight: number | undefined,
terminalWidth: number, terminalWidth: number,
theme?: import('../../themes/theme.js').Theme,
) => { ) => {
// 1. Normalize whitespace (replace tabs with spaces) *before* further processing // 1. Normalize whitespace (replace tabs with spaces) *before* further processing
const normalizedLines = parsedLines.map((line) => ({ 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 // Calculate the minimum indentation across all displayable lines
let baseIndentation = Infinity; // Start high to find the minimum let baseIndentation = Infinity; // Start high to find the minimum
for (const line of displayableLines) { for (const line of displayableLines) {
@ -237,27 +247,25 @@ const renderDiffContent = (
) { ) {
acc.push( acc.push(
<Box key={`gap-${index}`}> <Box key={`gap-${index}`}>
<Text wrap="truncate">{'═'.repeat(terminalWidth)}</Text> <Text wrap="truncate" color={Colors.Gray}>
{'═'.repeat(terminalWidth)}
</Text>
</Box>, </Box>,
); );
} }
const lineKey = `diff-line-${index}`; const lineKey = `diff-line-${index}`;
let gutterNumStr = ''; let gutterNumStr = '';
let color: string | undefined = undefined;
let prefixSymbol = ' '; let prefixSymbol = ' ';
let dim = false;
switch (line.type) { switch (line.type) {
case 'add': case 'add':
gutterNumStr = (line.newLine ?? '').toString(); gutterNumStr = (line.newLine ?? '').toString();
color = theme?.colors?.AccentGreen || 'green';
prefixSymbol = '+'; prefixSymbol = '+';
lastLineNumber = line.newLine ?? null; lastLineNumber = line.newLine ?? null;
break; break;
case 'del': case 'del':
gutterNumStr = (line.oldLine ?? '').toString(); gutterNumStr = (line.oldLine ?? '').toString();
color = theme?.colors?.AccentRed || 'red';
prefixSymbol = '-'; prefixSymbol = '-';
// For deletions, update lastLineNumber based on oldLine if it's advancing. // For deletions, update lastLineNumber based on oldLine if it's advancing.
// This helps manage gaps correctly if there are multiple consecutive deletions // This helps manage gaps correctly if there are multiple consecutive deletions
@ -268,7 +276,6 @@ const renderDiffContent = (
break; break;
case 'context': case 'context':
gutterNumStr = (line.newLine ?? '').toString(); gutterNumStr = (line.newLine ?? '').toString();
dim = true;
prefixSymbol = ' '; prefixSymbol = ' ';
lastLineNumber = line.newLine ?? null; lastLineNumber = line.newLine ?? null;
break; break;
@ -280,13 +287,26 @@ const renderDiffContent = (
acc.push( acc.push(
<Box key={lineKey} flexDirection="row"> <Box key={lineKey} flexDirection="row">
<Text color={Colors.Gray}>{gutterNumStr.padEnd(4)} </Text> <Text color={Colors.Gray}>
<Text color={color} dimColor={dim}> {gutterNumStr.padStart(gutterWidth)}{' '}
{prefixSymbol}{' '}
</Text>
<Text color={color} dimColor={dim} wrap="wrap">
{displayContent}
</Text> </Text>
{line.type === 'context' ? (
<>
<Text>{prefixSymbol} </Text>
<Text wrap="wrap">
{colorizeLine(displayContent, language)}
</Text>
</>
) : (
<Text
backgroundColor={
line.type === 'add' ? Colors.DiffAdded : Colors.DiffRemoved
}
wrap="wrap"
>
{prefixSymbol} {colorizeLine(displayContent, language)}
</Text>
)}
</Box>, </Box>,
); );
return acc; return acc;

View File

@ -17,6 +17,8 @@ const ansiLightColors: ColorsTheme = {
AccentGreen: 'green', AccentGreen: 'green',
AccentYellow: 'orange', AccentYellow: 'orange',
AccentRed: 'red', AccentRed: 'red',
DiffAdded: '#E5F2E5',
DiffRemoved: '#FFE5E5',
Comment: 'gray', Comment: 'gray',
Gray: 'gray', Gray: 'gray',
GradientColors: ['blue', 'green'], GradientColors: ['blue', 'green'],

View File

@ -17,6 +17,8 @@ const ansiColors: ColorsTheme = {
AccentGreen: 'green', AccentGreen: 'green',
AccentYellow: 'yellow', AccentYellow: 'yellow',
AccentRed: 'red', AccentRed: 'red',
DiffAdded: '#003300',
DiffRemoved: '#4D0000',
Comment: 'gray', Comment: 'gray',
Gray: 'gray', Gray: 'gray',
GradientColors: ['cyan', 'green'], GradientColors: ['cyan', 'green'],

View File

@ -17,6 +17,8 @@ const atomOneDarkColors: ColorsTheme = {
AccentGreen: '#98c379', AccentGreen: '#98c379',
AccentYellow: '#e6c07b', AccentYellow: '#e6c07b',
AccentRed: '#e06c75', AccentRed: '#e06c75',
DiffAdded: '#39544E',
DiffRemoved: '#562B2F',
Comment: '#5c6370', Comment: '#5c6370',
Gray: '#5c6370', Gray: '#5c6370',
GradientColors: ['#61aeee', '#98c379'], GradientColors: ['#61aeee', '#98c379'],

View File

@ -17,8 +17,10 @@ const ayuLightColors: ColorsTheme = {
AccentGreen: '#86b300', AccentGreen: '#86b300',
AccentYellow: '#f2ae49', AccentYellow: '#f2ae49',
AccentRed: '#f07171', AccentRed: '#f07171',
DiffAdded: '#C6EAD8',
DiffRemoved: '#FFCCCC',
Comment: '#ABADB1', Comment: '#ABADB1',
Gray: '#CCCFD3', Gray: '#a6aaaf',
GradientColors: ['#399ee6', '#86b300'], GradientColors: ['#399ee6', '#86b300'],
}; };

View File

@ -17,8 +17,10 @@ const ayuDarkColors: ColorsTheme = {
AccentGreen: '#AAD94C', AccentGreen: '#AAD94C',
AccentYellow: '#FFB454', AccentYellow: '#FFB454',
AccentRed: '#F26D78', AccentRed: '#F26D78',
DiffAdded: '#293022',
DiffRemoved: '#3D1215',
Comment: '#646A71', Comment: '#646A71',
Gray: '##3D4149', Gray: '#3D4149',
GradientColors: ['#FFB454', '#F26D78'], GradientColors: ['#FFB454', '#F26D78'],
}; };

View File

@ -17,6 +17,8 @@ const draculaColors: ColorsTheme = {
AccentGreen: '#50fa7b', AccentGreen: '#50fa7b',
AccentYellow: '#f1fa8c', AccentYellow: '#f1fa8c',
AccentRed: '#ff5555', AccentRed: '#ff5555',
DiffAdded: '#11431d',
DiffRemoved: '#6e1818',
Comment: '#6272a4', Comment: '#6272a4',
Gray: '#6272a4', Gray: '#6272a4',
GradientColors: ['#ff79c6', '#8be9fd'], GradientColors: ['#ff79c6', '#8be9fd'],

View File

@ -17,6 +17,8 @@ const githubDarkColors: ColorsTheme = {
AccentGreen: '#85E89D', AccentGreen: '#85E89D',
AccentYellow: '#FFAB70', AccentYellow: '#FFAB70',
AccentRed: '#F97583', AccentRed: '#F97583',
DiffAdded: '#3C4636',
DiffRemoved: '#502125',
Comment: '#6A737D', Comment: '#6A737D',
Gray: '#6A737D', Gray: '#6A737D',
GradientColors: ['#79B8FF', '#85E89D'], GradientColors: ['#79B8FF', '#85E89D'],

View File

@ -17,6 +17,8 @@ const githubLightColors: ColorsTheme = {
AccentGreen: '#008080', AccentGreen: '#008080',
AccentYellow: '#990073', AccentYellow: '#990073',
AccentRed: '#d14', AccentRed: '#d14',
DiffAdded: '#C6EAD8',
DiffRemoved: '#FFCCCC',
Comment: '#998', Comment: '#998',
Gray: '#999', Gray: '#999',
GradientColors: ['#458', '#008080'], GradientColors: ['#458', '#008080'],

View File

@ -17,6 +17,8 @@ const googleCodeColors: ColorsTheme = {
AccentGreen: '#080', AccentGreen: '#080',
AccentYellow: '#660', AccentYellow: '#660',
AccentRed: '#800', AccentRed: '#800',
DiffAdded: '#C6EAD8',
DiffRemoved: '#FEDEDE',
Comment: '#5f6368', Comment: '#5f6368',
Gray: lightTheme.Gray, Gray: lightTheme.Gray,
GradientColors: ['#066', '#606'], GradientColors: ['#066', '#606'],

View File

@ -17,6 +17,8 @@ const noColorColorsTheme: ColorsTheme = {
AccentGreen: '', AccentGreen: '',
AccentYellow: '', AccentYellow: '',
AccentRed: '', AccentRed: '',
DiffAdded: '',
DiffRemoved: '',
Comment: '', Comment: '',
Gray: '', Gray: '',
}; };

View File

@ -22,6 +22,8 @@ const shadesOfPurpleColors: ColorsTheme = {
AccentGreen: '#A5FF90', // Strings and many others AccentGreen: '#A5FF90', // Strings and many others
AccentYellow: '#fad000', // Title, main yellow AccentYellow: '#fad000', // Title, main yellow
AccentRed: '#ff628c', // Error/deletion accent AccentRed: '#ff628c', // Error/deletion accent
DiffAdded: '#383E45',
DiffRemoved: '#572244',
Comment: '#B362FF', // Comment color (same as AccentPurple) Comment: '#B362FF', // Comment color (same as AccentPurple)
Gray: '#726c86', // Gray color Gray: '#726c86', // Gray color
GradientColors: ['#4d21fc', '#847ace', '#ff628c'], GradientColors: ['#4d21fc', '#847ace', '#ff628c'],

View File

@ -23,10 +23,12 @@ const validCustomTheme: CustomTheme = {
AccentPurple: '#8B5CF6', AccentPurple: '#8B5CF6',
AccentCyan: '#06B6D4', AccentCyan: '#06B6D4',
AccentGreen: '#3CA84B', AccentGreen: '#3CA84B',
AccentYellow: '#D5A40A', AccentYellow: 'yellow',
AccentRed: '#DD4C4C', AccentRed: 'red',
Comment: '#008000', DiffAdded: 'green',
Gray: '#B7BECC', DiffRemoved: 'red',
Comment: 'gray',
Gray: 'gray',
}; };
describe('ThemeManager', () => { describe('ThemeManager', () => {

View File

@ -20,6 +20,8 @@ export interface ColorsTheme {
AccentGreen: string; AccentGreen: string;
AccentYellow: string; AccentYellow: string;
AccentRed: string; AccentRed: string;
DiffAdded: string;
DiffRemoved: string;
Comment: string; Comment: string;
Gray: string; Gray: string;
GradientColors?: string[]; GradientColors?: string[];
@ -41,8 +43,10 @@ export const lightTheme: ColorsTheme = {
AccentGreen: '#3CA84B', AccentGreen: '#3CA84B',
AccentYellow: '#D5A40A', AccentYellow: '#D5A40A',
AccentRed: '#DD4C4C', AccentRed: '#DD4C4C',
DiffAdded: '#C6EAD8',
DiffRemoved: '#FFCCCC',
Comment: '#008000', Comment: '#008000',
Gray: '#B7BECC', Gray: '#97a0b0',
GradientColors: ['#4796E4', '#847ACE', '#C3677F'], GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
}; };
@ -57,6 +61,8 @@ export const darkTheme: ColorsTheme = {
AccentGreen: '#A6E3A1', AccentGreen: '#A6E3A1',
AccentYellow: '#F9E2AF', AccentYellow: '#F9E2AF',
AccentRed: '#F38BA8', AccentRed: '#F38BA8',
DiffAdded: '#28350B',
DiffRemoved: '#430000',
Comment: '#6C7086', Comment: '#6C7086',
Gray: '#6C7086', Gray: '#6C7086',
GradientColors: ['#4796E4', '#847ACE', '#C3677F'], GradientColors: ['#4796E4', '#847ACE', '#C3677F'],
@ -73,6 +79,8 @@ export const ansiTheme: ColorsTheme = {
AccentGreen: 'green', AccentGreen: 'green',
AccentYellow: 'yellow', AccentYellow: 'yellow',
AccentRed: 'red', AccentRed: 'red',
DiffAdded: 'green',
DiffRemoved: 'red',
Comment: 'gray', Comment: 'gray',
Gray: 'gray', Gray: 'gray',
}; };
@ -328,6 +336,8 @@ export function validateCustomTheme(customTheme: Partial<CustomTheme>): {
'AccentGreen', 'AccentGreen',
'AccentYellow', 'AccentYellow',
'AccentRed', 'AccentRed',
'DiffAdded',
'DiffRemoved',
'Comment', 'Comment',
'Gray', 'Gray',
]; ];
@ -352,6 +362,8 @@ export function validateCustomTheme(customTheme: Partial<CustomTheme>): {
'AccentGreen', 'AccentGreen',
'AccentYellow', 'AccentYellow',
'AccentRed', 'AccentRed',
'DiffAdded',
'DiffRemoved',
'Comment', 'Comment',
'Gray', 'Gray',
]; ];

View File

@ -17,6 +17,8 @@ const xcodeColors: ColorsTheme = {
AccentGreen: '#007400', AccentGreen: '#007400',
AccentYellow: '#836C28', AccentYellow: '#836C28',
AccentRed: '#c41a16', AccentRed: '#c41a16',
DiffAdded: '#C6EAD8',
DiffRemoved: '#FEDEDE',
Comment: '#007400', Comment: '#007400',
Gray: '#c0c0c0', Gray: '#c0c0c0',
GradientColors: ['#1c00cf', '#007400'], GradientColors: ['#1c00cf', '#007400'],

View File

@ -88,6 +88,34 @@ function renderHastNode(
return null; 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. * 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 ( return (
<MaxSizedBox <MaxSizedBox
maxHeight={availableHeight} maxHeight={availableHeight}
@ -136,17 +159,19 @@ export function colorizeCode(
overflowDirection="top" overflowDirection="top"
> >
{lines.map((line, index) => { {lines.map((line, index) => {
const renderedNode = renderHastNode( const contentToRender = highlightAndRenderLine(
getHighlightedLines(line), line,
language,
activeTheme, activeTheme,
undefined,
); );
const contentToRender = renderedNode !== null ? renderedNode : line;
return ( return (
<Box key={index}> <Box key={index}>
<Text color={activeTheme.colors.Gray}> <Text color={activeTheme.colors.Gray}>
{`${String(index + 1 + hiddenLinesCount).padStart(padWidth, ' ')} `} {`${String(index + 1 + hiddenLinesCount).padStart(
padWidth,
' ',
)} `}
</Text> </Text>
<Text color={activeTheme.defaultColor} wrap="wrap"> <Text color={activeTheme.defaultColor} wrap="wrap">
{contentToRender} {contentToRender}