Skip to content

Detect quoted file paths with spaces as implicit links - #594

Open
bones7456 wants to merge 3 commits into
migueldeicaza:mainfrom
bones7456:fix/quoted-path-link-detection
Open

Detect quoted file paths with spaces as implicit links#594
bones7456 wants to merge 3 commits into
migueldeicaza:mainfrom
bones7456:fix/quoted-path-link-detection

Conversation

@bones7456

Copy link
Copy Markdown
Contributor

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:

'/Users/me/Screenshot 2026-07-15 at 09.58.24.png'

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 implicitLinkMatch over 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:

  • Opener anchoring instead of strict pairing — any quote directly followed by a path-looking prefix is treated as an opener, closed by the nearest quote of the same kind. This keeps detection robust when the surrounding prose contains apostrophes (don't miss '/tmp/a b.png').
  • Innermost candidate wins — nested quoting like "'/a b.png'" resolves to /a b.png.
  • The cell-offset → row-ranges mapping is factored out of implicitLinkMatch into a shared implicitMatch(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).
  • Non-path quoted strings ('hello world') and clicks outside the quoted range still return no match; regex behavior is unchanged otherwise.

Tests

8 new tests in LinkLookupTests covering single/double quotes, ~/ paths, nested quotes, wrapped lines, apostrophes in prose, and negative cases. Full suite passes (452 tests).

@bones7456

Copy link
Copy Markdown
Contributor Author

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 / or ., so greedy backtracking matched /x/face cropped.png only up to cropped., silently dropping the extension. The segment pattern now allows trailing word characters after the last //. ([\/.][\/.]\w*), so the full face cropped.png is matched. Prose words after a path (containing neither / nor .) are still excluded, and existing behavior is otherwise unchanged — full suite passes.

@migueldeicaza

Copy link
Copy Markdown
Owner

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.
@bones7456
bones7456 force-pushed the fix/quoted-path-link-detection branch from 8a21272 to a108a1b Compare August 10, 2026 09:03
@bones7456

Copy link
Copy Markdown
Contributor Author

Rebased on main and handled the quoting conventions you listed — `path' (GNU) and the curly pairs, plus `path` since markdown-flavored output is where most agent tools put paths. The "closed by the same character" rule is now an opener → closers map, so an opener pairs only with its own closer and a mismatched pair isn't a boundary.

On links being URLs rather than arbitrary file paths: the implicit branch already detects bare paths (that's the Ghostty port), and 1483b09 now opens them, file.swift:13 included. So the open question isn't whether a path can be a link, it's where one ends — and a quote pair is the only unambiguous answer available. Everything else stays heuristic.

Two notes on the tests, since it's easy to write ones that prove nothing here:

  • They use /tmp/dir.d with prose.txt and click inside with. An unquoted space-joined segment only counts when it carries a / or . of its own, so that click finds nothing without a quote pair — testUnquotedPathStopsAtProseSegment pins that. A path like /tmp/my file.txt passes all of them unquoted, which is why I switched away from it.
  • The dotless branch (anyPathSpaceSegments, pre-existing) joins any space-separated run, so /tmp/dir with prose.txt ok currently swallows the trailing ok. Not from this PR and I left it alone; happy to look at it separately. Quoted boundaries do fix that case.

Full suite (699 tests) passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants