╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │111 const commits = try Commits.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, commits_after); │ │112 │ │113 // each tab mirror carries this page's route for that tab; tabs not targeted │ │114 - // by the incoming route fall back to their root/first-page route. │ │115 - const route_name = (ui.RoutablePage.repoFilesRoute(rf.identity, files.ref_or_oid, files.ref_or_oid_value, files.dir) orelse return error.NotFound).repo; │ │114 + // by the incoming route fall back to their root/first-page route. the files │ │115 + // mirror carries the selected file (when the route named one) so the url │ │116 + // keeps it before focus enters the view. │ │117 + const files_path = if (files.selected_file) |f| try Files.childDir(arena.allocator(), files.dir, f) else files.dir; │ │118 + const route_name = (ui.RoutablePage.repoFilesRoute(rf.identity, files.ref_or_oid, files.ref_or_oid_value, files_path) orelse return error.NotFound).repo; │ │119 const commits_route_name = (ui.RoutablePage.repoCommitsRoute(rf.identity, commits.ref_or_oid, commits.ref_or_oid_value, commits_after) orelse return error.NotFound).repo_commits.name;│ │120 │ │121 return .{ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │36 ref_or_oid_value: []const u8, │ │37 // the directory being viewed, relative to the repo root ("" at the root). │ │38 dir: []const u8, │ │39 +// a file within `dir` to start selected (so its contents show and the url │ │40 +// carries it), or null to fall back to a README / the first row. set when the│ │41 +// route's path named a file rather than a directory. │ │42 +selected_file: ?[]const u8 = null, │ │43 entries: []const Entry, │ │44 // the "viewing <ref> <value>" banner shown above the listing. │ │45 sub_header: SubHeader, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────╮ │53 identity: []const u8, │ │54 requested_ref_or_oid: ?RefOrOid,│ │55 requested_value: []const u8, │ │52 - dir: []const u8, │ │56 + path: []const u8, │ │57 ) !Self { │ │58 const aa = arena.allocator(); │ │59 │ ╰────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │60 // no filesystem (wasm) or nowhere to look: empty listing pinned to whatever │ │61 // ref the route asked for. the wasm path never calls init anyway — it │ │62 // rebuilds from the serialized snapshot. │ │59 - const io = session.io orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value, dir); │ │60 - const repos_dir = session.repos_dir orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value, dir); │ │63 + const io = session.io orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value, path); │ │64 + const repos_dir = session.repos_dir orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value, path);│ │65 │ │66 // the repo's working copy lives at <repos_dir>/<hex event id>. │ │67 const hex = std.fmt.bytesToHex(event_id.*, .lower); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │71 // (transient; freed before init returns); the listing is built into the │ │72 // page arena so it outlives them. │ │73 const gpa = arena.child_allocator; │ │70 - var repo = rp.Repo(.xit, .{}).open(io, gpa, .{ .path = repo_path }) catch return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value, dir); │ │74 + var repo = rp.Repo(.xit, .{}).open(io, gpa, .{ .path = repo_path }) catch return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value, path);│ │75 defer repo.deinit(io, gpa); │ │76 │ │77 // resolve the requested ref (or the default branch) to the commit oid whose │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │79 // bad url (NotFound -> 404); the default-branch path falls through to empty. │ │80 const resolved = (try ui.ResolvedRefOrOid.init(&repo, io, aa, requested_ref_or_oid, requested_value)) orelse {│ │81 if (requested_ref_or_oid != null) return error.NotFound; │ │78 - return emptyResult(aa, identity, .branch, requested_value, dir); │ │82 + return emptyResult(aa, identity, .branch, requested_value, path); │ │83 }; │ │84 │ │85 // read the tree at that commit. building the read-only state mirrors what │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 86 // repo.status does internally, but for an arbitrary commit rather than HEAD. │ │ 83 - var moment = repo.core.latestMoment() catch return emptyResult(aa, identity, resolved.ref_or_oid, resolved.value, dir); │ │ 87 + var moment = repo.core.latestMoment() catch return emptyResult(aa, identity, resolved.ref_or_oid, resolved.value, path); │ │ 88 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; │ │ 85 - var tree = tr.Tree(.xit, .{}).init(state, io, gpa, &resolved.oid) catch return emptyResult(aa, identity, resolved.ref_or_oid, resolved.value, dir); │ │ 89 + var tree = tr.Tree(.xit, .{}).init(state, io, gpa, &resolved.oid) catch return emptyResult(aa, identity, resolved.ref_or_oid, resolved.value, path);│ │ 90 defer tree.deinit(); │ │ 91 │ │ 92 + // `path` names a file when it's an exact tree entry: list its parent │ │ 93 + // directory and start that file selected. otherwise `path` is the directory. │ │ 94 + var dir = path; │ │ 95 + var selected_file: ?[]const u8 = null; │ │ 96 + if (path.len != 0 and tree.entries.get(path) != null) { │ │ 97 + const slash = std.mem.lastIndexOfScalar(u8, path, '/'); │ │ 98 + selected_file = try aa.dupe(u8, if (slash) |s| path[s + 1 ..] else path); │ │ 99 + dir = if (slash) |s| path[0..s] else ""; │ │100 + } │ │101 + │ │102 // collect the immediate children of `dir`. each committed path is a full │ │103 // file path; a child is a directory when more path follows its first │ │104 // segment under `dir`. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │105 var children: std.StringArrayHashMapUnmanaged(bool) = .empty; // name -> is_dir │ │106 defer children.deinit(gpa); │ │107 const prefix_len = if (dir.len == 0) 0 else dir.len + 1; // skip "dir/" │ │ 94 - for (tree.entries.keys()) |path| { │ │108 + for (tree.entries.keys()) |entry_path| { │ │109 if (dir.len != 0) { │ │ 96 - if (!std.mem.startsWith(u8, path, dir) or path.len <= dir.len or path[dir.len] != '/') continue; │ │110 + if (!std.mem.startsWith(u8, entry_path, dir) or entry_path.len <= dir.len or entry_path[dir.len] != '/') continue;│ │111 } │ │ 98 - const rel = path[prefix_len..]; │ │112 + const rel = entry_path[prefix_len..]; │ │113 const slash = std.mem.indexOfScalar(u8, rel, '/'); │ │114 const name = if (slash) |s| rel[0..s] else rel; │ │115 const is_dir = slash != null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │133 if (is_dir != want_dir) continue; │ │134 var entry: Entry = .{ .name = try aa.dupe(u8, name), .is_dir = is_dir }; │ │135 if (!is_dir) { │ │122 - const path = try childDir(aa, dir, name); │ │123 - if (tree.entries.get(path)) |tree_entry| { │ │124 - const content = readFileContent(state, io, gpa, aa, path, tree_entry) catch │ │136 + const file_path = try childDir(aa, dir, name); │ │137 + if (tree.entries.get(file_path)) |tree_entry| { │ │138 + const content = readFileContent(state, io, gpa, aa, file_path, tree_entry) catch│ │139 FileContent{ .lines = &.{}, .is_binary = false }; │ │140 entry.lines = content.lines; │ │141 entry.is_binary = content.is_binary; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │151 .ref_or_oid = resolved.ref_or_oid, │ │152 .ref_or_oid_value = resolved.value, │ │153 .dir = try aa.dupe(u8, dir), │ │154 + .selected_file = selected_file, │ │155 .entries = entries, │ │156 .sub_header = try SubHeader.init(aa, resolved.ref_or_oid, resolved.value),│ │157 }; │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯