镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Decoded percent-encoded characters in repo names derived from direct generic git URLs. [#1666](https://github.com/sourcebot-dev/sourcebot/pull/1666)
- Fixed `.gitattributes` `linguist-language` overrides being ignored for files in subdirectories. [#1692](https://github.com/sourcebot-dev/sourcebot/pull/1692)
- Fixed selected language and repository filters disappearing when filtering the available options. [#1685](https://github.com/sourcebot-dev/sourcebot/pull/1685)
- Fixed code highlights not covering every line of a multi-line range. [#1699](https://github.com/sourcebot-dev/sourcebot/pull/1699)

## [5.1.15] - 2026-09-29

Expand Down
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
Loading