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.rs225
1 files changed, 86 insertions, 139 deletions
diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs
index 7f2891f3..f6d9885e 100644
--- a/helix-core/src/match_brackets.rs
+++ b/helix-core/src/match_brackets.rs
@@ -1,7 +1,7 @@
use std::iter;
-use crate::tree_sitter::Node;
use ropey::RopeSlice;
+use tree_sitter::Node;
use crate::movement::Direction::{self, Backward, Forward};
use crate::Syntax;
@@ -9,34 +9,16 @@ use crate::Syntax;
const MAX_PLAINTEXT_SCAN: usize = 10000;
const MATCH_LIMIT: usize = 16;
-pub const BRACKETS: [(char, char); 9] = [
+// Limit matching pairs to only ( ) { } [ ] < > ' ' " "
+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
-};
-
/// Returns the position of the matching bracket under cursor.
///
/// If the cursor is on the opening bracket, the position of
@@ -48,7 +30,7 @@ pub const PAIRS: [(char, char); BRACKETS.len() + 3] = {
/// 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)) {
+ if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
return None;
}
find_pair(syntax, doc, pos, false)
@@ -75,78 +57,74 @@ fn find_pair(
pos_: usize,
traverse_parents: bool,
) -> Option<usize> {
- let pos = doc.char_to_byte(pos_) as u32;
+ 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().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);
- }
+ if node.is_named() {
+ 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 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.
+ if traverse_parents || start_byte == pos {
+ return Some(end_char);
}
}
}
// this node itselt wasn't a pair but maybe its siblings are
- 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)
- {
+ // check if we are *on* the pair (special cased so we don't look
+ // at the current node twice and to jump to the start on that case)
+ if let Some(open) = as_close_pair(doc, &node) {
+ if let Some(pair_start) = find_pair_end(doc, node.prev_sibling(), open, 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 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();
+ if !traverse_parents {
+ // check if we are *on* the opening pair (special cased here as
+ // an opptimization since we only care about bracket on the cursor
+ // here)
+ if let Some(close) = as_open_pair(doc, &node) {
+ if let Some(pair_end) = find_pair_end(doc, node.next_sibling(), close, Forward) {
+ return Some(pair_end);
}
}
- } else if node.is_named() {
- break;
+ if node.is_named() {
+ break;
+ }
}
+ for close in
+ iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT)
+ {
+ let Some(open) = as_close_pair(doc, &close) else {
+ continue;
+ };
+ if find_pair_end(doc, Some(node), open, Backward).is_some() {
+ return doc.try_byte_to_char(close.start_byte()).ok();
+ }
+ }
let Some(parent) = node.parent() else {
break;
};
node = parent;
}
- let node = root.named_descendant_for_byte_range(pos, pos + 1)?;
+ let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
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)
+ let node_start = doc.byte_to_char(node.start_byte());
+ find_matching_bracket_plaintext(doc.byte_slice(node.byte_range()), pos_ - node_start)
+ .map(|pos| pos + node_start)
}
/// Returns the position of the matching bracket under cursor.
@@ -162,22 +140,14 @@ fn find_pair(
/// 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.
+ let bracket = doc.char(cursor_pos);
if !is_valid_bracket(bracket) {
return None;
}
// Determine the direction of the matching.
- let is_fwd = is_open_bracket(bracket);
+ let is_fwd = is_forward_bracket(bracket);
let chars_iter = if is_fwd {
doc.chars_at(cursor_pos + 1)
} else {
@@ -189,7 +159,19 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt
for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() {
if candidate == bracket {
open_cnt += 1;
- } else if candidate == matching_bracket {
+ } else if is_valid_pair(
+ doc,
+ if is_fwd {
+ cursor_pos
+ } else {
+ cursor_pos - i - 1
+ },
+ if is_fwd {
+ cursor_pos + i + 1
+ } else {
+ cursor_pos
+ },
+ ) {
// Return when all pending brackets have been closed.
if open_cnt == 1 {
return Some(if is_fwd {
@@ -205,55 +187,37 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt
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))
+fn is_valid_bracket(c: char) -> bool {
+ PAIRS.iter().any(|(l, r)| *l == c || *r == c)
}
-pub fn is_open_bracket(ch: char) -> bool {
- BRACKETS.iter().any(|(l, _)| *l == ch)
+fn is_forward_bracket(c: char) -> bool {
+ PAIRS.iter().any(|(l, _)| *l == c)
}
-pub fn is_close_bracket(ch: char) -> bool {
- BRACKETS.iter().any(|(_, r)| *r == ch)
+fn is_valid_pair(doc: RopeSlice, start_char: usize, end_char: usize) -> bool {
+ PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
}
-pub fn is_valid_bracket(ch: char) -> bool {
- BRACKETS.iter().any(|(l, r)| *l == ch || *r == ch)
-}
+fn surrounding_bytes(doc: RopeSlice, node: &Node) -> Option<(usize, usize)> {
+ let len = doc.len_bytes();
-pub fn is_open_pair(ch: char) -> bool {
- PAIRS.iter().any(|(l, _)| *l == ch)
-}
+ let start_byte = node.start_byte();
+ let end_byte = node.end_byte().saturating_sub(1);
-pub fn is_close_pair(ch: char) -> bool {
- PAIRS.iter().any(|(_, r)| *r == ch)
-}
+ if start_byte >= len || end_byte >= len {
+ return None;
+ }
-pub fn is_valid_pair(ch: char) -> bool {
- PAIRS.iter().any(|(l, r)| *l == ch || *r == ch)
+ Some((start_byte, end_byte))
}
/// 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)> {
+fn as_close_pair(doc: RopeSlice, node: &Node) -> Option<char> {
let close = as_char(doc, node)?.1;
PAIRS
.iter()
- .find_map(|&(open, close_)| (close_ == close).then_some((close, open)))
+ .find_map(|&(open, close_)| (close_ == close).then_some(open))
}
/// Checks if `node` or its siblings (at most MATCH_LIMIT nodes) is the specified closing char
@@ -264,7 +228,6 @@ fn as_close_pair(doc: RopeSlice, node: &Node) -> Option<(char, char)> {
fn find_pair_end(
doc: RopeSlice,
node: Option<Node>,
- start_char: char,
end_char: char,
direction: Direction,
) -> Option<usize> {
@@ -272,30 +235,20 @@ fn find_pair_end(
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
+ (end_char == c).then_some(pos)
})
}
-/// 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)> {
+/// Tests if this node is a pair close char and returns the expected open char
+fn as_open_pair(doc: RopeSlice, node: &Node) -> Option<char> {
let open = as_char(doc, node)?.1;
PAIRS
.iter()
- .find_map(|&(open_, close)| (open_ == open).then_some((open, close)))
+ .find_map(|&(open_, close)| (open_ == open).then_some(close))
}
/// If node is a single char return it (and its char position)
@@ -304,7 +257,7 @@ fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> {
if node.byte_range().len() != 1 {
return None;
}
- let pos = doc.try_byte_to_char(node.start_byte() as usize).ok()?;
+ let pos = doc.try_byte_to_char(node.start_byte()).ok()?;
Some((pos, doc.char(pos)))
}
@@ -313,12 +266,6 @@ 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);