gravel_core/
engine.rs

1use crate::{CoreMessage, performance::Stopwatch, scoring, timed};
2use abi_stable::std_types::RString;
3use abi_stable::{external_types::crossbeam_channel::RSender, sabi_trait, std_types::RStr, traits::IntoReprRust};
4use gravel_ffi::{ActionKind, ArcDynHit, FrontendMessage, HitActionContext, ProviderResult, RefDynHitActionContext};
5use gravel_ffi::{BoxDynProvider, QueryResult};
6use itertools::Itertools;
7use std::iter::once;
8
9/// Holds a [`BoxDynProvider`] and some additional metadata.
10struct ProviderInfo {
11	pub name: String,
12	pub provider: BoxDynProvider,
13	pub keyword: Option<String>,
14}
15
16/// Aggregates and scores hits from the given [`BoxDynProvider`]s.
17pub struct QueryEngine {
18	providers: Vec<ProviderInfo>,
19	action_context: ActionContext,
20}
21
22impl QueryEngine {
23	pub fn new(sender: RSender<CoreMessage>) -> Self {
24		Self {
25			providers: vec![],
26			action_context: ActionContext::new(sender),
27		}
28	}
29
30	/// Adds the provider to the engine's collection.
31	pub fn register(&mut self, name: String, provider: BoxDynProvider, keyword: Option<String>) -> &mut Self {
32		let info = ProviderInfo {
33			name,
34			provider,
35			keyword,
36		};
37
38		self.providers.push(info);
39		self
40	}
41
42	pub fn query(&self, query: RStr<'_>) -> QueryResult {
43		fn inner(engine: &QueryEngine, query: &str) -> QueryResult {
44			if let Some(result) = engine.try_keyword_query(query) {
45				return result;
46			}
47
48			engine.full_query(query)
49		}
50
51		let query = query.into_rust();
52
53		if query.trim().is_empty() {
54			return QueryResult::default();
55		}
56
57		inner(self, query)
58	}
59
60	pub fn run_hit_action(&self, hit: &ArcDynHit, kind: ActionKind) {
61		let context = (&self.action_context).into();
62
63		match kind {
64			ActionKind::Primary => hit.action(context),
65			ActionKind::Secondary => hit.secondary_action(context),
66		};
67	}
68
69	pub fn clear_caches(&self) {
70		for provider in &self.providers {
71			provider.provider.clear_caches();
72		}
73	}
74
75	/// Runs the query against all available providers.
76	fn full_query(&self, query: &str) -> QueryResult {
77		let providers = self.providers.iter().filter(|provider| provider.keyword.is_none());
78
79		query_all(providers, query)
80	}
81
82	/// Tries to find a provider with the a keyword that matches the query's.
83	/// If one is found, the keyword is stripped from the query and the
84	/// resulting new query is ran against that provider only.
85	fn try_keyword_query(&self, query: &str) -> Option<QueryResult> {
86		let first_word = query.split(' ').next()?;
87
88		let provider = self.check_keywords(first_word)?;
89
90		// remove the keyword from the query
91		let new_query = &query[first_word.len()..query.len()].trim_start();
92
93		Some(query_all(once(provider), new_query))
94	}
95
96	/// Tries to find a provider with the a keyword that matches the given string.
97	fn check_keywords(&self, first_word: &str) -> Option<&ProviderInfo> {
98		self.providers
99			.iter()
100			.find(|p| matches!(&p.keyword, Some(k) if k == first_word))
101	}
102}
103
104/// Queries providers; aggregates, scores and orders [`ArcDynHit`]s.
105#[expect(single_use_lifetimes)]
106fn query_all<'a>(providers: impl Iterator<Item = &'a ProviderInfo>, query: &str) -> QueryResult {
107	let hits = providers.flat_map(|p| query_one(p, query).hits).collect_vec();
108
109	timed!("scoring took", {
110		let hits = match query.trim() {
111			"*" => scoring::to_unscored(hits),
112			_ => scoring::to_scored(hits, query),
113		};
114
115		QueryResult::new(hits)
116	})
117}
118
119fn query_one(info: &ProviderInfo, query: &str) -> ProviderResult {
120	let stopwatch = Stopwatch::start();
121
122	let result = info.provider.query(query.into());
123
124	log::trace!(
125		"query for provider '{}' took {stopwatch} and produced {} hits",
126		info.name,
127		result.hits.len()
128	);
129
130	result
131}
132
133struct ActionContext {
134	sender: RSender<CoreMessage>,
135}
136
137impl ActionContext {
138	pub fn new(sender: RSender<CoreMessage>) -> Self {
139		Self { sender }
140	}
141
142	fn send(&self, message: CoreMessage) {
143		self.sender
144			.send(message)
145			.inspect_err(|e| log::error!("unable to send core message: {e}"))
146			.ok();
147	}
148
149	fn send_frontend(&self, message: FrontendMessage) {
150		self.send(CoreMessage::Frontend(message));
151	}
152}
153
154impl<'a> From<&'a ActionContext> for RefDynHitActionContext<'a> {
155	fn from(value: &'a ActionContext) -> Self {
156		Self::from_ptr(value, sabi_trait::TD_Opaque)
157	}
158}
159
160impl HitActionContext for ActionContext {
161	fn hide_frontend(&self) {
162		self.send_frontend(FrontendMessage::Hide);
163	}
164
165	fn refresh_frontend(&self) {
166		self.send_frontend(FrontendMessage::Refresh);
167	}
168
169	fn exit(&self) {
170		self.send_frontend(FrontendMessage::Exit);
171	}
172
173	fn restart(&self) {
174		self.send_frontend(FrontendMessage::Restart);
175	}
176
177	fn set_query(&self, query: RString) {
178		self.send_frontend(FrontendMessage::ShowWithQuery(query));
179	}
180
181	fn set_clipboard_text(&self, content: RString) {
182		self.send(CoreMessage::SetClipboardText(content.into_rust()));
183	}
184
185	fn clear_caches(&self) {
186		self.send(CoreMessage::ClearCaches);
187	}
188}