feat: Add option to hide line numbers in code blocks (#5857)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Gal Zahavi 2025-08-08 15:11:14 -07:00 committed by GitHub
parent 69322e12e4
commit c03ae43777
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 138 additions and 29 deletions

View File

@ -141,6 +141,7 @@ export interface Settings {
loadMemoryFromIncludeDirectories?: boolean; loadMemoryFromIncludeDirectories?: boolean;
chatCompression?: ChatCompressionSettings; chatCompression?: ChatCompressionSettings;
showLineNumbers?: boolean;
} }
export interface SettingsError { export interface SettingsError {

View File

@ -41,6 +41,7 @@ import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { checkForUpdates } from './ui/utils/updateCheck.js'; import { checkForUpdates } from './ui/utils/updateCheck.js';
import { handleAutoUpdate } from './utils/handleAutoUpdate.js'; import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
import { appEvents, AppEvent } from './utils/events.js'; import { appEvents, AppEvent } from './utils/events.js';
import { SettingsContext } from './ui/contexts/SettingsContext.js';
export function validateDnsResolutionOrder( export function validateDnsResolutionOrder(
order: string | undefined, order: string | undefined,
@ -257,12 +258,14 @@ export async function main() {
setWindowTitle(basename(workspaceRoot), settings); setWindowTitle(basename(workspaceRoot), settings);
const instance = render( const instance = render(
<React.StrictMode> <React.StrictMode>
<AppWrapper <SettingsContext.Provider value={settings}>
config={config} <AppWrapper
settings={settings} config={config}
startupWarnings={startupWarnings} settings={settings}
version={version} startupWarnings={startupWarnings}
/> version={version}
/>
</SettingsContext.Provider>
</React.StrictMode>, </React.StrictMode>,
{ exitOnCtrlC: false }, { exitOnCtrlC: false },
); );

View File

@ -0,0 +1,20 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useContext } from 'react';
import { LoadedSettings } from '../../config/settings.js';
export const SettingsContext = React.createContext<LoadedSettings | undefined>(
undefined,
);
export const useSettings = () => {
const context = useContext(SettingsContext);
if (context === undefined) {
throw new Error('useSettings must be used within a SettingsProvider');
}
return context;
};

View File

@ -20,6 +20,7 @@ import {
MaxSizedBox, MaxSizedBox,
MINIMUM_MAX_HEIGHT, MINIMUM_MAX_HEIGHT,
} from '../components/shared/MaxSizedBox.js'; } from '../components/shared/MaxSizedBox.js';
import { LoadedSettings } from '../../config/settings.js';
// Configure theming and parsing utilities. // Configure theming and parsing utilities.
const lowlight = createLowlight(common); const lowlight = createLowlight(common);
@ -129,9 +130,11 @@ export function colorizeCode(
availableHeight?: number, availableHeight?: number,
maxWidth?: number, maxWidth?: number,
theme?: Theme, theme?: Theme,
settings?: LoadedSettings,
): React.ReactNode { ): React.ReactNode {
const codeToHighlight = code.replace(/\n$/, ''); const codeToHighlight = code.replace(/\n$/, '');
const activeTheme = theme || themeManager.getActiveTheme(); const activeTheme = theme || themeManager.getActiveTheme();
const showLineNumbers = settings?.merged.showLineNumbers ?? true;
try { try {
// Render the HAST tree using the adapted theme // Render the HAST tree using the adapted theme
@ -167,12 +170,14 @@ export function colorizeCode(
return ( return (
<Box key={index}> <Box key={index}>
<Text color={activeTheme.colors.Gray}> {showLineNumbers && (
{`${String(index + 1 + hiddenLinesCount).padStart( <Text color={activeTheme.colors.Gray}>
padWidth, {`${String(index + 1 + hiddenLinesCount).padStart(
' ', padWidth,
)} `} ' ',
</Text> )} `}
</Text>
)}
<Text color={activeTheme.defaultColor} wrap="wrap"> <Text color={activeTheme.defaultColor} wrap="wrap">
{contentToRender} {contentToRender}
</Text> </Text>
@ -198,9 +203,11 @@ export function colorizeCode(
> >
{lines.map((line, index) => ( {lines.map((line, index) => (
<Box key={index}> <Box key={index}>
<Text color={activeTheme.defaultColor}> {showLineNumbers && (
{`${String(index + 1).padStart(padWidth, ' ')} `} <Text color={activeTheme.defaultColor}>
</Text> {`${String(index + 1).padStart(padWidth, ' ')} `}
</Text>
)}
<Text color={activeTheme.colors.Gray}>{line}</Text> <Text color={activeTheme.colors.Gray}>{line}</Text>
</Box> </Box>
))} ))}

View File

@ -7,6 +7,8 @@
import { render } from 'ink-testing-library'; import { render } from 'ink-testing-library';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MarkdownDisplay } from './MarkdownDisplay.js'; import { MarkdownDisplay } from './MarkdownDisplay.js';
import { LoadedSettings } from '../../config/settings.js';
import { SettingsContext } from '../contexts/SettingsContext.js';
describe('<MarkdownDisplay />', () => { describe('<MarkdownDisplay />', () => {
const baseProps = { const baseProps = {
@ -15,19 +17,32 @@ describe('<MarkdownDisplay />', () => {
availableTerminalHeight: 40, availableTerminalHeight: 40,
}; };
const mockSettings = new LoadedSettings(
{ path: '', settings: {} },
{ path: '', settings: {} },
{ path: '', settings: {} },
[],
);
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
it('renders nothing for empty text', () => { it('renders nothing for empty text', () => {
const { lastFrame } = render(<MarkdownDisplay {...baseProps} text="" />); const { lastFrame } = render(
<SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text="" />
</SettingsContext.Provider>,
);
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('renders a simple paragraph', () => { it('renders a simple paragraph', () => {
const text = 'Hello, world.'; const text = 'Hello, world.';
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -40,7 +55,9 @@ describe('<MarkdownDisplay />', () => {
#### Header 4 #### Header 4
`; `;
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -48,7 +65,9 @@ describe('<MarkdownDisplay />', () => {
it('renders a fenced code block with a language', () => { it('renders a fenced code block with a language', () => {
const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```'; const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```';
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -56,7 +75,9 @@ describe('<MarkdownDisplay />', () => {
it('renders a fenced code block without a language', () => { it('renders a fenced code block without a language', () => {
const text = '```\nplain text\n```'; const text = '```\nplain text\n```';
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -64,7 +85,9 @@ describe('<MarkdownDisplay />', () => {
it('handles unclosed (pending) code blocks', () => { it('handles unclosed (pending) code blocks', () => {
const text = '```typescript\nlet y = 2;'; const text = '```typescript\nlet y = 2;';
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} isPending={true} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} isPending={true} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -76,7 +99,9 @@ describe('<MarkdownDisplay />', () => {
+ item C + item C
`; `;
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -88,7 +113,9 @@ describe('<MarkdownDisplay />', () => {
* Level 3 * Level 3
`; `;
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -99,7 +126,9 @@ describe('<MarkdownDisplay />', () => {
2. Second item 2. Second item
`; `;
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -113,7 +142,9 @@ World
Test Test
`; `;
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -126,7 +157,9 @@ Test
| Cell 3 | Cell 4 | | Cell 3 | Cell 4 |
`; `;
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -138,7 +171,9 @@ Some text before.
|---| |---|
| 1 | 2 |`; | 1 | 2 |`;
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -148,7 +183,9 @@ Some text before.
Paragraph 2.`; Paragraph 2.`;
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
@ -169,8 +206,39 @@ some code
Another paragraph. Another paragraph.
`; `;
const { lastFrame } = render( const { lastFrame } = render(
<MarkdownDisplay {...baseProps} text={text} />, <SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
); );
expect(lastFrame()).toMatchSnapshot(); expect(lastFrame()).toMatchSnapshot();
}); });
it('hides line numbers in code blocks when showLineNumbers is false', () => {
const text = '```javascript\nconst x = 1;\n```';
const settings = new LoadedSettings(
{ path: '', settings: {} },
{ path: '', settings: { showLineNumbers: false } },
{ path: '', settings: {} },
[],
);
const { lastFrame } = render(
<SettingsContext.Provider value={settings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
);
expect(lastFrame()).toMatchSnapshot();
expect(lastFrame()).not.toContain(' 1 ');
});
it('shows line numbers in code blocks by default', () => {
const text = '```javascript\nconst x = 1;\n```';
const { lastFrame } = render(
<SettingsContext.Provider value={mockSettings}>
<MarkdownDisplay {...baseProps} text={text} />
</SettingsContext.Provider>,
);
expect(lastFrame()).toMatchSnapshot();
expect(lastFrame()).toContain(' 1 ');
});
}); });

View File

@ -10,6 +10,7 @@ import { Colors } from '../colors.js';
import { colorizeCode } from './CodeColorizer.js'; import { colorizeCode } from './CodeColorizer.js';
import { TableRenderer } from './TableRenderer.js'; import { TableRenderer } from './TableRenderer.js';
import { RenderInline } from './InlineMarkdownRenderer.js'; import { RenderInline } from './InlineMarkdownRenderer.js';
import { useSettings } from '../contexts/SettingsContext.js';
interface MarkdownDisplayProps { interface MarkdownDisplayProps {
text: string; text: string;
@ -298,6 +299,7 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
availableTerminalHeight, availableTerminalHeight,
terminalWidth, terminalWidth,
}) => { }) => {
const settings = useSettings();
const MIN_LINES_FOR_MESSAGE = 1; // Minimum lines to show before the "generating more" message const MIN_LINES_FOR_MESSAGE = 1; // Minimum lines to show before the "generating more" message
const RESERVED_LINES = 2; // Lines reserved for the message itself and potential padding const RESERVED_LINES = 2; // Lines reserved for the message itself and potential padding
@ -322,6 +324,8 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
lang, lang,
availableTerminalHeight, availableTerminalHeight,
terminalWidth - CODE_BLOCK_PREFIX_PADDING, terminalWidth - CODE_BLOCK_PREFIX_PADDING,
undefined,
settings,
); );
return ( return (
<Box paddingLeft={CODE_BLOCK_PREFIX_PADDING} flexDirection="column"> <Box paddingLeft={CODE_BLOCK_PREFIX_PADDING} flexDirection="column">
@ -338,6 +342,8 @@ const RenderCodeBlockInternal: React.FC<RenderCodeBlockProps> = ({
lang, lang,
availableTerminalHeight, availableTerminalHeight,
terminalWidth - CODE_BLOCK_PREFIX_PADDING, terminalWidth - CODE_BLOCK_PREFIX_PADDING,
undefined,
settings,
); );
return ( return (

View File

@ -23,6 +23,8 @@ exports[`<MarkdownDisplay /> > handles a table at the end of the input 1`] = `
exports[`<MarkdownDisplay /> > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`; exports[`<MarkdownDisplay /> > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`;
exports[`<MarkdownDisplay /> > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`;
exports[`<MarkdownDisplay /> > inserts a single space between paragraphs 1`] = ` exports[`<MarkdownDisplay /> > inserts a single space between paragraphs 1`] = `
"Paragraph 1. "Paragraph 1.
@ -87,3 +89,5 @@ exports[`<MarkdownDisplay /> > renders unordered lists with different markers 1`
+ item C + item C
" "
`; `;
exports[`<MarkdownDisplay /> > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`;