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