on master help sign in
Movement fixes, refactor and unit test suite (#217)
86af55cPabloMansanet
book/src/keymap.md+21−21
@@ -19,7 +19,7 @@
19| F | find previous char |
19| F | find previous char |
20| Home | move to the start of the line |
20| Home | move to the start of the line |
21| End | move to the end of the line |
21| End | move to the end of the line |
22−| m | Jump to matching bracket |
22+| m | Jump to matching bracket |
23| PageUp | Move page up |
23| PageUp | Move page up |
24| PageDown | Move page down |
24| PageDown | Move page down |
25| ctrl-u | Move half page up |
25| ctrl-u | Move half page up |
@@ -42,10 +42,10 @@
42| R | replace with yanked text |
42| R | replace with yanked text |
43| i | Insert before selection |
43| i | Insert before selection |
44| a | Insert after selection (append) |
44| a | Insert after selection (append) |
45−| I | Insert at the start of the line |
45+| I | Insert at the start of the line |
46−| A | Insert at the end of the line |
46+| A | Insert at the end of the line |
47−| o | Open new line below selection |
47+| o | Open new line below selection |
48−| o | Open new line above selection |
48+| o | Open new line above selection |
49| u | Undo change |
49| u | Undo change |
50| U | Redo change |
50| U | Redo change |
51| y | Yank selection |
51| y | Yank selection |
@@ -54,26 +54,26 @@
54| > | Indent selection |
54| > | Indent selection |
55| < | Unindent selection |
55| < | Unindent selection |
56| = | Format selection |
56| = | Format selection |
57−| d | Delete selection |
57+| d | Delete selection |
58−| c | Change selection (delete and enter insert mode) |
58+| c | Change selection (delete and enter insert mode) |
59
59
60### Selection manipulation
60### Selection manipulation
61
61
62| Key | Description |
62| Key | Description |
63|-----|-----------|
63|-----|-----------|
64−| s | Select all regex matches inside selections |
64+| s | Select all regex matches inside selections |
65−| S | Split selection into subselections on regex matches |
65+| S | Split selection into subselections on regex matches |
66−| alt-s | Split selection on newlines |
66+| alt-s | Split selection on newlines |
67−| ; | Collapse selection onto a single cursor |
67+| ; | Collapse selection onto a single cursor |
68−| alt-; | Flip selection cursor and anchor |
68+| alt-; | Flip selection cursor and anchor |
69−| % | Select entire file |
69+| % | Select entire file |
70−| x | Select current line |
70+| x | Select current line |
71−| X | Extend to next line |
71+| X | Extend to next line |
72−| [ | Expand selection to parent syntax node TODO: pick a key |
72+| [ | Expand selection to parent syntax node TODO: pick a key |
73| J | join lines inside selection |
73| J | join lines inside selection |
74| K | keep selections matching the regex TODO: overlapped by hover help |
74| K | keep selections matching the regex TODO: overlapped by hover help |
75| space | keep only the primary selection TODO: overlapped by space mode |
75| space | keep only the primary selection TODO: overlapped by space mode |
76−| ctrl-c | Comment/uncomment the selections |
76+| ctrl-c | Comment/uncomment the selections |
77
77
78### Search
78### Search
79
79
@@ -82,10 +82,10 @@ in reverse, or searching via smartcase.
82
82
83| Key | Description |
83| Key | Description |
84|-----|-----------|
84|-----|-----------|
85−| / | Search for regex pattern |
85+| / | Search for regex pattern |
86−| n | Select next search match |
86+| n | Select next search match |
87−| N | Add next search match to selection |
87+| N | Add next search match to selection |
88−| * | Use current selection as the search pattern |
88+| * | Use current selection as the search pattern |
89
89
90### Diagnostics
90### Diagnostics
91
91
helix-core/src/auto_pairs.rs+2−2
@@ -67,7 +67,7 @@ fn handle_open(
67
67
68 let mut offs = 0;
68 let mut offs = 0;
69
69
70− let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
70+ let transaction = Transaction::change_by_selection(doc, selection, |range| {
71 let pos = range.head;
71 let pos = range.head;
72 let next = next_char(doc, pos);
72 let next = next_char(doc, pos);
73
73
@@ -109,7 +109,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
109
109
110 let mut offs = 0;
110 let mut offs = 0;
111
111
112− let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
112+ let transaction = Transaction::change_by_selection(doc, selection, |range| {
113 let pos = range.head;
113 let pos = range.head;
114 let next = next_char(doc, pos);
114 let next = next_char(doc, pos);
115
115
helix-core/src/lib.rs−1
@@ -16,7 +16,6 @@ pub mod selection;
16mod state;
16mod state;
17pub mod syntax;
17pub mod syntax;
18mod transaction;
18mod transaction;
19−pub mod words;
20
19
21pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
20pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
22 line.chars().position(|ch| !ch.is_whitespace())
21 line.chars().position(|ch| !ch.is_whitespace())
helix-core/src/movement.rs+629−146
@@ -1,5 +1,12 @@
1+use std::iter::{self, from_fn, Peekable, SkipWhile};
2+
3+use ropey::iter::Chars;
4+
5+use crate::{
6+ coords_at_pos,
1−use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
7+ graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
2−use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection};
8+ pos_at_coords, Position, Range, RopeSlice,
9+};
3
10
4#[derive(Debug, Copy, Clone, PartialEq, Eq)]
11#[derive(Debug, Copy, Clone, PartialEq, Eq)]
5pub enum Direction {
12pub enum Direction {
@@ -7,39 +14,49 @@ pub enum Direction {
7 Backward,
14 Backward,
8}
15}
9
16
17+#[derive(Copy, Clone, PartialEq, Eq)]
18+pub enum Movement {
19+ Extend,
20+ Move,
21+}
22+
10pub fn move_horizontally(
23pub fn move_horizontally(
11− text: RopeSlice,
24+ slice: RopeSlice,
12 range: Range,
25 range: Range,
13 dir: Direction,
26 dir: Direction,
14 count: usize,
27 count: usize,
15− extend: bool,
28+ behaviour: Movement,
16) -> Range {
29) -> Range {
17 let pos = range.head;
30 let pos = range.head;
18− let line = text.char_to_line(pos);
31+ let line = slice.char_to_line(pos);
19 // TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way
32 // TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way
20 // we stop calculating past start/end of line.
33 // we stop calculating past start/end of line.
21 let pos = match dir {
34 let pos = match dir {
22 Direction::Backward => {
35 Direction::Backward => {
23− let start = text.line_to_char(line);
36+ let start = slice.line_to_char(line);
24− nth_prev_grapheme_boundary(text, pos, count).max(start)
37+ nth_prev_grapheme_boundary(slice, pos, count).max(start)
25 }
38 }
26 Direction::Forward => {
39 Direction::Forward => {
27 // Line end is pos at the start of next line - 1
40 // Line end is pos at the start of next line - 1
28− let end = text.line_to_char(line + 1).saturating_sub(1);
41+ let end = slice.line_to_char(line + 1).saturating_sub(1);
29− nth_next_grapheme_boundary(text, pos, count).min(end)
42+ nth_next_grapheme_boundary(slice, pos, count).min(end)
30 }
43 }
31 };
44 };
45+ let anchor = match behaviour {
46+ Movement::Extend => range.anchor,
47+ Movement::Move => pos,
48+ };
32− Range::new(if extend { range.anchor } else { pos }, pos)
49+ Range::new(anchor, pos)
33}
50}
34
51
35pub fn move_vertically(
52pub fn move_vertically(
36− text: RopeSlice,
53+ slice: RopeSlice,
37 range: Range,
54 range: Range,
38 dir: Direction,
55 dir: Direction,
39 count: usize,
56 count: usize,
40− extend: bool,
57+ behaviour: Movement,
41) -> Range {
58) -> Range {
42− let Position { row, col } = coords_at_pos(text, range.head);
59+ let Position { row, col } = coords_at_pos(slice, range.head);
43
60
44 let horiz = range.horiz.unwrap_or(col as u32);
61 let horiz = range.horiz.unwrap_or(col as u32);
45
62
@@ -47,138 +64,62 @@ pub fn move_vertically(
47 Direction::Backward => row.saturating_sub(count),
64 Direction::Backward => row.saturating_sub(count),
48 Direction::Forward => std::cmp::min(
65 Direction::Forward => std::cmp::min(
49 row.saturating_add(count),
66 row.saturating_add(count),
50− text.len_lines().saturating_sub(2),
67+ slice.len_lines().saturating_sub(2),
51 ),
68 ),
52 };
69 };
53
70
54 // convert to 0-indexed, subtract another 1 because len_chars() counts \n
71 // convert to 0-indexed, subtract another 1 because len_chars() counts \n
55− let new_line_len = text.line(new_line).len_chars().saturating_sub(2);
72+ let new_line_len = slice.line(new_line).len_chars().saturating_sub(2);
56
73
57 let new_col = std::cmp::min(horiz as usize, new_line_len);
74 let new_col = std::cmp::min(horiz as usize, new_line_len);
58
75
59− let pos = pos_at_coords(text, Position::new(new_line, new_col));
76+ let pos = pos_at_coords(slice, Position::new(new_line, new_col));
60
77
78+ let anchor = match behaviour {
79+ Movement::Extend => range.anchor,
80+ Movement::Move => pos,
81+ };
82+
61− let mut range = Range::new(if extend { range.anchor } else { pos }, pos);
83+ let mut range = Range::new(anchor, pos);
62 range.horiz = Some(horiz);
84 range.horiz = Some(horiz);
63 range
85 range
64}
86}
65
87
66−pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
88+pub fn move_next_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
67− let mut end = begin;
89+ word_move(slice, range, count, WordMotionTarget::NextWordStart)
68−
69− for _ in 0..count {
70− if begin + 1 == slice.len_chars() {
71− return None;
72− }
73−
74− let mut ch = slice.char(begin);
75− let next = slice.char(begin + 1);
76−
77− // if we're at the end of a word, or on whitespce right before new one
78− if categorize(ch) != categorize(next) {
79− begin += 1;
80− }
81−
82− if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
83− return None;
84− };
85− ch = slice.char(begin);
86−
87− end = begin + 1;
88−
89− if is_word(ch) {
90− skip_over_next(slice, &mut end, is_word);
91− } else if is_punctuation(ch) {
92− skip_over_next(slice, &mut end, is_punctuation);
93− }
94−
95− skip_over_next(slice, &mut end, char::is_whitespace);
96− }
97−
98− Some(Range::new(begin, end - 1))
99}
90}
100
91
101−pub fn move_prev_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
92+pub fn move_next_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
102− let mut with_end = false;
93+ word_move(slice, range, count, WordMotionTarget::NextWordEnd)
103− let mut end = begin;
104−
105− for _ in 0..count {
106− if begin == 0 {
107− return None;
108− }
109−
110− let ch = slice.char(begin);
111− let prev = slice.char(begin - 1);
112−
113− if categorize(ch) != categorize(prev) {
114− begin -= 1;
115− }
116−
117− // return if not skip while?
118− skip_over_prev(slice, &mut begin, |ch| ch == '\n');
119−
120− end = begin;
121−
122− with_end = skip_over_prev(slice, &mut end, char::is_whitespace);
123−
124− // refetch
125− let ch = slice.char(end);
126−
127− if is_word(ch) {
128− with_end = skip_over_prev(slice, &mut end, is_word);
129− } else if is_punctuation(ch) {
130− with_end = skip_over_prev(slice, &mut end, is_punctuation);
131− }
132− }
133−
134− Some(Range::new(begin, if with_end { end } else { end + 1 }))
135}
94}
136
95
137−pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
96+pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
138− let mut end = begin;
97+ word_move(slice, range, count, WordMotionTarget::PrevWordStart)
139−
140− for _ in 0..count {
141− if begin + 2 >= slice.len_chars() {
142− return None;
143− }
144−
145− let ch = slice.char(begin);
146− let next = slice.char(begin + 1);
147−
148− if categorize(ch) != categorize(next) {
149− begin += 1;
150− }
151−
152− if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
153− return None;
154− };
155−
156− end = begin;
157−
158− skip_over_next(slice, &mut end, char::is_whitespace);
159−
160− // refetch
161− let ch = slice.char(end);
162−
163− if is_word(ch) {
164− skip_over_next(slice, &mut end, is_word);
165− } else if is_punctuation(ch) {
166− skip_over_next(slice, &mut end, is_punctuation);
167− }
168− }
98+}
169
99
170− Some(Range::new(begin, end - 1))
100+fn word_move(slice: RopeSlice, mut range: Range, count: usize, target: WordMotionTarget) -> Range {
101+ (0..count).fold(range, |range, _| {
102+ slice.chars_at(range.head).range_to_target(target, range)
103+ })
171}
104}
172
105
173// ---- util ------------
106// ---- util ------------
174−
175−// used for by-word movement
176−
177#[inline]
107#[inline]
178pub(crate) fn is_word(ch: char) -> bool {
108pub(crate) fn is_word(ch: char) -> bool {
179 ch.is_alphanumeric() || ch == '_'
109 ch.is_alphanumeric() || ch == '_'
180}
110}
181
111
112+#[inline]
113+pub(crate) fn is_end_of_line(ch: char) -> bool {
114+ ch == '\n'
115+}
116+
117+#[inline]
118+// Whitespace, but not end of line
119+pub(crate) fn is_strict_whitespace(ch: char) -> bool {
120+ ch.is_whitespace() && !is_end_of_line(ch)
121+}
122+
182#[inline]
123#[inline]
183pub(crate) fn is_punctuation(ch: char) -> bool {
124pub(crate) fn is_punctuation(ch: char) -> bool {
184 use unicode_general_category::{get_general_category, GeneralCategory};
125 use unicode_general_category::{get_general_category, GeneralCategory};
@@ -199,7 +140,7 @@ pub(crate) fn is_punctuation(ch: char) -> bool {
199}
140}
200
141
201#[derive(Debug, Eq, PartialEq)]
142#[derive(Debug, Eq, PartialEq)]
202−pub(crate) enum Category {
143+pub enum Category {
203 Whitespace,
144 Whitespace,
204 Eol,
145 Eol,
205 Word,
146 Word,
@@ -209,7 +150,7 @@ pub(crate) enum Category {
209
150
210#[inline]
151#[inline]
211pub(crate) fn categorize(ch: char) -> Category {
152pub(crate) fn categorize(ch: char) -> Category {
212− if ch == '\n' {
153+ if is_end_of_line(ch) {
213 Category::Eol
154 Category::Eol
214 } else if ch.is_whitespace() {
155 } else if ch.is_whitespace() {
215 Category::Whitespace
156 Category::Whitespace
@@ -223,46 +164,160 @@ pub(crate) fn categorize(ch: char) -> Category {
223}
164}
224
165
225#[inline]
166#[inline]
226−/// Returns true if there are more characters left after the new position.
167+/// Returns first index that doesn't satisfy a given predicate when
168+/// advancing the character index.
169+///
170+/// Returns none if all characters satisfy the predicate.
227−pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
171+pub fn skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<usize>
228where
172where
229 F: Fn(char) -> bool,
173 F: Fn(char) -> bool,
230{
174{
231− let mut chars = slice.chars_at(*pos);
175+ let mut chars = slice.chars_at(pos).enumerate();
232−
176+ chars.find_map(|(i, c)| if !fun(c) { Some(pos + i) } else { None })
233− #[allow(clippy::while_let_on_iterator)]
234− while let Some(ch) = chars.next() {
235− if !fun(ch) {
236− break;
237− }
238− *pos += 1;
239− }
240− chars.next().is_some()
241}
177}
242
178
243#[inline]
179#[inline]
244−/// Returns true if the final pos matches the predicate.
180+/// Returns first index that doesn't satisfy a given predicate when
181+/// retreating the character index, saturating if all elements satisfy
182+/// the condition.
245−pub fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
183+pub fn backwards_skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<usize>
246where
184where
247 F: Fn(char) -> bool,
185 F: Fn(char) -> bool,
248{
186{
249− // need to +1 so that prev() includes current char
250− let mut chars = slice.chars_at(*pos + 1);
187+ let mut chars_starting_from_next = slice.chars_at(pos + 1);
188+ let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate();
189+ backwards.find_map(|(i, c)| {
190+ if !fun(c) {
191+ Some(pos.saturating_sub(i))
192+ } else {
193+ None
194+ }
195+ })
196+}
251
197
252− #[allow(clippy::while_let_on_iterator)]
198+/// Possible targets of a word motion
199+#[derive(Copy, Clone, Debug)]
200+pub enum WordMotionTarget {
201+ NextWordStart,
202+ NextWordEnd,
203+ PrevWordStart,
204+}
205+
206+pub trait CharHelpers {
207+ fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range;
208+}
209+
210+enum WordMotionPhase {
211+ Start,
212+ SkipNewlines,
213+ ReachTarget,
214+}
215+
216+impl CharHelpers for Chars<'_> {
217+ fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range {
218+ let range = origin;
219+ // Characters are iterated forward or backwards depending on the motion direction.
220+ let characters: Box<dyn Iterator<Item = char>> = match target {
221+ WordMotionTarget::PrevWordStart => {
222+ self.next();
223+ Box::new(from_fn(|| self.prev()))
224+ }
225+ _ => Box::new(self),
226+ };
227+
228+ // Index advancement also depends on the direction.
229+ let advance: &dyn Fn(&mut usize) = match target {
230+ WordMotionTarget::PrevWordStart => &|u| *u = u.saturating_sub(1),
231+ _ => &|u| *u += 1,
232+ };
233+
234+ let mut characters = characters.peekable();
235+ let mut phase = WordMotionPhase::Start;
236+ let mut head = origin.head;
237+ let mut anchor: Option<usize> = None;
238+ let is_boundary = |a: char, b: Option<char>| categorize(a) != categorize(b.unwrap_or(a));
253− while let Some(ch) = chars.prev() {
239+ while let Some(peek) = characters.peek().copied() {
254− if !fun(ch) {
240+ phase = match phase {
241+ WordMotionPhase::Start => {
242+ characters.next();
243+ if characters.peek().is_none() {
244+ break; // We're at the end, so there's nothing to do.
245+ }
246+ // Anchor may remain here if the head wasn't at a boundary
247+ if !is_boundary(peek, characters.peek().copied()) && !is_end_of_line(peek) {
248+ anchor = Some(head);
249+ }
250+ // First character is always skipped by the head
251+ advance(&mut head);
252+ WordMotionPhase::SkipNewlines
253+ }
254+ WordMotionPhase::SkipNewlines => {
255+ if is_end_of_line(peek) {
256+ characters.next();
257+ if characters.peek().is_some() {
258+ advance(&mut head);
259+ }
260+ WordMotionPhase::SkipNewlines
261+ } else {
262+ WordMotionPhase::ReachTarget
263+ }
264+ }
265+ WordMotionPhase::ReachTarget => {
266+ characters.next();
267+ anchor = anchor.or(Some(head));
268+ if reached_target(target, peek, characters.peek()) {
255− break;
269+ break;
270+ } else {
271+ advance(&mut head);
272+ }
273+ WordMotionPhase::ReachTarget
274+ }
275+ }
276+ }
277+ Range::new(anchor.unwrap_or(origin.anchor), head)
278+ }
279+}
280+
281+fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>) -> bool {
282+ let next_peek = match next_peek {
283+ Some(next_peek) => next_peek,
284+ None => return true,
285+ };
286+
287+ match target {
288+ WordMotionTarget::NextWordStart => {
289+ ((categorize(peek) != categorize(*next_peek))
290+ && (is_end_of_line(*next_peek) || !next_peek.is_whitespace()))
291+ }
292+ WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
293+ ((categorize(peek) != categorize(*next_peek))
294+ && (!peek.is_whitespace() || is_end_of_line(*next_peek)))
256 }
295 }
257− *pos = pos.saturating_sub(1);
258 }
296 }
259− fun(slice.char(*pos))
260}
297}
261
298
262#[cfg(test)]
299#[cfg(test)]
263mod test {
300mod test {
301+ use std::array::{self, IntoIter};
302+
303+ use ropey::Rope;
304+
264 use super::*;
305 use super::*;
265
306
307+ const SINGLE_LINE_SAMPLE: &str = "This is a simple alphabetic line";
308+ const MULTILINE_SAMPLE: &str = "\
309+ Multiline\n\
310+ text sample\n\
311+ which\n\
312+ is merely alphabetic\n\
313+ and whitespaced\n\
314+ ";
315+
316+ const MULTIBYTE_CHARACTER_SAMPLE: &str = "\
317+ パーティーへ行かないか\n\
318+ The text above is Japanese\n\
319+ ";
320+
266 #[test]
321 #[test]
267 fn test_vertical_move() {
322 fn test_vertical_move() {
268 let text = Rope::from("abcd\nefg\nwrs");
323 let text = Rope::from("abcd\nefg\nwrs");
@@ -273,17 +328,445 @@ mod test {
273 assert_eq!(
328 assert_eq!(
274 coords_at_pos(
329 coords_at_pos(
275 slice,
330 slice,
276− move_vertically(slice, range, Direction::Forward, 1, false).head
331+ move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head
277 ),
332 ),
278 (1, 2).into()
333 (1, 2).into()
279 );
334 );
280 }
335 }
281
336
337+ #[test]
338+ fn horizontal_moves_through_single_line_in_single_line_text() {
339+ let text = Rope::from(SINGLE_LINE_SAMPLE);
340+ let slice = text.slice(..);
341+ let position = pos_at_coords(slice, (0, 0).into());
342+
343+ let mut range = Range::point(position);
344+
345+ let moves_and_expected_coordinates = [
346+ ((Direction::Forward, 1usize), (0, 1)),
347+ ((Direction::Forward, 2usize), (0, 3)),
348+ ((Direction::Forward, 0usize), (0, 3)),
349+ ((Direction::Forward, 999usize), (0, 31)),
350+ ((Direction::Forward, 999usize), (0, 31)),
351+ ((Direction::Backward, 999usize), (0, 0)),
352+ ];
353+
354+ for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
355+ range = move_horizontally(slice, range, direction, amount, Movement::Move);
356+ assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
357+ }
358+ }
359+
360+ #[test]
361+ fn horizontal_moves_through_single_line_in_multiline_text() {
362+ let text = Rope::from(MULTILINE_SAMPLE);
363+ let slice = text.slice(..);
364+ let position = pos_at_coords(slice, (0, 0).into());
365+
366+ let mut range = Range::point(position);
367+
368+ let moves_and_expected_coordinates = IntoIter::new([
369+ ((Direction::Forward, 1usize), (0, 1)), // M_ltiline
370+ ((Direction::Forward, 2usize), (0, 3)), // Mul_iline
371+ ((Direction::Backward, 6usize), (0, 0)), // _ultiline
372+ ((Direction::Backward, 999usize), (0, 0)), // _ultiline
373+ ((Direction::Forward, 3usize), (0, 3)), // Mul_iline
374+ ((Direction::Forward, 0usize), (0, 3)), // Mul_iline
375+ ((Direction::Backward, 0usize), (0, 3)), // Mul_iline
376+ ((Direction::Forward, 999usize), (0, 9)), // Multilin_
377+ ((Direction::Forward, 999usize), (0, 9)), // Multilin_
378+ ]);
379+
380+ for ((direction, amount), coordinates) in moves_and_expected_coordinates {
381+ range = move_horizontally(slice, range, direction, amount, Movement::Move);
382+ assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
383+ assert_eq!(range.head, range.anchor);
384+ }
385+ }
386+
387+ #[test]
388+ fn selection_extending_moves_in_single_line_text() {
389+ let text = Rope::from(SINGLE_LINE_SAMPLE);
390+ let slice = text.slice(..);
391+ let position = pos_at_coords(slice, (0, 0).into());
392+
393+ let mut range = Range::point(position);
394+ let original_anchor = range.anchor;
395+
396+ let moves = IntoIter::new([
397+ (Direction::Forward, 1usize),
398+ (Direction::Forward, 5usize),
399+ (Direction::Backward, 3usize),
400+ ]);
401+
402+ for (direction, amount) in moves {
403+ range = move_horizontally(slice, range, direction, amount, Movement::Extend);
404+ assert_eq!(range.anchor, original_anchor);
405+ }
406+ }
407+
408+ #[test]
409+ fn vertical_moves_in_single_column() {
410+ let text = Rope::from(MULTILINE_SAMPLE);
411+ let slice = dbg!(&text).slice(..);
412+ let position = pos_at_coords(slice, (0, 0).into());
413+ let mut range = Range::point(position);
414+ let moves_and_expected_coordinates = IntoIter::new([
415+ ((Direction::Forward, 1usize), (1, 0)),
416+ ((Direction::Forward, 2usize), (3, 0)),
417+ ((Direction::Backward, 999usize), (0, 0)),
418+ ((Direction::Forward, 3usize), (3, 0)),
419+ ((Direction::Forward, 0usize), (3, 0)),
420+ ((Direction::Backward, 0usize), (3, 0)),
421+ ((Direction::Forward, 5), (4, 0)),
422+ ((Direction::Forward, 999usize), (4, 0)),
423+ ]);
424+
425+ for ((direction, amount), coordinates) in moves_and_expected_coordinates {
426+ range = move_vertically(slice, range, direction, amount, Movement::Move);
427+ assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
428+ assert_eq!(range.head, range.anchor);
429+ }
430+ }
431+
432+ #[test]
433+ fn vertical_moves_jumping_column() {
434+ let text = Rope::from(MULTILINE_SAMPLE);
435+ let slice = text.slice(..);
436+ let position = pos_at_coords(slice, (0, 0).into());
437+ let mut range = Range::point(position);
438+
439+ enum Axis {
440+ H,
441+ V,
442+ }
443+ let moves_and_expected_coordinates = IntoIter::new([
444+ // Places cursor at the end of line
445+ ((Axis::H, Direction::Forward, 8usize), (0, 8)),
446+ // First descent preserves column as the target line is wider
447+ ((Axis::V, Direction::Forward, 1usize), (1, 8)),
448+ // Second descent clamps column as the target line is shorter
449+ ((Axis::V, Direction::Forward, 1usize), (2, 4)),
450+ // Third descent restores the original column
451+ ((Axis::V, Direction::Forward, 1usize), (3, 8)),
452+ // Behaviour is preserved even through long jumps
453+ ((Axis::V, Direction::Backward, 999usize), (0, 8)),
454+ ((Axis::V, Direction::Forward, 999usize), (4, 8)),
455+ ]);
456+
457+ for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
458+ range = match axis {
459+ Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move),
460+ Axis::V => move_vertically(slice, range, direction, amount, Movement::Move),
461+ };
462+ assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
463+ assert_eq!(range.head, range.anchor);
464+ }
465+ }
466+
467+ #[test]
468+ fn multibyte_character_column_jumps() {
469+ let text = Rope::from(MULTIBYTE_CHARACTER_SAMPLE);
470+ let slice = text.slice(..);
471+ let position = pos_at_coords(slice, (0, 0).into());
472+ let mut range = Range::point(position);
473+
474+ // FIXME: The behaviour captured in this test diverges from both Kakoune and Vim. These
475+ // will attempt to preserve the horizontal position of the cursor, rather than
476+ // placing it at the same character index.
477+ enum Axis {
478+ H,
479+ V,
480+ }
481+ let moves_and_expected_coordinates = IntoIter::new([
482+ // Places cursor at the fourth kana
483+ ((Axis::H, Direction::Forward, 4), (0, 4)),
484+ // Descent places cursor at the fourth character.
485+ ((Axis::V, Direction::Forward, 1usize), (1, 4)),
486+ ]);
487+
488+ for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
489+ range = match axis {
490+ Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move),
491+ Axis::V => move_vertically(slice, range, direction, amount, Movement::Move),
492+ };
493+ assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
494+ assert_eq!(range.head, range.anchor);
495+ }
496+ }
497+
498+ #[test]
499+ #[should_panic]
500+ fn nonsensical_ranges_panic_on_forward_movement_attempt_in_debug_mode() {
501+ move_next_word_start(Rope::from("Sample").slice(..), Range::point(99999999), 1);
502+ }
503+
504+ #[test]
505+ #[should_panic]
506+ fn nonsensical_ranges_panic_on_forward_to_end_movement_attempt_in_debug_mode() {
507+ move_next_word_end(Rope::from("Sample").slice(..), Range::point(99999999), 1);
508+ }
509+
510+ #[test]
511+ #[should_panic]
512+ fn nonsensical_ranges_panic_on_backwards_movement_attempt_in_debug_mode() {
513+ move_prev_word_start(Rope::from("Sample").slice(..), Range::point(99999999), 1);
514+ }
515+
516+ #[test]
517+ fn test_behaviour_when_moving_to_start_of_next_words() {
518+ let tests = array::IntoIter::new([
519+ ("Basic forward motion stops at the first space",
520+ vec![(1, Range::new(0, 0), Range::new(0, 5))]),
521+ (" Starting from a boundary advances the anchor",
522+ vec![(1, Range::new(0, 0), Range::new(1, 9))]),
523+ ("Long whitespace gap is bridged by the head",
524+ vec![(1, Range::new(0, 0), Range::new(0, 10))]),
525+ ("Previous anchor is irrelevant for forward motions",
526+ vec![(1, Range::new(12, 0), Range::new(0, 8))]),
527+ (" Starting from whitespace moves to last space in sequence",
528+ vec![(1, Range::new(0, 0), Range::new(0, 3))]),
529+ ("Starting from mid-word leaves anchor at start position and moves head",
530+ vec![(1, Range::new(3, 3), Range::new(3, 8))]),
531+ ("Identifiers_with_underscores are considered a single word",
532+ vec![(1, Range::new(0, 0), Range::new(0, 28))]),
533+ ("Jumping\n into starting whitespace selects the spaces before 'into'",
534+ vec![(1, Range::new(0, 6), Range::new(8, 11))]),
535+ ("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
536+ vec![
537+ (1, Range::new(0, 0), Range::new(0, 11)),
538+ (1, Range::new(0, 11), Range::new(12, 14)),
539+ (1, Range::new(12, 14), Range::new(15, 17))
540+ ]),
541+ ("... ... punctuation and spaces behave as expected",
542+ vec![
543+ (1, Range::new(0, 0), Range::new(0, 5)),
544+ (1, Range::new(0, 5), Range::new(6, 9)),
545+ ]),
546+ (".._.._ punctuation is not joined by underscores into a single block",
547+ vec![(1, Range::new(0, 0), Range::new(0, 1))]),
548+ ("Newlines\n\nare bridged seamlessly.",
549+ vec![
550+ (1, Range::new(0, 0), Range::new(0, 7)),
551+ (1, Range::new(0, 7), Range::new(10, 13)),
552+ ]),
553+ ("Jumping\n\n\n\n\n\n from newlines to whitespace selects whitespace.",
554+ vec![
555+ (1, Range::new(0, 8), Range::new(13, 15)),
556+ ]),
557+ ("A failed motion does not modify the range",
558+ vec![
559+ (3, Range::new(37, 41), Range::new(37, 41)),
560+ ]),
561+ ("oh oh oh two character words!",
562+ vec![
563+ (1, Range::new(0, 0), Range::new(0, 2)),
564+ (1, Range::new(0, 2), Range::new(3, 5)),
565+ (1, Range::new(0, 1), Range::new(2, 2)),
566+ ]),
567+ ("Multiple motions at once resolve correctly",
568+ vec![
569+ (3, Range::new(0, 0), Range::new(17, 19)),
570+ ]),
571+ ("Excessive motions are performed partially",
572+ vec![
573+ (999, Range::new(0, 0), Range::new(32, 40)),
574+ ]),
575+ ("", // Edge case of moving forward in empty string
576+ vec![
577+ (1, Range::new(0, 0), Range::new(0, 0)),
578+ ]),
579+ ("\n\n\n\n\n", // Edge case of moving forward in all newlines
580+ vec![
581+ (1, Range::new(0, 0), Range::new(0, 4)),
582+ ]),
583+ ("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
584+ vec![
585+ (1, Range::new(0, 0), Range::new(1, 3)),
586+ (1, Range::new(1, 3), Range::new(5, 7)),
587+ ]),
588+ ("ヒーリクス multibyte characters behave as normal characters",
589+ vec![
590+ (1, Range::new(0, 0), Range::new(0, 5)),
591+ ]),
592+ ]);
593+
594+ for (sample, scenario) in tests {
595+ for (count, begin, expected_end) in scenario.into_iter() {
596+ let range = move_next_word_start(Rope::from(sample).slice(..), begin, count);
597+ assert_eq!(range, expected_end, "Case failed: [{}]", sample);
598+ }
599+ }
600+ }
601+
602+ #[test]
603+ fn test_behaviour_when_moving_to_start_of_previous_words() {
604+ let tests = array::IntoIter::new([
605+ ("Basic backward motion from the middle of a word",
606+ vec![(1, Range::new(3, 3), Range::new(3, 0))]),
607+ ("Starting from after boundary retreats the anchor",
608+ vec![(1, Range::new(0, 8), Range::new(7, 0))]),
609+ (" Jump to start of a word preceded by whitespace",
610+ vec![(1, Range::new(5, 5), Range::new(5, 4))]),
611+ (" Jump to start of line from start of word preceded by whitespace",
612+ vec![(1, Range::new(4, 4), Range::new(3, 0))]),
613+ ("Previous anchor is irrelevant for backward motions",
614+ vec![(1, Range::new(12, 5), Range::new(5, 0))]),
615+ (" Starting from whitespace moves to first space in sequence",
616+ vec![(1, Range::new(0, 3), Range::new(3, 0))]),
617+ ("Identifiers_with_underscores are considered a single word",
618+ vec![(1, Range::new(0, 20), Range::new(20, 0))]),
619+ ("Jumping\n \nback through a newline selects whitespace",
620+ vec![(1, Range::new(0, 13), Range::new(11, 8))]),
621+ ("Jumping to start of word from the end selects the word",
622+ vec![(1, Range::new(6, 6), Range::new(6, 0))]),
623+ ("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
624+ vec![
625+ (1, Range::new(30, 30), Range::new(30, 21)),
626+ (1, Range::new(30, 21), Range::new(20, 18)),
627+ (1, Range::new(20, 18), Range::new(17, 15))
628+ ]),
629+
630+ ("... ... punctuation and spaces behave as expected",
631+ vec![
632+ (1, Range::new(0, 10), Range::new(9, 6)),
633+ (1, Range::new(9, 6), Range::new(5, 0)),
634+ ]),
635+ (".._.._ punctuation is not joined by underscores into a single block",
636+ vec![(1, Range::new(0, 5), Range::new(4, 3))]),
637+ ("Newlines\n\nare bridged seamlessly.",
638+ vec![
639+ (1, Range::new(0, 10), Range::new(7, 0)),
640+ ]),
641+ ("Jumping \n\n\n\n\nback from within a newline group selects previous block",
642+ vec![
643+ (1, Range::new(0, 13), Range::new(10, 0)),
644+ ]),
645+ ("Failed motions do not modify the range",
646+ vec![
647+ (0, Range::new(3, 0), Range::new(3, 0)),
648+ ]),
649+ ("Multiple motions at once resolve correctly",
650+ vec![
651+ (3, Range::new(18, 18), Range::new(8, 0)),
652+ ]),
653+ ("Excessive motions are performed partially",
654+ vec![
655+ (999, Range::new(40, 40), Range::new(9, 0)),
656+ ]),
657+ ("", // Edge case of moving backwards in empty string
658+ vec![
659+ (1, Range::new(0, 0), Range::new(0, 0)),
660+ ]),
661+ ("\n\n\n\n\n", // Edge case of moving backwards in all newlines
662+ vec![
663+ (1, Range::new(0, 0), Range::new(0, 0)),
664+ ]),
665+ (" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
666+ vec![
667+ (1, Range::new(0, 7), Range::new(6, 4)),
668+ (1, Range::new(6, 4), Range::new(2, 0)),
669+ ]),
670+ ("ヒーリクス multibyte characters behave as normal characters",
671+ vec![
672+ (1, Range::new(0, 5), Range::new(4, 0)),
673+ ]),
674+ ]);
675+
676+ for (sample, scenario) in tests {
677+ for (count, begin, expected_end) in scenario.into_iter() {
678+ let range = move_prev_word_start(Rope::from(sample).slice(..), begin, count);
679+ assert_eq!(range, expected_end, "Case failed: [{}]", sample);
680+ }
681+ }
682+ }
683+
684+ #[test]
685+ fn test_behaviour_when_moving_to_end_of_next_words() {
686+ let tests = array::IntoIter::new([
687+ ("Basic forward motion from the start of a word to the end of it",
688+ vec![(1, Range::new(0, 0), Range::new(0, 4))]),
689+ ("Basic forward motion from the end of a word to the end of the next",
690+ vec![(1, Range::new(0, 4), Range::new(5, 12))]),
691+ ("Basic forward motion from the middle of a word to the end of it",
692+ vec![(1, Range::new(2, 2), Range::new(2, 4))]),
693+ (" Jumping to end of a word preceded by whitespace",
694+ vec![(1, Range::new(0, 0), Range::new(0, 10))]),
695+ (" Starting from a boundary advances the anchor",
696+ vec![(1, Range::new(0, 0), Range::new(1, 8))]),
697+ ("Previous anchor is irrelevant for end of word motion",
698+ vec![(1, Range::new(12, 2), Range::new(2, 7))]),
699+ ("Identifiers_with_underscores are considered a single word",
700+ vec![(1, Range::new(0, 0), Range::new(0, 27))]),
701+ ("Jumping\n into starting whitespace selects up to the end of next word",
702+ vec![(1, Range::new(0, 6), Range::new(8, 15))]),
703+ ("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
704+ vec![
705+ (1, Range::new(0, 0), Range::new(0, 11)),
706+ (1, Range::new(0, 11), Range::new(12, 14)),
707+ (1, Range::new(12, 14), Range::new(15, 17))
708+ ]),
709+ ("... ... punctuation and spaces behave as expected",
710+ vec![
711+ (1, Range::new(0, 0), Range::new(0, 2)),
712+ (1, Range::new(0, 2), Range::new(3, 8)),
713+ ]),
714+ (".._.._ punctuation is not joined by underscores into a single block",
715+ vec![(1, Range::new(0, 0), Range::new(0, 1))]),
716+ ("Newlines\n\nare bridged seamlessly.",
717+ vec![
718+ (1, Range::new(0, 0), Range::new(0, 7)),
719+ (1, Range::new(0, 7), Range::new(10, 12)),
720+ ]),
721+ ("Jumping\n\n\n\n\n\n from newlines to whitespace selects to end of next word.",
722+ vec![
723+ (1, Range::new(0, 8), Range::new(13, 19)),
724+ ]),
725+ ("A failed motion does not modify the range",
726+ vec![
727+ (3, Range::new(37, 41), Range::new(37, 41)),
728+ ]),
729+ ("Multiple motions at once resolve correctly",
730+ vec![
731+ (3, Range::new(0, 0), Range::new(16, 18)),
732+ ]),
733+ ("Excessive motions are performed partially",
734+ vec![
735+ (999, Range::new(0, 0), Range::new(31, 40)),
736+ ]),
737+ ("", // Edge case of moving forward in empty string
738+ vec![
739+ (1, Range::new(0, 0), Range::new(0, 0)),
740+ ]),
741+ ("\n\n\n\n\n", // Edge case of moving forward in all newlines
742+ vec![
743+ (1, Range::new(0, 0), Range::new(0, 4)),
744+ ]),
745+ ("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
746+ vec![
747+ (1, Range::new(0, 0), Range::new(1, 3)),
748+ (1, Range::new(1, 3), Range::new(5, 7)),
749+ ]),
750+ ("ヒーリクス multibyte characters behave as normal characters",
751+ vec![
752+ (1, Range::new(0, 0), Range::new(0, 4)),
753+ ]),
754+ ]);
755+
756+ for (sample, scenario) in tests {
757+ for (count, begin, expected_end) in scenario.into_iter() {
758+ let range = move_next_word_end(Rope::from(sample).slice(..), begin, count);
759+ assert_eq!(range, expected_end, "Case failed: [{}]", sample);
760+ }
761+ }
762+ }
763+
282 #[test]
764 #[test]
283 fn test_categorize() {
765 fn test_categorize() {
284 const WORD_TEST_CASE: &'static str =
766 const WORD_TEST_CASE: &'static str =
285 "_hello_world_あいうえおー12345678901234567890";
767 "_hello_world_あいうえおー12345678901234567890";
286− const PUNCTUATION_TEST_CASE: &'static str = "!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
768+ const PUNCTUATION_TEST_CASE: &'static str =
769+ "!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
287 const WHITESPACE_TEST_CASE: &'static str = "      ";
770 const WHITESPACE_TEST_CASE: &'static str = "      ";
288
771
289 assert_eq!(Category::Eol, categorize('\n'));
772 assert_eq!(Category::Eol, categorize('\n'));
helix-core/src/selection.rs+4
@@ -35,6 +35,10 @@ impl Range {
35 }
35 }
36 }
36 }
37
37
38+ pub fn point(head: usize) -> Self {
39+ Self::new(head, head)
40+ }
41+
38 /// Start of the range.
42 /// Start of the range.
39 #[inline]
43 #[inline]
40 #[must_use]
44 #[must_use]
helix-core/src/words.rsdeleted−68
@@ -1,68 +0,0 @@
1−use crate::movement::{categorize, is_punctuation, is_word, skip_over_prev};
2−use ropey::RopeSlice;
3−
4−#[must_use]
5−pub fn nth_prev_word_boundary(slice: RopeSlice, mut char_idx: usize, count: usize) -> usize {
6− let mut with_end = false;
7−
8− for _ in 0..count {
9− if char_idx == 0 {
10− break;
11− }
12−
13− // return if not skip while?
14− skip_over_prev(slice, &mut char_idx, |ch| ch == '\n');
15−
16− with_end = skip_over_prev(slice, &mut char_idx, char::is_whitespace);
17−
18− // refetch
19− let ch = slice.char(char_idx);
20−
21− if is_word(ch) {
22− with_end = skip_over_prev(slice, &mut char_idx, is_word);
23− } else if is_punctuation(ch) {
24− with_end = skip_over_prev(slice, &mut char_idx, is_punctuation);
25− }
26− }
27−
28− if with_end || char_idx == 0 {
29− char_idx
30− } else {
31− char_idx + 1
32− }
33−}
34−
35−#[test]
36−fn different_prev_word_boundary() {
37− use ropey::Rope;
38− let t = |x, y| {
39− let text = Rope::from(x);
40− let out = nth_prev_word_boundary(text.slice(..), text.len_chars().saturating_sub(1), 1);
41− assert_eq!(text.slice(..out), y, r#"from "{}""#, x);
42− };
43− t("abcd\nefg\nwrs", "abcd\nefg\n");
44− t("abcd\nefg\n", "abcd\n");
45− t("abcd\n", "");
46− t("hello, world!", "hello, world");
47− t("hello, world", "hello, ");
48− t("hello, ", "hello");
49− t("hello", "");
50− t(",", "");
51− t("こんにちは、世界!", "こんにちは、世界");
52− t("こんにちは、世界", "こんにちは、");
53− t("こんにちは、", "こんにちは");
54− t("こんにちは", "");
55− t("この世界。", "この世界");
56− t("この世界", "");
57− t("お前はもう死んでいる", "");
58− t("その300円です", ""); // TODO: should stop at 300
59− t("唱k", ""); // TODO: should stop at 唱
60− t(",", "");
61− t("1 + 1 = 2", "1 + 1 = ");
62− t("1 + 1 =", "1 + 1 ");
63− t("1 + 1", "1 + ");
64− t("1 + ", "1 ");
65− t("1 ", "");
66− t("1+1=2", "1+1=");
67− t("", "");
68−}
helix-term/src/commands.rs+32−81
@@ -4,8 +4,8 @@ use helix_core::{
4 movement::{self, Direction},
4 movement::{self, Direction},
5 object, pos_at_coords,
5 object, pos_at_coords,
6 regex::{self, Regex},
6 regex::{self, Regex},
7− register, search, selection, words, Change, ChangeSet, Position, Range, Rope, RopeSlice,
7+ register, search, selection, Change, ChangeSet, Position, Range, Rope, RopeSlice, Selection,
8− Selection, SmallVec, Tendril, Transaction,
8+ SmallVec, Tendril, Transaction,
9};
9};
10
10
11use helix_view::{
11use helix_view::{
@@ -19,6 +19,7 @@ use helix_lsp::{
19 util::{lsp_pos_to_pos, pos_to_lsp_pos, range_to_lsp_range},
19 util::{lsp_pos_to_pos, pos_to_lsp_pos, range_to_lsp_range},
20 OffsetEncoding,
20 OffsetEncoding,
21};
21};
22+use movement::Movement;
22
23
23use crate::{
24use crate::{
24 compositor::{Callback, Component, Compositor},
25 compositor::{Callback, Component, Compositor},
@@ -29,8 +30,10 @@ use crate::application::{LspCallbackWrapper, LspCallbacks};
29use futures_util::FutureExt;
30use futures_util::FutureExt;
30use std::future::Future;
31use std::future::Future;
31
32
33+use std::{
32−use std::borrow::Cow;
34+ borrow::Cow,
33−use std::path::{Path, PathBuf};
35+ path::{Path, PathBuf},
36+};
34
37
35use crossterm::event::{KeyCode, KeyEvent};
38use crossterm::event::{KeyCode, KeyEvent};
36use once_cell::sync::Lazy;
39use once_cell::sync::Lazy;
@@ -132,13 +135,7 @@ pub fn move_char_left(cx: &mut Context) {
132 let (view, doc) = cx.current();
135 let (view, doc) = cx.current();
133 let text = doc.text().slice(..);
136 let text = doc.text().slice(..);
134 let selection = doc.selection(view.id).transform(|range| {
137 let selection = doc.selection(view.id).transform(|range| {
135− movement::move_horizontally(
138+ movement::move_horizontally(text, range, Direction::Backward, count, Movement::Move)
136− text,
137− range,
138− Direction::Backward,
139− count,
140− false, /* extend */
141− )
142 });
139 });
143 doc.set_selection(view.id, selection);
140 doc.set_selection(view.id, selection);
144}
141}
@@ -148,13 +145,7 @@ pub fn move_char_right(cx: &mut Context) {
148 let (view, doc) = cx.current();
145 let (view, doc) = cx.current();
149 let text = doc.text().slice(..);
146 let text = doc.text().slice(..);
150 let selection = doc.selection(view.id).transform(|range| {
147 let selection = doc.selection(view.id).transform(|range| {
151− movement::move_horizontally(
148+ movement::move_horizontally(text, range, Direction::Forward, count, Movement::Move)
152− text,
153− range,
154− Direction::Forward,
155− count,
156− false, /* extend */
157− )
158 });
149 });
159 doc.set_selection(view.id, selection);
150 doc.set_selection(view.id, selection);
160}
151}
@@ -164,13 +155,7 @@ pub fn move_line_up(cx: &mut Context) {
164 let (view, doc) = cx.current();
155 let (view, doc) = cx.current();
165 let text = doc.text().slice(..);
156 let text = doc.text().slice(..);
166 let selection = doc.selection(view.id).transform(|range| {
157 let selection = doc.selection(view.id).transform(|range| {
167− movement::move_vertically(
158+ movement::move_vertically(text, range, Direction::Backward, count, Movement::Move)
168− text,
169− range,
170− Direction::Backward,
171− count,
172− false, /* extend */
173− )
174 });
159 });
175 doc.set_selection(view.id, selection);
160 doc.set_selection(view.id, selection);
176}
161}
@@ -180,13 +165,7 @@ pub fn move_line_down(cx: &mut Context) {
180 let (view, doc) = cx.current();
165 let (view, doc) = cx.current();
181 let text = doc.text().slice(..);
166 let text = doc.text().slice(..);
182 let selection = doc.selection(view.id).transform(|range| {
167 let selection = doc.selection(view.id).transform(|range| {
183− movement::move_vertically(
168+ movement::move_vertically(text, range, Direction::Forward, count, Movement::Move)
184− text,
185− range,
186− Direction::Forward,
187− count,
188− false, /* extend */
189− )
190 });
169 });
191 doc.set_selection(view.id, selection);
170 doc.set_selection(view.id, selection);
192}
171}
@@ -249,9 +228,9 @@ pub fn move_next_word_start(cx: &mut Context) {
249 let (view, doc) = cx.current();
228 let (view, doc) = cx.current();
250 let text = doc.text().slice(..);
229 let text = doc.text().slice(..);
251
230
252− let selection = doc.selection(view.id).transform(|range| {
231+ let selection = doc
232+ .selection(view.id)
253− movement::move_next_word_start(text, range.head, count).unwrap_or(range)
233+ .transform(|range| movement::move_next_word_start(text, range, count));
254− });
255
234
256 doc.set_selection(view.id, selection);
235 doc.set_selection(view.id, selection);
257}
236}
@@ -261,9 +240,9 @@ pub fn move_prev_word_start(cx: &mut Context) {
261 let (view, doc) = cx.current();
240 let (view, doc) = cx.current();
262 let text = doc.text().slice(..);
241 let text = doc.text().slice(..);
263
242
264− let selection = doc.selection(view.id).transform(|range| {
243+ let selection = doc
244+ .selection(view.id)
265− movement::move_prev_word_start(text, range.head, count).unwrap_or(range)
245+ .transform(|range| movement::move_prev_word_start(text, range, count));
266− });
267
246
268 doc.set_selection(view.id, selection);
247 doc.set_selection(view.id, selection);
269}
248}
@@ -275,7 +254,7 @@ pub fn move_next_word_end(cx: &mut Context) {
275
254
276 let selection = doc
255 let selection = doc
277 .selection(view.id)
256 .selection(view.id)
278− .transform(|range| movement::move_next_word_end(text, range.head, count).unwrap_or(range));
257+ .transform(|range| movement::move_next_word_end(text, range, count));
279
258
280 doc.set_selection(view.id, selection);
259 doc.set_selection(view.id, selection);
281}
260}
@@ -300,7 +279,7 @@ pub fn extend_next_word_start(cx: &mut Context) {
300 let text = doc.text().slice(..);
279 let text = doc.text().slice(..);
301
280
302 let selection = doc.selection(view.id).transform(|mut range| {
281 let selection = doc.selection(view.id).transform(|mut range| {
303− let word = movement::move_next_word_start(text, range.head, count).unwrap_or(range);
282+ let word = movement::move_next_word_start(text, range, count);
304 let pos = word.head;
283 let pos = word.head;
305 Range::new(range.anchor, pos)
284 Range::new(range.anchor, pos)
306 });
285 });
@@ -314,7 +293,7 @@ pub fn extend_prev_word_start(cx: &mut Context) {
314 let text = doc.text().slice(..);
293 let text = doc.text().slice(..);
315
294
316 let selection = doc.selection(view.id).transform(|mut range| {
295 let selection = doc.selection(view.id).transform(|mut range| {
317− let word = movement::move_prev_word_start(text, range.head, count).unwrap_or(range);
296+ let word = movement::move_prev_word_start(text, range, count);
318 let pos = word.head;
297 let pos = word.head;
319 Range::new(range.anchor, pos)
298 Range::new(range.anchor, pos)
320 });
299 });
@@ -327,7 +306,7 @@ pub fn extend_next_word_end(cx: &mut Context) {
327 let text = doc.text().slice(..);
306 let text = doc.text().slice(..);
328
307
329 let selection = doc.selection(view.id).transform(|mut range| {
308 let selection = doc.selection(view.id).transform(|mut range| {
330− let word = movement::move_next_word_end(text, range.head, count).unwrap_or(range);
309+ let word = movement::move_next_word_end(text, range, count);
331 let pos = word.head;
310 let pos = word.head;
332 Range::new(range.anchor, pos)
311 Range::new(range.anchor, pos)
333 });
312 });
@@ -578,13 +557,7 @@ pub fn extend_char_left(cx: &mut Context) {
578 let (view, doc) = cx.current();
557 let (view, doc) = cx.current();
579 let text = doc.text().slice(..);
558 let text = doc.text().slice(..);
580 let selection = doc.selection(view.id).transform(|range| {
559 let selection = doc.selection(view.id).transform(|range| {
581− movement::move_horizontally(
560+ movement::move_horizontally(text, range, Direction::Backward, count, Movement::Extend)
582− text,
583− range,
584− Direction::Backward,
585− count,
586− true, /* extend */
587− )
588 });
561 });
589 doc.set_selection(view.id, selection);
562 doc.set_selection(view.id, selection);
590}
563}
@@ -594,13 +567,7 @@ pub fn extend_char_right(cx: &mut Context) {
594 let (view, doc) = cx.current();
567 let (view, doc) = cx.current();
595 let text = doc.text().slice(..);
568 let text = doc.text().slice(..);
596 let selection = doc.selection(view.id).transform(|range| {
569 let selection = doc.selection(view.id).transform(|range| {
597− movement::move_horizontally(
570+ movement::move_horizontally(text, range, Direction::Forward, count, Movement::Extend)
598− text,
599− range,
600− Direction::Forward,
601− count,
602− true, /* extend */
603− )
604 });
571 });
605 doc.set_selection(view.id, selection);
572 doc.set_selection(view.id, selection);
606}
573}
@@ -610,13 +577,7 @@ pub fn extend_line_up(cx: &mut Context) {
610 let (view, doc) = cx.current();
577 let (view, doc) = cx.current();
611 let text = doc.text().slice(..);
578 let text = doc.text().slice(..);
612 let selection = doc.selection(view.id).transform(|range| {
579 let selection = doc.selection(view.id).transform(|range| {
613− movement::move_vertically(
580+ movement::move_vertically(text, range, Direction::Backward, count, Movement::Extend)
614− text,
615− range,
616− Direction::Backward,
617− count,
618− true, /* extend */
619− )
620 });
581 });
621 doc.set_selection(view.id, selection);
582 doc.set_selection(view.id, selection);
622}
583}
@@ -626,13 +587,7 @@ pub fn extend_line_down(cx: &mut Context) {
626 let (view, doc) = cx.current();
587 let (view, doc) = cx.current();
627 let text = doc.text().slice(..);
588 let text = doc.text().slice(..);
628 let selection = doc.selection(view.id).transform(|range| {
589 let selection = doc.selection(view.id).transform(|range| {
629− movement::move_vertically(
590+ movement::move_vertically(text, range, Direction::Forward, count, Movement::Extend)
630− text,
631− range,
632− Direction::Forward,
633− count,
634− true, /* extend */
635− )
636 });
591 });
637 doc.set_selection(view.id, selection);
592 doc.set_selection(view.id, selection);
638}
593}
@@ -1920,15 +1875,11 @@ pub mod insert {
1920 let count = cx.count();
1875 let count = cx.count();
1921 let (view, doc) = cx.current();
1876 let (view, doc) = cx.current();
1922 let text = doc.text().slice(..);
1877 let text = doc.text().slice(..);
1923− let transaction =
1878+ let selection = doc
1924− Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
1879+ .selection(view.id)
1925− (
1880+ .transform(|range| movement::move_prev_word_start(text, range, count));
1926− words::nth_prev_word_boundary(text, range.head, count),
1881+ doc.set_selection(view.id, selection);
1927− range.head,
1882+ delete_selection(cx)
1928− None,
1929− )
1930− });
1931− doc.apply(&transaction, view.id);
1932 }
1883 }
1933}
1884}
1934
1885
@@ -2183,7 +2134,7 @@ pub fn format_selections(cx: &mut Context) {
2183}
2134}
2184
2135
2185pub fn join_selections(cx: &mut Context) {
2136pub fn join_selections(cx: &mut Context) {
2186− use movement::skip_over_next;
2137+ use movement::skip_while;
2187 let (view, doc) = cx.current();
2138 let (view, doc) = cx.current();
2188 let text = doc.text();
2139 let text = doc.text();
2189 let slice = doc.text().slice(..);
2140 let slice = doc.text().slice(..);
@@ -2204,7 +2155,7 @@ pub fn join_selections(cx: &mut Context) {
2204 for line in lines {
2155 for line in lines {
2205 let mut start = text.line_to_char(line + 1).saturating_sub(1);
2156 let mut start = text.line_to_char(line + 1).saturating_sub(1);
2206 let mut end = start + 1;
2157 let mut end = start + 1;
2207− skip_over_next(slice, &mut end, |ch| matches!(ch, ' ' | '\t'));
2158+ end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);
2208
2159
2209 // need to skip from start, not end
2160 // need to skip from start, not end
2210 let change = (start, end, Some(fragment.clone()));
2161 let change = (start, end, Some(fragment.clone()));