Skip to main content

matchmaker/nucleo/
worker.rs

1// Original code from https://github.com/helix-editor/helix (MPL 2.0)
2// Modified by Squirreljetpack, 2025
3
4#![allow(unused)]
5
6use super::{Line, Span, Style, Text};
7use bitflags::bitflags;
8
9use ratatui::style::Modifier;
10use std::{
11    borrow::Cow,
12    sync::{
13        Arc,
14        atomic::{self, AtomicU32},
15    },
16};
17use unicode_segmentation::UnicodeSegmentation;
18use unicode_width::UnicodeWidthStr;
19
20use super::{injector::WorkerInjector, query::PickerQuery};
21use crate::{
22    SSS,
23    nucleo::Render,
24    utils::text::{apply_style_at, plain_text, wrap_text},
25};
26use cli_boilerplate_automation::text::StrExt;
27
28type ColumnFormatFn<T> = Box<dyn for<'a> Fn(&'a T) -> Text<'a> + Send + Sync>;
29pub struct Column<T> {
30    pub name: Arc<str>,
31    pub(super) format: ColumnFormatFn<T>,
32    /// Whether the column should be passed to nucleo for matching and filtering.
33    pub(super) filter: bool,
34}
35
36impl<T> Column<T> {
37    pub fn new_boxed(name: impl Into<Arc<str>>, format: ColumnFormatFn<T>) -> Self {
38        Self {
39            name: name.into(),
40            format,
41            filter: true,
42        }
43    }
44
45    pub fn new<F>(name: impl Into<Arc<str>>, f: F) -> Self
46    where
47        F: for<'a> Fn(&'a T) -> Text<'a> + SSS,
48    {
49        Self {
50            name: name.into(),
51            format: Box::new(f),
52            filter: true,
53        }
54    }
55
56    /// Disable filtering.
57    pub fn without_filtering(mut self) -> Self {
58        self.filter = false;
59        self
60    }
61
62    pub(super) fn format<'a>(&self, item: &'a T) -> Text<'a> {
63        (self.format)(item)
64    }
65
66    // Note: the characters should match the output of [`Self::format`]
67    pub(super) fn format_text<'a>(&self, item: &'a T) -> Cow<'a, str> {
68        Cow::Owned(plain_text(&(self.format)(item)))
69    }
70}
71
72/// Worker: can instantiate, push, and get results. A view into computation.
73///
74/// Additionally, the worker can affect the computation via find and restart.
75pub struct Worker<T>
76where
77    T: SSS,
78{
79    /// The inner `Nucleo` fuzzy matcher.
80    pub(crate) nucleo: nucleo::Nucleo<T>,
81    /// The last pattern that was matched against.
82    pub(super) query: PickerQuery,
83    /// A pre-allocated buffer used to collect match indices when fetching the results
84    /// from the matcher. This avoids having to re-allocate on each pass.
85    pub(super) col_indices_buffer: Vec<u32>,
86    pub(crate) columns: Arc<[Column<T>]>,
87
88    // Background tasks which push to the injector check their version matches this or exit
89    pub(super) version: Arc<AtomicU32>,
90    // pub settings: WorkerSettings,
91    column_options: Vec<ColumnOptions>,
92}
93
94// #[derive(Debug, Default)]
95// pub struct WorkerSettings {
96//     pub stable: bool,
97// }
98
99bitflags! {
100    #[derive(Default, Clone, Debug)]
101    pub struct ColumnOptions: u8 {
102        const Optional = 1 << 0;
103        const OrUseDefault = 1 << 2;
104    }
105}
106
107impl<T> Worker<T>
108where
109    T: SSS,
110{
111    /// Column names must be distinct!
112    pub fn new(columns: impl IntoIterator<Item = Column<T>>, default_column: usize) -> Self {
113        let columns: Arc<[_]> = columns.into_iter().collect();
114        let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
115
116        let inner = nucleo::Nucleo::new(
117            nucleo::Config::DEFAULT,
118            Arc::new(|| {}),
119            None,
120            matcher_columns,
121        );
122
123        Self {
124            nucleo: inner,
125            col_indices_buffer: Vec::with_capacity(128),
126            query: PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column),
127            column_options: vec![ColumnOptions::default(); columns.len()],
128            columns,
129            version: Arc::new(AtomicU32::new(0)),
130        }
131    }
132
133    #[cfg(feature = "experimental")]
134    pub fn set_column_options(&mut self, index: usize, options: ColumnOptions) {
135        if options.contains(ColumnOptions::Optional) {
136            self.nucleo
137                .pattern
138                .configure_column(index, nucleo::pattern::Variant::Optional)
139        }
140
141        self.column_options[index] = options
142    }
143
144    pub fn injector(&self) -> WorkerInjector<T> {
145        WorkerInjector {
146            inner: self.nucleo.injector(),
147            columns: self.columns.clone(),
148            version: self.version.load(atomic::Ordering::Relaxed),
149            picker_version: self.version.clone(),
150        }
151    }
152
153    pub fn find(&mut self, line: &str) {
154        let old_query = self.query.parse(line);
155        if self.query == old_query {
156            return;
157        }
158        for (i, column) in self
159            .columns
160            .iter()
161            .filter(|column| column.filter)
162            .enumerate()
163        {
164            let pattern = self
165                .query
166                .get(&column.name)
167                .map(|s| &**s)
168                .unwrap_or_else(|| {
169                    self.column_options[i]
170                        .contains(ColumnOptions::OrUseDefault)
171                        .then(|| self.query.primary_column_query())
172                        .flatten()
173                        .unwrap_or_default()
174                });
175
176            let old_pattern = old_query
177                .get(&column.name)
178                .map(|s| &**s)
179                .unwrap_or_else(|| {
180                    self.column_options[i]
181                        .contains(ColumnOptions::OrUseDefault)
182                        .then(|| {
183                            let name = self.query.primary_column_name()?;
184                            old_query.get(name).map(|s| &**s)
185                        })
186                        .flatten()
187                        .unwrap_or_default()
188                });
189
190            // Fastlane: most columns will remain unchanged after each edit.
191            if pattern == old_pattern {
192                continue;
193            }
194            let is_append = pattern.starts_with(old_pattern);
195
196            self.nucleo.pattern.reparse(
197                i,
198                pattern,
199                nucleo::pattern::CaseMatching::Smart,
200                nucleo::pattern::Normalization::Smart,
201                is_append,
202            );
203        }
204    }
205
206    // --------- UTILS
207    pub fn get_nth(&self, n: u32) -> Option<&T> {
208        self.nucleo
209            .snapshot()
210            .get_matched_item(n)
211            .map(|item| item.data)
212    }
213
214    pub fn new_snapshot(nucleo: &mut nucleo::Nucleo<T>) -> (&nucleo::Snapshot<T>, Status) {
215        let nucleo::Status { changed, running } = nucleo.tick(10);
216        let snapshot = nucleo.snapshot();
217        (
218            snapshot,
219            Status {
220                item_count: snapshot.item_count(),
221                matched_count: snapshot.matched_item_count(),
222                running,
223                changed,
224            },
225        )
226    }
227
228    pub fn raw_results(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator + '_ {
229        let snapshot = self.nucleo.snapshot();
230        snapshot.matched_items(..).map(|item| item.data)
231    }
232
233    /// matched item count, total item count
234    pub fn counts(&self) -> (u32, u32) {
235        let snapshot = self.nucleo.snapshot();
236        (snapshot.matched_item_count(), snapshot.item_count())
237    }
238
239    #[cfg(feature = "experimental")]
240    pub fn set_stability(&mut self, threshold: u32) {
241        self.nucleo.set_stability(threshold);
242    }
243
244    pub fn restart(&mut self, clear_snapshot: bool) {
245        self.nucleo.restart(clear_snapshot);
246    }
247}
248
249#[derive(Debug, Default, Clone)]
250pub struct Status {
251    pub item_count: u32,
252    pub matched_count: u32,
253    pub running: bool,
254    pub changed: bool,
255}
256
257#[derive(Debug, thiserror::Error)]
258pub enum WorkerError {
259    #[error("the matcher injector has been shut down")]
260    InjectorShutdown,
261    #[error("{0}")]
262    Custom(&'static str),
263}
264
265pub type WorkerResults<'a, T> = Vec<(Vec<Text<'a>>, &'a T, u16)>;
266
267impl<T: SSS> Worker<T> {
268    /// # Notes
269    /// - width is at least header width
270    pub fn results(
271        &mut self,
272        start: u32,
273        end: u32,
274        width_limits: &[u16],
275        highlight_style: Style,
276        matcher: &mut nucleo::Matcher,
277    ) -> (WorkerResults<'_, T>, Vec<u16>, Status) {
278        let (snapshot, status) = Self::new_snapshot(&mut self.nucleo);
279
280        let mut widths = vec![0u16; self.columns.len()]; // first cell reserved for prefix
281
282        let iter =
283            snapshot.matched_items(start.min(status.matched_count)..end.min(status.matched_count));
284
285        let table = iter
286            .map(|item| {
287                let mut widths = widths.iter_mut();
288                let mut col_idx = 0;
289                let mut height = 0;
290
291                let row = self
292                    .columns
293                    .iter()
294                    .zip(width_limits.iter().chain(std::iter::repeat(&u16::MAX)))
295                    .map(|(column, &width_limit)| {
296                        let max_width = widths.next().unwrap();
297                        let cell = column.format(item.data);
298
299                        // 0 represents hide
300                        if width_limit == 0 {
301                            return Text::from("");
302                        }
303
304                        let (cell, width) = if column.filter && width_limit == u16::MAX {
305                            let mut cell_width = 0;
306
307                            // get indices
308                            let indices_buffer = &mut self.col_indices_buffer;
309                            indices_buffer.clear();
310                            snapshot.pattern().column_pattern(col_idx).indices(
311                                item.matcher_columns[col_idx].slice(..),
312                                matcher,
313                                indices_buffer,
314                            );
315                            indices_buffer.sort_unstable();
316                            indices_buffer.dedup();
317                            let mut indices = indices_buffer.drain(..);
318
319                            let mut lines = vec![];
320                            let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
321                            let mut grapheme_idx = 0u32;
322
323                            for line in cell {
324                                let mut span_list = Vec::new();
325                                let mut current_span = String::new();
326                                let mut current_style = Style::default();
327                                let mut width = 0;
328
329                                for span in line {
330                                    // this looks like a bug on first glance, we are iterating
331                                    // graphemes but treating them as char indices. The reason that
332                                    // this is correct is that nucleo will only ever consider the first char
333                                    // of a grapheme (and discard the rest of the grapheme) so the indices
334                                    // returned by nucleo are essentially grapheme indecies
335                                    for grapheme in span.content.graphemes(true) {
336                                        let style = if grapheme_idx == next_highlight_idx {
337                                            next_highlight_idx = indices.next().unwrap_or(u32::MAX);
338                                            span.style.patch(highlight_style)
339                                        } else {
340                                            span.style
341                                        };
342                                        if style != current_style {
343                                            if !current_span.is_empty() {
344                                                span_list
345                                                    .push(Span::styled(current_span, current_style))
346                                            }
347                                            current_span = String::new();
348                                            current_style = style;
349                                        }
350                                        current_span.push_str(grapheme);
351                                        grapheme_idx += 1;
352                                    }
353                                    width += span.width();
354                                }
355
356                                span_list.push(Span::styled(current_span, current_style));
357                                lines.push(Line::from(span_list));
358                                cell_width = cell_width.max(width);
359                                grapheme_idx += 1; // newline?
360                            }
361
362                            col_idx += 1;
363                            (Text::from(lines), cell_width)
364                        } else if column.filter {
365                            let mut cell_width = 0;
366                            let mut wrapped = false;
367
368                            // get indices
369                            let indices_buffer = &mut self.col_indices_buffer;
370                            indices_buffer.clear();
371                            snapshot.pattern().column_pattern(col_idx).indices(
372                                item.matcher_columns[col_idx].slice(..),
373                                matcher,
374                                indices_buffer,
375                            );
376                            indices_buffer.sort_unstable();
377                            indices_buffer.dedup();
378                            let mut indices = indices_buffer.drain(..);
379
380                            let mut lines: Vec<Line<'_>> = vec![];
381                            let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
382                            let mut grapheme_idx = 0u32;
383
384                            for line in cell {
385                                let mut current_spans = Vec::new();
386                                let mut current_span = String::new();
387                                let mut current_style = Style::default();
388                                let mut current_width = 0;
389
390                                for span in line {
391                                    let mut graphemes = span.content.graphemes(true).peekable();
392                                    while let Some(grapheme) = graphemes.next() {
393                                        let grapheme_width = UnicodeWidthStr::width(grapheme);
394
395                                        if current_width + grapheme_width
396                                            > (width_limit - 1) as usize
397                                            && { grapheme_width > 1 || graphemes.peek().is_some() }
398                                        {
399                                            current_spans
400                                                .push(Span::styled(current_span, current_style));
401                                            current_spans.push(Span::styled(
402                                                "↵",
403                                                Style::default().add_modifier(Modifier::DIM),
404                                            ));
405                                            lines.push(Line::from(current_spans));
406
407                                            current_spans = Vec::new();
408                                            current_span = String::new();
409                                            current_width = 0;
410                                            wrapped = true;
411                                        }
412
413                                        let style = if grapheme_idx == next_highlight_idx {
414                                            next_highlight_idx = indices.next().unwrap_or(u32::MAX);
415                                            span.style.patch(highlight_style)
416                                        } else {
417                                            span.style
418                                        };
419
420                                        if style != current_style {
421                                            if !current_span.is_empty() {
422                                                current_spans
423                                                    .push(Span::styled(current_span, current_style))
424                                            }
425                                            current_span = String::new();
426                                            current_style = style;
427                                        }
428                                        current_span.push_str(grapheme);
429                                        grapheme_idx += 1;
430                                        current_width += grapheme_width;
431                                    }
432                                }
433
434                                current_spans.push(Span::styled(current_span, current_style));
435                                lines.push(Line::from(current_spans));
436                                cell_width = cell_width.max(current_width);
437                                grapheme_idx += 1; // newline?
438                            }
439
440                            col_idx += 1;
441
442                            (
443                                Text::from(lines),
444                                if wrapped {
445                                    width_limit as usize
446                                } else {
447                                    cell_width
448                                },
449                            )
450                        } else if width_limit != u16::MAX {
451                            let (cell, wrapped) = wrap_text(cell, width_limit - 1);
452
453                            let width = if wrapped {
454                                width_limit as usize
455                            } else {
456                                cell.width()
457                            };
458                            (cell, width)
459                        } else {
460                            let width = cell.width();
461                            (cell, width)
462                        };
463
464                        // update col width, row height
465                        if width as u16 > *max_width {
466                            *max_width = width as u16;
467                        }
468
469                        if cell.height() as u16 > height {
470                            height = cell.height() as u16;
471                        }
472
473                        cell
474                    });
475
476                (row.collect(), item.data, height)
477            })
478            .collect();
479
480        // Nonempty columns should have width at least their header
481        for (w, c) in widths.iter_mut().zip(self.columns.iter()) {
482            let name_width = c.name.width() as u16;
483            if *w != 0 {
484                *w = (*w).max(name_width);
485            }
486        }
487
488        (table, widths, status)
489    }
490
491    pub fn exact_column_match(&mut self, column: &str) -> Option<&T> {
492        let (i, col) = self
493            .columns
494            .iter()
495            .enumerate()
496            .find(|(_, c)| column == &*c.name)?;
497
498        let query = self.query.get(column).map(|s| &**s).or_else(|| {
499            self.column_options[i]
500                .contains(ColumnOptions::OrUseDefault)
501                .then(|| self.query.primary_column_query())
502                .flatten()
503        })?;
504
505        let snapshot = self.nucleo.snapshot();
506        snapshot.matched_items(..).find_map(|item| {
507            let content = col.format_text(item.data);
508            if content.as_str() == query {
509                Some(item.data)
510            } else {
511                None
512            }
513        })
514    }
515
516    pub fn format_with<'a>(&'a self, item: &'a T, col: &str) -> Option<Cow<'a, str>> {
517        self.columns
518            .iter()
519            .find(|c| &*c.name == col)
520            .map(|c| c.format_text(item))
521    }
522}