mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 02:24:09 -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:
@@ -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' },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 surrogate‑pair 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;
|
||||
|
||||
Reference in New Issue
Block a user