1use std::ops::Range;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12
13use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
14use polars::prelude::*;
15
16use crate::app::jobs::{Answer, Job, Progress};
17use crate::app::modals::filter_modal::{FilterOperator, FilterStatement, LogicalOperator};
18use crate::table::ViewRows;
19use crate::widgets::text_input::{TextInput, TextInputEvent};
20use crate::{App, AppEvent, InputMode, InputType};
21
22const ROW: &str = "__datui_find_row";
24const ROWS: &str = "__datui_find_rows";
26const FIRST_WINDOW: usize = 65_536;
32const LARGEST_WINDOW: usize = u32::MAX as usize;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct FindSpec {
38 pub pattern: String,
39 pub regex: bool,
41 pub fuzzy: bool,
44 pub column: Option<String>,
46}
47
48impl FindSpec {
49 pub fn ignores_case(&self) -> bool {
52 let mut chars = self.pattern.chars();
53 while let Some(c) = chars.next() {
54 if self.regex && c == '\\' {
55 chars.next();
56 continue;
57 }
58 if c.is_uppercase() {
59 return false;
60 }
61 }
62 true
63 }
64
65 pub(crate) fn regex_source(&self) -> Option<String> {
68 let case = if self.ignores_case() { "(?i)" } else { "" };
69 if self.fuzzy {
70 let letters: Vec<String> = self
71 .pattern
72 .chars()
73 .filter(|c| !c.is_whitespace())
74 .map(|c| regex::escape(&c.to_string()))
75 .collect();
76 return Some(format!("{case}{}", letters.join(".*")));
77 }
78 match (self.regex, self.ignores_case()) {
79 (false, false) => None,
80 (false, true) => Some(format!("{case}{}", regex::escape(&self.pattern))),
81 (true, _) => Some(format!("{case}{}", self.pattern)),
82 }
83 }
84
85 pub fn check(&self) -> Result<(), String> {
87 let Some(source) = self.regex_source().filter(|_| self.regex && !self.fuzzy) else {
88 return Ok(());
89 };
90 regex::Regex::new(&source).map(|_| ()).map_err(|e| {
91 let text = e.to_string();
94 let reason = text
95 .lines()
96 .rev()
97 .find(|line| !line.trim().is_empty())
98 .unwrap_or("invalid")
99 .trim()
100 .trim_start_matches("error: ")
101 .to_string();
102 format!("Not a regex: {reason}")
103 })
104 }
105
106 fn matches(&self, text: Expr) -> Expr {
108 let found = match self.regex_source() {
109 None => text.str().contains_literal(lit(self.pattern.clone())),
110 Some(source) => text.str().contains(lit(source), true),
111 };
112 found.fill_null(lit(false))
113 }
114
115 pub fn label(&self) -> String {
118 const LONGEST: usize = 18;
119 let g = crate::glyphs::get();
120 let mut text: String = self.pattern.chars().take(LONGEST).collect();
121 if self.pattern.chars().count() > LONGEST {
122 text.push_str(g.ellipsis);
123 }
124 if self.fuzzy {
125 format!("~{text}")
126 } else if self.regex {
127 format!("/{text}/")
128 } else {
129 format!("\"{text}\"")
130 }
131 }
132}
133
134pub(crate) fn cell_matches(spec: &FindSpec, name: &str, dtype: &DataType) -> Option<Expr> {
137 Some(spec.matches(text_of(name, dtype)?))
138}
139
140fn text_of(name: &str, dtype: &DataType) -> Option<Expr> {
143 if dtype.is_nested()
144 || dtype.is_object()
145 || matches!(
146 dtype,
147 DataType::Binary | DataType::BinaryOffset | DataType::Null
148 )
149 {
150 return None;
151 }
152 let column = col(name);
153 Some(if dtype.is_string() {
154 column
155 } else if let DataType::Duration(unit) = dtype {
156 duration_text(column, *unit)
158 } else if crate::past_calendar::can_leave_calendar(dtype) {
159 crate::past_calendar::text_expr(column, polars::chunked_array::cast::CastOptions::NonStrict)
162 } else {
163 column.cast(DataType::String)
164 })
165}
166
167fn duration_text(column: Expr, unit: TimeUnit) -> Expr {
169 column.map_with_fmt_str(
170 move |c| {
171 let text = c
172 .as_materialized_series()
173 .to_physical_repr()
174 .i64()?
175 .apply_into_string_amortized(|v, out| {
176 use std::fmt::Write;
177 let _ = write!(out, "{}", AnyValue::Duration(v, unit));
178 });
179 Ok(text.with_name(c.name().clone()).into_column())
180 },
181 |_: &Schema, field: &Field| Ok(Field::new(field.name().clone(), DataType::String)),
182 "find_duration_text",
183 )
184}
185
186pub(crate) fn searched_columns(
188 order: &[String],
189 schema: &Schema,
190 spec: &FindSpec,
191) -> Vec<(String, Expr)> {
192 order
193 .iter()
194 .filter(|name| spec.column.as_ref().is_none_or(|only| only == *name))
195 .filter_map(|name| {
196 let text = text_of(name, schema.get(name)?)?;
197 Some((name.clone(), spec.matches(text)))
198 })
199 .collect()
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum Direction {
205 Next,
206 Previous,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub(crate) struct Start {
213 pub(crate) row: usize,
214 pub(crate) column: Option<At>,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub(crate) enum At {
220 On(usize),
222 Before(usize),
224}
225
226impl At {
227 fn ahead(self) -> usize {
229 match self {
230 At::On(c) => c + 1,
231 At::Before(c) => c,
232 }
233 }
234
235 fn behind(self) -> usize {
237 match self {
238 At::On(c) | At::Before(c) => c,
239 }
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct Found {
246 pub row: usize,
247 pub column: String,
248 pub wrapped: bool,
250}
251
252#[derive(Debug, Clone)]
255struct Limit {
256 row: usize,
257 columns: Range<usize>,
258}
259
260pub(crate) const CANCELLED: &str = "Find cancelled";
262
263pub(crate) struct Search {
265 rows: ViewRows,
266 columns: Vec<String>,
267 exprs: Vec<Expr>,
268 stop: Arc<AtomicBool>,
269 report: Box<dyn Fn(usize) + Send>,
270 read: usize,
272 window: usize,
274}
275
276impl Search {
277 pub(crate) fn new(
278 rows: ViewRows,
279 columns: Vec<(String, Expr)>,
280 stop: Arc<AtomicBool>,
281 report: impl Fn(usize) + Send + 'static,
282 ) -> Self {
283 let (names, exprs): (Vec<String>, Vec<Expr>) = columns
284 .into_iter()
285 .enumerate()
286 .map(|(i, (name, expr))| (name, expr.alias(format!("m{i}"))))
287 .unzip();
288 let mut rows = rows;
291 if rows
292 .buffer
293 .as_ref()
294 .is_some_and(|(df, _)| names.iter().any(|n| df.column(n).is_err()))
295 {
296 rows.buffer = None;
297 }
298 let window = if rows.whole {
300 LARGEST_WINDOW
301 } else {
302 FIRST_WINDOW
303 };
304 Self {
305 rows,
306 columns: names,
307 exprs,
308 stop,
309 report: Box::new(report),
310 read: 0,
311 window,
312 }
313 }
314
315 pub(crate) fn run(
318 mut self,
319 start: Start,
320 direction: Direction,
321 ) -> Result<Option<Found>, String> {
322 let n = self.columns.len();
323 let r = start.row;
324 let found = |(row, column): (usize, usize), wrapped: bool, columns: &[String]| Found {
325 row,
326 column: columns[column].clone(),
327 wrapped,
328 };
329 match direction {
330 Direction::Next => {
331 let ahead = start.column.map(|at| Limit {
332 row: r,
333 columns: at.ahead()..n,
334 });
335 if let Some(hit) = self.forward(r, None, ahead.as_ref(), false)? {
336 return Ok(Some(found(hit, false, &self.columns)));
337 }
338 let (end, behind) = match start.column {
341 Some(at) => (
342 r + 1,
343 Some(Limit {
344 row: r,
345 columns: 0..at.ahead(),
346 }),
347 ),
348 None => (r, None),
349 };
350 Ok(self
351 .forward(0, Some(end), behind.as_ref(), false)?
352 .map(|hit| found(hit, true, &self.columns)))
353 }
354 Direction::Previous => {
355 let behind = start.column.map(|at| Limit {
356 row: r,
357 columns: 0..at.behind(),
358 });
359 if let Some(hit) = self.backward(0, r + 1, behind.as_ref())? {
360 return Ok(Some(found(hit, false, &self.columns)));
361 }
362 let (from, ahead) = match start.column {
364 Some(at) => (
365 r,
366 Some(Limit {
367 row: r,
368 columns: at.behind()..n,
369 }),
370 ),
371 None => (r + 1, None),
372 };
373 let hit = match self.rows.num_rows {
374 Some(total) => self.backward(from, total, ahead.as_ref())?,
375 None => self.forward(from, None, ahead.as_ref(), true)?,
377 };
378 Ok(hit.map(|hit| found(hit, true, &self.columns)))
379 }
380 }
381 }
382
383 fn forward(
386 &mut self,
387 from: usize,
388 end: Option<usize>,
389 limit: Option<&Limit>,
390 last: bool,
391 ) -> Result<Option<(usize, usize)>, String> {
392 let end = end.or(self.rows.num_rows);
393 let mut at = from;
394 let mut best = None;
395 while end.is_none_or(|end| at < end) {
396 self.check_stop()?;
397 let (len, buffered) = self.plan_forward(at, end, last);
398 if len == 0 {
399 break;
400 }
401 let (rows, hit) = self.window_at(at, len, buffered, limit, last)?;
402 if hit.is_some() {
403 if !last {
404 return Ok(hit);
405 }
406 best = hit;
407 }
408 if rows < len {
409 break;
410 }
411 at += len;
412 }
413 Ok(best)
414 }
415
416 fn backward(
418 &mut self,
419 from: usize,
420 end: usize,
421 limit: Option<&Limit>,
422 ) -> Result<Option<(usize, usize)>, String> {
423 let mut end = end;
424 while end > from {
425 self.check_stop()?;
426 let (start, buffered) = self.plan_backward(from, end);
427 let (_, hit) = self.window_at(start, end - start, buffered, limit, true)?;
428 if hit.is_some() {
429 return Ok(hit);
430 }
431 end = start;
432 }
433 Ok(None)
434 }
435
436 fn check_stop(&self) -> Result<(), String> {
437 if self.stop.load(Ordering::Relaxed) {
438 return Err(CANCELLED.to_string());
439 }
440 Ok(())
441 }
442
443 fn buffered(&self) -> Option<(usize, usize)> {
445 self.rows
446 .buffer
447 .as_ref()
448 .map(|(df, start)| (*start, start + df.height()))
449 }
450
451 fn plan_forward(&mut self, at: usize, end: Option<usize>, last: bool) -> (usize, bool) {
454 let end = end.unwrap_or(usize::MAX);
455 if let Some((start, stop)) = self.buffered()
456 && (start..stop).contains(&at)
457 {
458 return (stop.min(end) - at, true);
459 }
460 let mut window = self.window_for(last);
461 if self.rows.reads_up_to {
464 window = window.max(at);
465 }
466 let mut stop = at.saturating_add(window).min(end);
467 if let Some((start, _)) = self.buffered()
468 && at < start
469 {
470 stop = stop.min(start);
471 }
472 self.window = window.saturating_mul(2).min(LARGEST_WINDOW);
473 (stop - at, false)
474 }
475
476 fn window_for(&self, whole_range: bool) -> usize {
479 if whole_range && self.rows.reads_up_to {
480 LARGEST_WINDOW
481 } else {
482 self.window
483 }
484 }
485
486 fn plan_backward(&mut self, from: usize, end: usize) -> (usize, bool) {
489 if let Some((start, stop)) = self.buffered()
490 && (start..stop).contains(&(end - 1))
491 {
492 return (start.max(from), true);
493 }
494 let mut start = end.saturating_sub(self.window_for(true)).max(from);
495 if let Some((_, stop)) = self.buffered()
496 && end > stop
497 {
498 start = start.max(stop);
499 }
500 self.window = self.window.saturating_mul(2).min(LARGEST_WINDOW);
501 (start, false)
502 }
503
504 fn window_at(
508 &mut self,
509 start: usize,
510 len: usize,
511 buffered: bool,
512 limit: Option<&Limit>,
513 last: bool,
514 ) -> Result<(usize, Option<(usize, usize)>), String> {
515 let message = |e: PolarsError| crate::error_display::user_message_from_polars(&e);
516 let lf = match self.rows.buffer.as_ref().filter(|_| buffered) {
517 Some((df, at)) => df
518 .slice((start - at) as i64, len)
519 .lazy()
520 .select(self.exprs.clone()),
521 None => self
522 .rows
523 .window(start, len, self.exprs.clone())
524 .map_err(message)?,
525 };
526 let offset = IdxSize::try_from(start)
527 .map_err(|_| "The view has too many rows to find in".to_string())?;
528 let row = || col(ROW).cast(DataType::UInt64);
529 let mut aggregates = vec![polars::prelude::len().cast(DataType::UInt64).alias(ROWS)];
530 for i in 0..self.columns.len() {
531 let mut cell = col(format!("m{i}"));
532 if let Some(limit) = limit
533 && !limit.columns.contains(&i)
534 {
535 cell = cell.and(row().neq(lit(limit.row as u64)));
536 }
537 let rows = row().filter(cell);
538 let at = if last { rows.max() } else { rows.min() };
539 aggregates.push(at.alias(format!("f{i}")));
540 }
541 let lf = lf.with_row_index(ROW, Some(offset)).select(aggregates);
542 let df =
543 crate::analysis::statistics::collect_lazy(lf, self.rows.streaming).map_err(message)?;
544 let get = |name: &str| -> Option<u64> { df.column(name).ok()?.u64().ok()?.get(0) };
545 let rows = get(ROWS).unwrap_or(0) as usize;
546 let mut best: Option<(usize, usize)> = None;
547 for i in 0..self.columns.len() {
548 let Some(at) = get(&format!("f{i}")).map(|r| r as usize) else {
549 continue;
550 };
551 let better = best.is_none_or(|(b, _)| if last { at >= b } else { at < b });
554 if better {
555 best = Some((at, i));
556 }
557 }
558 self.read += rows;
559 (self.report)(self.read);
560 Ok((rows, best))
561 }
562}
563
564pub struct Find {
566 pub input: TextInput,
567 pub regex: bool,
569 pub fuzzy: bool,
571 pub in_column: bool,
573 pub column: Option<String>,
575 pub error: Option<String>,
577 pub active: Option<ActiveFind>,
579 pub live: Option<LiveMatches>,
581 live_rows: Option<(usize, usize, u64)>,
584 pub read: Option<usize>,
586}
587
588pub type MatchCells = std::collections::HashMap<String, std::collections::HashSet<usize>>;
591
592#[derive(Debug, Clone, Default)]
595pub struct LiveMatches {
596 pub cells: Arc<MatchCells>,
597}
598
599impl LiveMatches {
600 pub fn within(&self, rows: Range<usize>) -> usize {
602 self.cells
603 .values()
604 .map(|hits| hits.iter().filter(|r| rows.contains(r)).count())
605 .sum()
606 }
607}
608
609#[derive(Debug, Clone)]
611pub struct ActiveFind {
612 pub spec: FindSpec,
613 pub dataset: u64,
615 pub frame: u64,
617 pub hit: Option<(usize, String)>,
619 pub ordinal: Option<usize>,
621}
622
623impl Find {
624 pub fn new(input: TextInput) -> Self {
625 Self {
626 input,
627 regex: false,
628 fuzzy: false,
629 in_column: false,
630 column: None,
631 error: None,
632 active: None,
633 live: None,
634 live_rows: None,
635 read: None,
636 }
637 }
638
639 pub(crate) fn prompt_spec(&self) -> FindSpec {
641 FindSpec {
642 pattern: self.input.value().to_string(),
643 regex: self.regex,
644 fuzzy: self.fuzzy,
645 column: self.column.clone().filter(|_| self.in_column),
646 }
647 }
648}
649
650#[derive(Debug, Clone)]
652pub(crate) struct FindRun {
653 pub(crate) stop: Arc<AtomicBool>,
654 pub(crate) dataset: u64,
655 pub(crate) frame: u64,
656 pub(crate) direction: Direction,
657 pub(crate) from_top: bool,
659 pub(crate) from_hit: Option<Option<usize>>,
661}
662
663fn finding_status(spec: &FindSpec, read: Option<usize>) -> String {
665 match read {
666 Some(rows) if rows > 0 => format!(
669 "Finding {}... {} rows",
670 spec.label(),
671 crate::home::discover::format_rows(rows)
672 ),
673 _ => format!("Finding {}...", spec.label()),
674 }
675}
676
677impl App {
678 pub(crate) fn open_find(&mut self) {
681 if self.data_table_state.is_none() {
682 return;
683 }
684 self.prompt.find.column = self.find_column();
685 self.prompt.find.error = None;
686 match self.prompt.find.active.as_ref() {
687 Some(active) => {
688 let pattern = active.spec.pattern.clone();
689 self.prompt.find.input.set_value(pattern);
690 self.prompt.find.input.select_all();
691 }
692 None => self.prompt.find.input.clear(),
693 }
694 self.prompt.find.input.set_focused(true);
695 self.input_mode = InputMode::Editing;
696 self.prompt.input_type = Some(InputType::Find);
697 self.refresh_live_matches();
698 }
699
700 pub(crate) fn refresh_live_matches(&mut self) {
703 self.prompt.find.live = None;
704 self.prompt.find.live_rows = self.rows_on_hand_key();
705 let spec = self.prompt.find.prompt_spec();
706 if spec.pattern.trim().is_empty() || spec.check().is_err() {
707 return;
708 }
709 let Some((df, start)) = self.live_window() else {
710 return;
711 };
712 let Some(state) = self.data_table_state.as_ref() else {
713 return;
714 };
715 let columns: Vec<(String, Expr)> =
716 searched_columns(state.get_column_order(), state.schema(), &spec)
717 .into_iter()
718 .filter(|(name, _)| df.column(name).is_ok())
719 .collect();
720 if columns.is_empty() {
721 self.prompt.find.live = Some(LiveMatches::default());
722 return;
723 }
724 let exprs: Vec<Expr> = columns
725 .iter()
726 .enumerate()
727 .map(|(i, (_, expr))| expr.clone().alias(format!("m{i}")))
728 .collect();
729 let Ok(found) = df.lazy().select(exprs).collect() else {
731 return;
732 };
733 let mut cells = MatchCells::new();
734 for (i, (name, _)) in columns.iter().enumerate() {
735 let Ok(hits) = found
736 .column(&format!("m{i}"))
737 .and_then(|c| c.bool().cloned())
738 else {
739 continue;
740 };
741 let rows: std::collections::HashSet<usize> = hits
742 .iter()
743 .enumerate()
744 .filter(|(_, hit)| *hit == Some(true))
745 .map(|(row, _)| start + row)
746 .collect();
747 if !rows.is_empty() {
748 cells.insert(name.clone(), rows);
749 }
750 }
751 self.prompt.find.live = Some(LiveMatches {
752 cells: Arc::new(cells),
753 });
754 }
755
756 fn live_window(&self) -> Option<(DataFrame, usize)> {
759 let state = self.data_table_state.as_ref()?;
760 let (df, start) = state.rows_on_hand()?;
761 let page = state.visible_rows.max(1);
762 let from = state.start_row().saturating_sub(page).max(start);
763 let to = (state.start_row() + 2 * page).min(start + df.height());
764 (to > from).then(|| (df.slice((from - start) as i64, to - from), from))
765 }
766
767 fn rows_on_hand_key(&self) -> Option<(usize, usize, u64)> {
770 let state = self.data_table_state.as_ref()?;
771 let (df, start) = self.live_window()?;
772 Some((start, df.height(), state.len_generation()))
773 }
774
775 pub(crate) fn refresh_stale_live_matches(&mut self) -> bool {
778 let stale = self.prompt.input_type == Some(InputType::Find)
779 && self.prompt.find.live_rows != self.rows_on_hand_key();
780 if stale {
781 self.refresh_live_matches();
782 }
783 stale
784 }
785
786 pub fn live_on_screen(&self) -> Option<usize> {
789 let live = self.prompt.find.live.as_ref()?;
790 let state = self.data_table_state.as_ref()?;
791 let start = state.start_row();
792 Some(live.within(start..start + state.visible_rows))
793 }
794
795 pub fn live_cells(&self) -> Option<Arc<MatchCells>> {
797 (self.prompt.input_type == Some(InputType::Find))
798 .then_some(self.prompt.find.live.as_ref())
799 .flatten()
800 .map(|live| live.cells.clone())
801 }
802
803 pub(crate) fn find_column(&self) -> Option<String> {
806 self.data_table_state
807 .as_ref()?
808 .current_column()
809 .map(str::to_string)
810 }
811
812 fn close_find_prompt(&mut self) {
813 self.prompt.find.input.set_focused(false);
814 self.prompt.find.error = None;
815 self.prompt.find.live = None;
816 self.show_table();
817 }
818
819 pub(crate) fn find_prompt_key(&mut self, event: &KeyEvent) -> Option<AppEvent> {
822 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
823 if event.is_press() && ctrl {
824 match event.code {
825 KeyCode::Char('r') => {
826 self.prompt.find.regex = !self.prompt.find.regex;
827 self.prompt.find.fuzzy &= !self.prompt.find.regex;
828 self.prompt.find.error = None;
829 self.refresh_live_matches();
830 return None;
831 }
832 KeyCode::Char('t') => {
833 self.prompt.find.fuzzy = !self.prompt.find.fuzzy;
834 self.prompt.find.regex &= !self.prompt.find.fuzzy;
835 self.prompt.find.error = None;
836 self.refresh_live_matches();
837 return None;
838 }
839 KeyCode::Char('l') => {
840 self.prompt.find.in_column = !self.prompt.find.in_column;
841 self.refresh_live_matches();
842 return None;
843 }
844 KeyCode::Char('g') => return self.keep_matches(),
845 _ => {}
846 }
847 }
848 let before = self.prompt.find.input.value().to_string();
849 match self.prompt.find.input.handle_key(event, Some(&self.cache)) {
850 TextInputEvent::Submit => {
851 let spec = self.prompt.find.prompt_spec();
852 if spec.pattern.is_empty() {
853 self.prompt.find.active = None;
855 self.close_find_prompt();
856 return None;
857 }
858 if let Err(reason) = spec.check() {
859 self.prompt.find.error = Some(reason);
860 return None;
861 }
862 let _ = self.prompt.find.input.save_to_history(&self.cache);
863 self.close_find_prompt();
864 self.start_find(spec, Direction::Next, true);
865 }
866 TextInputEvent::Cancel => self.close_find_prompt(),
867 TextInputEvent::HistoryChanged | TextInputEvent::None => {
868 if self.prompt.find.input.value() != before {
869 self.prompt.find.error = None;
870 self.refresh_live_matches();
871 }
872 }
873 }
874 None
875 }
876
877 fn keep_matches(&mut self) -> Option<AppEvent> {
880 let spec = self.prompt.find.prompt_spec();
881 if spec.pattern.trim().is_empty() {
882 return None;
883 }
884 if let Err(reason) = spec.check() {
885 self.prompt.find.error = Some(reason);
886 return None;
887 }
888 let state = self.data_table_state.as_ref()?;
889 let operator = if spec.fuzzy {
890 FilterOperator::HasFuzzy
891 } else if spec.regex {
892 FilterOperator::HasRegex
893 } else {
894 FilterOperator::Has
895 };
896 let columns = if spec.column.is_none() {
898 state.get_column_order().to_vec()
899 } else {
900 Vec::new()
901 };
902 let statement = FilterStatement {
903 columns,
904 column: spec
905 .column
906 .clone()
907 .unwrap_or_else(|| crate::app::modals::filter_modal::ANY_COLUMN.to_string()),
908 operator,
909 value: spec.pattern.clone(),
910 logical_op: LogicalOperator::And,
911 };
912 let mut statements = state.view_filters().to_vec();
913 let frame = state.len_generation();
914 let _ = self.prompt.find.input.save_to_history(&self.cache);
915 self.close_find_prompt();
916 self.prompt.find.active = Some(ActiveFind {
917 spec,
918 dataset: self.dataset_generation,
919 frame,
920 hit: None,
921 ordinal: None,
922 });
923 if statements.contains(&statement) {
924 return None;
925 }
926 statements.push(statement);
927 Some(AppEvent::Applied(crate::Applied::Filter(statements)))
928 }
929
930 pub(crate) fn find_again(&mut self, direction: Direction) {
932 match self.prompt.find.active.as_ref() {
933 Some(active) if active.dataset == self.dataset_generation => {
934 let spec = active.spec.clone();
935 self.start_find(spec, direction, false);
936 }
937 _ => self.flash_note("Nothing to find yet: / finds".to_string()),
938 }
939 }
940
941 pub fn find_hit(&self) -> Option<(usize, String)> {
943 let active = self.prompt.find.active.as_ref()?;
944 let state = self.data_table_state.as_ref()?;
945 (active.dataset == self.dataset_generation && active.frame == state.len_generation())
946 .then(|| active.hit.clone())
947 .flatten()
948 }
949
950 pub fn find_mark(&self) -> Option<String> {
953 let active = self.prompt.find.active.as_ref()?;
954 if active.dataset != self.dataset_generation
957 || self.finding()
958 || self.prompt.input_type == Some(InputType::Find)
959 {
960 return None;
961 }
962 let mut mark = format!("find {}", active.spec.label());
963 if let Some(column) = &active.spec.column {
964 mark.push_str(&format!(" in {column}"));
965 }
966 if let Some(k) = active.ordinal.filter(|_| self.find_hit().is_some()) {
967 mark.push_str(&format!(
968 " {} match {}",
969 crate::glyphs::get().middot,
970 crate::numfmt::group_chrome(k)
971 ));
972 }
973 Some(mark)
974 }
975
976 pub(crate) fn find_shown(&self) -> bool {
978 self.prompt
979 .find
980 .active
981 .as_ref()
982 .is_some_and(|active| active.dataset == self.dataset_generation)
983 }
984
985 pub fn finding(&self) -> bool {
987 self.jobs
988 .current(|job| matches!(job, Job::Find(_) | Job::HexFind(_)))
989 .is_some()
990 }
991
992 pub(crate) fn cancel_find(&mut self) {
995 if self.stop_find() {
996 self.flash_note(CANCELLED.to_string());
997 }
998 }
999
1000 pub(crate) fn stop_find(&mut self) -> bool {
1002 if self.stop_hex_find() {
1003 return true;
1004 }
1005 let Some((_, Job::Find(run))) = self.jobs.current(|job| matches!(job, Job::Find(_))) else {
1006 return false;
1007 };
1008 run.stop.store(true, Ordering::Relaxed);
1009 self.jobs.cancel(|job| matches!(job, Job::Find(_)));
1010 self.status_message = None;
1011 self.prompt.find.read = None;
1012 true
1013 }
1014
1015 fn start_find(&mut self, spec: FindSpec, direction: Direction, fresh: bool) {
1018 let Some(state) = self.data_table_state.as_ref() else {
1019 return;
1020 };
1021 let columns = searched_columns(state.get_column_order(), state.schema(), &spec);
1022 if columns.is_empty() {
1023 self.flash_note(match &spec.column {
1024 Some(column) if !state.get_column_order().contains(column) => {
1025 format!("Nothing to find in {column}: it is not shown")
1026 }
1027 Some(column) => format!("Nothing to find in {column}: it holds no text"),
1028 None => "No column to find in".to_string(),
1029 });
1030 return;
1031 }
1032 let frame = state.len_generation();
1033 let row = state.cursor_row();
1034 let at = state.current_column().filter(|_| !fresh).map(|name| {
1036 match columns.iter().position(|(n, _)| n == name) {
1037 Some(c) => At::On(c),
1038 None => {
1039 let order = state.get_column_order();
1040 let place = |n: &str| order.iter().position(|o| o == n);
1041 let cursor = place(name);
1042 At::Before(columns.iter().filter(|(n, _)| place(n) < cursor).count())
1043 }
1044 }
1045 });
1046 let previous =
1047 self.prompt.find.active.as_ref().filter(|a| {
1048 a.dataset == self.dataset_generation && a.frame == frame && a.spec == spec
1049 });
1050 let on_hit = previous
1052 .and_then(|a| Some((a.hit.as_ref()?, a.ordinal)))
1053 .and_then(|((hit_row, name), ordinal)| {
1054 let c = columns.iter().position(|(n, _)| n == name)?;
1055 (*hit_row == row && at == Some(At::On(c))).then(|| (name.clone(), ordinal))
1056 });
1057 let start = Start { row, column: at };
1058 self.prompt.find.active = Some(ActiveFind {
1059 spec: spec.clone(),
1060 dataset: self.dataset_generation,
1061 frame,
1062 hit: on_hit.as_ref().map(|(name, _)| (row, name.clone())),
1063 ordinal: on_hit.as_ref().and_then(|(_, ordinal)| *ordinal),
1064 });
1065 let stop = Arc::new(AtomicBool::new(false));
1066 let run = FindRun {
1067 stop: stop.clone(),
1068 dataset: self.dataset_generation,
1069 frame,
1070 direction,
1071 from_top: row == 0 && at.is_none_or(|at| at.ahead() == 0),
1072 from_hit: on_hit.as_ref().map(|(_, ordinal)| *ordinal),
1073 };
1074 let rows = state.view_rows();
1075 let status = finding_status(&spec, None);
1076 self.prompt.find.read = None;
1077 self.spawn_job(Job::Find(run), Some(&status), move |worker| {
1078 let report = worker.reporter();
1079 let search = Search::new(rows, columns, stop, move |read| {
1080 report(Progress::Finding { rows: read })
1081 });
1082 Ok(Answer::Found(search.run(start, direction)?))
1083 });
1084 }
1085
1086 pub(crate) fn find_progress(&mut self, rows: usize) {
1088 self.prompt.find.read = Some(rows);
1089 if let Some(active) = self.prompt.find.active.as_ref() {
1090 self.status_message = Some(finding_status(&active.spec, Some(rows)));
1091 }
1092 }
1093
1094 pub(crate) fn find_answered(&mut self, run: FindRun, current: bool, found: Option<Found>) {
1096 if !current {
1097 return;
1098 }
1099 self.status_message = None;
1101 self.prompt.find.read = None;
1102 let Some(active) = self.prompt.find.active.as_mut() else {
1103 return;
1104 };
1105 let Some(state) = self.data_table_state.as_mut() else {
1106 return;
1107 };
1108 if active.dataset != run.dataset
1109 || active.frame != run.frame
1110 || state.len_generation() != run.frame
1111 {
1112 return;
1113 }
1114 let Some(found) = found else {
1115 active.hit = None;
1116 active.ordinal = None;
1117 let message = format!("No match for {}", active.spec.label());
1118 self.flash_note(message);
1119 return;
1120 };
1121 let same_cell = run.from_hit.is_some()
1122 && active
1123 .hit
1124 .as_ref()
1125 .is_some_and(|(row, name)| *row == found.row && *name == found.column);
1126 let before = run.from_hit.flatten();
1127 active.ordinal = match run.direction {
1128 _ if same_cell => before,
1129 Direction::Next if found.wrapped => Some(1),
1131 Direction::Next if run.from_hit.is_some() => before.map(|k| k + 1),
1132 Direction::Next => run.from_top.then_some(1),
1133 Direction::Previous if found.wrapped => None,
1134 Direction::Previous => before.and_then(|k| k.checked_sub(1)).filter(|k| *k > 0),
1135 };
1136 active.hit = Some((found.row, found.column.clone()));
1137 let needs_rows = state.go_to_found_row(found.row);
1138 state.set_current_column(&found.column);
1140 if found.wrapped {
1141 self.flash_note(match run.direction {
1142 Direction::Next => "Wrapped to the top".to_string(),
1143 Direction::Previous => "Wrapped to the bottom".to_string(),
1144 });
1145 }
1146 if needs_rows {
1147 self.spawn_async_collect(Self::LOADING_BUFFER);
1148 }
1149 }
1150
1151 pub(crate) fn find_failed(&mut self, current: bool, message: &str) {
1153 if !current {
1154 return;
1155 }
1156 self.status_message = None;
1157 self.prompt.find.read = None;
1158 if message != CANCELLED {
1159 self.flash_note(format!("Find failed: {message}"));
1160 }
1161 }
1162}
1163
1164#[cfg(test)]
1165mod tests;
1166
1167#[cfg(test)]
1169mod app_tests;