@@ -113,6 +113,10 @@ pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) -
113113 word_move ( slice, range, count, WordMotionTarget :: PrevLongWordStart )
114114}
115115
116+ pub fn move_prev_word_end ( slice : RopeSlice , range : Range , count : usize ) -> Range {
117+ word_move ( slice, range, count, WordMotionTarget :: PrevWordEnd )
118+ }
119+
116120fn word_move ( slice : RopeSlice , range : Range , count : usize , target : WordMotionTarget ) -> Range {
117121 ( 0 ..count) . fold ( range, |range, _| {
118122 slice. chars_at ( range. head ) . range_to_target ( target, range)
@@ -159,6 +163,7 @@ pub enum WordMotionTarget {
159163 NextWordStart ,
160164 NextWordEnd ,
161165 PrevWordStart ,
166+ PrevWordEnd ,
162167 // A "Long word" (also known as a WORD in vim/kakoune) is strictly
163168 // delimited by whitespace, and can consist of punctuation as well
164169 // as alphanumerics.
@@ -181,7 +186,9 @@ impl CharHelpers for Chars<'_> {
181186 fn range_to_target ( & mut self , target : WordMotionTarget , origin : Range ) -> Range {
182187 // Characters are iterated forward or backwards depending on the motion direction.
183188 let characters: Box < dyn Iterator < Item = char > > = match target {
184- WordMotionTarget :: PrevWordStart | WordMotionTarget :: PrevLongWordStart => {
189+ WordMotionTarget :: PrevWordStart
190+ | WordMotionTarget :: PrevLongWordStart
191+ | WordMotionTarget :: PrevWordEnd => {
185192 self . next ( ) ;
186193 Box :: new ( from_fn ( || self . prev ( ) ) )
187194 }
@@ -190,9 +197,9 @@ impl CharHelpers for Chars<'_> {
190197
191198 // Index advancement also depends on the direction.
192199 let advance: & dyn Fn ( & mut usize ) = match target {
193- WordMotionTarget :: PrevWordStart | WordMotionTarget :: PrevLongWordStart => {
194- & |u| * u = u . saturating_sub ( 1 )
195- }
200+ WordMotionTarget :: PrevWordStart
201+ | WordMotionTarget :: PrevLongWordStart
202+ | WordMotionTarget :: PrevWordEnd => & |u| * u = u . saturating_sub ( 1 ) ,
196203 _ => & |u| * u += 1 ,
197204 } ;
198205
@@ -265,7 +272,7 @@ fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>
265272 } ;
266273
267274 match target {
268- WordMotionTarget :: NextWordStart => {
275+ WordMotionTarget :: NextWordStart | WordMotionTarget :: PrevWordEnd => {
269276 is_word_boundary ( peek, * next_peek)
270277 && ( char_is_line_ending ( * next_peek) || !next_peek. is_whitespace ( ) )
271278 }
@@ -913,6 +920,88 @@ mod test {
913920 }
914921 }
915922
923+ #[ test]
924+ fn test_behaviour_when_moving_to_end_of_previous_words ( ) {
925+ let tests = array:: IntoIter :: new ( [
926+ ( "Basic backward motion from the middle of a word" ,
927+ vec ! [ ( 1 , Range :: new( 9 , 9 ) , Range :: new( 9 , 5 ) ) ] ) ,
928+ ( "Starting from after boundary retreats the anchor" ,
929+ vec ! [ ( 1 , Range :: new( 0 , 13 ) , Range :: new( 12 , 8 ) ) ] ) ,
930+ ( "Jump to end of a word succeeded by whitespace" ,
931+ vec ! [ ( 1 , Range :: new( 10 , 10 ) , Range :: new( 10 , 4 ) ) ] ) ,
932+ ( " Jump to start of line from end of word preceded by whitespace" ,
933+ vec ! [ ( 1 , Range :: new( 7 , 7 ) , Range :: new( 7 , 0 ) ) ] ) ,
934+ ( "Previous anchor is irrelevant for backward motions" ,
935+ vec ! [ ( 1 , Range :: new( 26 , 12 ) , Range :: new( 12 , 8 ) ) ] ) ,
936+ ( " Starting from whitespace moves to first space in sequence" ,
937+ vec ! [ ( 1 , Range :: new( 0 , 3 ) , Range :: new( 3 , 0 ) ) ] ) ,
938+ ( "Test identifiers_with_underscores are considered a single word" ,
939+ vec ! [ ( 1 , Range :: new( 0 , 25 ) , Range :: new( 25 , 4 ) ) ] ) ,
940+ ( "Jumping\n \n back through a newline selects whitespace" ,
941+ vec ! [ ( 1 , Range :: new( 0 , 13 ) , Range :: new( 11 , 8 ) ) ] ) ,
942+ ( "Jumping to start of word from the end selects the whole word" ,
943+ vec ! [ ( 1 , Range :: new( 15 , 15 ) , Range :: new( 15 , 10 ) ) ] ) ,
944+ ( "alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion" ,
945+ vec ! [
946+ ( 1 , Range :: new( 30 , 30 ) , Range :: new( 30 , 21 ) ) ,
947+ ( 1 , Range :: new( 30 , 21 ) , Range :: new( 20 , 18 ) ) ,
948+ ( 1 , Range :: new( 20 , 18 ) , Range :: new( 17 , 15 ) )
949+ ] ) ,
950+
951+ ( "... ... punctuation and spaces behave as expected" ,
952+ vec ! [
953+ ( 1 , Range :: new( 0 , 10 ) , Range :: new( 9 , 9 ) ) ,
954+ ( 1 , Range :: new( 9 , 6 ) , Range :: new( 5 , 3 ) ) ,
955+ ] ) ,
956+ ( ".._.._ punctuation is not joined by underscores into a single block" ,
957+ vec ! [ ( 1 , Range :: new( 0 , 5 ) , Range :: new( 4 , 3 ) ) ] ) ,
958+ ( "Newlines\n \n are bridged seamlessly." ,
959+ vec ! [
960+ ( 1 , Range :: new( 0 , 10 ) , Range :: new( 7 , 0 ) ) ,
961+ ] ) ,
962+ ( "Jumping \n \n \n \n \n back from within a newline group selects previous block" ,
963+ vec ! [
964+ ( 1 , Range :: new( 0 , 13 ) , Range :: new( 10 , 7 ) ) ,
965+ ] ) ,
966+ ( "Failed motions do not modify the range" ,
967+ vec ! [
968+ ( 0 , Range :: new( 3 , 0 ) , Range :: new( 3 , 0 ) ) ,
969+ ] ) ,
970+ ( "Multiple motions at once resolve correctly" ,
971+ vec ! [
972+ ( 3 , Range :: new( 23 , 23 ) , Range :: new( 15 , 8 ) ) ,
973+ ] ) ,
974+ ( "Excessive motions are performed partially" ,
975+ vec ! [
976+ ( 999 , Range :: new( 40 , 40 ) , Range :: new( 8 , 0 ) ) ,
977+ ] ) ,
978+ ( "" , // Edge case of moving backwards in empty string
979+ vec ! [
980+ ( 1 , Range :: new( 0 , 0 ) , Range :: new( 0 , 0 ) ) ,
981+ ] ) ,
982+ ( "\n \n \n \n \n " , // Edge case of moving backwards in all newlines
983+ vec ! [
984+ ( 1 , Range :: new( 0 , 0 ) , Range :: new( 0 , 0 ) ) ,
985+ ] ) ,
986+ ( " \n \n Jumping back through alternated space blocks and newlines selects the space blocks" ,
987+ vec ! [
988+ ( 1 , Range :: new( 0 , 7 ) , Range :: new( 6 , 4 ) ) ,
989+ ( 1 , Range :: new( 6 , 4 ) , Range :: new( 2 , 0 ) ) ,
990+ ] ) ,
991+ ( "Test ヒーリクス multibyte characters behave as normal characters" ,
992+ vec ! [
993+ ( 1 , Range :: new( 0 , 9 ) , Range :: new( 9 , 4 ) ) ,
994+ ] ) ,
995+ ] ) ;
996+
997+ for ( sample, scenario) in tests {
998+ for ( count, begin, expected_end) in scenario. into_iter ( ) {
999+ let range = move_prev_word_end ( Rope :: from ( sample) . slice ( ..) , begin, count) ;
1000+ assert_eq ! ( range, expected_end, "Case failed: [{}]" , sample) ;
1001+ }
1002+ }
1003+ }
1004+
9161005 #[ test]
9171006 fn test_behaviour_when_moving_to_end_of_next_long_words ( ) {
9181007 let tests = array:: IntoIter :: new ( [
0 commit comments