1use std::ops::Range;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicBool, Ordering};
18
19use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
20use polars::prelude::*;
21
22use crate::filter_modal::{FilterOperator, FilterStatement, LogicalOperator};
23use crate::jobs::{Answer, Job, Progress};
24use crate::widgets::datatable::ViewRows;
25use crate::widgets::text_input::{TextInput, TextInputEvent};
26use crate::{App, AppEvent, InputMode, InputType};
27
28const ROW: &str = "__datui_find_row";
30const ROWS: &str = "__datui_find_rows";
32const FIRST_WINDOW: usize = 65_536;
41const LARGEST_WINDOW: usize = u32::MAX as usize;
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct FindSpec {
47 pub pattern: String,
48 pub regex: bool,
50 pub fuzzy: bool,
53 pub column: Option<String>,
55}
56
57impl FindSpec {
58 pub fn ignores_case(&self) -> bool {
61 let mut chars = self.pattern.chars();
62 while let Some(c) = chars.next() {
63 if self.regex && c == '\\' {
64 chars.next();
65 continue;
66 }
67 if c.is_uppercase() {
68 return false;
69 }
70 }
71 true
72 }
73
74 pub(crate) fn regex_source(&self) -> Option<String> {
77 let case = if self.ignores_case() { "(?i)" } else { "" };
78 if self.fuzzy {
79 let letters: Vec<String> = self
80 .pattern
81 .chars()
82 .filter(|c| !c.is_whitespace())
83 .map(|c| regex::escape(&c.to_string()))
84 .collect();
85 return Some(format!("{case}{}", letters.join(".*")));
86 }
87 match (self.regex, self.ignores_case()) {
88 (false, false) => None,
89 (false, true) => Some(format!("{case}{}", regex::escape(&self.pattern))),
90 (true, _) => Some(format!("{case}{}", self.pattern)),
91 }
92 }
93
94 pub fn check(&self) -> Result<(), String> {
96 let Some(source) = self.regex_source().filter(|_| self.regex && !self.fuzzy) else {
97 return Ok(());
98 };
99 regex::Regex::new(&source).map(|_| ()).map_err(|e| {
100 let text = e.to_string();
103 let reason = text
104 .lines()
105 .rev()
106 .find(|line| !line.trim().is_empty())
107 .unwrap_or("invalid")
108 .trim()
109 .trim_start_matches("error: ")
110 .to_string();
111 format!("Not a regex: {reason}")
112 })
113 }
114
115 fn matches(&self, text: Expr) -> Expr {
117 let found = match self.regex_source() {
118 None => text.str().contains_literal(lit(self.pattern.clone())),
119 Some(source) => text.str().contains(lit(source), true),
120 };
121 found.fill_null(lit(false))
122 }
123
124 pub fn label(&self) -> String {
127 const LONGEST: usize = 18;
128 let g = crate::glyphs::get();
129 let mut text: String = self.pattern.chars().take(LONGEST).collect();
130 if self.pattern.chars().count() > LONGEST {
131 text.push_str(g.ellipsis);
132 }
133 if self.fuzzy {
134 format!("~{text}")
135 } else if self.regex {
136 format!("/{text}/")
137 } else {
138 format!("\"{text}\"")
139 }
140 }
141}
142
143pub(crate) fn cell_matches(spec: &FindSpec, name: &str, dtype: &DataType) -> Option<Expr> {
146 Some(spec.matches(text_of(name, dtype)?))
147}
148
149fn text_of(name: &str, dtype: &DataType) -> Option<Expr> {
152 if dtype.is_nested()
153 || dtype.is_object()
154 || matches!(
155 dtype,
156 DataType::Binary | DataType::BinaryOffset | DataType::Null
157 )
158 {
159 return None;
160 }
161 let column = col(name);
162 Some(if dtype.is_string() {
163 column
164 } else if let DataType::Duration(unit) = dtype {
165 duration_text(column, *unit)
167 } else if crate::past_calendar::can_leave_calendar(dtype) {
168 crate::past_calendar::text_expr(column, polars::chunked_array::cast::CastOptions::NonStrict)
171 } else {
172 column.cast(DataType::String)
173 })
174}
175
176fn duration_text(column: Expr, unit: TimeUnit) -> Expr {
178 column.map_with_fmt_str(
179 move |c| {
180 let text = c
181 .as_materialized_series()
182 .to_physical_repr()
183 .i64()?
184 .apply_into_string_amortized(|v, out| {
185 use std::fmt::Write;
186 let _ = write!(out, "{}", AnyValue::Duration(v, unit));
187 });
188 Ok(text.with_name(c.name().clone()).into_column())
189 },
190 |_: &Schema, field: &Field| Ok(Field::new(field.name().clone(), DataType::String)),
191 "find_duration_text",
192 )
193}
194
195pub(crate) fn searched_columns(
197 order: &[String],
198 schema: &Schema,
199 spec: &FindSpec,
200) -> Vec<(String, Expr)> {
201 order
202 .iter()
203 .filter(|name| spec.column.as_ref().is_none_or(|only| only == *name))
204 .filter_map(|name| {
205 let text = text_of(name, schema.get(name)?)?;
206 Some((name.clone(), spec.matches(text)))
207 })
208 .collect()
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum Direction {
214 Next,
215 Previous,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub(crate) struct Start {
223 pub(crate) row: usize,
224 pub(crate) column: Option<At>,
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub(crate) enum At {
230 On(usize),
232 Before(usize),
234}
235
236impl At {
237 fn ahead(self) -> usize {
239 match self {
240 At::On(c) => c + 1,
241 At::Before(c) => c,
242 }
243 }
244
245 fn behind(self) -> usize {
247 match self {
248 At::On(c) | At::Before(c) => c,
249 }
250 }
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct Found {
256 pub row: usize,
257 pub column: String,
258 pub wrapped: bool,
260}
261
262#[derive(Debug, Clone)]
265struct Limit {
266 row: usize,
267 columns: Range<usize>,
268}
269
270pub(crate) const CANCELLED: &str = "Find cancelled";
272
273pub(crate) struct Search {
275 rows: ViewRows,
276 columns: Vec<String>,
277 exprs: Vec<Expr>,
278 stop: Arc<AtomicBool>,
279 report: Box<dyn Fn(usize) + Send>,
280 read: usize,
282 window: usize,
284}
285
286impl Search {
287 pub(crate) fn new(
288 rows: ViewRows,
289 columns: Vec<(String, Expr)>,
290 stop: Arc<AtomicBool>,
291 report: impl Fn(usize) + Send + 'static,
292 ) -> Self {
293 let (names, exprs): (Vec<String>, Vec<Expr>) = columns
294 .into_iter()
295 .enumerate()
296 .map(|(i, (name, expr))| (name, expr.alias(format!("m{i}"))))
297 .unzip();
298 let mut rows = rows;
301 if rows
302 .buffer
303 .as_ref()
304 .is_some_and(|(df, _)| names.iter().any(|n| df.column(n).is_err()))
305 {
306 rows.buffer = None;
307 }
308 let window = if rows.whole {
310 LARGEST_WINDOW
311 } else {
312 FIRST_WINDOW
313 };
314 Self {
315 rows,
316 columns: names,
317 exprs,
318 stop,
319 report: Box::new(report),
320 read: 0,
321 window,
322 }
323 }
324
325 pub(crate) fn run(
328 mut self,
329 start: Start,
330 direction: Direction,
331 ) -> Result<Option<Found>, String> {
332 let n = self.columns.len();
333 let r = start.row;
334 let found = |(row, column): (usize, usize), wrapped: bool, columns: &[String]| Found {
335 row,
336 column: columns[column].clone(),
337 wrapped,
338 };
339 match direction {
340 Direction::Next => {
341 let ahead = start.column.map(|at| Limit {
342 row: r,
343 columns: at.ahead()..n,
344 });
345 if let Some(hit) = self.forward(r, None, ahead.as_ref(), false)? {
346 return Ok(Some(found(hit, false, &self.columns)));
347 }
348 let (end, behind) = match start.column {
351 Some(at) => (
352 r + 1,
353 Some(Limit {
354 row: r,
355 columns: 0..at.ahead(),
356 }),
357 ),
358 None => (r, None),
359 };
360 Ok(self
361 .forward(0, Some(end), behind.as_ref(), false)?
362 .map(|hit| found(hit, true, &self.columns)))
363 }
364 Direction::Previous => {
365 let behind = start.column.map(|at| Limit {
366 row: r,
367 columns: 0..at.behind(),
368 });
369 if let Some(hit) = self.backward(0, r + 1, behind.as_ref())? {
370 return Ok(Some(found(hit, false, &self.columns)));
371 }
372 let (from, ahead) = match start.column {
374 Some(at) => (
375 r,
376 Some(Limit {
377 row: r,
378 columns: at.behind()..n,
379 }),
380 ),
381 None => (r + 1, None),
382 };
383 let hit = match self.rows.num_rows {
384 Some(total) => self.backward(from, total, ahead.as_ref())?,
385 None => self.forward(from, None, ahead.as_ref(), true)?,
387 };
388 Ok(hit.map(|hit| found(hit, true, &self.columns)))
389 }
390 }
391 }
392
393 fn forward(
396 &mut self,
397 from: usize,
398 end: Option<usize>,
399 limit: Option<&Limit>,
400 last: bool,
401 ) -> Result<Option<(usize, usize)>, String> {
402 let end = end.or(self.rows.num_rows);
403 let mut at = from;
404 let mut best = None;
405 while end.is_none_or(|end| at < end) {
406 self.check_stop()?;
407 let (len, buffered) = self.plan_forward(at, end, last);
408 if len == 0 {
409 break;
410 }
411 let (rows, hit) = self.window_at(at, len, buffered, limit, last)?;
412 if hit.is_some() {
413 if !last {
414 return Ok(hit);
415 }
416 best = hit;
417 }
418 if rows < len {
419 break;
420 }
421 at += len;
422 }
423 Ok(best)
424 }
425
426 fn backward(
428 &mut self,
429 from: usize,
430 end: usize,
431 limit: Option<&Limit>,
432 ) -> Result<Option<(usize, usize)>, String> {
433 let mut end = end;
434 while end > from {
435 self.check_stop()?;
436 let (start, buffered) = self.plan_backward(from, end);
437 let (_, hit) = self.window_at(start, end - start, buffered, limit, true)?;
438 if hit.is_some() {
439 return Ok(hit);
440 }
441 end = start;
442 }
443 Ok(None)
444 }
445
446 fn check_stop(&self) -> Result<(), String> {
447 if self.stop.load(Ordering::Relaxed) {
448 return Err(CANCELLED.to_string());
449 }
450 Ok(())
451 }
452
453 fn buffered(&self) -> Option<(usize, usize)> {
455 self.rows
456 .buffer
457 .as_ref()
458 .map(|(df, start)| (*start, start + df.height()))
459 }
460
461 fn plan_forward(&mut self, at: usize, end: Option<usize>, last: bool) -> (usize, bool) {
465 let end = end.unwrap_or(usize::MAX);
466 if let Some((start, stop)) = self.buffered()
467 && (start..stop).contains(&at)
468 {
469 return (stop.min(end) - at, true);
470 }
471 let mut window = self.window_for(last);
472 if self.rows.reads_up_to {
476 window = window.max(at);
477 }
478 let mut stop = at.saturating_add(window).min(end);
479 if let Some((start, _)) = self.buffered()
480 && at < start
481 {
482 stop = stop.min(start);
483 }
484 self.window = window.saturating_mul(2).min(LARGEST_WINDOW);
485 (stop - at, false)
486 }
487
488 fn window_for(&self, whole_range: bool) -> usize {
491 if whole_range && self.rows.reads_up_to {
492 LARGEST_WINDOW
493 } else {
494 self.window
495 }
496 }
497
498 fn plan_backward(&mut self, from: usize, end: usize) -> (usize, bool) {
501 if let Some((start, stop)) = self.buffered()
502 && (start..stop).contains(&(end - 1))
503 {
504 return (start.max(from), true);
505 }
506 let mut start = end.saturating_sub(self.window_for(true)).max(from);
507 if let Some((_, stop)) = self.buffered()
508 && end > stop
509 {
510 start = start.max(stop);
511 }
512 self.window = self.window.saturating_mul(2).min(LARGEST_WINDOW);
513 (start, false)
514 }
515
516 fn window_at(
521 &mut self,
522 start: usize,
523 len: usize,
524 buffered: bool,
525 limit: Option<&Limit>,
526 last: bool,
527 ) -> Result<(usize, Option<(usize, usize)>), String> {
528 let message = |e: PolarsError| crate::error_display::user_message_from_polars(&e);
529 let lf = match self.rows.buffer.as_ref().filter(|_| buffered) {
530 Some((df, at)) => df
531 .slice((start - at) as i64, len)
532 .lazy()
533 .select(self.exprs.clone()),
534 None => self
535 .rows
536 .window(start, len, self.exprs.clone())
537 .map_err(message)?,
538 };
539 let offset = IdxSize::try_from(start)
540 .map_err(|_| "The view has too many rows to find in".to_string())?;
541 let row = || col(ROW).cast(DataType::UInt64);
542 let mut aggregates = vec![polars::prelude::len().cast(DataType::UInt64).alias(ROWS)];
543 for i in 0..self.columns.len() {
544 let mut cell = col(format!("m{i}"));
545 if let Some(limit) = limit
546 && !limit.columns.contains(&i)
547 {
548 cell = cell.and(row().neq(lit(limit.row as u64)));
549 }
550 let rows = row().filter(cell);
551 let at = if last { rows.max() } else { rows.min() };
552 aggregates.push(at.alias(format!("f{i}")));
553 }
554 let lf = lf.with_row_index(ROW, Some(offset)).select(aggregates);
555 let df = crate::statistics::collect_lazy(lf, self.rows.streaming).map_err(message)?;
556 let get = |name: &str| -> Option<u64> { df.column(name).ok()?.u64().ok()?.get(0) };
557 let rows = get(ROWS).unwrap_or(0) as usize;
558 let mut best: Option<(usize, usize)> = None;
559 for i in 0..self.columns.len() {
560 let Some(at) = get(&format!("f{i}")).map(|r| r as usize) else {
561 continue;
562 };
563 let better = best.is_none_or(|(b, _)| if last { at >= b } else { at < b });
566 if better {
567 best = Some((at, i));
568 }
569 }
570 self.read += rows;
571 (self.report)(self.read);
572 Ok((rows, best))
573 }
574}
575
576pub struct Find {
578 pub input: TextInput,
579 pub regex: bool,
581 pub fuzzy: bool,
583 pub in_column: bool,
585 pub column: Option<String>,
587 pub error: Option<String>,
589 pub active: Option<ActiveFind>,
591 pub live: Option<LiveMatches>,
593 live_rows: Option<(usize, usize, u64)>,
596 pub read: Option<usize>,
598}
599
600pub type MatchCells = std::collections::HashMap<String, std::collections::HashSet<usize>>;
603
604#[derive(Debug, Clone, Default)]
607pub struct LiveMatches {
608 pub cells: Arc<MatchCells>,
609}
610
611impl LiveMatches {
612 pub fn within(&self, rows: Range<usize>) -> usize {
614 self.cells
615 .values()
616 .map(|hits| hits.iter().filter(|r| rows.contains(r)).count())
617 .sum()
618 }
619}
620
621#[derive(Debug, Clone)]
623pub struct ActiveFind {
624 pub spec: FindSpec,
625 pub dataset: u64,
627 pub frame: u64,
629 pub hit: Option<(usize, String)>,
631 pub ordinal: Option<usize>,
633}
634
635impl Find {
636 pub fn new(input: TextInput) -> Self {
637 Self {
638 input,
639 regex: false,
640 fuzzy: false,
641 in_column: false,
642 column: None,
643 error: None,
644 active: None,
645 live: None,
646 live_rows: None,
647 read: None,
648 }
649 }
650
651 pub(crate) fn prompt_spec(&self) -> FindSpec {
653 FindSpec {
654 pattern: self.input.value().to_string(),
655 regex: self.regex,
656 fuzzy: self.fuzzy,
657 column: self.column.clone().filter(|_| self.in_column),
658 }
659 }
660}
661
662#[derive(Debug, Clone)]
664pub(crate) struct FindRun {
665 pub(crate) stop: Arc<AtomicBool>,
666 pub(crate) dataset: u64,
667 pub(crate) frame: u64,
668 pub(crate) direction: Direction,
669 pub(crate) from_top: bool,
671 pub(crate) from_hit: Option<Option<usize>>,
673}
674
675fn finding_status(spec: &FindSpec, read: Option<usize>) -> String {
677 match read {
678 Some(rows) if rows > 0 => format!(
681 "Finding {}... {} rows",
682 spec.label(),
683 crate::discover::format_rows(rows)
684 ),
685 _ => format!("Finding {}...", spec.label()),
686 }
687}
688
689impl App {
690 pub(crate) fn open_find(&mut self) {
693 if self.data_table_state.is_none() {
694 return;
695 }
696 self.find.column = self.find_column();
697 self.find.error = None;
698 match self.find.active.as_ref() {
699 Some(active) => {
700 let pattern = active.spec.pattern.clone();
701 self.find.input.set_value(pattern);
702 self.find.input.select_all();
703 }
704 None => self.find.input.clear(),
705 }
706 self.find.input.set_focused(true);
707 self.input_mode = InputMode::Editing;
708 self.input_type = Some(InputType::Find);
709 self.refresh_live_matches();
710 }
711
712 pub(crate) fn refresh_live_matches(&mut self) {
715 self.find.live = None;
716 self.find.live_rows = self.rows_on_hand_key();
717 let spec = self.find.prompt_spec();
718 if spec.pattern.trim().is_empty() || spec.check().is_err() {
719 return;
720 }
721 let Some(state) = self.data_table_state.as_ref() else {
722 return;
723 };
724 let Some((df, start)) = state.rows_on_hand() else {
725 return;
726 };
727 let columns: Vec<(String, Expr)> =
728 searched_columns(state.get_column_order(), state.schema(), &spec)
729 .into_iter()
730 .filter(|(name, _)| df.column(name).is_ok())
731 .collect();
732 if columns.is_empty() {
733 self.find.live = Some(LiveMatches::default());
734 return;
735 }
736 let exprs: Vec<Expr> = columns
737 .iter()
738 .enumerate()
739 .map(|(i, (_, expr))| expr.clone().alias(format!("m{i}")))
740 .collect();
741 let Ok(found) = df.clone().lazy().select(exprs).collect() else {
743 return;
744 };
745 let mut cells = MatchCells::new();
746 for (i, (name, _)) in columns.iter().enumerate() {
747 let Ok(hits) = found
748 .column(&format!("m{i}"))
749 .and_then(|c| c.bool().cloned())
750 else {
751 continue;
752 };
753 let rows: std::collections::HashSet<usize> = hits
754 .iter()
755 .enumerate()
756 .filter(|(_, hit)| *hit == Some(true))
757 .map(|(row, _)| start + row)
758 .collect();
759 if !rows.is_empty() {
760 cells.insert(name.clone(), rows);
761 }
762 }
763 self.find.live = Some(LiveMatches {
764 cells: Arc::new(cells),
765 });
766 }
767
768 fn rows_on_hand_key(&self) -> Option<(usize, usize, u64)> {
771 let state = self.data_table_state.as_ref()?;
772 let (df, start) = state.rows_on_hand()?;
773 Some((start, df.height(), state.len_generation()))
774 }
775
776 pub(crate) fn refresh_stale_live_matches(&mut self) {
780 if self.input_type == Some(InputType::Find)
781 && self.find.live_rows != self.rows_on_hand_key()
782 {
783 self.refresh_live_matches();
784 }
785 }
786
787 pub fn live_on_screen(&self) -> Option<usize> {
790 let live = self.find.live.as_ref()?;
791 let state = self.data_table_state.as_ref()?;
792 let start = state.start_row();
793 Some(live.within(start..start + state.visible_rows))
794 }
795
796 pub fn live_cells(&self) -> Option<Arc<MatchCells>> {
798 (self.input_type == Some(InputType::Find))
799 .then_some(self.find.live.as_ref())
800 .flatten()
801 .map(|live| live.cells.clone())
802 }
803
804 pub(crate) fn find_column(&self) -> Option<String> {
807 self.data_table_state
808 .as_ref()?
809 .current_column()
810 .map(str::to_string)
811 }
812
813 fn close_find_prompt(&mut self) {
814 self.find.input.set_focused(false);
815 self.find.error = None;
816 self.find.live = None;
817 self.input_mode = InputMode::Normal;
818 self.input_type = None;
819 }
820
821 pub(crate) fn find_prompt_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
825 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
826 if event.is_press() && ctrl {
827 match event.code {
828 KeyCode::Char('r') => {
829 self.find.regex = !self.find.regex;
830 self.find.fuzzy &= !self.find.regex;
831 self.find.error = None;
832 self.refresh_live_matches();
833 return None;
834 }
835 KeyCode::Char('t') => {
836 self.find.fuzzy = !self.find.fuzzy;
837 self.find.regex &= !self.find.fuzzy;
838 self.find.error = None;
839 self.refresh_live_matches();
840 return None;
841 }
842 KeyCode::Char('l') => {
843 self.find.in_column = !self.find.in_column;
844 self.refresh_live_matches();
845 return None;
846 }
847 KeyCode::Char('g') => return self.keep_matches(),
848 _ => {}
849 }
850 }
851 let before = self.find.input.value().to_string();
852 match self.find.input.handle_key(event, Some(&self.cache)) {
853 TextInputEvent::Submit => {
854 let spec = self.find.prompt_spec();
855 if spec.pattern.is_empty() {
856 self.find.active = None;
858 self.close_find_prompt();
859 return None;
860 }
861 if let Err(reason) = spec.check() {
862 self.find.error = Some(reason);
863 return None;
864 }
865 let _ = self.find.input.save_to_history(&self.cache);
866 self.close_find_prompt();
867 self.start_find(spec, Direction::Next, true);
868 }
869 TextInputEvent::Cancel => self.close_find_prompt(),
870 TextInputEvent::HistoryChanged | TextInputEvent::None => {
871 if self.find.input.value() != before {
872 self.find.error = None;
873 self.refresh_live_matches();
874 }
875 }
876 }
877 None
878 }
879
880 fn keep_matches(&mut self) -> Option<AppEvent> {
883 let spec = self.find.prompt_spec();
884 if spec.pattern.trim().is_empty() {
885 return None;
886 }
887 if let Err(reason) = spec.check() {
888 self.find.error = Some(reason);
889 return None;
890 }
891 let state = self.data_table_state.as_ref()?;
892 let operator = if spec.fuzzy {
893 FilterOperator::HasFuzzy
894 } else if spec.regex {
895 FilterOperator::HasRegex
896 } else {
897 FilterOperator::Has
898 };
899 let columns = if spec.column.is_none() {
901 state.get_column_order().to_vec()
902 } else {
903 Vec::new()
904 };
905 let statement = FilterStatement {
906 columns,
907 column: spec
908 .column
909 .clone()
910 .unwrap_or_else(|| crate::filter_modal::ANY_COLUMN.to_string()),
911 operator,
912 value: spec.pattern.clone(),
913 logical_op: LogicalOperator::And,
914 };
915 let mut statements = state.view_filters().to_vec();
916 let frame = state.len_generation();
917 let _ = self.find.input.save_to_history(&self.cache);
918 self.close_find_prompt();
919 self.find.active = Some(ActiveFind {
920 spec,
921 dataset: self.dataset_generation,
922 frame,
923 hit: None,
924 ordinal: None,
925 });
926 if statements.contains(&statement) {
927 return None;
928 }
929 statements.push(statement);
930 Some(AppEvent::Filter(statements))
931 }
932
933 pub(crate) fn find_again(&mut self, direction: Direction) {
935 match self.find.active.as_ref() {
936 Some(active) if active.dataset == self.dataset_generation => {
937 let spec = active.spec.clone();
938 self.start_find(spec, direction, false);
939 }
940 _ => self.flash_note("Nothing to find yet: / finds".to_string()),
941 }
942 }
943
944 pub fn find_hit(&self) -> Option<(usize, String)> {
946 let active = self.find.active.as_ref()?;
947 let state = self.data_table_state.as_ref()?;
948 (active.dataset == self.dataset_generation && active.frame == state.len_generation())
949 .then(|| active.hit.clone())
950 .flatten()
951 }
952
953 pub fn find_mark(&self) -> Option<String> {
956 let active = self.find.active.as_ref()?;
957 if active.dataset != self.dataset_generation
960 || self.finding()
961 || self.input_type == Some(InputType::Find)
962 {
963 return None;
964 }
965 let mut mark = format!("find {}", active.spec.label());
966 if let Some(column) = &active.spec.column {
967 mark.push_str(&format!(" in {column}"));
968 }
969 if let Some(k) = active.ordinal.filter(|_| self.find_hit().is_some()) {
970 mark.push_str(&format!(
971 " {} match {}",
972 crate::glyphs::get().middot,
973 crate::numfmt::group_chrome(k)
974 ));
975 }
976 Some(mark)
977 }
978
979 pub(crate) fn find_shown(&self) -> bool {
981 self.find
982 .active
983 .as_ref()
984 .is_some_and(|active| active.dataset == self.dataset_generation)
985 }
986
987 pub fn finding(&self) -> bool {
989 self.jobs
990 .current(|job| matches!(job, Job::Find(_) | Job::HexFind(_)))
991 .is_some()
992 }
993
994 pub(crate) fn cancel_find(&mut self) {
997 if self.stop_find() {
998 self.flash_note(CANCELLED.to_string());
999 }
1000 }
1001
1002 pub(crate) fn stop_find(&mut self) -> bool {
1004 if self.stop_hex_find() {
1005 return true;
1006 }
1007 let Some((_, Job::Find(run))) = self.jobs.current(|job| matches!(job, Job::Find(_))) else {
1008 return false;
1009 };
1010 run.stop.store(true, Ordering::Relaxed);
1011 self.jobs.cancel(|job| matches!(job, Job::Find(_)));
1012 self.status_message = None;
1013 self.find.read = None;
1014 true
1015 }
1016
1017 fn start_find(&mut self, spec: FindSpec, direction: Direction, fresh: bool) {
1020 let Some(state) = self.data_table_state.as_ref() else {
1021 return;
1022 };
1023 let columns = searched_columns(state.get_column_order(), state.schema(), &spec);
1024 if columns.is_empty() {
1025 self.flash_note(match &spec.column {
1026 Some(column) if !state.get_column_order().contains(column) => {
1027 format!("Nothing to find in {column}: it is not shown")
1028 }
1029 Some(column) => format!("Nothing to find in {column}: it holds no text"),
1030 None => "No column to find in".to_string(),
1031 });
1032 return;
1033 }
1034 let frame = state.len_generation();
1035 let row = state.cursor_row();
1036 let at = state.current_column().filter(|_| !fresh).map(|name| {
1038 match columns.iter().position(|(n, _)| n == name) {
1039 Some(c) => At::On(c),
1040 None => {
1041 let order = state.get_column_order();
1042 let place = |n: &str| order.iter().position(|o| o == n);
1043 let cursor = place(name);
1044 At::Before(columns.iter().filter(|(n, _)| place(n) < cursor).count())
1045 }
1046 }
1047 });
1048 let previous =
1049 self.find.active.as_ref().filter(|a| {
1050 a.dataset == self.dataset_generation && a.frame == frame && a.spec == spec
1051 });
1052 let on_hit = previous
1054 .and_then(|a| Some((a.hit.as_ref()?, a.ordinal)))
1055 .and_then(|((hit_row, name), ordinal)| {
1056 let c = columns.iter().position(|(n, _)| n == name)?;
1057 (*hit_row == row && at == Some(At::On(c))).then(|| (name.clone(), ordinal))
1058 });
1059 let start = Start { row, column: at };
1060 self.find.active = Some(ActiveFind {
1061 spec: spec.clone(),
1062 dataset: self.dataset_generation,
1063 frame,
1064 hit: on_hit.as_ref().map(|(name, _)| (row, name.clone())),
1065 ordinal: on_hit.as_ref().and_then(|(_, ordinal)| *ordinal),
1066 });
1067 let stop = Arc::new(AtomicBool::new(false));
1068 let run = FindRun {
1069 stop: stop.clone(),
1070 dataset: self.dataset_generation,
1071 frame,
1072 direction,
1073 from_top: row == 0 && at.is_none_or(|at| at.ahead() == 0),
1074 from_hit: on_hit.as_ref().map(|(_, ordinal)| *ordinal),
1075 };
1076 let rows = state.view_rows();
1077 let status = finding_status(&spec, None);
1078 self.find.read = None;
1079 self.spawn_job(Job::Find(run), Some(&status), move |worker| {
1080 let report = worker.reporter();
1081 let search = Search::new(rows, columns, stop, move |read| {
1082 report(Progress::Finding { rows: read })
1083 });
1084 Ok(Answer::Found(search.run(start, direction)?))
1085 });
1086 }
1087
1088 pub(crate) fn find_progress(&mut self, rows: usize) {
1090 self.find.read = Some(rows);
1091 if let Some(active) = self.find.active.as_ref() {
1092 self.status_message = Some(finding_status(&active.spec, Some(rows)));
1093 }
1094 }
1095
1096 pub(crate) fn find_answered(&mut self, run: FindRun, current: bool, found: Option<Found>) {
1098 if !current {
1099 return;
1100 }
1101 self.status_message = None;
1103 self.find.read = None;
1104 let Some(active) = self.find.active.as_mut() else {
1105 return;
1106 };
1107 let Some(state) = self.data_table_state.as_mut() else {
1108 return;
1109 };
1110 if active.dataset != run.dataset
1111 || active.frame != run.frame
1112 || state.len_generation() != run.frame
1113 {
1114 return;
1115 }
1116 let Some(found) = found else {
1117 active.hit = None;
1118 active.ordinal = None;
1119 let message = format!("No match for {}", active.spec.label());
1120 self.flash_note(message);
1121 return;
1122 };
1123 let same_cell = run.from_hit.is_some()
1124 && active
1125 .hit
1126 .as_ref()
1127 .is_some_and(|(row, name)| *row == found.row && *name == found.column);
1128 let before = run.from_hit.flatten();
1129 active.ordinal = match run.direction {
1130 _ if same_cell => before,
1131 Direction::Next if found.wrapped => Some(1),
1133 Direction::Next if run.from_hit.is_some() => before.map(|k| k + 1),
1134 Direction::Next => run.from_top.then_some(1),
1135 Direction::Previous if found.wrapped => None,
1136 Direction::Previous => before.and_then(|k| k.checked_sub(1)).filter(|k| *k > 0),
1137 };
1138 active.hit = Some((found.row, found.column.clone()));
1139 let needs_rows = state.go_to_found_row(found.row);
1140 state.set_current_column(&found.column);
1142 if found.wrapped {
1143 self.flash_note(match run.direction {
1144 Direction::Next => "Wrapped to the top".to_string(),
1145 Direction::Previous => "Wrapped to the bottom".to_string(),
1146 });
1147 }
1148 if needs_rows {
1149 self.spawn_async_collect(Self::LOADING_BUFFER);
1150 }
1151 }
1152
1153 pub(crate) fn find_failed(&mut self, current: bool, message: &str) {
1155 if !current {
1156 return;
1157 }
1158 self.status_message = None;
1159 self.find.read = None;
1160 if message != CANCELLED {
1161 self.flash_note(format!("Find failed: {message}"));
1162 }
1163 }
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169
1170 fn spec(pattern: &str, regex: bool) -> FindSpec {
1171 FindSpec {
1172 pattern: pattern.to_string(),
1173 regex,
1174 fuzzy: false,
1175 column: None,
1176 }
1177 }
1178
1179 fn frame() -> DataFrame {
1180 df!(
1181 "name" => ["Alice", "bob", "Carol", "dave", "alice"],
1182 "city" => ["Oslo", "Lima", "oslo", "Rome", "Lima"],
1183 "n" => [1i64, 22, 3, 42, 5],
1184 )
1185 .unwrap()
1186 }
1187
1188 fn walk(
1190 df: &DataFrame,
1191 spec: &FindSpec,
1192 buffer: Option<(DataFrame, usize)>,
1193 start: Start,
1194 direction: Direction,
1195 steps: usize,
1196 ) -> Vec<(usize, String, bool)> {
1197 let order: Vec<String> = df
1198 .get_column_names()
1199 .iter()
1200 .map(|n| n.to_string())
1201 .collect();
1202 let names: Vec<String> = searched_columns(&order, df.schema(), spec)
1203 .into_iter()
1204 .map(|(n, _)| n)
1205 .collect();
1206 let mut at = start;
1207 let mut out = Vec::new();
1208 for _ in 0..steps {
1209 let columns = searched_columns(&order, df.schema(), spec);
1210 let rows = ViewRows::of(df.clone().lazy(), buffer.clone());
1211 let search = Search::new(rows, columns, Arc::default(), |_| {});
1212 let Some(found) = search.run(at, direction).unwrap() else {
1213 break;
1214 };
1215 let column = names.iter().position(|n| *n == found.column).map(At::On);
1216 at = Start {
1217 row: found.row,
1218 column,
1219 };
1220 out.push((found.row, found.column, found.wrapped));
1221 }
1222 out
1223 }
1224
1225 fn cells(found: &[(usize, String, bool)]) -> Vec<(usize, &str)> {
1226 found.iter().map(|(r, c, _)| (*r, c.as_str())).collect()
1227 }
1228
1229 #[test]
1230 fn plain_text_ignores_case_until_a_capital_is_typed() {
1231 let df = frame();
1232 let start = Start {
1233 row: 0,
1234 column: None,
1235 };
1236 let lower = walk(&df, &spec("oslo", false), None, start, Direction::Next, 2);
1237 assert_eq!(cells(&lower), [(0, "city"), (2, "city")]);
1238 let upper = walk(&df, &spec("Oslo", false), None, start, Direction::Next, 2);
1239 assert_eq!(cells(&upper), [(0, "city"), (0, "city")], "{upper:?}");
1240 assert!(upper[1].2, "the one match wraps to itself");
1241 }
1242
1243 #[test]
1244 fn plain_text_is_literal() {
1245 let df = df!("a" => ["a.c", "abc"]).unwrap();
1246 let start = Start {
1247 row: 0,
1248 column: None,
1249 };
1250 let found = walk(&df, &spec("a.c", false), None, start, Direction::Next, 2);
1251 assert_eq!(cells(&found), [(0, "a"), (0, "a")]);
1252 }
1253
1254 #[test]
1255 fn a_regex_matches_and_keeps_smart_case() {
1256 let df = frame();
1257 let start = Start {
1258 row: 0,
1259 column: None,
1260 };
1261 let digits = walk(
1262 &df,
1263 &spec(r"^\d{2}$", true),
1264 None,
1265 start,
1266 Direction::Next,
1267 3,
1268 );
1269 assert_eq!(cells(&digits), [(1, "n"), (3, "n"), (1, "n")]);
1270 assert!(spec(r"^\Sl", true).ignores_case());
1272 let a = walk(&df, &spec(r"^a\S", true), None, start, Direction::Next, 2);
1273 assert_eq!(cells(&a), [(0, "name"), (4, "name")]);
1274 let capital = walk(&df, &spec("^A", true), None, start, Direction::Next, 2);
1275 assert_eq!(cells(&capital), [(0, "name"), (0, "name")]);
1276 }
1277
1278 #[test]
1279 fn a_bad_regex_says_why() {
1280 let err = spec("(ab", true).check().unwrap_err();
1281 assert!(err.starts_with("Not a regex"), "{err}");
1282 assert!(!err.contains('\n'), "{err}");
1283 assert!(spec("(ab", false).check().is_ok(), "plain text is literal");
1284 }
1285
1286 #[test]
1287 fn next_moves_cell_by_cell_and_wraps_to_the_top() {
1288 let df = frame();
1289 let start = Start {
1290 row: 0,
1291 column: None,
1292 };
1293 let found = walk(&df, &spec("li", false), None, start, Direction::Next, 5);
1296 assert_eq!(
1297 cells(&found),
1298 [
1299 (0, "name"),
1300 (1, "city"),
1301 (4, "name"),
1302 (4, "city"),
1303 (0, "name")
1304 ]
1305 );
1306 assert!(!found[3].2);
1307 assert!(found[4].2, "past the last match comes round to the first");
1308 }
1309
1310 #[test]
1311 fn previous_walks_back_and_wraps_to_the_bottom() {
1312 let df = frame();
1313 let start = Start {
1314 row: 1,
1315 column: Some(At::On(1)),
1316 };
1317 let found = walk(&df, &spec("li", false), None, start, Direction::Previous, 3);
1318 assert_eq!(cells(&found), [(0, "name"), (4, "city"), (4, "name")]);
1319 assert!(found[1].2, "before the first match comes round to the last");
1320 }
1321
1322 #[test]
1325 fn from_a_column_not_searched_the_cells_either_side_split() {
1326 let df = frame();
1327 let only_city = FindSpec {
1328 column: Some("city".to_string()),
1329 ..spec("li", false)
1330 };
1331 let order: Vec<String> = ["name", "city", "n"].map(String::from).to_vec();
1332 let one = |at: At, direction: Direction| {
1333 let columns = searched_columns(&order, df.schema(), &only_city);
1334 let rows = ViewRows::of(df.clone().lazy(), None);
1335 let search = Search::new(rows, columns, Arc::default(), |_| {});
1336 let found = search
1337 .run(
1338 Start {
1339 row: 1,
1340 column: Some(at),
1341 },
1342 direction,
1343 )
1344 .unwrap()
1345 .unwrap();
1346 (found.row, found.wrapped)
1347 };
1348 assert_eq!(one(At::Before(0), Direction::Next), (1, false));
1350 assert_eq!(one(At::Before(0), Direction::Previous), (4, true));
1351 assert_eq!(one(At::Before(1), Direction::Next), (4, false));
1353 assert_eq!(one(At::Before(1), Direction::Previous), (1, false));
1354 }
1355
1356 #[test]
1357 fn previous_wraps_with_the_row_count_known_too() {
1358 let df = frame();
1359 let columns = searched_columns(
1360 &["name".to_string(), "city".to_string()],
1361 df.schema(),
1362 &spec("li", false),
1363 );
1364 let mut rows = ViewRows::of(df.clone().lazy(), None);
1365 rows.num_rows = Some(df.height());
1366 let search = Search::new(rows, columns, Arc::default(), |_| {});
1367 let found = search
1368 .run(
1369 Start {
1370 row: 0,
1371 column: Some(At::On(0)),
1372 },
1373 Direction::Previous,
1374 )
1375 .unwrap()
1376 .unwrap();
1377 assert_eq!((found.row, found.column.as_str()), (4, "city"));
1378 assert!(found.wrapped);
1379 }
1380
1381 #[test]
1382 fn f_finds_a_match_on_the_cursor_row_itself() {
1383 let df = frame();
1384 let start = Start {
1385 row: 2,
1386 column: None,
1387 };
1388 let found = walk(&df, &spec("oslo", false), None, start, Direction::Next, 1);
1389 assert_eq!(cells(&found), [(2, "city")]);
1390 }
1391
1392 #[test]
1393 fn a_column_limit_skips_the_others() {
1394 let df = frame();
1395 let mut only = spec("li", false);
1396 only.column = Some("city".to_string());
1397 let start = Start {
1398 row: 0,
1399 column: None,
1400 };
1401 let found = walk(&df, &only, None, start, Direction::Next, 3);
1402 assert_eq!(cells(&found), [(1, "city"), (4, "city"), (1, "city")]);
1403 }
1404
1405 #[test]
1406 fn nothing_found_is_none() {
1407 let df = frame();
1408 let start = Start {
1409 row: 3,
1410 column: None,
1411 };
1412 assert!(walk(&df, &spec("zzz", false), None, start, Direction::Next, 1).is_empty());
1413 assert!(
1414 walk(
1415 &df,
1416 &spec("zzz", false),
1417 None,
1418 start,
1419 Direction::Previous,
1420 1
1421 )
1422 .is_empty()
1423 );
1424 }
1425
1426 #[test]
1429 fn a_match_beyond_the_buffer_is_read_from_the_view() {
1430 let n = 300_000usize;
1431 let values: Vec<String> = (0..n)
1432 .map(|i| {
1433 if i == 250_123 {
1434 "needle".to_string()
1435 } else {
1436 format!("hay{i}")
1437 }
1438 })
1439 .collect();
1440 let df = df!("v" => values).unwrap();
1441 let buffer = df.slice(100, 100);
1442 let columns = searched_columns(&["v".to_string()], df.schema(), &spec("needle", false));
1443 let reads = Arc::new(std::sync::Mutex::new(Vec::new()));
1444 let seen = reads.clone();
1445 let rows = ViewRows::of(df.clone().lazy(), Some((buffer, 100)));
1446 let search = Search::new(rows, columns, Arc::default(), move |read| {
1447 seen.lock().unwrap().push(read)
1448 });
1449 let found = search
1450 .run(
1451 Start {
1452 row: 150,
1453 column: None,
1454 },
1455 Direction::Next,
1456 )
1457 .unwrap()
1458 .unwrap();
1459 assert_eq!((found.row, found.wrapped), (250_123, false));
1460 let reads = reads.lock().unwrap();
1461 assert_eq!(
1464 reads.as_slice(),
1465 [50, 50 + 65_536, 50 + 65_536 + 131_072, n - 150]
1466 );
1467 }
1468
1469 #[test]
1470 fn a_stopped_search_ends_before_reading() {
1471 let df = frame();
1472 let columns = searched_columns(&["name".to_string()], df.schema(), &spec("a", false));
1473 let stop = Arc::new(AtomicBool::new(true));
1474 let search = Search::new(ViewRows::of(df.lazy(), None), columns, stop, |_| {
1475 panic!("nothing is read once stopped")
1476 });
1477 let ended = search.run(
1478 Start {
1479 row: 0,
1480 column: None,
1481 },
1482 Direction::Next,
1483 );
1484 assert_eq!(ended.unwrap_err(), CANCELLED);
1485 }
1486
1487 #[test]
1488 fn numbers_dates_and_categories_are_searched_as_text() {
1489 let df = df!(
1490 "d" => [chrono::NaiveDate::from_ymd_opt(2024, 5, 17).unwrap()],
1491 "x" => [12.5f64],
1492 "b" => [true],
1493 )
1494 .unwrap()
1495 .lazy()
1496 .with_column(
1497 lit("north")
1498 .cast(DataType::from_categories(Categories::global()))
1499 .alias("c"),
1500 )
1501 .collect()
1502 .unwrap();
1503 let start = Start {
1504 row: 0,
1505 column: None,
1506 };
1507 for (pattern, column) in [("05-17", "d"), ("2.5", "x"), ("true", "b"), ("nor", "c")] {
1508 let found = walk(&df, &spec(pattern, false), None, start, Direction::Next, 1);
1509 assert_eq!(cells(&found), [(0, column)], "{pattern}");
1510 }
1511 }
1512
1513 #[test]
1516 fn a_date_past_the_calendar_and_durations_are_text() {
1517 let d = Series::new("d".into(), [19_860i32, i32::MAX])
1518 .cast(&DataType::Date)
1519 .unwrap();
1520 let t = Series::new("t".into(), [3_600_000_000_000i64, 0])
1521 .cast(&DataType::Time)
1522 .unwrap();
1523 let dur = Series::new("dur".into(), [86_400_000i64, 1])
1524 .cast(&DataType::Duration(TimeUnit::Milliseconds))
1525 .unwrap();
1526 let df = DataFrame::new_infer_height(vec![d.into(), t.into(), dur.into()]).unwrap();
1527 let start = Start {
1528 row: 0,
1529 column: None,
1530 };
1531 let past = walk(&df, &spec("since", false), None, start, Direction::Next, 1);
1532 assert_eq!(cells(&past), [(1, "d")]);
1533 let time = walk(&df, &spec("01:00", false), None, start, Direction::Next, 1);
1534 assert_eq!(cells(&time), [(0, "t")]);
1535 let dur = walk(&df, &spec("1d", false), None, start, Direction::Next, 1);
1536 assert_eq!(cells(&dur), [(0, "dur")]);
1537 }
1538
1539 #[test]
1543 fn a_sorted_view_is_read_in_one_window() {
1544 let n = 300_000usize;
1545 let values: Vec<String> = (0..n)
1546 .map(|i| {
1547 if i == 7 {
1548 "needle".to_string()
1549 } else {
1550 format!("hay{i}")
1551 }
1552 })
1553 .collect();
1554 let df = df!("k" => (0..n as i64).collect::<Vec<_>>(), "v" => values).unwrap();
1555 let lf = df.lazy().sort(
1557 ["k"],
1558 SortMultipleOptions::default().with_order_descending(true),
1559 );
1560 let buffer = lf.clone().slice(0, 100).collect().unwrap();
1561 let columns = searched_columns(
1562 &["k".to_string(), "v".to_string()],
1563 &lf.clone().collect_schema().unwrap(),
1564 &spec("needle", false),
1565 );
1566 let reads = Arc::new(std::sync::Mutex::new(Vec::new()));
1567 let seen = reads.clone();
1568 let rows = ViewRows::of(lf, Some((buffer, 0)));
1569 assert!(rows.whole);
1570 let search = Search::new(rows, columns, Arc::default(), move |read| {
1571 seen.lock().unwrap().push(read)
1572 });
1573 let found = search
1574 .run(
1575 Start {
1576 row: 0,
1577 column: None,
1578 },
1579 Direction::Next,
1580 )
1581 .unwrap()
1582 .unwrap();
1583 assert_eq!((found.row, found.column.as_str()), (n - 8, "v"));
1584 assert_eq!(reads.lock().unwrap().as_slice(), [100, n]);
1585 }
1586
1587 #[test]
1590 fn previous_on_a_filtered_view_reads_the_range_once() {
1591 let n = 300_000usize;
1592 let values: Vec<String> = (0..n)
1593 .map(|i| {
1594 if i == 4 {
1595 "needle".to_string()
1596 } else {
1597 format!("hay{i}")
1598 }
1599 })
1600 .collect();
1601 let df = df!("k" => (0..n as i64).collect::<Vec<_>>(), "v" => values).unwrap();
1602 let lf = df.lazy().filter((col("k") % lit(2)).eq(lit(0)));
1604 let columns = || {
1605 searched_columns(
1606 &["k".to_string(), "v".to_string()],
1607 &lf.clone().collect_schema().unwrap(),
1608 &spec("needle", false),
1609 )
1610 };
1611 let previous = |row: usize, buffer: Option<(DataFrame, usize)>| {
1612 let reads = Arc::new(std::sync::Mutex::new(Vec::new()));
1613 let seen = reads.clone();
1614 let rows = ViewRows::of(lf.clone(), buffer);
1615 assert!(rows.reads_up_to && !rows.whole);
1616 let search = Search::new(rows, columns(), Arc::default(), move |read| {
1617 seen.lock().unwrap().push(read)
1618 });
1619 let found = search
1620 .run(Start { row, column: None }, Direction::Previous)
1621 .unwrap()
1622 .unwrap();
1623 let reads = reads.lock().unwrap().clone();
1624 (found.row, found.wrapped, reads)
1625 };
1626 let buffer = lf.clone().slice(140_000, 100).collect().unwrap();
1627 assert_eq!(
1628 previous(140_050, Some((buffer, 140_000))),
1629 (2, false, vec![51, 140_051]),
1630 "the buffer, then every row before it at once"
1631 );
1632 assert_eq!(
1633 previous(1, None),
1634 (2, true, vec![2, 150_000]),
1635 "nothing behind; round from the bottom in one read too"
1636 );
1637 }
1638
1639 #[test]
1643 fn next_from_deep_in_a_filtered_view_reads_the_rows_above_once() {
1644 let n = 600_000usize;
1645 let values: Vec<String> = (0..n)
1646 .map(|i| {
1647 if i == 598_000 {
1648 "needle".to_string()
1649 } else {
1650 format!("hay{i}")
1651 }
1652 })
1653 .collect();
1654 let df = df!("k" => (0..n as i64).collect::<Vec<_>>(), "v" => values).unwrap();
1655 let lf = df.lazy().filter((col("k") % lit(2)).eq(lit(0)));
1657 let columns = searched_columns(
1658 &["k".to_string(), "v".to_string()],
1659 &lf.clone().collect_schema().unwrap(),
1660 &spec("needle", false),
1661 );
1662 let buffer = lf.clone().slice(200_000, 100).collect().unwrap();
1663 let reads = Arc::new(std::sync::Mutex::new(Vec::new()));
1664 let seen = reads.clone();
1665 let rows = ViewRows::of(lf, Some((buffer, 200_000)));
1666 assert!(rows.reads_up_to && !rows.whole);
1667 let search = Search::new(rows, columns, Arc::default(), move |read| {
1668 seen.lock().unwrap().push(read)
1669 });
1670 let found = search
1671 .run(
1672 Start {
1673 row: 200_050,
1674 column: None,
1675 },
1676 Direction::Next,
1677 )
1678 .unwrap()
1679 .unwrap();
1680 assert_eq!((found.row, found.wrapped), (299_000, false));
1681 assert_eq!(reads.lock().unwrap().as_slice(), [50, 99_950]);
1684 }
1685
1686 #[test]
1687 fn a_view_skips_to_a_window_only_unfiltered_over_parquet_or_ipc() {
1688 use crate::widgets::datatable::reads_up_to_a_window;
1689 let dir = tempfile::tempdir().unwrap();
1690 let mut df = df!("k" => [1i64, 2, 3]).unwrap();
1691 let csv = dir.path().join("t.csv");
1692 CsvWriter::new(std::fs::File::create(&csv).unwrap())
1693 .finish(&mut df)
1694 .unwrap();
1695 let parquet = dir.path().join("t.parquet");
1696 ParquetWriter::new(std::fs::File::create(&parquet).unwrap())
1697 .finish(&mut df)
1698 .unwrap();
1699 let csv = LazyCsvReader::new(PlRefPath::try_from_path(&csv).unwrap())
1700 .finish()
1701 .unwrap();
1702 let parquet = LazyFrame::scan_parquet(
1703 PlRefPath::try_from_path(&parquet).unwrap(),
1704 Default::default(),
1705 )
1706 .unwrap();
1707 assert!(reads_up_to_a_window(&csv));
1708 assert!(!reads_up_to_a_window(&parquet));
1709 assert!(reads_up_to_a_window(&parquet.filter(col("k").gt(lit(1)))));
1710 }
1711
1712 #[test]
1713 fn bytes_and_nested_columns_are_skipped() {
1714 let schema = Schema::from_iter([
1715 Field::new("b".into(), DataType::Binary),
1716 Field::new("l".into(), DataType::List(Box::new(DataType::String))),
1717 Field::new("s".into(), DataType::String),
1718 ]);
1719 let order = ["b".to_string(), "l".to_string(), "s".to_string()];
1720 let searched: Vec<String> = searched_columns(&order, &schema, &spec("x", false))
1721 .into_iter()
1722 .map(|(n, _)| n)
1723 .collect();
1724 assert_eq!(searched, ["s"]);
1725 }
1726
1727 #[test]
1728 fn the_label_quotes_text_and_slashes_a_regex() {
1729 assert_eq!(spec("abc", false).label(), "\"abc\"");
1730 assert_eq!(spec("a+", true).label(), "/a+/");
1731 let long = spec(&"x".repeat(40), false).label();
1732 assert!(long.chars().count() < 25, "{long}");
1733 }
1734}
1735
1736#[cfg(test)]
1738mod app_tests {
1739 use super::*;
1740 use crate::widgets::datatable::DataTableState;
1741 use std::sync::mpsc::{self, Receiver};
1742 use std::time::{Duration, Instant};
1743
1744 fn key(app: &mut App, code: KeyCode) {
1745 key_with(app, code, KeyModifiers::NONE);
1746 }
1747
1748 fn key_with(app: &mut App, code: KeyCode, modifiers: KeyModifiers) {
1749 let mut next = app.event(&AppEvent::Key(KeyEvent::new(code, modifiers)));
1750 while let Some(event) = next {
1751 next = app.event(&event);
1752 }
1753 }
1754
1755 fn type_text(app: &mut App, text: &str) {
1756 for c in text.chars() {
1757 key(app, KeyCode::Char(c));
1758 }
1759 }
1760
1761 fn settle(app: &mut App, rx: &Receiver<AppEvent>) {
1763 let deadline = Instant::now() + Duration::from_secs(120);
1764 loop {
1765 let event = match rx.try_recv() {
1766 Ok(event) => event,
1767 Err(_) if app.count_waits_for_a_frame() => AppEvent::FramePainted,
1768 Err(_) if !crate::tests::work_pending(app) => return,
1769 Err(_) => {
1770 assert!(Instant::now() < deadline, "the find never answered");
1771 match rx.recv_timeout(Duration::from_millis(50)) {
1772 Ok(event) => event,
1773 Err(_) => continue,
1774 }
1775 }
1776 };
1777 let mut next = Some(event);
1778 while let Some(event) = next {
1779 next = app.event(&event);
1780 }
1781 }
1782 }
1783
1784 fn app_over(df: DataFrame) -> (App, Receiver<AppEvent>) {
1786 let (tx, rx) = mpsc::channel();
1787 let mut app = App::new(tx, crate::tests::test_runtime());
1788 let mut state =
1789 DataTableState::from_lazyframe(df.lazy(), &crate::OpenOptions::default()).unwrap();
1790 state.visible_rows = 10;
1791 app.data_table_state = Some(state);
1792 app.spawn_async_collect("Loading");
1793 settle(&mut app, &rx);
1794 (app, rx)
1795 }
1796
1797 fn haystack(rows: usize, needles: &[usize]) -> DataFrame {
1798 let values: Vec<String> = (0..rows)
1799 .map(|i| {
1800 if needles.contains(&i) {
1801 format!("needle {i}")
1802 } else {
1803 format!("hay {i}")
1804 }
1805 })
1806 .collect();
1807 df!("id" => (0..rows as i64).collect::<Vec<_>>(), "v" => values).unwrap()
1808 }
1809
1810 fn cursor(app: &App) -> usize {
1811 app.data_table_state.as_ref().unwrap().cursor_row()
1812 }
1813
1814 fn find(app: &mut App, rx: &Receiver<AppEvent>, pattern: &str) {
1815 key(app, KeyCode::Char('f'));
1816 assert_eq!(app.input_type, Some(InputType::Find));
1817 type_text(app, pattern);
1818 key(app, KeyCode::Enter);
1819 settle(app, rx);
1820 }
1821
1822 #[test]
1825 fn an_emptied_find_clears_the_find() {
1826 let (mut app, rx) = app_over(haystack(1_000, &[5, 700]));
1827 find(&mut app, &rx, "needle");
1828 assert!(app.find_mark().is_some());
1829
1830 key(&mut app, KeyCode::Char('f'));
1831 key(&mut app, KeyCode::Backspace);
1833 assert_eq!(app.find.input.value(), "");
1834 key(&mut app, KeyCode::Enter);
1835 assert_eq!(app.input_mode, InputMode::Normal);
1836 assert_eq!(app.find_mark(), None);
1837 assert_eq!(app.find_hit(), None);
1838
1839 let at = cursor(&app);
1840 key(&mut app, KeyCode::Char('n'));
1841 settle(&mut app, &rx);
1842 assert_eq!(cursor(&app), at, "n does not move");
1843 assert_eq!(app.flash_message(), Some("Nothing to find yet: / finds"));
1844 }
1845
1846 #[test]
1848 fn esc_at_the_table_clears_the_find() {
1849 let (mut app, rx) = app_over(haystack(1_000, &[5, 700]));
1850 find(&mut app, &rx, "needle");
1851 assert!(app.find_mark().is_some());
1852 key(&mut app, KeyCode::Esc);
1853 assert_eq!(app.find_mark(), None);
1854 assert_eq!(app.find_hit(), None);
1855 assert_eq!(app.input_mode, InputMode::Normal);
1856 }
1857
1858 #[test]
1859 fn f_then_n_and_capital_n_walk_the_matches_past_the_buffer() {
1860 let (mut app, rx) = app_over(haystack(300_000, &[5, 250_123]));
1861 let buffered_end = app.data_table_state.as_ref().unwrap().buffered_end();
1862 assert!(buffered_end < 250_123, "the far match is past the buffer");
1863
1864 find(&mut app, &rx, "needle");
1865 assert_eq!(app.input_mode, InputMode::Normal);
1866 assert_eq!(cursor(&app), 5);
1867 assert_eq!(app.find_hit(), Some((5, "v".to_string())));
1868 assert_eq!(
1869 app.find_mark().as_deref(),
1870 Some("find \"needle\" · match 1")
1871 );
1872
1873 key(&mut app, KeyCode::Char('n'));
1874 settle(&mut app, &rx);
1875 assert_eq!(cursor(&app), 250_123);
1876 let state = app.data_table_state.as_ref().unwrap();
1877 assert!(
1878 (state.buffered_start()..state.buffered_end()).contains(&250_123),
1879 "the rows around the match were read"
1880 );
1881 assert_eq!(
1882 app.find_mark().as_deref(),
1883 Some("find \"needle\" · match 2")
1884 );
1885
1886 key(&mut app, KeyCode::Char('n'));
1887 settle(&mut app, &rx);
1888 assert_eq!(cursor(&app), 5);
1889 assert_eq!(app.flash_message(), Some("Wrapped to the top"));
1890 assert_eq!(
1891 app.find_mark().as_deref(),
1892 Some("find \"needle\" · match 1")
1893 );
1894
1895 key(&mut app, KeyCode::Char('N'));
1896 settle(&mut app, &rx);
1897 assert_eq!(cursor(&app), 250_123);
1898 assert_eq!(app.flash_message(), Some("Wrapped to the bottom"));
1899 assert_eq!(
1900 app.find_mark().as_deref(),
1901 Some("find \"needle\""),
1902 "round from the bottom, which match it is is not known"
1903 );
1904 }
1905
1906 #[test]
1907 fn the_view_is_searched_and_left_as_it_was() {
1908 let (mut app, rx) = app_over(haystack(1_000, &[3, 700]));
1909 let state = app.data_table_state.as_mut().unwrap();
1910 state.sort(vec!["id".to_string()], false);
1911 settle(&mut app, &rx);
1912 let frame = app.data_table_state.as_ref().unwrap().len_generation();
1913 find(&mut app, &rx, "needle");
1914 let state = app.data_table_state.as_ref().unwrap();
1915 assert_eq!(cursor(&app), 299);
1917 assert_eq!(state.len_generation(), frame, "the view is the same frame");
1918 assert_eq!(state.get_sort_columns(), ["id".to_string()]);
1919 }
1920
1921 #[test]
1922 fn esc_stops_a_find_and_the_cursor_stays() {
1923 let (mut app, rx) = app_over(haystack(300_000, &[250_123]));
1924 key(&mut app, KeyCode::Char('f'));
1925 type_text(&mut app, "needle");
1926 key(&mut app, KeyCode::Enter);
1927 assert!(app.finding() && app.is_busy());
1928 assert!(app.hard_escape_while_busy(&KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)));
1929 key(&mut app, KeyCode::Esc);
1930 assert!(!app.finding());
1931 assert!(!app.is_busy(), "the keys are the user's again");
1932 assert_eq!(app.flash_message(), Some(CANCELLED));
1933 settle(&mut app, &rx);
1934 assert_eq!(cursor(&app), 0, "a cancelled find moves nothing");
1935 assert_eq!(app.find_hit(), None);
1936 }
1937
1938 #[test]
1939 fn ctrl_o_stops_a_find_on_the_way_home() {
1940 let (mut app, rx) = app_over(haystack(300_000, &[250_123]));
1941 find_started(&mut app);
1942 key_with(&mut app, KeyCode::Char('o'), KeyModifiers::CONTROL);
1943 assert!(!app.finding());
1944 assert_eq!(app.input_mode, InputMode::Home);
1945 settle(&mut app, &rx);
1946 assert_eq!(cursor(&app), 0);
1947 }
1948
1949 fn find_started(app: &mut App) {
1950 key(app, KeyCode::Char('f'));
1951 type_text(app, "needle");
1952 key(app, KeyCode::Enter);
1953 assert!(app.finding());
1954 }
1955
1956 #[test]
1957 fn the_prompt_toggles_regex_and_column_and_says_why_a_regex_is_bad() {
1958 let (mut app, rx) = app_over(haystack(20, &[]));
1959 key(&mut app, KeyCode::Char('f'));
1960 key_with(&mut app, KeyCode::Char('r'), KeyModifiers::CONTROL);
1961 key_with(&mut app, KeyCode::Char('l'), KeyModifiers::CONTROL);
1962 assert!(app.find.regex && app.find.in_column);
1963 assert_eq!(app.find.column.as_deref(), Some("id"));
1964 type_text(&mut app, "(1");
1965 key(&mut app, KeyCode::Enter);
1966 assert_eq!(app.input_type, Some(InputType::Find), "it stays open");
1967 assert!(
1968 app.find
1969 .error
1970 .as_deref()
1971 .unwrap()
1972 .starts_with("Not a regex")
1973 );
1974 key(&mut app, KeyCode::Backspace);
1975 key(&mut app, KeyCode::Backspace);
1976 assert!(app.find.error.is_none(), "an edit clears the reason");
1977 type_text(&mut app, "^12$");
1979 key(&mut app, KeyCode::Enter);
1980 settle(&mut app, &rx);
1981 assert_eq!(app.find_hit(), Some((12, "id".to_string())));
1982 assert_eq!(
1983 app.find_mark().as_deref(),
1984 Some("find /^12$/ in id · match 1")
1985 );
1986 }
1987
1988 #[test]
1991 fn the_column_cursor_is_the_find_column_and_a_match_moves_it() {
1992 let df = df!(
1993 "id" => (0..20i64).collect::<Vec<_>>(),
1994 "v" => (0..20).map(|i| format!("hay {i}")).collect::<Vec<_>>(),
1995 "w" => (0..20).map(|i| if i == 7 { "needle".to_string() } else { format!("w {i}") }).collect::<Vec<_>>(),
1996 )
1997 .unwrap();
1998 let (mut app, rx) = app_over(df);
1999 let current = |app: &App| {
2000 app.data_table_state
2001 .as_ref()
2002 .unwrap()
2003 .current_column()
2004 .map(str::to_string)
2005 };
2006 key(&mut app, KeyCode::Char('l'));
2007 assert_eq!(current(&app).as_deref(), Some("v"));
2008 key(&mut app, KeyCode::Char('f'));
2009 key_with(&mut app, KeyCode::Char('l'), KeyModifiers::CONTROL);
2010 assert_eq!(app.find.column.as_deref(), Some("v"));
2011 type_text(&mut app, "needle");
2012 key(&mut app, KeyCode::Enter);
2013 settle(&mut app, &rx);
2014 assert_eq!(app.find_hit(), None, "not in v");
2015
2016 key(&mut app, KeyCode::Char('f'));
2018 key_with(&mut app, KeyCode::Char('l'), KeyModifiers::CONTROL);
2019 key(&mut app, KeyCode::Enter);
2020 settle(&mut app, &rx);
2021 assert_eq!(app.find_hit(), Some((7, "w".to_string())));
2022 assert_eq!(cursor(&app), 7);
2023 assert_eq!(current(&app).as_deref(), Some("w"));
2024 key(&mut app, KeyCode::Char('f'));
2026 assert_eq!(app.find.column.as_deref(), Some("w"));
2027 }
2028
2029 #[test]
2032 fn n_and_capital_n_start_from_the_cursors_cell() {
2033 let cell = |hit: bool, i: usize| {
2034 if hit {
2035 "needle".to_string()
2036 } else {
2037 format!("hay {i}")
2038 }
2039 };
2040 let df = df!(
2041 "a" => (0..20).map(|i| cell(i == 2, i)).collect::<Vec<_>>(),
2042 "b" => (0..20).map(|i| cell(i == 2, i)).collect::<Vec<_>>(),
2043 "c" => (0..20).map(|i| cell(false, i)).collect::<Vec<_>>(),
2044 "d" => (0..20).map(|i| cell(i == 2 || i == 9, i)).collect::<Vec<_>>(),
2045 )
2046 .unwrap();
2047 let (mut app, rx) = app_over(df);
2048 find(&mut app, &rx, "needle");
2049 assert_eq!(app.find_hit(), Some((2, "a".to_string())));
2050 key(&mut app, KeyCode::Char('l'));
2052 key(&mut app, KeyCode::Char('l'));
2053 key(&mut app, KeyCode::Char('n'));
2054 settle(&mut app, &rx);
2055 assert_eq!(app.find_hit(), Some((2, "d".to_string())), "right of c");
2056 key(&mut app, KeyCode::Char('h'));
2058 key(&mut app, KeyCode::Char('N'));
2059 settle(&mut app, &rx);
2060 assert_eq!(app.find_hit(), Some((2, "b".to_string())), "left of c");
2061 for _ in 0..3 {
2063 key(&mut app, KeyCode::Char('j'));
2064 }
2065 assert_eq!(cursor(&app), 5);
2066 key(&mut app, KeyCode::Char('n'));
2067 settle(&mut app, &rx);
2068 assert_eq!(app.find_hit(), Some((9, "d".to_string())));
2069 }
2070
2071 #[test]
2072 fn no_match_says_so_and_nothing_moves() {
2073 let (mut app, rx) = app_over(haystack(50, &[]));
2074 key(&mut app, KeyCode::Char('j'));
2075 find(&mut app, &rx, "needle");
2076 assert_eq!(cursor(&app), 1);
2077 assert_eq!(app.flash_message(), Some("No match for \"needle\""));
2078 }
2079
2080 #[test]
2081 fn n_before_any_find_says_how_to_start_one() {
2082 let (mut app, _rx) = app_over(haystack(5, &[]));
2083 key(&mut app, KeyCode::Char('N'));
2084 assert_eq!(app.flash_message(), Some("Nothing to find yet: / finds"));
2085 assert!(
2086 !app.data_table_state.as_ref().unwrap().row_numbers(),
2087 "N no longer toggles row numbers"
2088 );
2089 }
2090
2091 #[test]
2092 fn hash_toggles_row_numbers() {
2093 let (mut app, _rx) = app_over(haystack(5, &[]));
2094 key(&mut app, KeyCode::Char('#'));
2095 assert!(app.data_table_state.as_ref().unwrap().row_numbers());
2096 key(&mut app, KeyCode::Char('#'));
2097 assert!(!app.data_table_state.as_ref().unwrap().row_numbers());
2098 }
2099
2100 #[test]
2102 fn width_keys_set_the_column_cursors_width() {
2103 use crate::widgets::column_widths::{UNSEEN_WIDTH, WIDTH_STEP, WidthChoice};
2104 let (mut app, _rx) = app_over(haystack(5, &[]));
2105 let name = app
2106 .data_table_state
2107 .as_ref()
2108 .unwrap()
2109 .current_column()
2110 .unwrap()
2111 .to_string();
2112 let width = |app: &App| app.data_table_state.as_ref().unwrap().width_choice(&name);
2113 let start = app
2114 .data_table_state
2115 .as_ref()
2116 .unwrap()
2117 .on_screen_width(&name)
2118 .unwrap_or(UNSEEN_WIDTH);
2119 key(&mut app, KeyCode::Char('>'));
2120 assert_eq!(width(&app), WidthChoice::Manual(start + WIDTH_STEP));
2121 key(&mut app, KeyCode::Char('<'));
2122 assert_eq!(width(&app), WidthChoice::Manual(start));
2123 key(&mut app, KeyCode::Char('='));
2124 assert_eq!(width(&app), WidthChoice::Fit);
2125 key(&mut app, KeyCode::Char('w'));
2126 assert_eq!(width(&app), WidthChoice::Auto);
2127 }
2128
2129 #[test]
2132 fn n_after_the_view_changes_starts_from_the_cursor_in_the_new_view() {
2133 let (mut app, rx) = app_over(haystack(1_000, &[5, 700]));
2134 find(&mut app, &rx, "needle");
2135 assert_eq!(app.find_hit(), Some((5, "v".to_string())));
2136 let state = app.data_table_state.as_mut().unwrap();
2137 state.sort(vec!["id".to_string()], false);
2138 settle(&mut app, &rx);
2139 assert_eq!(app.find_hit(), None, "the old cell is not this view's");
2140 assert_eq!(app.find_mark().as_deref(), Some("find \"needle\""));
2141 let from = cursor(&app);
2142 key(&mut app, KeyCode::Char('n'));
2143 settle(&mut app, &rx);
2144 let expected = if from < 299 { 299 } else { 994 };
2146 assert_eq!(app.find_hit(), Some((expected, "v".to_string())));
2147 assert_eq!(cursor(&app), expected);
2148 }
2149
2150 #[test]
2153 fn the_found_cell_is_highlighted_on_the_cursor_row() {
2154 for row_numbers in [false, true] {
2155 found_cell_is_highlighted(row_numbers);
2156 }
2157 }
2158
2159 fn found_cell_is_highlighted(row_numbers: bool) {
2160 use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
2161 let (mut app, rx) = app_over(haystack(30, &[4]));
2162 if row_numbers {
2163 key(&mut app, KeyCode::Char('#'));
2164 }
2165 find(&mut app, &rx, "needle 4");
2166 let style = app.theme.find_match_style();
2167 let draw = |app: &mut App| {
2168 let area = Rect::new(0, 0, 80, 24);
2169 let mut buf = Buffer::empty(area);
2170 app.render(area, &mut buf);
2171 buf
2172 };
2173 let buf = draw(&mut app);
2174 let marked: Vec<(u16, u16)> = buf
2175 .content()
2176 .iter()
2177 .enumerate()
2178 .filter(|(_, cell)| cell.bg == style.bg.unwrap())
2179 .map(|(i, _)| ((i % 80) as u16, (i / 80) as u16))
2180 .collect();
2181 assert!(!marked.is_empty(), "the cell is marked");
2182 let y = marked[0].1;
2183 let row: String = (0..80).map(|x| buf[(x, y)].symbol().to_string()).collect();
2184 assert!(row.contains("needle 4"), "{row}");
2185 let text: String = marked
2186 .iter()
2187 .map(|&(x, y)| buf[(x, y)].symbol().to_string())
2188 .collect();
2189 assert!(
2190 text.contains("needle 4"),
2191 "the mark is on the value: {text:?}"
2192 );
2193
2194 let cell_cursor = app.theme.cell_cursor_style();
2199 let is_cell_cursor = |cell: &ratatui::buffer::Cell| match cell_cursor.bg {
2200 Some(bg) => cell.bg == bg,
2201 None => cell.modifier.contains(ratatui::style::Modifier::REVERSED),
2202 };
2203 assert!(
2204 marked.iter().all(|&(x, y)| !is_cell_cursor(&buf[(x, y)])),
2205 "one style on the found cell"
2206 );
2207 assert!(
2208 (0..80).any(|x| is_cell_cursor(&buf[(x, 0)])),
2209 "the header carries the cursor"
2210 );
2211
2212 key(&mut app, KeyCode::Char('h'));
2215 let buf = draw(&mut app);
2216 assert!(
2217 !buf.content()
2218 .iter()
2219 .any(|cell| cell.bg == style.bg.unwrap()),
2220 "off its column, the cell is plain"
2221 );
2222 assert!((0..80).any(|x| is_cell_cursor(&buf[(x, y)])));
2223 key(&mut app, KeyCode::Char('l'));
2224 assert!(
2225 draw(&mut app)
2226 .content()
2227 .iter()
2228 .any(|cell| cell.bg == style.bg.unwrap()),
2229 "back on it, marked again"
2230 );
2231
2232 key(&mut app, KeyCode::Char('j'));
2233 let buf = draw(&mut app);
2234 assert!(
2235 !buf.content()
2236 .iter()
2237 .any(|cell| cell.bg == style.bg.unwrap()),
2238 "off its row, the cell is plain"
2239 );
2240 }
2241}