Fix cmd-click opening of relative/local paths (#9921)

This PR fixes an issue #9563 where relative file paths were not being
resolved against the terminal’s current working directory before
opening.


#### Verification
Tested with directories containing:
```
/tmp/test/test
❯ du -h .
  0B    ./spaces-end
  0B    ./with dot.
  0B    ./space middle
8.0K    .
```

Parent directory resolution also works as expected:
```
/tmp/test/test
❯ du -h ..
  0B    ../test/spaces-end
  0B    ../test/with dot.
  0B    ../test/space middle
8.0K    ../test
 16K    ..
 ```
 
@mitchellh  
In your original description you mentioned that “Links should work for all situations as they do in iTerm2.”  
I noticed that, for example, when running `ls`, the paths are not clickable, while they are clickable in iTerm2.
If you think this case should also be handled, I can open a separate PR for it once this one is accepted.
pull/9089/head
Mitchell Hashimoto 2025-12-16 09:07:16 -08:00 committed by GitHub
commit 29c0f982c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 42 additions and 3 deletions

View File

@ -2037,6 +2037,23 @@ pub fn pwd(
return try alloc.dupe(u8, terminal_pwd);
}
/// Resolves a relative file path to an absolute path using the terminal's pwd.
fn resolvePathForOpening(
self: *Surface,
path: []const u8,
) Allocator.Error!?[]const u8 {
if (!std.fs.path.isAbsolute(path)) {
const terminal_pwd = self.io.terminal.getPwd() orelse {
return null;
};
const resolved = try std.fs.path.resolve(self.alloc, &.{ terminal_pwd, path });
return resolved;
}
return null;
}
/// Returns the x/y coordinate of where the IME (Input Method Editor)
/// keyboard should be rendered.
pub fn imePoint(self: *const Surface) apprt.IMEPos {
@ -4265,7 +4282,12 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
.trim = false,
});
defer self.alloc.free(str);
try self.openUrl(.{ .kind = .unknown, .url = str });
const resolved_path = try self.resolvePathForOpening(str);
defer if (resolved_path) |p| self.alloc.free(p);
const url_to_open = resolved_path orelse str;
try self.openUrl(.{ .kind = .unknown, .url = url_to_open });
},
._open_osc8 => {

View File

@ -26,7 +26,7 @@ pub const regex =
"(?:" ++ url_schemes ++
\\)(?:
++ ipv6_url_pattern ++
\\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:\.\.\/|\.\/|\/)[\w\-.~:\/?#@!$&*+,;=%]+(?:\/[\w\-.~:\/?#@!$&*+,;=%]*)*
\\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:\.\.\/|\.\/|\/)(?:(?=[\w\-.~:\/?#@!$&*+,;=%]*\.)[\w\-.~:\/?#@!$&*+,;=%]+(?: [\w\-.~:\/?#@!$&*+,;=%]*[\/.])*(?: +(?= *$))?|(?![\w\-.~:\/?#@!$&*+,;=%]*\.)[\w\-.~:\/?#@!$&*+,;=%]+(?: [\w\-.~:\/?#@!$&*+,;=%]+)*(?: +(?= *$))?)
;
const url_schemes =
\\https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:
@ -253,6 +253,23 @@ test "url regex" {
.input = "IPv6 in markdown [link](http://[2001:db8::1]/docs)",
.expect = "http://[2001:db8::1]/docs",
},
// File paths with spaces
.{
.input = "./spaces-end. ",
.expect = "./spaces-end. ",
},
.{
.input = "./space middle",
.expect = "./space middle",
},
.{
.input = "../test folder/file.txt",
.expect = "../test folder/file.txt",
},
.{
.input = "/tmp/test folder/file.txt",
.expect = "/tmp/test folder/file.txt",
},
};
for (cases) |case| {