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