Skip to main content

ncp_engine/
lib.rs

1//! # Nucleo-picker-engine
2//!
3//! The `ncp-engine` crate is a fork of the [nucleo](https://docs.rs/nucleo) crate.
4//! It is not recommended for general use. This fork mainly exists to meet the specific
5//! requirements of [`nucleo-picker`](https://docs.rs/nucleo-picker).
6//!
7//! `ncp-engine` implements a high level matcher API that provides a performant
8//! parallel matcher worker threadpool. It is designed to allow integrating a `fzf`-like
9//! fuzzy matcher into a TUI application.
10//!
11//! For a fully-featured TUI implementation, see [nucleo-picker](htpps://docs.rs/nucleo-picker).
12//!
13//! Matching runs in a background threadpool while providing a snapshot of the last
14//! complete match on request. That means the matcher can update the results live while
15//! the user is typing, while never blocking the main UI thread (beyond a user provided
16//! timeout). Nucleo also supports fully concurrent lock-free (and wait-free) streaming
17//! of input items.
18//!
19//! The [`Nucleo`] struct serves as the main API entrypoint for this crate.
20
21#![deny(missing_docs)]
22
23use std::ops::{Bound, RangeBounds};
24use std::sync::Arc;
25use std::sync::atomic::{self, AtomicBool, Ordering};
26use std::time::Duration;
27
28use parking_lot::Mutex;
29use rayon::ThreadPool;
30
31use crate::pattern::MultiPattern;
32pub use crate::worker::MatchListConfig;
33use crate::worker::Worker;
34pub use ncp_matcher::{Config, Matcher, Utf32Str, Utf32String, chars};
35
36mod boxcar;
37mod par_sort;
38pub mod pattern;
39mod worker;
40
41#[cfg(test)]
42mod tests;
43
44/// A match candidate stored in a [`Nucleo`] worker.
45pub struct Item<'a, T> {
46    /// A reference to the underlying item provided to the matcher.
47    pub data: &'a T,
48    /// The representation of the data within the matcher.
49    pub matcher_columns: &'a [Utf32String],
50}
51
52/// A detached handle to a match candidate.
53///
54/// Unlike an [`Item`], which has a lifetime tied to the [`Snapshot`] or [`Injector`] from which it was
55/// created, a [`DetachedItem`] owns a handle to the underlying data and will persist even if the original
56/// [`Nucleo`] matcher has been dropped.
57///
58/// Similarly to an [injector](Injector), holding this handle will prevent the underlying data
59/// from being dropped. This handle is internally reference counted and can be cloned cheaply.
60///
61/// This handle implements [`PartialEq`] regardless of the value `T`. Equality is tested by checking that
62/// both items originate from the same matcher and that the internal indices are the same.
63pub struct DetachedItem<T> {
64    items: Arc<boxcar::Vec<T>>,
65    // this index is guaranteed to be valid for self.items
66    idx: u32,
67}
68
69impl<T> DetachedItem<T> {
70    unsafe fn new(items: &Arc<boxcar::Vec<T>>, idx: u32) -> Self {
71        Self {
72            items: Arc::clone(items),
73            idx,
74        }
75    }
76
77    /// Get the corresponding item.
78    pub fn item(&self) -> Item<'_, T> {
79        unsafe { self.items.get_unchecked(self.idx) }
80    }
81
82    /// Get the raw underlying index.
83    ///
84    /// This index is guaranteed to be valid for the snapshot or injector from which this
85    /// [`DetachedItem`] was originally constructed.
86    pub fn idx(&self) -> u32 {
87        self.idx
88    }
89}
90
91impl<T> PartialEq for DetachedItem<T> {
92    fn eq(&self, other: &Self) -> bool {
93        self.idx == other.idx && Arc::ptr_eq(&self.items, &other.items)
94    }
95}
96
97impl<T> Clone for DetachedItem<T> {
98    fn clone(&self) -> Self {
99        Self {
100            items: Arc::clone(&self.items),
101            idx: self.idx,
102        }
103    }
104}
105
106/// A handle that allows adding new items to a [`Nucleo`] worker.
107///
108/// An `Injector` is internally reference counted and can be cheaply
109/// cloned and sent across threads.
110pub struct Injector<T> {
111    items: Arc<boxcar::Vec<T>>,
112    notify: Arc<dyn Fn() + Sync + Send>,
113}
114
115impl<T> Clone for Injector<T> {
116    fn clone(&self) -> Self {
117        Self {
118            items: self.items.clone(),
119            notify: self.notify.clone(),
120        }
121    }
122}
123
124impl<T> Injector<T> {
125    /// Appends an element to the list of match candidates.
126    ///
127    /// This function is lock-free and wait-free. The returned `u32` is the internal index which
128    /// has been assigned to the provided value and is guaranteed to be valid unless
129    /// [`Nucleo::restart`] has been called.
130    ///
131    /// The `fill_columns` closure is called to generate the representation of the pushed value
132    /// within the matcher engine. The first argument is a reference to the provided value, and the
133    /// second argument is a slice where each entry corresponds to a column within the [`Nucleo`]
134    /// instance from which this `Injector` was created.
135    ///
136    /// ## Example
137    /// If the matcher has exactly one column and the item type `T` is a `String`, an appropriate
138    /// `fill_columns` closure might look like
139    /// ```
140    /// # use ncp_engine::Utf32String;
141    /// let fill_columns = |s: &String, cols: &mut [Utf32String]| {
142    ///      cols[0] = (&**s).into();
143    /// };
144    /// ```
145    pub fn push(&self, value: T, fill_columns: impl FnOnce(&T, &mut [Utf32String])) -> u32 {
146        let idx = self.items.push(value, fill_columns);
147        (self.notify)();
148        idx
149    }
150
151    /// Appends multiple elements to the list of matched items. This function is lock-free
152    /// and wait-free.
153    ///
154    /// You should favor this function over `push` if at least one of the following is true:
155    /// - The number of items you're adding can be computed beforehand and is typically larger
156    ///   than 1k
157    /// - You're able to batch incoming items
158    /// - You're adding items from multiple threads concurrently (this function results in less
159    ///   contention)
160    pub fn extend<I>(&self, values: I, fill_columns: impl Fn(&T, &mut [Utf32String]))
161    where
162        I: IntoIterator<Item = T>,
163    {
164        self.items.extend(values, fill_columns);
165        (self.notify)();
166    }
167
168    /// Returns the total number of items injected in the matcher.
169    ///
170    /// This may not match the number of items in the match snapshot if the matcher
171    /// is still running.
172    pub fn injected_items(&self) -> u32 {
173        self.items.count()
174    }
175
176    /// Returns a reference to the item at the given index.
177    #[inline]
178    pub fn get_item(&self, index: u32) -> Option<Item<'_, T>> {
179        self.items.get(index)
180    }
181
182    /// Returns a reference to the item at the given index without checking that the index is valid.
183    ///
184    /// # Safety
185    ///
186    /// Item at `index` must be initialized. That means you must have observed
187    /// `push` returning this value or `get` returning `Some` for this value.
188    /// Just because a later index is initialized doesn't mean that this index
189    /// is initialized
190    #[inline]
191    pub unsafe fn get_item_unchecked(&self, index: u32) -> Item<'_, T> {
192        unsafe { self.items.get_unchecked(index) }
193    }
194
195    /// Returns the detached item at the given index.
196    #[inline]
197    pub fn get_detached_item(&self, index: u32) -> Option<DetachedItem<T>> {
198        self.items
199            .is_valid(index)
200            .then(|| unsafe { DetachedItem::new(&self.items, index) })
201    }
202
203    /// Returns the detached item at the given index without checking that the index is valid.
204    ///
205    /// # Safety
206    ///
207    /// Item at `index` must be initialized. That means you must have observed
208    /// `push` returning this value or `get` returning `Some` for this value.
209    /// Just because a later index is initialized doesn't mean that this index
210    /// is initialized
211    #[inline]
212    pub unsafe fn get_detached_item_unchecked(&self, index: u32) -> DetachedItem<T> {
213        unsafe { DetachedItem::new(&self.items, index) }
214    }
215}
216
217/// A successful match computed by the [`Nucleo`] match.
218#[derive(PartialEq, Eq, Debug, Clone, Copy)]
219pub struct Match {
220    /// The score of the match.
221    pub score: u32,
222    /// The index of the match.
223    ///
224    /// The index is guaranteed to correspond to a valid item within the matcher and within the
225    /// same snapshot. Note that indices are invalidated if the matcher engine has been
226    /// [restarted](Nucleo::restart).
227    pub idx: u32,
228}
229
230/// The status of a [`Nucleo`] worker after a match.
231#[derive(PartialEq, Eq, Debug, Clone, Copy)]
232pub struct Status {
233    /// Whether the current snapshot has changed.
234    pub changed: bool,
235    /// Whether the matcher is still processing in the background.
236    pub running: bool,
237}
238
239/// A representation of the results of a [`Nucleo`] worker after finishing a
240/// [`tick`](Nucleo::tick).
241pub struct Snapshot<T> {
242    item_count: u32,
243    matches: Vec<Match>,
244    pattern: MultiPattern,
245    items: Arc<boxcar::Vec<T>>,
246}
247
248impl<T: Sync + Send + 'static> Snapshot<T> {
249    fn clear(&mut self, new_items: Arc<boxcar::Vec<T>>) {
250        self.item_count = 0;
251        self.matches.clear();
252        self.items = new_items;
253    }
254
255    fn update(&mut self, worker: &Worker<T>) {
256        self.item_count = worker.item_count();
257        self.pattern.clone_from(&worker.pattern);
258        self.matches.clone_from(&worker.matches);
259        if !Arc::ptr_eq(&worker.items, &self.items) {
260            self.items = worker.items.clone();
261        }
262    }
263
264    /// Returns that total number of items
265    pub fn item_count(&self) -> u32 {
266        self.item_count
267    }
268
269    /// Returns the pattern which items were matched against
270    pub fn pattern(&self) -> &MultiPattern {
271        &self.pattern
272    }
273
274    /// Returns that number of items that matched the pattern
275    pub fn matched_item_count(&self) -> u32 {
276        self.matches.len() as u32
277    }
278
279    /// Returns an iterator over the items that correspond to a subrange of
280    /// all the matches in this snapshot.
281    ///
282    /// # Panics
283    /// Panics if `range` has a range bound that is larger than
284    /// the matched item count
285    pub fn matched_items(
286        &self,
287        range: impl RangeBounds<u32>,
288    ) -> impl ExactSizeIterator<Item = Item<'_, T>> + DoubleEndedIterator + '_ {
289        // TODO: use TAIT
290        let start = match range.start_bound() {
291            Bound::Included(&start) => start as usize,
292            Bound::Excluded(&start) => start as usize + 1,
293            Bound::Unbounded => 0,
294        };
295        let end = match range.end_bound() {
296            Bound::Included(&end) => end as usize + 1,
297            Bound::Excluded(&end) => end as usize,
298            Bound::Unbounded => self.matches.len(),
299        };
300        self.matches[start..end]
301            .iter()
302            .map(|&m| unsafe { self.items.get_unchecked(m.idx) })
303    }
304
305    /// Returns a reference to the item at the given index.
306    ///
307    /// Returns `None` if the given `index` is not initialized. This function
308    /// is only guarteed to return `Some` for item indices that can be found in
309    /// the `matches` of this struct. Both smaller and larger indices may return
310    /// `None`.
311    #[inline]
312    pub fn get_item(&self, index: u32) -> Option<Item<'_, T>> {
313        self.items.get(index)
314    }
315
316    /// Returns a reference to the item at the given index.
317    ///
318    /// # Safety
319    ///
320    /// Item at `index` must be initialized. That means you must have observed a
321    /// match with the corresponding index in this exact snapshot. Observing
322    /// a higher index is not enough as item indices can be non-contigously
323    /// initialized
324    #[inline]
325    pub unsafe fn get_item_unchecked(&self, index: u32) -> Item<'_, T> {
326        unsafe { self.items.get_unchecked(index) }
327    }
328
329    /// Returns the detached item at the given index.
330    #[inline]
331    pub fn get_detached_item(&self, index: u32) -> Option<DetachedItem<T>> {
332        self.items
333            .is_valid(index)
334            .then(|| unsafe { DetachedItem::new(&self.items, index) })
335    }
336
337    /// Returns the detached item at the given index without checking that the index is valid.
338    ///
339    /// # Safety
340    ///
341    /// Item at `index` must be initialized. That means you must have observed
342    /// `push` returning this value or `get` returning `Some` for this value.
343    /// Just because a later index is initialized doesn't mean that this index
344    /// is initialized
345    #[inline]
346    pub unsafe fn get_detached_item_unchecked(&self, index: u32) -> DetachedItem<T> {
347        unsafe { DetachedItem::new(&self.items, index) }
348    }
349
350    /// Returns the matches corresponding to this snapshot.
351    #[inline]
352    pub fn matches(&self) -> &[Match] {
353        &self.matches
354    }
355
356    /// A convenience function to return the [`Item`] corresponding to the
357    /// `n`th match.
358    ///
359    /// Returns `None` if `n` is greater than or equal to the match count.
360    #[inline]
361    pub fn get_matched_item(&self, n: u32) -> Option<Item<'_, T>> {
362        // SAFETY: A match index is guaranteed to corresponding to a valid global index in this
363        // snapshot.
364        unsafe { Some(self.get_item_unchecked(self.matches.get(n as usize)?.idx)) }
365    }
366
367    /// A convenience function to return the [`DetachedItem`] corresponding to the
368    /// `n`th match.
369    ///
370    /// Returns `None` if `n` is greater than or equal to the match count.
371    #[inline]
372    pub fn get_matched_detached_item(&self, n: u32) -> Option<DetachedItem<T>> {
373        // SAFETY: A match index is guaranteed to corresponding to a valid global index in this
374        // snapshot.
375        unsafe { Some(self.get_detached_item_unchecked(self.matches.get(n as usize)?.idx)) }
376    }
377}
378
379#[repr(u8)]
380#[derive(Clone, Copy, PartialEq, Eq)]
381enum State {
382    Init,
383    /// items have been cleared but snapshot and items are still outdated
384    Cleared,
385    /// items are fresh
386    Fresh,
387}
388
389impl State {
390    fn matcher_item_refs(self) -> usize {
391        match self {
392            Self::Cleared => 1,
393            Self::Init | Self::Fresh => 2,
394        }
395    }
396
397    fn canceled(self) -> bool {
398        self != Self::Fresh
399    }
400
401    fn cleared(self) -> bool {
402        self != Self::Fresh
403    }
404}
405
406/// A high level matcher worker that quickly computes matches in a background
407/// threadpool.
408///
409/// ## Example
410/// ```
411/// use std::sync::atomic::{AtomicBool, Ordering};
412/// use std::sync::Arc;
413/// use std::thread;
414///
415/// use ncp_engine::{Config, Nucleo};
416///
417/// static NEEDS_UPDATE: AtomicBool = AtomicBool::new(false);
418///
419/// // initialize a new matcher with default configuration and one column
420/// let matcher = Nucleo::new(
421///     Config::DEFAULT,
422///     Arc::new(|| NEEDS_UPDATE.store(true, Ordering::Relaxed)),
423///     None,
424///     1
425/// );
426///
427/// // get a handle to add items to the matcher
428/// let injector = matcher.injector();
429///
430/// // add items to the matcher
431/// thread::spawn(move || {
432///     injector.push("Hello, world!".to_string(), |s, cols| {
433///         cols[0] = (&**s).into();
434///     });
435/// });
436/// ```
437pub struct Nucleo<T> {
438    // the way the API is build we totally don't actually need these to be Arcs
439    // but this lets us avoid some unsafe
440    canceled: Arc<AtomicBool>,
441    should_notify: Arc<AtomicBool>,
442    worker: Arc<Mutex<Worker<T>>>,
443    pool: ThreadPool,
444    state: State,
445    items: Arc<boxcar::Vec<T>>,
446    notify: Arc<dyn Fn() + Sync + Send>,
447    snapshot: Snapshot<T>,
448    /// The pattern matched by this matcher.
449    ///
450    /// To update the match pattern, use [`MultiPattern::reparse`]. Note that
451    /// the matcher worker will only become aware of the new pattern after a
452    /// call to [`tick`](Nucleo::tick).
453    pub pattern: MultiPattern,
454}
455
456impl<T: Sync + Send + 'static> Nucleo<T> {
457    /// Constructs a new `nucleo` worker threadpool with the provided `config`.
458    ///
459    /// `notify` is called whenever new information is available and
460    /// [`tick`](Nucleo::tick) should be called. Note that `notify` is not
461    /// debounced; that should be handled by the downstream crate (for example,
462    /// debouncing to only redraw at most every 1/60 seconds).
463    ///
464    /// If `None` is passed for the number of worker threads, nucleo will use
465    /// one thread per hardware thread.
466    ///
467    /// Nucleo can match items with multiple orthogonal properties. `columns`
468    /// indicates how many matching columns each item (and the pattern) has. The
469    /// number of columns cannot be changed after construction.
470    pub fn new(
471        config: Config,
472        notify: Arc<dyn Fn() + Sync + Send>,
473        num_threads: Option<usize>,
474        columns: u32,
475    ) -> Self {
476        Self::with_match_list_config(
477            config,
478            notify,
479            num_threads,
480            columns,
481            MatchListConfig::DEFAULT,
482        )
483    }
484
485    /// Constructs a new worker threadpool with the provided configuration, with pre-defined
486    /// configuration for the match list.
487    pub fn with_match_list_config(
488        config: Config,
489        notify: Arc<dyn Fn() + Sync + Send>,
490        num_threads: Option<usize>,
491        columns: u32,
492        match_list_config: MatchListConfig,
493    ) -> Self {
494        let (pool, worker) = Worker::new(
495            num_threads,
496            config,
497            notify.clone(),
498            columns,
499            match_list_config,
500        );
501        Self {
502            canceled: worker.canceled.clone(),
503            should_notify: worker.should_notify.clone(),
504            items: worker.items.clone(),
505            pool,
506            pattern: MultiPattern::new(columns as usize),
507            snapshot: Snapshot {
508                matches: Vec::with_capacity(2 * 1024),
509                pattern: MultiPattern::new(columns as usize),
510                item_count: 0,
511                items: worker.items.clone(),
512            },
513            worker: Arc::new(Mutex::new(worker)),
514            state: State::Init,
515            notify,
516        }
517    }
518
519    /// Returns the total number of active injectors.
520    pub fn active_injectors(&self) -> usize {
521        Arc::strong_count(&self.items)
522            - self.state.matcher_item_refs()
523            - (Arc::ptr_eq(&self.snapshot.items, &self.items)) as usize
524    }
525
526    /// Returns a snapshot of the current matcher state.
527    ///
528    /// This method is very cheap and can be called every time a snapshot is required. The
529    /// snapshot will not change unless [`tick`](Nucleo::tick) is called.
530    pub fn snapshot(&self) -> &Snapshot<T> {
531        &self.snapshot
532    }
533
534    /// Returns an injector that can be used for adding candidates to the matcher.
535    pub fn injector(&self) -> Injector<T> {
536        Injector {
537            items: self.items.clone(),
538            notify: self.notify.clone(),
539        }
540    }
541
542    /// Restart the the item stream. Removes all items and disconnects all
543    /// previously created injectors from this instance. If `clear_snapshot`
544    /// is `true` then all items and matches are removed from the [`Snapshot`]
545    /// immediately. Otherwise the snapshot will keep the current matches until
546    /// the matcher has run again.
547    ///
548    /// # Note
549    ///
550    /// The injectors will continue to function but they will not affect this
551    /// instance anymore. The old items will only be dropped when all injectors
552    /// and detached items are dropped.
553    pub fn restart(&mut self, clear_snapshot: bool) {
554        self.canceled.store(true, Ordering::Relaxed);
555        self.items = Arc::new(boxcar::Vec::with_capacity(1024, self.items.columns()));
556        self.state = State::Cleared;
557        if clear_snapshot {
558            self.snapshot.clear(self.items.clone());
559        }
560    }
561
562    /// Update the internal configuration.
563    pub fn update_config(&mut self, config: Config) {
564        self.worker.lock().update_config(config);
565    }
566
567    /// Set whether the matcher should sort search results by score after
568    /// matching. Defaults to true.
569    pub fn sort_results(&mut self, sort_results: bool) {
570        self.worker.lock().sort_results(sort_results);
571    }
572
573    /// Set whether the matcher should reverse the order of the input.
574    /// Defaults to false.
575    pub fn reverse_items(&mut self, reverse_items: bool) {
576        self.worker.lock().reverse_items(reverse_items);
577    }
578
579    /// Update the internal state to reflect any changes from the background worker
580    /// threads.
581    ///
582    /// This is the main way to interact with the matcher, and should be called
583    /// regularly (for example each time a frame is rendered). To avoid excessive
584    /// redraws this method will wait `timeout` milliseconds for the
585    /// worker threads to finish. It is recommend to set the timeout to 10ms.
586    pub fn tick(&mut self, timeout: u64) -> Status {
587        self.should_notify.store(false, atomic::Ordering::Relaxed);
588        let status = self.pattern.status();
589        let canceled = status != pattern::Status::Unchanged || self.state.canceled();
590        let mut res = self.tick_inner(timeout, canceled, status);
591        if !canceled {
592            return res;
593        }
594        self.state = State::Fresh;
595        let status2 = self.tick_inner(timeout, false, pattern::Status::Unchanged);
596        res.changed |= status2.changed;
597        res.running = status2.running;
598        res
599    }
600
601    fn tick_inner(&mut self, timeout: u64, canceled: bool, status: pattern::Status) -> Status {
602        let mut inner = if canceled {
603            self.pattern.reset_status();
604            self.canceled.store(true, atomic::Ordering::Relaxed);
605            self.worker.lock_arc()
606        } else {
607            let Some(worker) = self.worker.try_lock_arc_for(Duration::from_millis(timeout)) else {
608                self.should_notify.store(true, Ordering::Release);
609                return Status {
610                    changed: false,
611                    running: true,
612                };
613            };
614            worker
615        };
616
617        let changed = inner.running;
618
619        let running = canceled || self.items.count() > inner.item_count();
620        if inner.running {
621            inner.running = false;
622            if !inner.was_canceled && !self.state.canceled() {
623                self.snapshot.update(&inner);
624            }
625        }
626        if running {
627            inner.pattern.clone_from(&self.pattern);
628            self.canceled.store(false, atomic::Ordering::Relaxed);
629            if !canceled {
630                self.should_notify.store(true, atomic::Ordering::Release);
631            }
632            let cleared = self.state.cleared();
633            if cleared {
634                inner.items = self.items.clone();
635            }
636            self.pool
637                .spawn(move || unsafe { inner.run(status, cleared) });
638        }
639        Status { changed, running }
640    }
641}
642
643impl<T> Drop for Nucleo<T> {
644    fn drop(&mut self) {
645        // we ensure the worker quits before dropping items to ensure that
646        // the worker can always assume the items outlive it
647        self.canceled.store(true, atomic::Ordering::Relaxed);
648        let lock = self.worker.try_lock_for(Duration::from_secs(1));
649        if lock.is_none() {
650            unreachable!("thread pool failed to shutdown properly")
651        }
652    }
653}