on master help sign in
Jump to end char of surrounding pair from any cursor pos (#1121)
a3a3b0bMartin Junghanns
helix-core/src/match_brackets.rs+66−29
@@ -1,3 +1,5 @@
1+use tree_sitter::Node;
2+
1use crate::{Rope, Syntax};
3use crate::{Rope, Syntax};
2
4
3const PAIRS: &[(char, char)] = &[
5const PAIRS: &[(char, char)] = &[
@@ -6,50 +8,85 @@ const PAIRS: &[(char, char)] = &[
6 ('[', ']'),
8 ('[', ']'),
7 ('<', '>'),
9 ('<', '>'),
8 ('\'', '\''),
10 ('\'', '\''),
9− ('"', '"'),
11+ ('\"', '\"'),
10];
12];
13+
11// limit matching pairs to only ( ) { } [ ] < >
14// limit matching pairs to only ( ) { } [ ] < >
12
15
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`.
13#[must_use]
41#[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> {
15 let tree = syntax.tree();
47 let tree = syntax.tree();
48+ let pos = doc.char_to_byte(pos);
16
49
17− let byte_pos = doc.char_to_byte(pos);
50+ let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
18
51
19− // most naive implementation: find the innermost syntax node, if we're at the edge of a node,
52+ loop {
20− // return the other edge.
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));
21
55
22− let node = match tree
56+ if is_valid_pair(doc, start_char, end_char) {
23− .root_node()
57+ if end_byte == pos {
24− .named_descendant_for_byte_range(byte_pos, byte_pos)
58+ return Some(start_char);
25− {
59+ }
26− Some(node) => node,
60+ // We return the end char if the cursor is either on the start char
27− None => return None,
61+ // or at some arbitrary position between start and end char.
62+ return Some(end_char);
28− };
63+ }
29
64
30− if node.is_error() {
65+ if traverse_parents {
66+ node = node.parent()?;
67+ } else {
31− return None;
68+ return None;
69+ }
32 }
70 }
71+}
33
72
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)> {
34 let len = doc.len_bytes();
82 let len = doc.len_bytes();
83+
35 let start_byte = node.start_byte();
84 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+
37 if start_byte >= len || end_byte >= len {
87 if start_byte >= len || end_byte >= len {
38 return None;
88 return None;
39 }
89 }
40
90
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 {
91+ Some((start_byte, end_byte))
50− return Some(start_char);
51− }
52− }
53−
54− None
55}
92}
helix-term/src/commands.rs+3−1
@@ -4954,7 +4954,9 @@ fn match_brackets(cx: &mut Context) {
4954 if let Some(syntax) = doc.syntax() {
4954 if let Some(syntax) = doc.syntax() {
4955 let text = doc.text().slice(..);
4955 let text = doc.text().slice(..);
4956 let selection = doc.selection(view.id).clone().transform(|range| {
4956 let selection = doc.selection(view.id).clone().transform(|range| {
4957+ if let Some(pos) =
4957− if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
4958+ match_brackets::find_matching_bracket_fuzzy(syntax, doc.text(), range.anchor)
4959+ {
4958 range.put_cursor(text, pos, doc.mode == Mode::Select)
4960 range.put_cursor(text, pos, doc.mode == Mode::Select)
4959 } else {
4961 } else {
4960 range
4962 range
helix-term/src/ui/editor.rs+1−1
@@ -377,7 +377,7 @@ impl EditorView {
377 use helix_core::match_brackets;
377 use helix_core::match_brackets;
378 let pos = doc.selection(view.id).primary().cursor(text);
378 let pos = doc.selection(view.id).primary().cursor(text);
379
379
380− let pos = match_brackets::find(syntax, doc.text(), pos)
380+ let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
381 .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
381 .and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
382
382
383 if let Some(pos) = pos {
383 if let Some(pos) = pos {