diff options
Diffstat (limited to 'helix-core/src/textobject.rs')
-rw-r--r-- | helix-core/src/textobject.rs | 147 |
1 files changed, 146 insertions, 1 deletions
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index 5a55a6f1..fb6b7142 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -4,7 +4,8 @@ use ropey::RopeSlice; use tree_sitter::{Node, QueryCursor}; use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; -use crate::graphemes::next_grapheme_boundary; +use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary}; +use crate::line_ending::rope_is_line_ending; use crate::movement::Direction; use crate::surround; use crate::syntax::LanguageConfiguration; @@ -111,6 +112,71 @@ pub fn textobject_word( } } +pub fn textobject_para( + 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; + } + let mut lines = slice.lines_at(line_back); + // do not include current paragraph on paragraph end (include next) + if !(curr_empty_to_line && last_char) { + 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(); + for _ in 0..count - 1 { + while lines.next_if(|&e| !e).is_some() { + line += 1; + } + while lines.next_if(|&e| e).is_some() { + line += 1; + } + } + while lines.next_if(|&e| !e).is_some() { + line += 1; + } + // handle last whitespaces part separately depending on textobject + match textobject { + TextObject::Around => { + while lines.next_if(|&e| e).is_some() { + line += 1; + } + } + TextObject::Inside => {} + 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_surround( slice: RopeSlice, range: Range, @@ -289,6 +355,85 @@ 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_para(text.slice(..), r, TextObject::Inside, 1)); + let actual = crate::test::plain(&s, 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_para(text.slice(..), r, TextObject::Inside, 2)); + let actual = crate::test::plain(&s, 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_para(text.slice(..), r, TextObject::Around, 1)); + let actual = crate::test::plain(&s, selection); + assert_eq!(actual, expected, "\nbefore: `{before:?}`"); + } + } + + #[test] fn test_textobject_surround() { // (text, [(cursor position, textobject, final range, surround char, count), ...]) let tests = &[ |