matchmaker/nucleo/
worker.rs

1// Original code from https://github.com/helix-editor/helix (MPL 2.0)
2// Modified by Squirreljetpack, 2025
3
4use std::{
5    borrow::Cow,
6    sync::{
7        Arc,
8        atomic::{self, AtomicU32},
9    },
10};
11use super::{
12    Style, Stylize,
13    Line, Span, Text,
14};
15use unicode_segmentation::UnicodeSegmentation;
16use unicode_width::UnicodeWidthStr;
17
18use crate::{PickerItem, utils::text::plain_text};
19
20use super::{injector::WorkerInjector, query::PickerQuery};
21
22type ColumnFormatFn<T, C> = Box<dyn for<'a> Fn(&'a T, &'a C) -> Text<'a> + Send + Sync>;
23pub struct Column<T, D = ()> {
24    pub name: Arc<str>,
25    pub(super) format: ColumnFormatFn<T, D>,
26    /// Whether the column should be passed to nucleo for matching and filtering.
27    pub(super) filter: bool,
28    pub(super) hidden: bool,
29}
30
31impl<T, D> Column<T, D> {
32    pub fn new_boxed(name: impl Into<Arc<str>>, format: ColumnFormatFn<T, D>) -> Self {
33        Self {
34            name: name.into(),
35            format,
36            filter: true,
37            hidden: false,
38        }
39    }
40    
41    pub fn new<F>(name: impl Into<Arc<str>>, f: F) -> Self
42    where
43    F: for<'a> Fn(&'a T, &'a D) -> Text<'a> + Send + Sync + 'static,
44    {
45        Self {
46            name: name.into(),
47            format: Box::new(f),
48            filter: true,
49            hidden: false,
50        }
51    }
52    
53    pub fn with_hidden(mut self) -> Self {
54        self.hidden = true;
55        self
56    }
57    
58    pub fn without_filtering(mut self) -> Self {
59        self.filter = false;
60        self
61    }
62    
63    fn format<'a>(&self, item: &'a T, context: &'a D) -> Text<'a> {
64        (self.format)(item, context)
65    }
66    
67    pub(super) fn format_text<'a>(&self, item: &'a T, context: &'a D) -> Cow<'a, str> {
68        Cow::Owned(plain_text(&(self.format)(item, context)))
69    }
70}
71
72/// Worker: can instantiate, can push, can get lines and get nth, a view into computation
73pub struct Worker<T, C = ()>
74where
75T: PickerItem,
76{
77    /// The inner `Nucleo` fuzzy matcher.
78    pub(super) nucleo: nucleo::Nucleo<T>,
79    /// The last pattern that was matched against.
80    pub(super) query: PickerQuery,
81    /// A pre-allocated buffer used to collect match indices when fetching the results
82    /// from the matcher. This avoids having to re-allocate on each pass.
83    pub(super) col_indices_buffer: Vec<u32>,
84    pub(crate) columns: Arc<[Column<T, C>]>,
85    pub(super) context: Arc<C>,
86    
87    // Background tasks which push to the injector check their version matches this or exit
88    pub(super) version: Arc<AtomicU32>,
89}
90
91impl<T, C> Worker<T, C>
92where
93T: PickerItem,
94{
95    pub fn new(
96        columns: impl IntoIterator<Item = Column<T, C>>,
97        default_column: usize,
98        context: C,
99    ) -> Self {
100        let columns: Arc<[_]> = columns.into_iter().collect();
101        let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
102        
103        let inner = nucleo::Nucleo::new(
104            nucleo::Config::DEFAULT,
105            Arc::new(|| {}),
106            None,
107            matcher_columns,
108        );
109        
110        Self {
111            nucleo: inner,
112            col_indices_buffer: Vec::with_capacity(128),
113            query: PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column),
114            columns,
115            context: Arc::new(context),
116            version: Arc::new(AtomicU32::new(0)),
117        }
118    }
119    
120    pub fn injector(&self) -> WorkerInjector<T, C> {
121        WorkerInjector {
122            inner: self.nucleo.injector(),
123            columns: self.columns.clone(),
124            context: self.context.clone(),
125            version: self.version.load(atomic::Ordering::Relaxed),
126            picker_version: self.version.clone(),
127        }
128    }
129    
130    pub fn results(
131        &mut self,
132        start: u32,
133        end: u32,
134        matcher: &mut nucleo::Matcher,
135    ) -> (Vec<(Vec<Text<'_>>, &T, u16)>, Vec<u16>, Status) {
136        let (snapshot, status) = Self::new_snapshot(&mut self.nucleo);
137        let highlight_style = Style::new().green(); // todo
138        
139        let mut widths = vec![0u16; self.columns.len()];
140        
141        let iter =
142        snapshot.matched_items(start.min(status.matched_count)..end.min(status.matched_count));
143        
144        let table = iter
145        .map(|item| {
146            let mut widths = widths.iter_mut();
147            let mut col_idx = 0;
148            let mut height = 0;
149            
150            let row = self.columns.iter().map(|column| {
151                if column.hidden {
152                    return Text::from("");
153                }
154                
155                let max_width = widths.next().unwrap();
156                let mut cell = column.format(item.data, &self.context);
157                
158                let width = if column.filter {
159                    let mut cell_width = 0;
160                    
161                    // get indices
162                    let indices_buffer = &mut self.col_indices_buffer;
163                    indices_buffer.clear();
164                    snapshot.pattern().column_pattern(col_idx).indices(
165                        item.matcher_columns[col_idx].slice(..),
166                        matcher,
167                        indices_buffer,
168                    );
169                    indices_buffer.sort_unstable();
170                    indices_buffer.dedup();
171                    let mut indices = indices_buffer.drain(..);
172                    
173                    let mut lines = vec![];
174                    let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
175                    let mut grapheme_idx = 0u32;
176                    
177                    for line in cell {
178                        let mut span_list = Vec::new();
179                        let mut current_span = String::new();
180                        let mut current_style = Style::default();
181                        let mut width = 0;
182                        
183                        for span in line {
184                            // this looks like a bug on first glance, we are iterating
185                            // graphemes but treating them as char indices. The reason that
186                            // this is correct is that nucleo will only ever consider the first char
187                            // of a grapheme (and discard the rest of the grapheme) so the indices
188                            // returned by nucleo are essentially grapheme indecies
189                            for grapheme in span.content.graphemes(true) {
190                                let style = if grapheme_idx == next_highlight_idx {
191                                    next_highlight_idx = indices.next().unwrap_or(u32::MAX);
192                                    span.style.patch(highlight_style)
193                                } else {
194                                    span.style
195                                };
196                                if style != current_style {
197                                    if !current_span.is_empty() {
198                                        span_list
199                                        .push(Span::styled(current_span, current_style))
200                                    }
201                                    current_span = String::new();
202                                    current_style = style;
203                                }
204                                current_span.push_str(grapheme);
205                                grapheme_idx += 1;
206                            }
207                            width += span.width();
208                        }
209                        
210                        span_list.push(Span::styled(current_span, current_style));
211                        lines.push(Line::from(span_list));
212                        cell_width = cell_width.max(width);
213                        grapheme_idx += 1; // newline?
214                    }
215                    
216                    col_idx += 1;
217                    cell = Text::from(lines);
218                    cell_width
219                } else {
220                    cell.lines.iter().map(|l| l.width()).max().unwrap()
221                };
222                
223                // Update column width if needed
224                if width as u16 > *max_width {
225                    *max_width = width as u16;
226                }
227                if cell.height() as u16 > height {
228                    height = cell.height() as u16;
229                }
230                
231                Text::from(cell)
232            });
233            
234            (row.collect(), item.data, height)
235        })
236        .collect();
237        
238        for (w, c) in widths.iter_mut().zip(self.columns.iter()) {
239            let name_width = c.name.width() as u16;
240            if *w != 0 {
241                *w = (*w).max(name_width);
242            }
243        }
244        
245        (table, widths, status)
246    }
247    
248    pub fn find(&mut self, line: &str) {
249        let old_query = self.query.parse(line);
250        if self.query == old_query {
251            return;
252        }
253        for (i, column) in self
254        .columns
255        .iter()
256        .filter(|column| column.filter)
257        .enumerate()
258        {
259            let pattern = self
260            .query
261            .get(&column.name)
262            .map(|f| &**f)
263            .unwrap_or_default();
264            let old_pattern = old_query
265            .get(&column.name)
266            .map(|f| &**f)
267            .unwrap_or_default();
268            // Fastlane: most columns will remain unchanged after each edit.
269            if pattern == old_pattern {
270                continue;
271            }
272            let is_append = pattern.starts_with(old_pattern);
273            self.nucleo.pattern.reparse(
274                i,
275                pattern,
276                nucleo::pattern::CaseMatching::Smart,
277                nucleo::pattern::Normalization::Smart,
278                is_append,
279            );
280        }
281    }
282    
283    // anything need be done?
284    pub fn shutdown(&mut self) {
285        todo!()
286    }
287    
288    pub fn restart(&mut self, clear_snapshot: bool) {
289        self.nucleo.restart(clear_snapshot);
290    }
291    
292    // --------- UTILS
293    pub fn get_nth(&self, n: u32) -> Option<&T> {
294        self.nucleo
295        .snapshot()
296        .get_matched_item(n)
297        .map(|item| item.data)
298    }
299    
300    pub fn new_snapshot(nucleo: &mut nucleo::Nucleo<T>) -> (&nucleo::Snapshot<T>, Status) {
301        let nucleo::Status { changed, running } = nucleo.tick(10);
302        let snapshot = nucleo.snapshot();
303        (
304            snapshot,
305            Status {
306                item_count: snapshot.item_count(),
307                matched_count: snapshot.matched_item_count(),
308                running,
309                changed,
310            },
311        )
312    }
313    
314    pub fn raw_results(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator + '_ {
315        let snapshot = self.nucleo.snapshot();
316        snapshot.matched_items(..).map(|item| item.data)
317    }
318    
319    /// matched item count, total item count
320    pub fn counts(&self) -> (u32, u32) {
321        let snapshot = self.nucleo.snapshot();
322        (snapshot.matched_item_count(), snapshot.item_count())
323    }
324}
325
326#[derive(Debug, Default, Clone)]
327pub struct Status {
328    pub item_count: u32,
329    pub matched_count: u32,
330    pub running: bool,
331    pub changed: bool,
332}
333
334
335#[derive(Debug, thiserror::Error)]
336pub enum WorkerError {
337    #[error("the matcher injector has been shut down")]
338    InjectorShutdown,
339}