perf(ui): optimize text buffer and highlighting for large inputs (#16782)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
N. Taylor Mullen
2026-01-16 09:33:13 -08:00
committed by GitHub
parent fcd860e1b0
commit be37c26c88
10 changed files with 469 additions and 76 deletions

View File

@@ -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');
});
});
});