1use super::{Line, Span, Style, Text};
5use bitflags::bitflags;
6
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 super::{injector::WorkerInjector, query::PickerQuery};
19use crate::{
20 SSS,
21 nucleo::Render,
22 utils::text::{text_to_string, wrap_text},
23};
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 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 {
55 self.filter = false;
56 self
57 }
58
59 pub(super) fn format<'a>(&self, item: &'a T) -> Text<'a> {
60 (self.format)(item)
61 }
62
63 pub(super) fn format_text<'a>(&self, item: &'a T) -> Cow<'a, str> {
65 Cow::Owned(text_to_string(&(self.format)(item)))
66 }
67}
68
69pub struct Worker<T>
73where
74 T: SSS,
75{
76 pub(crate) nucleo: nucleo::Nucleo<T>,
78 pub(super) query: PickerQuery,
80 pub(super) col_indices_buffer: Vec<u32>,
83 pub(crate) columns: Arc<[Column<T>]>,
84
85 pub(super) version: Arc<AtomicU32>,
87 column_options: Vec<ColumnOptions>,
89}
90
91bitflags! {
97 #[derive(Default, Clone, Debug)]
98 pub struct ColumnOptions: u8 {
99 const Optional = 1 << 0;
100 const OrUseDefault = 1 << 2;
101 }
102}
103
104impl<T> Worker<T>
105where
106 T: SSS,
107{
108 pub fn new(columns: impl IntoIterator<Item = Column<T>>, default_column: usize) -> Self {
110 let columns: Arc<[_]> = columns.into_iter().collect();
111 let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
112
113 let inner = nucleo::Nucleo::new(
114 nucleo::Config::DEFAULT,
115 Arc::new(|| {}),
116 None,
117 matcher_columns,
118 );
119
120 Self {
121 nucleo: inner,
122 col_indices_buffer: Vec::with_capacity(128),
123 query: PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column),
124 column_options: vec![ColumnOptions::default(); columns.len()],
125 columns,
126 version: Arc::new(AtomicU32::new(0)),
127 }
128 }
129
130 #[cfg(feature = "experimental")]
131 pub fn set_column_options(&mut self, index: usize, options: ColumnOptions) {
132 if options.contains(ColumnOptions::Optional) {
133 self.nucleo
134 .pattern
135 .configure_column(index, nucleo::pattern::Variant::Optional)
136 }
137
138 self.column_options[index] = options
139 }
140
141 #[cfg(feature = "experimental")]
142 pub fn reverse_items(&mut self, reverse_items: bool) {
143 self.nucleo.reverse_items(reverse_items);
144 }
145
146 pub fn injector(&self) -> WorkerInjector<T> {
147 WorkerInjector {
148 inner: self.nucleo.injector(),
149 columns: self.columns.clone(),
150 version: self.version.load(atomic::Ordering::Relaxed),
151 picker_version: self.version.clone(),
152 }
153 }
154
155 pub fn find(&mut self, line: &str) {
156 let old_query = self.query.parse(line);
157 if self.query == old_query {
158 return;
159 }
160 for (i, column) in self
161 .columns
162 .iter()
163 .filter(|column| column.filter)
164 .enumerate()
165 {
166 let pattern = self
167 .query
168 .get(&column.name)
169 .map(|s| &**s)
170 .unwrap_or_else(|| {
171 self.column_options[i]
172 .contains(ColumnOptions::OrUseDefault)
173 .then(|| self.query.primary_column_query())
174 .flatten()
175 .unwrap_or_default()
176 });
177
178 let old_pattern = old_query
179 .get(&column.name)
180 .map(|s| &**s)
181 .unwrap_or_else(|| {
182 self.column_options[i]
183 .contains(ColumnOptions::OrUseDefault)
184 .then(|| {
185 let name = self.query.primary_column_name()?;
186 old_query.get(name).map(|s| &**s)
187 })
188 .flatten()
189 .unwrap_or_default()
190 });
191
192 if pattern == old_pattern {
194 continue;
195 }
196 let is_append = pattern.starts_with(old_pattern);
197
198 self.nucleo.pattern.reparse(
199 i,
200 pattern,
201 nucleo::pattern::CaseMatching::Smart,
202 nucleo::pattern::Normalization::Smart,
203 is_append,
204 );
205 }
206 }
207
208 pub fn get_nth(&self, n: u32) -> Option<&T> {
210 self.nucleo
211 .snapshot()
212 .get_matched_item(n)
213 .map(|item| item.data)
214 }
215
216 pub fn new_snapshot(nucleo: &mut nucleo::Nucleo<T>) -> (&nucleo::Snapshot<T>, Status) {
217 let nucleo::Status { changed, running } = nucleo.tick(10);
218 let snapshot = nucleo.snapshot();
219 (
220 snapshot,
221 Status {
222 item_count: snapshot.item_count(),
223 matched_count: snapshot.matched_item_count(),
224 running,
225 changed,
226 },
227 )
228 }
229
230 pub fn raw_results(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator + '_ {
231 let snapshot = self.nucleo.snapshot();
232 snapshot.matched_items(..).map(|item| item.data)
233 }
234
235 pub fn counts(&self) -> (u32, u32) {
237 let snapshot = self.nucleo.snapshot();
238 (snapshot.matched_item_count(), snapshot.item_count())
239 }
240
241 #[cfg(feature = "experimental")]
242 pub fn set_stability(&mut self, threshold: u32) {
243 self.nucleo.set_stability(threshold);
244 }
245
246 #[cfg(feature = "experimental")]
247 pub fn get_stability(&self) -> u32 {
248 self.nucleo.get_stability()
249 }
250
251 pub fn restart(&mut self, clear_snapshot: bool) {
252 self.nucleo.restart(clear_snapshot);
253 }
254}
255
256#[derive(Debug, Default, Clone)]
257pub struct Status {
258 pub item_count: u32,
259 pub matched_count: u32,
260 pub running: bool,
261 pub changed: bool,
262}
263
264#[derive(Debug, thiserror::Error)]
265pub enum WorkerError {
266 #[error("the matcher injector has been shut down")]
267 InjectorShutdown,
268 #[error("{0}")]
269 Custom(&'static str),
270}
271
272pub type WorkerResults<'a, T> = Vec<(Vec<Text<'a>>, &'a T)>;
274
275impl<T: SSS> Worker<T> {
276 pub fn results(
284 &mut self,
285 start: u32,
286 end: u32,
287 width_limits: &[u16],
288 highlight_style: Style,
289 matcher: &mut nucleo::Matcher,
290 match_start_context: Option<usize>,
291 ) -> (WorkerResults<'_, T>, Vec<u16>, Status) {
292 let (snapshot, status) = Self::new_snapshot(&mut self.nucleo);
293
294 let mut widths = vec![0u16; self.columns.len()];
295
296 let iter =
297 snapshot.matched_items(start.min(status.matched_count)..end.min(status.matched_count));
298
299 let table = iter
300 .map(|item| {
301 let mut widths = widths.iter_mut();
302
303 let row = self
304 .columns
305 .iter()
306 .enumerate()
307 .zip(width_limits.iter().chain(std::iter::repeat(&u16::MAX)))
308 .map(|((col_idx, column), &width_limit)| {
309 let max_width = widths.next().unwrap();
310 let cell = column.format(item.data);
311
312 if width_limit == 0 {
314 return Text::default();
315 }
316
317 let (cell, width) = if column.filter {
318 render_cell(
319 cell,
320 col_idx,
321 snapshot,
322 &item,
323 matcher,
324 highlight_style,
325 width_limit,
326 &mut self.col_indices_buffer,
327 match_start_context,
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 match_start_context: Option<usize>,
410) -> (Text<'static>, usize) {
411 let mut cell_width = 0;
412 let mut wrapped = false;
413
414 let indices_buffer = col_indices_buffer;
416 indices_buffer.clear();
417 snapshot.pattern().column_pattern(col_idx).indices(
418 item.matcher_columns[col_idx].slice(..),
419 matcher,
420 indices_buffer,
421 );
422 indices_buffer.sort_unstable();
423 indices_buffer.dedup();
424 let mut indices = indices_buffer.drain(..);
425
426 let mut lines = vec![];
427 let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
428 let mut grapheme_idx = 0u32;
429
430 for line in &cell {
431 let mut line_graphemes = Vec::new();
433 let mut first_match_idx = None;
434
435 for span in line {
436 let mut graphemes = span.content.graphemes(true).peekable();
442
443 while let Some(grapheme) = graphemes.next() {
444 let is_match = grapheme_idx == next_highlight_idx;
445
446 let style = if is_match {
447 next_highlight_idx = indices.next().unwrap_or(u32::MAX);
448 span.style.patch(highlight_style)
449 } else {
450 span.style
451 };
452
453 if is_match && first_match_idx.is_none() {
454 first_match_idx = Some(line_graphemes.len());
455 }
456
457 line_graphemes.push((grapheme, style));
458 grapheme_idx += 1;
459 }
460 }
461
462 let mut start_idx = 0;
464
465 if let (Some(n), Some(first_idx)) = (match_start_context, first_match_idx) {
466 start_idx = first_idx.saturating_sub(n);
467
468 if width_limit != u16::MAX {
470 let mut tail_width: usize = line_graphemes[start_idx..]
471 .iter()
472 .map(|(g, _)| g.width())
473 .sum();
474
475 while start_idx > 0 {
477 let prev_width = line_graphemes[start_idx - 1].0.width();
478 if tail_width + prev_width <= width_limit as usize {
479 start_idx -= 1;
480 tail_width += prev_width;
481 } else {
482 break;
483 }
484 }
485 }
486 }
487
488 let mut current_spans = Vec::new();
490 let mut current_span = String::new();
491 let mut current_style = Style::default();
492 let mut current_width = 0;
493
494 let mut graphemes = line_graphemes[start_idx..].iter().peekable();
495
496 while let Some(&(grapheme, style)) = graphemes.next() {
497 let grapheme_width = grapheme.width();
498
499 if width_limit != u16::MAX {
500 if current_width + grapheme_width > (width_limit - 1) as usize && {
501 grapheme_width > 1 || graphemes.peek().is_some()
502 } {
503 if !current_span.is_empty() {
504 current_spans.push(Span::styled(current_span, current_style));
505 }
506 current_spans.push(Span::styled(
507 "↵",
508 Style::default().add_modifier(Modifier::DIM),
509 ));
510 lines.push(Line::from(current_spans));
511
512 current_spans = Vec::new();
513 current_span = String::new();
514 current_width = 0;
515 wrapped = true;
516 }
517 }
518
519 if style != current_style {
520 if !current_span.is_empty() {
521 current_spans.push(Span::styled(current_span, current_style))
522 }
523 current_span = String::new();
524 current_style = style;
525 }
526 current_span.push_str(grapheme);
527 current_width += grapheme_width;
528 }
529
530 current_spans.push(Span::styled(current_span, current_style));
531 lines.push(Line::from(current_spans));
532 cell_width = cell_width.max(current_width);
533
534 grapheme_idx += 1; }
536
537 (
538 Text::from(lines),
539 if wrapped {
540 width_limit as usize
541 } else {
542 cell_width
543 },
544 )
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550 use nucleo::{Matcher, Nucleo};
551 use ratatui::style::{Color, Style};
552 use ratatui::text::Text;
553 use std::sync::Arc;
554
555 fn setup_nucleo_mocks(
557 search_query: &str,
558 item_text: &str,
559 ) -> (Nucleo<String>, Matcher, Vec<u32>) {
560 let mut nucleo = Nucleo::<String>::new(nucleo::Config::DEFAULT, Arc::new(|| {}), None, 1);
561
562 let injector = nucleo.injector();
563 injector.push(item_text.to_string(), |item, columns| {
564 columns[0] = item.clone().into();
565 });
566
567 nucleo.pattern.reparse(
568 0,
569 search_query,
570 nucleo::pattern::CaseMatching::Ignore,
571 nucleo::pattern::Normalization::Smart,
572 false,
573 );
574
575 nucleo.tick(10); let matcher = Matcher::default();
578 let buffer = Vec::new();
579
580 (nucleo, matcher, buffer)
581 }
582
583 #[test]
584 fn test_no_scroll_context_renders_normally() {
585 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
586 let snapshot = nucleo.snapshot();
587 let item = snapshot.get_item(0).unwrap();
588
589 let cell = Text::from("hello match world");
590 let highlight = Style::default().fg(Color::Red);
591
592 let (result_text, width) = render_cell(
593 cell,
594 0,
595 &snapshot,
596 &item,
597 &mut matcher,
598 highlight,
599 u16::MAX,
600 &mut buffer,
601 None,
602 );
603
604 let output_str = text_to_string(&result_text);
605 assert_eq!(output_str, "hello match world");
606 assert_eq!(width, 17);
607 }
608
609 #[test]
610 fn test_scroll_context_cuts_prefix_correctly() {
611 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
614 let snapshot = nucleo.snapshot();
615 let item = snapshot.get_item(0).unwrap();
616
617 let cell = Text::from("hello match world");
618 let highlight = Style::default().fg(Color::Red);
619
620 let (result_text, _) = render_cell(
622 cell,
623 0,
624 &snapshot,
625 &item,
626 &mut matcher,
627 highlight,
628 u16::MAX,
629 &mut buffer,
630 Some(2),
631 );
632
633 let output_str = text_to_string(&result_text);
634 assert_eq!(output_str, "o match world");
635 }
636
637 #[test]
638 fn test_scroll_context_backfills_to_fill_width_limit() {
639 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
647 let snapshot = nucleo.snapshot();
648 let item = snapshot.get_item(0).unwrap();
649
650 let cell = Text::from("abcdefghijmatch");
651 let highlight = Style::default().fg(Color::Red);
652
653 let (result_text, width) = render_cell(
654 cell,
655 0,
656 &snapshot,
657 &item,
658 &mut matcher,
659 highlight,
660 10,
661 &mut buffer,
662 Some(1),
663 );
664
665 let output_str = text_to_string(&result_text);
666 assert_eq!(output_str, "fghijmatch");
667 assert_eq!(width, 10);
668 }
669}