@@ -104,6 +104,18 @@ pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Ran
104104 word_move(slice, range, count, WordMotionTarget::PrevWordStart)
105105}
106106
107+pub fn move_next_long_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
108+ word_move(slice, range, count, WordMotionTarget::NextLongWordStart)
109+}
110+
111+pub fn move_next_long_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
112+ word_move(slice, range, count, WordMotionTarget::NextLongWordEnd)
113+}
114+
115+pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
116+ word_move(slice, range, count, WordMotionTarget::PrevLongWordStart)
117+}
118+
107119fn word_move(slice: RopeSlice, mut range: Range, count: usize, target: WordMotionTarget) -> Range {
108120 (0..count).fold(range, |range, _| {
109121 slice.chars_at(range.head).range_to_target(target, range)
@@ -150,6 +162,12 @@ pub enum WordMotionTarget {
150162 NextWordStart,
151163 NextWordEnd,
152164 PrevWordStart,
165+ // A "Long word" (also known as a WORD in vim/kakoune) is strictly
166+ // delimited by whitespace, and can consist of punctuation as well
167+ // as alphanumerics.
168+ NextLongWordStart,
169+ NextLongWordEnd,
170+ PrevLongWordStart,
153171}
154172
155173pub trait CharHelpers {
@@ -167,7 +185,7 @@ impl CharHelpers for Chars<'_> {
167185 let range = origin;
168186 // Characters are iterated forward or backwards depending on the motion direction.
169187 let characters: Box<dyn Iterator<Item = char>> = match target {
170− WordMotionTarget::PrevWordStart => {
188+ WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart => {
171189 self.next();
172190 Box::new(from_fn(|| self.prev()))
173191 }
@@ -176,7 +194,7 @@ impl CharHelpers for Chars<'_> {
176194
177195 // Index advancement also depends on the direction.
178196 let advance: &dyn Fn(&mut usize) = match target {
179− WordMotionTarget::PrevWordStart => &|u| *u = u.saturating_sub(1),
197+ WordMotionTarget::PrevWordStart | WordMotionTarget::PrevLongWordStart => &|u| *u = u.saturating_sub(1),
180198 _ => &|u| *u += 1,
181199 };
182200
@@ -229,6 +247,19 @@ impl CharHelpers for Chars<'_> {
229247 }
230248}
231249
250+fn is_word_boundary(a: char, b: char) -> bool {
251+ categorize_char(a) != categorize_char(b)
252+}
253+
254+fn is_long_word_boundary(a: char, b: char) -> bool {
255+ match (categorize_char(a), categorize_char(b)) {
256+ (CharCategory::Word, CharCategory::Punctuation)
257+ | (CharCategory::Punctuation, CharCategory::Word) => false,
258+ (a, b) if a != b => true,
259+ _ => false
260+ }
261+}
262+
232263fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>) -> bool {
233264 let next_peek = match next_peek {
234265 Some(next_peek) => next_peek,
@@ -237,11 +268,19 @@ fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>
237268
238269 match target {
239270 WordMotionTarget::NextWordStart => {
240− ((categorize_char(peek) != categorize_char(*next_peek))
271+ (is_word_boundary(peek, *next_peek)
241272 && (char_is_line_ending(*next_peek) || !next_peek.is_whitespace()))
242273 }
243274 WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
244− ((categorize_char(peek) != categorize_char(*next_peek))
275+ (is_word_boundary(peek, *next_peek)
276+ && (!peek.is_whitespace() || char_is_line_ending(*next_peek)))
277+ }
278+ WordMotionTarget::NextLongWordStart => {
279+ (is_long_word_boundary(peek, *next_peek)
280+ && (char_is_line_ending(*next_peek) || !next_peek.is_whitespace()))
281+ }
282+ WordMotionTarget::NextLongWordEnd | WordMotionTarget::PrevLongWordStart => {
283+ (is_long_word_boundary(peek, *next_peek)
245284 && (!peek.is_whitespace() || char_is_line_ending(*next_peek)))
246285 }
247286 }