mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
Modernize MaxSizedBox to use <Box maxHeight> and ResizeObservers (#16565)
This commit is contained in:
@@ -67,7 +67,6 @@ import {
|
||||
type InitializationResult,
|
||||
} from './core/initializer.js';
|
||||
import { validateAuthMethod } from './config/auth.js';
|
||||
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
|
||||
import { runZedIntegration } from './zed-integration/zedIntegration.js';
|
||||
import { cleanupExpiredSessions } from './utils/sessionCleanup.js';
|
||||
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
|
||||
@@ -562,7 +561,6 @@ export async function main() {
|
||||
|
||||
await setupTerminalAndTheme(config, settings);
|
||||
|
||||
setMaxSizedBoxDebugging(isDebugMode);
|
||||
const initAppHandle = startupProfiler.start('initialize_app');
|
||||
const initializationResult = await initializeApp(config, settings);
|
||||
initAppHandle?.end();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { MainContent } from './MainContent.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { Box, Text } from 'ink';
|
||||
@@ -41,9 +42,21 @@ vi.mock('../hooks/useAlternateBuffer.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('./HistoryItemDisplay.js', () => ({
|
||||
HistoryItemDisplay: ({ item }: { item: { content: string } }) => (
|
||||
HistoryItemDisplay: ({
|
||||
item,
|
||||
availableTerminalHeight,
|
||||
}: {
|
||||
item: { content: string };
|
||||
availableTerminalHeight?: number;
|
||||
}) => (
|
||||
<Box>
|
||||
<Text>HistoryItem: {item.content}</Text>
|
||||
<Text>
|
||||
HistoryItem: {item.content} (height:{' '}
|
||||
{availableTerminalHeight === undefined
|
||||
? 'undefined'
|
||||
: availableTerminalHeight}
|
||||
)
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
@@ -81,23 +94,32 @@ describe('MainContent', () => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('renders in normal buffer mode', () => {
|
||||
it('renders in normal buffer mode', async () => {
|
||||
const { lastFrame } = render(<MainContent />);
|
||||
await waitFor(() => expect(lastFrame()).toContain('AppHeader'));
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('AppHeader');
|
||||
expect(output).toContain('HistoryItem: Hello');
|
||||
expect(output).toContain('HistoryItem: Hi there');
|
||||
expect(output).toContain('HistoryItem: Hello (height: 20)');
|
||||
expect(output).toContain('HistoryItem: Hi there (height: 20)');
|
||||
});
|
||||
|
||||
it('renders in alternate buffer mode', () => {
|
||||
it('renders in alternate buffer mode', async () => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||
const { lastFrame } = render(<MainContent />);
|
||||
await waitFor(() => expect(lastFrame()).toContain('ScrollableList'));
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('ScrollableList');
|
||||
expect(output).toContain('AppHeader');
|
||||
expect(output).toContain('HistoryItem: Hello');
|
||||
expect(output).toContain('HistoryItem: Hi there');
|
||||
expect(output).toContain('HistoryItem: Hello (height: undefined)');
|
||||
expect(output).toContain('HistoryItem: Hi there (height: undefined)');
|
||||
});
|
||||
|
||||
it('does not constrain height in alternate buffer mode', async () => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||
const { lastFrame } = render(<MainContent />);
|
||||
await waitFor(() => expect(lastFrame()).toContain('HistoryItem: Hello'));
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,7 +65,9 @@ export const MainContent = () => {
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
uiState.constrainHeight ? availableTerminalHeight : undefined
|
||||
uiState.constrainHeight && !isAlternateBuffer
|
||||
? availableTerminalHeight
|
||||
: undefined
|
||||
}
|
||||
terminalWidth={mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
@@ -82,6 +84,7 @@ export const MainContent = () => {
|
||||
[
|
||||
pendingHistoryItems,
|
||||
uiState.constrainHeight,
|
||||
isAlternateBuffer,
|
||||
availableTerminalHeight,
|
||||
mainAreaWidth,
|
||||
uiState.isEditorDialogOpen,
|
||||
@@ -107,7 +110,7 @@ export const MainContent = () => {
|
||||
return (
|
||||
<MemoizedHistoryItemDisplay
|
||||
terminalWidth={mainAreaWidth}
|
||||
availableTerminalHeight={staticAreaMaxItemHeight}
|
||||
availableTerminalHeight={undefined}
|
||||
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
||||
key={item.item.id}
|
||||
item={item.item}
|
||||
@@ -119,13 +122,7 @@ export const MainContent = () => {
|
||||
return pendingItems;
|
||||
}
|
||||
},
|
||||
[
|
||||
version,
|
||||
mainAreaWidth,
|
||||
staticAreaMaxItemHeight,
|
||||
uiState.slashCommands,
|
||||
pendingItems,
|
||||
],
|
||||
[version, mainAreaWidth, uiState.slashCommands, pendingItems],
|
||||
);
|
||||
|
||||
if (isAlternateBuffer) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`MainContent > does not constrain height in alternate buffer mode 1`] = `
|
||||
"ScrollableList
|
||||
AppHeader
|
||||
HistoryItem: Hello (height: undefined)
|
||||
HistoryItem: Hi there (height: undefined)
|
||||
ShowMoreLines"
|
||||
`;
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { OverflowProvider } from '../../contexts/OverflowContext.js';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { waitFor } from '../../../test-utils/async.js';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import * as CodeColorizer from '../../utils/CodeColorizer.js';
|
||||
import { vi } from 'vitest';
|
||||
@@ -23,7 +24,7 @@ describe('<OverflowProvider><DiffRenderer /></OverflowProvider>', () => {
|
||||
describe.each([true, false])(
|
||||
'with useAlternateBuffer = %s',
|
||||
(useAlternateBuffer) => {
|
||||
it('should call colorizeCode with correct language for new file with known extension', () => {
|
||||
it('should call colorizeCode with correct language for new file with known extension', async () => {
|
||||
const newFileDiffContent = `
|
||||
diff --git a/test.py b/test.py
|
||||
new file mode 100644
|
||||
@@ -43,17 +44,19 @@ index 0000000..e69de29
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith({
|
||||
code: 'print("hello world")',
|
||||
language: 'python',
|
||||
availableHeight: undefined,
|
||||
maxWidth: 80,
|
||||
theme: undefined,
|
||||
settings: expect.anything(),
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith({
|
||||
code: 'print("hello world")',
|
||||
language: 'python',
|
||||
availableHeight: undefined,
|
||||
maxWidth: 80,
|
||||
theme: undefined,
|
||||
settings: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call colorizeCode with null language for new file with unknown extension', () => {
|
||||
it('should call colorizeCode with null language for new file with unknown extension', async () => {
|
||||
const newFileDiffContent = `
|
||||
diff --git a/test.unknown b/test.unknown
|
||||
new file mode 100644
|
||||
@@ -73,17 +76,19 @@ index 0000000..e69de29
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith({
|
||||
code: 'some content',
|
||||
language: null,
|
||||
availableHeight: undefined,
|
||||
maxWidth: 80,
|
||||
theme: undefined,
|
||||
settings: expect.anything(),
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith({
|
||||
code: 'some content',
|
||||
language: null,
|
||||
availableHeight: undefined,
|
||||
maxWidth: 80,
|
||||
theme: undefined,
|
||||
settings: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call colorizeCode with null language for new file if no filename is provided', () => {
|
||||
it('should call colorizeCode with null language for new file if no filename is provided', async () => {
|
||||
const newFileDiffContent = `
|
||||
diff --git a/test.txt b/test.txt
|
||||
new file mode 100644
|
||||
@@ -99,17 +104,19 @@ index 0000000..e69de29
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith({
|
||||
code: 'some text content',
|
||||
language: null,
|
||||
availableHeight: undefined,
|
||||
maxWidth: 80,
|
||||
theme: undefined,
|
||||
settings: expect.anything(),
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith({
|
||||
code: 'some text content',
|
||||
language: null,
|
||||
availableHeight: undefined,
|
||||
maxWidth: 80,
|
||||
theme: undefined,
|
||||
settings: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render diff content for existing file (not calling colorizeCode directly for the whole block)', () => {
|
||||
it('should render diff content for existing file (not calling colorizeCode directly for the whole block)', async () => {
|
||||
const existingFileDiffContent = `
|
||||
|
||||
diff --git a/test.txt b/test.txt
|
||||
@@ -131,6 +138,7 @@ index 0000001..0000002 100644
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
// colorizeCode is used internally by the line-by-line rendering, not for the whole block
|
||||
await waitFor(() => expect(lastFrame()).toContain('new line'));
|
||||
expect(mockColorizeCode).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: expect.stringContaining('old line'),
|
||||
@@ -144,7 +152,7 @@ index 0000001..0000002 100644
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle diff with only header and no changes', () => {
|
||||
it('should handle diff with only header and no changes', async () => {
|
||||
const noChangeDiff = `diff --git a/file.txt b/file.txt
|
||||
index 1234567..1234567 100644
|
||||
--- a/file.txt
|
||||
@@ -160,22 +168,24 @@ index 1234567..1234567 100644
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
await waitFor(() => expect(lastFrame()).toBeDefined());
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
expect(mockColorizeCode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty diff content', () => {
|
||||
it('should handle empty diff content', async () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<OverflowProvider>
|
||||
<DiffRenderer diffContent="" terminalWidth={80} />
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
await waitFor(() => expect(lastFrame()).toBeDefined());
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
expect(mockColorizeCode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render a gap indicator for skipped lines', () => {
|
||||
it('should render a gap indicator for skipped lines', async () => {
|
||||
const diffWithGap = `
|
||||
|
||||
diff --git a/file.txt b/file.txt
|
||||
@@ -200,10 +210,11 @@ index 123..456 100644
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
await waitFor(() => expect(lastFrame()).toContain('added line'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should not render a gap indicator for small gaps (<= MAX_CONTEXT_LINES_WITHOUT_GAP)', () => {
|
||||
it('should not render a gap indicator for small gaps (<= MAX_CONTEXT_LINES_WITHOUT_GAP)', async () => {
|
||||
const diffWithSmallGap = `
|
||||
|
||||
diff --git a/file.txt b/file.txt
|
||||
@@ -233,6 +244,7 @@ index abc..def 100644
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
await waitFor(() => expect(lastFrame()).toContain('context line 15'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -270,7 +282,7 @@ index 123..789 100644
|
||||
},
|
||||
])(
|
||||
'with terminalWidth $terminalWidth and height $height',
|
||||
({ terminalWidth, height }) => {
|
||||
async ({ terminalWidth, height }) => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
@@ -282,13 +294,14 @@ index 123..789 100644
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
await waitFor(() => expect(lastFrame()).toContain('anotherNew'));
|
||||
const output = lastFrame();
|
||||
expect(sanitizeOutput(output, terminalWidth)).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly render a diff with a SVN diff format', () => {
|
||||
it('should correctly render a diff with a SVN diff format', async () => {
|
||||
const newFileDiff = `
|
||||
|
||||
fileDiff Index: file.txt
|
||||
@@ -315,10 +328,11 @@ fileDiff Index: file.txt
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
await waitFor(() => expect(lastFrame()).toContain('newVar'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should correctly render a new file with no file extension correctly', () => {
|
||||
it('should correctly render a new file with no file extension correctly', async () => {
|
||||
const newFileDiff = `
|
||||
|
||||
fileDiff Index: Dockerfile
|
||||
@@ -341,6 +355,7 @@ fileDiff Index: Dockerfile
|
||||
</OverflowProvider>,
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
await waitFor(() => expect(lastFrame()).toContain('RUN npm run build'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { theme as semanticTheme } from '../../semantic-colors.js';
|
||||
import type { Theme } from '../../themes/theme.js';
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
|
||||
interface DiffLine {
|
||||
type: 'add' | 'del' | 'context' | 'hunk' | 'other';
|
||||
@@ -102,7 +101,6 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
|
||||
theme,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
|
||||
const screenReaderEnabled = useIsScreenReaderEnabled();
|
||||
|
||||
@@ -179,7 +177,6 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
|
||||
tabWidth,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
!isAlternateBuffer,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
@@ -192,7 +189,6 @@ export const DiffRenderer: React.FC<DiffRendererProps> = ({
|
||||
terminalWidth,
|
||||
theme,
|
||||
settings,
|
||||
isAlternateBuffer,
|
||||
tabWidth,
|
||||
]);
|
||||
|
||||
@@ -205,7 +201,6 @@ const renderDiffContent = (
|
||||
tabWidth = DEFAULT_TAB_WIDTH,
|
||||
availableTerminalHeight: number | undefined,
|
||||
terminalWidth: number,
|
||||
useMaxSizedBox: boolean,
|
||||
) => {
|
||||
// 1. Normalize whitespace (replace tabs with spaces) *before* further processing
|
||||
const normalizedLines = parsedLines.map((line) => ({
|
||||
@@ -283,22 +278,14 @@ const renderDiffContent = (
|
||||
) {
|
||||
acc.push(
|
||||
<Box key={`gap-${index}`}>
|
||||
{useMaxSizedBox ? (
|
||||
<Text wrap="truncate" color={semanticTheme.text.secondary}>
|
||||
{'═'.repeat(terminalWidth)}
|
||||
</Text>
|
||||
) : (
|
||||
// We can use a proper separator when not using max sized box.
|
||||
<Box
|
||||
borderStyle="double"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderBottom={false}
|
||||
width={terminalWidth}
|
||||
borderColor={semanticTheme.text.secondary}
|
||||
marginRight={1}
|
||||
></Box>
|
||||
)}
|
||||
<Box
|
||||
borderStyle="double"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderBottom={false}
|
||||
width={terminalWidth}
|
||||
borderColor={semanticTheme.text.secondary}
|
||||
></Box>
|
||||
</Box>,
|
||||
);
|
||||
}
|
||||
@@ -342,24 +329,15 @@ const renderDiffContent = (
|
||||
: undefined;
|
||||
acc.push(
|
||||
<Box key={lineKey} flexDirection="row">
|
||||
{useMaxSizedBox ? (
|
||||
<Text
|
||||
color={semanticTheme.text.secondary}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
{gutterNumStr.padStart(gutterWidth)}{' '}
|
||||
</Text>
|
||||
) : (
|
||||
<Box
|
||||
width={gutterWidth + 1}
|
||||
paddingRight={1}
|
||||
flexShrink={0}
|
||||
backgroundColor={backgroundColor}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Text color={semanticTheme.text.secondary}>{gutterNumStr}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<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>
|
||||
@@ -393,22 +371,14 @@ const renderDiffContent = (
|
||||
[],
|
||||
);
|
||||
|
||||
if (useMaxSizedBox) {
|
||||
return (
|
||||
<MaxSizedBox
|
||||
maxHeight={availableTerminalHeight}
|
||||
maxWidth={terminalWidth}
|
||||
key={key}
|
||||
>
|
||||
{content}
|
||||
</MaxSizedBox>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={key} flexDirection="column" width={terminalWidth} flexShrink={0}>
|
||||
<MaxSizedBox
|
||||
maxHeight={availableTerminalHeight}
|
||||
maxWidth={terminalWidth}
|
||||
key={key}
|
||||
>
|
||||
{content}
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
|
||||
export interface ToolConfirmationMessageProps {
|
||||
@@ -41,7 +40,6 @@ export const ToolConfirmationMessage: React.FC<
|
||||
}) => {
|
||||
const { onConfirm } = confirmationDetails;
|
||||
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const settings = useSettings();
|
||||
const allowPermanentApproval =
|
||||
settings.merged.security?.enablePermanentToolApproval ?? false;
|
||||
@@ -273,20 +271,14 @@ export const ToolConfirmationMessage: React.FC<
|
||||
bodyContentHeight -= 2; // Account for padding;
|
||||
}
|
||||
|
||||
const commandBox = (
|
||||
<Box>
|
||||
<Text color={theme.text.link}>{executionProps.command}</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
bodyContent = isAlternateBuffer ? (
|
||||
commandBox
|
||||
) : (
|
||||
bodyContent = (
|
||||
<MaxSizedBox
|
||||
maxHeight={bodyContentHeight}
|
||||
maxWidth={Math.max(terminalWidth, 1)}
|
||||
>
|
||||
{commandBox}
|
||||
<Box>
|
||||
<Text color={theme.text.link}>{executionProps.command}</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
);
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
@@ -338,7 +330,6 @@ export const ToolConfirmationMessage: React.FC<
|
||||
isDiffingEnabled,
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
isAlternateBuffer,
|
||||
allowPermanentApproval,
|
||||
]);
|
||||
|
||||
|
||||
@@ -27,31 +27,6 @@ vi.mock('./DiffRenderer.js', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/MarkdownDisplay.js', () => ({
|
||||
MarkdownDisplay: ({ text }: { text: string }) => (
|
||||
<Box>
|
||||
<Text>MarkdownDisplay: {text}</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../AnsiOutput.js', () => ({
|
||||
AnsiOutputText: ({ data }: { data: unknown }) => (
|
||||
<Box>
|
||||
<Text>AnsiOutputText: {JSON.stringify(data)}</Text>
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../shared/MaxSizedBox.js', () => ({
|
||||
MaxSizedBox: ({ children }: { children: React.ReactNode }) => (
|
||||
<Box>
|
||||
<Text>MaxSizedBox:</Text>
|
||||
{children}
|
||||
</Box>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock UIStateContext
|
||||
const mockUseUIState = vi.fn();
|
||||
vi.mock('../../contexts/UIStateContext.js', () => ({
|
||||
@@ -64,6 +39,25 @@ vi.mock('../../hooks/useAlternateBuffer.js', () => ({
|
||||
useAlternateBuffer: () => mockUseAlternateBuffer(),
|
||||
}));
|
||||
|
||||
// Mock useSettings
|
||||
vi.mock('../../contexts/SettingsContext.js', () => ({
|
||||
useSettings: () => ({
|
||||
merged: {
|
||||
ui: {
|
||||
useAlternateBuffer: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock useOverflowActions
|
||||
vi.mock('../../contexts/OverflowContext.js', () => ({
|
||||
useOverflowActions: () => ({
|
||||
addOverflowingId: vi.fn(),
|
||||
removeOverflowingId: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ToolResultDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -73,7 +67,7 @@ describe('ToolResultDisplay', () => {
|
||||
|
||||
it('renders string result as markdown by default', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay resultDisplay="Some result" terminalWidth={80} />,
|
||||
<ToolResultDisplay resultDisplay="**Some result**" terminalWidth={80} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -83,7 +77,7 @@ describe('ToolResultDisplay', () => {
|
||||
it('renders string result as plain text when renderOutputAsMarkdown is false', () => {
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="Some result"
|
||||
resultDisplay="**Some result**"
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
renderOutputAsMarkdown={false}
|
||||
@@ -126,9 +120,20 @@ describe('ToolResultDisplay', () => {
|
||||
});
|
||||
|
||||
it('renders ANSI output result', () => {
|
||||
const ansiResult = {
|
||||
text: 'ansi content',
|
||||
};
|
||||
const ansiResult: AnsiOutput = [
|
||||
[
|
||||
{
|
||||
text: 'ansi content',
|
||||
fg: 'red',
|
||||
bg: 'black',
|
||||
bold: false,
|
||||
italic: false,
|
||||
underline: false,
|
||||
dim: false,
|
||||
inverse: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay={ansiResult as unknown as AnsiOutput}
|
||||
@@ -157,20 +162,18 @@ describe('ToolResultDisplay', () => {
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('falls back to plain text if availableHeight is set and not in alternate buffer', () => {
|
||||
it('does not fall back to plain text if availableHeight is set and not in alternate buffer', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(false);
|
||||
// availableHeight calculation: 20 - 1 - 5 = 14 > 3
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="Some result"
|
||||
resultDisplay="**Some result**"
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
renderOutputAsMarkdown={true}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// Should force renderOutputAsMarkdown to false
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -178,7 +181,7 @@ describe('ToolResultDisplay', () => {
|
||||
mockUseAlternateBuffer.mockReturnValue(true);
|
||||
const { lastFrame } = render(
|
||||
<ToolResultDisplay
|
||||
resultDisplay="Some result"
|
||||
resultDisplay="**Some result**"
|
||||
terminalWidth={80}
|
||||
availableTerminalHeight={20}
|
||||
renderOutputAsMarkdown={true}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
|
||||
const STATIC_HEIGHT = 1;
|
||||
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
|
||||
@@ -42,7 +41,6 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
renderOutputAsMarkdown = true,
|
||||
}) => {
|
||||
const { renderMarkdown } = useUIState();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
|
||||
const availableHeight = availableTerminalHeight
|
||||
? Math.max(
|
||||
@@ -51,13 +49,6 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
)
|
||||
: undefined;
|
||||
|
||||
// Long tool call response in MarkdownDisplay doesn't respect availableTerminalHeight properly,
|
||||
// so if we aren't using alternate buffer mode, we're forcing it to not render as markdown when the response is too long, it will fallback
|
||||
// to render as plain text, which is contained within the terminal using MaxSizedBox
|
||||
if (availableHeight && !isAlternateBuffer) {
|
||||
renderOutputAsMarkdown = false;
|
||||
}
|
||||
|
||||
const combinedPaddingAndBorderWidth = 4;
|
||||
const childWidth = terminalWidth - combinedPaddingAndBorderWidth;
|
||||
|
||||
@@ -72,56 +63,59 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
|
||||
if (!truncatedResultDisplay) return null;
|
||||
|
||||
let content: React.ReactNode;
|
||||
|
||||
if (typeof truncatedResultDisplay === 'string' && renderOutputAsMarkdown) {
|
||||
content = (
|
||||
<MarkdownDisplay
|
||||
text={truncatedResultDisplay}
|
||||
terminalWidth={childWidth}
|
||||
renderMarkdown={renderMarkdown}
|
||||
isPending={false}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
typeof truncatedResultDisplay === 'string' &&
|
||||
!renderOutputAsMarkdown
|
||||
) {
|
||||
content = (
|
||||
<Text wrap="wrap" color={theme.text.primary}>
|
||||
{truncatedResultDisplay}
|
||||
</Text>
|
||||
);
|
||||
} else if (
|
||||
typeof truncatedResultDisplay === 'object' &&
|
||||
'fileDiff' in truncatedResultDisplay
|
||||
) {
|
||||
content = (
|
||||
<DiffRenderer
|
||||
diffContent={(truncatedResultDisplay as FileDiffResult).fileDiff}
|
||||
filename={(truncatedResultDisplay as FileDiffResult).fileName}
|
||||
availableTerminalHeight={availableHeight}
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
typeof truncatedResultDisplay === 'object' &&
|
||||
'todos' in truncatedResultDisplay
|
||||
) {
|
||||
// display nothing, as the TodoTray will handle rendering todos
|
||||
return null;
|
||||
} else {
|
||||
content = (
|
||||
<AnsiOutputText
|
||||
data={truncatedResultDisplay as AnsiOutput}
|
||||
availableTerminalHeight={availableHeight}
|
||||
width={childWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box width={childWidth} flexDirection="column">
|
||||
<Box flexDirection="column">
|
||||
{typeof truncatedResultDisplay === 'string' &&
|
||||
renderOutputAsMarkdown ? (
|
||||
<Box flexDirection="column">
|
||||
<MarkdownDisplay
|
||||
text={truncatedResultDisplay}
|
||||
terminalWidth={childWidth}
|
||||
renderMarkdown={renderMarkdown}
|
||||
isPending={false}
|
||||
/>
|
||||
</Box>
|
||||
) : typeof truncatedResultDisplay === 'string' &&
|
||||
!renderOutputAsMarkdown ? (
|
||||
isAlternateBuffer ? (
|
||||
<Box flexDirection="column" width={childWidth}>
|
||||
<Text wrap="wrap" color={theme.text.primary}>
|
||||
{truncatedResultDisplay}
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<MaxSizedBox maxHeight={availableHeight} maxWidth={childWidth}>
|
||||
<Box>
|
||||
<Text wrap="wrap" color={theme.text.primary}>
|
||||
{truncatedResultDisplay}
|
||||
</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
)
|
||||
) : typeof truncatedResultDisplay === 'object' &&
|
||||
'fileDiff' in truncatedResultDisplay ? (
|
||||
<DiffRenderer
|
||||
diffContent={(truncatedResultDisplay as FileDiffResult).fileDiff}
|
||||
filename={(truncatedResultDisplay as FileDiffResult).fileName}
|
||||
availableTerminalHeight={availableHeight}
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
) : typeof truncatedResultDisplay === 'object' &&
|
||||
'todos' in truncatedResultDisplay ? (
|
||||
// display nothing, as the TodoTray will handle rendering todos
|
||||
<></>
|
||||
) : (
|
||||
<AnsiOutputText
|
||||
data={truncatedResultDisplay as AnsiOutput}
|
||||
availableTerminalHeight={availableHeight}
|
||||
width={childWidth}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<MaxSizedBox maxHeight={availableHeight} maxWidth={childWidth}>
|
||||
{content}
|
||||
</MaxSizedBox>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,11 +10,11 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = false > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 30 and height 6 1`] = `
|
||||
"... first 10 lines hidden ...
|
||||
;
|
||||
21 + const anotherNew = 'test'
|
||||
;
|
||||
22 console.log('end of
|
||||
second hunk');"
|
||||
'test';
|
||||
21 + const anotherNew =
|
||||
'test';
|
||||
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`] = `
|
||||
@@ -84,22 +84,13 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a diff with a SVN diff format 1`] = `
|
||||
" 1 - const oldVar = 1;
|
||||
1 + const newVar = 1;
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
20 - const anotherOld = 'test';
|
||||
20 + const anotherNew = 'test';"
|
||||
`;
|
||||
|
||||
exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlternateBuffer = true > should correctly render a diff with multiple hunks and a gap indicator > with terminalWidth 30 and height 6 1`] = `
|
||||
" 1 console.log('first hunk');
|
||||
|
||||
2 - const oldVar = 1;
|
||||
2 + const newVar = 1;
|
||||
3 console.log('end of first
|
||||
hunk');
|
||||
═════════════════════════════
|
||||
20 console.log('second
|
||||
hunk');
|
||||
21 - const anotherOld =
|
||||
"... first 10 lines hidden ...
|
||||
'test';
|
||||
21 + const anotherNew =
|
||||
'test';
|
||||
@@ -108,11 +99,8 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
`;
|
||||
|
||||
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`] = `
|
||||
" 1 console.log('first hunk');
|
||||
2 - const oldVar = 1;
|
||||
2 + const newVar = 1;
|
||||
3 console.log('end of first hunk');
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
"... first 4 lines hidden ...
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
20 console.log('second hunk');
|
||||
21 - const anotherOld = 'test';
|
||||
21 + const anotherNew = 'test';
|
||||
@@ -124,7 +112,7 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
2 - const oldVar = 1;
|
||||
2 + const newVar = 1;
|
||||
3 console.log('end of first hunk');
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
20 console.log('second hunk');
|
||||
21 - const anotherOld = 'test';
|
||||
21 + const anotherNew = 'test';
|
||||
@@ -164,7 +152,7 @@ exports[`<OverflowProvider><DiffRenderer /></OverflowProvider> > with useAlterna
|
||||
" 1 context line 1
|
||||
2 - deleted line
|
||||
2 + added line
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
════════════════════════════════════════════════════════════════════════════════
|
||||
10 context line 10
|
||||
11 context line 11"
|
||||
`;
|
||||
|
||||
@@ -18,7 +18,7 @@ exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderM
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test **bold** and \`code\` markdown │"
|
||||
│ Test bold and code markdown │"
|
||||
`;
|
||||
|
||||
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=false '(default, regular buffer)' 1`] = `
|
||||
|
||||
@@ -1,326 +1,32 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolResultDisplay > falls back to plain text if availableHeight is set and not in alternate buffer 1`] = `"MaxSizedBox:Some result"`;
|
||||
exports[`ToolResultDisplay > does not fall back to plain text if availableHeight is set and not in alternate buffer 1`] = `"Some result"`;
|
||||
|
||||
exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with availableHeight 1`] = `"MarkdownDisplay: Some result"`;
|
||||
exports[`ToolResultDisplay > keeps markdown if in alternate buffer even with availableHeight 1`] = `"Some result"`;
|
||||
|
||||
exports[`ToolResultDisplay > renders ANSI output result 1`] = `"AnsiOutputText: {"text":"ansi content"}"`;
|
||||
exports[`ToolResultDisplay > renders ANSI output result 1`] = `"ansi content"`;
|
||||
|
||||
exports[`ToolResultDisplay > renders file diff result 1`] = `"DiffRenderer: test.ts - diff content"`;
|
||||
|
||||
exports[`ToolResultDisplay > renders nothing for todos result 1`] = `""`;
|
||||
|
||||
exports[`ToolResultDisplay > renders string result as markdown by default 1`] = `"MarkdownDisplay: Some result"`;
|
||||
exports[`ToolResultDisplay > renders string result as markdown by default 1`] = `"Some result"`;
|
||||
|
||||
exports[`ToolResultDisplay > renders string result as plain text when renderOutputAsMarkdown is false 1`] = `"MaxSizedBox:Some result"`;
|
||||
exports[`ToolResultDisplay > renders string result as plain text when renderOutputAsMarkdown is false 1`] = `"**Some result**"`;
|
||||
|
||||
exports[`ToolResultDisplay > truncates very long string results 1`] = `
|
||||
"MaxSizedBo...aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
"... first 251 lines hidden ...
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaa"
|
||||
`;
|
||||
|
||||
@@ -5,19 +5,14 @@
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { waitFor } from '../../../test-utils/async.js';
|
||||
import { OverflowProvider } from '../../contexts/OverflowContext.js';
|
||||
import { MaxSizedBox, setMaxSizedBoxDebugging } from './MaxSizedBox.js';
|
||||
import { MaxSizedBox } from './MaxSizedBox.js';
|
||||
import { Box, Text } from 'ink';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('<MaxSizedBox />', () => {
|
||||
// Make sure MaxSizedBox logs errors on invalid configurations.
|
||||
// This is useful for debugging issues with the component.
|
||||
// It should be set to false in production for performance and to avoid
|
||||
// cluttering the console if there are ignorable issues.
|
||||
setMaxSizedBoxDebugging(true);
|
||||
|
||||
it('renders children without truncation when they fit', () => {
|
||||
it('renders children without truncation when they fit', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
@@ -27,53 +22,105 @@ describe('<MaxSizedBox />', () => {
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals('Hello, World!');
|
||||
await waitFor(() => expect(lastFrame()).toContain('Hello, World!'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides lines when content exceeds maxHeight', () => {
|
||||
it('hides lines when content exceeds maxHeight', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
||||
<Box>
|
||||
<Box flexDirection="column">
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`... first 2 lines hidden ...
|
||||
Line 3`);
|
||||
await waitFor(() =>
|
||||
expect(lastFrame()).toContain('... first 2 lines hidden ...'),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', () => {
|
||||
it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
|
||||
<Box>
|
||||
<Box flexDirection="column">
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1
|
||||
... last 2 lines hidden ...`);
|
||||
await waitFor(() =>
|
||||
expect(lastFrame()).toContain('... last 2 lines hidden ...'),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wraps text that exceeds maxWidth', () => {
|
||||
it('shows plural "lines" when more than one line is hidden', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
||||
<Box flexDirection="column">
|
||||
<Text>Line 1</Text>
|
||||
<Text>Line 2</Text>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(lastFrame()).toContain('... first 2 lines hidden ...'),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows singular "line" when exactly one line is hidden', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={1}>
|
||||
<Box flexDirection="column">
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(lastFrame()).toContain('... first 1 line hidden ...'),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('accounts for additionalHiddenLinesCount', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={5}>
|
||||
<Box flexDirection="column">
|
||||
<Text>Line 1</Text>
|
||||
<Text>Line 2</Text>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(lastFrame()).toContain('... first 7 lines hidden ...'),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wraps text that exceeds maxWidth', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={10} maxHeight={5}>
|
||||
@@ -84,325 +131,66 @@ Line 3`);
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`This is a
|
||||
long line
|
||||
of text`);
|
||||
await waitFor(() => expect(lastFrame()).toContain('This is a'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles mixed wrapping and non-wrapping segments', () => {
|
||||
const multilineText = `This part will wrap around.
|
||||
And has a line break.
|
||||
Leading spaces preserved.`;
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={20} maxHeight={20}>
|
||||
<Box>
|
||||
<Text>Example</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>No Wrap: </Text>
|
||||
<Text wrap="wrap">{multilineText}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Longer No Wrap: </Text>
|
||||
<Text wrap="wrap">This part will wrap around.</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(
|
||||
`Example
|
||||
No Wrap: This part
|
||||
will wrap
|
||||
around.
|
||||
And has a
|
||||
line break.
|
||||
Leading
|
||||
spaces
|
||||
preserved.
|
||||
Longer No Wrap: This
|
||||
part
|
||||
will
|
||||
wrap
|
||||
arou
|
||||
nd.`,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles words longer than maxWidth by splitting them', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">Supercalifragilisticexpialidocious</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`... …
|
||||
istic
|
||||
expia
|
||||
lidoc
|
||||
ious`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not truncate when maxHeight is undefined', () => {
|
||||
it('does not truncate when maxHeight is undefined', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={undefined}>
|
||||
<Box>
|
||||
<Box flexDirection="column">
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1
|
||||
Line 2`);
|
||||
await waitFor(() => expect(lastFrame()).toContain('Line 1'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows plural "lines" when more than one line is hidden', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`... first 2 lines hidden ...
|
||||
Line 3`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows plural "lines" when more than one line is hidden and overflowDirection is bottom', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1
|
||||
... last 2 lines hidden ...`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders an empty box for empty children', () => {
|
||||
it('renders an empty box for empty children', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}></MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
// Expect an empty string or a box with nothing in it.
|
||||
// Ink renders an empty box as an empty string.
|
||||
expect(lastFrame()).equals('');
|
||||
// Use waitFor to ensure ResizeObserver has a chance to run
|
||||
await waitFor(() => expect(lastFrame()).toBeDefined());
|
||||
expect(lastFrame()?.trim()).equals('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wraps text with multi-byte unicode characters correctly', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">你好世界</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
// "你好" has a visual width of 4. "世界" has a visual width of 4.
|
||||
// With maxWidth=5, it should wrap after the second character.
|
||||
expect(lastFrame()).equals(`你好
|
||||
世界`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('wraps text with multi-byte emoji characters correctly', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">🐶🐶🐶🐶🐶</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
// Each "🐶" has a visual width of 2.
|
||||
// With maxWidth=5, it should wrap every 2 emojis.
|
||||
expect(lastFrame()).equals(`🐶🐶
|
||||
🐶🐶
|
||||
🐶`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('falls back to an ellipsis when width is extremely small', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={2} maxHeight={2}>
|
||||
<Box>
|
||||
<Text>No</Text>
|
||||
<Text wrap="wrap">wrap</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals('N…');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates long non-wrapping text with ellipsis', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={3} maxHeight={2}>
|
||||
<Box>
|
||||
<Text>ABCDE</Text>
|
||||
<Text wrap="wrap">wrap</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals('AB…');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates non-wrapping text containing line breaks', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={3} maxHeight={2}>
|
||||
<Box>
|
||||
<Text>{'A\nBCDE'}</Text>
|
||||
<Text wrap="wrap">wrap</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`A\n…`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('truncates emoji characters correctly with ellipsis', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={3} maxHeight={2}>
|
||||
<Box>
|
||||
<Text>🐶🐶🐶</Text>
|
||||
<Text wrap="wrap">wrap</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`🐶…`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows ellipsis for multiple rows with long non-wrapping text', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={3} maxHeight={3}>
|
||||
<Box>
|
||||
<Text>AAA</Text>
|
||||
<Text wrap="wrap">first</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>BBB</Text>
|
||||
<Text wrap="wrap">second</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>CCC</Text>
|
||||
<Text wrap="wrap">third</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`AA…\nBB…\nCC…`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('accounts for additionalHiddenLinesCount', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={5}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
// 1 line is hidden by overflow, 5 are additionally hidden.
|
||||
expect(lastFrame()).equals(`... first 7 lines hidden ...
|
||||
Line 3`);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles React.Fragment as a child', () => {
|
||||
it('handles React.Fragment as a child', async () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<>
|
||||
<Box>
|
||||
<Box flexDirection="column">
|
||||
<>
|
||||
<Text>Line 1 from Fragment</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2 from Fragment</Text>
|
||||
</Box>
|
||||
</>
|
||||
<Box>
|
||||
</>
|
||||
<Text>Line 3 direct child</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1 from Fragment
|
||||
Line 2 from Fragment
|
||||
Line 3 direct child`);
|
||||
await waitFor(() => expect(lastFrame()).toContain('Line 1 from Fragment'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('clips a long single text child from the top', () => {
|
||||
it('clips a long single text child from the top', async () => {
|
||||
const THIRTY_LINES = Array.from(
|
||||
{ length: 30 },
|
||||
(_, i) => `Line ${i + 1}`,
|
||||
).join('\n');
|
||||
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="top">
|
||||
<Box>
|
||||
<Text>{THIRTY_LINES}</Text>
|
||||
</Box>
|
||||
@@ -410,21 +198,18 @@ Line 3 direct child`);
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
const expected = [
|
||||
'... first 21 lines hidden ...',
|
||||
...Array.from({ length: 9 }, (_, i) => `Line ${22 + i}`),
|
||||
].join('\n');
|
||||
|
||||
expect(lastFrame()).equals(expected);
|
||||
await waitFor(() =>
|
||||
expect(lastFrame()).toContain('... first 21 lines hidden ...'),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('clips a long single text child from the bottom', () => {
|
||||
it('clips a long single text child from the bottom', async () => {
|
||||
const THIRTY_LINES = Array.from(
|
||||
{ length: 30 },
|
||||
(_, i) => `Line ${i + 1}`,
|
||||
).join('\n');
|
||||
|
||||
const { lastFrame, unmount } = render(
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="bottom">
|
||||
@@ -435,12 +220,10 @@ Line 3 direct child`);
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
const expected = [
|
||||
...Array.from({ length: 9 }, (_, i) => `Line ${i + 1}`),
|
||||
'... last 21 lines hidden ...',
|
||||
].join('\n');
|
||||
|
||||
expect(lastFrame()).equals(expected);
|
||||
await waitFor(() =>
|
||||
expect(lastFrame()).toContain('... last 21 lines hidden ...'),
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,15 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, { Fragment, useEffect, useId } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import stringWidth from 'string-width';
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
||||
import { Box, Text, ResizeObserver, type DOMElement } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { toCodePoints } from '../../utils/textUtils.js';
|
||||
import { useOverflowActions } from '../../contexts/OverflowContext.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
let enableDebugLog = false;
|
||||
|
||||
/**
|
||||
* Minimum height for the MaxSizedBox component.
|
||||
@@ -21,42 +17,10 @@ let enableDebugLog = false;
|
||||
*/
|
||||
export const MINIMUM_MAX_HEIGHT = 2;
|
||||
|
||||
export function setMaxSizedBoxDebugging(value: boolean) {
|
||||
enableDebugLog = value;
|
||||
}
|
||||
|
||||
function debugReportError(message: string, element: React.ReactNode) {
|
||||
if (!enableDebugLog) return;
|
||||
|
||||
if (!React.isValidElement(element)) {
|
||||
debugLogger.warn(
|
||||
message,
|
||||
`Invalid element: '${String(element)}' typeof=${typeof element}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let sourceMessage = '<Unknown file>';
|
||||
try {
|
||||
const elementWithSource = element as {
|
||||
_source?: { fileName?: string; lineNumber?: number };
|
||||
};
|
||||
const fileName = elementWithSource._source?.fileName;
|
||||
const lineNumber = elementWithSource._source?.lineNumber;
|
||||
sourceMessage = fileName ? `${fileName}:${lineNumber}` : '<Unknown file>';
|
||||
} catch (error) {
|
||||
debugLogger.warn('Error while trying to get file name:', error);
|
||||
}
|
||||
|
||||
debugLogger.warn(
|
||||
message,
|
||||
`${String(element.type)}. Source: ${sourceMessage}`,
|
||||
);
|
||||
}
|
||||
interface MaxSizedBoxProps {
|
||||
children?: React.ReactNode;
|
||||
maxWidth?: number;
|
||||
maxHeight: number | undefined;
|
||||
maxHeight?: number;
|
||||
overflowDirection?: 'top' | 'bottom';
|
||||
additionalHiddenLinesCount?: number;
|
||||
}
|
||||
@@ -64,41 +28,6 @@ interface MaxSizedBoxProps {
|
||||
/**
|
||||
* A React component that constrains the size of its children and provides
|
||||
* content-aware truncation when the content exceeds the specified `maxHeight`.
|
||||
*
|
||||
* `MaxSizedBox` requires a specific structure for its children to correctly
|
||||
* measure and render the content:
|
||||
*
|
||||
* 1. **Direct children must be `<Box>` elements.** Each `<Box>` represents a
|
||||
* single row of content.
|
||||
* 2. **Row `<Box>` elements must contain only `<Text>` elements.** These
|
||||
* `<Text>` elements can be nested and there are no restrictions to Text
|
||||
* element styling other than that non-wrapping text elements must be
|
||||
* before wrapping text elements.
|
||||
*
|
||||
* **Constraints:**
|
||||
* - **Box Properties:** Custom properties on the child `<Box>` elements are
|
||||
* ignored. In debug mode, runtime checks will report errors for any
|
||||
* unsupported properties.
|
||||
* - **Text Wrapping:** Within a single row, `<Text>` elements with no wrapping
|
||||
* (e.g., headers, labels) must appear before any `<Text>` elements that wrap.
|
||||
* - **Element Types:** Runtime checks will warn if unsupported element types
|
||||
* are used as children.
|
||||
*
|
||||
* @example
|
||||
* <MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
* <Box>
|
||||
* <Text>This is the first line.</Text>
|
||||
* </Box>
|
||||
* <Box>
|
||||
* <Text color="cyan" wrap="truncate">Non-wrapping Header: </Text>
|
||||
* <Text>This is the rest of the line which will wrap if it's too long.</Text>
|
||||
* </Box>
|
||||
* <Box>
|
||||
* <Text>
|
||||
* Line 3 with <Text color="yellow">nested styled text</Text> inside of it.
|
||||
* </Text>
|
||||
* </Box>
|
||||
* </MaxSizedBox>
|
||||
*/
|
||||
export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
||||
children,
|
||||
@@ -109,49 +38,50 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
||||
}) => {
|
||||
const id = useId();
|
||||
const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {};
|
||||
const observerRef = useRef<ResizeObserver | null>(null);
|
||||
const [contentHeight, setContentHeight] = useState(0);
|
||||
|
||||
const laidOutStyledText: StyledText[][] = [];
|
||||
const targetMaxHeight = Math.max(
|
||||
Math.round(maxHeight ?? Number.MAX_SAFE_INTEGER),
|
||||
MINIMUM_MAX_HEIGHT,
|
||||
const onRefChange = useCallback(
|
||||
(node: DOMElement | null) => {
|
||||
if (observerRef.current) {
|
||||
observerRef.current.disconnect();
|
||||
observerRef.current = null;
|
||||
}
|
||||
|
||||
if (node && maxHeight !== undefined) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (entry) {
|
||||
setContentHeight(entry.contentRect.height);
|
||||
}
|
||||
});
|
||||
observer.observe(node);
|
||||
observerRef.current = observer;
|
||||
}
|
||||
},
|
||||
[maxHeight],
|
||||
);
|
||||
|
||||
if (maxWidth === undefined) {
|
||||
throw new Error('maxWidth must be defined when maxHeight is set.');
|
||||
}
|
||||
function visitRows(element: React.ReactNode) {
|
||||
if (!React.isValidElement<{ children?: React.ReactNode }>(element)) {
|
||||
return;
|
||||
}
|
||||
const effectiveMaxHeight =
|
||||
maxHeight !== undefined
|
||||
? Math.max(Math.round(maxHeight), MINIMUM_MAX_HEIGHT)
|
||||
: undefined;
|
||||
|
||||
if (element.type === Fragment) {
|
||||
React.Children.forEach(element.props.children, visitRows);
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.type === Box) {
|
||||
layoutInkElementAsStyledText(element, maxWidth!, laidOutStyledText);
|
||||
return;
|
||||
}
|
||||
|
||||
debugReportError('MaxSizedBox children must be <Box> elements', element);
|
||||
}
|
||||
|
||||
React.Children.forEach(children, visitRows);
|
||||
|
||||
const contentWillOverflow =
|
||||
(targetMaxHeight !== undefined &&
|
||||
laidOutStyledText.length > targetMaxHeight) ||
|
||||
const isOverflowing =
|
||||
(effectiveMaxHeight !== undefined && contentHeight > effectiveMaxHeight) ||
|
||||
additionalHiddenLinesCount > 0;
|
||||
|
||||
// If we're overflowing, we need to hide at least 1 line for the message.
|
||||
const visibleContentHeight =
|
||||
contentWillOverflow && targetMaxHeight !== undefined
|
||||
? targetMaxHeight - 1
|
||||
: targetMaxHeight;
|
||||
isOverflowing && effectiveMaxHeight !== undefined
|
||||
? effectiveMaxHeight - 1
|
||||
: effectiveMaxHeight;
|
||||
|
||||
const hiddenLinesCount =
|
||||
visibleContentHeight !== undefined
|
||||
? Math.max(0, laidOutStyledText.length - visibleContentHeight)
|
||||
? Math.max(0, contentHeight - visibleContentHeight)
|
||||
: 0;
|
||||
|
||||
const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -166,36 +96,40 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
||||
};
|
||||
}, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]);
|
||||
|
||||
const visibleStyledText =
|
||||
hiddenLinesCount > 0
|
||||
? overflowDirection === 'top'
|
||||
? laidOutStyledText.slice(hiddenLinesCount, laidOutStyledText.length)
|
||||
: laidOutStyledText.slice(0, visibleContentHeight)
|
||||
: laidOutStyledText;
|
||||
if (effectiveMaxHeight === undefined) {
|
||||
return (
|
||||
<Box flexDirection="column" width={maxWidth}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const visibleLines = visibleStyledText.map((line, index) => (
|
||||
<Box key={index}>
|
||||
{line.length > 0 ? (
|
||||
line.map((segment, segIndex) => (
|
||||
<Text key={segIndex} {...segment.props}>
|
||||
{segment.text}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text> </Text>
|
||||
)}
|
||||
</Box>
|
||||
));
|
||||
const offset =
|
||||
hiddenLinesCount > 0 && overflowDirection === 'top' ? -hiddenLinesCount : 0;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width={maxWidth} flexShrink={0}>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
width={maxWidth}
|
||||
maxHeight={effectiveMaxHeight}
|
||||
flexShrink={0}
|
||||
>
|
||||
{totalHiddenLines > 0 && overflowDirection === 'top' && (
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
... first {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
|
||||
hidden ...
|
||||
</Text>
|
||||
)}
|
||||
{visibleLines}
|
||||
<Box flexDirection="column" overflow="hidden" flexGrow={1}>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
ref={onRefChange}
|
||||
flexShrink={0}
|
||||
marginTop={offset}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
{totalHiddenLines > 0 && overflowDirection === 'bottom' && (
|
||||
<Text color={theme.text.secondary} wrap="truncate">
|
||||
... last {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
|
||||
@@ -205,423 +139,3 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Define a type for styled text segments
|
||||
interface StyledText {
|
||||
text: string;
|
||||
props: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single row of content within the MaxSizedBox.
|
||||
*
|
||||
* A row can contain segments that are not wrapped, followed by segments that
|
||||
* are. This is a minimal implementation that only supports the functionality
|
||||
* needed today.
|
||||
*/
|
||||
interface Row {
|
||||
noWrapSegments: StyledText[];
|
||||
segments: StyledText[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the child elements of MaxSizedBox into an array of `Row` objects.
|
||||
*
|
||||
* This function expects a specific child structure to function correctly:
|
||||
* 1. The top-level child of `MaxSizedBox` should be a single `<Box>`. This
|
||||
* outer box is primarily for structure and is not directly rendered.
|
||||
* 2. Inside the outer `<Box>`, there should be one or more children. Each of
|
||||
* these children must be a `<Box>` that represents a row.
|
||||
* 3. Inside each "row" `<Box>`, the children must be `<Text>` components.
|
||||
*
|
||||
* The structure should look like this:
|
||||
* <MaxSizedBox>
|
||||
* <Box> // Row 1
|
||||
* <Text>...</Text>
|
||||
* <Text>...</Text>
|
||||
* </Box>
|
||||
* <Box> // Row 2
|
||||
* <Text>...</Text>
|
||||
* </Box>
|
||||
* </MaxSizedBox>
|
||||
*
|
||||
* It is an error for a <Text> child without wrapping to appear after a
|
||||
* <Text> child with wrapping within the same row Box.
|
||||
*
|
||||
* @param element The React node to flatten.
|
||||
* @returns An array of `Row` objects.
|
||||
*/
|
||||
function visitBoxRow(element: React.ReactNode): Row {
|
||||
if (
|
||||
!React.isValidElement<{ children?: React.ReactNode }>(element) ||
|
||||
element.type !== Box
|
||||
) {
|
||||
debugReportError(
|
||||
`All children of MaxSizedBox must be <Box> elements`,
|
||||
element,
|
||||
);
|
||||
return {
|
||||
noWrapSegments: [{ text: '<ERROR>', props: {} }],
|
||||
segments: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (enableDebugLog) {
|
||||
const boxProps = element.props as {
|
||||
children?: React.ReactNode;
|
||||
readonly flexDirection?:
|
||||
| 'row'
|
||||
| 'column'
|
||||
| 'row-reverse'
|
||||
| 'column-reverse';
|
||||
};
|
||||
// Ensure the Box has no props other than the default ones and key.
|
||||
let maxExpectedProps = 4;
|
||||
if (boxProps.children !== undefined) {
|
||||
// Allow the key prop, which is automatically added by React.
|
||||
maxExpectedProps += 1;
|
||||
}
|
||||
if (
|
||||
boxProps.flexDirection !== undefined &&
|
||||
boxProps.flexDirection !== 'row'
|
||||
) {
|
||||
debugReportError(
|
||||
'MaxSizedBox children must have flexDirection="row".',
|
||||
element,
|
||||
);
|
||||
}
|
||||
if (Object.keys(boxProps).length > maxExpectedProps) {
|
||||
debugReportError(
|
||||
`Boxes inside MaxSizedBox must not have additional props. ${Object.keys(
|
||||
boxProps,
|
||||
).join(', ')}`,
|
||||
element,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const row: Row = {
|
||||
noWrapSegments: [],
|
||||
segments: [],
|
||||
};
|
||||
|
||||
let hasSeenWrapped = false;
|
||||
|
||||
function visitRowChild(
|
||||
element: React.ReactNode,
|
||||
parentProps: Record<string, unknown> | undefined,
|
||||
) {
|
||||
if (element === null) {
|
||||
return;
|
||||
}
|
||||
if (typeof element === 'string' || typeof element === 'number') {
|
||||
const text = String(element);
|
||||
// Ignore empty strings as they don't need to be rendered.
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segment: StyledText = { text, props: parentProps ?? {} };
|
||||
|
||||
// Check the 'wrap' property from the merged props to decide the segment type.
|
||||
if (parentProps === undefined || parentProps['wrap'] === 'wrap') {
|
||||
hasSeenWrapped = true;
|
||||
row.segments.push(segment);
|
||||
} else {
|
||||
if (!hasSeenWrapped) {
|
||||
row.noWrapSegments.push(segment);
|
||||
} else {
|
||||
// put in the wrapped segment as the row is already stuck in wrapped mode.
|
||||
row.segments.push(segment);
|
||||
debugReportError(
|
||||
'Text elements without wrapping cannot appear after elements with wrapping in the same row.',
|
||||
element,
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!React.isValidElement<{ children?: React.ReactNode }>(element)) {
|
||||
debugReportError('Invalid element.', element);
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.type === Fragment) {
|
||||
React.Children.forEach(element.props.children, (child) =>
|
||||
visitRowChild(child, parentProps),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.type !== Text) {
|
||||
debugReportError(
|
||||
'Children of a row Box must be <Text> elements.',
|
||||
element,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Merge props from parent <Text> elements. Child props take precedence.
|
||||
const { children, ...currentProps } = element.props;
|
||||
const mergedProps =
|
||||
parentProps === undefined
|
||||
? currentProps
|
||||
: { ...parentProps, ...currentProps };
|
||||
React.Children.forEach(children, (child) =>
|
||||
visitRowChild(child, mergedProps),
|
||||
);
|
||||
}
|
||||
|
||||
React.Children.forEach(element.props.children, (child) =>
|
||||
visitRowChild(child, undefined),
|
||||
);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function layoutInkElementAsStyledText(
|
||||
element: React.ReactElement,
|
||||
maxWidth: number,
|
||||
output: StyledText[][],
|
||||
) {
|
||||
const row = visitBoxRow(element);
|
||||
if (row.segments.length === 0 && row.noWrapSegments.length === 0) {
|
||||
// Return a single empty line if there are no segments to display
|
||||
output.push([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const lines: StyledText[][] = [];
|
||||
const nonWrappingContent: StyledText[] = [];
|
||||
let noWrappingWidth = 0;
|
||||
|
||||
// First, lay out the non-wrapping segments
|
||||
row.noWrapSegments.forEach((segment) => {
|
||||
nonWrappingContent.push(segment);
|
||||
noWrappingWidth += stringWidth(segment.text);
|
||||
});
|
||||
|
||||
if (row.segments.length === 0) {
|
||||
// This is a bit of a special case when there are no segments that allow
|
||||
// wrapping. It would be ideal to unify.
|
||||
const lines: StyledText[][] = [];
|
||||
let currentLine: StyledText[] = [];
|
||||
nonWrappingContent.forEach((segment) => {
|
||||
const textLines = segment.text.split('\n');
|
||||
textLines.forEach((text, index) => {
|
||||
if (index > 0) {
|
||||
lines.push(currentLine);
|
||||
currentLine = [];
|
||||
}
|
||||
if (text) {
|
||||
currentLine.push({ text, props: segment.props });
|
||||
}
|
||||
});
|
||||
});
|
||||
if (
|
||||
currentLine.length > 0 ||
|
||||
(nonWrappingContent.length > 0 &&
|
||||
nonWrappingContent[nonWrappingContent.length - 1].text.endsWith('\n'))
|
||||
) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
for (const line of lines) {
|
||||
output.push(line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const availableWidth = maxWidth - noWrappingWidth;
|
||||
|
||||
if (availableWidth < 1) {
|
||||
// No room to render the wrapping segments. Truncate the non-wrapping
|
||||
// content and append an ellipsis so the line always fits within maxWidth.
|
||||
|
||||
// Handle line breaks in non-wrapping content when truncating
|
||||
const lines: StyledText[][] = [];
|
||||
let currentLine: StyledText[] = [];
|
||||
let currentLineWidth = 0;
|
||||
|
||||
for (const segment of nonWrappingContent) {
|
||||
const textLines = segment.text.split('\n');
|
||||
textLines.forEach((text, index) => {
|
||||
if (index > 0) {
|
||||
// New line encountered, finish current line and start new one
|
||||
lines.push(currentLine);
|
||||
currentLine = [];
|
||||
currentLineWidth = 0;
|
||||
}
|
||||
|
||||
if (text) {
|
||||
const textWidth = stringWidth(text);
|
||||
|
||||
// When there's no room for wrapping content, be very conservative
|
||||
// For lines after the first line break, show only ellipsis if the text would be truncated
|
||||
if (index > 0 && textWidth > 0) {
|
||||
// This is content after a line break - just show ellipsis to indicate truncation
|
||||
currentLine.push({ text: '…', props: {} });
|
||||
currentLineWidth = stringWidth('…');
|
||||
} else {
|
||||
// This is the first line or a continuation, try to fit what we can
|
||||
const maxContentWidth = Math.max(0, maxWidth - stringWidth('…'));
|
||||
|
||||
if (textWidth <= maxContentWidth && currentLineWidth === 0) {
|
||||
// Text fits completely on this line
|
||||
currentLine.push({ text, props: segment.props });
|
||||
currentLineWidth += textWidth;
|
||||
} else {
|
||||
// Text needs truncation
|
||||
const codePoints = toCodePoints(text);
|
||||
let truncatedWidth = currentLineWidth;
|
||||
let sliceEndIndex = 0;
|
||||
|
||||
for (const char of codePoints) {
|
||||
const charWidth = stringWidth(char);
|
||||
if (truncatedWidth + charWidth > maxContentWidth) {
|
||||
break;
|
||||
}
|
||||
truncatedWidth += charWidth;
|
||||
sliceEndIndex++;
|
||||
}
|
||||
|
||||
const slice = codePoints.slice(0, sliceEndIndex).join('');
|
||||
if (slice) {
|
||||
currentLine.push({ text: slice, props: segment.props });
|
||||
}
|
||||
currentLine.push({ text: '…', props: {} });
|
||||
currentLineWidth = truncatedWidth + stringWidth('…');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add the last line if it has content or if the last segment ended with \n
|
||||
if (
|
||||
currentLine.length > 0 ||
|
||||
(nonWrappingContent.length > 0 &&
|
||||
nonWrappingContent[nonWrappingContent.length - 1].text.endsWith('\n'))
|
||||
) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
|
||||
// If we don't have any lines yet, add an ellipsis line
|
||||
if (lines.length === 0) {
|
||||
lines.push([{ text: '…', props: {} }]);
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
output.push(line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, lay out the wrapping segments
|
||||
let wrappingPart: StyledText[] = [];
|
||||
let wrappingPartWidth = 0;
|
||||
|
||||
function addWrappingPartToLines() {
|
||||
if (lines.length === 0) {
|
||||
lines.push([...nonWrappingContent, ...wrappingPart]);
|
||||
} else {
|
||||
if (noWrappingWidth > 0) {
|
||||
lines.push([
|
||||
...[{ text: ' '.repeat(noWrappingWidth), props: {} }],
|
||||
...wrappingPart,
|
||||
]);
|
||||
} else {
|
||||
lines.push(wrappingPart);
|
||||
}
|
||||
}
|
||||
wrappingPart = [];
|
||||
wrappingPartWidth = 0;
|
||||
}
|
||||
|
||||
function addToWrappingPart(text: string, props: Record<string, unknown>) {
|
||||
if (
|
||||
wrappingPart.length > 0 &&
|
||||
wrappingPart[wrappingPart.length - 1].props === props
|
||||
) {
|
||||
wrappingPart[wrappingPart.length - 1].text += text;
|
||||
} else {
|
||||
wrappingPart.push({ text, props });
|
||||
}
|
||||
}
|
||||
|
||||
row.segments.forEach((segment) => {
|
||||
const linesFromSegment = segment.text.split('\n');
|
||||
|
||||
linesFromSegment.forEach((lineText, lineIndex) => {
|
||||
if (lineIndex > 0) {
|
||||
addWrappingPartToLines();
|
||||
}
|
||||
|
||||
const words = lineText.split(/(\s+)/); // Split by whitespace
|
||||
|
||||
words.forEach((word) => {
|
||||
if (!word) return;
|
||||
const wordWidth = stringWidth(word);
|
||||
|
||||
if (
|
||||
wrappingPartWidth + wordWidth > availableWidth &&
|
||||
wrappingPartWidth > 0
|
||||
) {
|
||||
addWrappingPartToLines();
|
||||
if (/^\s+$/.test(word)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (wordWidth > availableWidth) {
|
||||
// Word is too long, needs to be split across lines
|
||||
const wordAsCodePoints = toCodePoints(word);
|
||||
let remainingWordAsCodePoints = wordAsCodePoints;
|
||||
while (remainingWordAsCodePoints.length > 0) {
|
||||
let splitIndex = 0;
|
||||
let currentSplitWidth = 0;
|
||||
for (const char of remainingWordAsCodePoints) {
|
||||
const charWidth = stringWidth(char);
|
||||
if (
|
||||
wrappingPartWidth + currentSplitWidth + charWidth >
|
||||
availableWidth
|
||||
) {
|
||||
break;
|
||||
}
|
||||
currentSplitWidth += charWidth;
|
||||
splitIndex++;
|
||||
}
|
||||
|
||||
if (splitIndex > 0) {
|
||||
const part = remainingWordAsCodePoints
|
||||
.slice(0, splitIndex)
|
||||
.join('');
|
||||
addToWrappingPart(part, segment.props);
|
||||
wrappingPartWidth += stringWidth(part);
|
||||
remainingWordAsCodePoints =
|
||||
remainingWordAsCodePoints.slice(splitIndex);
|
||||
}
|
||||
|
||||
if (remainingWordAsCodePoints.length > 0) {
|
||||
addWrappingPartToLines();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addToWrappingPart(word, segment.props);
|
||||
wrappingPartWidth += wordWidth;
|
||||
}
|
||||
});
|
||||
});
|
||||
// Split omits a trailing newline, so we need to handle it here
|
||||
if (segment.text.endsWith('\n')) {
|
||||
addWrappingPartToLines();
|
||||
}
|
||||
});
|
||||
|
||||
if (wrappingPart.length > 0) {
|
||||
addWrappingPartToLines();
|
||||
}
|
||||
for (const line of lines) {
|
||||
output.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<MaxSizedBox /> > accounts for additionalHiddenLinesCount 1`] = `
|
||||
"... first 7 lines hidden ...
|
||||
Line 3"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > clips a long single text child from the bottom 1`] = `
|
||||
"Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
Line 4
|
||||
Line 5
|
||||
Line 6
|
||||
Line 7
|
||||
Line 8
|
||||
Line 9
|
||||
... last 21 lines hidden ..."
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > clips a long single text child from the top 1`] = `
|
||||
"... first 21 lines hidden ...
|
||||
Line 22
|
||||
Line 23
|
||||
Line 24
|
||||
Line 25
|
||||
Line 26
|
||||
Line 27
|
||||
Line 28
|
||||
Line 29
|
||||
Line 30"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > does not truncate when maxHeight is undefined 1`] = `
|
||||
"Line 1
|
||||
Line 2"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > handles React.Fragment as a child 1`] = `
|
||||
"Line 1 from Fragment
|
||||
Line 2 from Fragment
|
||||
Line 3 direct child"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > hides lines at the end when content exceeds maxHeight and overflowDirection is bottom 1`] = `
|
||||
"Line 1
|
||||
... last 2 lines hidden ..."
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > hides lines when content exceeds maxHeight 1`] = `
|
||||
"... first 2 lines hidden ...
|
||||
Line 3"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > renders children without truncation when they fit 1`] = `"Hello, World!"`;
|
||||
|
||||
exports[`<MaxSizedBox /> > shows plural "lines" when more than one line is hidden 1`] = `
|
||||
"... first 2 lines hidden ...
|
||||
Line 3"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > shows singular "line" when exactly one line is hidden 1`] = `
|
||||
"... first 1 line hidden ...
|
||||
Line 1"
|
||||
`;
|
||||
|
||||
exports[`<MaxSizedBox /> > wraps text that exceeds maxWidth 1`] = `
|
||||
"This is a
|
||||
long line
|
||||
of text"
|
||||
`;
|
||||
@@ -178,17 +178,8 @@ export function colorizeCode({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box key={index} minHeight={useMaxSizedBox ? undefined : 1}>
|
||||
{/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
|
||||
{showLineNumbers && useMaxSizedBox && (
|
||||
<Text color={activeTheme.colors.Gray}>
|
||||
{`${String(index + 1 + hiddenLinesCount).padStart(
|
||||
padWidth,
|
||||
' ',
|
||||
)} `}
|
||||
</Text>
|
||||
)}
|
||||
{showLineNumbers && !useMaxSizedBox && (
|
||||
<Box key={index} minHeight={1}>
|
||||
{showLineNumbers && (
|
||||
<Box
|
||||
minWidth={padWidth + 1}
|
||||
flexShrink={0}
|
||||
@@ -236,14 +227,8 @@ export function colorizeCode({
|
||||
const lines = codeToHighlight.split('\n');
|
||||
const padWidth = String(lines.length).length; // Calculate padding width based on number of lines
|
||||
const fallbackLines = lines.map((line, index) => (
|
||||
<Box key={index} minHeight={useMaxSizedBox ? undefined : 1}>
|
||||
{/* We have to render line numbers differently depending on whether we are using MaxSizeBox or not */}
|
||||
{showLineNumbers && useMaxSizedBox && (
|
||||
<Text color={activeTheme.defaultColor}>
|
||||
{`${String(index + 1).padStart(padWidth, ' ')} `}
|
||||
</Text>
|
||||
)}
|
||||
{showLineNumbers && !useMaxSizedBox && (
|
||||
<Box key={index} minHeight={1}>
|
||||
{showLineNumbers && (
|
||||
<Box
|
||||
minWidth={padWidth + 1}
|
||||
flexShrink={0}
|
||||
|
||||
Reference in New Issue
Block a user