| use std::cmp::max; |
| use std::mem; |
| use std::ops::RangeInclusive; |
| |
| use regex_automata::{dense, DenseDFA, Error as RegexError, DFA}; |
| |
| use crate::grid::{BidirectionalIterator, Dimensions, GridIterator, Indexed}; |
| use crate::index::{Boundary, Column, Direction, Point, Side}; |
| use crate::term::cell::{Cell, Flags}; |
| use crate::term::Term; |
| |
| /// Used to match equal brackets, when performing a bracket-pair selection. |
| const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; |
| |
| pub type Match = RangeInclusive<Point>; |
| |
| /// Terminal regex search state. |
| #[derive(Clone, Debug)] |
| pub struct RegexSearch { |
| /// Locate end of match searching right. |
| right_fdfa: DenseDFA<Vec<usize>, usize>, |
| /// Locate start of match searching right. |
| right_rdfa: DenseDFA<Vec<usize>, usize>, |
| |
| /// Locate start of match searching left. |
| left_fdfa: DenseDFA<Vec<usize>, usize>, |
| /// Locate end of match searching left. |
| left_rdfa: DenseDFA<Vec<usize>, usize>, |
| } |
| |
| impl RegexSearch { |
| /// Build the forward and backward search DFAs. |
| pub fn new(search: &str) -> Result<RegexSearch, RegexError> { |
| // Check case info for smart case |
| let has_uppercase = search.chars().any(|c| c.is_uppercase()); |
| |
| // Create Regex DFAs for all search directions. |
| let mut builder = dense::Builder::new(); |
| let builder = builder.case_insensitive(!has_uppercase); |
| |
| let left_fdfa = builder.clone().reverse(true).build(search)?; |
| let left_rdfa = builder.clone().anchored(true).longest_match(true).build(search)?; |
| |
| let right_fdfa = builder.clone().build(search)?; |
| let right_rdfa = builder.anchored(true).longest_match(true).reverse(true).build(search)?; |
| |
| Ok(RegexSearch { right_fdfa, right_rdfa, left_fdfa, left_rdfa }) |
| } |
| } |
| |
| impl<T> Term<T> { |
| /// Get next search match in the specified direction. |
| pub fn search_next( |
| &self, |
| dfas: &RegexSearch, |
| mut origin: Point, |
| direction: Direction, |
| side: Side, |
| mut max_lines: Option<usize>, |
| ) -> Option<Match> { |
| origin = self.expand_wide(origin, direction); |
| |
| max_lines = max_lines.filter(|max_lines| max_lines + 1 < self.total_lines()); |
| |
| match direction { |
| Direction::Right => self.next_match_right(dfas, origin, side, max_lines), |
| Direction::Left => self.next_match_left(dfas, origin, side, max_lines), |
| } |
| } |
| |
| /// Find the next match to the right of the origin. |
| fn next_match_right( |
| &self, |
| dfas: &RegexSearch, |
| origin: Point, |
| side: Side, |
| max_lines: Option<usize>, |
| ) -> Option<Match> { |
| let start = self.line_search_left(origin); |
| let mut end = start; |
| |
| // Limit maximum number of lines searched. |
| end = match max_lines { |
| Some(max_lines) => { |
| let line = (start.line + max_lines).grid_clamp(self, Boundary::None); |
| Point::new(line, self.last_column()) |
| }, |
| _ => end.sub(self, Boundary::None, 1), |
| }; |
| |
| let mut regex_iter = RegexIter::new(start, end, Direction::Right, self, dfas).peekable(); |
| |
| // Check if there's any match at all. |
| let first_match = regex_iter.peek()?.clone(); |
| |
| let regex_match = regex_iter |
| .find(|regex_match| { |
| let match_point = Self::match_side(regex_match, side); |
| |
| // If the match's point is beyond the origin, we're done. |
| match_point.line < start.line |
| || match_point.line > origin.line |
| || (match_point.line == origin.line && match_point.column >= origin.column) |
| }) |
| .unwrap_or(first_match); |
| |
| Some(regex_match) |
| } |
| |
| /// Find the next match to the left of the origin. |
| fn next_match_left( |
| &self, |
| dfas: &RegexSearch, |
| origin: Point, |
| side: Side, |
| max_lines: Option<usize>, |
| ) -> Option<Match> { |
| let start = self.line_search_right(origin); |
| let mut end = start; |
| |
| // Limit maximum number of lines searched. |
| end = match max_lines { |
| Some(max_lines) => { |
| let line = (start.line - max_lines).grid_clamp(self, Boundary::None); |
| Point::new(line, Column(0)) |
| }, |
| _ => end.add(self, Boundary::None, 1), |
| }; |
| |
| let mut regex_iter = RegexIter::new(start, end, Direction::Left, self, dfas).peekable(); |
| |
| // Check if there's any match at all. |
| let first_match = regex_iter.peek()?.clone(); |
| |
| let regex_match = regex_iter |
| .find(|regex_match| { |
| let match_point = Self::match_side(regex_match, side); |
| |
| // If the match's point is beyond the origin, we're done. |
| match_point.line > start.line |
| || match_point.line < origin.line |
| || (match_point.line == origin.line && match_point.column <= origin.column) |
| }) |
| .unwrap_or(first_match); |
| |
| Some(regex_match) |
| } |
| |
| /// Get the side of a match. |
| fn match_side(regex_match: &Match, side: Side) -> Point { |
| match side { |
| Side::Right => *regex_match.end(), |
| Side::Left => *regex_match.start(), |
| } |
| } |
| |
| /// Find the next regex match to the left of the origin point. |
| /// |
| /// The origin is always included in the regex. |
| pub fn regex_search_left(&self, dfas: &RegexSearch, start: Point, end: Point) -> Option<Match> { |
| // Find start and end of match. |
| let match_start = self.regex_search(start, end, Direction::Left, &dfas.left_fdfa)?; |
| let match_end = self.regex_search(match_start, start, Direction::Right, &dfas.left_rdfa)?; |
| |
| Some(match_start..=match_end) |
| } |
| |
| /// Find the next regex match to the right of the origin point. |
| /// |
| /// The origin is always included in the regex. |
| pub fn regex_search_right( |
| &self, |
| dfas: &RegexSearch, |
| start: Point, |
| end: Point, |
| ) -> Option<Match> { |
| // Find start and end of match. |
| let match_end = self.regex_search(start, end, Direction::Right, &dfas.right_fdfa)?; |
| let match_start = self.regex_search(match_end, start, Direction::Left, &dfas.right_rdfa)?; |
| |
| Some(match_start..=match_end) |
| } |
| |
| /// Find the next regex match. |
| /// |
| /// This will always return the side of the first match which is farthest from the start point. |
| fn regex_search( |
| &self, |
| start: Point, |
| end: Point, |
| direction: Direction, |
| dfa: &impl DFA, |
| ) -> Option<Point> { |
| let topmost_line = self.topmost_line(); |
| let screen_lines = self.screen_lines() as i32; |
| let last_column = self.last_column(); |
| |
| // Advance the iterator. |
| let next = match direction { |
| Direction::Right => GridIterator::next, |
| Direction::Left => GridIterator::prev, |
| }; |
| |
| let mut iter = self.grid.iter_from(start); |
| let mut state = dfa.start_state(); |
| let mut last_wrapped = false; |
| let mut regex_match = None; |
| let mut done = false; |
| |
| let mut cell = iter.cell(); |
| self.skip_fullwidth(&mut iter, &mut cell, direction); |
| let mut c = cell.c; |
| |
| let mut point = iter.point(); |
| |
| loop { |
| // Convert char to array of bytes. |
| let mut buf = [0; 4]; |
| let utf8_len = c.encode_utf8(&mut buf).len(); |
| |
| // Pass char to DFA as individual bytes. |
| for i in 0..utf8_len { |
| // Inverse byte order when going left. |
| let byte = match direction { |
| Direction::Right => buf[i], |
| Direction::Left => buf[utf8_len - i - 1], |
| }; |
| |
| // Since we get the state from the DFA, it doesn't need to be checked. |
| state = unsafe { dfa.next_state_unchecked(state, byte) }; |
| } |
| |
| // Handle regex state changes. |
| if dfa.is_match_or_dead_state(state) { |
| if dfa.is_dead_state(state) { |
| break; |
| } else { |
| regex_match = Some(point); |
| } |
| } |
| |
| // Stop once we've reached the target point. |
| if point == end || done { |
| break; |
| } |
| |
| // Advance grid cell iterator. |
| let mut cell = match next(&mut iter) { |
| Some(Indexed { cell, .. }) => cell, |
| None => { |
| // Wrap around to other end of the scrollback buffer. |
| let line = topmost_line - point.line + screen_lines - 1; |
| let start = Point::new(line, last_column - point.column); |
| iter = self.grid.iter_from(start); |
| iter.cell() |
| }, |
| }; |
| |
| // Check for completion before potentially skipping over fullwidth characters. |
| done = iter.point() == end; |
| |
| self.skip_fullwidth(&mut iter, &mut cell, direction); |
| |
| let wrapped = cell.flags.contains(Flags::WRAPLINE); |
| c = cell.c; |
| |
| let last_point = mem::replace(&mut point, iter.point()); |
| |
| // Handle linebreaks. |
| if (last_point.column == last_column && point.column == Column(0) && !last_wrapped) |
| || (last_point.column == Column(0) && point.column == last_column && !wrapped) |
| { |
| match regex_match { |
| Some(_) => break, |
| None => state = dfa.start_state(), |
| } |
| } |
| |
| last_wrapped = wrapped; |
| } |
| |
| regex_match |
| } |
| |
| /// Advance a grid iterator over fullwidth characters. |
| fn skip_fullwidth<'a>( |
| &self, |
| iter: &'a mut GridIterator<'_, Cell>, |
| cell: &mut &'a Cell, |
| direction: Direction, |
| ) { |
| match direction { |
| // In the alternate screen buffer there might not be a wide char spacer after a wide |
| // char, so we only advance the iterator when the wide char is not in the last column. |
| Direction::Right |
| if cell.flags.contains(Flags::WIDE_CHAR) |
| && iter.point().column < self.last_column() => |
| { |
| iter.next(); |
| }, |
| Direction::Right if cell.flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => { |
| if let Some(Indexed { cell: new_cell, .. }) = iter.next() { |
| *cell = new_cell; |
| } |
| iter.next(); |
| }, |
| Direction::Left if cell.flags.contains(Flags::WIDE_CHAR_SPACER) => { |
| if let Some(Indexed { cell: new_cell, .. }) = iter.prev() { |
| *cell = new_cell; |
| } |
| |
| let prev = iter.point().sub(self, Boundary::Grid, 1); |
| if self.grid[prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) { |
| iter.prev(); |
| } |
| }, |
| _ => (), |
| } |
| } |
| |
| /// Find next matching bracket. |
| pub fn bracket_search(&self, point: Point) -> Option<Point> { |
| let start_char = self.grid[point].c; |
| |
| // Find the matching bracket we're looking for |
| let (forward, end_char) = BRACKET_PAIRS.iter().find_map(|(open, close)| { |
| if open == &start_char { |
| Some((true, *close)) |
| } else if close == &start_char { |
| Some((false, *open)) |
| } else { |
| None |
| } |
| })?; |
| |
| let mut iter = self.grid.iter_from(point); |
| |
| // For every character match that equals the starting bracket, we |
| // ignore one bracket of the opposite type. |
| let mut skip_pairs = 0; |
| |
| loop { |
| // Check the next cell |
| let cell = if forward { iter.next() } else { iter.prev() }; |
| |
| // Break if there are no more cells |
| let cell = match cell { |
| Some(cell) => cell, |
| None => break, |
| }; |
| |
| // Check if the bracket matches |
| if cell.c == end_char && skip_pairs == 0 { |
| return Some(cell.point); |
| } else if cell.c == start_char { |
| skip_pairs += 1; |
| } else if cell.c == end_char { |
| skip_pairs -= 1; |
| } |
| } |
| |
| None |
| } |
| |
| /// Find left end of semantic block. |
| pub fn semantic_search_left(&self, mut point: Point) -> Point { |
| // Limit the starting point to the last line in the history |
| point.line = max(point.line, self.topmost_line()); |
| |
| let mut iter = self.grid.iter_from(point); |
| let last_column = self.columns() - 1; |
| |
| let wide = Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER; |
| while let Some(cell) = iter.prev() { |
| if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c) { |
| break; |
| } |
| |
| if cell.point.column == last_column && !cell.flags.contains(Flags::WRAPLINE) { |
| break; // cut off if on new line or hit escape char |
| } |
| |
| point = cell.point; |
| } |
| |
| point |
| } |
| |
| /// Find right end of semantic block. |
| pub fn semantic_search_right(&self, mut point: Point) -> Point { |
| // Limit the starting point to the last line in the history |
| point.line = max(point.line, self.topmost_line()); |
| |
| let wide = Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER; |
| let last_column = self.columns() - 1; |
| |
| for cell in self.grid.iter_from(point) { |
| if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c) { |
| break; |
| } |
| |
| point = cell.point; |
| |
| if point.column == last_column && !cell.flags.contains(Flags::WRAPLINE) { |
| break; // cut off if on new line or hit escape char |
| } |
| } |
| |
| point |
| } |
| |
| /// Find the beginning of the current line across linewraps. |
| pub fn line_search_left(&self, mut point: Point) -> Point { |
| while point.line > self.topmost_line() |
| && self.grid[point.line - 1i32][self.last_column()].flags.contains(Flags::WRAPLINE) |
| { |
| point.line -= 1; |
| } |
| |
| point.column = Column(0); |
| |
| point |
| } |
| |
| /// Find the end of the current line across linewraps. |
| pub fn line_search_right(&self, mut point: Point) -> Point { |
| while point.line + 1 < self.screen_lines() |
| && self.grid[point.line][self.last_column()].flags.contains(Flags::WRAPLINE) |
| { |
| point.line += 1; |
| } |
| |
| point.column = self.last_column(); |
| |
| point |
| } |
| } |
| |
| /// Iterator over regex matches. |
| pub struct RegexIter<'a, T> { |
| point: Point, |
| end: Point, |
| direction: Direction, |
| dfas: &'a RegexSearch, |
| term: &'a Term<T>, |
| done: bool, |
| } |
| |
| impl<'a, T> RegexIter<'a, T> { |
| pub fn new( |
| start: Point, |
| end: Point, |
| direction: Direction, |
| term: &'a Term<T>, |
| dfas: &'a RegexSearch, |
| ) -> Self { |
| Self { point: start, done: false, end, direction, term, dfas } |
| } |
| |
| /// Skip one cell, advancing the origin point to the next one. |
| fn skip(&mut self) { |
| self.point = self.term.expand_wide(self.point, self.direction); |
| |
| self.point = match self.direction { |
| Direction::Right => self.point.add(self.term, Boundary::None, 1), |
| Direction::Left => self.point.sub(self.term, Boundary::None, 1), |
| }; |
| } |
| |
| /// Get the next match in the specified direction. |
| fn next_match(&self) -> Option<Match> { |
| match self.direction { |
| Direction::Right => self.term.regex_search_right(self.dfas, self.point, self.end), |
| Direction::Left => self.term.regex_search_left(self.dfas, self.point, self.end), |
| } |
| } |
| } |
| |
| impl<'a, T> Iterator for RegexIter<'a, T> { |
| type Item = Match; |
| |
| fn next(&mut self) -> Option<Self::Item> { |
| if self.done { |
| return None; |
| } |
| |
| // Since the end itself might be a single cell match, we search one more time. |
| if self.point == self.end { |
| self.done = true; |
| } |
| |
| let regex_match = self.next_match()?; |
| |
| self.point = *regex_match.end(); |
| if self.point == self.end { |
| // Stop when the match terminates right on the end limit. |
| self.done = true; |
| } else { |
| // Move the new search origin past the match. |
| self.skip(); |
| } |
| |
| Some(regex_match) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use crate::config::Config; |
| use crate::index::{Column, Line}; |
| use crate::term::test::mock_term; |
| use crate::term::SizeInfo; |
| |
| #[test] |
| fn regex_right() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| testing66\r\n\ |
| Alacritty\n\ |
| 123\r\n\ |
| Alacritty\r\n\ |
| 123\ |
| "); |
| |
| // Check regex across wrapped and unwrapped lines. |
| let dfas = RegexSearch::new("Ala.*123").unwrap(); |
| let start = Point::new(Line(1), Column(0)); |
| let end = Point::new(Line(4), Column(2)); |
| let match_start = Point::new(Line(1), Column(0)); |
| let match_end = Point::new(Line(2), Column(2)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=match_end)); |
| } |
| |
| #[test] |
| fn regex_left() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| testing66\r\n\ |
| Alacritty\n\ |
| 123\r\n\ |
| Alacritty\r\n\ |
| 123\ |
| "); |
| |
| // Check regex across wrapped and unwrapped lines. |
| let dfas = RegexSearch::new("Ala.*123").unwrap(); |
| let start = Point::new(Line(4), Column(2)); |
| let end = Point::new(Line(1), Column(0)); |
| let match_start = Point::new(Line(1), Column(0)); |
| let match_end = Point::new(Line(2), Column(2)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end)); |
| } |
| |
| #[test] |
| fn nested_regex() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| Ala -> Alacritty -> critty\r\n\ |
| critty\ |
| "); |
| |
| // Greedy stopped at linebreak. |
| let dfas = RegexSearch::new("Ala.*critty").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(0), Column(25)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end)); |
| |
| // Greedy stopped at dead state. |
| let dfas = RegexSearch::new("Ala[^y]*critty").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(0), Column(15)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end)); |
| } |
| |
| #[test] |
| fn no_match_right() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| first line\n\ |
| broken second\r\n\ |
| third\ |
| "); |
| |
| let dfas = RegexSearch::new("nothing").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(2), Column(4)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), None); |
| } |
| |
| #[test] |
| fn no_match_left() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| first line\n\ |
| broken second\r\n\ |
| third\ |
| "); |
| |
| let dfas = RegexSearch::new("nothing").unwrap(); |
| let start = Point::new(Line(2), Column(4)); |
| let end = Point::new(Line(0), Column(0)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), None); |
| } |
| |
| #[test] |
| fn include_linebreak_left() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| testing123\r\n\ |
| xxx\ |
| "); |
| |
| // Make sure the cell containing the linebreak is not skipped. |
| let dfas = RegexSearch::new("te.*123").unwrap(); |
| let start = Point::new(Line(1), Column(0)); |
| let end = Point::new(Line(0), Column(0)); |
| let match_start = Point::new(Line(0), Column(0)); |
| let match_end = Point::new(Line(0), Column(9)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end)); |
| } |
| |
| #[test] |
| fn include_linebreak_right() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| xxx\r\n\ |
| testing123\ |
| "); |
| |
| // Make sure the cell containing the linebreak is not skipped. |
| let dfas = RegexSearch::new("te.*123").unwrap(); |
| let start = Point::new(Line(0), Column(2)); |
| let end = Point::new(Line(1), Column(9)); |
| let match_start = Point::new(Line(1), Column(0)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=end)); |
| } |
| |
| #[test] |
| fn skip_dead_cell() { |
| let term = mock_term("alacritty"); |
| |
| // Make sure dead state cell is skipped when reversing. |
| let dfas = RegexSearch::new("alacrit").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(0), Column(6)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end)); |
| } |
| |
| #[test] |
| fn reverse_search_dead_recovery() { |
| let term = mock_term("zooo lense"); |
| |
| // Make sure the reverse DFA operates the same as a forward DFA. |
| let dfas = RegexSearch::new("zoo").unwrap(); |
| let start = Point::new(Line(0), Column(9)); |
| let end = Point::new(Line(0), Column(0)); |
| let match_start = Point::new(Line(0), Column(0)); |
| let match_end = Point::new(Line(0), Column(2)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end)); |
| } |
| |
| #[test] |
| fn multibyte_unicode() { |
| let term = mock_term("testвосибing"); |
| |
| let dfas = RegexSearch::new("te.*ing").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(0), Column(11)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end)); |
| |
| let dfas = RegexSearch::new("te.*ing").unwrap(); |
| let start = Point::new(Line(0), Column(11)); |
| let end = Point::new(Line(0), Column(0)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(end..=start)); |
| } |
| |
| #[test] |
| fn fullwidth() { |
| let term = mock_term("a🦇x🦇"); |
| |
| let dfas = RegexSearch::new("[^ ]*").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(0), Column(5)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end)); |
| |
| let dfas = RegexSearch::new("[^ ]*").unwrap(); |
| let start = Point::new(Line(0), Column(5)); |
| let end = Point::new(Line(0), Column(0)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(end..=start)); |
| } |
| |
| #[test] |
| fn singlecell_fullwidth() { |
| let term = mock_term("🦇"); |
| |
| let dfas = RegexSearch::new("🦇").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(0), Column(1)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=end)); |
| |
| let dfas = RegexSearch::new("🦇").unwrap(); |
| let start = Point::new(Line(0), Column(1)); |
| let end = Point::new(Line(0), Column(0)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(end..=start)); |
| } |
| |
| #[test] |
| fn end_on_fullwidth() { |
| let term = mock_term("jarr🦇"); |
| |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(0), Column(4)); |
| |
| // Ensure ending without a match doesn't loop indefinitely. |
| let dfas = RegexSearch::new("x").unwrap(); |
| assert_eq!(term.regex_search_right(&dfas, start, end), None); |
| |
| let dfas = RegexSearch::new("x").unwrap(); |
| let match_end = Point::new(Line(0), Column(5)); |
| assert_eq!(term.regex_search_right(&dfas, start, match_end), None); |
| |
| // Ensure match is captured when only partially inside range. |
| let dfas = RegexSearch::new("jarr🦇").unwrap(); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(start..=match_end)); |
| } |
| |
| #[test] |
| fn wrapping() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| xxx\r\n\ |
| xxx\ |
| "); |
| |
| let dfas = RegexSearch::new("xxx").unwrap(); |
| let start = Point::new(Line(0), Column(2)); |
| let end = Point::new(Line(1), Column(2)); |
| let match_start = Point::new(Line(1), Column(0)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=end)); |
| |
| let dfas = RegexSearch::new("xxx").unwrap(); |
| let start = Point::new(Line(1), Column(0)); |
| let end = Point::new(Line(0), Column(0)); |
| let match_end = Point::new(Line(0), Column(2)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(end..=match_end)); |
| } |
| |
| #[test] |
| fn wrapping_into_fullwidth() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| 🦇xx\r\n\ |
| xx🦇\ |
| "); |
| |
| let dfas = RegexSearch::new("🦇x").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(1), Column(3)); |
| let match_start = Point::new(Line(0), Column(0)); |
| let match_end = Point::new(Line(0), Column(2)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=match_end)); |
| |
| let dfas = RegexSearch::new("x🦇").unwrap(); |
| let start = Point::new(Line(1), Column(2)); |
| let end = Point::new(Line(0), Column(0)); |
| let match_start = Point::new(Line(1), Column(1)); |
| let match_end = Point::new(Line(1), Column(3)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end)); |
| } |
| |
| #[test] |
| fn leading_spacer() { |
| #[rustfmt::skip] |
| let mut term = mock_term("\ |
| xxx \n\ |
| 🦇xx\ |
| "); |
| term.grid[Line(0)][Column(3)].flags.insert(Flags::LEADING_WIDE_CHAR_SPACER); |
| |
| let dfas = RegexSearch::new("🦇x").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(1), Column(3)); |
| let match_start = Point::new(Line(0), Column(3)); |
| let match_end = Point::new(Line(1), Column(2)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=match_end)); |
| |
| let dfas = RegexSearch::new("🦇x").unwrap(); |
| let start = Point::new(Line(1), Column(3)); |
| let end = Point::new(Line(0), Column(0)); |
| let match_start = Point::new(Line(0), Column(3)); |
| let match_end = Point::new(Line(1), Column(2)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end)); |
| |
| let dfas = RegexSearch::new("x🦇").unwrap(); |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(1), Column(3)); |
| let match_start = Point::new(Line(0), Column(2)); |
| let match_end = Point::new(Line(1), Column(1)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=match_end)); |
| |
| let dfas = RegexSearch::new("x🦇").unwrap(); |
| let start = Point::new(Line(1), Column(3)); |
| let end = Point::new(Line(0), Column(0)); |
| let match_start = Point::new(Line(0), Column(2)); |
| let match_end = Point::new(Line(1), Column(1)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end)); |
| } |
| |
| #[test] |
| fn wide_without_spacer() { |
| let size = SizeInfo::new(2., 2., 1., 1., 0., 0., false); |
| let mut term = Term::new(&Config::default(), size, ()); |
| term.grid[Line(0)][Column(0)].c = 'x'; |
| term.grid[Line(0)][Column(1)].c = '字'; |
| term.grid[Line(0)][Column(1)].flags = Flags::WIDE_CHAR; |
| |
| let dfas = RegexSearch::new("test").unwrap(); |
| |
| let start = Point::new(Line(0), Column(0)); |
| let end = Point::new(Line(0), Column(1)); |
| |
| let mut iter = RegexIter::new(start, end, Direction::Right, &term, &dfas); |
| assert_eq!(iter.next(), None); |
| } |
| |
| #[test] |
| fn wrap_around_to_another_end() { |
| #[rustfmt::skip] |
| let term = mock_term("\ |
| abc\r\n\ |
| def\ |
| "); |
| |
| // Bottom to top. |
| let dfas = RegexSearch::new("abc").unwrap(); |
| let start = Point::new(Line(1), Column(0)); |
| let end = Point::new(Line(0), Column(2)); |
| let match_start = Point::new(Line(0), Column(0)); |
| let match_end = Point::new(Line(0), Column(2)); |
| assert_eq!(term.regex_search_right(&dfas, start, end), Some(match_start..=match_end)); |
| |
| // Top to bottom. |
| let dfas = RegexSearch::new("def").unwrap(); |
| let start = Point::new(Line(0), Column(2)); |
| let end = Point::new(Line(1), Column(0)); |
| let match_start = Point::new(Line(1), Column(0)); |
| let match_end = Point::new(Line(1), Column(2)); |
| assert_eq!(term.regex_search_left(&dfas, start, end), Some(match_start..=match_end)); |
| } |
| } |