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
This commit is contained in:
parent
b95c9470de
commit
a3a3b0b517
3 changed files with 74 additions and 35 deletions
|
@ -1,3 +1,5 @@
|
|||
use tree_sitter::Node;
|
||||
|
||||
use crate::{Rope, Syntax};
|
||||
|
||||
const PAIRS: &[(char, char)] = &[
|
||||
|
@ -6,50 +8,85 @@ const PAIRS: &[(char, char)] = &[
|
|||
('[', ']'),
|
||||
('<', '>'),
|
||||
('\'', '\''),
|
||||
('"', '"'),
|
||||
('\"', '\"'),
|
||||
];
|
||||
|
||||
// limit matching pairs to only ( ) { } [ ] < >
|
||||
|
||||
// Returns the position of the matching bracket under cursor.
|
||||
//
|
||||
// If the cursor is one the opening bracket, the position of
|
||||
// the closing bracket is returned. If the cursor in the closing
|
||||
// bracket, the position of the opening bracket is returned.
|
||||
//
|
||||
// If the cursor is not on a bracket, `None` is returned.
|
||||
#[must_use]
|
||||
pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
|
||||
let tree = syntax.tree();
|
||||
|
||||
let byte_pos = doc.char_to_byte(pos);
|
||||
|
||||
// most naive implementation: find the innermost syntax node, if we're at the edge of a node,
|
||||
// return the other edge.
|
||||
|
||||
let node = match tree
|
||||
.root_node()
|
||||
.named_descendant_for_byte_range(byte_pos, byte_pos)
|
||||
{
|
||||
Some(node) => node,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
if node.is_error() {
|
||||
pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
|
||||
if pos >= doc.len_chars() || !is_valid_bracket(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: &Rope, pos: usize) -> Option<usize> {
|
||||
find_pair(syntax, doc, pos, true)
|
||||
}
|
||||
|
||||
fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) -> Option<usize> {
|
||||
let tree = syntax.tree();
|
||||
let pos = doc.char_to_byte(pos);
|
||||
|
||||
let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
|
||||
|
||||
loop {
|
||||
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.
|
||||
return Some(end_char);
|
||||
}
|
||||
|
||||
if traverse_parents {
|
||||
node = node.parent()?;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_bracket(c: char) -> bool {
|
||||
PAIRS.iter().any(|(l, r)| *l == c || *r == c)
|
||||
}
|
||||
|
||||
fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool {
|
||||
PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
|
||||
}
|
||||
|
||||
fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
|
||||
let len = doc.len_bytes();
|
||||
|
||||
let start_byte = node.start_byte();
|
||||
let end_byte = node.end_byte().saturating_sub(1); // it's end exclusive
|
||||
let end_byte = node.end_byte().saturating_sub(1);
|
||||
|
||||
if start_byte >= len || end_byte >= len {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start_char = doc.byte_to_char(start_byte);
|
||||
let end_char = doc.byte_to_char(end_byte);
|
||||
|
||||
if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
|
||||
if start_byte == byte_pos {
|
||||
return Some(end_char);
|
||||
}
|
||||
|
||||
if end_byte == byte_pos {
|
||||
return Some(start_char);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
Some((start_byte, end_byte))
|
||||
}
|
||||
|
|
|
@ -4954,7 +4954,9 @@ fn match_brackets(cx: &mut Context) {
|
|||
if let Some(syntax) = doc.syntax() {
|
||||
let text = doc.text().slice(..);
|
||||
let selection = doc.selection(view.id).clone().transform(|range| {
|
||||
if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
|
||||
if let Some(pos) =
|
||||
match_brackets::find_matching_bracket_fuzzy(syntax, doc.text(), range.anchor)
|
||||
{
|
||||
range.put_cursor(text, pos, doc.mode == Mode::Select)
|
||||
} else {
|
||||
range
|
||||
|
|
|
@ -377,7 +377,7 @@ impl EditorView {
|
|||
use helix_core::match_brackets;
|
||||
let pos = doc.selection(view.id).primary().cursor(text);
|
||||
|
||||
let pos = match_brackets::find(syntax, doc.text(), pos)
|
||||
let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
|
||||
.and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
|
||||
|
||||
if let Some(pos) = pos {
|
||||
|
|
Loading…
Add table
Reference in a new issue