on master help sign in
Add paragraph textobject

Change parameter/argument key from p to a since paragraph only have p but parameter are also called arguments sometimes and a is not used.

commit8350ee9a0ef38ff4e78bfa5dc30e874d6d2510ef authorIvan Tham <pickfire@riseup.net> dateMon 21 Feb 2022, 22:58 (5 years ago) parente2a6e33
5 files changed+162−14
book/src/keymap.md+2 helix-core/src/movement.rs+11−12 helix-core/src/test.rs+2−1 helix-core/src/textobject.rs+146−1 helix-term/src/commands.rs+1
book/src/keymap.md+2
@@ -277,6 +277,8 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire
277277| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
278278| `]o` | Go to next comment (**TS**) | `goto_next_comment` |
279279| `[o` | Go to previous comment (**TS**) | `goto_prev_comment` |
280+| `]p` | Go to next paragraph | `goto_next_paragraph` |
281+| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
280282| `[space` | Add newline above | `add_newline_above` |
281283| `]space` | Add newline below | `add_newline_below` |
282284
helix-core/src/movement.rs+11−12
@@ -153,12 +153,12 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar
153153pub fn move_prev_para(slice: RopeSlice, range: Range, count: usize, behavior: Movement) -> Range {
154154 let mut line = range.cursor_line(slice);
155155 let first_char = slice.line_to_char(line) == range.cursor(slice);
156+ let prev_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
156157 let curr_line_empty = rope_is_line_ending(slice.line(line));
157− let last_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
158− let line_to_empty = last_line_empty && !curr_line_empty;
158+ let prev_empty_to_line = prev_line_empty && !curr_line_empty;
159159
160− // iterate current line if first character after paragraph boundary
161− if line_to_empty && !first_char {
160+ // skip character before paragraph boundary
161+ if prev_empty_to_line && !first_char {
162162 line += 1;
163163 }
164164 let mut lines = slice.lines_at(line);
@@ -176,7 +176,7 @@ pub fn move_prev_para(slice: RopeSlice, range: Range, count: usize, behavior: Mo
176176 let head = slice.line_to_char(line);
177177 let anchor = if behavior == Movement::Move {
178178 // exclude first character after paragraph boundary
179− if line_to_empty && first_char {
179+ if prev_empty_to_line && first_char {
180180 range.cursor(slice)
181181 } else {
182182 range.head
@@ -193,13 +193,12 @@ pub fn move_next_para(slice: RopeSlice, range: Range, count: usize, behavior: Mo
193193 prev_grapheme_boundary(slice, slice.line_to_char(line + 1)) == range.cursor(slice);
194194 let curr_line_empty = rope_is_line_ending(slice.line(line));
195195 let next_line_empty = rope_is_line_ending(slice.line(line.saturating_sub(1)));
196− let empty_to_line = curr_line_empty && !next_line_empty;
196+ let curr_empty_to_line = curr_line_empty && !next_line_empty;
197197
198− // iterate current line if first character after paragraph boundary
199− if empty_to_line && last_char {
198+ // skip character after paragraph boundary
199+ if curr_empty_to_line && last_char {
200200 line += 1;
201201 }
202−
203202 let mut lines = slice.lines_at(line).map(rope_is_line_ending).peekable();
204203 for _ in 0..count {
205204 while lines.next_if(|&e| !e).is_some() {
@@ -211,7 +210,7 @@ pub fn move_next_para(slice: RopeSlice, range: Range, count: usize, behavior: Mo
211210 }
212211 let head = slice.line_to_char(line);
213212 let anchor = if behavior == Movement::Move {
214− if empty_to_line && last_char {
213+ if curr_empty_to_line && last_char {
215214 range.head
216215 } else {
217216 range.cursor(slice)
@@ -1256,7 +1255,7 @@ mod test {
12561255 #[test]
12571256 fn test_behaviour_when_moving_to_prev_paragraph_single() {
12581257 let tests = [
1259− ("^@", "@^"),
1258+ ("^@", "^@"),
12601259 ("^s@tart at\nfirst char\n", "@s^tart at\nfirst char\n"),
12611260 ("start at\nlast char^\n@", "@start at\nlast char\n^"),
12621261 ("goto\nfirst\n\n^p@aragraph", "@goto\nfirst\n\n^paragraph"),
@@ -1315,7 +1314,7 @@ mod test {
13151314 #[test]
13161315 fn test_behaviour_when_moving_to_next_paragraph_single() {
13171316 let tests = [
1318− ("^@", "@^"),
1317+ ("^@", "^@"),
13191318 ("^s@tart at\nfirst char\n", "^start at\nfirst char\n@"),
13201319 ("start at\nlast char^\n@", "start at\nlast char^\n@"),
13211320 (
helix-core/src/test.rs+2−1
@@ -97,8 +97,9 @@ pub fn plain(s: &str, selection: Selection) -> String {
9797 .enumerate()
9898 .flat_map(|(i, range)| {
9999 [
100− (range.anchor, '^'),
100+ // sort like this before reversed so anchor < head later
101101 (range.head, if i == primary { '@' } else { '|' }),
102+ (range.anchor, '^'),
102103 ]
103104 })
104105 .collect();
helix-core/src/textobject.rs+146−1
@@ -4,7 +4,8 @@ use ropey::RopeSlice;
44use tree_sitter::{Node, QueryCursor};
55
66use 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;
89use crate::movement::Direction;
910use crate::surround;
1011use crate::syntax::LanguageConfiguration;
@@ -111,6 +112,71 @@ pub fn textobject_word(
111112 }
112113}
113114
115+pub fn textobject_para(
116+ slice: RopeSlice,
117+ range: Range,
118+ textobject: TextObject,
119+ count: usize,
120+) -> Range {
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)));
125+ let last_char =
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;
129+
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 {
133+ line_back += 1;
134+ }
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) {
138+ lines.reverse();
139+ let mut lines = lines.map(rope_is_line_ending).peekable();
140+ while lines.next_if(|&e| e).is_some() {
141+ line_back -= 1;
142+ }
143+ while lines.next_if(|&e| !e).is_some() {
144+ line_back -= 1;
145+ }
146+ }
147+
148+ // skip character after paragraph boundary
149+ if curr_empty_to_line && last_char {
150+ line += 1;
151+ }
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() {
155+ line += 1;
156+ }
157+ while lines.next_if(|&e| e).is_some() {
158+ line += 1;
159+ }
160+ }
161+ while lines.next_if(|&e| !e).is_some() {
162+ line += 1;
163+ }
164+ // handle last whitespaces part separately depending on textobject
165+ match textobject {
166+ TextObject::Around => {
167+ while lines.next_if(|&e| e).is_some() {
168+ line += 1;
169+ }
170+ }
171+ TextObject::Inside => {}
172+ TextObject::Movement => unreachable!(),
173+ }
174+
175+ let anchor = slice.line_to_char(line_back);
176+ let head = slice.line_to_char(line);
177+ Range::new(anchor, head)
178+}
179+
114180pub fn textobject_surround(
115181 slice: RopeSlice,
116182 range: Range,
@@ -288,6 +354,85 @@ mod test {
288354 }
289355 }
290356
357+ #[test]
358+ fn test_textobject_paragraph_inside_single() {
359+ let tests = [
360+ ("^@", "^@"),
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^@"),
365+ (
366+ "empty to line\n^\n@paragraph boundary\n\n",
367+ "empty to line\n\n^paragraph boundary\n@\n",
368+ ),
369+ (
370+ "line to empty\n\n^p@aragraph boundary\n\n",
371+ "line to empty\n\n^paragraph boundary\n@\n",
372+ ),
373+ ];
374+
375+ for (before, expected) in tests {
376+ let (s, selection) = crate::test::print(before);
377+ let text = Rope::from(s.as_str());
378+ let selection =
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:?}`");
382+ }
383+ }
384+
385+ #[test]
386+ fn test_textobject_paragraph_inside_double() {
387+ let tests = [
388+ (
389+ "last two\n\n^p@aragraph\n\nwithout whitespaces\n\n",
390+ "last two\n\n^paragraph\n\nwithout whitespaces\n@\n",
391+ ),
392+ (
393+ "last two\n^\n@paragraph\n\nwithout whitespaces\n\n",
394+ "last two\n\n^paragraph\n\nwithout whitespaces\n@\n",
395+ ),
396+ ];
397+
398+ for (before, expected) in tests {
399+ let (s, selection) = crate::test::print(before);
400+ let text = Rope::from(s.as_str());
401+ let selection =
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:?}`");
405+ }
406+ }
407+
408+ #[test]
409+ fn test_textobject_paragraph_around_single() {
410+ let tests = [
411+ ("^@", "^@"),
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^@"),
416+ (
417+ "empty to line\n^\n@paragraph boundary\n\n",
418+ "empty to line\n\n^paragraph boundary\n\n@",
419+ ),
420+ (
421+ "line to empty\n\n^p@aragraph boundary\n\n",
422+ "line to empty\n\n^paragraph boundary\n\n@",
423+ ),
424+ ];
425+
426+ for (before, expected) in tests {
427+ let (s, selection) = crate::test::print(before);
428+ let text = Rope::from(s.as_str());
429+ let selection =
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:?}`");
433+ }
434+ }
435+
291436 #[test]
292437 fn test_textobject_surround() {
293438 // (text, [(cursor position, textobject, final range, surround char, count), ...])
helix-term/src/commands.rs+1
@@ -3991,6 +3991,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
39913991 'f' => textobject_treesitter("function", range),
39923992 'a' => textobject_treesitter("parameter", range),
39933993 'o' => textobject_treesitter("comment", range),
3994+ 'p' => textobject::textobject_para(text, range, objtype, count),
39943995 'm' => {
39953996 let ch = text.char(range.cursor(text));
39963997 if !ch.is_ascii_alphanumeric() {