Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/match_brackets.rs')
| -rw-r--r-- | helix-core/src/match_brackets.rs | 336 |
1 files changed, 40 insertions, 296 deletions
diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index 7f2891f3..0189dedd 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -1,54 +1,28 @@ -use std::iter; +use tree_sitter::Node; -use crate::tree_sitter::Node; -use ropey::RopeSlice; +use crate::{Rope, Syntax}; -use crate::movement::Direction::{self, Backward, Forward}; -use crate::Syntax; - -const MAX_PLAINTEXT_SCAN: usize = 10000; -const MATCH_LIMIT: usize = 16; - -pub const BRACKETS: [(char, char); 9] = [ +const PAIRS: &[(char, char)] = &[ ('(', ')'), ('{', '}'), ('[', ']'), ('<', '>'), - ('‘', '’'), - ('“', '”'), - ('«', '»'), - ('「', '」'), - ('(', ')'), + ('\'', '\''), + ('\"', '\"'), ]; -// The difference between BRACKETS and PAIRS is that we can find matching -// BRACKETS in a plain text file, but we can't do the same for PAIRs. -// PAIRS also contains all BRACKETS. -pub const PAIRS: [(char, char); BRACKETS.len() + 3] = { - let mut pairs = [(' ', ' '); BRACKETS.len() + 3]; - let mut idx = 0; - while idx < BRACKETS.len() { - pairs[idx] = BRACKETS[idx]; - idx += 1; - } - pairs[idx] = ('"', '"'); - pairs[idx + 1] = ('\'', '\''); - pairs[idx + 2] = ('`', '`'); - pairs -}; +// limit matching pairs to only ( ) { } [ ] < > ' ' " " -/// Returns the position of the matching bracket under cursor. -/// -/// If the cursor is on the opening bracket, the position of -/// the closing bracket is returned. If the cursor on the closing -/// bracket, the position of the opening bracket is returned. -/// -/// If the cursor is not on a bracket, `None` is returned. -/// -/// If no matching bracket is found, `None` is returned. +// Returns the position of the matching bracket under cursor. +// +// If the cursor is one the opening bracket, the position of +// the closing bracket is returned. If the cursor in the closing +// bracket, the position of the opening bracket is returned. +// +// If the cursor is not on a bracket, `None` is returned. #[must_use] -pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option<usize> { - if pos >= doc.len_chars() || !is_valid_pair(doc.char(pos)) { +pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> { + if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) { return None; } find_pair(syntax, doc, pos, false) @@ -65,284 +39,54 @@ pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Opt // // If no surrounding scope is found, the function returns `None`. #[must_use] -pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option<usize> { +pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> { find_pair(syntax, doc, pos, true) } -fn find_pair( - syntax: &Syntax, - doc: RopeSlice, - pos_: usize, - traverse_parents: bool, -) -> Option<usize> { - let pos = doc.char_to_byte(pos_) as u32; +fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) -> Option<usize> { + let tree = syntax.tree(); + let pos = doc.char_to_byte(pos); - let root = syntax.tree_for_byte_range(pos, pos).root_node(); - let mut node = root.descendant_for_byte_range(pos, pos)?; + let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?; loop { - if node.is_named() && node.child_count() >= 2 { - let open = node.child(0).unwrap(); - let close = node.child(node.child_count() - 1).unwrap(); - - if let (Some((start_pos, open)), Some((end_pos, close))) = - (as_char(doc, &open), as_char(doc, &close)) - { - if PAIRS.contains(&(open, close)) { - if end_pos == pos_ { - return Some(start_pos); - } - - // We return the end char if the cursor is either on the start char - // or at some arbitrary position between start and end char. - if traverse_parents || start_pos == pos_ { - return Some(end_pos); - } - } - } - } - // this node itselt wasn't a pair but maybe its siblings are + let (start_byte, end_byte) = surrounding_bytes(doc, &node)?; + let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte)); - if let Some((start_char, end_char)) = as_close_pair(doc, &node) { - if let Some(pair_start) = - find_pair_end(doc, node.prev_sibling(), start_char, end_char, Backward) - { - return Some(pair_start); - } - } - if let Some((start_char, end_char)) = as_open_pair(doc, &node) { - if let Some(pair_end) = - find_pair_end(doc, node.next_sibling(), start_char, end_char, Forward) - { - return Some(pair_end); + if is_valid_pair(doc, start_char, end_char) { + if end_byte == pos { + return Some(start_char); } + // We return the end char if the cursor is either on the start char + // or at some arbitrary position between start and end char. + return Some(end_char); } if traverse_parents { - for sibling in - iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT) - { - let Some((start_char, end_char)) = as_close_pair(doc, &sibling) else { - continue; - }; - if find_pair_end(doc, sibling.prev_sibling(), start_char, end_char, Backward) - .is_some() - { - return doc.try_byte_to_char(sibling.start_byte() as usize).ok(); - } - } - } else if node.is_named() { - break; - } - - let Some(parent) = node.parent() else { - break; - }; - node = parent; - } - let node = root.named_descendant_for_byte_range(pos, pos + 1)?; - if node.child_count() != 0 { - return None; - } - let node_start = doc.byte_to_char(node.start_byte() as usize); - let node_text = doc.byte_slice(node.start_byte() as usize..node.end_byte() as usize); - find_matching_bracket_plaintext(node_text, pos_ - node_start).map(|pos| pos + node_start) -} - -/// Returns the position of the matching bracket under cursor. -/// This function works on plain text and ignores tree-sitter grammar. -/// The search is limited to `MAX_PLAINTEXT_SCAN` characters -/// -/// If the cursor is on the opening bracket, the position of -/// the closing bracket is returned. If the cursor on the closing -/// bracket, the position of the opening bracket is returned. -/// -/// If the cursor is not on a bracket, `None` is returned. -/// -/// If no matching bracket is found, `None` is returned. -#[must_use] -pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> { - let bracket = doc.get_char(cursor_pos)?; - let matching_bracket = { - let pair = get_pair(bracket); - if pair.0 == bracket { - pair.1 + node = node.parent()?; } else { - pair.0 - } - }; - // Don't do anything when the cursor is not on top of a bracket. - if !is_valid_bracket(bracket) { - return None; - } - - // Determine the direction of the matching. - let is_fwd = is_open_bracket(bracket); - let chars_iter = if is_fwd { - doc.chars_at(cursor_pos + 1) - } else { - doc.chars_at(cursor_pos).reversed() - }; - - let mut open_cnt = 1; - - for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() { - if candidate == bracket { - open_cnt += 1; - } else if candidate == matching_bracket { - // Return when all pending brackets have been closed. - if open_cnt == 1 { - return Some(if is_fwd { - cursor_pos + i + 1 - } else { - cursor_pos - i - 1 - }); - } - open_cnt -= 1; + return None; } } - - None -} - -/// Returns the open and closing chars pair. If not found in -/// [`BRACKETS`] returns (ch, ch). -/// -/// ``` -/// use helix_core::match_brackets::get_pair; -/// -/// assert_eq!(get_pair('['), ('[', ']')); -/// assert_eq!(get_pair('}'), ('{', '}')); -/// assert_eq!(get_pair('"'), ('"', '"')); -/// ``` -pub fn get_pair(ch: char) -> (char, char) { - PAIRS - .iter() - .find(|(open, close)| *open == ch || *close == ch) - .copied() - .unwrap_or((ch, ch)) -} - -pub fn is_open_bracket(ch: char) -> bool { - BRACKETS.iter().any(|(l, _)| *l == ch) -} - -pub fn is_close_bracket(ch: char) -> bool { - BRACKETS.iter().any(|(_, r)| *r == ch) -} - -pub fn is_valid_bracket(ch: char) -> bool { - BRACKETS.iter().any(|(l, r)| *l == ch || *r == ch) -} - -pub fn is_open_pair(ch: char) -> bool { - PAIRS.iter().any(|(l, _)| *l == ch) -} - -pub fn is_close_pair(ch: char) -> bool { - PAIRS.iter().any(|(_, r)| *r == ch) } -pub fn is_valid_pair(ch: char) -> bool { - PAIRS.iter().any(|(l, r)| *l == ch || *r == ch) +fn is_valid_bracket(c: char) -> bool { + PAIRS.iter().any(|(l, r)| *l == c || *r == c) } -/// Tests if this node is a pair close char and returns the expected open char -/// and close char contained in this node -fn as_close_pair(doc: RopeSlice, node: &Node) -> Option<(char, char)> { - let close = as_char(doc, node)?.1; - PAIRS - .iter() - .find_map(|&(open, close_)| (close_ == close).then_some((close, open))) +fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool { + PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) } -/// Checks if `node` or its siblings (at most MATCH_LIMIT nodes) is the specified closing char -/// -/// # Returns -/// -/// The position of the found node or `None` otherwise -fn find_pair_end( - doc: RopeSlice, - node: Option<Node>, - start_char: char, - end_char: char, - direction: Direction, -) -> Option<usize> { - let advance = match direction { - Forward => Node::next_sibling, - Backward => Node::prev_sibling, - }; - let mut depth = 0; - iter::successors(node, advance) - .take(MATCH_LIMIT) - .find_map(|node| { - let (pos, c) = as_char(doc, &node)?; - if c == end_char { - if depth == 0 { - return Some(pos); - } - depth -= 1; - } else if c == start_char { - depth += 1; - } - None - }) -} +fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> { + let len = doc.len_bytes(); -/// Tests if this node is a pair open char and returns the expected close char -/// and open char contained in this node -fn as_open_pair(doc: RopeSlice, node: &Node) -> Option<(char, char)> { - let open = as_char(doc, node)?.1; - PAIRS - .iter() - .find_map(|&(open_, close)| (open_ == open).then_some((open, close))) -} + let start_byte = node.start_byte(); + let end_byte = node.end_byte().saturating_sub(1); -/// If node is a single char return it (and its char position) -fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> { - // TODO: multi char/non ASCII pairs - if node.byte_range().len() != 1 { + if start_byte >= len || end_byte >= len { return None; } - let pos = doc.try_byte_to_char(node.start_byte() as usize).ok()?; - Some((pos, doc.char(pos))) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn find_matching_bracket_empty_file() { - let actual = find_matching_bracket_plaintext("".into(), 0); - assert_eq!(actual, None); - } - #[test] - fn test_find_matching_bracket_current_line_plaintext() { - let assert = |input: &str, pos, expected| { - let input = RopeSlice::from(input); - let actual = find_matching_bracket_plaintext(input, pos); - assert_eq!(expected, actual.unwrap()); - - let actual = find_matching_bracket_plaintext(input, expected); - assert_eq!(pos, actual.unwrap(), "expected symmetrical behaviour"); - }; - - assert("(hello)", 0, 6); - assert("((hello))", 0, 8); - assert("((hello))", 1, 7); - assert("(((hello)))", 2, 8); - - assert("key: ${value}", 6, 12); - assert("key: ${value} # (some comment)", 16, 29); - - assert("(paren (paren {bracket}))", 0, 24); - assert("(paren (paren {bracket}))", 7, 23); - assert("(paren (paren {bracket}))", 14, 22); - - assert("(prev line\n ) (middle) ( \n next line)", 0, 12); - assert("(prev line\n ) (middle) ( \n next line)", 14, 21); - assert("(prev line\n ) (middle) ( \n next line)", 23, 36); - } + Some((start_byte, end_byte)) } |