diff options
Diffstat (limited to 'helix-core')
-rw-r--r-- | helix-core/src/diagnostic.rs | 4 | ||||
-rw-r--r-- | helix-core/src/match_brackets.rs | 95 | ||||
-rw-r--r-- | helix-core/src/register.rs | 14 | ||||
-rw-r--r-- | helix-core/src/selection.rs | 6 | ||||
-rw-r--r-- | helix-core/src/surround.rs | 148 | ||||
-rw-r--r-- | helix-core/src/textobject.rs | 10 |
6 files changed, 171 insertions, 106 deletions
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index ad1ba16a..4fcf51c9 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -1,7 +1,7 @@ //! LSP diagnostic utility types. /// Describes the severity level of a [`Diagnostic`]. -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum Severity { Error, Warning, @@ -17,7 +17,7 @@ pub struct Range { } /// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html) -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Diagnostic { pub range: Range, pub line: usize, diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index 136ce320..cd554005 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -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_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(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> { +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 byte_pos = doc.char_to_byte(pos); + let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?; - // most naive implementation: find the innermost syntax node, if we're at the edge of a node, - // return the other edge. + 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)); - let node = match tree - .root_node() - .named_descendant_for_byte_range(byte_pos, byte_pos) - { - Some(node) => node, - None => return None, - }; + 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 node.is_error() { - return None; + 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)) } diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs index c5444eb7..b9eb497d 100644 --- a/helix-core/src/register.rs +++ b/helix-core/src/register.rs @@ -15,7 +15,11 @@ impl Register { } pub fn new_with_values(name: char, values: Vec<String>) -> Self { - Self { name, values } + if name == '_' { + Self::new(name) + } else { + Self { name, values } + } } pub const fn name(&self) -> char { @@ -27,11 +31,15 @@ impl Register { } pub fn write(&mut self, values: Vec<String>) { - self.values = values; + if self.name != '_' { + self.values = values; + } } pub fn push(&mut self, value: String) { - self.values.push(value); + if self.name != '_' { + self.values.push(value); + } } } diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index b4d1dffa..116a1c7c 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -308,10 +308,10 @@ impl Range { } impl From<(usize, usize)> for Range { - fn from(tuple: (usize, usize)) -> Self { + fn from((anchor, head): (usize, usize)) -> Self { Self { - anchor: tuple.0, - head: tuple.1, + anchor, + head, horiz: None, } } diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index 32161b70..b53b0a78 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -1,4 +1,4 @@ -use crate::{search, Selection}; +use crate::{search, Range, Selection}; use ropey::RopeSlice; pub const PAIRS: &[(char, char)] = &[ @@ -35,33 +35,27 @@ pub fn get_pair(ch: char) -> (char, char) { pub fn find_nth_pairs_pos( text: RopeSlice, ch: char, - pos: usize, + range: Range, n: usize, ) -> Option<(usize, usize)> { - let (open, close) = get_pair(ch); - - if text.len_chars() < 2 || pos >= text.len_chars() { + if text.len_chars() < 2 || range.to() >= text.len_chars() { return None; } + let (open, close) = get_pair(ch); + let pos = range.cursor(text); + if open == close { if Some(open) == text.get_char(pos) { - // Special case: cursor is directly on a matching char. - match pos { - 0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)), - _ if (pos + 1) == text.len_chars() => { - Some((search::find_nth_prev(text, open, pos, n)?, pos)) - } - // We return no match because there's no way to know which - // side of the char we should be searching on. - _ => None, - } - } else { - Some(( - search::find_nth_prev(text, open, pos, n)?, - search::find_nth_next(text, close, pos, n)?, - )) + // Cursor is directly on match char. We return no match + // because there's no way to know which side of the char + // we should be searching on. + return None; } + Some(( + search::find_nth_prev(text, open, pos, n)?, + search::find_nth_next(text, close, pos, n)?, + )) } else { Some(( find_nth_open_pair(text, open, close, pos, n)?, @@ -160,8 +154,8 @@ pub fn get_surround_pos( ) -> Option<Vec<usize>> { let mut change_pos = Vec::new(); - for range in selection { - let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?; + for &range in selection { + let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range, skip)?; if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) { return None; } @@ -178,67 +172,91 @@ mod test { use ropey::Rope; use smallvec::SmallVec; - #[test] - fn test_find_nth_pairs_pos() { - let doc = Rope::from("some (text) here"); + fn check_find_nth_pair_pos( + text: &str, + cases: Vec<(usize, char, usize, Option<(usize, usize)>)>, + ) { + let doc = Rope::from(text); let slice = doc.slice(..); - // cursor on [t]ext - assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10))); - assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10))); - // cursor on so[m]e - assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None); - // cursor on bracket itself - assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10))); - assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10))); + for (cursor_pos, ch, n, expected_range) in cases { + let range = find_nth_pairs_pos(slice, ch, (cursor_pos, cursor_pos + 1).into(), n); + assert_eq!( + range, expected_range, + "Expected {:?}, got {:?}", + expected_range, range + ); + } } #[test] - fn test_find_nth_pairs_pos_skip() { - let doc = Rope::from("(so (many (good) text) here)"); - let slice = doc.slice(..); + fn test_find_nth_pairs_pos() { + check_find_nth_pair_pos( + "some (text) here", + vec![ + // cursor on [t]ext + (6, '(', 1, Some((5, 10))), + (6, ')', 1, Some((5, 10))), + // cursor on so[m]e + (2, '(', 1, None), + // cursor on bracket itself + (5, '(', 1, Some((5, 10))), + (10, '(', 1, Some((5, 10))), + ], + ); + } - // cursor on go[o]d - assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15))); - assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21))); - assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27))); + #[test] + fn test_find_nth_pairs_pos_skip() { + check_find_nth_pair_pos( + "(so (many (good) text) here)", + vec![ + // cursor on go[o]d + (13, '(', 1, Some((10, 15))), + (13, '(', 2, Some((4, 21))), + (13, '(', 3, Some((0, 27))), + ], + ); } #[test] fn test_find_nth_pairs_pos_same() { - let doc = Rope::from("'so 'many 'good' text' here'"); - let slice = doc.slice(..); - - // cursor on go[o]d - assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15))); - assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21))); - assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27))); - // cursor on the quotes - assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None); - // this is the best we can do since opening and closing pairs are same - assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4))); - assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27))); + check_find_nth_pair_pos( + "'so 'many 'good' text' here'", + vec![ + // cursor on go[o]d + (13, '\'', 1, Some((10, 15))), + (13, '\'', 2, Some((4, 21))), + (13, '\'', 3, Some((0, 27))), + // cursor on the quotes + (10, '\'', 1, None), + ], + ) } #[test] fn test_find_nth_pairs_pos_step() { - let doc = Rope::from("((so)((many) good (text))(here))"); - let slice = doc.slice(..); - - // cursor on go[o]d - assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24))); - assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31))); + check_find_nth_pair_pos( + "((so)((many) good (text))(here))", + vec![ + // cursor on go[o]d + (15, '(', 1, Some((5, 24))), + (15, '(', 2, Some((0, 31))), + ], + ) } #[test] fn test_find_nth_pairs_pos_mixed() { - let doc = Rope::from("(so [many {good} text] here)"); - let slice = doc.slice(..); - - // cursor on go[o]d - assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15))); - assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21))); - assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27))); + check_find_nth_pair_pos( + "(so [many {good} text] here)", + vec![ + // cursor on go[o]d + (13, '{', 1, Some((10, 15))), + (13, '[', 1, Some((4, 21))), + (13, '(', 1, Some((0, 27))), + ], + ) } #[test] diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index 24f063d4..21ceec04 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -114,7 +114,7 @@ pub fn textobject_surround( ch: char, count: usize, ) -> Range { - surround::find_nth_pairs_pos(slice, ch, range.head, count) + surround::find_nth_pairs_pos(slice, ch, range, count) .map(|(anchor, head)| match textobject { TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head), TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)), @@ -170,7 +170,7 @@ mod test { #[test] fn test_textobject_word() { - // (text, [(cursor position, textobject, final range), ...]) + // (text, [(char position, textobject, final range), ...]) let tests = &[ ( "cursor at beginning of doc", @@ -269,7 +269,9 @@ mod test { let slice = doc.slice(..); for &case in scenario { let (pos, objtype, expected_range) = case; - let result = textobject_word(slice, Range::point(pos), objtype, 1, false); + // cursor is a single width selection + let range = Range::new(pos, pos + 1); + let result = textobject_word(slice, range, objtype, 1, false); assert_eq!( result, expected_range.into(), @@ -283,7 +285,7 @@ mod test { #[test] fn test_textobject_surround() { - // (text, [(cursor position, textobject, final range, count), ...]) + // (text, [(cursor position, textobject, final range, surround char, count), ...]) let tests = &[ ( "simple (single) surround pairs", |