nucleo/lib.rs
1/*!
2`nucleo` is a high level crate that provides a high level matcher API that
3provides a highly effective (parallel) matcher worker. It's designed to allow
4quickly plugging a fully featured (and faster) fzf/skim like fuzzy matcher into
5your TUI application.
6
7It's designed to run matching on a background threadpool while providing a
8snapshot of the last complete match. That means the matcher can update the
9results live while the user is typing while never blocking the main UI thread
10(beyond a user provided timeout). Nucleo also supports fully concurrent lock-free
11(and wait-free) streaming of input items.
12
13The [`Nucleo`] struct serves as the main API entrypoint for this crate.
14
15# Status
16
17Nucleo is used in the helix-editor and therefore has a large user base with lots
18or real world testing. The core matcher implementation is considered complete
19and is unlikely to see major changes. The `nucleo-matcher` crate is finished and
20ready for widespread use, breaking changes should be very rare (a 1.0 release
21should not be far away).
22
23While the high level `nucleo` crate also works well (and is also used in helix),
24there are still additional features that will be added in the future. The high
25level crate also need better documentation and will likely see a few minor API
26changes in the future.
27
28*/
29use std::ops::{Bound, RangeBounds};
30use std::sync::atomic::{self, AtomicBool, Ordering};
31use std::sync::Arc;
32use std::time::Duration;
33
34use parking_lot::Mutex;
35use rayon::ThreadPool;
36
37use crate::pattern::MultiPattern;
38use crate::worker::Worker;
39pub use nucleo_matcher::{chars, Config, Matcher, Utf32Str, Utf32String};
40
41mod boxcar;
42mod par_sort;
43pub mod pattern;
44mod worker;
45
46#[cfg(test)]
47mod tests;
48
49/// A match candidate stored in a [`Nucleo`] worker.
50pub struct Item<'a, T> {
51 pub data: &'a T,
52 pub matcher_columns: &'a [Utf32String],
53}
54
55/// A handle that allows adding new items to a [`Nucleo`] worker.
56///
57/// It's internally reference counted and can be cheaply cloned
58/// and sent across threads.
59pub struct Injector<T> {
60 items: Arc<boxcar::Vec<T>>,
61 notify: Arc<(dyn Fn() + Sync + Send)>,
62}
63
64impl<T> Clone for Injector<T> {
65 fn clone(&self) -> Self {
66 Injector {
67 items: self.items.clone(),
68 notify: self.notify.clone(),
69 }
70 }
71}
72
73impl<T> Injector<T> {
74 /// Appends an element to the list of matched items.
75 /// This function is lock-free and wait-free.
76 pub fn push(&self, value: T, fill_columns: impl FnOnce(&T, &mut [Utf32String])) -> u32 {
77 let idx = self.items.push(value, fill_columns);
78 (self.notify)();
79 idx
80 }
81
82 /// Appends multiple elements to the list of matched items.
83 /// This function is lock-free and wait-free.
84 ///
85 /// You should favor this function over `push` if at least one of the following is true:
86 /// - the number of items you're adding can be computed beforehand and is typically larger
87 /// than 1k
88 /// - you're able to batch incoming items
89 /// - you're adding items from multiple threads concurrently (this function results in less
90 /// contention)
91 pub fn extend<I>(&self, values: I, fill_columns: impl Fn(&T, &mut [Utf32String]))
92 where
93 I: IntoIterator<Item = T> + ExactSizeIterator,
94 {
95 self.items.extend(values, fill_columns);
96 (self.notify)();
97 }
98
99 /// Returns the total number of items injected in the matcher. This might
100 /// not match the number of items in the match snapshot (if the matcher
101 /// is still running)
102 pub fn injected_items(&self) -> u32 {
103 self.items.count()
104 }
105
106 /// Returns a reference to the item at the given index.
107 ///
108 /// # Safety
109 ///
110 /// Item at `index` must be initialized. That means you must have observed
111 /// `push` returning this value or `get` returning `Some` for this value.
112 /// Just because a later index is initialized doesn't mean that this index
113 /// is initialized
114 pub unsafe fn get_unchecked(&self, index: u32) -> Item<'_, T> {
115 self.items.get_unchecked(index)
116 }
117
118 /// Returns a reference to the element at the given index.
119 pub fn get(&self, index: u32) -> Option<Item<'_, T>> {
120 self.items.get(index)
121 }
122}
123
124/// An [item](crate::Item) that was successfully matched by a [`Nucleo`] worker.
125#[derive(PartialEq, Eq, Debug, Clone, Copy)]
126pub struct Match {
127 pub score: u32,
128 pub idx: u32,
129}
130
131/// That status of a [`Nucleo`] worker after a match.
132#[derive(PartialEq, Eq, Debug, Clone, Copy)]
133pub struct Status {
134 /// Whether the current snapshot has changed.
135 pub changed: bool,
136 /// Whether the matcher is still processing in the background.
137 pub running: bool,
138}
139
140/// A snapshot represent the results of a [`Nucleo`] worker after
141/// finishing a [`tick`](Nucleo::tick).
142pub struct Snapshot<T: Sync + Send + 'static> {
143 item_count: u32,
144 matches: Vec<Match>,
145 pattern: MultiPattern,
146 items: Arc<boxcar::Vec<T>>,
147}
148
149impl<T: Sync + Send + 'static> Snapshot<T> {
150 fn clear(&mut self, new_items: Arc<boxcar::Vec<T>>) {
151 self.item_count = 0;
152 self.matches.clear();
153 self.items = new_items
154 }
155
156 fn update(&mut self, worker: &Worker<T>) {
157 self.item_count = worker.item_count();
158 self.pattern.clone_from(&worker.pattern);
159 self.matches.clone_from(&worker.matches);
160 if !Arc::ptr_eq(&worker.items, &self.items) {
161 self.items = worker.items.clone()
162 }
163 }
164
165 /// Returns that total number of items
166 pub fn item_count(&self) -> u32 {
167 self.item_count
168 }
169
170 /// Returns the pattern which items were matched against
171 pub fn pattern(&self) -> &MultiPattern {
172 &self.pattern
173 }
174
175 /// Returns that number of items that matched the pattern
176 pub fn matched_item_count(&self) -> u32 {
177 self.matches.len() as u32
178 }
179
180 /// Returns an iterator over the items that correspond to a subrange of
181 /// all the matches in this snapshot.
182 ///
183 /// # Panics
184 /// Panics if `range` has a range bound that is larger than
185 /// the matched item count
186 pub fn matched_items(
187 &self,
188 range: impl RangeBounds<u32>,
189 ) -> impl ExactSizeIterator<Item = Item<'_, T>> + DoubleEndedIterator + '_ {
190 // TODO: use TAIT
191 let start = match range.start_bound() {
192 Bound::Included(&start) => start as usize,
193 Bound::Excluded(&start) => start as usize + 1,
194 Bound::Unbounded => 0,
195 };
196 let end = match range.end_bound() {
197 Bound::Included(&end) => end as usize + 1,
198 Bound::Excluded(&end) => end as usize,
199 Bound::Unbounded => self.matches.len(),
200 };
201 self.matches[start..end]
202 .iter()
203 .map(|&m| unsafe { self.items.get_unchecked(m.idx) })
204 }
205
206 /// Returns a reference to the item at the given index.
207 ///
208 /// # Safety
209 ///
210 /// Item at `index` must be initialized. That means you must have observed a
211 /// match with the corresponding index in this exact snapshot. Observing
212 /// a higher index is not enough as item indices can be non-contigously
213 /// initialized
214 #[inline]
215 pub unsafe fn get_item_unchecked(&self, index: u32) -> Item<'_, T> {
216 self.items.get_unchecked(index)
217 }
218
219 /// Returns a reference to the item at the given index.
220 ///
221 /// Returns `None` if the given `index` is not initialized. This function
222 /// is only guarteed to return `Some` for item indices that can be found in
223 /// the `matches` of this struct. Both smaller and larger indices may return
224 /// `None`.
225 #[inline]
226 pub fn get_item(&self, index: u32) -> Option<Item<'_, T>> {
227 self.items.get(index)
228 }
229
230 /// Return the matches corresponding to this snapshot.
231 #[inline]
232 pub fn matches(&self) -> &[Match] {
233 &self.matches
234 }
235
236 /// A convenience function to return the [`Item`] corresponding to the
237 /// `n`th match.
238 ///
239 /// Returns `None` if `n` is greater than or equal to the match count.
240 #[inline]
241 pub fn get_matched_item(&self, n: u32) -> Option<Item<'_, T>> {
242 // SAFETY: A match index is guaranteed to corresponding to a valid global index in this
243 // snapshot.
244 unsafe { Some(self.get_item_unchecked(self.matches.get(n as usize)?.idx)) }
245 }
246}
247
248#[repr(u8)]
249#[derive(Clone, Copy, PartialEq, Eq)]
250enum State {
251 Init,
252 /// items have been cleared but snapshot and items are still outdated
253 Cleared,
254 /// items are fresh
255 Fresh,
256}
257
258impl State {
259 fn matcher_item_refs(self) -> usize {
260 match self {
261 State::Cleared => 1,
262 State::Init | State::Fresh => 2,
263 }
264 }
265
266 fn canceled(self) -> bool {
267 self != State::Fresh
268 }
269
270 fn cleared(self) -> bool {
271 self != State::Fresh
272 }
273}
274
275/// A high level matcher worker that quickly computes matches in a background
276/// threadpool.
277pub struct Nucleo<T: Sync + Send + 'static> {
278 // the way the API is build we totally don't actually need these to be Arcs
279 // but this lets us avoid some unsafe
280 canceled: Arc<AtomicBool>,
281 should_notify: Arc<AtomicBool>,
282 worker: Arc<Mutex<Worker<T>>>,
283 pool: ThreadPool,
284 state: State,
285 items: Arc<boxcar::Vec<T>>,
286 notify: Arc<(dyn Fn() + Sync + Send)>,
287 snapshot: Snapshot<T>,
288 /// The pattern matched by this matcher. To update the match pattern
289 /// [`MultiPattern::reparse`](`pattern::MultiPattern::reparse`) should be used.
290 /// Note that the matcher worker will only become aware of the new pattern
291 /// after a call to [`tick`](Nucleo::tick).
292 pub pattern: MultiPattern,
293}
294
295impl<T: Sync + Send + 'static> Nucleo<T> {
296 /// Constructs a new `nucleo` worker threadpool with the provided `config`.
297 ///
298 /// `notify` is called everytime new information is available and
299 /// [`tick`](Nucleo::tick) should be called. Note that `notify` is not
300 /// debounced, that should be handled by the downstream crate (for example
301 /// debouncing to only redraw at most every 1/60 seconds).
302 ///
303 /// If `None` is passed for the number of worker threads, nucleo will use
304 /// one thread per hardware thread.
305 ///
306 /// Nucleo can match items with multiple orthogonal properties. `columns`
307 /// indicates how many matching columns each item (and the pattern) has. The
308 /// number of columns cannot be changed after construction.
309 pub fn new(
310 config: Config,
311 notify: Arc<(dyn Fn() + Sync + Send)>,
312 num_threads: Option<usize>,
313 columns: u32,
314 ) -> Self {
315 let (pool, worker) = Worker::new(num_threads, config, notify.clone(), columns);
316 Self {
317 canceled: worker.canceled.clone(),
318 should_notify: worker.should_notify.clone(),
319 items: worker.items.clone(),
320 pool,
321 pattern: MultiPattern::new(columns as usize),
322 snapshot: Snapshot {
323 matches: Vec::with_capacity(2 * 1024),
324 pattern: MultiPattern::new(columns as usize),
325 item_count: 0,
326 items: worker.items.clone(),
327 },
328 worker: Arc::new(Mutex::new(worker)),
329 state: State::Init,
330 notify,
331 }
332 }
333
334 /// Returns the total number of active injectors
335 pub fn active_injectors(&self) -> usize {
336 Arc::strong_count(&self.items)
337 - self.state.matcher_item_refs()
338 - (Arc::ptr_eq(&self.snapshot.items, &self.items)) as usize
339 }
340
341 /// Returns a snapshot of the current matcher state.
342 pub fn snapshot(&self) -> &Snapshot<T> {
343 &self.snapshot
344 }
345
346 /// Returns an injector that can be used for adding candidates to the matcher.
347 pub fn injector(&self) -> Injector<T> {
348 Injector {
349 items: self.items.clone(),
350 notify: self.notify.clone(),
351 }
352 }
353
354 /// Restart the the item stream. Removes all items and disconnects all
355 /// previously created injectors from this instance. If `clear_snapshot`
356 /// is `true` then all items and matched are removed from the [`Snapshot`]
357 /// immediately. Otherwise the snapshot will keep the current matches until
358 /// the matcher has run again.
359 ///
360 /// # Note
361 ///
362 /// The injectors will continue to function but they will not affect this
363 /// instance anymore. The old items will only be dropped when all injectors
364 /// were dropped.
365 pub fn restart(&mut self, clear_snapshot: bool) {
366 self.canceled.store(true, Ordering::Relaxed);
367 self.items = Arc::new(boxcar::Vec::with_capacity(1024, self.items.columns()));
368 self.state = State::Cleared;
369 if clear_snapshot {
370 self.snapshot.clear(self.items.clone());
371 }
372 }
373
374 /// Update the internal configuration.
375 pub fn update_config(&mut self, config: Config) {
376 self.worker.lock().update_config(config)
377 }
378
379 // Set whether the matcher should sort search results by score after
380 // matching. Defaults to true.
381 pub fn sort_results(&mut self, sort_results: bool) {
382 self.worker.lock().sort_results(sort_results)
383 }
384
385 // Set whether the matcher should reverse the order of the input.
386 // Defaults to false.
387 pub fn reverse_items(&mut self, reverse_items: bool) {
388 self.worker.lock().reverse_items(reverse_items)
389 }
390
391 /// The main way to interact with the matcher, this should be called
392 /// regularly (for example each time a frame is rendered). To avoid
393 /// excessive redraws this method will wait `timeout` milliseconds for the
394 /// worker thread to finish. It is recommend to set the timeout to 10ms.
395 pub fn tick(&mut self, timeout: u64) -> Status {
396 self.should_notify.store(false, atomic::Ordering::Relaxed);
397 let status = self.pattern.status();
398 let canceled = status != pattern::Status::Unchanged || self.state.canceled();
399 let mut res = self.tick_inner(timeout, canceled, status);
400 if !canceled {
401 return res;
402 }
403 self.state = State::Fresh;
404 let status2 = self.tick_inner(timeout, false, pattern::Status::Unchanged);
405 res.changed |= status2.changed;
406 res.running = status2.running;
407 res
408 }
409
410 fn tick_inner(&mut self, timeout: u64, canceled: bool, status: pattern::Status) -> Status {
411 let mut inner = if canceled {
412 self.pattern.reset_status();
413 self.canceled.store(true, atomic::Ordering::Relaxed);
414 self.worker.lock_arc()
415 } else {
416 let Some(worker) = self.worker.try_lock_arc_for(Duration::from_millis(timeout)) else {
417 self.should_notify.store(true, Ordering::Release);
418 return Status {
419 changed: false,
420 running: true,
421 };
422 };
423 worker
424 };
425
426 let changed = inner.running;
427
428 let running = canceled || self.items.count() > inner.item_count();
429 if inner.running {
430 inner.running = false;
431 if !inner.was_canceled && !self.state.canceled() {
432 self.snapshot.update(&inner)
433 }
434 }
435 if running {
436 inner.pattern.clone_from(&self.pattern);
437 self.canceled.store(false, atomic::Ordering::Relaxed);
438 if !canceled {
439 self.should_notify.store(true, atomic::Ordering::Release);
440 }
441 let cleared = self.state.cleared();
442 if cleared {
443 inner.items = self.items.clone();
444 }
445 self.pool
446 .spawn(move || unsafe { inner.run(status, cleared) })
447 }
448 Status { changed, running }
449 }
450}
451
452impl<T: Sync + Send> Drop for Nucleo<T> {
453 fn drop(&mut self) {
454 // we ensure the worker quits before dropping items to ensure that
455 // the worker can always assume the items outlive it
456 self.canceled.store(true, atomic::Ordering::Relaxed);
457 let lock = self.worker.try_lock_for(Duration::from_secs(1));
458 if lock.is_none() {
459 unreachable!("thread pool failed to shutdown properly")
460 }
461 }
462}