1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
use regex::Regex;
pub struct SearchQuery {
pub raw: String,
pub regex: Regex,
}
impl SearchQuery {
pub fn new(raw: String) -> Result<Self, regex::Error> {
let regex = Regex::new(&raw)?;
Ok(Self { raw, regex })
}
}
pub enum SearchStatus {
/// Background scan still in progress; more matches may arrive.
Searching,
/// Scan finished; `matching_rows` is the complete result set.
Complete,
}
pub struct SearchState {
pub query: SearchQuery,
/// Sorted absolute row indices (0-based) that contain at least one match.
/// Grows as background chunks arrive.
pub matching_rows: Vec<usize>,
/// Index into `matching_rows` for the currently selected match.
pub current_idx: usize,
pub status: SearchStatus,
/// Set to true once we've auto-jumped to the first match after the
/// initial search results arrive.
pub initial_jump_done: bool,
/// Column the search was scoped to, if any (Column/Cell selection modes).
/// `None` means the search covers all columns (Row mode).
pub col_idx: Option<usize>,
}
impl SearchState {
pub fn new(query: SearchQuery, col_idx: Option<usize>) -> Self {
Self {
query,
matching_rows: Vec::new(),
current_idx: 0,
status: SearchStatus::Searching,
initial_jump_done: false,
col_idx,
}
}
/// `(current_1based, total, complete)` — used by the status bar.
/// Returns `(0, 0, false)` when no matches have arrived yet.
pub fn match_info(&self) -> (usize, usize, bool) {
let complete = matches!(self.status, SearchStatus::Complete);
if self.matching_rows.is_empty() {
(0, 0, complete)
} else {
(self.current_idx + 1, self.matching_rows.len(), complete)
}
}
pub fn current_row(&self) -> Option<usize> {
self.matching_rows.get(self.current_idx).copied()
}
/// Jump to the next match strictly after `cursor_row`, wrapping if needed.
/// Uses the cursor position so manual navigation between `n` presses is respected.
pub fn next_from(&mut self, cursor_row: usize) -> Option<usize> {
if self.matching_rows.is_empty() {
return None;
}
// partition_point gives the first index where row > cursor_row
let idx = self.matching_rows.partition_point(|&r| r <= cursor_row);
self.current_idx = idx % self.matching_rows.len();
self.current_row()
}
/// Jump to the previous match strictly before `cursor_row`, wrapping if needed.
pub fn prev_from(&mut self, cursor_row: usize) -> Option<usize> {
if self.matching_rows.is_empty() {
return None;
}
// partition_point gives the first index where r >= cursor_row;
// the element before it is the last one strictly before cursor_row.
let idx = self.matching_rows.partition_point(|&r| r < cursor_row);
self.current_idx = idx.checked_sub(1).unwrap_or(self.matching_rows.len() - 1);
self.current_row()
}
}