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