File references in chat messages can now use the 'path:start-end' form (e.g. 'src/foo.ts:120-145'). The reference becomes clickable in the renderer and, on click, the file opens at the start line. Range selection is intentionally not done at this layer — the 'path:start-end' form is parsed only so the link resolves to the correct path; navigation jumps to the start line, matching the behavior of the existing 'path:line' form. - Extract the file-reference parser to a dedicated module so it can be unit-tested without pulling in the markdown renderer's worker dependencies. - Add a range branch to 'parseFileReference' and update the block-code path regex to recognize the new form. - Switch the colon-form regex to a non-greedy path match so 'path:line:col' is no longer mis-parsed as 'path:line' with the first numeric suffix dropped into the path. - Add a unit test covering the new and existing parser forms.
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
|
|
import { parseFileReference, type ParsedFileReference } from './fileReferenceParser';
|
|
|
|
const parse = (value: string): ParsedFileReference | null => parseFileReference(value);
|
|
|
|
describe('parseFileReference', () => {
|
|
test('returns null for empty or whitespace input', () => {
|
|
expect(parse('')).toBeNull();
|
|
expect(parse(' ')).toBeNull();
|
|
});
|
|
|
|
test('parses bare path', () => {
|
|
expect(parse('src/foo.ts')).toEqual({ path: 'src/foo.ts' });
|
|
});
|
|
|
|
test('parses path with single line', () => {
|
|
expect(parse('src/foo.ts:42')).toEqual({ path: 'src/foo.ts', line: 42 });
|
|
});
|
|
|
|
test('parses path with line and column', () => {
|
|
expect(parse('src/foo.ts:42:8')).toEqual({ path: 'src/foo.ts', line: 42, column: 8 });
|
|
});
|
|
|
|
test('parses path with line range', () => {
|
|
expect(parse('src/foo.ts:42-58')).toEqual({
|
|
path: 'src/foo.ts',
|
|
line: 42,
|
|
endLine: 58,
|
|
});
|
|
});
|
|
|
|
test('parses path with single-line range (start equals end)', () => {
|
|
expect(parse('src/foo.ts:10-10')).toEqual({
|
|
path: 'src/foo.ts',
|
|
line: 10,
|
|
endLine: 10,
|
|
});
|
|
});
|
|
|
|
test('rejects range with end before start', () => {
|
|
expect(parse('src/foo.ts:20-10')).toBeNull();
|
|
});
|
|
|
|
test('falls back to path-only when range endpoint is non-numeric', () => {
|
|
// `src/foo.ts:10-abc` and `src/foo.ts:abc-20` are malformed; the
|
|
// line info is discarded and only the path is returned (the trailing
|
|
// `:`-suffix is stripped).
|
|
expect(parse('src/foo.ts:10-abc')).toEqual({ path: 'src/foo.ts' });
|
|
expect(parse('src/foo.ts:abc-20')).toEqual({ path: 'src/foo.ts' });
|
|
});
|
|
|
|
test('strips backtick and quote wrapping from range forms', () => {
|
|
expect(parse('`src/foo.ts:10-20`')).toEqual({
|
|
path: 'src/foo.ts',
|
|
line: 10,
|
|
endLine: 20,
|
|
});
|
|
expect(parse('"src/foo.ts:1-3"')).toEqual({
|
|
path: 'src/foo.ts',
|
|
line: 1,
|
|
endLine: 3,
|
|
});
|
|
});
|
|
|
|
test('parses absolute Windows path with line range', () => {
|
|
expect(parse('C:/repo/src/foo.ts:5-9')).toEqual({
|
|
path: 'C:/repo/src/foo.ts',
|
|
line: 5,
|
|
endLine: 9,
|
|
});
|
|
});
|
|
|
|
test('preserves line:col form (does not interpret as range)', () => {
|
|
expect(parse('src/foo.ts:42:8')).toEqual({
|
|
path: 'src/foo.ts',
|
|
line: 42,
|
|
column: 8,
|
|
});
|
|
});
|
|
|
|
test('preserves hash form', () => {
|
|
expect(parse('src/foo.ts#L42C8')).toEqual({
|
|
path: 'src/foo.ts',
|
|
line: 42,
|
|
column: 8,
|
|
});
|
|
expect(parse('src/foo.ts#L42')).toEqual({
|
|
path: 'src/foo.ts',
|
|
line: 42,
|
|
});
|
|
});
|
|
|
|
test('range form takes precedence over line-only when suffix matches digits-dash-digits', () => {
|
|
const result = parse('src/foo.ts:42-58');
|
|
expect(result).toEqual({ path: 'src/foo.ts', line: 42, endLine: 58 });
|
|
});
|
|
});
|