feat: terse transformations of image paths in text buffer (#4924)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
Co-authored-by: owenofbrien <86964623+owenofbrien@users.noreply.github.com>
This commit is contained in:
Pyush Sinha
2025-12-23 12:46:09 -08:00
committed by GitHub
parent 02a36afc38
commit 873d10df42
9 changed files with 772 additions and 75 deletions

View File

@@ -134,3 +134,103 @@ describe('parseInputForHighlighting', () => {
]);
});
});
describe('parseInputForHighlighting with Transformations', () => {
const transformations = [
{
logStart: 10,
logEnd: 19,
logicalText: '@test.png',
collapsedText: '[Image test.png]',
},
];
it('should show collapsed transformation when cursor is not on it', () => {
const line = 'Check out @test.png';
const result = parseInputForHighlighting(
line,
0, // line index
transformations,
0, // cursor not on transformation
);
expect(result).toEqual([
{ text: 'Check out ', type: 'default' },
{ text: '[Image test.png]', type: 'file' },
]);
});
it('should show expanded transformation when cursor is on it', () => {
const line = 'Check out @test.png';
const result = parseInputForHighlighting(
line,
0, // line index
transformations,
11, // cursor on transformation
);
expect(result).toEqual([
{ text: 'Check out ', type: 'default' },
{ text: '@test.png', type: 'file' },
]);
});
it('should handle multiple transformations in a line', () => {
const line = 'Images: @test1.png and @test2.png';
const multiTransformations = [
{
logStart: 8,
logEnd: 18,
logicalText: '@test1.png',
collapsedText: '[Image test1.png]',
},
{
logStart: 23,
logEnd: 33,
logicalText: '@test2.png',
collapsedText: '[Image test2.png]',
},
];
// Cursor not on any transformation
let result = parseInputForHighlighting(line, 0, multiTransformations, 0);
expect(result).toEqual([
{ text: 'Images: ', type: 'default' },
{ text: '[Image test1.png]', type: 'file' },
{ text: ' and ', type: 'default' },
{ text: '[Image test2.png]', type: 'file' },
]);
// Cursor on first transformation
result = parseInputForHighlighting(line, 0, multiTransformations, 10);
expect(result).toEqual([
{ text: 'Images: ', type: 'default' },
{ text: '@test1.png', type: 'file' },
{ text: ' and ', type: 'default' },
{ text: '[Image test2.png]', type: 'file' },
]);
});
it('should handle empty transformations array', () => {
const line = 'Check out @test.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' },
]);
});
it('should handle cursor at transformation boundaries', () => {
const line = 'Check out @test.png';
const result = parseInputForHighlighting(
line,
0,
transformations,
10, // cursor at start of transformation
);
expect(result[1]).toEqual({ text: '@test.png', type: 'file' });
});
});

View File

@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Transformation } from '../components/shared/text-buffer.js';
import { cpLen, cpSlice } from './textUtils.js';
export type HighlightToken = {
@@ -20,57 +21,81 @@ const HIGHLIGHT_REGEX = /(^\/[a-zA-Z0-9_-]+|@(?:\\ |[^,\s;!?()[\]{}])+)/g;
export function parseInputForHighlighting(
text: string,
index: number,
transformations: Transformation[] = [],
cursorCol?: number,
): readonly HighlightToken[] {
HIGHLIGHT_REGEX.lastIndex = 0;
if (!text) {
return [{ text: '', type: 'default' }];
}
const parseUntransformedInput = (text: string): HighlightToken[] => {
const tokens: HighlightToken[] = [];
if (!text) return tokens;
HIGHLIGHT_REGEX.lastIndex = 0;
let last = 0;
let match: RegExpExecArray | null;
while ((match = HIGHLIGHT_REGEX.exec(text)) !== null) {
const [fullMatch] = match;
const matchIndex = match.index;
if (matchIndex > last) {
tokens.push({ text: text.slice(last, matchIndex), type: 'default' });
}
const type = fullMatch.startsWith('/') ? 'command' : 'file';
if (type === 'command' && index !== 0) {
tokens.push({ text: fullMatch, type: 'default' });
} else {
tokens.push({ text: fullMatch, type });
}
last = matchIndex + fullMatch.length;
}
if (last < text.length) {
tokens.push({ text: text.slice(last), type: 'default' });
}
return tokens;
};
const tokens: HighlightToken[] = [];
let lastIndex = 0;
let match;
while ((match = HIGHLIGHT_REGEX.exec(text)) !== null) {
const [fullMatch] = match;
const matchIndex = match.index;
let column = 0;
const sortedTransformations = (transformations ?? [])
.slice()
.sort((a, b) => a.logStart - b.logStart);
// Add the text before the match as a default token
if (matchIndex > lastIndex) {
tokens.push({
text: text.slice(lastIndex, matchIndex),
type: 'default',
});
}
for (const transformation of sortedTransformations) {
const textBeforeTransformation = cpSlice(
text,
column,
transformation.logStart,
);
tokens.push(...parseUntransformedInput(textBeforeTransformation));
// 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,
});
}
const isCursorInside =
typeof cursorCol === 'number' &&
cursorCol >= transformation.logStart &&
cursorCol <= transformation.logEnd;
const transformationText = isCursorInside
? transformation.logicalText
: transformation.collapsedText;
tokens.push({ text: transformationText, type: 'file' });
lastIndex = matchIndex + fullMatch.length;
}
// Add any remaining text after the last match
if (lastIndex < text.length) {
tokens.push({
text: text.slice(lastIndex),
type: 'default',
});
column = transformation.logEnd;
}
const textAfterFinalTransformation = cpSlice(text, column);
tokens.push(...parseUntransformedInput(textAfterFinalTransformation));
return tokens;
}
export function buildSegmentsForVisualSlice(
export function parseSegmentsFromTokens(
tokens: readonly HighlightToken[],
sliceStart: number,
sliceEnd: number,
@@ -102,6 +127,5 @@ export function buildSegmentsForVisualSlice(
tokenCpStart += tokenLen;
}
return segments;
}