Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/textobject.rs')
| -rw-r--r-- | helix-core/src/textobject.rs | 262 |
1 files changed, 22 insertions, 240 deletions
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index 008228f4..975ed115 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -1,16 +1,16 @@ use std::fmt::Display; use ropey::RopeSlice; +use tree_sitter::{Node, QueryCursor}; use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; -use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary}; -use crate::line_ending::rope_is_line_ending; +use crate::graphemes::next_grapheme_boundary; use crate::movement::Direction; -use crate::syntax; +use crate::surround; +use crate::syntax::LanguageConfiguration; use crate::Range; -use crate::{surround, Syntax}; -fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize { +fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize { use CharCategory::{Eol, Whitespace}; let iter = match direction { @@ -33,7 +33,7 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, lo match categorize_char(ch) { Eol | Whitespace => return pos, category => { - if !long && category != prev_category && pos != 0 && pos != slice.len_chars() { + if category != prev_category && pos != 0 && pos != slice.len_chars() { return pos; } else { match direction { @@ -53,8 +53,6 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, lo pub enum TextObject { Around, Inside, - /// Used for moving between objects. - Movement, } impl Display for TextObject { @@ -62,7 +60,6 @@ impl Display for TextObject { f.write_str(match self { Self::Around => "around", Self::Inside => "inside", - Self::Movement => "movement", }) } } @@ -73,14 +70,13 @@ pub fn textobject_word( range: Range, textobject: TextObject, _count: usize, - long: bool, ) -> Range { let pos = range.cursor(slice); - let word_start = find_word_boundary(slice, pos, Direction::Backward, long); + let word_start = find_word_boundary(slice, pos, Direction::Backward); let word_end = match slice.get_char(pos).map(categorize_char) { None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos, - _ => find_word_boundary(slice, pos + 1, Direction::Forward, long), + _ => find_word_boundary(slice, pos + 1, Direction::Forward), }; // Special case. @@ -107,146 +103,20 @@ pub fn textobject_word( Range::new(word_start - whitespace_count_left, word_end) } } - TextObject::Movement => unreachable!(), } } -pub fn textobject_paragraph( - slice: RopeSlice, - range: Range, - textobject: TextObject, - count: usize, -) -> Range { - let mut line = range.cursor_line(slice); - let prev_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1))); - let curr_line_empty = rope_is_line_ending(slice.line(line)); - let next_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1))); - let last_char = - prev_grapheme_boundary(slice, slice.line_to_char(line + 1)) == range.cursor(slice); - let prev_empty_to_line = prev_line_empty && !curr_line_empty; - let curr_empty_to_line = curr_line_empty && !next_line_empty; - - // skip character before paragraph boundary - let mut line_back = line; // line but backwards - if prev_empty_to_line || curr_empty_to_line { - line_back += 1; - } - // do not include current paragraph on paragraph end (include next) - if !(curr_empty_to_line && last_char) { - let mut lines = slice.lines_at(line_back); - lines.reverse(); - let mut lines = lines.map(rope_is_line_ending).peekable(); - while lines.next_if(|&e| e).is_some() { - line_back -= 1; - } - while lines.next_if(|&e| !e).is_some() { - line_back -= 1; - } - } - - // skip character after paragraph boundary - if curr_empty_to_line && last_char { - line += 1; - } - let mut lines = slice.lines_at(line).map(rope_is_line_ending).peekable(); - let mut count_done = 0; // count how many non-whitespace paragraphs done - for _ in 0..count { - let mut done = false; - while lines.next_if(|&e| !e).is_some() { - line += 1; - done = true; - } - while lines.next_if(|&e| e).is_some() { - line += 1; - } - count_done += done as usize; - } - - // search one paragraph backwards for last paragraph - // makes `map` at the end of the paragraph with trailing newlines useful - let last_paragraph = count_done != count && lines.peek().is_none(); - if last_paragraph { - let mut lines = slice.lines_at(line_back); - lines.reverse(); - let mut lines = lines.map(rope_is_line_ending).peekable(); - while lines.next_if(|&e| e).is_some() { - line_back -= 1; - } - while lines.next_if(|&e| !e).is_some() { - line_back -= 1; - } - } - - // handle last whitespaces part separately depending on textobject - match textobject { - TextObject::Around => {} - TextObject::Inside => { - // remove last whitespace paragraph - let mut lines = slice.lines_at(line); - lines.reverse(); - let mut lines = lines.map(rope_is_line_ending).peekable(); - while lines.next_if(|&e| e).is_some() { - line -= 1; - } - } - TextObject::Movement => unreachable!(), - } - - let anchor = slice.line_to_char(line_back); - let head = slice.line_to_char(line); - Range::new(anchor, head) -} - -pub fn textobject_pair_surround( - syntax: Option<&Syntax>, +pub fn textobject_surround( slice: RopeSlice, range: Range, textobject: TextObject, ch: char, count: usize, ) -> Range { - textobject_pair_surround_impl(syntax, slice, range, textobject, Some(ch), count) -} - -pub fn textobject_pair_surround_closest( - syntax: Option<&Syntax>, - slice: RopeSlice, - range: Range, - textobject: TextObject, - count: usize, -) -> Range { - textobject_pair_surround_impl(syntax, slice, range, textobject, None, count) -} - -fn textobject_pair_surround_impl( - syntax: Option<&Syntax>, - slice: RopeSlice, - range: Range, - textobject: TextObject, - ch: Option<char>, - count: usize, -) -> Range { - let pair_pos = match ch { - Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count), - None => surround::find_nth_closest_pairs_pos(syntax, slice, range, count), - }; - pair_pos + surround::find_nth_pairs_pos(slice, ch, range.head, count) .map(|(anchor, head)| match textobject { - TextObject::Inside => { - if anchor < head { - Range::new(next_grapheme_boundary(slice, anchor), head) - } else { - Range::new(anchor, next_grapheme_boundary(slice, head)) - } - } - TextObject::Around => { - if anchor < head { - Range::new(anchor, next_grapheme_boundary(slice, head)) - } else { - Range::new(next_grapheme_boundary(slice, anchor), head) - } - } - TextObject::Movement => unreachable!(), + TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head), + TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)), }) .unwrap_or(range) } @@ -259,18 +129,18 @@ pub fn textobject_treesitter( range: Range, textobject: TextObject, object_name: &str, - syntax: &Syntax, - loader: &syntax::Loader, + slice_tree: Node, + lang_config: &LanguageConfiguration, _count: usize, ) -> Range { - let root = syntax.tree().root_node(); - let textobject_query = loader.textobject_query(syntax.root_language()); let get_range = move || -> Option<Range> { let byte_pos = slice.char_to_byte(range.cursor(slice)); let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner - let node = textobject_query? - .capture_nodes(&capture_name, &root, slice)? + let mut cursor = QueryCursor::new(); + let node = lang_config + .textobject_query()? + .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)? .filter(|node| node.byte_range().contains(&byte_pos)) .min_by_key(|node| node.byte_range().len())?; @@ -299,7 +169,7 @@ mod test { #[test] fn test_textobject_word() { - // (text, [(char position, textobject, final range), ...]) + // (text, [(cursor position, textobject, final range), ...]) let tests = &[ ( "cursor at beginning of doc", @@ -398,9 +268,7 @@ mod test { let slice = doc.slice(..); for &case in scenario { let (pos, objtype, expected_range) = case; - // cursor is a single width selection - let range = Range::new(pos, pos + 1); - let result = textobject_word(slice, range, objtype, 1, false); + let result = textobject_word(slice, Range::point(pos), objtype, 1); assert_eq!( result, expected_range.into(), @@ -413,93 +281,8 @@ mod test { } #[test] - fn test_textobject_paragraph_inside_single() { - let tests = [ - ("#[|]#", "#[|]#"), - ("firs#[t|]#\n\nparagraph\n\n", "#[first\n|]#\nparagraph\n\n"), - ( - "second\n\npa#[r|]#agraph\n\n", - "second\n\n#[paragraph\n|]#\n", - ), - ("#[f|]#irst char\n\n", "#[first char\n|]#\n"), - ("last char\n#[\n|]#", "#[last char\n|]#\n"), - ( - "empty to line\n#[\n|]#paragraph boundary\n\n", - "empty to line\n\n#[paragraph boundary\n|]#\n", - ), - ( - "line to empty\n\n#[p|]#aragraph boundary\n\n", - "line to empty\n\n#[paragraph boundary\n|]#\n", - ), - ]; - - for (before, expected) in tests { - let (s, selection) = crate::test::print(before); - let text = Rope::from(s.as_str()); - let selection = selection - .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 1)); - let actual = crate::test::plain(s.as_ref(), &selection); - assert_eq!(actual, expected, "\nbefore: `{:?}`", before); - } - } - - #[test] - fn test_textobject_paragraph_inside_double() { - let tests = [ - ( - "last two\n\n#[p|]#aragraph\n\nwithout whitespaces\n\n", - "last two\n\n#[paragraph\n\nwithout whitespaces\n|]#\n", - ), - ( - "last two\n#[\n|]#paragraph\n\nwithout whitespaces\n\n", - "last two\n\n#[paragraph\n\nwithout whitespaces\n|]#\n", - ), - ]; - - for (before, expected) in tests { - let (s, selection) = crate::test::print(before); - let text = Rope::from(s.as_str()); - let selection = selection - .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 2)); - let actual = crate::test::plain(s.as_ref(), &selection); - assert_eq!(actual, expected, "\nbefore: `{:?}`", before); - } - } - - #[test] - fn test_textobject_paragraph_around_single() { - let tests = [ - ("#[|]#", "#[|]#"), - ("firs#[t|]#\n\nparagraph\n\n", "#[first\n\n|]#paragraph\n\n"), - ( - "second\n\npa#[r|]#agraph\n\n", - "second\n\n#[paragraph\n\n|]#", - ), - ("#[f|]#irst char\n\n", "#[first char\n\n|]#"), - ("last char\n#[\n|]#", "#[last char\n\n|]#"), - ( - "empty to line\n#[\n|]#paragraph boundary\n\n", - "empty to line\n\n#[paragraph boundary\n\n|]#", - ), - ( - "line to empty\n\n#[p|]#aragraph boundary\n\n", - "line to empty\n\n#[paragraph boundary\n\n|]#", - ), - ]; - - for (before, expected) in tests { - let (s, selection) = crate::test::print(before); - let text = Rope::from(s.as_str()); - let selection = selection - .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Around, 1)); - let actual = crate::test::plain(s.as_ref(), &selection); - assert_eq!(actual, expected, "\nbefore: `{:?}`", before); - } - } - - #[test] fn test_textobject_surround() { - // (text, [(cursor position, textobject, final range, surround char, count), ...]) + // (text, [(cursor position, textobject, final range, count), ...]) let tests = &[ ( "simple (single) surround pairs", @@ -575,8 +358,7 @@ mod test { let slice = doc.slice(..); for &case in scenario { let (pos, objtype, expected_range, ch, count) = case; - let result = - textobject_pair_surround(None, slice, Range::point(pos), objtype, ch, count); + let result = textobject_surround(slice, Range::point(pos), objtype, ch, count); assert_eq!( result, expected_range.into(), |