89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
import { expect, test } from 'bun:test';
|
|||
|
|
import { completePath, matchPaths, pathToken } from '../src/complete';
|
||
|
|
|
||
|
|
const paths = [
|
||
|
|
'README.md',
|
||
|
|
'src/app.ts',
|
||
|
|
'src/session.ts',
|
||
|
|
'src/ui/App.tsx',
|
||
|
|
'src/ui/Panels.tsx',
|
||
|
|
'test/session.test.ts',
|
||
|
|
'vendor/src/legacy.ts',
|
||
|
|
];
|
||
|
|
|
||
|
|
test('a bare @ opens the token with an empty query', () => {
|
||
|
|
expect(pathToken('@', 1)).toEqual({ start: 0, end: 1, query: '' });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the token is the text between @ and the cursor', () => {
|
||
|
|
expect(pathToken('look at @src/ses', 16)).toEqual({ start: 8, end: 16, query: 'src/ses' });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('@ mid-word is not a completion, so an email is left alone', () => {
|
||
|
|
expect(pathToken('mail me@example.com', 19)).toBeUndefined();
|
||
|
|
expect(pathToken('user@host', 9)).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a space ends the token', () => {
|
||
|
|
expect(pathToken('@src/app.ts and then', 20)).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the cursor before the @ sees no token', () => {
|
||
|
|
expect(pathToken('@src', 0)).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the nearest @ wins when there are two', () => {
|
||
|
|
const token = pathToken('@first then @sec', 16);
|
||
|
|
expect(token?.query).toBe('sec');
|
||
|
|
expect(token?.start).toBe(12);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('an empty query offers the shallowest paths first', () => {
|
||
|
|
expect(matchPaths(paths, '', 3)).toEqual(['README.md', 'src/app.ts', 'src/session.ts']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a directory prefix narrows to what is under it', () => {
|
||
|
|
const hits = matchPaths(paths, 'src/ui/');
|
||
|
|
expect(hits).toEqual(['src/ui/App.tsx', 'src/ui/Panels.tsx']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('prefix matches rank above substring matches', () => {
|
||
|
|
const hits = matchPaths(paths, 'src/');
|
||
|
|
expect(hits[0]).toBe('src/app.ts');
|
||
|
|
// vendor/src/legacy.ts contains "src/" but is not under src/, so it comes last.
|
||
|
|
expect(hits.at(-1)).toBe('vendor/src/legacy.ts');
|
||
|
|
expect(hits.indexOf('src/session.ts')).toBeLessThan(hits.indexOf('vendor/src/legacy.ts'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('matching ignores case', () => {
|
||
|
|
expect(matchPaths(paths, 'readme')).toEqual(['README.md']);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a query matching nothing yields nothing', () => {
|
||
|
|
expect(matchPaths(paths, 'zzz')).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the limit is honoured', () => {
|
||
|
|
expect(matchPaths(paths, 's', 2)).toHaveLength(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('completion replaces the token with a plain relative path', () => {
|
||
|
|
const value = 'look at @src/ses';
|
||
|
|
const token = pathToken(value, value.length)!;
|
||
|
|
expect(completePath(value, token, 'src/session.ts')).toEqual({
|
||
|
|
value: 'look at src/session.ts ',
|
||
|
|
cursor: 23,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('completion keeps whatever followed the cursor', () => {
|
||
|
|
const value = '@src/ses and explain';
|
||
|
|
const token = pathToken(value, 8)!;
|
||
|
|
const { value: next } = completePath(value, token, 'src/session.ts');
|
||
|
|
expect(next).toBe('src/session.ts and explain');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the inserted path carries no @', () => {
|
||
|
|
const token = pathToken('@READ', 5)!;
|
||
|
|
expect(completePath('@READ', token, 'README.md').value).not.toContain('@');
|
||
|
|
});
|