chore: fix lint warnings in DiffRenderer

This commit is contained in:
Taylor Mullen
2026-01-21 17:47:08 -08:00
parent 1033550f78
commit 8f5022c4ae
8 changed files with 284 additions and 83 deletions

View File

@@ -128,7 +128,12 @@ describe('<Header />', () => {
},
background: {
primary: '',
diff: { added: '', removed: '' },
diff: {
added: '',
addedHighlight: '',
removed: '',
removedHighlight: '',
},
},
border: {
default: '',

View File

@@ -8,6 +8,7 @@ import type React from 'react';
import { useMemo } from 'react';
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
import crypto from 'node:crypto';
import * as Diff from 'diff';
import { colorizeCode, colorizeLine } from '../../utils/CodeColorizer.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { theme as semanticTheme } from '../../semantic-colors.js';
@@ -21,6 +22,42 @@ interface DiffLine {
content: string;
}
interface DiffChangeGroup {
type: 'change';
removed: DiffLine[];
added: DiffLine[];
}
type GroupedDiffLine = DiffLine | DiffChangeGroup;
function groupDiffLines(lines: DiffLine[]): GroupedDiffLine[] {
const grouped: GroupedDiffLine[] = [];
let i = 0;
while (i < lines.length) {
if (lines[i].type === 'del') {
const removed: DiffLine[] = [];
while (i < lines.length && lines[i].type === 'del') {
removed.push(lines[i]);
i++;
}
const added: DiffLine[] = [];
while (i < lines.length && lines[i].type === 'add') {
added.push(lines[i]);
i++;
}
if (added.length > 0) {
grouped.push({ type: 'change', removed, added });
} else {
grouped.push(...removed);
}
} else {
grouped.push(lines[i]);
i++;
}
}
return grouped;
}
function parseDiffWithLineNumbers(diffContent: string): DiffLine[] {
const lines = diffContent.split('\n');
const result: DiffLine[] = [];
@@ -256,18 +293,27 @@ const renderDiffContent = (
? `diff-box-${filename}`
: `diff-box-${crypto.createHash('sha1').update(JSON.stringify(parsedLines)).digest('hex')}`;
const groupedLines = groupDiffLines(displayableLines);
let lastLineNumber: number | null = null;
const MAX_CONTEXT_LINES_WITHOUT_GAP = 5;
const content = displayableLines.reduce<React.ReactNode[]>(
(acc, line, index) => {
// Determine the relevant line number for gap calculation based on type
const content = groupedLines.reduce<React.ReactNode[]>(
(acc, entry, index) => {
// Determine the relevant line number for gap calculation
let relevantLineNumberForGapCalc: number | null = null;
if (line.type === 'add' || line.type === 'context') {
relevantLineNumberForGapCalc = line.newLine ?? null;
} else if (line.type === 'del') {
// For deletions, the gap is typically in relation to the original file's line numbering
relevantLineNumberForGapCalc = line.oldLine ?? null;
if ('type' in entry && entry.type === 'change') {
const firstLine = entry.removed[0] || entry.added[0];
relevantLineNumberForGapCalc =
(firstLine.type === 'add' ? firstLine.newLine : firstLine.oldLine) ??
null;
} else {
const line = entry;
if (line.type === 'add' || line.type === 'context') {
relevantLineNumberForGapCalc = line.newLine ?? null;
} else if (line.type === 'del') {
relevantLineNumberForGapCalc = line.oldLine ?? null;
}
}
if (
@@ -290,82 +336,102 @@ const renderDiffContent = (
);
}
const lineKey = `diff-line-${index}`;
let gutterNumStr = '';
let prefixSymbol = ' ';
if ('type' in entry && entry.type === 'change') {
const removedText = entry.removed
.map((l) => l.content.substring(baseIndentation))
.join('\n');
const addedText = entry.added
.map((l) => l.content.substring(baseIndentation))
.join('\n');
const wordDiffs = Diff.diffWordsWithSpace(removedText, addedText);
switch (line.type) {
case 'add':
gutterNumStr = (line.newLine ?? '').toString();
prefixSymbol = '+';
lastLineNumber = line.newLine ?? null;
break;
case 'del':
gutterNumStr = (line.oldLine ?? '').toString();
prefixSymbol = '-';
// For deletions, update lastLineNumber based on oldLine if it's advancing.
// This helps manage gaps correctly if there are multiple consecutive deletions
// or if a deletion is followed by a context line far away in the original file.
// Render removed lines
const removedLinesParts = renderChangesForType(
'del',
wordDiffs,
semanticTheme.background.diff.removedHighlight,
);
entry.removed.forEach((line, i) => {
const displayContentParts = removedLinesParts[i] || [];
acc.push(
renderLine(
line,
`del-${index}-${i}`,
gutterWidth,
'-',
semanticTheme.background.diff.removed,
displayContentParts.length > 0 ? displayContentParts : undefined,
baseIndentation,
language,
),
);
if (line.oldLine !== undefined) {
lastLineNumber = line.oldLine;
}
break;
case 'context':
gutterNumStr = (line.newLine ?? '').toString();
prefixSymbol = ' ';
});
// Render added lines
const addedLinesParts = renderChangesForType(
'add',
wordDiffs,
semanticTheme.background.diff.addedHighlight,
);
entry.added.forEach((line, i) => {
const displayContentParts = addedLinesParts[i] || [];
acc.push(
renderLine(
line,
`add-${index}-${i}`,
gutterWidth,
'+',
semanticTheme.background.diff.added,
displayContentParts.length > 0 ? displayContentParts : undefined,
baseIndentation,
language,
),
);
lastLineNumber = line.newLine ?? null;
break;
default:
return acc;
});
} else {
const line = entry;
let prefixSymbol = ' ';
let backgroundColor: string | undefined = undefined;
switch (line.type) {
case 'add':
prefixSymbol = '+';
backgroundColor = semanticTheme.background.diff.added;
lastLineNumber = line.newLine ?? null;
break;
case 'del':
prefixSymbol = '-';
backgroundColor = semanticTheme.background.diff.removed;
if (line.oldLine !== undefined) {
lastLineNumber = line.oldLine;
}
break;
case 'context':
prefixSymbol = ' ';
lastLineNumber = line.newLine ?? null;
break;
default:
break;
}
acc.push(
renderLine(
line,
`line-${index}`,
gutterWidth,
prefixSymbol,
backgroundColor,
undefined,
baseIndentation,
language,
),
);
}
const displayContent = line.content.substring(baseIndentation);
const backgroundColor =
line.type === 'add'
? semanticTheme.background.diff.added
: line.type === 'del'
? semanticTheme.background.diff.removed
: undefined;
acc.push(
<Box key={lineKey} flexDirection="row">
<Box
width={gutterWidth + 1}
paddingRight={1}
flexShrink={0}
backgroundColor={backgroundColor}
justifyContent="flex-end"
>
<Text color={semanticTheme.text.secondary}>{gutterNumStr}</Text>
</Box>
{line.type === 'context' ? (
<>
<Text>{prefixSymbol} </Text>
<Text wrap="wrap">{colorizeLine(displayContent, language)}</Text>
</>
) : (
<Text
backgroundColor={
line.type === 'add'
? semanticTheme.background.diff.added
: semanticTheme.background.diff.removed
}
wrap="wrap"
>
<Text
color={
line.type === 'add'
? semanticTheme.status.success
: semanticTheme.status.error
}
>
{prefixSymbol}
</Text>{' '}
{colorizeLine(displayContent, language)}
</Text>
)}
</Box>,
);
return acc;
},
[],
@@ -382,6 +448,85 @@ const renderDiffContent = (
);
};
const renderLine = (
line: DiffLine,
key: string,
gutterWidth: number,
prefixSymbol: string,
backgroundColor: string | undefined,
displayContentParts: React.ReactNode[] | undefined,
baseIndentation: number,
language: string | null,
) => {
const gutterNumStr =
(line.type === 'add' || line.type === 'context'
? line.newLine
: line.oldLine
)?.toString() || '';
const displayContent = line.content.substring(baseIndentation);
return (
<Box key={key} flexDirection="row">
<Box
width={gutterWidth + 1}
paddingRight={1}
flexShrink={0}
backgroundColor={backgroundColor}
justifyContent="flex-end"
>
<Text color={semanticTheme.text.secondary}>{gutterNumStr}</Text>
</Box>
<Text backgroundColor={backgroundColor} wrap="wrap">
<Text
color={
line.type === 'add'
? semanticTheme.status.success
: line.type === 'del'
? semanticTheme.status.error
: undefined
}
>
{prefixSymbol}
</Text>{' '}
{displayContentParts
? displayContentParts
: colorizeLine(displayContent, language)}
</Text>
</Box>
);
};
function renderChangesForType(
type: 'add' | 'del',
allChanges: Diff.Change[],
highlightColor: string | undefined,
) {
const lines: React.ReactNode[][] = [[]];
allChanges.forEach((change, changeIndex) => {
if (type === 'add' && change.removed) return;
if (type === 'del' && change.added) return;
const isHighlighted =
(type === 'add' && change.added) || (type === 'del' && change.removed);
const color = isHighlighted ? highlightColor : undefined;
const parts = change.value.split('\n');
parts.forEach((part, partIndex) => {
if (partIndex > 0) lines.push([]);
lines[lines.length - 1].push(
<Text
key={`change-${changeIndex}-part-${partIndex}`}
backgroundColor={color}
>
{part}
</Text>,
);
});
});
return lines;
}
const getLanguageFromExtension = (extension: string): string | null => {
const languageMap: { [key: string]: string } = {
js: 'javascript',

View File

@@ -13,8 +13,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
'test';
21 + const anotherNew =
'test';
22 console.log('end of second
hunk');"
22 console.log('end of
second hunk');"
`;
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height 6 1`] = `
@@ -94,8 +94,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
'test';
21 + const anotherNew =
'test';
22 console.log('end of second
hunk');"
22 console.log('end of
second hunk');"
`;
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 80 and height 6 1`] = `

View File

@@ -19,7 +19,9 @@ const githubDarkColors: ColorsTheme = {
AccentYellow: '#FFAB70',
AccentRed: '#F97583',
DiffAdded: '#3C4636',
DiffAddedHighlight: '#5C6656',
DiffRemoved: '#502125',
DiffRemovedHighlight: '#704145',
Comment: '#6A737D',
Gray: '#6A737D',
DarkGray: interpolateColor('#6A737D', '#24292e', 0.5),

View File

@@ -19,7 +19,9 @@ const githubLightColors: ColorsTheme = {
AccentYellow: '#990073',
AccentRed: '#d14',
DiffAdded: '#C6EAD8',
DiffAddedHighlight: '#A2D9B1',
DiffRemoved: '#FFCCCC',
DiffRemovedHighlight: '#FFB3B3',
Comment: '#998',
Gray: '#999',
DarkGray: interpolateColor('#999', '#f8f8f8', 0.5),

View File

@@ -38,7 +38,9 @@ const noColorSemanticColors: SemanticColors = {
primary: '',
diff: {
added: '',
addedHighlight: '',
removed: '',
removedHighlight: '',
},
},
border: {

View File

@@ -18,7 +18,9 @@ export interface SemanticColors {
primary: string;
diff: {
added: string;
addedHighlight: string;
removed: string;
removedHighlight: string;
};
};
border: {
@@ -50,7 +52,10 @@ export const lightSemanticColors: SemanticColors = {
primary: lightTheme.Background,
diff: {
added: lightTheme.DiffAdded,
addedHighlight: lightTheme.DiffAddedHighlight ?? lightTheme.DiffAdded,
removed: lightTheme.DiffRemoved,
removedHighlight:
lightTheme.DiffRemovedHighlight ?? lightTheme.DiffRemoved,
},
},
border: {
@@ -82,7 +87,9 @@ export const darkSemanticColors: SemanticColors = {
primary: darkTheme.Background,
diff: {
added: darkTheme.DiffAdded,
addedHighlight: darkTheme.DiffAddedHighlight ?? darkTheme.DiffAdded,
removed: darkTheme.DiffRemoved,
removedHighlight: darkTheme.DiffRemovedHighlight ?? darkTheme.DiffRemoved,
},
},
border: {
@@ -114,7 +121,9 @@ export const ansiSemanticColors: SemanticColors = {
primary: ansiTheme.Background,
diff: {
added: ansiTheme.DiffAdded,
addedHighlight: ansiTheme.DiffAddedHighlight ?? ansiTheme.DiffAdded,
removed: ansiTheme.DiffRemoved,
removedHighlight: ansiTheme.DiffRemovedHighlight ?? ansiTheme.DiffRemoved,
},
},
border: {

View File

@@ -26,7 +26,9 @@ export interface ColorsTheme {
AccentYellow: string;
AccentRed: string;
DiffAdded: string;
DiffAddedHighlight?: string;
DiffRemoved: string;
DiffRemovedHighlight?: string;
Comment: string;
Gray: string;
DarkGray: string;
@@ -48,7 +50,9 @@ export interface CustomTheme {
primary?: string;
diff?: {
added?: string;
addedHighlight?: string;
removed?: string;
removedHighlight?: string;
};
};
border?: {
@@ -77,7 +81,9 @@ export interface CustomTheme {
AccentYellow?: string;
AccentRed?: string;
DiffAdded?: string;
DiffAddedHighlight?: string;
DiffRemoved?: string;
DiffRemovedHighlight?: string;
Comment?: string;
Gray?: string;
DarkGray?: string;
@@ -96,7 +102,9 @@ export const lightTheme: ColorsTheme = {
AccentYellow: '#D5A40A',
AccentRed: '#DD4C4C',
DiffAdded: '#C6EAD8',
DiffAddedHighlight: '#A2D9B1',
DiffRemoved: '#FFCCCC',
DiffRemovedHighlight: '#FFB3B3',
Comment: '#008000',
Gray: '#97a0b0',
DarkGray: interpolateColor('#97a0b0', '#FAFAFA', 0.5),
@@ -115,7 +123,9 @@ export const darkTheme: ColorsTheme = {
AccentYellow: '#F9E2AF',
AccentRed: '#F38BA8',
DiffAdded: '#28350B',
DiffAddedHighlight: '#435515',
DiffRemoved: '#430000',
DiffRemovedHighlight: '#700000',
Comment: '#6C7086',
Gray: '#6C7086',
DarkGray: interpolateColor('#6C7086', '#1E1E2E', 0.5),
@@ -134,7 +144,9 @@ export const ansiTheme: ColorsTheme = {
AccentYellow: 'yellow',
AccentRed: 'red',
DiffAdded: 'green',
DiffAddedHighlight: 'green',
DiffRemoved: 'red',
DiffRemovedHighlight: 'red',
Comment: 'gray',
Gray: 'gray',
DarkGray: 'gray',
@@ -177,7 +189,11 @@ export class Theme {
primary: this.colors.Background,
diff: {
added: this.colors.DiffAdded,
addedHighlight:
this.colors.DiffAddedHighlight ?? this.colors.DiffAdded,
removed: this.colors.DiffRemoved,
removedHighlight:
this.colors.DiffRemovedHighlight ?? this.colors.DiffRemoved,
},
},
border: {
@@ -275,8 +291,20 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
AccentRed: customTheme.status?.error ?? customTheme.AccentRed ?? '',
DiffAdded:
customTheme.background?.diff?.added ?? customTheme.DiffAdded ?? '',
DiffAddedHighlight:
customTheme.background?.diff?.addedHighlight ??
customTheme.DiffAddedHighlight ??
customTheme.background?.diff?.added ??
customTheme.DiffAdded ??
'',
DiffRemoved:
customTheme.background?.diff?.removed ?? customTheme.DiffRemoved ?? '',
DiffRemovedHighlight:
customTheme.background?.diff?.removedHighlight ??
customTheme.DiffRemovedHighlight ??
customTheme.background?.diff?.removed ??
customTheme.DiffRemoved ??
'',
Comment: customTheme.ui?.comment ?? customTheme.Comment ?? '',
Gray: customTheme.text?.secondary ?? customTheme.Gray ?? '',
DarkGray:
@@ -442,7 +470,15 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
primary: customTheme.background?.primary ?? colors.Background,
diff: {
added: customTheme.background?.diff?.added ?? colors.DiffAdded,
addedHighlight:
customTheme.background?.diff?.addedHighlight ??
colors.DiffAddedHighlight ??
colors.DiffAdded,
removed: customTheme.background?.diff?.removed ?? colors.DiffRemoved,
removedHighlight:
customTheme.background?.diff?.removedHighlight ??
colors.DiffRemovedHighlight ??
colors.DiffRemoved,
},
},
border: {