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
+2 -2
View File
@@ -212,13 +212,13 @@ describe('parseInputForHighlighting with Transformations', () => {
});
it('should handle empty transformations array', () => {
const line = 'Check out @test.png';
const line = 'Check out @test_no_transform.png';
const result = parseInputForHighlighting(line, 0, [], 0);
// Should fall back to default highlighting
expect(result).toEqual([
{ text: 'Check out ', type: 'default' },
{ text: '@test.png', type: 'file' },
{ text: '@test_no_transform.png', type: 'file' },
]);
});
+24 -1
View File
@@ -4,8 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { LruCache } from '@google/gemini-cli-core';
import type { Transformation } from '../components/shared/text-buffer.js';
import { cpLen, cpSlice } from './textUtils.js';
import { LRU_BUFFER_PERF_CACHE_LIMIT } from '../constants.js';
export type HighlightToken = {
text: string;
@@ -18,12 +20,30 @@ export type HighlightToken = {
// semicolon, common punctuation, and brackets.
const HIGHLIGHT_REGEX = /(^\/[a-zA-Z0-9_-]+|@(?:\\ |[^,\s;!?()[\]{}])+)/g;
const highlightCache = new LruCache<string, readonly HighlightToken[]>(
LRU_BUFFER_PERF_CACHE_LIMIT,
);
export function parseInputForHighlighting(
text: string,
index: number,
transformations: Transformation[] = [],
cursorCol?: number,
): readonly HighlightToken[] {
let isCursorInsideTransform = false;
if (cursorCol !== undefined) {
for (const transform of transformations) {
if (cursorCol >= transform.logStart && cursorCol <= transform.logEnd) {
isCursorInsideTransform = true;
break;
}
}
}
const cacheKey = `${index === 0 ? 'F' : 'N'}:${isCursorInsideTransform ? cursorCol : 'NC'}:${text}`;
const cached = highlightCache.get(cacheKey);
if (cached !== undefined) return cached;
HIGHLIGHT_REGEX.lastIndex = 0;
if (!text) {
@@ -79,7 +99,7 @@ export function parseInputForHighlighting(
tokens.push(...parseUntransformedInput(textBeforeTransformation));
const isCursorInside =
typeof cursorCol === 'number' &&
cursorCol !== undefined &&
cursorCol >= transformation.logStart &&
cursorCol <= transformation.logEnd;
const transformationText = isCursorInside
@@ -92,6 +112,9 @@ export function parseInputForHighlighting(
const textAfterFinalTransformation = cpSlice(text, column);
tokens.push(...parseUntransformedInput(textAfterFinalTransformation));
highlightCache.set(cacheKey, tokens);
return tokens;
}
+21 -12
View File
@@ -8,6 +8,8 @@ import stripAnsi from 'strip-ansi';
import ansiRegex from 'ansi-regex';
import { stripVTControlCharacters } from 'node:util';
import stringWidth from 'string-width';
import { LruCache } from '@google/gemini-cli-core';
import { LRU_BUFFER_PERF_CACHE_LIMIT } from '../constants.js';
/**
* Calculates the maximum width of a multi-line ASCII art string.
@@ -28,9 +30,11 @@ export const getAsciiArtWidth = (asciiArt: string): number => {
* code units so that surrogatepair emoji count as one "column".)
* ---------------------------------------------------------------------- */
// Cache for code points to reduce GC pressure
const codePointsCache = new Map<string, string[]>();
// Cache for code points
const MAX_STRING_LENGTH_TO_CACHE = 1000;
const codePointsCache = new LruCache<string, string[]>(
LRU_BUFFER_PERF_CACHE_LIMIT,
);
export function toCodePoints(str: string): string[] {
// ASCII fast path - check if all chars are ASCII (0-127)
@@ -48,14 +52,14 @@ export function toCodePoints(str: string): string[] {
// Cache short strings
if (str.length <= MAX_STRING_LENGTH_TO_CACHE) {
const cached = codePointsCache.get(str);
if (cached) {
if (cached !== undefined) {
return cached;
}
}
const result = Array.from(str);
// Cache result (unlimited like Ink)
// Cache result
if (str.length <= MAX_STRING_LENGTH_TO_CACHE) {
codePointsCache.set(str, result);
}
@@ -119,21 +123,26 @@ export function stripUnsafeCharacters(str: string): string {
.join('');
}
// String width caching for performance optimization
const stringWidthCache = new Map<string, number>();
const stringWidthCache = new LruCache<string, number>(
LRU_BUFFER_PERF_CACHE_LIMIT,
);
/**
* Cached version of stringWidth function for better performance
* Follows Ink's approach with unlimited cache (no eviction)
*/
export const getCachedStringWidth = (str: string): number => {
// ASCII printable chars have width 1
if (/^[\x20-\x7E]*$/.test(str)) {
return str.length;
// ASCII printable chars (32-126) have width 1.
// This is a very frequent path, so we use a fast numeric check.
if (str.length === 1) {
const code = str.charCodeAt(0);
if (code >= 0x20 && code <= 0x7e) {
return 1;
}
}
if (stringWidthCache.has(str)) {
return stringWidthCache.get(str)!;
const cached = stringWidthCache.get(str);
if (cached !== undefined) {
return cached;
}
let width: number;