use alacritty_terminal::{ grid::Dimensions, index::{Column, Direction, Point}, term::{ Term, search::{Match, RegexIter, RegexSearch}, }, }; use gpui::Context; use super::view::Terminal; use crate::editor::FindOptions; /// A find's matches over the scrollback and the screen, and the one stepped to. #[derive(Default)] pub(crate) struct Search { all: Vec, current: Option, } impl Search { pub(crate) fn visible(&self) -> (&[Match], Option) { (&self.all, self.current) } } /// `query` under `options` as the grid's search reads it; word edges are ASCII, which its lazy DFA needs. fn pattern(query: &str, options: FindOptions) -> String { let body = if options.regex { query.to_string() } else { regex::escape(query) }; let body = if options.word { format!(r"(?-u:\b)(?:{body})(?-u:\b)") } else { body }; let case = if options.case { "(?-i)" } else { "(?i)" }; format!("{case}{body}") } /// Every match of `pattern` from the oldest line to the newest. fn search_all(term: &Term, pattern: &str) -> Result, String> { let mut regex = RegexSearch::new(pattern).map_err(|error| error.to_string())?; let start = Point::new(term.topmost_line(), Column(0)); let end = Point::new(term.bottommost_line(), term.last_column()); Ok(RegexIter::new(start, end, Direction::Right, term, &mut regex).collect()) } impl Terminal { /// Finds `query` in the scrollback and the screen and steps to the newest match; a bad pattern is an error to show. pub fn find( &mut self, query: &str, options: FindOptions, cx: &mut Context, ) -> Result { let all = if query.is_empty() { Vec::new() } else { search_all(&self.term.lock(), &pattern(query, options))? }; log::debug!("terminal: {} matches of {query:?}", all.len()); self.search = Search { current: all.len().checked_sub(1), all, }; self.reveal(cx); Ok(self.search.all.len()) } /// Steps to the next match, or the one before, round the ends. pub fn step(&mut self, forward: bool, cx: &mut Context) { let count = self.search.all.len(); let Some(at) = self.search.current else { return; }; self.search.current = Some(if forward { (at + 1) % count } else { (at + count - 1) % count }); self.reveal(cx); } /// How many matches there are, and which one is current. pub fn matches(&self) -> (usize, Option) { (self.search.all.len(), self.search.current) } fn reveal(&mut self, cx: &mut Context) { if let Some(at) = self.search.current { let start = *self.search.all[at].start(); self.term.lock().scroll_to_point(start); } cx.notify(); } } #[cfg(test)] mod tests { use alacritty_terminal::term::test::mock_term; use super::*; #[test] fn find_takes_case_words_and_patterns() { let term = mock_term("one two One\r\nthree phone one"); let count = |query: &str, options: FindOptions| { search_all(&term, &pattern(query, options)).map(|all| all.len()) }; assert_eq!(count("one", FindOptions::default()), Ok(4)); let case = FindOptions { case: true, ..FindOptions::default() }; assert_eq!(count("One", case), Ok(1)); let word = FindOptions { word: true, ..FindOptions::default() }; assert_eq!(count("one", word), Ok(3), "phone holds no whole word"); let regex = FindOptions { regex: true, ..FindOptions::default() }; assert_eq!(count("t[wh]", regex), Ok(2)); assert!(count("(", regex).is_err()); } }