@@ -1,5 +1,12 @@
1+use std::iter::{self, from_fn, Peekable, SkipWhile};
3+use ropey::iter::Chars;
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,
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 {
17+#[derive(Copy, Clone, PartialEq, Eq)]
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)
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)
45+ let anchor = match behaviour {
46+ Movement::Extend => range.anchor,
47+ Movement::Move => pos,
32− Range::new(if extend { range.anchor } else { pos }, pos)
49+ Range::new(anchor, pos)
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);
44 let horiz = range.horiz.unwrap_or(col as u32);
61 let horiz = range.horiz.unwrap_or(col as u32);
@@ -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),
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);
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);
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));
78+ let anchor = match behaviour {
79+ Movement::Extend => range.anchor,
80+ Movement::Move => pos,
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);
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)
70− if begin + 1 == slice.len_chars() {
74− let mut ch = slice.char(begin);
75− let next = slice.char(begin + 1);
77− // if we're at the end of a word, or on whitespce right before new one
78− if categorize(ch) != categorize(next) {
82− if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
85− ch = slice.char(begin);
90− skip_over_next(slice, &mut end, is_word);
91− } else if is_punctuation(ch) {
92− skip_over_next(slice, &mut end, is_punctuation);
95− skip_over_next(slice, &mut end, char::is_whitespace);
98− Some(Range::new(begin, end - 1))
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;
110− let ch = slice.char(begin);
111− let prev = slice.char(begin - 1);
113− if categorize(ch) != categorize(prev) {
117− // return if not skip while?
118− skip_over_prev(slice, &mut begin, |ch| ch == '\n');
122− with_end = skip_over_prev(slice, &mut end, char::is_whitespace);
125− let ch = slice.char(end);
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);
134− Some(Range::new(begin, if with_end { end } else { end + 1 }))
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)
141− if begin + 2 >= slice.len_chars() {
145− let ch = slice.char(begin);
146− let next = slice.char(begin + 1);
148− if categorize(ch) != categorize(next) {
152− if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
158− skip_over_next(slice, &mut end, char::is_whitespace);
161− let ch = slice.char(end);
164− skip_over_next(slice, &mut end, is_word);
165− } else if is_punctuation(ch) {
166− skip_over_next(slice, &mut end, is_punctuation);
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)
173// ---- util ------------
106// ---- util ------------
175−// used for by-word movement
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 == '_'
113+pub(crate) fn is_end_of_line(ch: char) -> bool {
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)
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 {
201#[derive(Debug, Eq, PartialEq)]
142#[derive(Debug, Eq, PartialEq)]
202−pub(crate) enum Category {
143+pub enum Category {
203 Whitespace,
144 Whitespace,
@@ -209,7 +150,7 @@ pub(crate) enum Category {
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 {
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.
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>
229 F: Fn(char) -> bool,
173 F: Fn(char) -> bool,
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() {
240− chars.next().is_some()
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
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>
247 F: Fn(char) -> bool,
185 F: Fn(char) -> bool,
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)| {
191+ Some(pos.saturating_sub(i))
252− #[allow(clippy::while_let_on_iterator)]
198+/// Possible targets of a word motion
199+#[derive(Copy, Clone, Debug)]
200+pub enum WordMotionTarget {
206+pub trait CharHelpers {
207+ fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range;
210+enum WordMotionPhase {
216+impl CharHelpers for Chars<'_> {
217+ fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range {
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 => {
223+ Box::new(from_fn(|| self.prev()))
225+ _ => Box::new(self),
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),
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 => {
243+ if characters.peek().is_none() {
244+ break; // We're at the end, so there's nothing to do.
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);
250+ // First character is always skipped by the head
252+ WordMotionPhase::SkipNewlines
254+ WordMotionPhase::SkipNewlines => {
255+ if is_end_of_line(peek) {
257+ if characters.peek().is_some() {
260+ WordMotionPhase::SkipNewlines
262+ WordMotionPhase::ReachTarget
265+ WordMotionPhase::ReachTarget => {
267+ anchor = anchor.or(Some(head));
268+ if reached_target(target, peek, characters.peek()) {
273+ WordMotionPhase::ReachTarget
277+ Range::new(anchor.unwrap_or(origin.anchor), head)
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,
288+ WordMotionTarget::NextWordStart => {
289+ ((categorize(peek) != categorize(*next_peek))
290+ && (is_end_of_line(*next_peek) || !next_peek.is_whitespace()))
292+ WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
293+ ((categorize(peek) != categorize(*next_peek))
294+ && (!peek.is_whitespace() || is_end_of_line(*next_peek)))
257− *pos = pos.saturating_sub(1);
259− fun(slice.char(*pos))
262#[cfg(test)]
299#[cfg(test)]
263mod test {
300mod test {
301+ use std::array::{self, IntoIter};
264 use super::*;
305 use super::*;
307+ const SINGLE_LINE_SAMPLE: &str = "This is a simple alphabetic line";
308+ const MULTILINE_SAMPLE: &str = "\
312+ is merely alphabetic\n\
316+ const MULTIBYTE_CHARACTER_SAMPLE: &str = "\
318+ The text above is Japanese\n\
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(
276− move_vertically(slice, range, Direction::Forward, 1, false).head
331+ move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head
278 (1, 2).into()
333 (1, 2).into()
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());
343+ let mut range = Range::point(position);
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)),
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())
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());
366+ let mut range = Range::point(position);
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_
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);
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());
393+ let mut range = Range::point(position);
394+ let original_anchor = range.anchor;
396+ let moves = IntoIter::new([
397+ (Direction::Forward, 1usize),
398+ (Direction::Forward, 5usize),
399+ (Direction::Backward, 3usize),
402+ for (direction, amount) in moves {
403+ range = move_horizontally(slice, range, direction, amount, Movement::Extend);
404+ assert_eq!(range.anchor, original_anchor);
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)),
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);
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);
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)),
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),
462+ assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
463+ assert_eq!(range.head, range.anchor);
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);
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.
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)),
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),
493+ assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
494+ assert_eq!(range.head, range.anchor);
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);
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);
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);
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",
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))
541+ ("... ... punctuation and spaces behave as expected",
543+ (1, Range::new(0, 0), Range::new(0, 5)),
544+ (1, Range::new(0, 5), Range::new(6, 9)),
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.",
550+ (1, Range::new(0, 0), Range::new(0, 7)),
551+ (1, Range::new(0, 7), Range::new(10, 13)),
553+ ("Jumping\n\n\n\n\n\n from newlines to whitespace selects whitespace.",
555+ (1, Range::new(0, 8), Range::new(13, 15)),
557+ ("A failed motion does not modify the range",
559+ (3, Range::new(37, 41), Range::new(37, 41)),
561+ ("oh oh oh two character words!",
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)),
567+ ("Multiple motions at once resolve correctly",
569+ (3, Range::new(0, 0), Range::new(17, 19)),
571+ ("Excessive motions are performed partially",
573+ (999, Range::new(0, 0), Range::new(32, 40)),
575+ ("", // Edge case of moving forward in empty string
577+ (1, Range::new(0, 0), Range::new(0, 0)),
579+ ("\n\n\n\n\n", // Edge case of moving forward in all newlines
581+ (1, Range::new(0, 0), Range::new(0, 4)),
583+ ("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
585+ (1, Range::new(0, 0), Range::new(1, 3)),
586+ (1, Range::new(1, 3), Range::new(5, 7)),
588+ ("ヒーリクス multibyte characters behave as normal characters",
590+ (1, Range::new(0, 0), Range::new(0, 5)),
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);
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",
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))
630+ ("... ... punctuation and spaces behave as expected",
632+ (1, Range::new(0, 10), Range::new(9, 6)),
633+ (1, Range::new(9, 6), Range::new(5, 0)),
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.",
639+ (1, Range::new(0, 10), Range::new(7, 0)),
641+ ("Jumping \n\n\n\n\nback from within a newline group selects previous block",
643+ (1, Range::new(0, 13), Range::new(10, 0)),
645+ ("Failed motions do not modify the range",
647+ (0, Range::new(3, 0), Range::new(3, 0)),
649+ ("Multiple motions at once resolve correctly",
651+ (3, Range::new(18, 18), Range::new(8, 0)),
653+ ("Excessive motions are performed partially",
655+ (999, Range::new(40, 40), Range::new(9, 0)),
657+ ("", // Edge case of moving backwards in empty string
659+ (1, Range::new(0, 0), Range::new(0, 0)),
661+ ("\n\n\n\n\n", // Edge case of moving backwards in all newlines
663+ (1, Range::new(0, 0), Range::new(0, 0)),
665+ (" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
667+ (1, Range::new(0, 7), Range::new(6, 4)),
668+ (1, Range::new(6, 4), Range::new(2, 0)),
670+ ("ヒーリクス multibyte characters behave as normal characters",
672+ (1, Range::new(0, 5), Range::new(4, 0)),
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);
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",
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))
709+ ("... ... punctuation and spaces behave as expected",
711+ (1, Range::new(0, 0), Range::new(0, 2)),
712+ (1, Range::new(0, 2), Range::new(3, 8)),
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.",
718+ (1, Range::new(0, 0), Range::new(0, 7)),
719+ (1, Range::new(0, 7), Range::new(10, 12)),
721+ ("Jumping\n\n\n\n\n\n from newlines to whitespace selects to end of next word.",
723+ (1, Range::new(0, 8), Range::new(13, 19)),
725+ ("A failed motion does not modify the range",
727+ (3, Range::new(37, 41), Range::new(37, 41)),
729+ ("Multiple motions at once resolve correctly",
731+ (3, Range::new(0, 0), Range::new(16, 18)),
733+ ("Excessive motions are performed partially",
735+ (999, Range::new(0, 0), Range::new(31, 40)),
737+ ("", // Edge case of moving forward in empty string
739+ (1, Range::new(0, 0), Range::new(0, 0)),
741+ ("\n\n\n\n\n", // Edge case of moving forward in all newlines
743+ (1, Range::new(0, 0), Range::new(0, 4)),
745+ ("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
747+ (1, Range::new(0, 0), Range::new(1, 3)),
748+ (1, Range::new(1, 3), Range::new(5, 7)),
750+ ("ヒーリクス multibyte characters behave as normal characters",
752+ (1, Range::new(0, 0), Range::new(0, 4)),
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);
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 = " ";
289 assert_eq!(Category::Eol, categorize('\n'));
772 assert_eq!(Category::Eol, categorize('\n'));