on master help sign in
Add (prev) paragraph motion

Also improved testing facility. Fix #1580

commit8b02bf2ea8c07926697ad8e1e01d77596a540d59 authorIvan Tham <pickfire@riseup.net> dateMon 7 Feb 2022, 23:37 (5 years ago) parente4561d1
6 files changed+270
helix-core/src/lib.rs+1 helix-core/src/line_ending.rs+5 helix-core/src/movement.rs+121 helix-core/src/test.rs+111 helix-term/src/commands.rs+30 helix-term/src/keymap/default.rs+2
helix-core/src/lib.rs+1
@@ -24,6 +24,7 @@ pub mod shellwords;
2424mod state;
2525pub mod surround;
2626pub mod syntax;
27+pub mod test;
2728pub mod textobject;
2829mod transaction;
2930
helix-core/src/line_ending.rs+5
@@ -119,6 +119,11 @@ pub fn str_is_line_ending(s: &str) -> bool {
119119 LineEnding::from_str(s).is_some()
120120}
121121
122+#[inline]
123+pub fn rope_is_line_ending(r: RopeSlice) -> bool {
124+ r.chunks().all(str_is_line_ending)
125+}
126+
122127/// Attempts to detect what line ending the passed document uses.
123128pub fn auto_detect_line_ending(doc: &Rope) -> Option<LineEnding> {
124129 // Return first matched line ending. Not all possible line endings
helix-core/src/movement.rs+121
@@ -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}
helix-core/src/test.rsnew file+111
@@ -0,0 +1,111 @@
1+//! Test helpers.
2+use crate::{Range, Selection};
3+use smallvec::SmallVec;
4+use std::cmp::Reverse;
5+
6+/// Convert annotated test string to test string and selection.
7+///
8+/// `^` for `anchor` and `|` for head (`@` for primary), both must appear
9+/// or otherwise it will panic.
10+///
11+/// # Examples
12+///
13+/// ```
14+/// use helix_core::{Range, Selection, test::print};
15+/// use smallvec::smallvec;
16+///
17+/// assert_eq!(
18+/// print("^a@b|c^"),
19+/// ("abc".to_owned(), Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0))
20+/// );
21+/// ```
22+///
23+/// # Panics
24+///
25+/// Panics when missing primary or appeared more than once.
26+/// Panics when missing head or anchor.
27+/// Panics when head come after head or anchor come after anchor.
28+pub fn print(s: &str) -> (String, Selection) {
29+ let mut anchor = None;
30+ let mut head = None;
31+ let mut primary = None;
32+ let mut ranges = SmallVec::new();
33+ let mut i = 0;
34+ let s = s
35+ .chars()
36+ .filter(|c| {
37+ match c {
38+ '^' if anchor != None => panic!("anchor without head {s:?}"),
39+ '^' if head == None => anchor = Some(i),
40+ '^' => ranges.push(Range::new(i, head.take().unwrap())),
41+ '|' if head != None => panic!("head without anchor {s:?}"),
42+ '|' if anchor == None => head = Some(i),
43+ '|' => ranges.push(Range::new(anchor.take().unwrap(), i)),
44+ '@' if primary != None => panic!("head (primary) already appeared {s:?}"),
45+ '@' if head != None => panic!("head (primary) without anchor {s:?}"),
46+ '@' if anchor == None => {
47+ primary = Some(ranges.len());
48+ head = Some(i);
49+ }
50+ '@' => {
51+ primary = Some(ranges.len());
52+ ranges.push(Range::new(anchor.take().unwrap(), i));
53+ }
54+ _ => {
55+ i += 1;
56+ return true;
57+ }
58+ };
59+ false
60+ })
61+ .collect();
62+ if head.is_some() {
63+ panic!("missing anchor (|) {s:?}");
64+ }
65+ if anchor.is_some() {
66+ panic!("missing head (^) {s:?}");
67+ }
68+ let primary = match primary {
69+ Some(i) => i,
70+ None => panic!("missing primary (@) {s:?}"),
71+ };
72+ let selection = Selection::new(ranges, primary);
73+ (s, selection)
74+}
75+
76+/// Convert test string and selection to annotated test string.
77+///
78+/// `^` for `anchor` and `|` for head (`@` for primary).
79+///
80+/// # Examples
81+///
82+/// ```
83+/// use helix_core::{Range, Selection, test::plain};
84+/// use smallvec::smallvec;
85+///
86+/// assert_eq!(
87+/// plain("abc", Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)),
88+/// "^a@b|c^".to_owned()
89+/// );
90+/// ```
91+pub fn plain(s: &str, selection: Selection) -> String {
92+ let primary = selection.primary_index();
93+ let mut out = String::with_capacity(s.len() + 2 * selection.len());
94+ out.push_str(s);
95+ let mut insertion: Vec<_> = selection
96+ .iter()
97+ .enumerate()
98+ .flat_map(|(i, range)| {
99+ [
100+ (range.anchor, '^'),
101+ (range.head, if i == primary { '@' } else { '|' }),
102+ ]
103+ })
104+ .collect();
105+ // insert in reverse order
106+ insertion.sort_unstable_by_key(|k| Reverse(k.0));
107+ for (i, c) in insertion {
108+ out.insert(i, c);
109+ }
110+ out
111+}
helix-term/src/commands.rs+30
@@ -209,6 +209,8 @@ impl MappableCommand {
209209 move_next_long_word_start, "Move to beginning of next long word",
210210 move_prev_long_word_start, "Move to beginning of previous long word",
211211 move_next_long_word_end, "Move to end of next long word",
212+ move_prev_para, "Move to previous paragraph",
213+ move_next_para, "Move to next paragraph",
212214 extend_next_word_start, "Extend to beginning of next word",
213215 extend_prev_word_start, "Extend to beginning of previous word",
214216 extend_next_long_word_start, "Extend to beginning of next long word",
@@ -902,6 +904,34 @@ fn move_next_long_word_end(cx: &mut Context) {
902904 move_word_impl(cx, movement::move_next_long_word_end)
903905}
904906
907+fn move_para_impl<F>(cx: &mut Context, move_fn: F)
908+where
909+ F: Fn(RopeSlice, Range, usize, Movement) -> Range,
910+{
911+ let count = cx.count();
912+ let (view, doc) = current!(cx.editor);
913+ let text = doc.text().slice(..);
914+ let behavior = if doc.mode == Mode::Select {
915+ Movement::Extend
916+ } else {
917+ Movement::Move
918+ };
919+
920+ let selection = doc
921+ .selection(view.id)
922+ .clone()
923+ .transform(|range| move_fn(text, range, count, behavior));
924+ doc.set_selection(view.id, selection);
925+}
926+
927+fn move_prev_para(cx: &mut Context) {
928+ move_para_impl(cx, movement::move_prev_para)
929+}
930+
931+fn move_next_para(cx: &mut Context) {
932+ move_para_impl(cx, movement::move_next_para)
933+}
934+
905935fn goto_file_start(cx: &mut Context) {
906936 if cx.count.is_some() {
907937 goto_line(cx);
helix-term/src/keymap/default.rs+2
@@ -104,6 +104,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
104104 "c" => goto_prev_class,
105105 "a" => goto_prev_parameter,
106106 "o" => goto_prev_comment,
107+ "p" => move_prev_para,
107108 "space" => add_newline_above,
108109 },
109110 "]" => { "Right bracket"
@@ -113,6 +114,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
113114 "c" => goto_next_class,
114115 "a" => goto_next_parameter,
115116 "o" => goto_next_comment,
117+ "p" => move_next_para,
116118 "space" => add_newline_below,
117119 },
118120