Files
gemini-cli/packages/cli/src/ui/utils/highlight.ts
2025-09-17 20:17:50 +00:00

104 lines
2.5 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { cpLen, cpSlice } from './textUtils.js';
export type HighlightToken = {
text: string;
type: 'default' | 'command' | 'file';
};
const HIGHLIGHT_REGEX = /(^\/[a-zA-Z0-9_-]+|@(?:\\ |[a-zA-Z0-9_./-])+)/g;
export function parseInputForHighlighting(
text: string,
index: number,
): readonly HighlightToken[] {
if (!text) {
return [{ text: '', type: 'default' }];
}
const tokens: HighlightToken[] = [];
let lastIndex = 0;
let match;
while ((match = HIGHLIGHT_REGEX.exec(text)) !== null) {
const [fullMatch] = match;
const matchIndex = match.index;
// Add the text before the match as a default token
if (matchIndex > lastIndex) {
tokens.push({
text: text.slice(lastIndex, matchIndex),
type: 'default',
});
}
// Add the matched token
const type = fullMatch.startsWith('/') ? 'command' : 'file';
// Only highlight slash commands if the index is 0.
if (type === 'command' && index !== 0) {
tokens.push({
text: fullMatch,
type: 'default',
});
} else {
tokens.push({
text: fullMatch,
type,
});
}
lastIndex = matchIndex + fullMatch.length;
}
// Add any remaining text after the last match
if (lastIndex < text.length) {
tokens.push({
text: text.slice(lastIndex),
type: 'default',
});
}
return tokens;
}
export function buildSegmentsForVisualSlice(
tokens: readonly HighlightToken[],
sliceStart: number,
sliceEnd: number,
): readonly HighlightToken[] {
if (sliceStart >= sliceEnd) return [];
const segments: HighlightToken[] = [];
let tokenCpStart = 0;
for (const token of tokens) {
const tokenLen = cpLen(token.text);
const tokenStart = tokenCpStart;
const tokenEnd = tokenStart + tokenLen;
const overlapStart = Math.max(tokenStart, sliceStart);
const overlapEnd = Math.min(tokenEnd, sliceEnd);
if (overlapStart < overlapEnd) {
const sliceStartInToken = overlapStart - tokenStart;
const sliceEndInToken = overlapEnd - tokenStart;
const rawSlice = cpSlice(token.text, sliceStartInToken, sliceEndInToken);
const last = segments[segments.length - 1];
if (last && last.type === token.type) {
last.text += rawSlice;
} else {
segments.push({ type: token.type, text: rawSlice });
}
}
tokenCpStart += tokenLen;
}
return segments;
}