feat: Add option to hide line numbers in code blocks (#5857)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
parent
69322e12e4
commit
c03ae43777
|
@ -141,6 +141,7 @@ export interface Settings {
|
||||||
loadMemoryFromIncludeDirectories?: boolean;
|
loadMemoryFromIncludeDirectories?: boolean;
|
||||||
|
|
||||||
chatCompression?: ChatCompressionSettings;
|
chatCompression?: ChatCompressionSettings;
|
||||||
|
showLineNumbers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsError {
|
export interface SettingsError {
|
||||||
|
|
|
@ -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 },
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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 ');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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;"`;
|
||||||
|
|
Loading…
Reference in New Issue