on master help sign in
Jump to end char of surrounding pair from any cursor pos (#1121)

* Jump to end char of surrounding pair from any cursor pos * Separate bracket matching into exact and fuzzy search * Add constants for bracket chars * Abort early if char under cursor is not a bracket * Simplify bracket char validation * Refactor node search and unify find methods * Remove bracket constants

commita3a3b0b517d0e690f3efc66b17ac7b9f769dba9d authorMartin Junghanns <martin.junghanns@neo4j.com> dateSat 20 Nov 2021, 06:17 (5 years ago) parentb95c947
3 files changed+70−31
helix-core/src/match_brackets.rs+66−29 helix-term/src/commands.rs+3−1 helix-term/src/ui/editor.rs+1−1
helix-core/src/match_brackets.rs+66−29
@@ -1,3 +1,5 @@
1+use tree_sitter::Node;
2+
13use crate::{Rope, Syntax};
24
35const PAIRS: &[(char, char)] = &[
@@ -6,50 +8,85 @@ const PAIRS: &[(char, char)] = &[
68 ('[', ']'),
79 ('<', '>'),
810 ('\'', '\''),
9− ('"', '"'),
11+ ('\"', '\"'),
1012];
13+
1114// limit matching pairs to only ( ) { } [ ] < >
1215
16+// Returns the position of the matching bracket under cursor.
17+//
18+// If the cursor is one the opening bracket, the position of
19+// the closing bracket is returned. If the cursor in the closing
20+// bracket, the position of the opening bracket is returned.
21+//
22+// If the cursor is not on a bracket, `None` is returned.
23+#[must_use]
24+pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
25+ if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
26+ return None;
27+ }
28+ find_pair(syntax, doc, pos, false)
29+}
30+
31+// Returns the position of the bracket that is closing the current scope.
32+//
33+// If the cursor is on an opening or closing bracket, the function
34+// behaves equivalent to [`find_matching_bracket`].
35+//
36+// If the cursor position is within a scope, the function searches
37+// for the surrounding scope that is surrounded by brackets and
38+// returns the position of the closing bracket for that scope.
39+//
40+// If no surrounding scope is found, the function returns `None`.
1341#[must_use]
14−pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
42+pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
43+ find_pair(syntax, doc, pos, true)
44+}
45+
46+fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) -> Option<usize> {
1547 let tree = syntax.tree();
48+ let pos = doc.char_to_byte(pos);
1649
17− let byte_pos = doc.char_to_byte(pos);
50+ let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
1851
19− // most naive implementation: find the innermost syntax node, if we're at the edge of a node,
20− // return the other edge.
52+ loop {
53+ let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
54+ let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
2155
22− let node = match tree
23− .root_node()
24− .named_descendant_for_byte_range(byte_pos, byte_pos)
25− {
26− Some(node) => node,
27− None => return None,
28− };
56+ if is_valid_pair(doc, start_char, end_char) {
57+ if end_byte == pos {
58+ return Some(start_char);
59+ }
60+ // We return the end char if the cursor is either on the start char
61+ // or at some arbitrary position between start and end char.
62+ return Some(end_char);
63+ }
2964
30− if node.is_error() {
31− return None;
65+ if traverse_parents {
66+ node = node.parent()?;
67+ } else {
68+ return None;
69+ }
3270 }
71+}
3372
73+fn is_valid_bracket(c: char) -> bool {
74+ PAIRS.iter().any(|(l, r)| *l == c || *r == c)
75+}
76+
77+fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool {
78+ PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
79+}
80+
81+fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
3482 let len = doc.len_bytes();
83+
3584 let start_byte = node.start_byte();
36− let end_byte = node.end_byte().saturating_sub(1); // it's end exclusive
85+ let end_byte = node.end_byte().saturating_sub(1);
86+
3787 if start_byte >= len || end_byte >= len {
3888 return None;
3989 }
4090
41− let start_char = doc.byte_to_char(start_byte);
42− let end_char = doc.byte_to_char(end_byte);
43−
44− if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
45− if start_byte == byte_pos {
46− return Some(end_char);
47− }
48−
49− if end_byte == byte_pos {
50− return Some(start_char);
51− }
52− }
53−
54− None
91+ Some((start_byte, end_byte))
5592}
helix-term/src/commands.rs+3−1
@@ -4954,7 +4954,9 @@ fn match_brackets(cx: &mut Context) {
49544954 if let Some(syntax) = doc.syntax() {
49554955 let text = doc.text().slice(..);
49564956 let selection = doc.selection(view.id).clone().transform(|range| {
4957− if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
4957+ if let Some(pos) =
4958+ match_brackets::find_matching_bracket_fuzzy(syntax, doc.text(), range.anchor)
4959+ {
49584960 range.put_cursor(text, pos, doc.mode == Mode::Select)
49594961 } else {
49604962 range
helix-term/src/ui/editor.rs+1−1
@@ -377,7 +377,7 @@ impl EditorView {
377377 use helix_core::match_brackets;
378378 let pos = doc.selection(view.id).primary().cursor(text);
379379
380− let pos = match_brackets::find(syntax, doc.text(), pos)
380+ let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
381381 .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
382382
383383 if let Some(pos) = pos {