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