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