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