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}