╭ changed ────────────╮ │src/search_thread.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │41 try srch.replace(DB, index, allocator, key, old_text, new_text); │ │42 } │ │43 │ │44 -// the doc keys matching `query_text`, in key order, narrowed to each non-null│ │45 -// set in `filters`, which must be keyed the same way │ │44 +// the doc keys matching `query_text`, in key order │ │45 pub fn query( │ │46 comptime DB: type, │ │47 comptime hash_kind: hash.HashKind, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 49 haxy_moment: DB.HashMap(.read_only), │ │ 50 aa: std.mem.Allocator, │ │ 51 query_text: []const u8, │ │ 53 - filters: []const ?DB.SortedSet(.read_only), │ │ 52 ) !srch.Query(DB) { │ │ 55 - // no index means no words match, which one empty term expresses │ │ 56 - var results: srch.Query(DB) = .{ .terms = .empty }; │ │ 57 - if (try haxy_moment.getCursor(hash.hashInt(hash_kind, indexKey(kind)))) |cursor| { │ │ 58 - results = try srch.Query(DB).init(try DB.SortedMap(.read_only).init(cursor), aa, query_text); │ │ 59 - } else { │ │ 53 + const cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, indexKey(kind))) orelse { │ │ 54 + // no index means no words match, which one empty term expresses │ │ 55 + var results: srch.Query(DB) = .{ .terms = .empty }; │ │ 56 try results.terms.append(aa, .{ .postings = &.{} }); │ │ 57 + return results; │ │ 58 + }; │ │ 59 + return srch.Query(DB).init(try DB.SortedMap(.read_only).init(cursor), aa, query_text); │ │ 60 +} │ │ 61 + │ │ 62 +pub const label_prefix = "label:"; │ │ 63 +pub const assignee_prefix = "assignee:"; │ │ 64 + │ │ 65 +// a thread search split into its filter terms and the words left for the index │ │ 66 +pub const Filters = struct { │ │ 67 + labels: []const []const u8, │ │ 68 + assignees: []const []const u8, │ │ 69 + // the remaining terms joined by single spaces, "" when none │ │ 70 + words: []const u8, │ │ 71 +}; │ │ 72 + │ │ 73 +// split `search` on whitespace into label and assignee terms and plain words. │ │ 74 +// a prefix with no value is dropped. │ │ 75 +pub fn parseFilters(aa: std.mem.Allocator, search: []const u8) !Filters { │ │ 76 + var labels: std.ArrayList([]const u8) = .empty; │ │ 77 + var assignees: std.ArrayList([]const u8) = .empty; │ │ 78 + var words: std.ArrayList([]const u8) = .empty; │ │ 79 + var iter = std.mem.tokenizeAny(u8, search, &std.ascii.whitespace); │ │ 80 + while (iter.next()) |term| { │ │ 81 + if (std.mem.startsWith(u8, term, label_prefix)) { │ │ 82 + if (term.len > label_prefix.len) try labels.append(aa, term[label_prefix.len..]); │ │ 83 + } else if (std.mem.startsWith(u8, term, assignee_prefix)) { │ │ 84 + if (term.len > assignee_prefix.len) try assignees.append(aa, term[assignee_prefix.len..]); │ │ 85 + } else try words.append(aa, term); │ │ 86 } │ │ 62 - for (filters) |filter| if (filter) |set| try results.require(aa, set); │ │ 63 - return results; │ │ 87 + return .{ .labels = labels.items, .assignees = assignees.items, .words = try std.mem.join(aa, " ", words.items) };│ │ 88 +} │ │ 89 + │ │ 90 +// `search` with its `prefix` terms replaced by one for `value`, put first │ │ 91 +pub fn withFilter(aa: std.mem.Allocator, search: []const u8, prefix: []const u8, value: []const u8) ![]const u8 { │ │ 92 + var terms: std.ArrayList([]const u8) = .empty; │ │ 93 + try terms.append(aa, try std.mem.concat(aa, u8, &.{ prefix, value })); │ │ 94 + var iter = std.mem.tokenizeAny(u8, search, &std.ascii.whitespace); │ │ 95 + while (iter.next()) |term| { │ │ 96 + if (!std.mem.startsWith(u8, term, prefix)) try terms.append(aa, term); │ │ 97 + } │ │ 98 + return std.mem.join(aa, " ", terms.items); │ │ 99 } │ │100 │ │101 // the text a thread is indexed under, none once it is removed: the title and │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯