Files
openchamber/packages/ui/src/components/chat/MarkdownRendererImpl.test.ts
T
Catan 0f3b7f96c2 feat(chat): recognize file:start-end as a clickable reference (#2000)
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.
2026-07-06 00:58:44 +03:00

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 });
});
});