@@ -4,7 +4,8 @@ use ropey::RopeSlice;
4use tree_sitter::{Node, QueryCursor};
4use tree_sitter::{Node, QueryCursor};
6use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
6use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
7−use crate::graphemes::next_grapheme_boundary;
7+use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
8+use crate::line_ending::rope_is_line_ending;
8use crate::movement::Direction;
9use crate::movement::Direction;
9use crate::surround;
10use crate::surround;
10use crate::syntax::LanguageConfiguration;
11use crate::syntax::LanguageConfiguration;
@@ -111,6 +112,71 @@ pub fn textobject_word(
115+pub fn textobject_para(
118+ textobject: TextObject,
121+ let mut line = range.cursor_line(slice);
122+ let prev_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
123+ let curr_line_empty = rope_is_line_ending(slice.line(line));
124+ let next_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
126+ prev_grapheme_boundary(slice, slice.line_to_char(line + 1)) == range.cursor(slice);
127+ let prev_empty_to_line = prev_line_empty && !curr_line_empty;
128+ let curr_empty_to_line = curr_line_empty && !next_line_empty;
130+ // skip character before paragraph boundary
131+ let mut line_back = line; // line but backwards
132+ if prev_empty_to_line || curr_empty_to_line {
135+ let mut lines = slice.lines_at(line_back);
136+ // do not include current paragraph on paragraph end (include next)
137+ if !(curr_empty_to_line && last_char) {
139+ let mut lines = lines.map(rope_is_line_ending).peekable();
140+ while lines.next_if(|&e| e).is_some() {
143+ while lines.next_if(|&e| !e).is_some() {
148+ // skip character after paragraph boundary
149+ if curr_empty_to_line && last_char {
152+ let mut lines = slice.lines_at(line).map(rope_is_line_ending).peekable();
153+ for _ in 0..count - 1 {
154+ while lines.next_if(|&e| !e).is_some() {
157+ while lines.next_if(|&e| e).is_some() {
161+ while lines.next_if(|&e| !e).is_some() {
164+ // handle last whitespaces part separately depending on textobject
166+ TextObject::Around => {
167+ while lines.next_if(|&e| e).is_some() {
171+ TextObject::Inside => {}
172+ TextObject::Movement => unreachable!(),
175+ let anchor = slice.line_to_char(line_back);
176+ let head = slice.line_to_char(line);
177+ Range::new(anchor, head)
114pub fn textobject_surround(
180pub fn textobject_surround(
115 slice: RopeSlice,
181 slice: RopeSlice,
116 range: Range,
182 range: Range,
@@ -288,6 +354,85 @@ mod test {
358+ fn test_textobject_paragraph_inside_single() {
361+ ("firs^t@\n\nparagraph\n\n", "^first\n@\nparagraph\n\n"),
362+ ("second\n\npa^r@agraph\n\n", "second\n\n^paragraph\n@\n"),
363+ ("^f@irst char\n\n", "^first char\n@\n"),
364+ ("last char\n^\n@", "last char\n\n^@"),
366+ "empty to line\n^\n@paragraph boundary\n\n",
367+ "empty to line\n\n^paragraph boundary\n@\n",
370+ "line to empty\n\n^p@aragraph boundary\n\n",
371+ "line to empty\n\n^paragraph boundary\n@\n",
375+ for (before, expected) in tests {
376+ let (s, selection) = crate::test::print(before);
377+ let text = Rope::from(s.as_str());
379+ selection.transform(|r| textobject_para(text.slice(..), r, TextObject::Inside, 1));
380+ let actual = crate::test::plain(&s, selection);
381+ assert_eq!(actual, expected, "\nbefore: `{before:?}`");
386+ fn test_textobject_paragraph_inside_double() {
389+ "last two\n\n^p@aragraph\n\nwithout whitespaces\n\n",
390+ "last two\n\n^paragraph\n\nwithout whitespaces\n@\n",
393+ "last two\n^\n@paragraph\n\nwithout whitespaces\n\n",
394+ "last two\n\n^paragraph\n\nwithout whitespaces\n@\n",
398+ for (before, expected) in tests {
399+ let (s, selection) = crate::test::print(before);
400+ let text = Rope::from(s.as_str());
402+ selection.transform(|r| textobject_para(text.slice(..), r, TextObject::Inside, 2));
403+ let actual = crate::test::plain(&s, selection);
404+ assert_eq!(actual, expected, "\nbefore: `{before:?}`");
409+ fn test_textobject_paragraph_around_single() {
412+ ("firs^t@\n\nparagraph\n\n", "^first\n\n@paragraph\n\n"),
413+ ("second\n\npa^r@agraph\n\n", "second\n\n^paragraph\n\n@"),
414+ ("^f@irst char\n\n", "^first char\n\n@"),
415+ ("last char\n^\n@", "last char\n\n^@"),
417+ "empty to line\n^\n@paragraph boundary\n\n",
418+ "empty to line\n\n^paragraph boundary\n\n@",
421+ "line to empty\n\n^p@aragraph boundary\n\n",
422+ "line to empty\n\n^paragraph boundary\n\n@",
426+ for (before, expected) in tests {
427+ let (s, selection) = crate::test::print(before);
428+ let text = Rope::from(s.as_str());
430+ selection.transform(|r| textobject_para(text.slice(..), r, TextObject::Around, 1));
431+ let actual = crate::test::plain(&s, selection);
432+ assert_eq!(actual, expected, "\nbefore: `{before:?}`");
292 fn test_textobject_surround() {
437 fn test_textobject_surround() {
293 // (text, [(cursor position, textobject, final range, surround char, count), ...])
438 // (text, [(cursor position, textobject, final range, surround char, count), ...])