╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────╮ │16 const Focus = xitui.focus.Focus; │ │17 const inp = @import("../input.zig"); │ │18 const srch_thrd = @import("../../search_thread.zig");│ │19 +const srch = @import("../../search.zig"); │ │20 const diff3 = @import("../../diff3.zig"); │ │21 const Comment = @import("../Repo/Comment.zig"); │ │22 const Attachment = @import("../Repo/Attachment.zig");│ ╰─────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │158 return try ui.Author.init(admin_moment, arena, commit_object.content.commit.metadata.author orelse "");│ │159 } │ │160 │ │160 -pub fn statusSet( │ │161 +fn statusSet( │ │162 comptime Data: type, │ │163 comptime DB: type, │ │164 statuses: DB.SortedMap(.read_only), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │168 return try DB.SortedSet(.read_only).init(cursor);│ │169 } │ │170 │ │170 -pub fn labelStatusSet( │ │171 +fn labelStatusSet( │ │172 comptime Data: type, │ │173 comptime DB: type, │ │174 label_statuses: DB.SortedMap(.read_only), │ ╰──────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │179 const key = Data.Event.labelStatusKey(&key_buffer, label, status) catch return null; │ │180 const cursor = (try label_statuses.getCursor(key)) orelse return null; │ │181 return try DB.SortedSet(.read_only).init(cursor); │ │182 +} │ │183 + │ │184 +// the sets `status`'s listing intersects: each label's set for the status (each │ │185 +// within the status set, which joins only without labels), then `extra`. none │ │186 +// when one is missing, which lists nothing. │ │187 +pub fn statusSets( │ │188 + comptime Data: type, │ │189 + comptime hash_kind: hash.HashKind, │ │190 + aa: std.mem.Allocator, │ │191 + haxy_moment: evt.EventDB(hash_kind).HashMap(.read_only), │ │192 + labels: []const []const u8, │ │193 + // null when one of them is missing │ │194 + extra: ?[]const evt.EventDB(hash_kind).SortedSet(.read_only), │ │195 + status: Data.Status, │ │196 +) ![]const evt.EventDB(hash_kind).SortedSet(.read_only) { │ │197 + const DB = evt.EventDB(hash_kind); │ │198 + var sets: std.ArrayList(DB.SortedSet(.read_only)) = .empty; │ │199 + if (labels.len == 0) { │ │200 + const cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, Data.Event.status_to_id_set_key)) orelse return &.{}; │ │201 + try sets.append(aa, (try statusSet(Data, DB, try DB.SortedMap(.read_only).init(cursor), status)) orelse return &.{}); │ │202 + } else { │ │203 + const cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, Data.Event.label_status_to_id_set_key)) orelse return &.{};│ │204 + const label_statuses = try DB.SortedMap(.read_only).init(cursor); │ │205 + for (labels) |label| try sets.append(aa, (try labelStatusSet(Data, DB, label_statuses, label, status)) orelse return &.{}); │ │206 + } │ │207 + try sets.appendSlice(aa, extra orelse return &.{}); │ │208 + return sets.items; │ │209 } │ │210 │ │211 +// whether `order_key` is in every one of `sets`, and there is at least one │ │212 +pub fn inEverySet(comptime DB: type, sets: []const DB.SortedSet(.read_only), order_key: []const u8) !bool { │ │213 + if (sets.len == 0) return false; │ │214 + for (sets) |set| { │ │215 + if (!try set.contains(order_key)) return false; │ │216 + } │ │217 + return true; │ │218 +} │ │219 + │ │220 pub fn loadLabels( │ │221 comptime Data: type, │ │222 comptime hash_kind: hash.HashKind, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │268 admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │269 haxy_moment: evt.EventDB(hash_kind).HashMap(.read_only), │ │270 records: evt.EventDB(hash_kind).HashMap(.read_only), │ │234 - set_maybe: ?evt.EventDB(hash_kind).SortedSet(.read_only), │ │271 + // the sets the listing intersects, all keyed alike; none lists nothing│ │272 + sets: []const evt.EventDB(hash_kind).SortedSet(.read_only), │ │273 root_key: ?[]const u8, │ │274 conflict_set: ?evt.EventDB(hash_kind).SortedSet(.read_only), │ │275 selected_id: []const u8, │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │276 comments_start: usize, │ │239 - // the search the set is narrowed to, or null to list it whole │ │240 - query: ?[]const u8, │ │241 - // a second set the listing is narrowed to, keyed like `set_maybe` │ │242 - extra_set: ?evt.EventDB(hash_kind).SortedSet(.read_only), │ │277 + // the words the listing is narrowed to, "" for none │ │278 + words: []const u8, │ │279 viewer: ?ui.Viewer, │ │280 ) !Data.Window { │ │245 - if (query != null or extra_set != null) return loadSearchWindow(Data, kind, hash_kind, arena, admin_moment, haxy_moment, records, set_maybe, root_key, conflict_set, selected_id, comments_start, query orelse "", extra_set, viewer);│ │246 - const set = set_maybe orelse return .empty; │ │281 + if (sets.len == 0) return .empty; │ │282 + if (sets.len > 1 or words.len != 0) return loadSearchWindow(Data, kind, hash_kind, arena, admin_moment, haxy_moment, records, sets, root_key, conflict_set, selected_id, comments_start, words, viewer); │ │283 + const set = sets[0]; │ │284 const aa = arena.allocator(); │ │285 │ │286 var prev_id: ?[]const u8 = null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │323 // how many matches one search counts before the count is marked as capped │ │324 const max_search_results = 1000; │ │325 │ │289 -// `set`'s entries matching `query_text` (any when empty) and held by │ │290 -// `extra_set` when set, windowed like `loadWindow`. one pass │ │291 -// yields the window, its neighbours' roots and the match count, which stops at│ │292 -// the cap. │ │326 +// the intersection of `sets` narrowed to `words`, windowed like `loadWindow`. │ │327 +// one pass yields the window, its neighbours' roots and the match count, which│ │328 +// stops at the cap. │ │329 fn loadSearchWindow( │ │330 comptime Data: type, │ │331 comptime kind: evt.EventKind, │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │334 admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │335 haxy_moment: evt.EventDB(hash_kind).HashMap(.read_only), │ │336 records: evt.EventDB(hash_kind).HashMap(.read_only), │ │301 - set_maybe: ?evt.EventDB(hash_kind).SortedSet(.read_only), │ │337 + sets: []const evt.EventDB(hash_kind).SortedSet(.read_only), │ │338 root_key: ?[]const u8, │ │339 conflict_set: ?evt.EventDB(hash_kind).SortedSet(.read_only),│ │340 selected_id: []const u8, │ ╰─────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │341 comments_start: usize, │ │306 - query_text: []const u8, │ │307 - extra_set: ?evt.EventDB(hash_kind).SortedSet(.read_only),│ │342 + words: []const u8, │ │343 viewer: ?ui.Viewer, │ │344 ) !Data.Window { │ │310 - const set = set_maybe orelse return .empty; │ │345 const DB = evt.EventDB(hash_kind); │ │346 const aa = arena.allocator(); │ │347 const root = root_key orelse ""; │ ╰──────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │352 var keys: std.ArrayList(OrderKey) = .empty; │ │353 if (comptime kind == .discuss) { │ │354 // discussions list by activity but are indexed by creation, so the │ │321 - // matching ids are gathered and the listed set is walked in its order │ │322 - // until every one has been found │ │323 - var matches: std.AutoHashMapUnmanaged([evt.event_id_size]u8, void) = .empty; │ │324 - var results = try srch_thrd.query(DB, hash_kind, kind, haxy_moment, aa, query_text, &.{extra_set}); │ │325 - while (try results.next()) |key| try matches.put(aa, (try resultKey(key))[@sizeOf(u64)..].*, {}); │ │355 + // word matches are gathered by id and the listed sets' intersection is │ │356 + // walked in its order until every one has been found │ │357 + // words too short to index narrow nothing, as on the other tabs │ │358 + var matches: ?std.AutoHashMapUnmanaged([evt.event_id_size]u8, void) = null; │ │359 + if (words.len != 0) { │ │360 + var results = try srch_thrd.query(DB, hash_kind, kind, haxy_moment, aa, words); │ │361 + if (results.terms.items.len != 0) { │ │362 + var found: std.AutoHashMapUnmanaged([evt.event_id_size]u8, void) = .empty; │ │363 + while (try results.next()) |key| try found.put(aa, (try resultKey(key))[@sizeOf(u64)..].*, {});│ │364 + matches = found; │ │365 + } │ │366 + } │ │367 │ │327 - var iter = try set.iteratorFromIndex(0); │ │328 - while (matches.count() > 0) { │ │329 - const pair = try ((try iter.next()) orelse break).readKeyValuePair(); │ │330 - var order_key: OrderKey = undefined; │ │331 - _ = try pair.key_cursor.readBytes(&order_key); │ │332 - if (!matches.remove(order_key[@sizeOf(u64)..].*)) continue; │ │368 + var listed: srch.Query(DB) = .{ .terms = .empty }; │ │369 + for (sets) |set| try listed.require(aa, set); │ │370 + while (true) { │ │371 + if (matches) |found| if (found.count() == 0) break; │ │372 + const order_key = try resultKey((try listed.next()) orelse break); │ │373 + if (matches) |*found| if (!found.remove(order_key[@sizeOf(u64)..].*)) continue; │ │374 try keys.append(aa, order_key); │ │375 if (windowKnown(Data, keys.items, root)) break; │ │376 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯