Skip to main content

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}