1use super::{Line, Span, Style, Text};
5use bitflags::bitflags;
6use std::{
7 borrow::Cow,
8 sync::{
9 Arc,
10 atomic::{self, AtomicU32},
11 },
12};
13use unicode_segmentation::UnicodeSegmentation;
14use unicode_width::UnicodeWidthStr;
15
16use super::{injector::WorkerInjector, query::PickerQuery};
17use crate::{
18 SSS,
19 nucleo::Render,
20 utils::text::{hscroll_indicator, text_to_string, wrap_text, wrapping_indicator},
21};
22
23type ColumnFormatFn<T> = Box<dyn for<'a> Fn(&'a T) -> Text<'a> + Send + Sync>;
24pub struct Column<T> {
25 pub name: Arc<str>,
26 pub(super) format: ColumnFormatFn<T>,
27 pub(super) filter: bool,
29}
30
31impl<T> Column<T> {
32 pub fn new_boxed(name: impl Into<Arc<str>>, format: ColumnFormatFn<T>) -> Self {
33 Self {
34 name: name.into(),
35 format,
36 filter: true,
37 }
38 }
39
40 pub fn new<F>(name: impl Into<Arc<str>>, f: F) -> Self
41 where
42 F: for<'a> Fn(&'a T) -> Text<'a> + SSS,
43 {
44 Self {
45 name: name.into(),
46 format: Box::new(f),
47 filter: true,
48 }
49 }
50
51 pub fn without_filtering(mut self) -> Self {
53 self.filter = false;
54 self
55 }
56
57 pub fn format<'a>(&self, item: &'a T) -> Text<'a> {
58 (self.format)(item)
59 }
60
61 pub fn format_text<'a>(&self, item: &'a T) -> Cow<'a, str> {
63 Cow::Owned(text_to_string(&(self.format)(item)))
64 }
65}
66
67pub struct Worker<T>
71where
72 T: SSS,
73{
74 pub nucleo: nucleo::Nucleo<T>,
76 pub query: PickerQuery,
78 pub col_indices_buffer: Vec<u32>,
81 pub columns: Arc<[Column<T>]>,
82
83 pub(super) version: Arc<AtomicU32>,
85 column_options: Vec<ColumnOptions>,
87}
88
89bitflags! {
95 #[derive(Default, Clone, Debug)]
96 pub struct ColumnOptions: u8 {
97 const Optional = 1 << 0;
98 const OrUseDefault = 1 << 2;
99 }
100}
101
102impl<T> Worker<T>
103where
104 T: SSS,
105{
106 pub fn new(columns: impl IntoIterator<Item = Column<T>>, default_column: usize) -> Self {
108 let columns: Arc<[_]> = columns.into_iter().collect();
109 let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
110
111 let inner = nucleo::Nucleo::new(
112 nucleo::Config::DEFAULT,
113 Arc::new(|| {}),
114 None,
115 matcher_columns,
116 );
117
118 Self {
119 nucleo: inner,
120 col_indices_buffer: Vec::with_capacity(128),
121 query: PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column),
122 column_options: vec![ColumnOptions::default(); columns.len()],
123 columns,
124 version: Arc::new(AtomicU32::new(0)),
125 }
126 }
127
128 #[cfg(feature = "experimental")]
129 pub fn set_column_options(&mut self, index: usize, options: ColumnOptions) {
130 if options.contains(ColumnOptions::Optional) {
131 self.nucleo
132 .pattern
133 .configure_column(index, nucleo::pattern::Variant::Optional)
134 }
135
136 self.column_options[index] = options
137 }
138
139 #[cfg(feature = "experimental")]
140 pub fn reverse_items(&mut self, reverse_items: bool) {
141 self.nucleo.reverse_items(reverse_items);
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 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 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 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 #[cfg(feature = "experimental")]
245 pub fn get_stability(&self) -> u32 {
246 self.nucleo.get_stability()
247 }
248
249 pub fn restart(&mut self, clear_snapshot: bool) {
250 self.nucleo.restart(clear_snapshot);
251 }
252}
253
254#[derive(Debug, Default, Clone)]
255pub struct Status {
256 pub item_count: u32,
257 pub matched_count: u32,
258 pub running: bool,
259 pub changed: bool,
260}
261
262#[derive(Debug, thiserror::Error)]
263pub enum WorkerError {
264 #[error("the matcher injector has been shut down")]
265 InjectorShutdown,
266 #[error("{0}")]
267 Custom(&'static str),
268}
269
270pub type WorkerResults<'a, T> = Vec<(Vec<Text<'a>>, &'a T)>;
272
273impl<T: SSS> Worker<T> {
274 pub fn results(
282 &mut self,
283 start: u32,
284 end: u32,
285 width_limits: &[u16],
286 highlight_style: Style,
287 matcher: &mut nucleo::Matcher,
288 autoscroll: Option<(usize, usize)>,
289 hscroll_offset: i8,
290 ) -> (WorkerResults<'_, T>, Vec<u16>, Status) {
291 let (snapshot, status) = Self::new_snapshot(&mut self.nucleo);
292
293 let mut widths = vec![0u16; self.columns.len()];
294
295 let iter =
296 snapshot.matched_items(start.min(status.matched_count)..end.min(status.matched_count));
297
298 let table = iter
299 .map(|item| {
300 let mut widths = widths.iter_mut();
301
302 let row = self
303 .columns
304 .iter()
305 .enumerate()
306 .zip(width_limits.iter().chain(std::iter::repeat(&u16::MAX)))
307 .map(|((col_idx, column), &width_limit)| {
308 let max_width = widths.next().unwrap();
309 let cell = column.format(item.data);
310
311 if width_limit == 0 {
313 return Text::default();
314 }
315
316 let (cell, width) = if column.filter {
317 render_cell(
318 cell,
319 col_idx,
320 snapshot,
321 &item,
322 matcher,
323 highlight_style,
324 width_limit,
325 &mut self.col_indices_buffer,
326 autoscroll,
327 hscroll_offset,
328 )
329 } else if width_limit != u16::MAX {
330 let (cell, wrapped) = wrap_text(cell, width_limit - 1);
331
332 let width = if wrapped {
333 width_limit as usize
334 } else {
335 cell.width()
336 };
337 (cell, width)
338 } else {
339 let width = cell.width();
340 (cell, width)
341 };
342
343 if width as u16 > *max_width {
345 *max_width = width as u16;
346 }
347
348 cell
349 });
350
351 (row.collect(), item.data)
352 })
353 .collect();
354
355 for (w, c) in widths.iter_mut().zip(self.columns.iter()) {
357 let name_width = c.name.width() as u16;
358 if *w != 0 {
359 *w = (*w).max(name_width);
360 }
361 }
362
363 (table, widths, status)
364 }
365
366 pub fn exact_column_match(&mut self, column: &str) -> Option<&T> {
367 let (i, col) = self
368 .columns
369 .iter()
370 .enumerate()
371 .find(|(_, c)| column == &*c.name)?;
372
373 let query = self.query.get(column).map(|s| &**s).or_else(|| {
374 self.column_options[i]
375 .contains(ColumnOptions::OrUseDefault)
376 .then(|| self.query.primary_column_query())
377 .flatten()
378 })?;
379
380 let snapshot = self.nucleo.snapshot();
381 snapshot.matched_items(..).find_map(|item| {
382 let content = col.format_text(item.data);
383 if content.as_str() == query {
384 Some(item.data)
385 } else {
386 None
387 }
388 })
389 }
390
391 pub fn format_with<'a>(&'a self, item: &'a T, col: &str) -> Option<Cow<'a, str>> {
392 self.columns
393 .iter()
394 .find(|c| &*c.name == col)
395 .map(|c| c.format_text(item))
396 }
397}
398
399fn render_cell<T: SSS>(
400 cell: Text<'_>,
402 col_idx: usize,
403 snapshot: &nucleo::Snapshot<T>,
404 item: &nucleo::Item<T>,
405 matcher: &mut nucleo::Matcher,
406 highlight_style: Style,
407 width_limit: u16,
408 col_indices_buffer: &mut Vec<u32>,
409 autoscroll: Option<(usize, usize)>,
410 hscroll_offset: i8,
411) -> (Text<'static>, usize) {
412 let mut cell_width = 0;
413 let mut wrapped = false;
414
415 let indices_buffer = col_indices_buffer;
417 indices_buffer.clear();
418 snapshot.pattern().column_pattern(col_idx).indices(
419 item.matcher_columns[col_idx].slice(..),
420 matcher,
421 indices_buffer,
422 );
423 indices_buffer.sort_unstable();
424 indices_buffer.dedup();
425 let mut indices = indices_buffer.drain(..);
426
427 let mut lines = vec![];
428 let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
429 let mut grapheme_idx = 0u32;
430
431 for line in &cell {
432 let mut line_graphemes = Vec::new();
434 let mut first_match_idx = None;
435
436 for span in line {
437 let mut graphemes = span.content.graphemes(true).peekable();
443
444 while let Some(grapheme) = graphemes.next() {
445 let is_match = grapheme_idx == next_highlight_idx;
446
447 let style = if is_match {
448 next_highlight_idx = indices.next().unwrap_or(u32::MAX);
449 span.style.patch(highlight_style)
450 } else {
451 span.style
452 };
453
454 if is_match && first_match_idx.is_none() {
455 first_match_idx = Some(line_graphemes.len());
456 }
457
458 line_graphemes.push((grapheme, style));
459 grapheme_idx += 1;
460 }
461 }
462
463 let mut start_idx;
465 let mut preserved_prefix = vec![];
466
467 if let Some((preserved, context)) = autoscroll {
468 let first_idx = first_match_idx.unwrap_or(0);
469 start_idx = (first_idx as i32 + hscroll_offset as i32 - context as i32).max(0) as usize;
470
471 if width_limit != u16::MAX {
472 let mut tail_width: usize = line_graphemes[start_idx..]
473 .iter()
474 .map(|(g, _)| g.width())
475 .sum();
476
477 let preserved_width = line_graphemes[..preserved.min(line_graphemes.len())]
478 .iter()
479 .map(|(g, _)| g.width())
480 .sum::<usize>();
481
482 let gap_indicator_width = 1;
483
484 while start_idx > preserved {
486 let prev_width = line_graphemes[start_idx - 1].0.width();
487 if tail_width + preserved_width + gap_indicator_width + prev_width
488 <= width_limit as usize
489 {
490 start_idx -= 1;
491 tail_width += prev_width;
492 } else {
493 break;
494 }
495 }
496 }
497
498 if start_idx <= preserved + 1 {
499 start_idx = 0;
500 } else {
501 preserved_prefix = line_graphemes[..preserved].to_vec();
502 }
503 } else {
504 start_idx = hscroll_offset.max(0) as usize;
505 }
506
507 let mut current_spans = Vec::new();
509 let mut current_span = String::new();
510 let mut current_style = Style::default();
511 let mut current_width = 0;
512
513 if start_idx > 0 && autoscroll.is_some() {
515 if !preserved_prefix.is_empty() {
516 for (g, s) in preserved_prefix {
517 if s != current_style {
518 if !current_span.is_empty() {
519 current_spans.push(Span::styled(current_span, current_style));
520 }
521 current_span = String::new();
522 current_style = s;
523 }
524 current_span.push_str(g);
525 current_width += g.width();
526 }
527 if !current_span.is_empty() {
528 current_spans.push(Span::styled(current_span, current_style));
529 }
530 }
531 current_spans.push(hscroll_indicator());
532 current_width += 1;
533
534 current_span = String::new();
535 current_style = Style::default();
536 }
537
538 let mut graphemes = line_graphemes[start_idx..].iter().peekable();
539
540 while let Some(&(grapheme, style)) = graphemes.next() {
541 let grapheme_width = grapheme.width();
542
543 if width_limit != u16::MAX {
544 if current_width + grapheme_width > (width_limit - 1) as usize && {
545 grapheme_width > 1 || graphemes.peek().is_some()
546 } {
547 if !current_span.is_empty() {
548 current_spans.push(Span::styled(current_span, current_style));
549 }
550 current_spans.push(wrapping_indicator());
551 lines.push(Line::from(current_spans));
552
553 current_spans = Vec::new();
554 current_span = String::new();
555 current_width = 0;
556 wrapped = true;
557 }
558 }
559
560 if style != current_style {
561 if !current_span.is_empty() {
562 current_spans.push(Span::styled(current_span, current_style))
563 }
564 current_span = String::new();
565 current_style = style;
566 }
567 current_span.push_str(grapheme);
568 current_width += grapheme_width;
569 }
570
571 current_spans.push(Span::styled(current_span, current_style));
572 lines.push(Line::from(current_spans));
573 cell_width = cell_width.max(current_width);
574
575 grapheme_idx += 1; }
577
578 (
579 Text::from(lines),
580 if wrapped {
581 width_limit as usize
582 } else {
583 cell_width
584 },
585 )
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use nucleo::{Matcher, Nucleo};
592 use ratatui::style::{Color, Style};
593 use ratatui::text::Text;
594 use std::sync::Arc;
595
596 fn setup_nucleo_mocks(
598 search_query: &str,
599 item_text: &str,
600 ) -> (Nucleo<String>, Matcher, Vec<u32>) {
601 let mut nucleo = Nucleo::<String>::new(nucleo::Config::DEFAULT, Arc::new(|| {}), None, 1);
602
603 let injector = nucleo.injector();
604 injector.push(item_text.to_string(), |item, columns| {
605 columns[0] = item.clone().into();
606 });
607
608 nucleo.pattern.reparse(
609 0,
610 search_query,
611 nucleo::pattern::CaseMatching::Ignore,
612 nucleo::pattern::Normalization::Smart,
613 false,
614 );
615
616 nucleo.tick(10); let matcher = Matcher::default();
619 let buffer = Vec::new();
620
621 (nucleo, matcher, buffer)
622 }
623
624 #[test]
625 fn test_no_scroll_context_renders_normally() {
626 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
627 let snapshot = nucleo.snapshot();
628 let item = snapshot.get_item(0).unwrap();
629
630 let cell = Text::from("hello match world");
631 let highlight = Style::default().fg(Color::Red);
632
633 let (result_text, width) = render_cell(
634 cell,
635 0,
636 &snapshot,
637 &item,
638 &mut matcher,
639 highlight,
640 u16::MAX,
641 &mut buffer,
642 None,
643 0,
644 );
645
646 let output_str = text_to_string(&result_text);
647 assert_eq!(output_str, "hello match world");
648 assert_eq!(width, 17);
649 }
650
651 #[test]
652 fn test_scroll_context_cuts_prefix_correctly() {
653 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
660 let snapshot = nucleo.snapshot();
661 let item = snapshot.get_item(0).unwrap();
662
663 let cell = Text::from("hello match world");
664 let highlight = Style::default().fg(Color::Red);
665
666 let (result_text, _) = render_cell(
668 cell,
669 0,
670 &snapshot,
671 &item,
672 &mut matcher,
673 highlight,
674 u16::MAX,
675 &mut buffer,
676 Some((0, 2)),
677 0,
678 );
679
680 let output_str = text_to_string(&result_text);
681 assert_eq!(output_str, "…o match world");
682 }
683
684 #[test]
685 fn test_scroll_context_backfills_to_fill_width_limit() {
686 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
701 let snapshot = nucleo.snapshot();
702 let item = snapshot.get_item(0).unwrap();
703
704 let cell = Text::from("abcdefghijmatch");
705 let highlight = Style::default().fg(Color::Red);
706
707 let (result_text, width) = render_cell(
708 cell,
709 0,
710 &snapshot,
711 &item,
712 &mut matcher,
713 highlight,
714 10,
715 &mut buffer,
716 Some((0, 1)),
717 0,
718 );
719
720 let output_str = text_to_string(&result_text);
721 assert_eq!(output_str, "…ghijmatch");
722 assert_eq!(width, 10);
723 }
724
725 #[test]
726 fn test_preserved_prefix_and_ellipsis() {
727 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
740 let snapshot = nucleo.snapshot();
741 let item = snapshot.get_item(0).unwrap();
742
743 let cell = Text::from("abcdefghijmatch");
744 let highlight = Style::default().fg(Color::Red);
745
746 let (result_text, width) = render_cell(
747 cell,
748 0,
749 &snapshot,
750 &item,
751 &mut matcher,
752 highlight,
753 10,
754 &mut buffer,
755 Some((3, 1)),
756 0,
757 );
758
759 let output_str = text_to_string(&result_text);
760 assert_eq!(output_str, "abc…jmatch");
761 assert_eq!(width, 10);
762 }
763
764 #[test]
765 fn test_wrap() {
766 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefmatch");
767 let snapshot = nucleo.snapshot();
768 let item = snapshot.get_item(0).unwrap();
769
770 let cell = Text::from("abcdefmatch");
771 let highlight = Style::default().fg(Color::Red);
772
773 let (result_text, width) = render_cell(
774 cell,
775 0,
776 &snapshot,
777 &item,
778 &mut matcher,
779 highlight,
780 10,
781 &mut buffer,
782 Some((3, 1)),
783 -2,
784 );
785
786 let output_str = text_to_string(&result_text);
787 assert_eq!(output_str, "abcdefmat↵\nch");
788 assert_eq!(width, 10);
789 }
790}