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.rs356
1 files changed, 28 insertions, 328 deletions
diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs
index 7f2891f3..a4b2fb9c 100644
--- a/helix-core/src/match_brackets.rs
+++ b/helix-core/src/match_brackets.rs
@@ -1,348 +1,48 @@
-use std::iter;
+use crate::{Rope, Syntax};
-use crate::tree_sitter::Node;
-use ropey::RopeSlice;
+const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')];
+// limit matching pairs to only ( ) { } [ ] < >
-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] = [
- ('(', ')'),
- ('{', '}'),
- ('[', ']'),
- ('<', '>'),
- ('‘', '’'),
- ('“', '”'),
- ('«', '»'),
- ('「', '」'),
- ('(', ')'),
-];
-
-// 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
-};
-
-/// 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.
-#[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)) {
- return None;
- }
- find_pair(syntax, doc, pos, false)
-}
-
-// Returns the position of the bracket that is closing the current scope.
-//
-// If the cursor is on an opening or closing bracket, the function
-// behaves equivalent to [`find_matching_bracket`].
-//
-// If the cursor position is within a scope, the function searches
-// for the surrounding scope that is surrounded by brackets and
-// returns the position of the closing bracket for that scope.
-//
-// 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> {
- 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;
+pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
+ let tree = syntax.tree();
- let root = syntax.tree_for_byte_range(pos, pos).root_node();
- let mut node = root.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 byte_pos = doc.char_to_byte(pos);
- 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);
- }
- }
+ // most naive implementation: find the innermost syntax node, if we're at the edge of a node,
+ // return the other edge.
- 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 node = match tree
+ .root_node()
+ .named_descendant_for_byte_range(byte_pos, byte_pos)
+ {
+ Some(node) => node,
+ None => return None,
+ };
- 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 {
+ if node.is_error() {
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
- } else {
- pair.0
- }
- };
- // Don't do anything when the cursor is not on top of a bracket.
- if !is_valid_bracket(bracket) {
+ let len = doc.len_bytes();
+ let start_byte = node.start_byte();
+ let end_byte = node.end_byte().saturating_sub(1); // it's end exclusive
+ if start_byte >= len || end_byte >= len {
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 start_char = doc.byte_to_char(start_byte);
+ let end_char = doc.byte_to_char(end_byte);
- let mut open_cnt = 1;
+ if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
+ if start_byte == byte_pos {
+ return Some(end_char);
+ }
- 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;
+ if end_byte == byte_pos {
+ return Some(start_char);
}
}
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)
-}
-
-/// 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)))
-}
-
-/// 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
- })
-}
-
-/// 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)))
-}
-
-/// 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 {
- 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);
- }
-}