Skip to main content

datui_lib/
find.rs

1//! Find in the table: `/` (or `f`) asks for a pattern, `n` and `N` move between the
2//! cells that hold it.
3//!
4//! As the pattern is typed, the cells that match among the rows on hand light up and
5//! are counted (`3 on screen`); nothing is read for that. Ctrl+G keeps only the rows
6//! that match, as a filter the Sort & Filter sidebar lists.
7//!
8//! The view is searched as it stands — query, filters, sort and the columns shown —
9//! and never changed: only the cursor moves. The search runs off the UI thread as a
10//! job, reading the view a window at a time the way pages are read, starting from the
11//! buffer the table already holds. It looks for the next match only: nothing counts
12//! every match, so a find near the cursor reads little, and the count of matches is
13//! known only as far as the finds so far have walked from the top.
14
15use 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
28/// The row index a window is read with.
29const ROW: &str = "__datui_find_row";
30/// The rows a window held.
31const ROWS: &str = "__datui_find_rows";
32/// Rows in the first window read past the buffer. Each window after it is twice the
33/// one before: a view that cannot skip to a window (a filter, a CSV) reads up to it
34/// every time, and doubling keeps that within twice the rows a single pass would
35/// read, while a match a page past the buffer costs one small read. A view that sees
36/// every row before its first (a sort) is read in one window instead, and so is the
37/// range behind the cursor on a view that cannot skip: its first window back would
38/// read all of it anyway. Forward on such a view, a window is never smaller than the
39/// rows above it, which it reads too.
40const FIRST_WINDOW: usize = 65_536;
41/// The most rows one window reads: a slice's length is a `u32`.
42const LARGEST_WINDOW: usize = u32::MAX as usize;
43
44/// What a find looks for.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct FindSpec {
47    pub pattern: String,
48    /// The pattern is a regular expression rather than plain text.
49    pub regex: bool,
50    /// The pattern's letters in order, anything between: `smth` finds `Smith`.
51    /// Spaces in the pattern are ignored.
52    pub fuzzy: bool,
53    /// Only this column, when set; otherwise every column shown.
54    pub column: Option<String>,
55}
56
57impl FindSpec {
58    /// Smart case: a pattern with no capital letter ignores case. In a regex an
59    /// escape such as `\S` is a class, not a capital.
60    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    /// The regex a cell's text is matched with, or `None` for a case-sensitive plain
75    /// pattern, which is matched literally.
76    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    /// Why the pattern cannot be searched, in one line.
95    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            // The crate's message draws the pattern with a caret under it; the last
101            // line is the reason.
102            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    /// Whether a cell's `text` matches; false for a null.
116    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    /// How the pattern reads on the control bar: quoted plain text, or a regex
125    /// between slashes, cut short when long.
126    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
143/// Whether a cell of column `name` matches `spec`, or `None` for a column a find
144/// skips: the one place a cell is matched, for a find and for the filter it keeps.
145pub(crate) fn cell_matches(spec: &FindSpec, name: &str, dtype: &DataType) -> Option<Expr> {
146    Some(spec.matches(text_of(name, dtype)?))
147}
148
149/// A column's values as text to match, or `None` for a column a find skips: bytes,
150/// nested values and nulls have no text of their own.
151fn 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        // Polars has no cast from a duration to text; written as the table shows it.
166        duration_text(column, *unit)
167    } else if crate::past_calendar::can_leave_calendar(dtype) {
168        // A plain cast panics on a date past the calendar; this writes it as its
169        // stored number, as the table does.
170        crate::past_calendar::text_expr(column, polars::chunked_array::cast::CastOptions::NonStrict)
171    } else {
172        column.cast(DataType::String)
173    })
174}
175
176/// A duration column as the text the table shows for it, such as `1d 2h`.
177fn 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
195/// The columns a find over `order` reads, in that order, and the match of each.
196pub(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/// Which way a find goes.
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub enum Direction {
214    Next,
215    Previous,
216}
217
218/// Where a find starts: a view row, and in it the cursor's place among the columns
219/// searched. With no place the whole row is in reach: `f` finds the first match at or
220/// after the cursor.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub(crate) struct Start {
223    pub(crate) row: usize,
224    pub(crate) column: Option<At>,
225}
226
227/// The cursor's place in its row, as an index into the columns searched.
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub(crate) enum At {
230    /// On a searched column.
231    On(usize),
232    /// On a column not searched, just before this searched one (or past the last).
233    Before(usize),
234}
235
236impl At {
237    /// The first searched column past the cursor.
238    fn ahead(self) -> usize {
239        match self {
240            At::On(c) => c + 1,
241            At::Before(c) => c,
242        }
243    }
244
245    /// The searched columns before this one are behind the cursor.
246    fn behind(self) -> usize {
247        match self {
248            At::On(c) | At::Before(c) => c,
249        }
250    }
251}
252
253/// The cell a find landed on.
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct Found {
256    pub row: usize,
257    pub column: String,
258    /// It went past the end (or the start) and came round to reach it.
259    pub wrapped: bool,
260}
261
262/// In row `row`, only the columns in `columns` are in reach: the cells on the far
263/// side of the start are left to the other half of the search.
264#[derive(Debug, Clone)]
265struct Limit {
266    row: usize,
267    columns: Range<usize>,
268}
269
270/// Why a search stopped before it answered.
271pub(crate) const CANCELLED: &str = "Find cancelled";
272
273/// One search through a view, on a worker.
274pub(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    /// Rows read so far, for the progress line.
281    read: usize,
282    /// Rows in the next window read past the buffer.
283    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        // The buffer serves only while it holds every column searched: hiding a
299        // column re-reads it narrower.
300        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        // A sort costs a whole pass for any window, so it gets one.
309        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    /// Find the next match from `start` going `direction`, wrapping round the view
326    /// once. `None` when nothing in the view matches.
327    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                // Round from the top: up to the start row, and in it the cells up to
349                // the cursor's, which is a match of its own when it is the only one.
350                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                // Round from the bottom, down to the cursor's cell.
373                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                    // With no count, the end is found by reading to it.
386                    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    /// The first match in rows `[from, end)`, or the last with `last`. With no `end`
394    /// the rows run to the view's end, found by reading to it.
395    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    /// The last match in rows `[from, end)`, read from the end down.
427    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    /// The buffer's rows, as `(start, end)`.
454    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    /// The next window from `at`: the rest of the buffer when `at` is in it, else
462    /// rows read from the view, stopping short of the buffer. For the `last` match,
463    /// every row to the end is read, so a view that cannot skip reads them at once.
464    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        // A view that cannot skip reads the rows above a window with it: a window no
473        // smaller than them keeps a find from deep in the view from reading them
474        // again for every doubling.
475        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    /// Rows in the next window read from the view. Read backwards, or to the end, a
489    /// view that cannot skip to a window takes the whole range in one.
490    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    /// The window that ends at `end`, no lower than `from`: the buffer's rows when
499    /// the row before `end` is in it, else rows read from the view.
500    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    /// Read rows `[start, start + len)` and find the first match among them (the
517    /// last with `last`). Returns the rows there were, and the match as its row and
518    /// column. One aggregate per column: the window's matching cells are never
519    /// collected, only where the first (or last) one is.
520    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            // Reading order: the first row, and in it the first column; backwards,
564            // the last row and the last column.
565            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
576/// The find prompt, and the find in effect.
577pub struct Find {
578    pub input: TextInput,
579    /// The prompt's pattern is a regex.
580    pub regex: bool,
581    /// The prompt's pattern is letters in order.
582    pub fuzzy: bool,
583    /// The prompt limits the find to `column`.
584    pub in_column: bool,
585    /// The column the prompt opened on: the column cursor's.
586    pub column: Option<String>,
587    /// Why the pattern typed cannot be searched.
588    pub error: Option<String>,
589    /// The find `n` and `N` repeat.
590    pub active: Option<ActiveFind>,
591    /// The cells the prompt's pattern matches among the rows on hand.
592    pub live: Option<LiveMatches>,
593    /// The rows on hand `live` was worked out over: their first view row, how many,
594    /// and the frame. Rows that arrive or a new frame make it stale.
595    live_rows: Option<(usize, usize, u64)>,
596    /// Rows the find reading has read, for the footer's progress line.
597    pub read: Option<usize>,
598}
599
600/// The view rows a pattern being typed matches among the rows on hand, by column
601/// name: looked up by the table as it draws, without a name cloned per cell.
602pub type MatchCells = std::collections::HashMap<String, std::collections::HashSet<usize>>;
603
604/// The cells a pattern being typed matches among the rows on hand. Worked out in
605/// memory as the pattern or the rows on hand change; never read.
606#[derive(Debug, Clone, Default)]
607pub struct LiveMatches {
608    pub cells: Arc<MatchCells>,
609}
610
611impl LiveMatches {
612    /// The matches in view rows `rows`: the count the prompt shows.
613    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/// The find in effect, and where it last landed.
622#[derive(Debug, Clone)]
623pub struct ActiveFind {
624    pub spec: FindSpec,
625    /// The dataset it was made for.
626    pub dataset: u64,
627    /// The frame (`len_generation`) its cell is a cell of.
628    pub frame: u64,
629    /// The cell it landed on: view row and column name.
630    pub hit: Option<(usize, String)>,
631    /// Which match that is, counting from the top, when the finds so far say.
632    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    /// The spec the prompt describes.
652    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/// A find running on a worker: how to stop it, and what its answer is judged by.
663#[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    /// It started before every cell of the view, so its first match is match 1.
670    pub(crate) from_top: bool,
671    /// It started from the cell the last find landed on, with that match's number.
672    pub(crate) from_hit: Option<Option<usize>>,
673}
674
675/// What the control bar says while a find reads.
676fn finding_status(spec: &FindSpec, read: Option<usize>) -> String {
677    match read {
678        // Abbreviated, as the row count beside it is: the line shares a narrow bar
679        // with the way out and the count.
680        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    /// `/` (or `f`) at the table: the find prompt, holding the last pattern, selected
691    /// so that typing replaces it.
692    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    /// Light up the cells the prompt's pattern matches among the rows on hand. Only
713    /// what is already in memory is matched, so typing never waits on a read.
714    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        // In memory: the rows on hand are a frame already collected.
742        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    /// Which rows are on hand, to tell when the live matches were worked out over
769    /// others.
770    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    /// While the find prompt is open, work the matches out again when the rows on
777    /// hand changed under it: a collect after the footer took a row, a follow's new
778    /// rows.
779    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    /// The matches the prompt's pattern has among the rows on screen, while the
788    /// prompt is open.
789    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    /// The cells to light up: the prompt's matches while it is open.
797    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    /// The column a find limited to one column searches: the column cursor's, which a
805    /// find moves to the cell it lands on.
806    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    /// A key in the find prompt. Ctrl+R switches regex, Ctrl+T letters in order,
822    /// Ctrl+L the column limit, and Ctrl+G keeps the rows that match; the field keeps
823    /// its readline keys and its history.
824    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                    // An emptied field is how a find is taken back (#644).
857                    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    /// Ctrl+G in the find prompt: keep only the rows that match, as a filter added to
881    /// the sidebar's, and leave the find in effect for `n` and `N` among them.
882    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        // Over every column, the ones shown now: what the find searched.
900        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    /// `n` / `N` at the table: the find in effect again, from the cursor's cell.
934    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    /// The cell the find in effect landed on, while the view is the one it searched.
945    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    /// What the control bar says about the find in effect: the pattern, and which
954    /// match the cursor is on when that is known.
955    pub fn find_mark(&self) -> Option<String> {
956        let active = self.find.active.as_ref()?;
957        // While it reads, the busy line names the pattern and the bar needs the room
958        // for the rows read; while the prompt is open, the prompt is the find.
959        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    /// Whether a find is in effect on this dataset, so Esc at the table clears it.
980    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    /// Whether a find is reading.
988    pub fn finding(&self) -> bool {
989        self.jobs
990            .current(|job| matches!(job, Job::Find(_) | Job::HexFind(_)))
991            .is_some()
992    }
993
994    /// Stop the find that is reading: Esc while it runs. Its worker stops at its next
995    /// window, and its answer, if it comes first, is dropped.
996    pub(crate) fn cancel_find(&mut self) {
997        if self.stop_find() {
998            self.flash_note(CANCELLED.to_string());
999        }
1000    }
1001
1002    /// Stop the find that is reading, if one is. Returns whether one was.
1003    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    /// Start a find for `spec` from the cursor. `n` and `N` start past the cursor's
1018    /// cell; `fresh` (`f`), the whole cursor row is in reach.
1019    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        // `n` and `N` go on from the cursor's cell, as in vim; `f` reads its whole row.
1037        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        // On the cell the last find landed on, the count of matches goes on from it.
1053        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    /// A find's progress: the rows it has read.
1089    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    /// A find answered. The cursor goes to the cell found, the view as it was.
1097    pub(crate) fn find_answered(&mut self, run: FindRun, current: bool, found: Option<Found>) {
1098        if !current {
1099            return;
1100        }
1101        // The line was the find's progress, which the job's own line no longer is.
1102        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            // Round from the top, it is the first match in the view.
1132            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        // The cursor takes the found cell's column, scrolling as little as it takes.
1141        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    /// A find failed: the reason on the control bar, and nothing moved.
1154    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    /// Every match of `spec` in `df` reading forward from `start`, `steps` times.
1189    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        // `\S` is a class, not a capital: still case-blind.
1271        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        // Both "Lima" rows and "alice": every cell holding "li", row by row and left
1294        // to right within a row.
1295        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    /// On a column not searched, the cursor sits between the searched ones: the
1323    /// cells to its right are ahead of it, those to its left behind.
1324    #[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        // On `name`, left of `city`: row 1's Lima is ahead, and behind is round.
1349        assert_eq!(one(At::Before(0), Direction::Next), (1, false));
1350        assert_eq!(one(At::Before(0), Direction::Previous), (4, true));
1351        // On `n`, right of it: the other way about.
1352        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    /// A long view: the buffer holds rows 100..200, the match is far below it, read
1427    /// in growing windows from the view, and the rows read are reported.
1428    #[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        // The buffer's rest first, then windows of 65,536, 131,072 and the rest of the
1462        // view, read from row 200 on: none of it twice.
1463        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    /// A date past the calendar, on which a plain cast panics, is its stored number;
1514    /// durations and times are text too, so no type fails the whole find.
1515    #[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    /// A sorted view costs a whole pass for any window, so it is read in one: the
1540    /// match far down is found with one read past the buffer, not a window per
1541    /// doubling.
1542    #[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        // Descending: the needle (k = 7) is view row n - 8.
1556        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    /// `N` on a view that cannot skip to a window reads the range behind the cursor
1588    /// in one pass, and so does its way round from the bottom.
1589    #[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        // Even k only: the needle (k = 4) is view row 2, of 150,000.
1603        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    /// `n` from deep in a view that cannot skip reads a first window as large as the
1640    /// rows above it, rather than doubling up from a small one and reading those
1641    /// rows again for each window.
1642    #[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        // Even k only: the needle (k = 598,000) is view row 299,000, of 300,000.
1656        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        // The buffer's rest, then the rest of the view in one window: not 65,536
1682        // rows and then 131,072, each reading the 200,000 above it again.
1683        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/// The keys at the table, through the app.
1737#[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    /// Handle what the workers send until nothing is owed.
1762    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    /// An app over `df` with its first buffer read, ten rows on screen.
1785    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    /// Enter on an emptied field takes the find back (#644): no mark, no hit, and `n`
1823    /// has nothing to repeat.
1824    #[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        // The old pattern is selected, so Backspace empties the field.
1832        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    /// Esc at the table clears a find before it backs out of anything else (#644).
1847    #[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        // Descending: row 299 of the view is id 700.
1916        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        // Only in `id`: "hay 12" in `v` is not a match for `^12$`.
1978        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    /// Ctrl+L limits a find to the column cursor's column, and a match moves the
1989    /// column cursor to the found cell's column.
1990    #[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        // Every column: the match is in `w`, and the column cursor goes there.
2017        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        // The next limited find opens on it.
2025        key(&mut app, KeyCode::Char('f'));
2026        assert_eq!(app.find.column.as_deref(), Some("w"));
2027    }
2028
2029    /// `n` and `N` go on from the cursor's cell: moved along the found row, the next
2030    /// match is the one to the cursor's right, the previous the one to its left.
2031    #[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        // To `c`, past the match in `b`.
2051        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        // Back to `c`: the previous match is `b`, left of it.
2057        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        // Down the rows: from row 5, the next is row 9, not the rest of row 2.
2062        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    /// `<` `>` `=` `w` at the table set the column cursor's width at once (#647).
2101    #[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    /// After the view changes under it, `n` starts from the cursor in the new view:
2130    /// the cell it landed on belongs to a frame that is gone.
2131    #[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        // Descending: id 700 is view row 299, id 5 row 994.
2145        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    /// The found cell is drawn in the theme's find slot, on the cursor's row, and
2151    /// nowhere once the cursor moves off it; with row numbers shown too.
2152    #[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        // The found cell is the current cell, drawn as found rather than as the cell
2195        // cursor; the header keeps the cell cursor's mark.
2196        // The cell cursor's look on this terminal: its tint, or reversed where the
2197        // tint cannot show.
2198        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        // The column cursor off it: the cell is plain, and the new current cell is the
2213        // cell cursor's.
2214        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}