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 config::AutoscrollSettings,
21 nucleo::Render,
22 utils::text::{
23 hscroll_indicator, text_to_string, truncation_indicator, wrap_text, wrapping_indicator,
24 },
25};
26
27type ColumnFormatFn<T> = Box<dyn for<'a> Fn(&'a T) -> Text<'a> + Send + Sync>;
28pub struct Column<T> {
29 pub name: Arc<str>,
30 pub(super) format: ColumnFormatFn<T>,
31 pub(super) filter: bool,
33}
34
35impl<T> Column<T> {
36 pub fn new_boxed(name: impl Into<Arc<str>>, format: ColumnFormatFn<T>) -> Self {
37 Self {
38 name: name.into(),
39 format,
40 filter: true,
41 }
42 }
43
44 pub fn new<F>(name: impl Into<Arc<str>>, f: F) -> Self
45 where
46 F: for<'a> Fn(&'a T) -> Text<'a> + SSS,
47 {
48 Self {
49 name: name.into(),
50 format: Box::new(f),
51 filter: true,
52 }
53 }
54
55 pub fn without_filtering(mut self) -> Self {
57 self.filter = false;
58 self
59 }
60
61 pub fn format<'a>(&self, item: &'a T) -> Text<'a> {
62 (self.format)(item)
63 }
64
65 pub fn format_text<'a>(&self, item: &'a T) -> Cow<'a, str> {
67 Cow::Owned(text_to_string(&(self.format)(item)))
68 }
69}
70
71pub struct Worker<T>
75where
76 T: SSS,
77{
78 pub nucleo: nucleo::Nucleo<T>,
80 pub query: PickerQuery,
82 pub col_indices_buffer: Vec<u32>,
85 pub columns: Arc<[Column<T>]>,
86
87 pub(super) version: Arc<AtomicU32>,
89 column_options: Vec<ColumnOptions>,
91}
92
93bitflags! {
99 #[derive(Default, Clone, Debug)]
100 pub struct ColumnOptions: u8 {
101 const Optional = 1 << 0;
102 const OrUseDefault = 1 << 2;
103 }
104}
105
106impl<T> Worker<T>
107where
108 T: SSS,
109{
110 pub fn new(columns: impl IntoIterator<Item = Column<T>>, default_column: usize) -> Self {
112 let columns: Arc<[_]> = columns.into_iter().collect();
113 let matcher_columns = columns.iter().filter(|col| col.filter).count() as u32;
114
115 let inner = nucleo::Nucleo::new(
116 nucleo::Config::DEFAULT,
117 Arc::new(|| {}),
118 None,
119 matcher_columns,
120 );
121
122 Self {
123 nucleo: inner,
124 col_indices_buffer: Vec::with_capacity(128),
125 query: PickerQuery::new(columns.iter().map(|col| &col.name).cloned(), default_column),
126 column_options: vec![ColumnOptions::default(); columns.len()],
127 columns,
128 version: Arc::new(AtomicU32::new(0)),
129 }
130 }
131
132 #[cfg(feature = "experimental")]
133 pub fn set_column_options(&mut self, index: usize, options: ColumnOptions) {
134 if options.contains(ColumnOptions::Optional) {
135 self.nucleo
136 .pattern
137 .configure_column(index, nucleo::pattern::Variant::Optional)
138 }
139
140 self.column_options[index] = options
141 }
142
143 #[cfg(feature = "experimental")]
144 pub fn reverse_items(&mut self, reverse_items: bool) {
145 self.nucleo.reverse_items(reverse_items);
146 }
147
148 pub fn injector(&self) -> WorkerInjector<T> {
149 WorkerInjector {
150 inner: self.nucleo.injector(),
151 columns: self.columns.clone(),
152 version: self.version.load(atomic::Ordering::Relaxed),
153 picker_version: self.version.clone(),
154 }
155 }
156
157 pub fn find(&mut self, line: &str) {
158 let old_query = self.query.parse(line);
159 if self.query == old_query {
160 return;
161 }
162 for (i, column) in self
163 .columns
164 .iter()
165 .filter(|column| column.filter)
166 .enumerate()
167 {
168 let pattern = self
169 .query
170 .get(&column.name)
171 .map(|s| &**s)
172 .unwrap_or_else(|| {
173 self.column_options[i]
174 .contains(ColumnOptions::OrUseDefault)
175 .then(|| self.query.primary_column_query())
176 .flatten()
177 .unwrap_or_default()
178 });
179
180 let old_pattern = old_query
181 .get(&column.name)
182 .map(|s| &**s)
183 .unwrap_or_else(|| {
184 self.column_options[i]
185 .contains(ColumnOptions::OrUseDefault)
186 .then(|| {
187 let name = self.query.primary_column_name()?;
188 old_query.get(name).map(|s| &**s)
189 })
190 .flatten()
191 .unwrap_or_default()
192 });
193
194 if pattern == old_pattern {
196 continue;
197 }
198 let is_append = pattern.starts_with(old_pattern);
199
200 self.nucleo.pattern.reparse(
201 i,
202 pattern,
203 nucleo::pattern::CaseMatching::Smart,
204 nucleo::pattern::Normalization::Smart,
205 is_append,
206 );
207 }
208 }
209
210 pub fn get_nth(&self, n: u32) -> Option<&T> {
212 self.nucleo
213 .snapshot()
214 .get_matched_item(n)
215 .map(|item| item.data)
216 }
217
218 pub fn new_snapshot(nucleo: &mut nucleo::Nucleo<T>) -> (&nucleo::Snapshot<T>, Status) {
219 let nucleo::Status { changed, running } = nucleo.tick(10);
220 let snapshot = nucleo.snapshot();
221 (
222 snapshot,
223 Status {
224 item_count: snapshot.item_count(),
225 matched_count: snapshot.matched_item_count(),
226 running,
227 changed,
228 },
229 )
230 }
231
232 pub fn raw_results(&self) -> impl ExactSizeIterator<Item = &T> + DoubleEndedIterator + '_ {
233 let snapshot = self.nucleo.snapshot();
234 snapshot.matched_items(..).map(|item| item.data)
235 }
236
237 pub fn counts(&self) -> (u32, u32) {
239 let snapshot = self.nucleo.snapshot();
240 (snapshot.matched_item_count(), snapshot.item_count())
241 }
242
243 #[cfg(feature = "experimental")]
244 pub fn set_stability(&mut self, threshold: u32) {
245 self.nucleo.set_stability(threshold);
246 }
247
248 #[cfg(feature = "experimental")]
249 pub fn get_stability(&self) -> u32 {
250 self.nucleo.get_stability()
251 }
252
253 pub fn restart(&mut self, clear_snapshot: bool) {
254 self.nucleo.restart(clear_snapshot);
255 }
256}
257
258#[derive(Debug, Default, Clone)]
259pub struct Status {
260 pub item_count: u32,
261 pub matched_count: u32,
262 pub running: bool,
263 pub changed: bool,
264}
265
266#[derive(Debug, thiserror::Error)]
267pub enum WorkerError {
268 #[error("the matcher injector has been shut down")]
269 InjectorShutdown,
270 #[error("{0}")]
271 Custom(&'static str),
272}
273
274pub type WorkerResults<'a, T> = Vec<(Vec<Text<'a>>, &'a T)>;
276
277impl<T: SSS> Worker<T> {
278 pub fn results(
286 &mut self,
287 start: u32,
288 end: u32,
289 width_limits: &[u16],
290 wrap: bool,
291 max_height: usize,
292 highlight_style: Style,
293 matcher: &mut nucleo::Matcher,
294 autoscroll: AutoscrollSettings,
295 hscroll_offset: i8,
296 vscroll: (u8, bool),
297 show_skipped: bool,
298 ) -> (WorkerResults<'_, T>, Vec<u16>, Status) {
299 let (snapshot, status) = Self::new_snapshot(&mut self.nucleo);
300
301 let mut widths = vec![0u16; self.columns.len()];
302
303 let iter =
304 snapshot.matched_items(start.min(status.matched_count)..end.min(status.matched_count));
305
306 let (vscroll_offset, stacked) = vscroll;
307
308 let table = iter
309 .filter_map(|item| {
310 let mut row = vec![];
311
312 let mut to_skip = vscroll_offset as usize;
313 let mut skip = !show_skipped;
314 for c in self.columns.iter() {
315 let mut t = c.format(item.data);
316 if stacked {
317 if to_skip >= t.height() {
318 to_skip -= t.height();
319 t.lines.clear();
320 } else {
321 skip = false;
322 t.lines.drain(..to_skip);
323 to_skip = 0;
324 if max_height > 0 && t.height() > max_height {
325 t.lines.truncate(max_height);
326 if let Some(last_line) = t.lines.last_mut() {
327 last_line.spans.push(truncation_indicator());
328 }
329 }
330 }
331 } else {
332 if t.height() > to_skip {
333 skip = false;
334 }
335 t.lines.drain(..to_skip);
336 if max_height > 0 && t.height() > max_height {
337 t.lines.truncate(max_height);
338 if let Some(last_line) = t.lines.last_mut() {
339 last_line.spans.push(truncation_indicator());
340 }
341 }
342 }
343 row.push(t);
344 }
345 if skip {
346 return None;
347 }
348
349 let row: Vec<Text> = row
350 .into_iter()
351 .enumerate()
352 .zip(width_limits.iter().chain(std::iter::repeat(&u16::MAX)))
353 .map(|((col_idx, cell), &width_limit)| {
354 let column = &self.columns[col_idx];
355
356 let (cell, _) = if width_limit == 0 {
357 (Default::default(), if wrap { 1 } else { cell.width() })
358 } else if column.filter {
359 render_cell(
360 cell,
361 col_idx,
362 snapshot,
363 &item,
364 matcher,
365 highlight_style,
366 wrap,
367 width_limit,
368 &mut self.col_indices_buffer,
369 autoscroll,
370 hscroll_offset,
371 )
372 } else if wrap {
373 let (cell, wrapped) = wrap_text(cell, width_limit);
374
375 let width = if wrapped {
376 width_limit as usize
377 } else {
378 cell.width()
379 };
380 (cell, width)
381 } else {
382 let width = cell.width();
383 (cell, width)
384 };
385
386 cell
387 })
388 .collect();
389
390 for (w, cell) in widths.iter_mut().zip(&row) {
392 let width = cell.width() as u16;
393 if width > *w {
394 *w = width;
395 }
396 }
397
398 Some((row, item.data))
399 })
400 .collect();
401
402 for (w, c) in widths.iter_mut().zip(self.columns.iter()) {
404 let name_width = c.name.width() as u16;
405 if *w != 0 {
406 *w = (*w).max(name_width);
407 }
408 }
409
410 (table, widths, status)
411 }
412
413 pub fn exact_column_match(&mut self, column: &str) -> Option<&T> {
414 let (i, col) = self
415 .columns
416 .iter()
417 .enumerate()
418 .find(|(_, c)| column == &*c.name)?;
419
420 let query = self.query.get(column).map(|s| &**s).or_else(|| {
421 self.column_options[i]
422 .contains(ColumnOptions::OrUseDefault)
423 .then(|| self.query.primary_column_query())
424 .flatten()
425 })?;
426
427 let snapshot = self.nucleo.snapshot();
428 snapshot.matched_items(..).find_map(|item| {
429 let content = col.format_text(item.data);
430 if content.as_str() == query {
431 Some(item.data)
432 } else {
433 None
434 }
435 })
436 }
437
438 pub fn format_with<'a>(&'a self, item: &'a T, col: &str) -> Option<Cow<'a, str>> {
439 self.columns
440 .iter()
441 .find(|c| &*c.name == col)
442 .map(|c| c.format_text(item))
443 }
444}
445
446fn render_cell<T: SSS>(
447 cell: Text<'_>,
448 col_idx: usize,
449 snapshot: &nucleo::Snapshot<T>,
450 item: &nucleo::Item<T>,
451 matcher: &mut nucleo::Matcher,
452 highlight_style: Style,
453 wrap: bool,
454 width_limit: u16,
455 col_indices_buffer: &mut Vec<u32>,
456 mut autoscroll: AutoscrollSettings,
457 hscroll_offset: i8,
458) -> (Text<'static>, usize) {
459 autoscroll.end &= !wrap;
461
462 let mut cell_width = 0;
463 let mut wrapped = false;
464
465 let indices_buffer = col_indices_buffer;
467 indices_buffer.clear();
468 snapshot.pattern().column_pattern(col_idx).indices(
469 item.matcher_columns[col_idx].slice(..),
470 matcher,
471 indices_buffer,
472 );
473 indices_buffer.sort_unstable();
474 indices_buffer.dedup();
475 let mut indices = indices_buffer.drain(..);
476
477 let mut lines = vec![];
478 let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
479 let mut grapheme_idx = 0u32;
480
481 let mut line_graphemes = Vec::new();
482
483 for line in &cell {
484 line_graphemes.clear();
486 let mut match_idx = None;
487
488 for span in line {
489 for grapheme in span.content.graphemes(true) {
495 let is_match = grapheme_idx == next_highlight_idx;
496
497 let style = if is_match {
498 next_highlight_idx = indices.next().unwrap_or(u32::MAX);
499 span.style.patch(highlight_style)
500 } else {
501 span.style
502 };
503
504 if is_match && (autoscroll.end || match_idx.is_none()) {
505 match_idx = Some(line_graphemes.len());
506 }
507
508 line_graphemes.push((grapheme, style));
509 grapheme_idx += 1;
510 }
511 }
512
513 let mut i; if autoscroll.enabled && autoscroll.end {
517 i = match_idx.unwrap_or(line_graphemes.len());
518
519 let target_width = if let Some(x) = match_idx {
520 (width_limit as usize)
521 .saturating_sub(autoscroll.context.min(line_graphemes.len() - x - 1))
522 } else {
523 width_limit as usize
524 }
525 .saturating_sub(1);
526
527 let mut current_width = 0;
528
529 while i > 0 {
530 let w = line_graphemes[i - 1].0.width();
531 if current_width + w > target_width {
532 break;
533 }
534 i -= 1;
535 current_width += w;
536 }
537 if i > 1 {
538 i += 1;
539 } else {
540 i = 0;
541 }
542 } else if autoscroll.enabled
543 && let Some(m_idx) = match_idx
544 {
545 i = (m_idx as i32 + hscroll_offset as i32 - autoscroll.context as i32).max(0) as usize;
546
547 let mut tail_width: usize = line_graphemes[i..].iter().map(|(g, _)| g.width()).sum();
548
549 let preserved_width = line_graphemes
550 [..autoscroll.initial_preserved.min(line_graphemes.len())]
551 .iter()
552 .map(|(g, _)| g.width())
553 .sum::<usize>();
554
555 while i > autoscroll.initial_preserved {
557 let prev_width = line_graphemes[i - 1].0.width();
558 if tail_width + preserved_width + 1 + prev_width <= width_limit as usize {
559 i -= 1;
560 tail_width += prev_width;
561 } else {
562 break;
563 }
564 }
565
566 if i <= autoscroll.initial_preserved + 1 {
567 i = 0;
568 }
569 } else {
570 i = hscroll_offset.max(0) as usize;
571 };
572
573 let mut current_spans = Vec::new();
575 let mut current_span = String::new();
576 let mut current_style = Style::default();
577 let mut current_width = 0;
578
579 if i > 0 && autoscroll.enabled {
581 let preserved = autoscroll.initial_preserved;
582 for (g, s) in line_graphemes.drain(..preserved) {
583 if s != current_style {
584 if !current_span.is_empty() {
585 current_spans.push(Span::styled(current_span, current_style));
586 }
587 current_span = String::new();
588 current_style = s;
589 }
590 current_span.push_str(g);
591 }
592 if !current_span.is_empty() {
593 current_spans.push(Span::styled(current_span, current_style));
594 }
595 i -= preserved;
596
597 current_width += current_spans.iter().map(|x| x.width()).sum::<usize>();
598 current_spans.push(hscroll_indicator());
599 current_width += 1;
600
601 current_span = String::new();
602 current_style = Style::default();
603 }
604
605 let full_line_width = (!wrap).then(|| {
606 current_width
607 + line_graphemes[i..]
608 .iter()
609 .map(|(g, _)| g.width())
610 .sum::<usize>()
611 });
612
613 let mut graphemes = line_graphemes.drain(i..);
614
615 while let Some((mut grapheme, mut style)) = graphemes.next() {
616 if current_width + grapheme.width() > width_limit as usize {
617 if !current_span.is_empty() {
618 current_spans.push(Span::styled(current_span, current_style));
619 current_span = String::new();
620 }
621 if wrap {
622 current_spans.push(wrapping_indicator());
623 lines.push(Line::from(take(&mut current_spans)));
624
625 current_width = 0;
626 wrapped = true;
627 } else {
628 break;
629 }
630 } else if current_width + grapheme.width() == width_limit as usize {
631 if wrap {
632 let mut new = grapheme.to_string();
633 if current_style != style {
634 current_spans.push(Span::styled(take(&mut current_span), current_style));
635 current_style = style;
636 };
637 while let Some((grapheme2, style2)) = graphemes.next() {
638 if grapheme2.width() == 0 {
639 new.push_str(grapheme2);
640 } else {
641 if !current_span.is_empty() {
642 current_spans.push(Span::styled(current_span, current_style));
643 }
644 current_spans.push(wrapping_indicator());
645 lines.push(Line::from(take(&mut current_spans)));
646
647 current_span = new.clone(); current_width = grapheme.width();
650 wrapped = true;
651
652 grapheme = grapheme2;
653 style = style2;
654 break; }
656 }
657 if !wrapped {
658 current_span.push_str(&new);
659 current_spans.push(Span::styled(take(&mut current_span), style));
661 current_style = style;
662 current_width += grapheme.width();
663 break;
664 }
665 } else {
666 if style != current_style {
667 if !current_span.is_empty() {
668 current_spans.push(Span::styled(current_span, current_style));
669 }
670 current_span = String::new();
671 current_style = style;
672 }
673 current_span.push_str(grapheme);
674 current_width += grapheme.width();
675 break;
676 }
677 }
678
679 if style != current_style {
681 if !current_span.is_empty() {
682 current_spans.push(Span::styled(current_span, current_style))
683 }
684 current_span = String::new();
685 current_style = style;
686 }
687 current_span.push_str(grapheme);
688 current_width += grapheme.width();
689 }
690
691 current_spans.push(Span::styled(current_span, current_style));
692 lines.push(Line::from(current_spans));
693 cell_width = cell_width.max(full_line_width.unwrap_or(current_width));
694
695 grapheme_idx += 1; }
697
698 (
699 Text::from(lines),
700 if wrapped {
701 width_limit as usize
702 } else {
703 cell_width
704 },
705 )
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use nucleo::{Matcher, Nucleo};
712 use ratatui::style::{Color, Style};
713 use ratatui::text::Text;
714 use std::sync::Arc;
715
716 fn setup_nucleo_mocks(
718 search_query: &str,
719 item_text: &str,
720 ) -> (Nucleo<String>, Matcher, Vec<u32>) {
721 let mut nucleo = Nucleo::<String>::new(nucleo::Config::DEFAULT, Arc::new(|| {}), None, 1);
722
723 let injector = nucleo.injector();
724 injector.push(item_text.to_string(), |item, columns| {
725 columns[0] = item.clone().into();
726 });
727
728 nucleo.pattern.reparse(
729 0,
730 search_query,
731 nucleo::pattern::CaseMatching::Ignore,
732 nucleo::pattern::Normalization::Smart,
733 false,
734 );
735
736 nucleo.tick(10); let matcher = Matcher::default();
739 let buffer = Vec::new();
740
741 (nucleo, matcher, buffer)
742 }
743
744 #[test]
745 fn test_no_scroll_context_renders_normally() {
746 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
747 let snapshot = nucleo.snapshot();
748 let item = snapshot.get_item(0).unwrap();
749
750 let cell = Text::from("hello match world");
751 let highlight = Style::default().fg(Color::Red);
752
753 let (result_text, width) = render_cell(
754 cell,
755 0,
756 &snapshot,
757 &item,
758 &mut matcher,
759 highlight,
760 false,
761 u16::MAX,
762 &mut buffer,
763 AutoscrollSettings {
764 enabled: false,
765 ..Default::default()
766 },
767 0,
768 );
769
770 let output_str = text_to_string(&result_text);
771 assert_eq!(output_str, "hello match world");
772 assert_eq!(width, 17);
773 }
774
775 #[test]
776 fn test_scroll_context_cuts_prefix_correctly() {
777 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "hello match world");
778 let snapshot = nucleo.snapshot();
779 let item = snapshot.get_item(0).unwrap();
780
781 let cell = Text::from("hello match world");
782 let highlight = Style::default().fg(Color::Red);
783
784 let (result_text, _) = render_cell(
785 cell,
786 0,
787 &snapshot,
788 &item,
789 &mut matcher,
790 highlight,
791 false,
792 u16::MAX,
793 &mut buffer,
794 AutoscrollSettings {
795 initial_preserved: 0,
796 context: 2,
797 ..Default::default()
798 },
799 0,
800 );
801
802 let output_str = text_to_string(&result_text);
803 assert_eq!(output_str, "hello match world");
804 }
805
806 #[test]
807 fn test_scroll_context_backfills_to_fill_width_limit() {
808 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
823 let snapshot = nucleo.snapshot();
824 let item = snapshot.get_item(0).unwrap();
825
826 let cell = Text::from("abcdefghijmatch");
827 let highlight = Style::default().fg(Color::Red);
828
829 let (result_text, width) = render_cell(
830 cell,
831 0,
832 &snapshot,
833 &item,
834 &mut matcher,
835 highlight,
836 false,
837 10,
838 &mut buffer,
839 AutoscrollSettings {
840 initial_preserved: 0,
841 context: 1,
842 ..Default::default()
843 },
844 0,
845 );
846
847 let output_str = text_to_string(&result_text);
848 assert_eq!(output_str, "…ghijmatch");
849 assert_eq!(width, 10);
850 }
851
852 #[test]
853 fn test_preserved_prefix_and_ellipsis() {
854 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
867 let snapshot = nucleo.snapshot();
868 let item = snapshot.get_item(0).unwrap();
869
870 let cell = Text::from("abcdefghijmatch");
871 let highlight = Style::default().fg(Color::Red);
872
873 let (result_text, width) = render_cell(
874 cell,
875 0,
876 &snapshot,
877 &item,
878 &mut matcher,
879 highlight,
880 false,
881 10,
882 &mut buffer,
883 AutoscrollSettings {
884 initial_preserved: 3,
885 context: 1,
886 ..Default::default()
887 },
888 0,
889 );
890
891 let output_str = text_to_string(&result_text);
892 assert_eq!(output_str, "abc…jmatch");
893 assert_eq!(width, 10);
894 }
895
896 #[test]
897 fn test_wrap() {
898 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefmatch");
899 let snapshot = nucleo.snapshot();
900 let item = snapshot.get_item(0).unwrap();
901
902 let cell = Text::from("abcdefmatch");
903 let highlight = Style::default().fg(Color::Red);
904
905 let (result_text, width) = render_cell(
906 cell,
907 0,
908 &snapshot,
909 &item,
910 &mut matcher,
911 highlight,
912 true,
913 10,
914 &mut buffer,
915 AutoscrollSettings {
916 initial_preserved: 3,
917 context: 1,
918 ..Default::default()
919 },
920 -2,
921 );
922
923 let output_str = text_to_string(&result_text);
924 assert_eq!(output_str, "abcdefmat↵\nch");
925 assert_eq!(width, 10);
926 }
927
928 #[test]
929 fn test_wrap_edge_case_6_chars_width_5() {
930 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("", "123456");
931 let snapshot = nucleo.snapshot();
932 let item = snapshot.get_item(0).unwrap();
933
934 let cell = Text::from("123456");
935 let highlight = Style::default().fg(Color::Red);
936
937 let (result_text, width) = render_cell(
938 cell,
939 0,
940 &snapshot,
941 &item,
942 &mut matcher,
943 highlight,
944 true,
945 5,
946 &mut buffer,
947 AutoscrollSettings {
948 enabled: false,
949 ..Default::default()
950 },
951 0,
952 );
953
954 let output_str = text_to_string(&result_text);
955 assert_eq!(output_str, "1234↵\n56");
957 assert_eq!(width, 5);
958 }
959
960 #[test]
961 fn test_autoscroll_end() {
962 let (nucleo, mut matcher, mut buffer) = setup_nucleo_mocks("match", "abcdefghijmatch");
963 let snapshot = nucleo.snapshot();
964 let item = snapshot.get_item(0).unwrap();
965
966 let cell = Text::from("abcdefghijmatch");
967 let highlight = Style::default().fg(Color::Red);
968
969 let (result_text, width) = render_cell(
970 cell,
971 0,
972 &snapshot,
973 &item,
974 &mut matcher,
975 highlight,
976 false,
977 10,
978 &mut buffer,
979 AutoscrollSettings {
980 end: true,
981 context: 4,
982 ..Default::default()
983 },
984 0,
985 );
986
987 let output_str = text_to_string(&result_text);
988 assert_eq!(output_str, "…ghijmatch");
989 assert_eq!(width, 10);
990 }
991}