╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │799 // the typed query when enter should submit it: the box exists and holds │ │800 // focus. the caller owns the returned text. │ │801 pub fn submittedText(self: *SearchHeader, allocator: std.mem.Allocator) !?[]u8 {│ │752 - const text_input = self.searchInput() orelse return null; │ │753 - if (self.box.getFocus().child_id != text_input.getFocus().id) return null; │ │754 - return try text_input.text(allocator); │ │802 + const search_box = self.searchBox() orelse return null; │ │803 + if (self.box.getFocus().child_id != search_box.getFocus().id) return null; │ │804 + return try search_box.text(allocator); │ │805 } │ │806 │ │807 // enter the header, preferring the search box. │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │808 pub fn focusHeader(self: *SearchHeader, root_focus: *Focus) bool {│ │759 - if (self.searchInput()) |text_input| { │ │760 - root_focus.setFocus(text_input.getFocus().id); │ │809 + if (self.searchBox()) |search_box| { │ │810 + root_focus.setFocus(search_box.getFocus().id); │ │811 return true; │ │812 } │ │813 const clone_url = self.cloneUrl() orelse return false; │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │816 } │ │817 │ │818 pub fn hasFocusable(self: *SearchHeader) bool { │ │769 - return self.cloneUrl() != null or self.searchInput() != null;│ │819 + return self.cloneUrl() != null or self.searchBox() != null; │ │820 } │ │821 │ │822 fn cloneUrl(self: *SearchHeader) ?*CopyableText { │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │827 return null; │ │828 } │ │829 │ │780 - fn searchInput(self: *SearchHeader) ?*wgt.TextInput { │ │830 + fn searchBox(self: *SearchHeader) ?*SearchBox { │ │831 for (self.box.children.values()) |*child| switch (child.widget) {│ │782 - .text_input => |*text_input| return text_input, │ │832 + .search_box => |*search_box| return search_box, │ │833 else => {}, │ │834 }; │ │835 return null; │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │14 const Grid = xitui.grid.Grid; │ │15 const Focus = xitui.focus.Focus; │ │16 const inp = @import("../input.zig"); │ │17 +const srch = @import("../../search_thread.zig"); │ │18 const diff3 = @import("../../diff3.zig"); │ │19 const Comment = @import("../Repo/Comment.zig"); │ │20 const Attachment = @import("../Repo/Attachment.zig");│ ╰─────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │203 try tags.append(aa, tag); │ │204 } │ │205 return tags.items; │ │206 +} │ │207 + │ │208 +// the key a list set (and the search index) stores a thread under │ │209 +const OrderKey = [@sizeOf(u64) + evt.event_id_size]u8; │ │210 + │ │211 +// a search result's bytes as an order key │ │212 +fn resultKey(key: []const u8) !OrderKey { │ │213 + if (key.len != @sizeOf(u64) + evt.event_id_size) return error.InvalidSearchIndex;│ │214 + var order_key: OrderKey = undefined; │ │215 + @memcpy(&order_key, key); │ │216 + return order_key; │ │217 +} │ │218 + │ │219 +// the hex event id an order key ends with │ │220 +fn keyIdHex(order_key: *const OrderKey) [evt.event_id_size * 2]u8 { │ │221 + return std.fmt.bytesToHex(order_key[@sizeOf(u64)..].*, .lower); │ │222 } │ │223 │ │224 pub fn loadWindow( │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │225 comptime Data: type, │ │226 + comptime kind: evt.EventKind, │ │227 comptime hash_kind: hash.HashKind, │ │228 arena: *std.heap.ArenaAllocator, │ │229 admin_moment: ?evt.AdminDB.HashMap(.read_only),│ ╰────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │234 conflict_set: ?evt.EventDB(hash_kind).SortedSet(.read_only), │ │235 selected_id: []const u8, │ │236 comments_start: usize, │ │237 + // the search the set is narrowed to, or null to list it whole │ │238 + query: ?[]const u8, │ │239 ) !Data.Window { │ │240 + if (query) |text| return loadSearchWindow(Data, kind, hash_kind, arena, admin_moment, haxy_moment, records, set_maybe, root_key, conflict_set, selected_id, comments_start, text);│ │241 const set = set_maybe orelse return .empty; │ │221 - const DB = evt.EventDB(hash_kind); │ │242 const aa = arena.allocator(); │ │243 │ │244 var prev_id: ?[]const u8 = null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │248 prev_id = ""; │ │249 } else if (rank > Data.page_size) { │ │250 const pair = try set.getIndexKeyValuePair(@intCast(rank - Data.page_size)) orelse return error.NotFound;│ │231 - var previous: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │251 + var previous: OrderKey = undefined; │ │252 _ = try pair.key_cursor.readBytes(&previous); │ │233 - const hex = std.fmt.bytesToHex(previous[@sizeOf(u64)..].*, .lower); │ │234 - prev_id = try aa.dupe(u8, &hex); │ │253 + prev_id = try aa.dupe(u8, &keyIdHex(&previous)); │ │254 } │ │255 break :blk try set.iteratorFrom(key); │ │256 } else try set.iteratorFromIndex(0); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │260 while (try iter.next()) |cursor_value| { │ │261 var cursor = cursor_value; │ │262 const pair = try cursor.readKeyValuePair(); │ │244 - var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │263 + var order_key: OrderKey = undefined; │ │264 _ = try pair.key_cursor.readBytes(&order_key); │ │246 - const id = order_key[@sizeOf(u64)..]; │ │247 - const id_hex = std.fmt.bytesToHex(id.*, .lower); │ │265 if (items.items.len == Data.page_size) { │ │249 - next_id = try aa.dupe(u8, &id_hex); │ │266 + next_id = try aa.dupe(u8, &keyIdHex(&order_key)); │ │267 break; │ │268 } │ │252 - const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse continue; │ │253 - const record = try evt.read(Data.Event.Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); │ │254 - try items.append(aa, .{ │ │255 - .id = try aa.dupe(u8, &id_hex), │ │256 - .record = record, │ │257 - .author = try ui.Author.initFromEmail(admin_moment, arena, record.author_email), │ │258 - .conflicted = if (conflict_set) |conflicts| try conflicts.contains(&order_key) else false, │ │259 - .comments = try Comment.loadWindow( │ │260 - hash_kind, │ │261 - arena, │ │262 - admin_moment, │ │263 - haxy_moment, │ │264 - evt.Comment.thread_id_to_comment_id_set_key, │ │265 - &id_hex, │ │266 - if (std.mem.eql(u8, &id_hex, selected_id)) comments_start else 0, │ │267 - ), │ │268 - .attachments = try Attachment.load(hash_kind, arena, haxy_moment, &id_hex), │ │269 - }); │ │269 + const entry = (try loadEntry(Data, hash_kind, arena, admin_moment, haxy_moment, records, &order_key, conflict_set, selected_id, comments_start)) orelse continue;│ │270 + try items.append(aa, entry); │ │271 } │ │272 │ │273 return .{ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯