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