╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────╮ │src/ui/Repo/Diff.zig│ ╰────────────────────╯ ╭─────────────────────────────────────────────────────────────────╮ │17 text: []const u8, │ │18 // one byte per line of text: the edit's +/-/space prefix│ │19 prefixes: []const u8, │ │20 + status: Status = .changed, │ │21 +}; │ │22 +pub const Status = enum { │ │23 + added, │ │24 + removed, │ │25 + changed, │ │26 + │ │27 + fn label(self: Status) []const u8 { │ │28 + return switch (self) { │ │29 + .added => " added ", │ │30 + .removed => " removed ", │ │31 + .changed => " changed ", │ │32 + }; │ │33 + } │ │34 }; │ │35 pub const Window = struct { │ │36 hunks: []const Hunk = &.{}, │ ╰─────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │46 │ │47 pub const Route = union(enum) { │ │48 repo: struct { identity: []const u8, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8, base_oid: []const u8 }, │ │35 - patchrev: struct { identity: []const u8, id: []const u8 }, │ │36 - // `oid` names one commit's diff against `base_oid` ("" = the whole patch) │ │37 - fork: struct { identity: []const u8, id: []const u8, oid: []const u8 = "", base_oid: []const u8 = "" }, │ │49 + // `base_oid` is the revision's base commit, which the url leaves out │ │50 + patchrev: struct { identity: []const u8, id: []const u8, base_oid: []const u8 }, │ │51 + // `oid` names one commit's diff against `base_oid` ("" = the whole patch). │ │52 + // `patch_base_oid` is the whole patch's base commit, which the url leaves out │ │53 + fork: struct { identity: []const u8, id: []const u8, oid: []const u8 = "", base_oid: []const u8 = "", patch_base_oid: []const u8 = "" },│ │54 │ │55 pub fn page(self: Route, start: usize, path: []const u8) ?ui.RoutablePage { │ │56 return switch (self) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │64 const route = self.page(start, path) orelse return error.RouteTooLong; │ │65 return arena.allocator().print("a:{s}", .{try route.toUrl(arena)}); │ │66 } │ │67 + │ │68 + // a link to the file's content in the files view, from the base commit for a removed file │ │69 + fn fileLink(self: Route, arena: *std.heap.ArenaAllocator, path: []const u8, status: Status) ![]const u8 { │ │70 + const route = (if (status == .removed) switch (self) { │ │71 + .repo => |r| ui.RoutablePage.repoFilesRoute(r.identity, .object, r.base_oid, path, 0), │ │72 + .patchrev => |r| ui.RoutablePage.repoFilesRoute(r.identity, .object, r.base_oid, path, 0), │ │73 + .fork => |f| ui.RoutablePage.forkFilesRoute(f.identity, f.id, if (f.base_oid.len != 0) f.base_oid else f.patch_base_oid, path, 0),│ │74 + } else switch (self) { │ │75 + .repo => |r| ui.RoutablePage.repoFilesRoute(r.identity, r.ref_or_oid, r.value, path, 0), │ │76 + .patchrev => |r| ui.RoutablePage.repoPatchRevFilesRoute(r.identity, r.id, path, 0), │ │77 + .fork => |f| ui.RoutablePage.forkFilesRoute(f.identity, f.id, f.oid, path, 0), │ │78 + }) orelse return error.RouteTooLong; │ │79 + return arena.allocator().print("a:{s}", .{try route.toUrl(arena)}); │ │80 + } │ │81 }; │ │82 │ │83 pub fn init( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │100 break :blk .{ │ │101 record.base_tree_oid[0..oid_len].*, │ │102 record.head_tree_oid[0..oid_len].*, │ │ 73 - Route{ .patchrev = .{ .identity = identity, .id = try aa.dupe(u8, route.patchrev_id.slice()) } }, │ │103 + Route{ .patchrev = .{ .identity = identity, .id = try aa.dupe(u8, route.patchrev_id.slice()), .base_oid = try aa.dupe(u8, record.event.base_oid) } },│ │104 }; │ │105 } else blk: { │ │106 const resolved = (try ui.ResolvedRefOrOid(repo_kind, repo_opts).init(repo, io, aa, route.ref_or_oid, route.value.slice())) orelse return error.NotFound; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │158 defer pair.deinit(); │ │159 │ │160 if (path.len != 0 and !std.mem.eql(u8, pair.path, path)) continue; │ │161 + │ │162 + // an added or removed file shows only its path │ │163 + const status: Status = if (pair.a.source == .nothing) .added else if (pair.b.source == .nothing) .removed else .changed;│ │164 + if (status != .changed) { │ │165 + const i = index; │ │166 + index += 1; │ │167 + if (i < start) continue; │ │168 + if (hunks.items.len >= page_size) { │ │169 + has_more = true; │ │170 + break :file_loop; │ │171 + } │ │172 + try hunks.append(arena, .{ .path = try arena.dupe(u8, pair.path), .text = "", .prefixes = "", .status = status }); │ │173 + continue; │ │174 + } │ │175 │ │176 var hunk_iter = try df.HunkIterator(repo_kind, repo_opts).init(gpa, &pair.a, &pair.b); │ │177 defer hunk_iter.deinit(gpa); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │251 │ │252 fn appendWindow(data: @This(), allocator: std.mem.Allocator, session: *ui.Session, box: *wgt.Box(ui.Widget)) !void { │ │253 if (data.path.len != 0) { │ │210 - try addLink(allocator, box, data.path, ""); │ │254 + const status = if (data.window.hunks.len > 0) data.window.hunks[0].status else .changed; │ │255 + try addSpans(allocator, box, &.{.{ .text = data.path }}, try data.route.fileLink(session.page_arena, data.path, status), status.label()); │ │256 try addLink(allocator, box, "← all files", try data.route.link(session.page_arena, 0, "")); │ │257 } │ │258 if (data.window.start > 0) try addLink(allocator, box, "← previous", try data.route.link(session.page_arena, data.window.start -| page_size, data.path));│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │259 for (data.window.hunks) |hunk| { │ │215 - if (data.path.len == 0) if (hunk.path) |path| try addLink(allocator, box, path, try data.route.link(session.page_arena, 0, path)); │ │260 + if (data.path.len == 0) if (hunk.path) |path| { │ │261 + // a changed file's path narrows the diff to it, the others open the file │ │262 + const link = if (hunk.status == .changed) try data.route.link(session.page_arena, 0, path) else try data.route.fileLink(session.page_arena, path, hunk.status);│ │263 + try addSpans(allocator, box, &.{.{ .text = path }}, link, hunk.status.label()); │ │264 + }; │ │265 if (hunk.text.len != 0) try addHunk(allocator, box, hunk); │ │266 } │ │267 if (data.window.has_more) try addLink(allocator, box, "next →", try data.route.link(session.page_arena, data.window.start + page_size, data.path)); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │268 } │ │269 │ │270 fn addLink(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), text: []const u8, link: []const u8) !void {│ │222 - try addSpans(allocator, box, &.{.{ .text = text }}, link); │ │271 + try addSpans(allocator, box, &.{.{ .text = text }}, link, ""); │ │272 } │ │273 │ │274 // one span per line, inserted lines green and deleted lines red. the text │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │288 try spans.append(allocator, .{ .text = hunk.text[start..end], .style = style }); │ │289 start = end; │ │290 } │ │242 - try addSpans(allocator, box, spans.items, ""); │ │291 + try addSpans(allocator, box, spans.items, "", ""); │ │292 } │ │293 │ │245 -fn addSpans(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), spans: []const wgt.Span, link: []const u8) !void { │ │246 - var tb = try wgt.TextBox.initSpans(allocator, spans, .{ .border_style = .single, .round_corners = true, .wrap_kind = .none }); │ │294 +fn addSpans(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), spans: []const wgt.Span, link: []const u8, label: []const u8) !void { │ │295 + var tb = try wgt.TextBox.initSpans(allocator, spans, .{ .border_style = .single, .round_corners = true, .wrap_kind = .none, .label = label });│ │296 errdefer tb.deinit(allocator); │ │297 tb.getFocus().mode = .all; │ │298 if (link.len != 0) tb.getFocus().kind = .{ .custom = link }; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯