use unicode_width::UnicodeWidthChar;
use crate::search::{Match, SearchOptions};
use crate::selection::SelectionSpan;
use super::Term;
impl Term {
pub fn search(&self, query: &str) -> Vec<Match> {
self.search_with(query, SearchOptions::default())
}
pub fn search_with(&self, query: &str, opts: SearchOptions) -> Vec<Match> {
let q: Vec<char> = query.chars().collect();
if q.is_empty() {
return Vec::new();
}
let ci = opts
.case_sensitive
.map_or_else(|| !q.iter().any(|c| c.is_uppercase()), |cs| !cs);
let fold = |c: char| {
if ci {
c.to_lowercase().next().unwrap_or(c)
} else {
c
}
};
let needle: Vec<char> = q.iter().map(|&c| fold(c)).collect();
let re = if opts.regex {
match regex::RegexBuilder::new(query).case_insensitive(ci).build() {
Ok(re) => Some(re),
Err(_) => return Vec::new(),
}
} else {
None
};
let total = self.scrollback.len() + self.grid.rows();
let floor = self.abs_floor();
let mut matches = Vec::new();
let mut r = floor;
while r < total {
let mut hay: Vec<char> = Vec::new();
let mut pos: Vec<(usize, usize)> = Vec::new();
let mut line = r;
loop {
let cells = self.abs_line(line);
for (col, cell) in cells.iter().enumerate() {
if cell.is_spacer() {
continue;
}
hay.push(cell.c());
pos.push((line, col));
if let Some(marks) = self.combining_at(line, col) {
for &m in marks {
hay.push(m);
pos.push((line, col));
}
}
}
let soft = self.abs_row(line).is_wrapped();
if soft && line + 1 < total {
line += 1;
} else {
break;
}
}
while hay.last().is_some_and(|c| *c == ' ') {
hay.pop();
pos.pop();
}
let push_range = |cs: usize, ce: usize, matches: &mut Vec<Match>| {
if opts.whole_word && !word_bounded(&hay, cs, ce - cs) {
return;
}
let m = Match {
start_line: pos[cs].0,
start_col: pos[cs].1,
end_line: pos[ce - 1].0,
end_col: pos[ce - 1].1,
};
if matches.last() != Some(&m) {
matches.push(m);
}
};
if let Some(re) = &re {
let hay_str: String = hay.iter().collect();
for mat in re.find_iter(&hay_str) {
if mat.start() == mat.end() {
continue; }
let cs = hay_str[..mat.start()].chars().count();
let ce = hay_str[..mat.end()].chars().count();
push_range(cs, ce, &mut matches);
}
} else {
let mut i = 0;
while needle.len() <= hay.len() && i + needle.len() <= hay.len() {
let hit = hay[i..i + needle.len()]
.iter()
.enumerate()
.all(|(k, &c)| fold(c) == needle[k]);
if hit {
let before = matches.len();
push_range(i, i + needle.len(), &mut matches);
i += if matches.len() > before {
needle.len()
} else {
1
};
} else {
i += 1;
}
}
}
r = line + 1;
}
matches
}
pub fn search_scroll_to(&mut self, m: &Match) {
let target = self.scrollback.len().saturating_sub(m.start_line);
self.set_display_offset(target);
}
pub fn match_spans(&self, m: &Match) -> Vec<SelectionSpan> {
let rows = self.grid.rows();
let top = self.scrollback.len() - self.display_offset;
let mut spans = Vec::new();
for line in m.start_line..=m.end_line {
if line < top {
continue;
}
let row = line - top;
if row >= rows {
break;
}
let last = self.abs_line(line).len().saturating_sub(1);
let left = if line == m.start_line {
m.start_col.min(last)
} else {
0
};
let right = if line == m.end_line {
m.end_col.min(last)
} else {
last
};
if right >= left {
spans.push(SelectionSpan { row, left, right });
}
}
spans
}
pub fn set_search_highlights(&mut self, matches: Vec<Match>) {
self.search_highlights = matches;
self.active_search_highlight = None;
}
pub fn set_active_search_highlight(&mut self, index: Option<usize>) {
self.active_search_highlight = index.and_then(|i| self.search_highlights.get(i)).copied();
}
pub fn set_active_search_match(&mut self, m: Option<Match>) {
self.active_search_highlight = m;
}
pub(super) fn invalidate_search_highlights(&mut self) {
self.search_highlights.clear();
self.active_search_highlight = None;
}
}
fn word_bounded(hay: &[char], i: usize, len: usize) -> bool {
let is_word = |c: char| c.is_alphanumeric() || c == '_' || c.width() == Some(0);
let left = i == 0 || !is_word(hay[i - 1]);
let right = i + len == hay.len() || !is_word(hay[i + len]);
left && right
}