Skip to content

Commit

Permalink
bracket_matching: add plaintext matching fallback to tree-sitter matc…
Browse files Browse the repository at this point in the history
…hing

This patch introduces bracket matching independent of tree-sitter grammar.
For the initial iteration of this feature, only match on the current line.
This matching is introduced as a fallback in cases where the tree-sitter
matcher does not match any bracket.

This fallback should provide a better experience to users that are editing
documents without tree-sitter grammar, but also provides a better experience
in cases like the ones reported in helix-editor#3614

If we find that this feature works well, we could consider extending it
for multi-line matching, but I wanted to keep it small for the first iteration
and gather thoughts beforehand.
  • Loading branch information
alevinval committed Jun 1, 2023
1 parent d511122 commit 2bb2562
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 1 deletion.
96 changes: 96 additions & 0 deletions helix-core/src/match_brackets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,63 @@ fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) ->
}
}

// Returns the position of the bracket that is closing, searching only in
// the current line, and works on plain text, ignoring the tree-sitter grammar.
//
// If the cursor is on an opening or closing bracket, the function
// behaves equivalent to [`find_matching_bracket`].
//
// If no matchig bracket is found on the current line, returns None.
#[must_use]
pub fn find_matching_bracket_current_line_plaintext(doc: &Rope, pos: usize) -> Option<usize> {
// Don't do anything when the cursor is not on top of a bracket.
let mut c = doc.char(pos);
if !is_valid_bracket(c) {
return None;
}

let bracket = c;
let bracket_pos = pos;

// Determine the direction of the matching
let is_fwd = is_forward_bracket(c);
let line = doc.byte_to_line(pos);
let end = doc.line_to_byte(if is_fwd { line + 1 } else { line });
let range = if is_fwd {
Range::Forward((pos + 1)..end)
} else {
Range::Backward((end..pos).rev())
};

let mut open_cnt = 1;

for pos in range {
c = doc.char(pos);

if !is_valid_bracket(c) {
continue;
} else if bracket == c {
open_cnt += 1;
} else if is_valid_pair(doc, bracket_pos, pos) || is_valid_pair(doc, pos, bracket_pos) {
open_cnt -= 1;
}

if open_cnt == 0 {
return Some(pos);
}
}

None
}

fn is_valid_bracket(c: char) -> bool {
PAIRS.iter().any(|(l, r)| *l == c || *r == c)
}

fn is_forward_bracket(c: char) -> bool {
PAIRS.iter().any(|(l, _)| *l == c)
}

fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool {
PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
}
Expand All @@ -90,3 +143,46 @@ fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {

Some((start_byte, end_byte))
}

enum Range {
Forward(std::ops::Range<usize>),
Backward(std::iter::Rev<std::ops::Range<usize>>),
}

impl Iterator for Range {
type Item = usize;
fn next(&mut self) -> Option<usize> {
match self {
Range::Forward(range) => range.next(),
Range::Backward(range) => range.next(),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_find_matching_bracket_current_line_plaintext() {
let assert = |input: &str, pos, expected| {
let input = &Rope::from(input);
let actual = find_matching_bracket_current_line_plaintext(input, pos);
assert_eq!(expected, actual.unwrap());

let reverse = find_matching_bracket_current_line_plaintext(input, actual.unwrap());
assert_eq!(pos, reverse.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("(paren (paren {bracket}))", 0, 24);
assert("(paren (paren {bracket}))", 7, 23);
assert("(paren (paren {bracket}))", 14, 22);
}
}
9 changes: 8 additions & 1 deletion helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4537,7 +4537,14 @@ fn match_brackets(cx: &mut Context) {
{
range.put_cursor(text, pos, cx.editor.mode == Mode::Select)
} else {
range
if let Some(pos) = match_brackets::find_matching_bracket_current_line_plaintext(
doc.text(),
range.cursor(text),
) {
range.put_cursor(text, pos, cx.editor.mode == Mode::Select)
} else {
range
}
}
});
doc.set_selection(view.id, selection);
Expand Down

0 comments on commit 2bb2562

Please sign in to comment.