diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index a3bd8d47..3c4270d7 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -141,6 +141,7 @@ export interface Settings { loadMemoryFromIncludeDirectories?: boolean; chatCompression?: ChatCompressionSettings; + showLineNumbers?: boolean; } export interface SettingsError { diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 771fcacb..cba18047 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -41,6 +41,7 @@ import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js'; import { checkForUpdates } from './ui/utils/updateCheck.js'; import { handleAutoUpdate } from './utils/handleAutoUpdate.js'; import { appEvents, AppEvent } from './utils/events.js'; +import { SettingsContext } from './ui/contexts/SettingsContext.js'; export function validateDnsResolutionOrder( order: string | undefined, @@ -257,12 +258,14 @@ export async function main() { setWindowTitle(basename(workspaceRoot), settings); const instance = render( - + + + , { exitOnCtrlC: false }, ); diff --git a/packages/cli/src/ui/contexts/SettingsContext.tsx b/packages/cli/src/ui/contexts/SettingsContext.tsx new file mode 100644 index 00000000..130ba66e --- /dev/null +++ b/packages/cli/src/ui/contexts/SettingsContext.tsx @@ -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( + undefined, +); + +export const useSettings = () => { + const context = useContext(SettingsContext); + if (context === undefined) { + throw new Error('useSettings must be used within a SettingsProvider'); + } + return context; +}; diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx index 58b32c7e..b183d556 100644 --- a/packages/cli/src/ui/utils/CodeColorizer.tsx +++ b/packages/cli/src/ui/utils/CodeColorizer.tsx @@ -20,6 +20,7 @@ import { MaxSizedBox, MINIMUM_MAX_HEIGHT, } from '../components/shared/MaxSizedBox.js'; +import { LoadedSettings } from '../../config/settings.js'; // Configure theming and parsing utilities. const lowlight = createLowlight(common); @@ -129,9 +130,11 @@ export function colorizeCode( availableHeight?: number, maxWidth?: number, theme?: Theme, + settings?: LoadedSettings, ): React.ReactNode { const codeToHighlight = code.replace(/\n$/, ''); const activeTheme = theme || themeManager.getActiveTheme(); + const showLineNumbers = settings?.merged.showLineNumbers ?? true; try { // Render the HAST tree using the adapted theme @@ -167,12 +170,14 @@ export function colorizeCode( return ( - - {`${String(index + 1 + hiddenLinesCount).padStart( - padWidth, - ' ', - )} `} - + {showLineNumbers && ( + + {`${String(index + 1 + hiddenLinesCount).padStart( + padWidth, + ' ', + )} `} + + )} {contentToRender} @@ -198,9 +203,11 @@ export function colorizeCode( > {lines.map((line, index) => ( - - {`${String(index + 1).padStart(padWidth, ' ')} `} - + {showLineNumbers && ( + + {`${String(index + 1).padStart(padWidth, ' ')} `} + + )} {line} ))} diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx index 312c1b5b..dba6bb6d 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx @@ -7,6 +7,8 @@ import { render } from 'ink-testing-library'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { MarkdownDisplay } from './MarkdownDisplay.js'; +import { LoadedSettings } from '../../config/settings.js'; +import { SettingsContext } from '../contexts/SettingsContext.js'; describe('', () => { const baseProps = { @@ -15,19 +17,32 @@ describe('', () => { availableTerminalHeight: 40, }; + const mockSettings = new LoadedSettings( + { path: '', settings: {} }, + { path: '', settings: {} }, + { path: '', settings: {} }, + [], + ); + beforeEach(() => { vi.clearAllMocks(); }); it('renders nothing for empty text', () => { - const { lastFrame } = render(); + const { lastFrame } = render( + + + , + ); expect(lastFrame()).toMatchSnapshot(); }); it('renders a simple paragraph', () => { const text = 'Hello, world.'; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -40,7 +55,9 @@ describe('', () => { #### Header 4 `; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -48,7 +65,9 @@ describe('', () => { it('renders a fenced code block with a language', () => { const text = '```javascript\nconst x = 1;\nconsole.log(x);\n```'; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -56,7 +75,9 @@ describe('', () => { it('renders a fenced code block without a language', () => { const text = '```\nplain text\n```'; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -64,7 +85,9 @@ describe('', () => { it('handles unclosed (pending) code blocks', () => { const text = '```typescript\nlet y = 2;'; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -76,7 +99,9 @@ describe('', () => { + item C `; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -88,7 +113,9 @@ describe('', () => { * Level 3 `; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -99,7 +126,9 @@ describe('', () => { 2. Second item `; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -113,7 +142,9 @@ World Test `; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -126,7 +157,9 @@ Test | Cell 3 | Cell 4 | `; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -138,7 +171,9 @@ Some text before. |---| | 1 | 2 |`; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -148,7 +183,9 @@ Some text before. Paragraph 2.`; const { lastFrame } = render( - , + + + , ); expect(lastFrame()).toMatchSnapshot(); }); @@ -169,8 +206,39 @@ some code Another paragraph. `; const { lastFrame } = render( - , + + + , ); 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( + + + , + ); + 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( + + + , + ); + expect(lastFrame()).toMatchSnapshot(); + expect(lastFrame()).toContain(' 1 '); + }); }); diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index eb5dc453..7568e1f8 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -10,6 +10,7 @@ import { Colors } from '../colors.js'; import { colorizeCode } from './CodeColorizer.js'; import { TableRenderer } from './TableRenderer.js'; import { RenderInline } from './InlineMarkdownRenderer.js'; +import { useSettings } from '../contexts/SettingsContext.js'; interface MarkdownDisplayProps { text: string; @@ -298,6 +299,7 @@ const RenderCodeBlockInternal: React.FC = ({ availableTerminalHeight, terminalWidth, }) => { + const settings = useSettings(); 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 @@ -322,6 +324,8 @@ const RenderCodeBlockInternal: React.FC = ({ lang, availableTerminalHeight, terminalWidth - CODE_BLOCK_PREFIX_PADDING, + undefined, + settings, ); return ( @@ -338,6 +342,8 @@ const RenderCodeBlockInternal: React.FC = ({ lang, availableTerminalHeight, terminalWidth - CODE_BLOCK_PREFIX_PADDING, + undefined, + settings, ); return ( diff --git a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap index 66436bbd..223c293b 100644 --- a/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap +++ b/packages/cli/src/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap @@ -23,6 +23,8 @@ exports[` > handles a table at the end of the input 1`] = ` exports[` > handles unclosed (pending) code blocks 1`] = `" 1 let y = 2;"`; +exports[` > hides line numbers in code blocks when showLineNumbers is false 1`] = `" const x = 1;"`; + exports[` > inserts a single space between paragraphs 1`] = ` "Paragraph 1. @@ -87,3 +89,5 @@ exports[` > renders unordered lists with different markers 1` + item C " `; + +exports[` > shows line numbers in code blocks by default 1`] = `" 1 const x = 1;"`;