@@ -10,6 +10,7 @@ use crate::{
1010 next_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary,
1111 prev_grapheme_boundary,
1212 },
13+ line_ending::{rope_is_line_ending, str_is_line_ending},
1314 pos_at_coords,
1415 syntax::LanguageConfiguration,
1516 textobject::TextObject,
@@ -149,6 +150,63 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar
149150 })
150151}
151152
153+pub fn move_prev_para(slice: RopeSlice, range: Range, count: usize, behavior: Movement) -> Range {
154+ let mut line = range.cursor_line(slice);
155+ let first_char = slice.line_to_char(line) == range.cursor(slice);
156+ 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;
159+
160+ // iterate current line if first character after paragraph boundary
161+ if line_to_empty && !first_char {
162+ line += 1;
163+ }
164+ let mut lines = slice.lines_at(line);
165+ lines.reverse();
166+ let mut lines = lines.map(rope_is_line_ending).peekable();
167+ for _ in 0..count {
168+ while lines.next_if(|&e| e).is_some() {
169+ line -= 1;
170+ }
171+ while lines.next_if(|&e| !e).is_some() {
172+ line -= 1;
173+ }
174+ }
175+
176+ let head = slice.line_to_char(line);
177+ let anchor = if behavior == Movement::Move {
178+ // exclude first character after paragraph boundary
179+ if line_to_empty && first_char {
180+ range.cursor(slice)
181+ } else {
182+ range.head
183+ }
184+ } else {
185+ range.put_cursor(slice, head, true).anchor
186+ };
187+ Range::new(anchor, head)
188+}
189+
190+pub fn move_next_para(slice: RopeSlice, range: Range, count: usize, behavior: Movement) -> Range {
191+ let mut line = slice.char_to_line(range.head);
192+ let lines = slice.lines_at(line);
193+ let mut lines = lines.map(|l| l.chunks().all(str_is_line_ending)).peekable();
194+ for _ in 0..count {
195+ while lines.next_if(|&e| !e).is_some() {
196+ line += 1;
197+ }
198+ while lines.next_if(|&e| e).is_some() {
199+ line += 1;
200+ }
201+ }
202+ let anchor = if behavior == Movement::Move {
203+ range.cursor(slice)
204+ } else {
205+ range.anchor
206+ };
207+ Range::new(anchor, slice.line_to_char(line))
208+}
209+
152210// ---- util ------------
153211
154212#[inline]
@@ -1179,4 +1237,67 @@ mod test {
11791237 }
11801238 }
11811239 }
1240+
1241+ #[test]
1242+ fn test_behaviour_when_moving_to_prev_paragraph_single() {
1243+ let tests = [
1244+ ("^@", "@^"),
1245+ ("^s@tart at\nfirst char\n", "@s^tart at\nfirst char\n"),
1246+ ("start at\nlast char^\n@", "@start at\nlast char\n^"),
1247+ ("goto\nfirst\n\n^p@aragraph", "@goto\nfirst\n\n^paragraph"),
1248+ ("goto\nfirst\n^\n@paragraph", "@goto\nfirst\n\n^paragraph"),
1249+ ("goto\nsecond\n\np^a@ragraph", "goto\nsecond\n\n@pa^ragraph"),
1250+ (
1251+ "here\n\nhave\nmultiple\nparagraph\n\n\n\n\n^@",
1252+ "here\n\n@have\nmultiple\nparagraph\n\n\n\n\n^",
1253+ ),
1254+ ];
1255+
1256+ for (actual, expected) in tests {
1257+ let (s, selection) = crate::test::print(actual);
1258+ let text = Rope::from(s.as_str());
1259+ let selection =
1260+ selection.transform(|r| move_prev_para(text.slice(..), r, 1, Movement::Move));
1261+ let actual = crate::test::plain(&s, selection);
1262+ assert_eq!(actual, expected);
1263+ }
1264+ }
1265+
1266+ #[ignore]
1267+ #[test]
1268+ fn test_behaviour_when_moving_to_prev_paragraph_double() {}
1269+
1270+ #[test]
1271+ fn test_behaviour_when_moving_to_next_paragraph_single() {
1272+ let tests = [
1273+ ("^@", "@^"),
1274+ ("^s@tart at\nfirst char\n", "^start at\nfirst char\n@"),
1275+ ("start at\nlast char^\n@", "start at\nlast char^\n@"),
1276+ (
1277+ "a\nb\n\n^g@oto\nthird\n\nparagraph",
1278+ "a\nb\n\n^goto\nthird\n\n@paragraph",
1279+ ),
1280+ (
1281+ "a\nb\n^\n@goto\nthird\n\nparagraph",
1282+ "a\nb\n\n^goto\nthird\n\n@paragraph",
1283+ ),
1284+ (
1285+ "a\nb^\n@\ngoto\nsecond\n\nparagraph",
1286+ "a\nb^\n\n@goto\nsecond\n\nparagraph",
1287+ ),
1288+ (
1289+ "here\n\nhave\n^m@ultiple\nparagraph\n\n\n\n\n",
1290+ "here\n\nhave\n^multiple\nparagraph\n\n\n\n\n@",
1291+ ),
1292+ ];
1293+
1294+ for (actual, expected) in tests {
1295+ let (s, selection) = crate::test::print(actual);
1296+ let text = Rope::from(s.as_str());
1297+ let selection =
1298+ selection.transform(|r| move_next_para(text.slice(..), r, 1, Movement::Move));
1299+ let actual = crate::test::plain(&s, selection);
1300+ assert_eq!(actual, expected);
1301+ }
1302+ }
11821303}