mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(preview): never glue the entries of a path listing into one path
`git ls-files`, `find` and `ls -R` print one path per row. An extensionless entry followed by an equal-or-shorter one looked like a path broken short of the edge, so `src/components` + `src/index.ts` linked `src/componentssrc/index.ts` and the real link was gone. Measured with the shipped terminalPathLine over this repo: `git ls-files` 8 false joins (one over 6 rows), `find src docs scripts` 5. A next row is now a new entry when it opens with `./` or `../`, or has a `/` and opens on the same top directory the broken token did. A wrapped path continues inside a name, so the captures/2 + 026-10-10/... case still joins. After the change: 1 and 0 (`docker/server.Dockerfile` + `docs/SPEEDRUN.md`, a broken token with no extension that no row-shape rule can tell from a wrap). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6bffc0481c
commit
6bfc1b88d8
2 files changed
+55
-3
No files matched your search
@@ -2217,6 +2217,10 @@ const FILE_LINK_ROOT_GROUP = /^\(\\\/\(\?:([^()]*)\)/.exec(FILE_PATH_LINK_PATTER
|
|||||||
const PATH_TOKEN_ENDS_WITH_EXTENSION = new RegExp(`\\.${FILE_LINK_EXTENSION_GROUP}$`);
|
const PATH_TOKEN_ENDS_WITH_EXTENSION = new RegExp(`\\.${FILE_LINK_EXTENSION_GROUP}$`);
|
||||||
const PATH_TOKEN_HAS_EXTENSION = new RegExp(`\\.${FILE_LINK_EXTENSION_GROUP}\\b`);
|
const PATH_TOKEN_HAS_EXTENSION = new RegExp(`\\.${FILE_LINK_EXTENSION_GROUP}\\b`);
|
||||||
const PATH_TOKEN_STARTS_NEW_PATH = new RegExp(`^(?:~\\/|\\/(?:${FILE_LINK_ROOT_GROUP})\\/)`);
|
const PATH_TOKEN_STARTS_NEW_PATH = new RegExp(`^(?:~\\/|\\/(?:${FILE_LINK_ROOT_GROUP})\\/)`);
|
||||||
|
/** A relative head that names its own location (`./a`, `../a`) is an entry of its own. */
|
||||||
|
const PATH_TOKEN_STARTS_EXPLICIT_RELATIVE = /^\.{1,2}\//;
|
||||||
|
/** First directory of a path, a leading `/` or `./` ignored: `src` for `src/a.ts` and `/src/a.ts`. */
|
||||||
|
const pathFirstSegment = (path) => path.replace(/^(?:\.?\/)+/, '').split('/')[0];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the line `nextText` carries on a file path `prevText` broke off.
|
* Whether the line `nextText` carries on a file path `prevText` broke off.
|
||||||
@@ -2226,15 +2230,20 @@ const PATH_TOKEN_STARTS_NEW_PATH = new RegExp(`^(?:~\\/|\\/(?:${FILE_LINK_ROOT_G
|
|||||||
* spanning three or more lines; only worth keeping once a later line ends it),
|
* spanning three or more lines; only worth keeping once a later line ends it),
|
||||||
* and `null` otherwise. The broken-off token must already look like a path in
|
* and `null` otherwise. The broken-off token must already look like a path in
|
||||||
* progress: it contains a `/`, ends on a path character and does not yet end
|
* progress: it contains a `/`, ends on a path character and does not yet end
|
||||||
* on an extension. A next line that opens a NEW rooted path (`/home/…`, `~/…`)
|
* on an extension. A next line that opens a NEW path is never glued on: a rooted
|
||||||
* is never glued on.
|
* one (`/home/…`, `~/…`), `./…`/`../…`, or a relative one that opens on the same
|
||||||
|
* top directory the broken token did (the next entry of a listing).
|
||||||
*/
|
*/
|
||||||
function pathContinuation(prevText, nextText) {
|
function pathContinuation(prevText, nextText) {
|
||||||
const tail = /[^\s"'<>|;&]+$/.exec(String(prevText || '').replace(/\s+$/, ''))?.[0] || '';
|
const tail = /[^\s"'<>|;&]+$/.exec(String(prevText || '').replace(/\s+$/, ''))?.[0] || '';
|
||||||
if (!tail.includes('/') || !/[\w\-.~+@%/]$/.test(tail) || PATH_TOKEN_ENDS_WITH_EXTENSION.test(tail)) return null;
|
if (!tail.includes('/') || !/[\w\-.~+@%/]$/.test(tail) || PATH_TOKEN_ENDS_WITH_EXTENSION.test(tail)) return null;
|
||||||
const next = String(nextText || '').replace(/^\s+/, '');
|
const next = String(nextText || '').replace(/^\s+/, '');
|
||||||
const head = /^[^\s"'<>|;&]+/.exec(next)?.[0] || '';
|
const head = /^[^\s"'<>|;&]+/.exec(next)?.[0] || '';
|
||||||
if (!head || PATH_TOKEN_STARTS_NEW_PATH.test(head)) return null;
|
if (!head || PATH_TOKEN_STARTS_NEW_PATH.test(head) || PATH_TOKEN_STARTS_EXPLICIT_RELATIVE.test(head)) return null;
|
||||||
|
// A listing (`git ls-files`, `find`, `ls -R`) puts one path per row, so `src/components`
|
||||||
|
// followed by `src/index.ts` is two entries, not one path broken after `src/components`.
|
||||||
|
// A wrapped path continues INSIDE a name: its next row does not open on the same top directory.
|
||||||
|
if (head.includes('/') && pathFirstSegment(head) === pathFirstSegment(tail)) return null;
|
||||||
if (PATH_TOKEN_HAS_EXTENSION.test(head)) return 'join';
|
if (PATH_TOKEN_HAS_EXTENSION.test(head)) return 'join';
|
||||||
return head === next.replace(/\s+$/, '') ? 'chain' : null;
|
return head === next.replace(/\s+$/, '') ? 'chain' : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,49 @@ describe('terminalPathLine', () => {
|
|||||||
expect(terminalPathLine(buffer(['see /tmp/a/b.png', 'c/d.png']), 0, 80)).toMatchObject({ endRow: 0 });
|
expect(terminalPathLine(buffer(['see /tmp/a/b.png', 'c/d.png']), 0, 80)).toMatchObject({ endRow: 0 });
|
||||||
expect(terminalPathLine(buffer(['/home/joe/dir/', '/home/x.png']), 0, 80)).toMatchObject({ endRow: 0 });
|
expect(terminalPathLine(buffer(['/home/joe/dir/', '/home/x.png']), 0, 80)).toMatchObject({ endRow: 0 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// `git ls-files`, `find` and `ls -R` print one path per row. An extensionless
|
||||||
|
// entry (a directory, `Dockerfile`, `.gitignore`) followed by an entry that is
|
||||||
|
// equal or shorter looked exactly like a path broken short of the edge, and the
|
||||||
|
// two were glued: `src/components` + `src/index.ts` linked `src/componentssrc/index.ts`
|
||||||
|
// and the real `src/index.ts` link was gone (Ark0N, PR #604, round 2).
|
||||||
|
describe('a listing is never glued into one path', () => {
|
||||||
|
const LISTING = [
|
||||||
|
'src/components',
|
||||||
|
'src/index.ts',
|
||||||
|
'src/lib',
|
||||||
|
'./index.ts',
|
||||||
|
'../docs/README.md',
|
||||||
|
'packages/gesture-control/.gitignore',
|
||||||
|
'packages/gesture-control/README.md',
|
||||||
|
'packages/gesture-control/package.json',
|
||||||
|
'data/runs/a.jsonl',
|
||||||
|
'data/runs/b.jsonl',
|
||||||
|
'data/runs/c.jsonl',
|
||||||
|
'data/runs/d.json',
|
||||||
|
];
|
||||||
|
|
||||||
|
it('keeps every row its own line', () => {
|
||||||
|
const buf = buffer(LISTING, 120);
|
||||||
|
LISTING.forEach((entry, row) => {
|
||||||
|
const line = terminalPathLine(buf, row, 120)!;
|
||||||
|
expect([line.startRow, line.endRow], entry).toEqual([row, row]);
|
||||||
|
expect(line.text, entry).toBe(entry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('links the entry that used to be swallowed', () => {
|
||||||
|
const buf = buffer(LISTING, 120);
|
||||||
|
expect(paths(terminalPathLine(buf, 1, 120)!.text)).toEqual(['src/index.ts']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still glues a path whose next row opens on a different directory (a real wrap)', () => {
|
||||||
|
// The control: `…/captures/2` + `026-10-10/…` has a `/` in its head too.
|
||||||
|
const line = terminalPathLine(buffer(BROKEN), 0, 80)!;
|
||||||
|
expect([line.startRow, line.endRow]).toEqual([0, 1]);
|
||||||
|
expect(paths(line.text)).toEqual([BROKEN_PATH]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
type Link = {
|
type Link = {
|
||||||
|
|||||||
Reference in new issue
Block a user