╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │112 - .message = try readMessage(repo_kind, repo_opts, aa, commit_object),│ │140 + .message = text, │ │141 + .message_truncated = truncated, │ │142 .hunks = &.{}, │ │143 .window_start = 0, │ │144 .has_prev = false, │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │153 // renders from the snapshot), so gate it out of the wasm build. │ │154 if (!builtin.cpu.arch.isWasm()) { │ │155 for (buf[0..count], oids[0..count], 0..) |*commit, oid, i| { │ │127 - // the selected commit (the first, == start_oid) shows the window the │ │128 - // url asks for; the rest show their first window. │ │129 - // the path filter only applies to the top commit (the walk root). │ │130 - const window_start = if (i == 0) start else 0; │ │131 - const rendered = renderCommitDiff(repo_kind, repo_opts, io, gpa, aa, repo, oid, window_start, diff_page, if (i == 0) path else "") catch │ │156 + if (root_message and i == 0) continue; │ │157 + // the walk root shows the window the url asks for and its filter; │ │158 + // the rest show their first window, unfiltered. │ │159 + const window_start = if (i == 0) root_start else 0; │ │160 + const rendered = renderCommitDiff(repo_kind, repo_opts, io, gpa, aa, repo, oid, window_start, diff_page, if (i == 0) root_path else "") catch│ │161 RenderedDiff{ .hunks = &.{}, .has_prev = false, .has_more = false }; │ │162 commit.hunks = rendered.hunks; │ │163 commit.window_start = window_start; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │170 .identity = try aa.dupe(u8, identity), │ │171 .ref_or_oid = resolved.ref_or_oid, │ │172 .ref_or_oid_value = resolved.value, │ │144 - .path = try aa.dupe(u8, path), │ │173 .commits = try aa.dupe(Commit, buf[0..count]), │ │174 .next_start = next_start, │ │175 + .content = try pageContent(aa, content), │ │176 .header = try Header.init(aa, resolved.ref_or_oid, resolved.value),│ │177 }; │ │178 } │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │179 │ │180 // an empty listing pinned to a ref, for the wasm / no-repo / unresolved paths. │ │152 -pub fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8, path: []const u8) !Self { │ │181 +pub fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8, content: ui.RoutablePage.RepoCommitsRoute.Content) !Self {│ │182 return .{ │ │183 .identity = try aa.dupe(u8, identity), │ │184 .ref_or_oid = ref_or_oid, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │185 .ref_or_oid_value = try aa.dupe(u8, value), │ │157 - .path = try aa.dupe(u8, path), │ │186 .commits = &.{}, │ │187 .next_start = null, │ │188 + .content = try pageContent(aa, content), │ │189 .header = try Header.init(aa, ref_or_oid, value), │ │190 + }; │ │191 +} │ │192 + │ │193 +// the page's content for a route's, duped into `aa`. the diff window doesn't │ │194 +// carry over: each commit holds the one its pane shows as `window_start`. │ │195 +fn pageContent(aa: std.mem.Allocator, content: ui.RoutablePage.RepoCommitsRoute.Content) !Content {│ │196 + return switch (content) { │ │197 + .diff => |d| .{ .diff = .{ .path = try aa.dupe(u8, d.path.slice()) } }, │ │198 + .message => .message, │ │199 }; │ │200 } │ │201 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │339 }); │ │340 } │ │341 │ │304 -// the whole commit message, trimmed and duped into `aa`. an oversized │ │305 -// message falls back to that first line. │ │342 +// the commit message, trimmed and read into `aa`, plus whether it ran past│ │343 +// `limit` and was cut short there. │ │344 fn readMessage( │ │345 comptime repo_kind: rp.RepoKind, │ │346 comptime repo_opts: rp.RepoOpts(repo_kind), │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │347 aa: std.mem.Allocator, │ │348 commit_object: *xit.object.Object(repo_kind, repo_opts), │ │311 -) ![]const u8 { │ │312 - const md = commit_object.content.commit.metadata; │ │313 - try commit_object.object_reader.seekTo(commit_object.content.commit.message_position); │ │314 - const message = commit_object.object_reader.interface.allocRemaining(aa, .limited(max_message_size)) catch │ │315 - return try aa.dupe(u8, std.mem.trim(u8, md.message orelse "", " \t\r\n")); │ │316 - return std.mem.trim(u8, message, " \t\r\n"); │ │349 + limit: usize, │ │350 +) !struct { []const u8, bool } { │ │351 + var message: std.ArrayList(u8) = .empty; │ │352 + // a message past the limit keeps the bytes read so far │ │353 + const truncated = if (commit_object.readMessage(aa, &message, .limited(limit))) |_| │ │354 + false │ │355 + else |err| switch (err) { │ │356 + error.StreamTooLong => true, │ │357 + else => |e| return e, │ │358 + }; │ │359 + // the cut lands wherever the limit falls, so a truncated message keeps │ │360 + // only its whole lines: a line break can't fall inside a codepoint, and │ │361 + // the widgets need valid utf-8. a first line past the limit keeps none. │ │362 + const text = if (truncated) message.items[0 .. std.mem.lastIndexOfScalar(u8, message.items, '\n') orelse 0] else message.items;│ │363 + return .{ std.mem.trim(u8, text, " \t\r\n"), truncated }; │ │364 +} │ │365 + │ │366 +// a focusable, word-wrapping box holding a commit message. `bottom_label` │ │367 +// marks a message the page only shows part of ("" when it's whole). │ │368 +fn messageBox(allocator: std.mem.Allocator, message: []const u8, bottom_label: []const u8) !wgt.TextBox(ui.Widget) { │ │369 + var tb = try wgt.TextBox(ui.Widget).init(allocator, message, .{ │ │370 + .border_style = .single, │ │371 + .rounded_corners = true, │ │372 + .wrap_kind = .word, │ │373 + .label = " commit message ", │ │374 + .bottom_label = bottom_label, │ │375 + }); │ │376 + tb.getFocus().focusable = true; │ │377 + return tb; │ │378 } │ │379 │ │380 // the first line of a commit message, for the one-row list entries. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │392 session: *ui.Session, │ │393 // the commit whose diff the pane currently shows (index into data.commits).│ │394 diffed_index: ?usize, │ │334 - // the diff pane's message row, capped to the pane width so it word-wraps. │ │335 - message_id: ?usize, │ │395 │ │396 const header_index: usize = 0; │ │397 const content_index: usize = 1; │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────╮ │472 .data = data, │ │473 .session = session, │ │474 .diffed_index = null,│ │416 - .message_id = null, │ │475 }; │ │476 } │ │477 │ ╰──────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │530 try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │531 } │ │532 │ │475 - // the selected commit's full message, focusable like the rows around it. it │ │476 - // word-wraps, which needs a bounded width: the diff scroll grants its rows │ │477 - // an unbounded one, so build caps this row via `message_id`. │ │478 - fn addMessageBox(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), message: []const u8) !void { │ │479 - if (message.len == 0) return; │ │480 - var tb = try wgt.TextBox(ui.Widget).init(allocator, message, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .word, .label = "commit message" });│ │533 + // how much of the message a pane's box holds: the preview links to the pane │ │534 + // holding the whole thing, which is that link's destination. │ │535 + const MessageBox = enum { preview, whole }; │ │536 + │ │537 + // the selected commit's message. │ │538 + fn addMessageBox(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), commit: Commit, kind: MessageBox) !void { │ │539 + // a truncated message keeps its box even with no whole line to show, │ │540 + // since the box is what links to the whole thing. │ │541 + if (commit.message.len == 0 and !commit.message_truncated) return; │ │542 + const cut_short = commit.message_truncated and kind == .preview; │ │543 + var tb = try messageBox(allocator, commit.message, if (cut_short) " click or press enter to see more " else ""); │ │544 errdefer tb.deinit(allocator); │ │482 - tb.getFocus().focusable = true; │ │545 + if (kind == .preview) tb.getFocus().kind = .{ .custom = try messageLink(self.session.page_arena, self.data.identity, commit.oid) }; │ │546 try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │484 - self.message_id = tb.getFocus().id; │ │547 } │ │548 │ │549 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯