Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4396,7 +4396,28 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
const resolved_path = try self.resolvePathForOpening(str);
defer if (resolved_path) |p| self.alloc.free(p);

const url_to_open = resolved_path orelse str;
// Compilers and tools commonly suffix file paths with
// `:line[:col]` (e.g. `src/main.zig:42:10`). If the text
// as-is doesn't resolve to a real file but the path with
// the suffix stripped does, open the stripped path so that
// the click does something useful instead of silently
// failing. Exact matches above take precedence so that
// files whose names actually contain a numeric suffix
// still open correctly.
const stripped_path: ?[]const u8 = stripped: {
if (resolved_path != null) break :stripped null;
const parsed = input.Link.parseFilePath(str);
if (parsed.line == null) break :stripped null;
if (std.fs.path.isAbsolute(parsed.path)) {
std.fs.accessAbsolute(parsed.path, .{}) catch
break :stripped null;
break :stripped try self.alloc.dupe(u8, parsed.path);
}
break :stripped try self.resolvePathForOpening(parsed.path);
};
defer if (stripped_path) |p| self.alloc.free(p);

const url_to_open = resolved_path orelse stripped_path orelse str;
try self.openUrl(.{ .kind = .unknown, .url = url_to_open });
},

Expand Down
67 changes: 67 additions & 0 deletions src/input/Link.zig
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,73 @@ pub const Highlight = union(enum) {
hover_mods: Mods,
};

/// The result of parsing a matched link string for a trailing
/// `:line[:col]` suffix, e.g. `src/main.zig:42:10`.
pub const ParsedFilePath = struct {
path: []const u8,
line: ?[]const u8 = null,
col: ?[]const u8 = null,
};

/// Parse a trailing `:line[:col]` suffix from a matched link string.
/// The numeric components are returned as substrings of the input;
/// they are validated to be all digits but not range-checked.
pub fn parseFilePath(str: []const u8) ParsedFilePath {
var result: ParsedFilePath = .{ .path = str };
for (0..2) |_| {
const idx = std.mem.lastIndexOfScalar(u8, result.path, ':') orelse break;
const seg = result.path[idx + 1 ..];
if (!allDigits(seg)) break;
result.col = result.line;
result.line = seg;
result.path = result.path[0..idx];
}

return result;
}

fn allDigits(s: []const u8) bool {
if (s.len == 0) return false;
for (s) |c| if (!std.ascii.isDigit(c)) return false;
return true;
}

test "parseFilePath" {
const testing = std.testing;

{
const p = parseFilePath("src/main.zig");
try testing.expectEqualStrings("src/main.zig", p.path);
try testing.expect(p.line == null);
try testing.expect(p.col == null);
}
{
const p = parseFilePath("src/main.zig:42");
try testing.expectEqualStrings("src/main.zig", p.path);
try testing.expectEqualStrings("42", p.line.?);
try testing.expect(p.col == null);
}
{
const p = parseFilePath("/abs/path/main.zig:42:10");
try testing.expectEqualStrings("/abs/path/main.zig", p.path);
try testing.expectEqualStrings("42", p.line.?);
try testing.expectEqualStrings("10", p.col.?);
}
{
// Numeric suffix segments are parsed even for non-path strings;
// callers are expected to validate the path on disk.
const p = parseFilePath("magnet:?xt=urn:btih:1234567890");
try testing.expectEqualStrings("magnet:?xt=urn:btih", p.path);
try testing.expectEqualStrings("1234567890", p.line.?);
try testing.expect(p.col == null);
}
{
const p = parseFilePath("foo:bar");
try testing.expectEqualStrings("foo:bar", p.path);
try testing.expect(p.line == null);
}
}

/// Returns a new oni.Regex that can be used to match the link.
pub fn oniRegex(self: *const Link) !oni.Regex {
return try oni.Regex.init(
Expand Down
Loading