mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
perf(ui): optimize table rendering by memoizing styled characters (#18770)
This commit is contained in:
@@ -314,4 +314,35 @@ describe('TableRenderer', () => {
|
||||
});
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'renders correctly when headers are empty but rows have data',
|
||||
headers: [] as string[],
|
||||
rows: [['Data 1', 'Data 2']],
|
||||
expected: ['Data 1', 'Data 2'],
|
||||
},
|
||||
{
|
||||
name: 'renders correctly when there are more headers than columns in rows',
|
||||
headers: ['Header 1', 'Header 2', 'Header 3'],
|
||||
rows: [['Data 1', 'Data 2']],
|
||||
expected: ['Header 1', 'Header 2', 'Header 3', 'Data 1', 'Data 2'],
|
||||
},
|
||||
])('$name', ({ headers, rows, expected }) => {
|
||||
const terminalWidth = 50;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<TableRenderer
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expected.forEach((text) => {
|
||||
expect(output).toContain(text);
|
||||
});
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,8 +28,7 @@ const MIN_COLUMN_WIDTH = 5;
|
||||
const COLUMN_PADDING = 2;
|
||||
const TABLE_MARGIN = 2;
|
||||
|
||||
const calculateWidths = (text: string) => {
|
||||
const styledChars = toStyledCharacters(text);
|
||||
const calculateWidths = (styledChars: StyledChar[]) => {
|
||||
const contentWidth = styledCharsWidth(styledChars);
|
||||
|
||||
const words: StyledChar[][] = wordBreakStyledChars(styledChars);
|
||||
@@ -60,31 +59,48 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
[headers],
|
||||
);
|
||||
|
||||
const styledHeaders = useMemo(
|
||||
() => cleanedHeaders.map((header) => toStyledCharacters(header)),
|
||||
[cleanedHeaders],
|
||||
);
|
||||
|
||||
const styledRows = useMemo(
|
||||
() => rows.map((row) => row.map((cell) => toStyledCharacters(cell))),
|
||||
[rows],
|
||||
);
|
||||
|
||||
const { wrappedHeaders, wrappedRows, adjustedWidths } = useMemo(() => {
|
||||
const numColumns = styledRows.reduce(
|
||||
(max, row) => Math.max(max, row.length),
|
||||
styledHeaders.length,
|
||||
);
|
||||
|
||||
// --- Define Constraints per Column ---
|
||||
const constraints = cleanedHeaders.map((header, colIndex) => {
|
||||
let { contentWidth: maxContentWidth, maxWordWidth } =
|
||||
calculateWidths(header);
|
||||
const constraints = Array.from({ length: numColumns }).map(
|
||||
(_, colIndex) => {
|
||||
const headerStyledChars = styledHeaders[colIndex] || [];
|
||||
let { contentWidth: maxContentWidth, maxWordWidth } =
|
||||
calculateWidths(headerStyledChars);
|
||||
|
||||
rows.forEach((row) => {
|
||||
const cell = row[colIndex] || '';
|
||||
const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } =
|
||||
calculateWidths(cell);
|
||||
styledRows.forEach((row) => {
|
||||
const cellStyledChars = row[colIndex] || [];
|
||||
const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } =
|
||||
calculateWidths(cellStyledChars);
|
||||
|
||||
maxContentWidth = Math.max(maxContentWidth, cellWidth);
|
||||
maxWordWidth = Math.max(maxWordWidth, cellWordWidth);
|
||||
});
|
||||
maxContentWidth = Math.max(maxContentWidth, cellWidth);
|
||||
maxWordWidth = Math.max(maxWordWidth, cellWordWidth);
|
||||
});
|
||||
|
||||
const minWidth = maxWordWidth;
|
||||
const maxWidth = Math.max(minWidth, maxContentWidth);
|
||||
const minWidth = maxWordWidth;
|
||||
const maxWidth = Math.max(minWidth, maxContentWidth);
|
||||
|
||||
return { minWidth, maxWidth };
|
||||
});
|
||||
return { minWidth, maxWidth };
|
||||
},
|
||||
);
|
||||
|
||||
// --- Calculate Available Space ---
|
||||
// Fixed overhead: borders (n+1) + padding (2n)
|
||||
const fixedOverhead =
|
||||
cleanedHeaders.length + 1 + cleanedHeaders.length * COLUMN_PADDING;
|
||||
const fixedOverhead = numColumns + 1 + numColumns * COLUMN_PADDING;
|
||||
const availableWidth = Math.max(
|
||||
0,
|
||||
terminalWidth - fixedOverhead - TABLE_MARGIN,
|
||||
@@ -136,17 +152,18 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
}
|
||||
|
||||
// --- Pre-wrap and Optimize Widths ---
|
||||
const actualColumnWidths = new Array(cleanedHeaders.length).fill(0);
|
||||
const actualColumnWidths = new Array(numColumns).fill(0);
|
||||
|
||||
const wrapAndProcessRow = (row: string[]) => {
|
||||
const wrapAndProcessRow = (row: StyledChar[][]) => {
|
||||
const rowResult: ProcessedLine[][] = [];
|
||||
row.forEach((cell, colIndex) => {
|
||||
// Ensure we iterate up to numColumns, filling with empty cells if needed
|
||||
for (let colIndex = 0; colIndex < numColumns; colIndex++) {
|
||||
const cellStyledChars = row[colIndex] || [];
|
||||
const allocatedWidth = finalContentWidths[colIndex];
|
||||
const contentWidth = Math.max(1, allocatedWidth);
|
||||
|
||||
const contentStyledChars = toStyledCharacters(cell);
|
||||
const wrappedStyledLines = wrapStyledChars(
|
||||
contentStyledChars,
|
||||
cellStyledChars,
|
||||
contentWidth,
|
||||
);
|
||||
|
||||
@@ -161,19 +178,18 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
width: styledCharsWidth(line),
|
||||
}));
|
||||
rowResult.push(lines);
|
||||
});
|
||||
}
|
||||
return rowResult;
|
||||
};
|
||||
|
||||
const wrappedHeaders = wrapAndProcessRow(cleanedHeaders);
|
||||
const wrappedRows = rows.map((row) => wrapAndProcessRow(row));
|
||||
const wrappedHeaders = wrapAndProcessRow(styledHeaders);
|
||||
const wrappedRows = styledRows.map((row) => wrapAndProcessRow(row));
|
||||
|
||||
// Use the TIGHTEST widths that fit the wrapped content + padding
|
||||
const adjustedWidths = actualColumnWidths.map((w) => w + COLUMN_PADDING);
|
||||
|
||||
return { wrappedHeaders, wrappedRows, adjustedWidths };
|
||||
}, [cleanedHeaders, rows, terminalWidth]);
|
||||
|
||||
}, [styledHeaders, styledRows, terminalWidth]);
|
||||
// Helper function to render a cell with proper width
|
||||
const renderCell = (
|
||||
content: ProcessedLine,
|
||||
|
||||
@@ -44,6 +44,26 @@ exports[`TableRenderer > 'renders a table with only emojis and …' 1`] = `
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`TableRenderer > 'renders correctly when headers are em…' 1`] = `
|
||||
"
|
||||
┌────────┬────────┐
|
||||
│ │ │
|
||||
├────────┼────────┤
|
||||
│ Data 1 │ Data 2 │
|
||||
└────────┴────────┘
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`TableRenderer > 'renders correctly when there are more…' 1`] = `
|
||||
"
|
||||
┌──────────┬──────────┬──────────┐
|
||||
│ Header 1 │ Header 2 │ Header 3 │
|
||||
├──────────┼──────────┼──────────┤
|
||||
│ Data 1 │ Data 2 │ │
|
||||
└──────────┴──────────┴──────────┘
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`TableRenderer > handles wrapped bold headers without showing markers 1`] = `
|
||||
"
|
||||
┌─────────────┬───────┬─────────┐
|
||||
|
||||
Reference in New Issue
Block a user