1use super::{Line, Span, Style, Text};
5use bitflags::bitflags;
6use std::{
7 borrow::Cow,
8 mem::take,
9 sync::{
10 Arc,
11 atomic::{self, AtomicU32},
12 },
13};
14use unicode_segmentation::UnicodeSegmentation;
15use unicode_width::UnicodeWidthStr;
16
17use super::{injector::WorkerInjector, query::PickerQuery};
18use crate::{
19 SSS,
20 nucleo::Render,
21 utils::text::{hscroll_indicator, text_to_string, wrap_text, wrapping_indicator},
22};
23
24type ColumnFormatFn<T> = Box<dyn for<'a> Fn(&'a T) -> Text<'a> + Send + Sync>;
25pub struct Column<T> {
26 pub name: Arc<str>,
27 pub(super) format: ColumnFormatFn<T>,
28 pub(super) filter: bool,
30}
31
32impl<T> Column<T> {
33 pub fn new_boxed(name: impl Into<Arc<str>>, format: ColumnFormatFn<T>) -> Self {
34 Self {
35 name: name.into(),
36 format,
37 filter: true,
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) -> Text<'a> + SSS,
44 {
45 Self {
46 name: name.into(),
47 format: Box::new(f),
48 filter: true,
49 }
50 }
51
52 pub fn without_filtering(mut self) -> Self {
54 self.filter = false;
55 self
56 }
57
58 pub fn format<'a>(&self, item: &'a T) -> Text<'a> {
59 (self.format)(item)
60 }
61
62 pub fn format_text<'a>(&self, item: &'a T) -> Cow<'a, str> {
64 Cow::Owned(text_to_string(&(self.format)(item)))
65 }
66}
67
68pub struct Worker<T>
72where
73 T: SSS,
74{
75 pub nucleo: nucleo::Nucleo<T>,
77 pub query: PickerQuery,
79 pub col_indices_buffer: Vec<u32>,
82 pub columns: Arc<[Column<T>]>,
83
84 pub(super) version: Arc<AtomicU32>,
86 column_options: Vec<ColumnOptions>,
88}
89
90bitflags! {
96 #[derive(Default, Clone, Debug)]
97 pub struct ColumnOptions: u8 {
98 const Optional = 1 << 0;
99 const OrUseDefault = 1 << 2;
100 }
101}
102
103impl<T> Worker<T>
104where
105 T: SSS,
106{
107 pub fn new(columns: impl IntoIterator<Item = Column<T>>, default_column: usize) -> Self {
109 let columns: Arc<[_]> = columns.into_iter().collect();
110 let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
111
112 let inner = nucleo::Nucleo::new(
113 nucleo::Config::DEFAULT,
114 Arc::new(|| {}),
115 None,
116 matcher_columns,
117 );
118
119 Self {
120 nucleo: inner,
121 col_indices_buffer: Vec::with_capacity(128),
122 query: PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column),
123 column_options: vec![ColumnOptions::default(); columns.len()],
124 columns,
125 version: Arc::new(AtomicU32::new(0)),
126 }
127 }
128
129 #[cfg(feature = "experimental")]
130 pub fn set_column_options(&mut self, index: usize, options: ColumnOptions) {
131 if options.contains(ColumnOptions::Optional) {
132 self.nucleo
133 .pattern
134 .configure_column(index, nucleo::pattern::Variant::Optional)
135 }
136
137 self.column_options[index] = options
138 }
139
140 #[cfg(feature = "experimental")]
141 pub fn reverse_items(&mut self, reverse_items: bool) {
142 self.nucleo.reverse_items(reverse_items);
143 }
144
145 pub fn injector(&self) -> WorkerInjector<T> {
146 WorkerInjector {
147 inner: self.nucleo.injector(),
148 columns: self.columns.clone(),
149 version: self.version.load(atomic::Ordering::Relaxed),
150 picker_version: self.version.clone(),
151 }
152 }
153
154 pub fn find(&mut self, line: &str) {
155 let old_query = self.query.parse(line);
156 if self.query == old_query {
157 return;
158 }
159 for (i, column) in self
160 .columns
161 .iter()
162 .filter(|column| column.filter)
163 .enumerate()
164 {
165 let pattern = self
166 .query
167 .get(&column.name)
168 .map(|s| &**s)
169 .unwrap_or_else(|| {
170 self.column_options[i]
171 .contains(ColumnOptions::OrUseDefault)
172 .then(|| self.query.primary_column_query())
173 .flatten()
174 .unwrap_or_default()
175 });
176
177 let old_pattern = old_query
178 .get(&column.name)
179 .map(|s| &**s)
180 .unwrap_or_else(|| {
181 self.column_options[i]
182 .contains(ColumnOptions::OrUseDefault)
183 .then(|| {
184 let name = self.query.primary_column_name()?;
185 old_query.get(name).map(|s| &**s)
186 })
187 .flatten()
188 .unwrap_or_default()
189 });
190
191 if pattern == old_pattern {
193 continue;
194 }
195 let is_append = pattern.starts_with(old_pattern);
196
197 self.nucleo.pattern.reparse(
198 i,
199 pattern,
200 nucleo::pattern::CaseMatching::Smart,
201 nucleo::pattern::Normalization::Smart,
202 is_append,
203 );
204 }
205 }
206
207 pub fn get_nth(&self, n: u32) -> Option<&T> {
209 self.nucleo
210 .snapshot()
211 .get_matched_item(n)
212 .map(|item| item.data)
213 }
214
215 pub fn new_snapshot(nucleo: &mut nucleo::Nucleo<T>) -> (&nucleo::Snapshot<T>, Status) {
216 let nucleo::Status { changed, running } = nucleo.tick(10);
217 let snapshot = nucleo.snapshot();
218 (
219 snapshot,
220 Status {
221 item_count: snapshot.item_count(),
222 matched_count: snapshot.matched_item_count(),
223 running,
224 changed,
225 },
226 )
227 }
228
229 pub fn raw_results(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator + '_ {
230 let snapshot = self.nucleo.snapshot();
231 snapshot.matched_items(..).map(|item| item.data)
232 }
233
234 pub fn counts(&self) -> (u32, u32) {
236 let snapshot = self.nucleo.snapshot();
237 (snapshot.matched_item_count(), snapshot.item_count())
238 }
239
240 #[cfg(feature = "experimental")]
241 pub fn set_stability(&mut self, threshold: u32) {
242 self.nucleo.set_stability(threshold);
243 }
244
245 #[cfg(feature = "experimental")]
246 pub fn get_stability(&self) -> u32 {
247 self.nucleo.get_stability()
248 }
249
250 pub fn restart(&mut self, clear_snapshot: bool) {
251 self.nucleo.restart(clear_snapshot);
252 }
253}
254
255#[derive(Debug, Default, Clone)]
256pub struct Status {
257 pub item_count: u32,
258 pub matched_count: u32,
259 pub running: bool,
260 pub changed: bool,
261}
262
263#[derive(Debug, thiserror::Error)]
264pub enum WorkerError {
265 #[error("the matcher injector has been shut down")]
266 InjectorShutdown,
267 #[error("{0}")]
268 Custom(&'static str),
269}
270
271pub type WorkerResults<'a, T> = Vec<(Vec<Text<'a>>, &'a T)>;
273
274impl<T: SSS> Worker<T> {
275 pub fn results(
283 &mut self,
284 start: u32,
285 end: u32,
286 width_limits: &[u16],
287 wrap: bool,
288 highlight_style: Style,
289 matcher: &mut nucleo::Matcher,
290 autoscroll: Option<(usize, usize)>,
291 hscroll_offset: i8,
292 ) -> (WorkerResults<'_, T>, Vec<u16>, Status) {
293 let (snapshot, status) = Self::new_snapshot(&mut self.nucleo);
294
295 let mut widths = vec![0u16; self.columns.len()];
296
297 let iter =
298 snapshot.matched_items(start.min(status.matched_count)..end.min(status.matched_count));
299
300 let table = iter
301 .map(|item| {
302 let mut widths = widths.iter_mut();
303
304 let row = self
305 .columns
306 .iter()
307 .enumerate()
308 .zip(width_limits.iter().chain(std::iter::repeat(&u16::MAX)))
309 .map(|((col_idx, column), &width_limit)| {
310 let max_width = widths.next().unwrap();
311 let cell = column.format(item.data);
312
313 if width_limit == 0 {
315 return Text::default();
316 }
317
318 let (cell, width) = if column.filter {
319 render_cell(
320 cell,
321 col_idx,
322 snapshot,
323 &item,
324 matcher,
325 highlight_style,
326 wrap,
327 width_limit,
328 &mut self.col_indices_buffer,
329 autoscroll,
330 hscroll_offset,
331 )
332 } else if wrap {
334 let (cell, wrapped) = wrap_text(cell, width_limit.saturating_sub(1));
335
336 let width = if wrapped {
337 width_limit as usize
338 } else {
339 cell.width()
340 };
341 (cell, width)
342 } else {
343 let width = cell.width();
344 (cell, width)
345 };
346
347 if width as u16 > *max_width {
349 *max_width = width as u16;
350 }
351
352 cell
353 });
354
355 (row.collect(), item.data)
356 })
357 .collect();
358
359 for (w, c) in widths.iter_mut().zip(self.columns.iter()) {
361 let name_width = c.name.width() as u16;
362 if *w != 0 {
363 *w = (*w).max(name_width);
364 }
365 }
366
367 (table, widths, status)
368 }
369
370 pub fn exact_column_match(&mut self, column: &str) -> Option<&T> {
371 let (i, col) = self
372 .columns
373 .iter()
374 .enumerate()
375 .find(|(_, c)| column == &*c.name)?;
376
377 let query = self.query.get(column).map(|s| &**s).or_else(|| {
378 self.column_options[i]
379 .contains(ColumnOptions::OrUseDefault)
380 .then(|| self.query.primary_column_query())
381 .flatten()
382 })?;
383
384 let snapshot = self.nucleo.snapshot();
385 snapshot.matched_items(..).find_map(|item| {
386 let content = col.format_text(item.data);
387 if content.as_str() == query {
388 Some(item.data)
389 } else {
390 None
391 }
392 })
393 }
394
395 pub fn format_with<'a>(&'a self, item: &'a T, col: &str) -> Option<Cow<'a, str>> {
396 self.columns
397 .iter()
398 .find(|c| &*c.name == col)
399 .map(|c| c.format_text(item))
400 }
401}
402
403fn render_cell<T: SSS>(
404 cell: Text<'_>,
405 col_idx: usize,
406 snapshot: &nucleo::Snapshot<T>,
407 item: &nucleo::Item<T>,
408 matcher: &mut nucleo::Matcher,
409 highlight_style: Style,
410 wrap: bool,
411 width_limit: u16,
412 col_indices_buffer: &mut Vec<u32>,
413 autoscroll: Option<(usize, usize)>, hscroll_offset: i8,
415) -> (Text<'static>, usize) {
416 let mut cell_width = 0;
417 let mut wrapped = false;
418
419 let indices_buffer = col_indices_buffer;
421 indices_buffer.clear();
422 snapshot.pattern().column_pattern(col_idx).indices(
423 item.matcher_columns[col_idx].slice(..),
424 matcher,
425 indices_buffer,
426 );
427 indices_buffer.sort_unstable();
428 indices_buffer.dedup();
429 let mut indices = indices_buffer.drain(..);
430
431 let mut lines = vec![];
432 let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
433 let mut grapheme_idx = 0u32;
434
435 for line in &cell {
436 let mut line_graphemes = Vec::new();
438 let mut first_match_idx = None;
439
440 for span in line {
441 let mut graphemes = span.content.graphemes(true).peekable();
447
448 while let Some(grapheme) = graphemes.next() {
449 let is_match = grapheme_idx == next_highlight_idx;
450
451 let style = if is_match {
452 next_highlight_idx = indices.next().unwrap_or(u32::MAX);
453 span.style.patch(highlight_style)
454 } else {
455 span.style
456 };
457
458 if is_match && first_match_idx.is_none() {
459 first_match_idx = Some(line_graphemes.len());
460 }
461
462 line_graphemes.push((grapheme, style));
463 grapheme_idx += 1;
464 }
465 }
466
467 let mut start_idx;
469
470 if let Some((preserved, context)) = autoscroll
471 && let Some(first_idx) = first_match_idx
472 {
473 start_idx = (first_idx as i32 + hscroll_offset as i32 - context as i32).max(0) as usize;
474
475 let mut tail_width: usize = line_graphemes[start_idx..]
476 .iter()
477 .map(|(g, _)| g.width())
478 .sum();
479
480 let preserved_width = line_graphemes[..preserved.min(line_graphemes.len())]
481 .iter()
482 .map(|(g, _)| g.width())
483 .sum::<usize>();
484
485 while start_idx > preserved {
487 let prev_width = line_graphemes[start_idx - 1].0.width();
488 if tail_width + preserved_width + 1 + prev_width <= width_limit as usize {
489 start_idx -= 1;
490 tail_width += prev_width;
491 } else {
492 break;
493 }
494 }
495
496 if start_idx <= preserved + 1 {
497 start_idx = 0;
498 }
499 } else {
500 start_idx = hscroll_offset.max(0) as usize;
501 }
502
503 let mut current_spans = Vec::new();
505 let mut current_span = String::new();
506 let mut current_style = Style::default();
507 let mut current_width = 0;
508
509 if start_idx > 0 && autoscroll.is_some() {
511 if let Some((preserved, _)) = autoscroll {
512 for (g, s) in line_graphemes.drain(..preserved) {
513 if s != current_style {
514 if !current_span.is_empty() {
515 current_spans.push(Span::styled(current_span, current_style));
516 }
517 current_span = String::new();
518 current_style = s;
519 }
520 current_span.push_str(g);
521 }
522 if !current_span.is_empty() {
523 current_spans.push(Span::styled(current_span, current_style));
524 }
525 start_idx -= preserved;
526 }
527 current_width += current_spans.iter().map(|x| x.width()).sum::<usize>();
528 current_spans.push(hscroll_indicator());
529 current_width += 1;
530
531 current_span = String::new();
532 current_style = Style::default();
533 }
534
535 let full_line_width = (!wrap).then(|| {
536 current_width
537 + line_graphemes[start_idx..]
538 .iter()
539 .map(|(g, _)| g.width())
540 .sum::<usize>()
541 });
542
543 let mut graphemes = line_graphemes.drain(start_idx..);
544
545 while let Some((mut grapheme, mut style)) = graphemes.next() {
546 if current_width + grapheme.width() > width_limit as usize {
547 if !current_span.is_empty() {
548 current_spans.push(Span::styled(current_span, current_style));
549 current_span = String::new();
550 }
551 if wrap {
552 current_spans.push(wrapping_indicator());
553 lines.push(Line::from(take(&mut current_spans)));
554
555 current_width = 0;
556 wrapped = true;
557 } else {
558 break;
559 }
560 } else if current_width + grapheme.width() == width_limit as usize {
561 if wrap {
562 let mut new = grapheme.to_string();
563 if current_style != style {
564 current_spans.push(Span::styled(take(&mut current_span), current_style));
565 current_style = style;
566 };
567 while let Some((grapheme2, style2)) = graphemes.next() {
568 if grapheme2.width() == 0 {
569 new.push_str(grapheme2);
570 } else {
571 if !current_span.is_empty() {
572 current_spans.push(Span::styled(current_span, current_style));
573 }
574 current_spans.push(wrapping_indicator());
575 lines.push(Line::from(take(&mut current_spans)));
576
577 current_span = new.clone(); current_width = grapheme.width();
580 wrapped = true;
581
582 grapheme = grapheme2;
583 style = style2;
584 break; }
586 }
587 if !wrapped {
588 current_span.push_str(&new);
589 current_spans.push(Span::styled(take(&mut current_span), style));
591 current_style = style;
592 current_width += grapheme.width();
593 break;
594 }
595 } else {
596 if style != current_style {
597 if !current_span.is_empty() {
598 current_spans.push(Span::styled(current_span, current_style));
599 }
600 current_span = String::new();
601 current_style = style;
602 }
603 current_span.push_str(grapheme);
604 current_width += grapheme.width();
605 break;
606 }
607 }
608
609 if style != current_style {
611 if !current_span.is_empty() {
612 current_spans.push(Span::styled(current_span, current_style))
613 }
614 current_span = String::new();
615 current_style = style;
616 }
617 current_span.push_str(grapheme);
618 current_width += grapheme.width();
619 }
620
621 current_spans.push(Span::styled(current_span, current_style));
622 lines.push(Line::from(current_spans));
623 cell_width = cell_width.max(full_line_width.unwrap_or(current_width));
624
625 grapheme_idx += 1; }
627
628 (
629 Text::from(lines),
630 if wrapped {
631 width_limit as usize
632 } else {
633 cell_width
634 },
635 )
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use nucleo::{Matcher, Nucleo};
642 use ratatui::style::{Color, Style};
643 use ratatui::text::Text;
644 use std::sync::Arc;
645
646 fn setup_nucleo_mocks(
648 search_query: &str,
649 item_text: &str,
650 ) -> (Nucleo<String>, Matcher, Vec<u32>) {
651 let mut nucleo = Nucleo::<String>::new(nucleo::Config::DEFAULT, Arc::new(|| {}), None, 1);
652
653 let injector = nucleo.injector();
654 injector.push(item_text.to_string(), |item, columns| {
655 columns[0] = item.clone().into();
656 });
657
658 nucleo.pattern.reparse(
659 0,
660 search_query,
661 nucleo::pattern::CaseMatching::Ignore,
662 nucleo::pattern::Normalization::Smart,
663 false,
664 );
665
666 nucleo.tick(10); let matcher = Matcher::default();
669 let buffer = Vec::new();
670
671 (nucleo, matcher, buffer)
672 }
673
674 #[test]
675 fn test_no_scroll_context_renders_normally() {
676 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
677 let snapshot = nucleo.snapshot();
678 let item = snapshot.get_item(0).unwrap();
679
680 let cell = Text::from("hello match world");
681 let highlight = Style::default().fg(Color::Red);
682
683 let (result_text, width) = render_cell(
684 cell,
685 0,
686 &snapshot,
687 &item,
688 &mut matcher,
689 highlight,
690 false,
691 u16::MAX,
692 &mut buffer,
693 None,
694 0,
695 );
696
697 let output_str = text_to_string(&result_text);
698 assert_eq!(output_str, "hello match world");
699 assert_eq!(width, 17);
700 }
701
702 #[test]
703 fn test_scroll_context_cuts_prefix_correctly() {
704 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
705 let snapshot = nucleo.snapshot();
706 let item = snapshot.get_item(0).unwrap();
707
708 let cell = Text::from("hello match world");
709 let highlight = Style::default().fg(Color::Red);
710
711 let (result_text, _) = render_cell(
712 cell,
713 0,
714 &snapshot,
715 &item,
716 &mut matcher,
717 highlight,
718 false,
719 u16::MAX,
720 &mut buffer,
721 Some((0, 2)),
722 0,
723 );
724
725 let output_str = text_to_string(&result_text);
726 assert_eq!(output_str, "hello match world");
727 }
728
729 #[test]
730 fn test_scroll_context_backfills_to_fill_width_limit() {
731 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
746 let snapshot = nucleo.snapshot();
747 let item = snapshot.get_item(0).unwrap();
748
749 let cell = Text::from("abcdefghijmatch");
750 let highlight = Style::default().fg(Color::Red);
751
752 let (result_text, width) = render_cell(
753 cell,
754 0,
755 &snapshot,
756 &item,
757 &mut matcher,
758 highlight,
759 false,
760 10,
761 &mut buffer,
762 Some((0, 1)),
763 0,
764 );
765
766 let output_str = text_to_string(&result_text);
767 assert_eq!(output_str, "…ghijmatch");
768 assert_eq!(width, 10);
769 }
770
771 #[test]
772 fn test_preserved_prefix_and_ellipsis() {
773 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
786 let snapshot = nucleo.snapshot();
787 let item = snapshot.get_item(0).unwrap();
788
789 let cell = Text::from("abcdefghijmatch");
790 let highlight = Style::default().fg(Color::Red);
791
792 let (result_text, width) = render_cell(
793 cell,
794 0,
795 &snapshot,
796 &item,
797 &mut matcher,
798 highlight,
799 false,
800 10,
801 &mut buffer,
802 Some((3, 1)),
803 0,
804 );
805
806 let output_str = text_to_string(&result_text);
807 assert_eq!(output_str, "abc…jmatch");
808 assert_eq!(width, 10);
809 }
810
811 #[test]
812 fn test_wrap() {
813 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefmatch");
814 let snapshot = nucleo.snapshot();
815 let item = snapshot.get_item(0).unwrap();
816
817 let cell = Text::from("abcdefmatch");
818 let highlight = Style::default().fg(Color::Red);
819
820 let (result_text, width) = render_cell(
821 cell,
822 0,
823 &snapshot,
824 &item,
825 &mut matcher,
826 highlight,
827 true,
828 10,
829 &mut buffer,
830 Some((3, 1)),
831 -2,
832 );
833
834 let output_str = text_to_string(&result_text);
835 assert_eq!(output_str, "abcdefmat↵\nch");
836 assert_eq!(width, 10);
837 }
838
839 #[test]
840 fn test_wrap_edge_case_6_chars_width_5() {
841 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("", "123456");
842 let snapshot = nucleo.snapshot();
843 let item = snapshot.get_item(0).unwrap();
844
845 let cell = Text::from("123456");
846 let highlight = Style::default().fg(Color::Red);
847
848 let (result_text, width) = render_cell(
849 cell,
850 0,
851 &snapshot,
852 &item,
853 &mut matcher,
854 highlight,
855 true,
856 5,
857 &mut buffer,
858 None,
859 0,
860 );
861
862 let output_str = text_to_string(&result_text);
863 assert_eq!(output_str, "1234↵\n56");
865 assert_eq!(width, 5);
866 }
867}