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