diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index b44d149fb..003a1f370 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -177,6 +177,10 @@ pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Ran word_move(slice, range, count, WordMotionTarget::PrevWordStart) } +pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { + word_move(slice, range, count, WordMotionTarget::PrevWordEnd) +} + pub fn move_next_long_word_start(slice: RopeSlice, range: Range, count: usize) -> Range { word_move(slice, range, count, WordMotionTarget::NextLongWordStart) } @@ -189,8 +193,8 @@ pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) - word_move(slice, range, count, WordMotionTarget::PrevLongWordStart) } -pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { - word_move(slice, range, count, WordMotionTarget::PrevWordEnd) +pub fn move_prev_long_word_end(slice: RopeSlice, range: Range, count: usize) -> Range { + word_move(slice, range, count, WordMotionTarget::PrevLongWordEnd) } fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range { @@ -199,6 +203,7 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart | WordMotionTarget::PrevWordEnd + | WordMotionTarget::PrevLongWordEnd ); // Special-case early-out. @@ -377,6 +382,7 @@ pub enum WordMotionTarget { NextLongWordStart, NextLongWordEnd, PrevLongWordStart, + PrevLongWordEnd, } pub trait CharHelpers { @@ -393,6 +399,7 @@ impl CharHelpers for Chars<'_> { WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart | WordMotionTarget::PrevWordEnd + | WordMotionTarget::PrevLongWordEnd ); // Reverse the iterator if needed for the motion direction. @@ -479,7 +486,7 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo is_word_boundary(prev_ch, next_ch) && (!prev_ch.is_whitespace() || char_is_line_ending(next_ch)) } - WordMotionTarget::NextLongWordStart => { + WordMotionTarget::NextLongWordStart | WordMotionTarget::PrevLongWordEnd => { is_long_word_boundary(prev_ch, next_ch) && (char_is_line_ending(next_ch) || !next_ch.is_whitespace()) } @@ -1445,6 +1452,100 @@ mod test { } } + #[test] + fn test_behaviour_when_moving_to_end_of_prev_long_words() { + let tests = [ + ( + "Basic backward motion from the middle of a word", + vec![(1, Range::new(3, 3), Range::new(4, 0))], + ), + ("Starting from after boundary retreats the anchor", + vec![(1, Range::new(0, 9), Range::new(8, 0))], + ), + ( + "Jump to end of a word succeeded by whitespace", + vec![(1, Range::new(10, 10), Range::new(10, 4))], + ), + ( + " Jump to start of line from end of word preceded by whitespace", + vec![(1, Range::new(3, 4), Range::new(4, 0))], + ), + ("Previous anchor is irrelevant for backward motions", + vec![(1, Range::new(12, 5), Range::new(6, 0))]), + ( + " Starting from whitespace moves to first space in sequence", + vec![(1, Range::new(0, 4), Range::new(4, 0))], + ), + ("Identifiers_with_underscores are considered a single word", + vec![(1, Range::new(0, 20), Range::new(20, 0))]), + ( + "Jumping\n \nback through a newline selects whitespace", + vec![(1, Range::new(0, 13), Range::new(12, 8))], + ), + ( + "Jumping to start of word from the end selects the word", + vec![(1, Range::new(6, 7), Range::new(7, 0))], + ), + ( + "alphanumeric.!,and.?=punctuation are treated exactly the same", + vec![(1, Range::new(29, 30), Range::new(30, 0))], + ), + ( + "... ... punctuation and spaces behave as expected", + vec![ + (1, Range::new(0, 10), Range::new(9, 3)), + (1, Range::new(10, 6), Range::new(7, 3)), + ], + ), + (".._.._ punctuation is joined by underscores into a single block", + vec![(1, Range::new(0, 6), Range::new(6, 0))]), + ( + "Newlines\n\nare bridged seamlessly.", + vec![(1, Range::new(0, 10), Range::new(8, 0))], + ), + ( + "Jumping \n\n\n\n\nback from within a newline group selects previous block", + vec![(1, Range::new(0, 13), Range::new(11, 7))], + ), + ( + "Failed motions do not modify the range", + vec![(0, Range::new(3, 0), Range::new(3, 0))], + ), + ( + "Multiple motions at once resolve correctly", + vec![(3, Range::new(19, 19), Range::new(8, 0))], + ), + ( + "Excessive motions are performed partially", + vec![(999, Range::new(40, 40), Range::new(9, 0))], + ), + ( + "", // Edge case of moving backwards in empty string + vec![(1, Range::new(0, 0), Range::new(0, 0))], + ), + ( + "\n\n\n\n\n", // Edge case of moving backwards in all newlines + vec![(1, Range::new(5, 5), Range::new(0, 0))], + ), + (" \n \nJumping back through alternated space blocks and newlines selects the space blocks", + vec![ + (1, Range::new(0, 8), Range::new(7, 4)), + (1, Range::new(7, 4), Range::new(3, 0)), + ]), + ("ヒーリ..クス multibyte characters behave as normal characters, including when interacting with punctuation", + vec![ + (1, Range::new(0, 8), Range::new(7, 0)), + ]), + ]; + + for (sample, scenario) in tests { + for (count, begin, expected_end) in scenario.into_iter() { + let range = move_prev_long_word_end(Rope::from(sample).slice(..), begin, count); + assert_eq!(range, expected_end, "Case failed: [{}]", sample); + } + } + } + #[test] fn test_behaviour_when_moving_to_prev_paragraph_single() { let tests = [ diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 0c24c6933..99f50b041 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -247,6 +247,7 @@ impl MappableCommand { move_next_long_word_start, "Move to start of next long word", move_prev_long_word_start, "Move to start of previous long word", move_next_long_word_end, "Move to end of next long word", + move_prev_long_word_end, "Move to end of previous long word", extend_next_word_start, "Extend to start of next word", extend_prev_word_start, "Extend to start of previous word", extend_next_word_end, "Extend to end of next word", @@ -254,6 +255,7 @@ impl MappableCommand { extend_next_long_word_start, "Extend to start of next long word", extend_prev_long_word_start, "Extend to start of previous long word", extend_next_long_word_end, "Extend to end of next long word", + extend_prev_long_word_end, "Extend to end of prev long word", find_till_char, "Move till next occurrence of char", find_next_char, "Move to next occurrence of char", extend_till_char, "Extend till next occurrence of char", @@ -1067,6 +1069,10 @@ fn move_prev_long_word_start(cx: &mut Context) { move_word_impl(cx, movement::move_prev_long_word_start) } +fn move_prev_long_word_end(cx: &mut Context) { + move_word_impl(cx, movement::move_prev_long_word_end) +} + fn move_next_long_word_end(cx: &mut Context) { move_word_impl(cx, movement::move_next_long_word_end) } @@ -1224,6 +1230,10 @@ fn extend_prev_long_word_start(cx: &mut Context) { extend_word_impl(cx, movement::move_prev_long_word_start) } +fn extend_prev_long_word_end(cx: &mut Context) { + extend_word_impl(cx, movement::move_prev_long_word_end) +} + fn extend_next_long_word_end(cx: &mut Context) { extend_word_impl(cx, movement::move_next_long_word_end) }