1use 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
39pub const THROTTLE: Duration = Duration::from_millis(40);
42
43pub const FRESH: Duration = Duration::from_secs(300);
45
46const CACHED_ANSWERS: usize = 48;
48
49const WARM_FOR: Duration = Duration::from_secs(60);
51
52const LOCAL_CANDIDATES: usize = 600;
54
55pub const GROUP_LIMIT: usize = 50;
57
58#[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#[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
84pub trait RemoteSource: Send + Sync + 'static {
88 fn id(&self) -> &str;
90
91 fn label(&self) -> String;
93
94 fn origin(&self) -> Source {
98 Source::host(self.label())
99 }
100
101 fn accepts(&self, query: &QuerySpec) -> bool {
104 !query.text.trim().is_empty()
105 }
106
107 fn warm(&self) {}
110
111 fn search(
115 &self,
116 query: &QuerySpec,
117 cancel: &Cancel,
118 page: &mut dyn FnMut(Vec<Item>),
119 ) -> Result<(), RemoteError>;
120}
121
122pub trait LocalSource: Send + Sync + 'static {
127 fn search(&self, query: &QuerySpec, limit: usize) -> Vec<Item>;
129}
130
131#[derive(Clone, Debug, PartialEq)]
133pub struct ResultGroup {
134 pub kind: &'static str,
135 pub label: String,
137 pub form: Form,
138 pub items: Vec<Item>,
139 pub more: usize,
141}
142
143#[derive(Clone, Debug, PartialEq)]
145pub struct RemoteSection {
146 pub source: String,
147 pub label: String,
148 pub state: Option<Arrival<(), RemoteError>>,
152 pub clock: Option<ArrivalClock>,
155 pub groups: Vec<ResultGroup>,
157}
158
159impl RemoteSection {
160 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 #[must_use]
173 pub fn is_waiting(&self) -> bool {
174 self.state.as_ref().is_some_and(Arrival::is_pending)
175 }
176
177 #[must_use]
179 pub fn is_answered(&self) -> bool {
180 matches!(self.state, Some(Arrival::Ready(())))
181 }
182
183 #[must_use]
185 pub fn failure(&self) -> Option<&RemoteError> {
186 self.state.as_ref().and_then(Arrival::failure)
187 }
188
189 #[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 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 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 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#[derive(Clone, Debug, Default, PartialEq)]
227pub struct QueryResults {
228 pub generation: u64,
230 pub query: QuerySpec,
231 pub local: Vec<ResultGroup>,
233 pub remote: Vec<RemoteSection>,
235}
236
237impl QueryResults {
238 #[must_use]
241 pub fn pending(&self) -> bool {
242 self.remote.iter().any(RemoteSection::is_waiting)
243 }
244
245 #[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 #[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 #[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 #[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#[derive(Clone, Debug, Default, PartialEq, Eq)]
290pub struct QueryTimings {
291 pub local: Option<Duration>,
293 pub remote_first: Vec<Option<Duration>>,
295 pub remote_done: Vec<Option<Duration>>,
297}
298
299struct 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
313struct CachedAnswer {
315 key: String,
316 query: QuerySpec,
317 items: Vec<Item>,
318 complete: bool,
319 at: Instant,
320}
321
322struct RemoteSlot {
324 source: Arc<dyn RemoteSource>,
325 last_sent: Option<Instant>,
326 due: Option<Instant>,
328 cancel: Option<Cancel>,
329 warmed: Option<Instant>,
330 cache: VecDeque<CachedAnswer>,
331 provisional: HashSet<(&'static str, String)>,
334 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 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
386struct 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 prose_wanted: HashMap<u64, usize>,
401}
402
403struct ProseIndex {
406 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
426fn 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 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 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 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 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 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 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
598fn 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
628fn 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
642const EXCERPT_REACH: usize = 96;
645
646const EXCERPT_LENGTH: usize = 180;
648
649#[must_use]
653pub fn excerpt(passage: &str, query: &str) -> String {
654 let chars: Vec<(usize, char)> = passage.char_indices().collect();
655 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 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 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
708fn 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
723fn 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
753pub 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 prose_builds: u64,
770 prose_send: Sender<ProseIndex>,
771 prose_built: Receiver<ProseIndex>,
772 source: Option<Arc<dyn LocalSource>>,
774}
775
776impl QueryEngine {
777 #[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 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 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 #[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 #[must_use]
855 pub fn with_background(mut self, background: Background) -> Self {
856 self.background = background;
857 self
858 }
859
860 #[must_use]
862 pub fn kinds(&self) -> &[KindDecl] {
863 &self.kinds
864 }
865
866 pub fn set_waker(&mut self, wake: WakeHandle) {
869 self.wake = wake;
870 }
871
872 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 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 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 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 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 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 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 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 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 #[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 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 #[must_use]
1109 pub fn results(&self) -> &QueryResults {
1110 &self.results
1111 }
1112
1113 #[must_use]
1115 pub fn timings(&self) -> &QueryTimings {
1116 &self.timings
1117 }
1118
1119 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 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 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 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 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 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 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 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 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 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
1405fn 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}