mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-06 11:21:15 -07:00
perf(ui): optimize text buffer and highlighting for large inputs (#16782)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
@@ -4,10 +4,13 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../../test-utils/render.js';
|
||||
import {
|
||||
renderHook,
|
||||
renderHookWithProviders,
|
||||
} from '../../../test-utils/render.js';
|
||||
import type {
|
||||
Viewport,
|
||||
TextBuffer,
|
||||
@@ -55,6 +58,10 @@ const initialState: TextBufferState = {
|
||||
};
|
||||
|
||||
describe('textBufferReducer', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return the initial state if state is undefined', () => {
|
||||
const action = { type: 'unknown_action' } as unknown as TextBufferAction;
|
||||
const state = textBufferReducer(initialState, action);
|
||||
@@ -381,6 +388,10 @@ describe('useTextBuffer', () => {
|
||||
viewport = { width: 10, height: 3 }; // Default viewport for tests
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
it('should initialize with empty text and cursor at (0,0) by default', () => {
|
||||
const { result } = renderHook(() =>
|
||||
@@ -2371,6 +2382,10 @@ describe('Unicode helper functions', () => {
|
||||
});
|
||||
|
||||
describe('Transformation Utilities', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getTransformedImagePath', () => {
|
||||
it('should transform a simple image path', () => {
|
||||
expect(getTransformedImagePath('@test.png')).toBe('[Image test.png]');
|
||||
@@ -2547,4 +2562,125 @@ describe('Transformation Utilities', () => {
|
||||
expect(result.transformedToLogMap).toEqual([0]); // Just the trailing position
|
||||
});
|
||||
});
|
||||
|
||||
describe('Layout Caching and Invalidation', () => {
|
||||
it.each([
|
||||
{
|
||||
desc: 'via setText',
|
||||
actFn: (result: { current: TextBuffer }) =>
|
||||
result.current.setText('changed line'),
|
||||
expected: 'changed line',
|
||||
},
|
||||
{
|
||||
desc: 'via replaceRange',
|
||||
actFn: (result: { current: TextBuffer }) =>
|
||||
result.current.replaceRange(0, 0, 0, 13, 'changed line'),
|
||||
expected: 'changed line',
|
||||
},
|
||||
])(
|
||||
'should invalidate cache when line content changes $desc',
|
||||
({ actFn, expected }) => {
|
||||
const viewport = { width: 80, height: 24 };
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useTextBuffer({
|
||||
initialText: 'original line',
|
||||
viewport,
|
||||
isValidPath: () => true,
|
||||
}),
|
||||
);
|
||||
|
||||
const originalLayout = result.current.visualLayout;
|
||||
|
||||
act(() => {
|
||||
actFn(result);
|
||||
});
|
||||
|
||||
expect(result.current.visualLayout).not.toBe(originalLayout);
|
||||
expect(result.current.allVisualLines[0]).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should invalidate cache when viewport width changes', () => {
|
||||
const viewport = { width: 80, height: 24 };
|
||||
const { result, rerender } = renderHookWithProviders(
|
||||
({ vp }) =>
|
||||
useTextBuffer({
|
||||
initialText:
|
||||
'a very long line that will wrap when the viewport is small',
|
||||
viewport: vp,
|
||||
isValidPath: () => true,
|
||||
}),
|
||||
{ initialProps: { vp: viewport } },
|
||||
);
|
||||
|
||||
const originalLayout = result.current.visualLayout;
|
||||
|
||||
// Shrink viewport to force wrapping change
|
||||
rerender({ vp: { width: 10, height: 24 } });
|
||||
|
||||
expect(result.current.visualLayout).not.toBe(originalLayout);
|
||||
expect(result.current.allVisualLines.length).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('should correctly handle cursor expansion/collapse in cached layout', () => {
|
||||
const viewport = { width: 80, height: 24 };
|
||||
const text = 'Check @image.png here';
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useTextBuffer({
|
||||
initialText: text,
|
||||
viewport,
|
||||
isValidPath: () => true,
|
||||
}),
|
||||
);
|
||||
|
||||
// Cursor at start (collapsed)
|
||||
act(() => {
|
||||
result.current.moveToOffset(0);
|
||||
});
|
||||
expect(result.current.allVisualLines[0]).toContain('[Image image.png]');
|
||||
|
||||
// Move cursor onto the @path (expanded)
|
||||
act(() => {
|
||||
result.current.moveToOffset(7); // onto @
|
||||
});
|
||||
expect(result.current.allVisualLines[0]).toContain('@image.png');
|
||||
expect(result.current.allVisualLines[0]).not.toContain(
|
||||
'[Image image.png]',
|
||||
);
|
||||
|
||||
// Move cursor away (collapsed again)
|
||||
act(() => {
|
||||
result.current.moveToOffset(0);
|
||||
});
|
||||
expect(result.current.allVisualLines[0]).toContain('[Image image.png]');
|
||||
});
|
||||
|
||||
it('should reuse cache for unchanged lines during editing', () => {
|
||||
const viewport = { width: 80, height: 24 };
|
||||
const initialText = 'line 1\nline 2\nline 3';
|
||||
const { result } = renderHookWithProviders(() =>
|
||||
useTextBuffer({
|
||||
initialText,
|
||||
viewport,
|
||||
isValidPath: () => true,
|
||||
}),
|
||||
);
|
||||
|
||||
const layout1 = result.current.visualLayout;
|
||||
|
||||
// Edit line 1
|
||||
act(() => {
|
||||
result.current.moveToOffset(0);
|
||||
result.current.insert('X');
|
||||
});
|
||||
|
||||
const layout2 = result.current.visualLayout;
|
||||
expect(layout2).not.toBe(layout1);
|
||||
|
||||
// Verify that visual lines for line 2 and 3 (indices 1 and 2 in visualLines)
|
||||
// are identical in content if not in object reference (the arrays are rebuilt, but contents are cached)
|
||||
expect(result.current.allVisualLines[1]).toBe('line 2');
|
||||
expect(result.current.allVisualLines[2]).toBe('line 3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user