╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │132 name: Array(repo_route_max_len), │ │133 // the tag the list is filtered to, url-encoded ("" = unfiltered). │ │134 tag: Array(issue_tag_route_max_len) = .{}, │ │135 + // the query the list is filtered to, url-encoded ("" = unfiltered). │ │136 + search: Array(search_route_max_len) = .{}, │ │137 // the issue the route names: the list's window root, or the issue the│ │138 // edit/description/resolve view shows. │ │139 selected: Array(evt.event_id_size * 2) = .{}, │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │151 repo_patches: struct { │ │152 name: Array(repo_route_max_len), │ │153 tag: Array(patch_tag_route_max_len) = .{}, │ │154 + search: Array(search_route_max_len) = .{}, │ │155 selected: Array(evt.event_id_size * 2) = .{},│ │156 theirs: Array(theirs_route_max_len) = .{}, │ │157 view: PatchesView = .open, │ ╰──────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────╮ │161 repo_discussions: struct { │ │162 name: Array(repo_route_max_len), │ │163 tag: Array(discussion_tag_route_max_len) = .{},│ │164 + search: Array(search_route_max_len) = .{}, │ │165 selected: Array(evt.event_id_size * 2) = .{}, │ │166 view: DiscussionsView = .recent, │ │167 comments_start: usize = 0, │ ╰────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │555 return route; │ │556 } │ │557 │ │554 - // carry a search query on a commits route, url-encoding it into the route. │ │558 + // carry a search query on a commits or thread-list route, url-encoding it │ │559 + // into the route. │ │560 pub fn withSearch(self: RoutablePage, query: []const u8) ?RoutablePage { │ │561 const search = encodeParam(search_route_max_len, query) orelse return null; │ │557 - var route = self; │ │562 + return withEncodedSearch(self, search.slice()); │ │563 + } │ │564 + │ │565 + // the tag and search segments of a thread list url, either one optional │ │566 + fn writeListFilters(writer: *std.Io.Writer, tag: []const u8, search: []const u8) !void { │ │567 + if (tag.len != 0) try writer.print("/" ++ tag_filter_seg ++ "{s}", .{tag}); │ │568 + if (search.len != 0) try writer.print("/" ++ search_seg ++ "{s}", .{search}); │ │569 + } │ │570 + │ │571 + // carry the already-encoded list filters a selected thread's url names │ │572 + fn withEncodedFilters(self: ?RoutablePage, tag: []const u8, search: []const u8) ?RoutablePage { │ │573 + var route = withEncodedSearch(self, search) orelse return null; │ │574 + switch (route) { │ │575 + inline .repo_issues, .repo_patches, .repo_discussions => |*thread| thread.tag = @TypeOf(thread.tag).from(tag) orelse return null,│ │576 + else => return null, │ │577 + } │ │578 + return route; │ │579 + } │ │580 + │ │581 + // carry an already-encoded search param, as a url hands it over │ │582 + fn withEncodedSearch(self: ?RoutablePage, value: []const u8) ?RoutablePage { │ │583 + var route = self orelse return null; │ │584 + const search = Array(search_route_max_len).from(value) orelse return null; │ │585 switch (route) { │ │586 .repo_commits => |*c| c.search = search, │ │587 + .repo_issues => |*i| i.search = search, │ │588 + .repo_patches => |*p| p.search = search, │ │589 + .repo_discussions => |*d| d.search = search, │ │590 else => return null, │ │591 } │ │592 return route; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1054 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/" ++ comment_seg ++ "{s}", .{ prefix, i.selected.slice(), i.comment.slice() }) │ │1055 else │ │1056 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/" ++ comment_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, i.selected.slice(), i.comment.slice(), i.comments_start });│ │1027 - if (i.selected.len != 0) break :blk if (i.comments_start == 0) │ │1028 - try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}", .{ prefix, i.selected.slice() }) │ │1029 - else │ │1030 - try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, i.selected.slice(), i.comments_start }); │ │1031 - break :blk if (i.tag.len == 0) │ │1032 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}", .{ prefix, @tagName(i.view) }) │ │1033 - else │ │1034 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/" ++ tag_filter_seg ++ "{s}", .{ prefix, @tagName(i.view), i.tag.slice() }); │ │1057 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1058 + // the list the thread is selected in stays filtered, so paging │ │1059 + // and reloading keep the tag and the query │ │1060 + if (i.selected.len != 0) { │ │1061 + try out.writer.print("{s}/" ++ issue_seg ++ "{s}", .{ prefix, i.selected.slice() }); │ │1062 + try writeListFilters(&out.writer, i.tag.slice(), i.search.slice()); │ │1063 + if (i.comments_start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{i.comments_start}); │ │1064 + break :blk out.written(); │ │1065 + } │ │1066 + try out.writer.print("{s}/issues/{s}", .{ prefix, @tagName(i.view) }); │ │1067 + try writeListFilters(&out.writer, i.tag.slice(), i.search.slice()); │ │1068 + break :blk out.written(); │ │1069 }, │ │1070 .repo_patches => |p| blk: { │ │1071 const prefix = try repoUrlPrefix(arena, p.name.slice()); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1094 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/" ++ comment_seg ++ "{s}", .{ prefix, p.selected.slice(), p.comment.slice() }) │ │1095 else │ │1096 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/" ++ comment_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, p.selected.slice(), p.comment.slice(), p.comments_start });│ │1063 - if (p.selected.len != 0) break :blk if (p.comments_start == 0) │ │1064 - try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}", .{ prefix, p.selected.slice() }) │ │1065 - else │ │1066 - try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, p.selected.slice(), p.comments_start }); │ │1067 - break :blk if (p.tag.len == 0) │ │1068 - try std.fmt.allocPrint(arena.allocator(), "{s}/patches/{s}", .{ prefix, @tagName(p.view) }) │ │1069 - else │ │1070 - try std.fmt.allocPrint(arena.allocator(), "{s}/patches/{s}/" ++ tag_filter_seg ++ "{s}", .{ prefix, @tagName(p.view), p.tag.slice() }); │ │1097 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1098 + // the list the thread is selected in stays filtered, so paging │ │1099 + // and reloading keep the tag and the query │ │1100 + if (p.selected.len != 0) { │ │1101 + try out.writer.print("{s}/" ++ patch_seg ++ "{s}", .{ prefix, p.selected.slice() }); │ │1102 + try writeListFilters(&out.writer, p.tag.slice(), p.search.slice()); │ │1103 + if (p.comments_start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{p.comments_start}); │ │1104 + break :blk out.written(); │ │1105 + } │ │1106 + try out.writer.print("{s}/patches/{s}", .{ prefix, @tagName(p.view) }); │ │1107 + try writeListFilters(&out.writer, p.tag.slice(), p.search.slice()); │ │1108 + break :blk out.written(); │ │1109 }, │ │1110 .repo_discussions => |t| blk: { │ │1111 const prefix = try repoUrlPrefix(arena, t.name.slice()); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1124 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discuss_seg ++ "{s}/" ++ comment_seg ++ "{s}", .{ prefix, t.selected.slice(), t.comment.slice() }) │ │1125 else │ │1126 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discuss_seg ++ "{s}/" ++ comment_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, t.selected.slice(), t.comment.slice(), t.comments_start });│ │1089 - if (t.selected.len != 0) break :blk if (t.comments_start == 0) │ │1090 - try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discuss_seg ++ "{s}", .{ prefix, t.selected.slice() }) │ │1091 - else │ │1092 - try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discuss_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, t.selected.slice(), t.comments_start }); │ │1093 - break :blk if (t.tag.len == 0) │ │1094 - try std.fmt.allocPrint(arena.allocator(), "{s}/discussions/{s}", .{ prefix, @tagName(t.view) }) │ │1095 - else │ │1096 - try std.fmt.allocPrint(arena.allocator(), "{s}/discussions/{s}/" ++ tag_filter_seg ++ "{s}", .{ prefix, @tagName(t.view), t.tag.slice() }); │ │1127 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1128 + // the list the thread is selected in stays filtered, so paging │ │1129 + // and reloading keep the tag and the query │ │1130 + if (t.selected.len != 0) { │ │1131 + try out.writer.print("{s}/" ++ discuss_seg ++ "{s}", .{ prefix, t.selected.slice() }); │ │1132 + try writeListFilters(&out.writer, t.tag.slice(), t.search.slice()); │ │1133 + if (t.comments_start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{t.comments_start}); │ │1134 + break :blk out.written(); │ │1135 + } │ │1136 + try out.writer.print("{s}/discussions/{s}", .{ prefix, @tagName(t.view) }); │ │1137 + try writeListFilters(&out.writer, t.tag.slice(), t.search.slice()); │ │1138 + break :blk out.written(); │ │1139 }, │ │1140 .repo_events => |e| blk: { │ │1141 const prefix = try repoUrlPrefix(arena, e.name.slice()); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1413 } │ │1414 return null; │ │1415 } │ │1374 - if (!params.only(&.{.start})) return null; │ │1375 - return repoThreadCommentsRoute(.issue, pair, issue_id, params.start() orelse return null); │ │1416 + if (!params.only(&.{ .start, .tag, .search })) return null; │ │1417 + return withEncodedFilters(repoThreadCommentsRoute(.issue, pair, issue_id, params.start() orelse return null), params.values.get(.tag) orelse "", params.values.get(.search) orelse "");│ │1418 } │ │1419 if (std.mem.startsWith(u8, tab, patch_seg)) { │ │1420 const patch_id = tab[patch_seg.len..]; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1450 } │ │1451 return null; │ │1452 } │ │1411 - if (!params.only(&.{.start})) return null; │ │1412 - return repoThreadCommentsRoute(.patch, pair, patch_id, params.start() orelse return null); │ │1453 + if (!params.only(&.{ .start, .tag, .search })) return null; │ │1454 + return withEncodedFilters(repoThreadCommentsRoute(.patch, pair, patch_id, params.start() orelse return null), params.values.get(.tag) orelse "", params.values.get(.search) orelse "");│ │1455 } │ │1456 if (std.mem.startsWith(u8, tab, discuss_seg)) { │ │1457 const discussion_id = tab[discuss_seg.len..]; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1481 if (std.mem.eql(u8, tail, "description")) return if (params.only(&.{})) repoThreadDescriptionRoute(.discuss, pair, discussion_id) else null; │ │1482 return null; │ │1483 } │ │1442 - if (!params.only(&.{.start})) return null; │ │1443 - return repoThreadCommentsRoute(.discuss, pair, discussion_id, params.start() orelse return null); │ │1484 + if (!params.only(&.{ .start, .tag, .search })) return null; │ │1485 + return withEncodedFilters(repoThreadCommentsRoute(.discuss, pair, discussion_id, params.start() orelse return null), params.values.get(.tag) orelse "", params.values.get(.search) orelse "");│ │1486 } │ │1487 if (std.mem.eql(u8, tab, "issues")) { │ │1488 // the word is a list view; a filter with neither is never emitted. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯