Detect quoted file paths with spaces as implicit links - #594
Conversation
|
Pushed a follow-up commit that also improves the unquoted spaces case: the Ghostty-style space-segment rule required each space-joined segment to end with |
|
I need to think about this, the idea of the link is that it represents URLs, not arbitrary file paths, and while certainly some apps use '/path' others might use "/path" or `path' or the typographical versions of those. |
Implicit link detection uses a Ghostty-style regex that stops at spaces unless the following segment ends in '/' or '.', so a path such as /Users/me/Screenshot 2026-07-15 at 09.58.24.png is only matched up to 'Screenshot' even when the shell has quoted it unambiguously. Add a pre-pass over the implicit line map: when the lookup target sits inside a '...' or "..." pair whose content starts with /, ~/, ./ or ../, return the whole quoted content (quotes excluded) as the link. Openers are anchored on a path-looking prefix rather than strict sequential pairing, so apostrophes in surrounding prose don't confuse it, and the innermost candidate wins for nested quotes. The cell-offset mapping is factored out of implicitLinkMatch into implicitMatch(in:text:startOffset:endOffset:) and shared by both paths, so hover highlighting and click both see the full quoted range.
A space-separated segment only counts as part of an unquoted path when
it contains '/' or '.', but the segment pattern also had to *end* with
one of those, so greedy backtracking cut '/x/face cropped.png' at
'cropped.' and left the extension out of the match. Allow trailing word
characters after the last '/' or '.' so the full extension is absorbed.
Prose words after a path (' and', ' at') still contain neither
character and are still excluded.
Straight single and double quotes were the only pairs treated as a path boundary, but output quotes paths several other ways: GNU tools use `like this', markdown-flavored output (and the AI agents that emit it) uses `like this`, and anything that went through smart-quote substitution uses the curly pairs. Replace the "closed by the same character" rule with an opener → closers map, so an opener pairs only with its own closer and a mismatched pair is not a boundary. The new tests use "/tmp/dir.d with prose.txt" and click inside "with": an unquoted space-joined segment counts only when it carries a "/" or "." of its own, so that click finds nothing unless a quote pair supplies the boundary — testUnquotedPathStopsAtProseSegment pins that down. It keeps the cases honest; a path like "/tmp/my file.txt" would pass them unquoted.
8a21272 to
a108a1b
Compare
|
Rebased on main and handled the quoting conventions you listed — On links being URLs rather than arbitrary file paths: the implicit branch already detects bare paths (that's the Ghostty port), and Two notes on the tests, since it's easy to write ones that prove nothing here:
Full suite (699 tests) passes. |
Problem
Implicit link detection uses the Ghostty-style regex, which only continues past a space when the next segment ends in
/or.. A path like:is therefore only matched up to
Screenshot, even though the surrounding quotes make the boundary unambiguous — this is exactly how shells and many CLI tools print paths containing spaces.Change
Add a pre-pass in
implicitLinkMatchover the already-built implicit line map: when the lookup target sits inside a'...'or"..."pair whose content starts with/,~/,./or../, the whole quoted content (quotes excluded) is returned as the link.Details:
don't miss '/tmp/a b.png')."'/a b.png'"resolves to/a b.png.implicitLinkMatchinto a sharedimplicitMatch(in:text:startOffset:endOffset:), used by both the regex path and the new quoted path, so hover highlighting and click resolve the same full range (including across wrapped lines).'hello world') and clicks outside the quoted range still return no match; regex behavior is unchanged otherwise.Tests
8 new tests in
LinkLookupTestscovering single/double quotes,~/paths, nested quotes, wrapped lines, apostrophes in prose, and negative cases. Full suite passes (452 tests).