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}