Skip to main content

tilo_app/framework/views/
engine.rs

1//! The query behind a query view: the app's local items answered from an
2//! index in the frame of the keystroke, and its remote sources streamed in
3//! below them.
4//!
5//! - **Local.** The app hands its items over whenever they change
6//!   ([`QueryEngine::sync_local`]); the index takes only what changed. A query
7//!   is answered from it before [`QueryEngine::set_query`] returns.
8//! - **Remote.** Each [`RemoteSource`] is asked off the UI thread. A query
9//!   goes out at once unless one went out less than [`THROTTLE`] ago, in
10//!   which case it goes out when that much has passed, and only the newest
11//!   query does; asking again cancels the request before it. Answers are
12//!   cached by query: asking the same again shows them at once, and a query
13//!   that goes on from a cached one ("auror" after "auro") shows what of the
14//!   earlier answer still matches while its own is on the way. Focusing the
15//!   field warms the sources ([`QueryEngine::warm`]).
16//! - **Stable rows.** Remote rows come in their own sections, below the
17//!   local ones, and an answer never touches a local row. While the person
18//!   points at or has the keyboard in a remote section
19//!   ([`QueryEngine::hold`]), what arrives only goes on at its end: no row
20//!   they could be aiming at moves.
21
22use std::collections::{HashMap, HashSet, VecDeque};
23use std::hash::{Hash, Hasher};
24use std::sync::Arc;
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::mpsc::{Receiver, Sender, channel};
27
28use tilo_resource::SearchBody;
29use tilo_search::{Document, FieldWeight, Matching, Page, SearchIndex};
30use ui_core::WakeHandle;
31use ui_core::arriving::{Arrival, ArrivalClock, Source, Step};
32use ui_core::background::Background;
33use ui_core::time::{Duration, Instant};
34
35use super::super::root::{BodyText, field_weight};
36use super::kind::{FieldRole, Form, Item, KindDecl};
37use super::query::QuerySpec;
38
39/// The least time between two requests to one remote source while the
40/// person types. A query typed within it goes out when it has passed.
41pub const THROTTLE: Duration = Duration::from_millis(40);
42
43/// How long a remote answer is shown from the cache without asking again.
44pub const FRESH: Duration = Duration::from_secs(300);
45
46/// How many answers each remote source keeps.
47const CACHED_ANSWERS: usize = 48;
48
49/// How long warming a source lasts before focusing the field warms it again.
50const WARM_FOR: Duration = Duration::from_secs(60);
51
52/// How many local matches are looked at for one query.
53const LOCAL_CANDIDATES: usize = 600;
54
55/// How many items one group of results shows.
56pub const GROUP_LIMIT: usize = 50;
57
58/// Asked by a running request whether it should stop: a newer query took
59/// its place.
60#[derive(Clone, Debug, Default)]
61pub struct Cancel(Arc<AtomicBool>);
62
63impl Cancel {
64    #[must_use]
65    pub fn is_cancelled(&self) -> bool {
66        self.0.load(Ordering::Acquire)
67    }
68
69    fn cancel(&self) {
70        self.0.store(true, Ordering::Release);
71    }
72}
73
74/// Why a remote source could not answer, as its section says it.
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct RemoteError(pub String);
77
78impl std::fmt::Display for RemoteError {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.write_str(&self.0)
81    }
82}
83
84/// A source of items that are not local: a streaming catalogue, a store, a
85/// collaborator's library. It keeps its own index; nothing of it is
86/// downloaded to be searched.
87pub trait RemoteSource: Send + Sync + 'static {
88    /// A stable id, for its cache and its section.
89    fn id(&self) -> &str;
90
91    /// What its section is headed ("`YouTube` Music").
92    fn label(&self) -> String;
93
94    /// Where its answers come from, as its section's placeholder names it
95    /// while one is on the way: a host serving the catalogue by default; a
96    /// collaborator's library is a peer.
97    fn origin(&self) -> Source {
98        Source::host(self.label())
99    }
100
101    /// Whether it answers `query` at all: a source does not know the
102    /// person's own marks (Liked), so a query narrowed by them stays local.
103    fn accepts(&self, query: &QuerySpec) -> bool {
104        !query.text.trim().is_empty()
105    }
106
107    /// Open the connection before the first query: the person focused the
108    /// field. Runs off the UI thread.
109    fn warm(&self) {}
110
111    /// Answer `query`, handing each page of items to `page` as it arrives,
112    /// best first. Runs off the UI thread; stop early when `cancel` says
113    /// so.
114    fn search(
115        &self,
116        query: &QuerySpec,
117        cancel: &Cancel,
118        page: &mut dyn FnMut(Vec<Item>),
119    ) -> Result<(), RemoteError>;
120}
121
122/// An index of the app's own that answers with the local items, in the
123/// same frame: matches in what this device does not hold yet, such as a
124/// library's synced index of books whose text is elsewhere. It answers on
125/// the UI thread, so within the frame's budget.
126pub trait LocalSource: Send + Sync + 'static {
127    /// The items `query` finds, best first, at most `limit`.
128    fn search(&self, query: &QuerySpec, limit: usize) -> Vec<Item>;
129}
130
131/// The items of one type in a list of results, best first.
132#[derive(Clone, Debug, PartialEq)]
133pub struct ResultGroup {
134    pub kind: &'static str,
135    /// The type's plural ("Songs").
136    pub label: String,
137    pub form: Form,
138    pub items: Vec<Item>,
139    /// How many more matched than the group shows.
140    pub more: usize,
141}
142
143/// What one remote source has found for the query.
144#[derive(Clone, Debug, PartialEq)]
145pub struct RemoteSection {
146    pub source: String,
147    pub label: String,
148    /// Where its answer stands: pending (its rows streaming in), answered,
149    /// or unreachable, in words (empty when it gave none). `None` when the
150    /// query is not one it answers.
151    pub state: Option<Arrival<(), RemoteError>>,
152    /// The timing of its answer, for its placeholder
153    /// ([`ui_core::widget::ArrivalFace`]): set when it was asked.
154    pub clock: Option<ArrivalClock>,
155    /// Its rows, as groups in the order they are shown.
156    pub groups: Vec<ResultGroup>,
157}
158
159impl RemoteSection {
160    /// A section of a source not asked.
161    fn unasked(source: String, label: String) -> Self {
162        Self {
163            source,
164            label,
165            state: None,
166            clock: None,
167            groups: Vec::new(),
168        }
169    }
170
171    /// Whether its answer is still on the way.
172    #[must_use]
173    pub fn is_waiting(&self) -> bool {
174        self.state.as_ref().is_some_and(Arrival::is_pending)
175    }
176
177    /// Whether it answered in full.
178    #[must_use]
179    pub fn is_answered(&self) -> bool {
180        matches!(self.state, Some(Arrival::Ready(())))
181    }
182
183    /// Why it could not answer, once it could not.
184    #[must_use]
185    pub fn failure(&self) -> Option<&RemoteError> {
186        self.state.as_ref().and_then(Arrival::failure)
187    }
188
189    /// The timing of the placeholder that stands where its rows will be:
190    /// while its answer is on the way and no row has come yet. A painter
191    /// paints [`ui_core::widget::ArrivalFace`] from it in the space of a row.
192    #[must_use]
193    pub fn placeholder(&self) -> Option<&ArrivalClock> {
194        if self.is_waiting() && self.groups.is_empty() {
195            self.clock.as_ref()
196        } else {
197            None
198        }
199    }
200
201    /// It was asked at `now`; its answer comes from `from`.
202    fn ask(&mut self, from: Source, now: Instant) {
203        self.state = Some(Arrival::Pending { from: from.clone() });
204        self.clock = Some(ArrivalClock::new(now, from));
205    }
206
207    /// Its answer (or why there is none) landed at `now`.
208    fn settle(&mut self, answer: Result<(), RemoteError>, now: Instant) {
209        self.state = Some(answer.into());
210        if let Some(clock) = self.clock.as_mut() {
211            clock.arrive(now);
212        }
213    }
214
215    /// The rows shown, in order.
216    pub fn rows(&self) -> impl Iterator<Item = &Item> {
217        self.groups.iter().flat_map(|group| &group.items)
218    }
219
220    fn holds(&self, item: &Item) -> bool {
221        self.rows().any(|row| row.key() == item.key())
222    }
223}
224
225/// What a query has found so far.
226#[derive(Clone, Debug, Default, PartialEq)]
227pub struct QueryResults {
228    /// Which query this is: bumped by every change of the query.
229    pub generation: u64,
230    pub query: QuerySpec,
231    /// The local matches, a group per type in the types' order.
232    pub local: Vec<ResultGroup>,
233    /// A section per remote source, in the order the sources were given.
234    pub remote: Vec<RemoteSection>,
235}
236
237impl QueryResults {
238    /// Whether a remote answer is still on the way: the window's loading
239    /// line shows while it is.
240    #[must_use]
241    pub fn pending(&self) -> bool {
242        self.remote.iter().any(RemoteSection::is_waiting)
243    }
244
245    /// The placeholder step each section shows at `now` (`None` where it
246    /// shows none): a painter that kept what it drew repaints when this
247    /// changes.
248    #[must_use]
249    pub fn placeholder_steps(&self, now: Instant) -> Vec<Option<Step>> {
250        self.remote
251            .iter()
252            .map(|section| section.placeholder().map(|clock| clock.step(now)))
253            .collect()
254    }
255
256    /// When a section's placeholder next changes step: the deadline its
257    /// window wakes at, so the steps take no frames in between.
258    #[must_use]
259    pub fn next_placeholder_step(&self, now: Instant) -> Option<Instant> {
260        self.remote
261            .iter()
262            .filter_map(RemoteSection::placeholder)
263            .filter_map(|clock| clock.next_step_at(now))
264            .min()
265    }
266
267    /// Whether nothing was found anywhere, and nothing is still coming.
268    #[must_use]
269    pub fn is_empty(&self) -> bool {
270        self.local.is_empty()
271            && !self.pending()
272            && self.remote.iter().all(|section| section.groups.is_empty())
273    }
274
275    /// Every row shown, local first, as `(type, id)`: what a person sees
276    /// from the top down.
277    #[must_use]
278    pub fn order(&self) -> Vec<(&'static str, String)> {
279        self.local
280            .iter()
281            .flat_map(|group| &group.items)
282            .chain(self.remote.iter().flat_map(RemoteSection::rows))
283            .map(|item| (item.kind, item.id.clone()))
284            .collect()
285    }
286}
287
288/// How long a query took to show its results, from the moment it was set.
289#[derive(Clone, Debug, Default, PartialEq, Eq)]
290pub struct QueryTimings {
291    /// The local results: answered before the query was set.
292    pub local: Option<Duration>,
293    /// Each remote source's first rows, in the order the sources were given.
294    pub remote_first: Vec<Option<Duration>>,
295    /// Each remote source's whole answer.
296    pub remote_done: Vec<Option<Duration>>,
297}
298
299/// One message from a request running off the UI thread.
300struct Answer {
301    source: usize,
302    generation: u64,
303    query: QuerySpec,
304    event: AnswerEvent,
305}
306
307enum AnswerEvent {
308    Page(Vec<Item>),
309    Done,
310    Failed(String),
311}
312
313/// One answer a remote source gave, kept by query.
314struct CachedAnswer {
315    key: String,
316    query: QuerySpec,
317    items: Vec<Item>,
318    complete: bool,
319    at: Instant,
320}
321
322/// One remote source and what is in flight to it.
323struct RemoteSlot {
324    source: Arc<dyn RemoteSource>,
325    last_sent: Option<Instant>,
326    /// When the current query goes out, if it has not yet.
327    due: Option<Instant>,
328    cancel: Option<Cancel>,
329    warmed: Option<Instant>,
330    cache: VecDeque<CachedAnswer>,
331    /// The rows shown before this query's own answer: what an earlier,
332    /// cached answer had that still matches.
333    provisional: HashSet<(&'static str, String)>,
334    /// What of this query's own answer has arrived.
335    answered: Vec<Item>,
336    sent_at: Option<Instant>,
337}
338
339impl RemoteSlot {
340    fn cached(&self, key: &str) -> Option<&CachedAnswer> {
341        self.cache.iter().find(|answer| answer.key == key)
342    }
343
344    /// The complete cached answer `query` goes on from, with the longest
345    /// text.
346    fn refined_from(&self, query: &QuerySpec) -> Option<&CachedAnswer> {
347        self.cache
348            .iter()
349            .filter(|answer| answer.complete && query.refines(&answer.query))
350            .max_by_key(|answer| answer.query.text.len())
351    }
352
353    fn keep(&mut self, query: &QuerySpec, items: &[Item], complete: bool, at: Instant) {
354        let key = query.key();
355        if let Some(at_index) = self.cache.iter().position(|answer| answer.key == key) {
356            let mut answer = self.cache.remove(at_index).unwrap_or_else(|| CachedAnswer {
357                key: key.clone(),
358                query: query.clone(),
359                items: Vec::new(),
360                complete: false,
361                at,
362            });
363            for item in items {
364                if !answer.items.iter().any(|held| held.key() == item.key()) {
365                    answer.items.push(item.clone());
366                }
367            }
368            answer.complete |= complete;
369            answer.at = at;
370            self.cache.push_back(answer);
371        } else {
372            self.cache.push_back(CachedAnswer {
373                key,
374                query: query.clone(),
375                items: items.to_vec(),
376                complete,
377                at,
378            });
379        }
380        while self.cache.len() > CACHED_ANSWERS {
381            self.cache.pop_front();
382        }
383    }
384}
385
386/// The app's local items and their index.
387///
388/// Items' fields are indexed here, a change at a time, in the frame it is
389/// handed over. Their prose is indexed apart ([`ProseIndex`]): reading a
390/// library's books takes longer than a frame, so it is built off the UI
391/// thread, again only when the bodies themselves change, and searches
392/// answer from the last one built until the next lands.
393struct LocalIndex {
394    index: SearchIndex,
395    keys: HashMap<(&'static str, String), u64>,
396    items: HashMap<u64, (Item, u64)>,
397    next_key: u64,
398    prose: Option<ProseIndex>,
399    /// The bodies the newest prose index asked for holds, by item key.
400    prose_wanted: HashMap<u64, usize>,
401}
402
403/// The prose of the items that have a body, indexed by item key: a match
404/// names the segment it is in.
405struct ProseIndex {
406    /// Which build this is; a build older than one already in is dropped.
407    build: u64,
408    index: SearchIndex,
409}
410
411impl ProseIndex {
412    fn built(build: u64, bodies: Vec<(u64, SearchBody)>) -> Self {
413        let mut index = SearchIndex::new(Matching::LOOSE);
414        index.replace_all(
415            bodies
416                .into_iter()
417                .map(|(key, body)| Document::new(key).body(Arc::new(BodyText(body.shared())))),
418        );
419        if let Err(error) = index.commit() {
420            tracing::warn!(%error, "could not index a view's prose");
421        }
422        Self { build, index }
423    }
424}
425
426/// Where a body's text lives: what tells one body from another.
427fn body_address(body: &SearchBody) -> usize {
428    Arc::as_ptr(&body.shared()).cast::<()>().addr()
429}
430
431impl Default for LocalIndex {
432    fn default() -> Self {
433        Self {
434            // Titles are names a person half remembers: typos match too.
435            index: SearchIndex::new(Matching::LOOSE),
436            keys: HashMap::new(),
437            items: HashMap::new(),
438            next_key: 0,
439            prose: None,
440            prose_wanted: HashMap::new(),
441        }
442    }
443}
444
445fn fingerprint(item: &Item) -> u64 {
446    let mut hasher = std::collections::hash_map::DefaultHasher::new();
447    item.fields.hash(&mut hasher);
448    item.found_by.hash(&mut hasher);
449    // A body is the same body only while it shares one text.
450    item.body.as_ref().map(body_address).hash(&mut hasher);
451    hasher.finish()
452}
453
454fn weight(role: Option<FieldRole>) -> FieldWeight {
455    match role {
456        Some(FieldRole::Title) => FieldWeight::TITLE,
457        Some(FieldRole::Subtitle) => FieldWeight::SUBTITLE,
458        _ => FieldWeight::METADATA,
459    }
460}
461
462impl LocalIndex {
463    /// Take `items` as the whole local set: what changed is indexed again,
464    /// what is gone leaves. Returns whether anything changed, and the
465    /// bodies a new prose index is to hold when they changed.
466    fn sync(
467        &mut self,
468        kinds: &[KindDecl],
469        items: impl IntoIterator<Item = Item>,
470    ) -> (bool, Option<Vec<(u64, SearchBody)>>) {
471        let mut seen = HashSet::new();
472        let mut changed = false;
473        let mut bodies = Vec::new();
474        let mut wanted = HashMap::new();
475        for item in items {
476            let print = fingerprint(&item);
477            let key = if let Some(key) = self.keys.get(&(item.kind, item.id.clone())) {
478                *key
479            } else {
480                let key = self.next_key;
481                self.next_key = self.next_key.wrapping_add(1);
482                self.keys.insert((item.kind, item.id.clone()), key);
483                key
484            };
485            seen.insert(key);
486            if let Some(body) = &item.body {
487                wanted.insert(key, body_address(body));
488                bodies.push((key, body.clone()));
489            }
490            if self.items.get(&key).is_some_and(|(_, held)| *held == print) {
491                continue;
492            }
493            let kind = kinds.iter().find(|kind| kind.kind == item.kind);
494            let document = item
495                .fields
496                .iter()
497                .filter_map(|(field, value)| match kind {
498                    Some(kind) => kind
499                        .field_of(field)
500                        .filter(|decl| decl.searchable)
501                        .map(|decl| (value.as_str(), weight(decl.role))),
502                    None => Some((value.as_str(), FieldWeight::METADATA)),
503                })
504                .chain(
505                    item.found_by
506                        .iter()
507                        .map(|(text, weight)| (text.as_str(), field_weight(*weight))),
508                )
509                .fold(Document::new(key), |document, (value, weight)| {
510                    document.field(value, weight)
511                });
512            self.index.upsert(document);
513            self.items.insert(key, (item, print));
514            changed = true;
515        }
516        let gone: Vec<u64> = self
517            .items
518            .keys()
519            .filter(|key| !seen.contains(*key))
520            .copied()
521            .collect();
522        for key in gone {
523            if let Some((item, _)) = self.items.remove(&key) {
524                self.keys.remove(&(item.kind, item.id));
525            }
526            self.index.remove(key);
527            changed = true;
528        }
529        if changed && let Err(error) = self.index.commit() {
530            tracing::warn!(%error, "could not index a view's local items");
531        }
532        let prose = (wanted != self.prose_wanted).then(|| {
533            self.prose_wanted = wanted;
534            bodies
535        });
536        (changed, prose)
537    }
538
539    /// Answer from `prose` from now on, unless a newer build is in.
540    fn land_prose(&mut self, prose: ProseIndex) -> bool {
541        if self
542            .prose
543            .as_ref()
544            .is_some_and(|held| held.build > prose.build)
545        {
546            return false;
547        }
548        self.prose = Some(prose);
549        true
550    }
551
552    /// The local items that match `query`, best first. A query of chips
553    /// alone lists everything they admit, by title.
554    fn search(&self, kinds: &[KindDecl], query: &QuerySpec) -> Vec<Item> {
555        if query.text.trim().is_empty() {
556            if query.chips.is_empty() {
557                return Vec::new();
558            }
559            let mut found: Vec<(String, &Item)> = self
560                .items
561                .values()
562                .map(|(item, _)| item)
563                .filter(|item| query.admits(item))
564                .map(|item| (tilo_search::fold(title_of(kinds, item)), item))
565                .collect();
566            found.sort_by(|(a_title, a), (b_title, b)| {
567                a_title.cmp(b_title).then_with(|| a.id.cmp(&b.id))
568            });
569            return found.into_iter().map(|(_, item)| item.clone()).collect();
570        }
571        let mut seen = HashSet::new();
572        // Matches in fields first, then in prose, as one index would rank
573        // them.
574        let prose = self.prose.as_ref().map(|prose| {
575            prose
576                .index
577                .search(&query.text, Page::first(LOCAL_CANDIDATES))
578                .hits
579        });
580        self.index
581            .search(&query.text, Page::first(LOCAL_CANDIDATES))
582            .hits
583            .into_iter()
584            .chain(prose.into_iter().flatten())
585            .filter_map(|hit| {
586                let (item, _) = self.items.get(&hit.key)?;
587                Some(match hit.segment {
588                    Some(segment) => located(kinds, item, segment, &query.text)?,
589                    None => item.clone(),
590                })
591            })
592            .filter(|item| query.admits(item))
593            .filter(|item| seen.insert((item.kind, item.id.clone())))
594            .collect()
595    }
596}
597
598/// The hit a match in `segment` of `item`'s body is: the location the body
599/// names there, as an item of the location's type, carrying the filter
600/// values of the item it is in and the passage the match is in. `None`
601/// when the body does not locate the segment or no type is the location's.
602fn located(kinds: &[KindDecl], item: &Item, segment: usize, text: &str) -> Option<Item> {
603    let body = item.body.as_ref()?.text();
604    if !body.locates(segment) {
605        return None;
606    }
607    let mut location = Item::from_entry(kinds, &body.location(segment)?)?;
608    let decl = kinds.iter().find(|kind| kind.kind == location.kind)?;
609    for (key, value) in &item.fields {
610        let filters = decl
611            .field_of(key)
612            .is_some_and(|field| field.filter.is_some());
613        if filters && location.value(key).is_none() {
614            location.fields.push((*key, value.clone()));
615        }
616    }
617    if let Some(field) = decl
618        .fields
619        .iter()
620        .find(|field| field.role == Some(FieldRole::Excerpt))
621    {
622        let passage = segment_text(body, segment);
623        location = location.with(field.key, excerpt(&passage, text));
624    }
625    Some(location)
626}
627
628/// The prose of `segment`, its pieces joined.
629fn segment_text(body: &dyn tilo_resource::SearchText, segment: usize) -> String {
630    let mut text = String::new();
631    for at in 0..body.piece_count(segment) {
632        if let Some(piece) = body.piece(segment, at) {
633            if piece.space_before && !text.is_empty() {
634                text.push(' ');
635            }
636            text.push_str(piece.text);
637        }
638    }
639    text
640}
641
642/// How far back from where it would start an excerpt looks for a word's
643/// start.
644const EXCERPT_REACH: usize = 96;
645
646/// How many characters an excerpt shows in all.
647const EXCERPT_LENGTH: usize = 180;
648
649/// The part of `passage` around the first word of `query` it holds, the
650/// match in its middle, cut at word boundaries and marked where it was cut:
651/// what a hit inside prose shows of it.
652#[must_use]
653pub fn excerpt(passage: &str, query: &str) -> String {
654    let chars: Vec<(usize, char)> = passage.char_indices().collect();
655    // The passage folded a character at a time, each folded character
656    // pointing back at the one it came from.
657    let mut folded = String::new();
658    let mut origin = Vec::new();
659    for (at, (_, ch)) in chars.iter().enumerate() {
660        tilo_search::fold_char(*ch, &mut |folded_char| {
661            folded.push(folded_char);
662            origin.extend(std::iter::repeat_n(at, folded_char.len_utf8()));
663        });
664    }
665    let (found, word_len) = query
666        .split_whitespace()
667        .map(tilo_search::fold)
668        .filter(|word| !word.is_empty() && !word.contains(':'))
669        .find_map(|word| {
670            let byte = folded.find(&word)?;
671            Some((*origin.get(byte)?, word.chars().count()))
672        })
673        .unwrap_or((0, 0));
674    // The match in the middle, or as near it as the passage's ends allow.
675    let before = EXCERPT_LENGTH.saturating_sub(word_len) / 2;
676    let mut start = found
677        .saturating_sub(before)
678        .min(chars.len().saturating_sub(EXCERPT_LENGTH));
679    let planned = start;
680    // Back to the word's start, unless the word runs on for long.
681    while start > 0
682        && planned.saturating_sub(start) < EXCERPT_REACH
683        && !chars
684            .get(start.saturating_sub(1))
685            .is_some_and(|(_, ch)| ch.is_whitespace())
686    {
687        start = start.saturating_sub(1);
688    }
689    let mut end = start.saturating_add(EXCERPT_LENGTH).min(chars.len());
690    while end < chars.len() && !chars.get(end).is_some_and(|(_, ch)| ch.is_whitespace()) {
691        end = end.saturating_add(1);
692    }
693    let byte = |at: usize| chars.get(at).map_or(passage.len(), |(byte, _)| *byte);
694    let mut shown = passage
695        .get(byte(start)..byte(end))
696        .unwrap_or_default()
697        .trim()
698        .to_owned();
699    if start > 0 {
700        shown.insert(0, '\u{2026}');
701    }
702    if end < chars.len() {
703        shown.push('\u{2026}');
704    }
705    shown
706}
707
708/// The value of `item`'s title field, as its type declares it; its first
709/// field when the type declares none.
710fn title_of<'a>(kinds: &[KindDecl], item: &'a Item) -> &'a str {
711    let title = kinds
712        .iter()
713        .find(|kind| kind.kind == item.kind)
714        .and_then(|kind| {
715            kind.fields
716                .iter()
717                .find(|field| field.role == Some(FieldRole::Title))
718        })
719        .and_then(|field| item.value(field.key));
720    title.unwrap_or_else(|| item.fields.first().map_or("", |(_, value)| value.as_str()))
721}
722
723/// `items` as groups, one per type in the order of `kinds`, keeping their
724/// order within a type; at most `limit` items a group.
725fn grouped(kinds: &[KindDecl], items: Vec<Item>, limit: usize) -> Vec<ResultGroup> {
726    let mut groups: Vec<ResultGroup> = Vec::new();
727    for item in items {
728        match groups.iter_mut().find(|group| group.kind == item.kind) {
729            Some(group) if group.items.len() < limit => group.items.push(item),
730            Some(group) => group.more = group.more.saturating_add(1),
731            None => {
732                let decl = kinds.iter().find(|kind| kind.kind == item.kind);
733                groups.push(ResultGroup {
734                    kind: item.kind,
735                    label: decl.map_or_else(|| item.kind.to_owned(), |kind| kind.plural.clone()),
736                    form: decl.map_or(Form::Row, |kind| kind.form),
737                    items: vec![item],
738                    more: 0,
739                });
740            }
741        }
742    }
743    let order = |kind: &str| {
744        kinds
745            .iter()
746            .position(|decl| decl.kind == kind)
747            .unwrap_or(usize::MAX)
748    };
749    groups.sort_by_key(|group| order(group.kind));
750    groups
751}
752
753/// The query of one query view: its local index, its remote sources, and
754/// what it has found so far.
755pub struct QueryEngine {
756    kinds: Vec<KindDecl>,
757    local: LocalIndex,
758    remotes: Vec<RemoteSlot>,
759    open: bool,
760    results: QueryResults,
761    timings: QueryTimings,
762    started: Option<Instant>,
763    held: bool,
764    background: Background,
765    wake: WakeHandle,
766    send: Sender<Answer>,
767    answers: Receiver<Answer>,
768    /// How many prose indexes have been asked for.
769    prose_builds: u64,
770    prose_send: Sender<ProseIndex>,
771    prose_built: Receiver<ProseIndex>,
772    /// The app's own index, answering with the local items.
773    source: Option<Arc<dyn LocalSource>>,
774}
775
776impl QueryEngine {
777    /// An engine for items of `kinds`, which also order the groups. It runs
778    /// remote requests on the executor current now and wakes the window it
779    /// is made in when they answer.
780    #[must_use]
781    pub fn new(kinds: Vec<KindDecl>) -> Self {
782        let (send, answers) = channel();
783        let (prose_send, prose_built) = channel();
784        Self {
785            kinds,
786            local: LocalIndex::default(),
787            remotes: Vec::new(),
788            open: false,
789            results: QueryResults::default(),
790            timings: QueryTimings::default(),
791            started: None,
792            held: false,
793            background: Background::current(),
794            wake: WakeHandle::new(),
795            send,
796            answers,
797            prose_builds: 0,
798            prose_send,
799            prose_built,
800            source: None,
801        }
802    }
803
804    /// Answer with `source` too, beside the local items; `None` stops. What
805    /// it finds for the open query joins the rows after those shown.
806    pub fn set_local_source(&mut self, source: Option<Arc<dyn LocalSource>>) {
807        self.source = source;
808        if self.open {
809            self.add_found();
810        }
811    }
812
813    /// What the local items and the app's own index find for `query`, the
814    /// items first, each once.
815    fn found_locally(&self, query: &QuerySpec) -> Vec<Item> {
816        let mut found = self.local.search(&self.kinds, query);
817        if let Some(source) = &self.source
818            && !query.text.trim().is_empty()
819        {
820            let mut seen: HashSet<(&'static str, String)> = found
821                .iter()
822                .map(|item| (item.kind, item.id.clone()))
823                .collect();
824            found.extend(
825                source
826                    .search(query, LOCAL_CANDIDATES)
827                    .into_iter()
828                    .filter(|item| query.admits(item))
829                    .filter(|item| seen.insert((item.kind, item.id.clone()))),
830            );
831        }
832        found
833    }
834
835    /// Ask `source` too, in a section below the local results.
836    #[must_use]
837    pub fn with_remote(mut self, source: Arc<dyn RemoteSource>) -> Self {
838        self.remotes.push(RemoteSlot {
839            source,
840            last_sent: None,
841            due: None,
842            cancel: None,
843            warmed: None,
844            cache: VecDeque::new(),
845            provisional: HashSet::new(),
846            answered: Vec::new(),
847            sent_at: None,
848        });
849        self
850    }
851
852    /// Run remote requests on `background` instead (a test's pumped
853    /// executor).
854    #[must_use]
855    pub fn with_background(mut self, background: Background) -> Self {
856        self.background = background;
857        self
858    }
859
860    /// The types the engine knows.
861    #[must_use]
862    pub fn kinds(&self) -> &[KindDecl] {
863        &self.kinds
864    }
865
866    /// Wake `wake`'s window when a remote answer arrives or a request falls
867    /// due, in place of the one current when the engine was made.
868    pub fn set_waker(&mut self, wake: WakeHandle) {
869        self.wake = wake;
870    }
871
872    /// Take `items` as the app's whole local set; only what changed is
873    /// indexed again. The rows an open query shows follow the change where
874    /// they stand: a row whose item changed shows it changed (liked now), a
875    /// row whose item left or no longer matches goes, and nothing new comes
876    /// in above the rows below it until the query changes. Liking a
877    /// catalogue song in the results brings it and its album into the
878    /// library, and neither may push the row the person just pressed from
879    /// under the pointer.
880    ///
881    /// Prose ([`Item::body`]) is indexed off the UI thread when the bodies
882    /// change; the matches in it join the open query's rows when it lands
883    /// ([`Self::poll`]), after the rows shown.
884    pub fn sync_local(&mut self, items: impl IntoIterator<Item = Item>) {
885        let (changed, prose) = self.local.sync(&self.kinds, items);
886        if let Some(bodies) = prose {
887            self.build_prose(bodies);
888        }
889        if !changed || !self.open {
890            return;
891        }
892        let fresh: HashMap<(&'static str, String), Item> = self
893            .found_locally(&self.results.query)
894            .into_iter()
895            .map(|item| ((item.kind, item.id.clone()), item))
896            .collect();
897        for group in &mut self.results.local {
898            group.items = group
899                .items
900                .iter()
901                .filter_map(|item| fresh.get(&(item.kind, item.id.clone())).cloned())
902                .collect();
903        }
904        self.results.local.retain(|group| !group.items.is_empty());
905    }
906
907    /// Index `bodies` off the UI thread, or here when nothing runs work in
908    /// the background.
909    fn build_prose(&mut self, bodies: Vec<(u64, SearchBody)>) {
910        self.prose_builds = self.prose_builds.wrapping_add(1);
911        let build = self.prose_builds;
912        let send = self.prose_send.clone();
913        let wake = self.wake.clone();
914        let spawned = self.background.spawn("view-prose-index", {
915            let bodies = bodies.clone();
916            move || {
917                let _ = send.send(ProseIndex::built(build, bodies));
918                wake.wake();
919            }
920        });
921        if spawned.is_err() {
922            self.land_prose(ProseIndex::built(build, bodies));
923        }
924    }
925
926    /// Answer from `prose`, and add what it finds for the open query after
927    /// the rows shown: a row the person sees never moves for it.
928    fn land_prose(&mut self, prose: ProseIndex) -> bool {
929        if !self.local.land_prose(prose) || !self.open {
930            return false;
931        }
932        self.add_found()
933    }
934
935    /// Add what the open query finds now and did not before after the rows
936    /// shown: a row the person sees never moves for it.
937    fn add_found(&mut self) -> bool {
938        let shown: HashSet<(&'static str, String)> = self
939            .results
940            .local
941            .iter()
942            .flat_map(|group| &group.items)
943            .map(|item| (item.kind, item.id.clone()))
944            .collect();
945        let found: Vec<Item> = self
946            .found_locally(&self.results.query)
947            .into_iter()
948            .filter(|item| !shown.contains(&(item.kind, item.id.clone())))
949            .collect();
950        if found.is_empty() {
951            return false;
952        }
953        for group in grouped(&self.kinds, found, GROUP_LIMIT) {
954            match self
955                .results
956                .local
957                .iter_mut()
958                .find(|held| held.kind == group.kind)
959            {
960                Some(held) => {
961                    let workspace = GROUP_LIMIT.saturating_sub(held.items.len());
962                    let extra = group.items.len().saturating_sub(workspace);
963                    held.items.extend(group.items.into_iter().take(workspace));
964                    held.more = held.more.saturating_add(group.more).saturating_add(extra);
965                }
966                None => self.results.local.push(group),
967            }
968        }
969        true
970    }
971
972    /// The person focused the search field: open the remote sources'
973    /// connections before the first character.
974    pub fn warm(&mut self) {
975        let now = self.background.now();
976        for slot in &mut self.remotes {
977            if slot
978                .warmed
979                .is_some_and(|at| now.saturating_duration_since(at) < WARM_FOR)
980            {
981                continue;
982            }
983            slot.warmed = Some(now);
984            let source = Arc::clone(&slot.source);
985            if self
986                .background
987                .spawn("view-query-warm", move || source.warm())
988                .is_err()
989            {
990                tracing::debug!("no background work here: a remote source stays cold");
991            }
992        }
993    }
994
995    /// Ask `query`. The local results are in [`Self::results`] when this
996    /// returns; remote ones follow through [`Self::poll`]. Asking what is
997    /// asked already changes nothing. Returns whether the results changed.
998    pub fn set_query(&mut self, query: QuerySpec) -> bool {
999        if self.open && query.key() == self.results.query.key() {
1000            if query != self.results.query {
1001                // The same question typed another way: chips picked rather
1002                // than typed keep their look.
1003                self.results.query = query;
1004                return true;
1005            }
1006            return false;
1007        }
1008        let now = self.background.now();
1009        self.open = true;
1010        self.started = Some(now);
1011        self.results.generation = self.results.generation.wrapping_add(1);
1012        self.results.query = query;
1013        self.timings = QueryTimings {
1014            local: None,
1015            remote_first: vec![None; self.remotes.len()],
1016            remote_done: vec![None; self.remotes.len()],
1017        };
1018        // Prose indexed since the last poll answers this query already.
1019        while let Ok(prose) = self.prose_built.try_recv() {
1020            self.local.land_prose(prose);
1021        }
1022        let measure = ui_core::time::now();
1023        self.answer_locally();
1024        self.timings.local = Some(ui_core::time::now().saturating_duration_since(measure));
1025        let mut sections = Vec::with_capacity(self.remotes.len());
1026        for at in 0..self.remotes.len() {
1027            sections.push(self.start_remote(at, now));
1028        }
1029        self.results.remote = sections;
1030        self.arm(now);
1031        true
1032    }
1033
1034    /// Ask remote source `source` again for the open query, after it
1035    /// could not answer: its section waits again, its placeholder timed
1036    /// from now. Whether it was asked.
1037    pub fn retry(&mut self, source: &str) -> bool {
1038        let Some(at) = self
1039            .results
1040            .remote
1041            .iter()
1042            .position(|section| section.source == source && section.failure().is_some())
1043        else {
1044            return false;
1045        };
1046        let now = self.background.now();
1047        let section = self.start_remote(at, now);
1048        if let Some(shown) = self.results.remote.get_mut(at) {
1049            *shown = section;
1050        }
1051        self.arm(now);
1052        true
1053    }
1054
1055    /// The open query's filter chips, each value filter's values counted
1056    /// among what the query finds without that filter's own choice: the
1057    /// library searched again without it, and the remote rows shown.
1058    #[must_use]
1059    pub fn facet_offers(&self) -> Vec<super::chips::FacetOffer> {
1060        let query = &self.results.query;
1061        let remote: Vec<Item> = self
1062            .results
1063            .remote
1064            .iter()
1065            .flat_map(RemoteSection::rows)
1066            .cloned()
1067            .collect();
1068        super::chips::offers_counting(&self.kinds, query, &|key| {
1069            let mut wider = query.clone();
1070            wider.chips.retain(|chip| chip.key != key);
1071            let mut rows = if query.chips.iter().any(|chip| chip.key == key) {
1072                self.found_locally(&wider)
1073            } else {
1074                super::chips::result_rows(&self.results)
1075                    .into_iter()
1076                    .filter(|item| !remote.iter().any(|shown| shown.key() == item.key()))
1077                    .collect()
1078            };
1079            rows.extend(remote.iter().cloned());
1080            rows
1081        })
1082    }
1083
1084    /// Stop: every request is cancelled and nothing is shown.
1085    pub fn close(&mut self) {
1086        for slot in &mut self.remotes {
1087            if let Some(cancel) = slot.cancel.take() {
1088                cancel.cancel();
1089            }
1090            slot.due = None;
1091            slot.provisional.clear();
1092            slot.answered.clear();
1093        }
1094        self.open = false;
1095        self.held = false;
1096        self.results.generation = self.results.generation.wrapping_add(1);
1097        self.results.query = QuerySpec::default();
1098        self.results.local.clear();
1099        self.results.remote.clear();
1100    }
1101
1102    #[must_use]
1103    pub fn is_open(&self) -> bool {
1104        self.open
1105    }
1106
1107    /// What the query has found so far.
1108    #[must_use]
1109    pub fn results(&self) -> &QueryResults {
1110        &self.results
1111    }
1112
1113    /// How long the open query took to show what it has.
1114    #[must_use]
1115    pub fn timings(&self) -> &QueryTimings {
1116        &self.timings
1117    }
1118
1119    /// Whether the person points at, or has the keyboard in, a remote
1120    /// section. While they do, arriving rows only go on at the end of what is
1121    /// shown; when they stop, a finished answer is laid out in its order.
1122    pub fn hold(&mut self, held: bool) {
1123        if self.held == held {
1124            return;
1125        }
1126        self.held = held;
1127        if !held {
1128            for at in 0..self.remotes.len() {
1129                self.relayout(at);
1130            }
1131        }
1132    }
1133
1134    /// Send what is due and take in what arrived. Returns whether the
1135    /// results changed. Call every frame while a query is open; the engine
1136    /// wakes the window when something arrives or falls due.
1137    pub fn poll(&mut self) -> bool {
1138        let now = self.background.now();
1139        let mut changed = false;
1140        for at in 0..self.remotes.len() {
1141            if self
1142                .remotes
1143                .get(at)
1144                .and_then(|slot| slot.due)
1145                .is_some_and(|due| due <= now)
1146            {
1147                self.dispatch(at, now);
1148            }
1149        }
1150        while let Ok(answer) = self.answers.try_recv() {
1151            changed |= self.take(answer, now);
1152        }
1153        while let Ok(prose) = self.prose_built.try_recv() {
1154            changed |= self.land_prose(prose);
1155        }
1156        self.arm(now);
1157        changed
1158    }
1159
1160    fn answer_locally(&mut self) {
1161        let found = self.found_locally(&self.results.query);
1162        self.results.local = grouped(&self.kinds, found, GROUP_LIMIT);
1163    }
1164
1165    /// The section `at` starts with for the query just set, and its request
1166    /// scheduled.
1167    fn start_remote(&mut self, at: usize, now: Instant) -> RemoteSection {
1168        let query = self.results.query.clone();
1169        let local: HashSet<(&'static str, String)> = self
1170            .results
1171            .local
1172            .iter()
1173            .flat_map(|group| &group.items)
1174            .map(|item| (item.kind, item.id.clone()))
1175            .collect();
1176        let kinds = self.kinds.clone();
1177        let Some(slot) = self.remotes.get_mut(at) else {
1178            return RemoteSection::unasked(String::new(), String::new());
1179        };
1180        if let Some(cancel) = slot.cancel.take() {
1181            cancel.cancel();
1182        }
1183        slot.provisional.clear();
1184        slot.answered.clear();
1185        slot.due = None;
1186        slot.sent_at = None;
1187        let mut section = RemoteSection::unasked(slot.source.id().to_owned(), slot.source.label());
1188        if !slot.source.accepts(&query) {
1189            return section;
1190        }
1191        let not_local = |item: &&Item| !local.contains(&(item.kind, item.id.clone()));
1192        if let Some(cached) = slot.cached(&query.key()).filter(|cached| cached.complete) {
1193            let items: Vec<Item> = cached.items.iter().filter(not_local).cloned().collect();
1194            section.groups = grouped(&kinds, items, GROUP_LIMIT);
1195            if now.saturating_duration_since(cached.at) < FRESH {
1196                section.state = Some(Arrival::Ready(()));
1197                if let Some(first) = self.timings.remote_first.get_mut(at) {
1198                    *first = Some(Duration::ZERO);
1199                }
1200                if let Some(done) = self.timings.remote_done.get_mut(at) {
1201                    *done = Some(Duration::ZERO);
1202                }
1203                return section;
1204            }
1205            slot.provisional = section
1206                .rows()
1207                .map(|item| (item.kind, item.id.clone()))
1208                .collect();
1209        } else if let Some(earlier) = slot.refined_from(&query) {
1210            let items: Vec<Item> = earlier
1211                .items
1212                .iter()
1213                .filter(not_local)
1214                .filter(|item| {
1215                    query.admits(item)
1216                        && query.text_admits(item, kinds.iter().find(|kind| kind.kind == item.kind))
1217                })
1218                .cloned()
1219                .collect();
1220            section.groups = grouped(&kinds, items, GROUP_LIMIT);
1221            slot.provisional = section
1222                .rows()
1223                .map(|item| (item.kind, item.id.clone()))
1224                .collect();
1225        }
1226        section.ask(slot.source.origin(), now);
1227        let ready = slot
1228            .last_sent
1229            .map_or(now, |sent| sent.checked_add(THROTTLE).unwrap_or(now));
1230        slot.due = Some(ready.max(now));
1231        section
1232    }
1233
1234    /// Send the open query to source `at`, cancelling what it was asked
1235    /// before.
1236    fn dispatch(&mut self, at: usize, now: Instant) {
1237        let generation = self.results.generation;
1238        let query = self.results.query.clone();
1239        let Some(slot) = self.remotes.get_mut(at) else {
1240            return;
1241        };
1242        slot.due = None;
1243        if let Some(cancel) = slot.cancel.take() {
1244            cancel.cancel();
1245        }
1246        let cancel = Cancel::default();
1247        slot.cancel = Some(cancel.clone());
1248        slot.last_sent = Some(now);
1249        slot.sent_at = Some(now);
1250        let source = Arc::clone(&slot.source);
1251        let send = self.send.clone();
1252        let wake = self.wake.clone();
1253        let spawned = self.background.spawn("view-query-remote", move || {
1254            let tell = |event: AnswerEvent| {
1255                let _ = send.send(Answer {
1256                    source: at,
1257                    generation,
1258                    query: query.clone(),
1259                    event,
1260                });
1261                wake.wake();
1262            };
1263            let result = source.search(&query, &cancel, &mut |items| {
1264                tell(AnswerEvent::Page(items));
1265            });
1266            tell(match result {
1267                Ok(()) => AnswerEvent::Done,
1268                Err(error) => AnswerEvent::Failed(error.0),
1269            });
1270        });
1271        if spawned.is_err()
1272            && let Some(section) = self.results.remote.get_mut(at)
1273        {
1274            section.settle(Err(RemoteError(String::new())), now);
1275        }
1276    }
1277
1278    /// Take in one answer. Returns whether the results changed.
1279    fn take(&mut self, answer: Answer, now: Instant) -> bool {
1280        let current = self.open && answer.generation == self.results.generation;
1281        let Some(slot) = self.remotes.get_mut(answer.source) else {
1282            return false;
1283        };
1284        // Every answer is kept, a cancelled query's too: typing back to it
1285        // shows it at once.
1286        match &answer.event {
1287            AnswerEvent::Page(items) => slot.keep(&answer.query, items, false, now),
1288            AnswerEvent::Done => slot.keep(&answer.query, &[], true, now),
1289            AnswerEvent::Failed(_) => {}
1290        }
1291        if !current {
1292            return false;
1293        }
1294        let since = self
1295            .started
1296            .map(|started| now.saturating_duration_since(started));
1297        let at = answer.source;
1298        match answer.event {
1299            AnswerEvent::Page(items) => {
1300                if let Some(first) = self.timings.remote_first.get_mut(at)
1301                    && first.is_none()
1302                {
1303                    *first = since;
1304                }
1305                slot.answered.extend(items.iter().cloned());
1306                self.merge(at, items);
1307            }
1308            AnswerEvent::Done => {
1309                slot.cancel = None;
1310                if let Some(done) = self.timings.remote_done.get_mut(at) {
1311                    *done = since;
1312                }
1313                if let Some(section) = self.results.remote.get_mut(at) {
1314                    section.settle(Ok(()), now);
1315                }
1316                self.relayout(at);
1317            }
1318            AnswerEvent::Failed(message) => {
1319                slot.cancel = None;
1320                if let Some(section) = self.results.remote.get_mut(at) {
1321                    section.settle(Err(RemoteError(message)), now);
1322                }
1323            }
1324        }
1325        true
1326    }
1327
1328    /// The local rows, which no remote row repeats.
1329    fn local_keys(&self) -> HashSet<(&'static str, String)> {
1330        self.results
1331            .local
1332            .iter()
1333            .flat_map(|group| &group.items)
1334            .map(|item| (item.kind, item.id.clone()))
1335            .collect()
1336    }
1337
1338    /// Put arriving `items` into section `at`: after what is shown while it
1339    /// is held, else in their order.
1340    fn merge(&mut self, at: usize, items: Vec<Item>) {
1341        let local = self.local_keys();
1342        let held = self.held;
1343        let kinds = self.kinds.clone();
1344        let Some(section) = self.results.remote.get_mut(at) else {
1345            return;
1346        };
1347        let fresh: Vec<Item> = items
1348            .into_iter()
1349            .filter(|item| !local.contains(&(item.kind, item.id.clone())) && !section.holds(item))
1350            .collect();
1351        if fresh.is_empty() {
1352            return;
1353        }
1354        if held {
1355            append(section, fresh, &kinds);
1356        } else {
1357            let mut all: Vec<Item> = section.rows().cloned().collect();
1358            all.extend(fresh);
1359            section.groups = grouped(&kinds, all, GROUP_LIMIT);
1360        }
1361    }
1362
1363    /// Lay section `at` out in the order of its own answer, once that is in
1364    /// and nobody holds it: the earlier answer's rows it did not repeat
1365    /// leave.
1366    fn relayout(&mut self, at: usize) {
1367        if self.held {
1368            return;
1369        }
1370        let local = self.local_keys();
1371        let kinds = self.kinds.clone();
1372        let (Some(slot), Some(section)) = (self.remotes.get(at), self.results.remote.get_mut(at))
1373        else {
1374            return;
1375        };
1376        if !section.is_answered() {
1377            return;
1378        }
1379        let mut seen = HashSet::new();
1380        let items: Vec<Item> = slot
1381            .answered
1382            .iter()
1383            .filter(|item| !local.contains(&(item.kind, item.id.clone())))
1384            .filter(|item| seen.insert((item.kind, item.id.clone())))
1385            .cloned()
1386            .collect();
1387        let laid_out = grouped(&kinds, items, GROUP_LIMIT);
1388        if laid_out != section.groups {
1389            section.groups = laid_out;
1390        }
1391    }
1392
1393    /// Wake the window when the next request falls due.
1394    fn arm(&self, now: Instant) {
1395        if let Some(due) = self.remotes.iter().filter_map(|slot| slot.due).min() {
1396            if due <= now {
1397                self.wake.wake();
1398            } else {
1399                self.background.wake_at(due, self.wake.clone());
1400            }
1401        }
1402    }
1403}
1404
1405/// Put `items` after everything `section` shows: into its last group when
1406/// they are of its type, else in groups of their own after it.
1407fn append(section: &mut RemoteSection, items: Vec<Item>, kinds: &[KindDecl]) {
1408    for group in grouped(kinds, items, usize::MAX) {
1409        match section.groups.last_mut() {
1410            Some(last) if last.kind == group.kind => last.items.extend(group.items),
1411            _ => section.groups.push(group),
1412        }
1413    }
1414}