╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │111 │ │112 // the "key:value" segments of a url tail, collected in any order │ │113 const Params = struct { │ │114 - const ParamKey = enum { start, branch, tag, object }; │ │114 + const ParamKey = enum { start, line, branch, tag, object }; │ │115 │ │116 // a branch:/tag:/object: param; the value stays url-encoded │ │117 const RefParam = struct { kind: RefOrOid, value: []const u8 };│ ╰───────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │159 return std.fmt.parseInt(usize, value, 10) catch null; │ │160 } │ │161 │ │162 + // the "line:<n>" first-line param (0 when absent), null when malformed│ │163 + fn line(self: Params) ?usize { │ │164 + const value = self.values.get(.line) orelse return 0; │ │165 + return std.fmt.parseInt(usize, value, 10) catch null; │ │166 + } │ │167 + │ │168 // the single branch:/tag:/object: param, null when none is set │ │169 fn ref(self: Params) error{BadUrl}!?RefParam { │ │170 var result: ?RefParam = null; │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │185 // the `Params` key spellings the url emitters use │ │186 const tag_filter_seg = @tagName(Params.ParamKey.tag) ++ ":"; │ │187 const start_seg = @tagName(Params.ParamKey.start) ++ ":"; │ │188 + const line_seg = @tagName(Params.ParamKey.line) ++ ":"; │ │189 // marks a files or commits route's trailing path: the value is the raw│ │190 // remainder of the url, so it must come last │ │191 const path_seg = "path:"; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │192 │ │193 // a repo route is "username/reponame", optionally followed by the files │ │187 - // tab's "/files/<refkind>:<refvalue>[/start:<n>][/path:<dir>]"; the bare │ │194 + // tab's "/files/<refkind>:<refvalue>[/line:<n>][/path:<dir>]"; the bare │ │195 // pair means the default branch's files root. a local session's routes │ │196 // elide the identity: "" at the bare root, else starting at the tail's "/".│ │197 pub const repo_route_max_len = 1024; │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │241 ref_kind: ?RefOrOid = null, │ │242 ref_value: Array(ref_route_max_len) = .{}, │ │243 path: Array(repo_route_max_len) = .{}, │ │237 - start: usize = 0, │ │244 + // the selected file's first visible line (1-based; 0 = unset).│ │245 + line: usize = 0, │ │246 }; │ │247 │ │248 pub const RepoCommitsRoute = struct { │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │256 │ │257 // build a `.repo_files` route (a null ref_kind = the bare default-branch │ │258 // root). null if the result doesn't fit the inline name. │ │251 - pub fn repoFilesRoute(identity: []const u8, ref_kind: ?RefOrOid, ref_value: []const u8, dir: []const u8, start: usize) ?RoutablePage {│ │259 + pub fn repoFilesRoute(identity: []const u8, ref_kind: ?RefOrOid, ref_value: []const u8, dir: []const u8, line: usize) ?RoutablePage { │ │260 if (ref_kind == null and (ref_value.len != 0 or dir.len != 0)) return null; │ │261 if (ref_kind != null and ref_value.len == 0 and dir.len != 0) return null; │ │262 return .{ .repo_files = .{ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │264 .ref_kind = ref_kind, │ │265 .ref_value = Array(ref_route_max_len).from(ref_value) orelse return null,│ │266 .path = Array(repo_route_max_len).from(dir) orelse return null, │ │259 - .start = start, │ │267 + .line = line, │ │268 } }; │ │269 } │ │270 │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │364 .user_auth => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/auth", .{name.slice()}), │ │365 .repo_files => |f| blk: { │ │366 const prefix = try repoUrlPrefix(arena, f.name.slice()); │ │359 - if (f.ref_kind == null and f.start == 0) break :blk if (prefix.len == 0) "/" else prefix; │ │367 + if (f.ref_kind == null and f.line == 0) break :blk if (prefix.len == 0) "/" else prefix; │ │368 var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │369 try out.writer.print("{s}/" ++ files_seg, .{prefix}); │ │370 if (f.ref_kind) |kind| if (f.ref_value.len != 0) try out.writer.print("/{s}:{s}", .{ @tagName(kind), f.ref_value.slice() });│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │363 - if (f.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{f.start}); │ │371 + if (f.line != 0) try out.writer.print("/" ++ line_seg ++ "{d}", .{f.line}); │ │372 if (f.path.len != 0) try out.writer.print("/" ++ path_seg ++ "{s}", .{f.path.slice()});│ │373 break :blk out.written(); │ │374 }, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │484 // route): a tab keyword followed by "key:value" params in any order, plus │ │485 // the files tab's trailing "path:<dir>". │ │486 fn repoSubRoute(pair: []const u8, sub: []const u8) ?RoutablePage { │ │479 - // the params the ref-pinned tabs (files/commits) accept │ │480 - const ref_tab_keys: []const Params.ParamKey = &.{ .start, .branch, .tag, .object };│ │487 var segments = std.mem.splitScalar(u8, sub, '/'); │ │488 const tab = segments.next() orelse return null; │ │489 var params = Params{}; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯