feat: make DiffRenderer respect ui.showLineNumbers setting

This commit is contained in:
刘伟光 2026-01-21 11:25:14 +08:00
parent 6eb16c0bcf
commit 4c6780b79d
4 changed files with 123 additions and 14 deletions

View file

@ -9,6 +9,7 @@ import { render } from 'ink-testing-library';
import { DiffRenderer } from './DiffRenderer.js';
import * as CodeColorizer from '../../utils/CodeColorizer.js';
import { vi } from 'vitest';
import type { LoadedSettings } from '../../../config/settings.js';
describe('<OverflowProvider><DiffRenderer /></OverflowProvider>', () => {
const mockColorizeCode = vi.spyOn(CodeColorizer, 'colorizeCode');
@ -45,6 +46,7 @@ index 0000000..e69de29
undefined,
80,
undefined,
undefined,
);
});
@ -73,6 +75,7 @@ index 0000000..e69de29
undefined,
80,
undefined,
undefined,
);
});
@ -97,6 +100,7 @@ index 0000000..e69de29
undefined,
80,
undefined,
undefined,
);
});
@ -362,4 +366,86 @@ fileDiff Index: Dockerfile
2 RUN npm install
3 RUN npm run build`);
});
describe('showLineNumbers setting', () => {
const diffContent = `
diff --git a/test.txt b/test.txt
index 0000001..0000002 100644
--- a/test.txt
+++ b/test.txt
@@ -1,2 +1,2 @@
-old line 1
+new line 1
context line 2
`;
it('should show line numbers by default when settings is undefined', () => {
const { lastFrame } = render(
<OverflowProvider>
<DiffRenderer
diffContent={diffContent}
filename="test.txt"
terminalWidth={80}
/>
</OverflowProvider>,
);
const output = lastFrame();
expect(output).toContain('1 -');
expect(output).toContain('1 +');
expect(output).toContain('2 ');
});
it('should show line numbers when showLineNumbers is true', () => {
const mockSettings = {
merged: {
ui: {
showLineNumbers: true,
},
},
} as unknown as LoadedSettings;
const { lastFrame } = render(
<OverflowProvider>
<DiffRenderer
diffContent={diffContent}
filename="test.txt"
terminalWidth={80}
settings={mockSettings}
/>
</OverflowProvider>,
);
const output = lastFrame();
expect(output).toContain('1 -');
expect(output).toContain('1 +');
expect(output).toContain('2 ');
});
it('should hide line numbers when showLineNumbers is false', () => {
const mockSettings = {
merged: {
ui: {
showLineNumbers: false,
},
},
} as unknown as LoadedSettings;
const { lastFrame } = render(
<OverflowProvider>
<DiffRenderer
diffContent={diffContent}
filename="test.txt"
terminalWidth={80}
settings={mockSettings}
/>
</OverflowProvider>,
);
const output = lastFrame();
// Line numbers should not be present
expect(output).not.toMatch(/^\s*\d+\s*[-+]/m);
// But the content should still be there
expect(output).toContain('old line 1');
expect(output).toContain('new line 1');
expect(output).toContain('context line 2');
});
});
});

View file

@ -11,6 +11,7 @@ import { colorizeCode, colorizeLine } from '../../utils/CodeColorizer.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { theme as semanticTheme } from '../../semantic-colors.js';
import type { Theme } from '../../themes/theme.js';
import type { LoadedSettings } from '../../../config/settings.js';
interface DiffLine {
type: 'add' | 'del' | 'context' | 'hunk' | 'other';
@ -86,6 +87,7 @@ interface DiffRendererProps {
availableTerminalHeight?: number;
terminalWidth: number;
theme?: Theme;
settings?: LoadedSettings;
}
const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization
@ -97,6 +99,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
availableTerminalHeight,
terminalWidth,
theme,
settings,
}) => {
const screenReaderEnabled = useIsScreenReaderEnabled();
if (!diffContent || typeof diffContent !== 'string') {
@ -157,6 +160,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
availableTerminalHeight,
terminalWidth,
theme,
settings,
);
} else {
renderedOutput = renderDiffContent(
@ -165,6 +169,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
tabWidth,
availableTerminalHeight,
terminalWidth,
settings,
);
}
@ -177,6 +182,7 @@ const renderDiffContent = (
tabWidth = DEFAULT_TAB_WIDTH,
availableTerminalHeight: number | undefined,
terminalWidth: number,
settings?: LoadedSettings,
) => {
// 1. Normalize whitespace (replace tabs with spaces) *before* further processing
const normalizedLines = parsedLines.map((line) => ({
@ -201,6 +207,8 @@ const renderDiffContent = (
);
}
const showLineNumbers = settings?.merged.ui?.showLineNumbers ?? true;
const maxLineNumber = Math.max(
0,
...displayableLines.map((l) => l.oldLine ?? 0),
@ -299,18 +307,20 @@ const renderDiffContent = (
acc.push(
<Box key={lineKey} flexDirection="row">
<Text
color={semanticTheme.text.secondary}
backgroundColor={
line.type === 'add'
? semanticTheme.background.diff.added
: line.type === 'del'
? semanticTheme.background.diff.removed
: undefined
}
>
{gutterNumStr.padStart(gutterWidth)}{' '}
</Text>
{showLineNumbers && (
<Text
color={semanticTheme.text.secondary}
backgroundColor={
line.type === 'add'
? semanticTheme.background.diff.added
: line.type === 'del'
? semanticTheme.background.diff.removed
: undefined
}
>
{gutterNumStr.padStart(gutterWidth)}{' '}
</Text>
)}
{line.type === 'context' ? (
<>
<Text>{prefixSymbol} </Text>

View file

@ -58,10 +58,17 @@ vi.mock('../GeminiRespondingSpinner.js', () => ({
vi.mock('./DiffRenderer.js', () => ({
DiffRenderer: function MockDiffRenderer({
diffContent,
settings,
}: {
diffContent: string;
settings?: unknown;
}) {
return <Text>MockDiff:{diffContent}</Text>;
return (
<Text>
MockDiff:{diffContent}
{settings ? ':withSettings' : ''}
</Text>
);
},
}));
vi.mock('../../utils/MarkdownDisplay.js', () => ({

View file

@ -30,6 +30,8 @@ import {
TOOL_STATUS,
} from '../../constants.js';
import { theme } from '../../semantic-colors.js';
import { SettingsContext } from '../../contexts/SettingsContext.js';
import type { LoadedSettings } from '../../../config/settings.js';
const STATIC_HEIGHT = 1;
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
@ -210,12 +212,14 @@ const DiffResultRenderer: React.FC<{
data: { fileDiff: string; fileName: string };
availableHeight?: number;
childWidth: number;
}> = ({ data, availableHeight, childWidth }) => (
settings?: LoadedSettings;
}> = ({ data, availableHeight, childWidth, settings }) => (
<DiffRenderer
diffContent={data.fileDiff}
filename={data.fileName}
availableTerminalHeight={availableHeight}
terminalWidth={childWidth}
settings={settings}
/>
);
@ -243,6 +247,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
ptyId,
config,
}) => {
const settings = React.useContext(SettingsContext);
const isThisShellFocused =
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
status === ToolCallStatus.Executing &&
@ -349,6 +354,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
data={displayRenderer.data}
availableHeight={availableHeight}
childWidth={childWidth}
settings={settings}
/>
)}
{displayRenderer.type === 'ansi' && (