1use crate::{
2 Vim,
3 motion::{self, Motion, MotionKind},
4 object::Object,
5 state::Mode,
6};
7use editor::{
8 Bias, DisplayPoint,
9 display_map::{DisplaySnapshot, ToDisplayPoint},
10 movement::TextLayoutDetails,
11};
12use gpui::{Context, Window};
13use language::Selection;
14
15impl Vim {
16 pub fn change_motion(
17 &mut self,
18 motion: Motion,
19 times: Option<usize>,
20 forced_motion: bool,
21 window: &mut Window,
22 cx: &mut Context<Self>,
23 ) {
24 // Some motions ignore failure when switching to normal mode
25 let mut motion_kind = if matches!(
26 motion,
27 Motion::Left
28 | Motion::Right
29 | Motion::EndOfLine { .. }
30 | Motion::WrappingLeft
31 | Motion::StartOfLine { .. }
32 ) {
33 Some(MotionKind::Exclusive)
34 } else {
35 None
36 };
37 self.update_editor(window, cx, |vim, editor, window, cx| {
38 let text_layout_details = editor.text_layout_details(window);
39 editor.transact(window, cx, |editor, window, cx| {
40 // We are swapping to insert mode anyway. Just set the line end clipping behavior now
41 editor.set_clip_at_line_ends(false, cx);
42 editor.change_selections(Default::default(), window, cx, |s| {
43 s.move_with(|map, selection| {
44 let kind = match motion {
45 Motion::NextWordStart { ignore_punctuation }
46 | Motion::NextSubwordStart { ignore_punctuation } => {
47 expand_changed_word_selection(
48 map,
49 selection,
50 times,
51 ignore_punctuation,
52 &text_layout_details,
53 motion == Motion::NextSubwordStart { ignore_punctuation },
54 )
55 }
56 _ => {
57 let kind = motion.expand_selection(
58 map,
59 selection,
60 times,
61 &text_layout_details,
62 forced_motion,
63 );
64 if matches!(
65 motion,
66 Motion::CurrentLine | Motion::Down { .. } | Motion::Up { .. }
67 ) {
68 let mut start_offset =
69 selection.start.to_offset(map, Bias::Left);
70 let classifier = map
71 .buffer_snapshot
72 .char_classifier_at(selection.start.to_point(map));
73 for (ch, offset) in map.buffer_chars_at(start_offset) {
74 if ch == '\n' || !classifier.is_whitespace(ch) {
75 break;
76 }
77 start_offset = offset + ch.len_utf8();
78 }
79 selection.start = start_offset.to_display_point(map);
80 }
81 kind
82 }
83 };
84 if let Some(kind) = kind {
85 motion_kind.get_or_insert(kind);
86 }
87 });
88 });
89 if let Some(kind) = motion_kind {
90 vim.copy_selections_content(editor, kind, window, cx);
91 editor.insert("", window, cx);
92 editor.refresh_inline_completion(true, false, window, cx);
93 }
94 });
95 });
96
97 if motion_kind.is_some() {
98 self.switch_mode(Mode::Insert, false, window, cx)
99 } else {
100 self.switch_mode(Mode::Normal, false, window, cx)
101 }
102 }
103
104 pub fn change_object(
105 &mut self,
106 object: Object,
107 around: bool,
108 window: &mut Window,
109 cx: &mut Context<Self>,
110 ) {
111 let mut objects_found = false;
112 self.update_editor(window, cx, |vim, editor, window, cx| {
113 // We are swapping to insert mode anyway. Just set the line end clipping behavior now
114 editor.set_clip_at_line_ends(false, cx);
115 editor.transact(window, cx, |editor, window, cx| {
116 editor.change_selections(Default::default(), window, cx, |s| {
117 s.move_with(|map, selection| {
118 objects_found |= object.expand_selection(map, selection, around);
119 });
120 });
121 if objects_found {
122 vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
123 editor.insert("", window, cx);
124 editor.refresh_inline_completion(true, false, window, cx);
125 }
126 });
127 });
128
129 if objects_found {
130 self.switch_mode(Mode::Insert, false, window, cx);
131 } else {
132 self.switch_mode(Mode::Normal, false, window, cx);
133 }
134 }
135}
136
137// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
138// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
139// on a non-blank. This is because "cw" is interpreted as change-word, and a
140// word does not include the following white space. {Vi: "cw" when on a blank
141// followed by other blanks changes only the first blank; this is probably a
142// bug, because "dw" deletes all the blanks}
143fn expand_changed_word_selection(
144 map: &DisplaySnapshot,
145 selection: &mut Selection<DisplayPoint>,
146 times: Option<usize>,
147 ignore_punctuation: bool,
148 text_layout_details: &TextLayoutDetails,
149 use_subword: bool,
150) -> Option<MotionKind> {
151 let is_in_word = || {
152 let classifier = map
153 .buffer_snapshot
154 .char_classifier_at(selection.start.to_point(map));
155 let in_word = map
156 .buffer_chars_at(selection.head().to_offset(map, Bias::Left))
157 .next()
158 .map(|(c, _)| !classifier.is_whitespace(c))
159 .unwrap_or_default();
160 in_word
161 };
162 if (times.is_none() || times.unwrap() == 1) && is_in_word() {
163 let next_char = map
164 .buffer_chars_at(
165 motion::next_char(map, selection.end, false).to_offset(map, Bias::Left),
166 )
167 .next();
168 match next_char {
169 Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false),
170 _ => {
171 if use_subword {
172 selection.end =
173 motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
174 } else {
175 selection.end =
176 motion::next_word_end(map, selection.end, ignore_punctuation, 1, false);
177 }
178 selection.end = motion::next_char(map, selection.end, false);
179 }
180 }
181 Some(MotionKind::Inclusive)
182 } else {
183 let motion = if use_subword {
184 Motion::NextSubwordStart { ignore_punctuation }
185 } else {
186 Motion::NextWordStart { ignore_punctuation }
187 };
188 motion.expand_selection(map, selection, times, text_layout_details, false)
189 }
190}
191
192#[cfg(test)]
193mod test {
194 use indoc::indoc;
195
196 use crate::test::NeovimBackedTestContext;
197
198 #[gpui::test]
199 async fn test_change_h(cx: &mut gpui::TestAppContext) {
200 let mut cx = NeovimBackedTestContext::new(cx).await;
201 cx.simulate("c h", "Teˇst").await.assert_matches();
202 cx.simulate("c h", "Tˇest").await.assert_matches();
203 cx.simulate("c h", "ˇTest").await.assert_matches();
204 cx.simulate(
205 "c h",
206 indoc! {"
207 Test
208 ˇtest"},
209 )
210 .await
211 .assert_matches();
212 }
213
214 #[gpui::test]
215 async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
216 let mut cx = NeovimBackedTestContext::new(cx).await;
217 cx.simulate("c backspace", "Teˇst").await.assert_matches();
218 cx.simulate("c backspace", "Tˇest").await.assert_matches();
219 cx.simulate("c backspace", "ˇTest").await.assert_matches();
220 cx.simulate(
221 "c backspace",
222 indoc! {"
223 Test
224 ˇtest"},
225 )
226 .await
227 .assert_matches();
228 }
229
230 #[gpui::test]
231 async fn test_change_l(cx: &mut gpui::TestAppContext) {
232 let mut cx = NeovimBackedTestContext::new(cx).await;
233 cx.simulate("c l", "Teˇst").await.assert_matches();
234 cx.simulate("c l", "Tesˇt").await.assert_matches();
235 }
236
237 #[gpui::test]
238 async fn test_change_w(cx: &mut gpui::TestAppContext) {
239 let mut cx = NeovimBackedTestContext::new(cx).await;
240 cx.simulate("c w", "Teˇst").await.assert_matches();
241 cx.simulate("c w", "Tˇest test").await.assert_matches();
242 cx.simulate("c w", "Testˇ test").await.assert_matches();
243 cx.simulate("c w", "Tesˇt test").await.assert_matches();
244 cx.simulate(
245 "c w",
246 indoc! {"
247 Test teˇst
248 test"},
249 )
250 .await
251 .assert_matches();
252 cx.simulate(
253 "c w",
254 indoc! {"
255 Test tesˇt
256 test"},
257 )
258 .await
259 .assert_matches();
260 cx.simulate(
261 "c w",
262 indoc! {"
263 Test test
264 ˇ
265 test"},
266 )
267 .await
268 .assert_matches();
269
270 cx.simulate("c shift-w", "Test teˇst-test test")
271 .await
272 .assert_matches();
273 }
274
275 #[gpui::test]
276 async fn test_change_e(cx: &mut gpui::TestAppContext) {
277 let mut cx = NeovimBackedTestContext::new(cx).await;
278 cx.simulate("c e", "Teˇst Test").await.assert_matches();
279 cx.simulate("c e", "Tˇest test").await.assert_matches();
280 cx.simulate(
281 "c e",
282 indoc! {"
283 Test teˇst
284 test"},
285 )
286 .await
287 .assert_matches();
288 cx.simulate(
289 "c e",
290 indoc! {"
291 Test tesˇt
292 test"},
293 )
294 .await
295 .assert_matches();
296 cx.simulate(
297 "c e",
298 indoc! {"
299 Test test
300 ˇ
301 test"},
302 )
303 .await
304 .assert_matches();
305
306 cx.simulate("c shift-e", "Test teˇst-test test")
307 .await
308 .assert_matches();
309 }
310
311 #[gpui::test]
312 async fn test_change_b(cx: &mut gpui::TestAppContext) {
313 let mut cx = NeovimBackedTestContext::new(cx).await;
314 cx.simulate("c b", "Teˇst Test").await.assert_matches();
315 cx.simulate("c b", "Test ˇtest").await.assert_matches();
316 cx.simulate("c b", "Test1 test2 ˇtest3")
317 .await
318 .assert_matches();
319 cx.simulate(
320 "c b",
321 indoc! {"
322 Test test
323 ˇtest"},
324 )
325 .await
326 .assert_matches();
327 cx.simulate(
328 "c b",
329 indoc! {"
330 Test test
331 ˇ
332 test"},
333 )
334 .await
335 .assert_matches();
336
337 cx.simulate("c shift-b", "Test test-test ˇtest")
338 .await
339 .assert_matches();
340 }
341
342 #[gpui::test]
343 async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
344 let mut cx = NeovimBackedTestContext::new(cx).await;
345 cx.simulate(
346 "c $",
347 indoc! {"
348 The qˇuick
349 brown fox"},
350 )
351 .await
352 .assert_matches();
353 cx.simulate(
354 "c $",
355 indoc! {"
356 The quick
357 ˇ
358 brown fox"},
359 )
360 .await
361 .assert_matches();
362 }
363
364 #[gpui::test]
365 async fn test_change_0(cx: &mut gpui::TestAppContext) {
366 let mut cx = NeovimBackedTestContext::new(cx).await;
367
368 cx.simulate(
369 "c 0",
370 indoc! {"
371 The qˇuick
372 brown fox"},
373 )
374 .await
375 .assert_matches();
376 cx.simulate(
377 "c 0",
378 indoc! {"
379 The quick
380 ˇ
381 brown fox"},
382 )
383 .await
384 .assert_matches();
385 }
386
387 #[gpui::test]
388 async fn test_change_k(cx: &mut gpui::TestAppContext) {
389 let mut cx = NeovimBackedTestContext::new(cx).await;
390
391 cx.simulate(
392 "c k",
393 indoc! {"
394 The quick
395 brown ˇfox
396 jumps over"},
397 )
398 .await
399 .assert_matches();
400 cx.simulate(
401 "c k",
402 indoc! {"
403 The quick
404 brown fox
405 jumps ˇover"},
406 )
407 .await
408 .assert_matches();
409 cx.simulate(
410 "c k",
411 indoc! {"
412 The qˇuick
413 brown fox
414 jumps over"},
415 )
416 .await
417 .assert_matches();
418 cx.simulate(
419 "c k",
420 indoc! {"
421 ˇ
422 brown fox
423 jumps over"},
424 )
425 .await
426 .assert_matches();
427 cx.simulate(
428 "c k",
429 indoc! {"
430 The quick
431 brown fox
432 ˇjumps over"},
433 )
434 .await
435 .assert_matches();
436 }
437
438 #[gpui::test]
439 async fn test_change_j(cx: &mut gpui::TestAppContext) {
440 let mut cx = NeovimBackedTestContext::new(cx).await;
441 cx.simulate(
442 "c j",
443 indoc! {"
444 The quick
445 brown ˇfox
446 jumps over"},
447 )
448 .await
449 .assert_matches();
450 cx.simulate(
451 "c j",
452 indoc! {"
453 The quick
454 brown fox
455 jumps ˇover"},
456 )
457 .await
458 .assert_matches();
459 cx.simulate(
460 "c j",
461 indoc! {"
462 The qˇuick
463 brown fox
464 jumps over"},
465 )
466 .await
467 .assert_matches();
468 cx.simulate(
469 "c j",
470 indoc! {"
471 The quick
472 brown fox
473 ˇ"},
474 )
475 .await
476 .assert_matches();
477 cx.simulate(
478 "c j",
479 indoc! {"
480 The quick
481 ˇbrown fox
482 jumps over"},
483 )
484 .await
485 .assert_matches();
486 }
487
488 #[gpui::test]
489 async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
490 let mut cx = NeovimBackedTestContext::new(cx).await;
491 cx.simulate(
492 "c shift-g",
493 indoc! {"
494 The quick
495 brownˇ fox
496 jumps over
497 the lazy"},
498 )
499 .await
500 .assert_matches();
501 cx.simulate(
502 "c shift-g",
503 indoc! {"
504 The quick
505 brownˇ fox
506 jumps over
507 the lazy"},
508 )
509 .await
510 .assert_matches();
511 cx.simulate(
512 "c shift-g",
513 indoc! {"
514 The quick
515 brown fox
516 jumps over
517 the lˇazy"},
518 )
519 .await
520 .assert_matches();
521 cx.simulate(
522 "c shift-g",
523 indoc! {"
524 The quick
525 brown fox
526 jumps over
527 ˇ"},
528 )
529 .await
530 .assert_matches();
531 }
532
533 #[gpui::test]
534 async fn test_change_cc(cx: &mut gpui::TestAppContext) {
535 let mut cx = NeovimBackedTestContext::new(cx).await;
536 cx.simulate(
537 "c c",
538 indoc! {"
539 The quick
540 brownˇ fox
541 jumps over
542 the lazy"},
543 )
544 .await
545 .assert_matches();
546
547 cx.simulate(
548 "c c",
549 indoc! {"
550 ˇThe quick
551 brown fox
552 jumps over
553 the lazy"},
554 )
555 .await
556 .assert_matches();
557
558 cx.simulate(
559 "c c",
560 indoc! {"
561 The quick
562 broˇwn fox
563 jumps over
564 the lazy"},
565 )
566 .await
567 .assert_matches();
568 }
569
570 #[gpui::test]
571 async fn test_change_gg(cx: &mut gpui::TestAppContext) {
572 let mut cx = NeovimBackedTestContext::new(cx).await;
573 cx.simulate(
574 "c g g",
575 indoc! {"
576 The quick
577 brownˇ fox
578 jumps over
579 the lazy"},
580 )
581 .await
582 .assert_matches();
583 cx.simulate(
584 "c g g",
585 indoc! {"
586 The quick
587 brown fox
588 jumps over
589 the lˇazy"},
590 )
591 .await
592 .assert_matches();
593 cx.simulate(
594 "c g g",
595 indoc! {"
596 The qˇuick
597 brown fox
598 jumps over
599 the lazy"},
600 )
601 .await
602 .assert_matches();
603 cx.simulate(
604 "c g g",
605 indoc! {"
606 ˇ
607 brown fox
608 jumps over
609 the lazy"},
610 )
611 .await
612 .assert_matches();
613 }
614
615 #[gpui::test]
616 async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
617 let mut cx = NeovimBackedTestContext::new(cx).await;
618
619 for count in 1..=5 {
620 cx.simulate_at_each_offset(
621 &format!("c {count} j"),
622 indoc! {"
623 ˇThe quˇickˇ browˇn
624 ˇ
625 ˇfox ˇjumpsˇ-ˇoˇver
626 ˇthe lazy dog
627 "},
628 )
629 .await
630 .assert_matches();
631 }
632 }
633
634 #[gpui::test]
635 async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
636 let mut cx = NeovimBackedTestContext::new(cx).await;
637
638 for count in 1..=5 {
639 cx.simulate_at_each_offset(
640 &format!("c {count} l"),
641 indoc! {"
642 ˇThe quˇickˇ browˇn
643 ˇ
644 ˇfox ˇjumpsˇ-ˇoˇver
645 ˇthe lazy dog
646 "},
647 )
648 .await
649 .assert_matches();
650 }
651 }
652
653 #[gpui::test]
654 async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
655 let mut cx = NeovimBackedTestContext::new(cx).await;
656
657 for count in 1..=5 {
658 cx.simulate_at_each_offset(
659 &format!("c {count} b"),
660 indoc! {"
661 ˇThe quˇickˇ browˇn
662 ˇ
663 ˇfox ˇjumpsˇ-ˇoˇver
664 ˇthe lazy dog
665 "},
666 )
667 .await
668 .assert_matches()
669 }
670 }
671
672 #[gpui::test]
673 async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
674 let mut cx = NeovimBackedTestContext::new(cx).await;
675
676 for count in 1..=5 {
677 cx.simulate_at_each_offset(
678 &format!("c {count} e"),
679 indoc! {"
680 ˇThe quˇickˇ browˇn
681 ˇ
682 ˇfox ˇjumpsˇ-ˇoˇver
683 ˇthe lazy dog
684 "},
685 )
686 .await
687 .assert_matches();
688 }
689 }
690}