Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it } from 'vitest';
import { SourceRange } from '@/features/search';
import { computeLineHighlightRanges } from './lightweightCodeHighlighter';

describe('computeLineHighlightRanges', () => {
it('returns empty array when highlightRanges is undefined or empty', () => {
expect(computeLineHighlightRanges(undefined, 1, 20)).toEqual([]);
expect(computeLineHighlightRanges([], 1, 20)).toEqual([]);
});

it('correctly maps a single-line range on the target line and ignores other lines', () => {
const ranges: SourceRange[] = [
{
start: { lineNumber: 2, column: 5, byteOffset: 4 },
end: { lineNumber: 2, column: 12, byteOffset: 11 },
},
];

// Before target line
expect(computeLineHighlightRanges(ranges, 1, 30)).toEqual([]);

// On target line: column 5 (0-indexed 4) to column 12 (0-indexed 11)
expect(computeLineHighlightRanges(ranges, 2, 30)).toEqual([
{ from: 4, to: 11 },
]);

// After target line
expect(computeLineHighlightRanges(ranges, 3, 30)).toEqual([]);
});

it('correctly splits multi-line ranges across start, intermediate, and end lines', () => {
const multilineRange: SourceRange = {
start: { lineNumber: 1, column: 15, byteOffset: 14 },
end: { lineNumber: 3, column: 10, byteOffset: 50 },
};

const line1 = 'const handleOrder = async ('; // length 27
const line2 = ' userId: string,'; // length 19
const line3 = ' items: CartItem[]'; // length 21
const line4 = ');'; // length 2

// Line 1: Starts at column 15 (0-indexed 14) and highlights to end of line
expect(computeLineHighlightRanges([multilineRange], 1, line1.length)).toEqual([
{ from: 14, to: line1.length },
]);

// Line 2 (intermediate line): Highlights entire line (0 to length)
expect(computeLineHighlightRanges([multilineRange], 2, line2.length)).toEqual([
{ from: 0, to: line2.length },
]);

// Line 3 (end line): Highlights from beginning (0) to end column (0-indexed 9)
expect(computeLineHighlightRanges([multilineRange], 3, line3.length)).toEqual([
{ from: 0, to: 9 },
]);

// Line 4: Outside range
expect(computeLineHighlightRanges([multilineRange], 4, line4.length)).toEqual([]);
});

it('clamps end column to lineLength if end column exceeds line length', () => {
const range: SourceRange = {
start: { lineNumber: 1, column: 1, byteOffset: 0 },
end: { lineNumber: 1, column: 50, byteOffset: 49 },
};

expect(computeLineHighlightRanges([range], 1, 20)).toEqual([
{ from: 0, to: 20 },
]);
});

it('filters out invalid or empty ranges where to <= from', () => {
const emptyRange: SourceRange = {
start: { lineNumber: 1, column: 5, byteOffset: 4 },
end: { lineNumber: 1, column: 5, byteOffset: 4 },
};

expect(computeLineHighlightRanges([emptyRange], 1, 20)).toEqual([]);
});

it('handles multiple ranges on the same line', () => {
const ranges: SourceRange[] = [
{
start: { lineNumber: 1, column: 1, byteOffset: 0 },
end: { lineNumber: 1, column: 5, byteOffset: 4 },
},
{
start: { lineNumber: 1, column: 10, byteOffset: 9 },
end: { lineNumber: 1, column: 15, byteOffset: 14 },
},
];

expect(computeLineHighlightRanges(ranges, 1, 30)).toEqual([
{ from: 0, to: 4 },
{ from: 9, to: 14 },
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,37 @@ const MAX_NUMBER_OF_CHARACTER_PER_LINE = 1000;
*
* Inspired by: https://github.com/craftzdog/react-codemirror-runmode
*/
/**
* Calculates 0-based character ranges to highlight on a specific line from 1-based SourceRanges,
* handling single-line, start, intermediate, and end lines of multi-line ranges.
*/
export const computeLineHighlightRanges = (
highlightRanges: SourceRange[] | undefined,
lineNumber: number,
lineLength: number,
): { from: number; to: number }[] => {
if (!highlightRanges || highlightRanges.length === 0) {
return [];
}

return highlightRanges
.filter(
(range) =>
lineNumber >= range.start.lineNumber &&
lineNumber <= range.end.lineNumber,
)
.map((range) => {
const isStart = range.start.lineNumber === lineNumber;
const isEnd = range.end.lineNumber === lineNumber;

const from = isStart ? Math.max(0, range.start.column - 1) : 0;
const to = isEnd ? Math.min(lineLength, range.end.column - 1) : lineLength;

return { from, to };
})
.filter((range) => range.to > range.from);
};

export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((props: LightweightCodeHighlighter) => {
const {
language,
Expand Down Expand Up @@ -64,13 +95,11 @@ export const LightweightCodeHighlighter = memo<LightweightCodeHighlighter>((prop
.map(async (line, index) => {
const lineNumber = index + lineNumbersOffset;

// @todo: we will need to handle the case where a range spans multiple lines.
const ranges = highlightRanges?.filter(range => {
return range.start.lineNumber === lineNumber || range.end.lineNumber === lineNumber;
}).map(range => ({
from: range.start.column - 1,
to: range.end.column - 1,
}));
const ranges = computeLineHighlightRanges(
highlightRanges,
lineNumber,
line.length,
);

const snippets = await highlightCode(
language,
Expand Down