1use crate::{
2 ObjectScope, Vim,
3 motion::{Motion, MotionKind},
4 object::Object,
5 state::Mode,
6};
7use collections::{HashMap, HashSet};
8use editor::{
9 Bias, DisplayPoint,
10 display_map::{DisplaySnapshot, ToDisplayPoint},
11};
12use gpui::{Context, Window};
13use language::{Point, Selection};
14use multi_buffer::MultiBufferRow;
15
16impl Vim {
17 pub fn delete_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 self.stop_recording(cx);
26 self.update_editor(cx, |vim, editor, cx| {
27 let text_layout_details = editor.text_layout_details(window);
28 editor.transact(window, cx, |editor, window, cx| {
29 editor.set_clip_at_line_ends(false, cx);
30 let mut original_columns: HashMap<_, _> = Default::default();
31 let mut motion_kind = None;
32 let mut ranges_to_copy = Vec::new();
33 editor.change_selections(Default::default(), window, cx, |s| {
34 s.move_with(|map, selection| {
35 let original_head = selection.head();
36 original_columns.insert(selection.id, original_head.column());
37 let kind = motion.expand_selection(
38 map,
39 selection,
40 times,
41 &text_layout_details,
42 forced_motion,
43 );
44 ranges_to_copy
45 .push(selection.start.to_point(map)..selection.end.to_point(map));
46
47 // When deleting line-wise, we always want to delete a newline.
48 // If there is one after the current line, it goes; otherwise we
49 // pick the one before.
50 if kind == Some(MotionKind::Linewise) {
51 let start = selection.start.to_point(map);
52 let end = selection.end.to_point(map);
53 if end.row < map.buffer_snapshot().max_point().row {
54 selection.end = Point::new(end.row + 1, 0).to_display_point(map)
55 } else if start.row > 0 {
56 selection.start = Point::new(
57 start.row - 1,
58 map.buffer_snapshot()
59 .line_len(MultiBufferRow(start.row - 1)),
60 )
61 .to_display_point(map)
62 }
63 }
64 if let Some(kind) = kind {
65 motion_kind.get_or_insert(kind);
66 }
67 });
68 });
69 let Some(kind) = motion_kind else { return };
70 vim.copy_ranges(editor, kind, false, ranges_to_copy, window, cx);
71 editor.insert("", window, cx);
72
73 // Fixup cursor position after the deletion
74 editor.set_clip_at_line_ends(true, cx);
75 editor.change_selections(Default::default(), window, cx, |s| {
76 s.move_with(|map, selection| {
77 let mut cursor = selection.head();
78 if kind.linewise()
79 && let Some(column) = original_columns.get(&selection.id)
80 {
81 *cursor.column_mut() = *column
82 }
83 cursor = map.clip_point(cursor, Bias::Left);
84 selection.collapse_to(cursor, selection.goal)
85 });
86 });
87 editor.refresh_edit_prediction(true, false, window, cx);
88 });
89 });
90 }
91
92 pub fn delete_object(
93 &mut self,
94 object: Object,
95 scope: ObjectScope,
96 times: Option<usize>,
97 window: &mut Window,
98 cx: &mut Context<Self>,
99 ) {
100 self.stop_recording(cx);
101 self.update_editor(cx, |vim, editor, cx| {
102 editor.transact(window, cx, |editor, window, cx| {
103 editor.set_clip_at_line_ends(false, cx);
104 // Emulates behavior in vim where if we expanded backwards to include a newline
105 // the cursor gets set back to the start of the line
106 let mut should_move_to_start: HashSet<_> = Default::default();
107
108 // Emulates behavior in vim where after deletion the cursor should try to move
109 // to the same column it was before deletion if the line is not empty or only
110 // contains whitespace
111 let mut column_before_move: HashMap<_, _> = Default::default();
112 let target_mode = object.target_visual_mode(vim.mode, &scope);
113
114 editor.change_selections(Default::default(), window, cx, |s| {
115 s.move_with(|map, selection| {
116 let cursor_point = selection.head().to_point(map);
117 if target_mode == Mode::VisualLine {
118 column_before_move.insert(selection.id, cursor_point.column);
119 }
120
121 object.expand_selection(map, selection, &scope, times);
122 let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
123 let mut move_selection_start_to_previous_line =
124 |map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {
125 let start = selection.start.to_offset(map, Bias::Left);
126 if selection.start.row().0 > 0 {
127 should_move_to_start.insert(selection.id);
128 selection.start =
129 (start - '\n'.len_utf8()).to_display_point(map);
130 }
131 };
132 let range = selection.start.to_offset(map, Bias::Left)
133 ..selection.end.to_offset(map, Bias::Right);
134 let contains_only_newlines = map
135 .buffer_chars_at(range.start)
136 .take_while(|(_, p)| p < &range.end)
137 .all(|(char, _)| char == '\n')
138 && !offset_range.is_empty();
139 let end_at_newline = map
140 .buffer_chars_at(range.end)
141 .next()
142 .map(|(c, _)| c == '\n')
143 .unwrap_or(false);
144
145 // If expanded range contains only newlines and
146 // the object is around or sentence, expand to include a newline
147 // at the end or start
148 if (matches!(scope, ObjectScope::AroundTrimmed | ObjectScope::Around)
149 || object == Object::Sentence)
150 && contains_only_newlines
151 {
152 if end_at_newline {
153 move_selection_end_to_next_line(map, selection);
154 } else {
155 move_selection_start_to_previous_line(map, selection);
156 }
157 }
158
159 // Does post-processing for the trailing newline and EOF
160 // when not cancelled.
161 let cancelled =
162 matches!(scope, ObjectScope::AroundTrimmed | ObjectScope::Around)
163 && selection.start == selection.end;
164 if object == Object::Paragraph && !cancelled {
165 // EOF check should be done before including a trailing newline.
166 if ends_at_eof(map, selection) {
167 move_selection_start_to_previous_line(map, selection);
168 }
169
170 if end_at_newline {
171 move_selection_end_to_next_line(map, selection);
172 }
173 }
174 });
175 });
176 vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
177 editor.insert("", window, cx);
178
179 // Fixup cursor position after the deletion
180 editor.set_clip_at_line_ends(true, cx);
181 editor.change_selections(Default::default(), window, cx, |s| {
182 s.move_with(|map, selection| {
183 let mut cursor = selection.head();
184 if should_move_to_start.contains(&selection.id) {
185 *cursor.column_mut() = 0;
186 } else if let Some(column) = column_before_move.get(&selection.id)
187 && *column > 0
188 {
189 let mut cursor_point = cursor.to_point(map);
190 cursor_point.column = *column;
191 cursor = map
192 .buffer_snapshot()
193 .clip_point(cursor_point, Bias::Left)
194 .to_display_point(map);
195 }
196 cursor = map.clip_point(cursor, Bias::Left);
197 selection.collapse_to(cursor, selection.goal)
198 });
199 });
200 editor.refresh_edit_prediction(true, false, window, cx);
201 });
202 });
203 }
204}
205
206fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
207 let end = selection.end.to_offset(map, Bias::Left);
208 selection.end = (end + '\n'.len_utf8()).to_display_point(map);
209}
210
211fn ends_at_eof(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) -> bool {
212 selection.end.to_point(map) == map.buffer_snapshot().max_point()
213}
214
215#[cfg(test)]
216mod test {
217 use gpui::KeyBinding;
218 use indoc::indoc;
219
220 use crate::{
221 PushObject,
222 state::Mode,
223 test::{NeovimBackedTestContext, VimTestContext},
224 };
225
226 #[gpui::test]
227 async fn test_delete_h(cx: &mut gpui::TestAppContext) {
228 let mut cx = NeovimBackedTestContext::new(cx).await;
229 cx.simulate("d h", "Teˇst").await.assert_matches();
230 cx.simulate("d h", "Tˇest").await.assert_matches();
231 cx.simulate("d h", "ˇTest").await.assert_matches();
232 cx.simulate(
233 "d h",
234 indoc! {"
235 Test
236 ˇtest"},
237 )
238 .await
239 .assert_matches();
240 }
241
242 #[gpui::test]
243 async fn test_delete_l(cx: &mut gpui::TestAppContext) {
244 let mut cx = NeovimBackedTestContext::new(cx).await;
245 cx.simulate("d l", "ˇTest").await.assert_matches();
246 cx.simulate("d l", "Teˇst").await.assert_matches();
247 cx.simulate("d l", "Tesˇt").await.assert_matches();
248 cx.simulate(
249 "d l",
250 indoc! {"
251 Tesˇt
252 test"},
253 )
254 .await
255 .assert_matches();
256 }
257
258 #[gpui::test]
259 async fn test_delete_w(cx: &mut gpui::TestAppContext) {
260 let mut cx = NeovimBackedTestContext::new(cx).await;
261 cx.simulate(
262 "d w",
263 indoc! {"
264 Test tesˇt
265 test"},
266 )
267 .await
268 .assert_matches();
269
270 cx.simulate("d w", "Teˇst").await.assert_matches();
271 cx.simulate("d w", "Tˇest test").await.assert_matches();
272 cx.simulate(
273 "d w",
274 indoc! {"
275 Test teˇst
276 test"},
277 )
278 .await
279 .assert_matches();
280 cx.simulate(
281 "d w",
282 indoc! {"
283 Test tesˇt
284 test"},
285 )
286 .await
287 .assert_matches();
288
289 cx.simulate(
290 "d w",
291 indoc! {"
292 Test test
293 ˇ
294 test"},
295 )
296 .await
297 .assert_matches();
298
299 cx.simulate("d shift-w", "Test teˇst-test test")
300 .await
301 .assert_matches();
302 }
303
304 #[gpui::test]
305 async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) {
306 let mut cx = NeovimBackedTestContext::new(cx).await;
307 cx.simulate("d e", "Teˇst Test\n").await.assert_matches();
308 cx.simulate("d e", "Tˇest test\n").await.assert_matches();
309 cx.simulate(
310 "d e",
311 indoc! {"
312 Test teˇst
313 test"},
314 )
315 .await
316 .assert_matches();
317 cx.simulate(
318 "d e",
319 indoc! {"
320 Test tesˇt
321 test"},
322 )
323 .await
324 .assert_matches();
325
326 cx.simulate("d e", "Test teˇst-test test")
327 .await
328 .assert_matches();
329 }
330
331 #[gpui::test]
332 async fn test_delete_b(cx: &mut gpui::TestAppContext) {
333 let mut cx = NeovimBackedTestContext::new(cx).await;
334 cx.simulate("d b", "Teˇst Test").await.assert_matches();
335 cx.simulate("d b", "Test ˇtest").await.assert_matches();
336 cx.simulate("d b", "Test1 test2 ˇtest3")
337 .await
338 .assert_matches();
339 cx.simulate(
340 "d b",
341 indoc! {"
342 Test test
343 ˇtest"},
344 )
345 .await
346 .assert_matches();
347 cx.simulate(
348 "d b",
349 indoc! {"
350 Test test
351 ˇ
352 test"},
353 )
354 .await
355 .assert_matches();
356
357 cx.simulate("d shift-b", "Test test-test ˇtest")
358 .await
359 .assert_matches();
360 }
361
362 #[gpui::test]
363 async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
364 let mut cx = NeovimBackedTestContext::new(cx).await;
365 cx.simulate(
366 "d $",
367 indoc! {"
368 The qˇuick
369 brown fox"},
370 )
371 .await
372 .assert_matches();
373 cx.simulate(
374 "d $",
375 indoc! {"
376 The quick
377 ˇ
378 brown fox"},
379 )
380 .await
381 .assert_matches();
382 }
383
384 #[gpui::test]
385 async fn test_delete_end_of_paragraph(cx: &mut gpui::TestAppContext) {
386 let mut cx = NeovimBackedTestContext::new(cx).await;
387 cx.simulate(
388 "d }",
389 indoc! {"
390 ˇhello world.
391
392 hello world."},
393 )
394 .await
395 .assert_matches();
396
397 cx.simulate(
398 "d }",
399 indoc! {"
400 ˇhello world.
401 hello world."},
402 )
403 .await
404 .assert_matches();
405 }
406
407 #[gpui::test]
408 async fn test_delete_0(cx: &mut gpui::TestAppContext) {
409 let mut cx = NeovimBackedTestContext::new(cx).await;
410 cx.simulate(
411 "d 0",
412 indoc! {"
413 The qˇuick
414 brown fox"},
415 )
416 .await
417 .assert_matches();
418 cx.simulate(
419 "d 0",
420 indoc! {"
421 The quick
422 ˇ
423 brown fox"},
424 )
425 .await
426 .assert_matches();
427 }
428
429 #[gpui::test]
430 async fn test_delete_k(cx: &mut gpui::TestAppContext) {
431 let mut cx = NeovimBackedTestContext::new(cx).await;
432 cx.simulate(
433 "d k",
434 indoc! {"
435 The quick
436 brown ˇfox
437 jumps over"},
438 )
439 .await
440 .assert_matches();
441 cx.simulate(
442 "d k",
443 indoc! {"
444 The quick
445 brown fox
446 jumps ˇover"},
447 )
448 .await
449 .assert_matches();
450 cx.simulate(
451 "d k",
452 indoc! {"
453 The qˇuick
454 brown fox
455 jumps over"},
456 )
457 .await
458 .assert_matches();
459 cx.simulate(
460 "d k",
461 indoc! {"
462 ˇbrown fox
463 jumps over"},
464 )
465 .await
466 .assert_matches();
467 }
468
469 #[gpui::test]
470 async fn test_delete_j(cx: &mut gpui::TestAppContext) {
471 let mut cx = NeovimBackedTestContext::new(cx).await;
472 cx.simulate(
473 "d j",
474 indoc! {"
475 The quick
476 brown ˇfox
477 jumps over"},
478 )
479 .await
480 .assert_matches();
481 cx.simulate(
482 "d j",
483 indoc! {"
484 The quick
485 brown fox
486 jumps ˇover"},
487 )
488 .await
489 .assert_matches();
490 cx.simulate(
491 "d j",
492 indoc! {"
493 The qˇuick
494 brown fox
495 jumps over"},
496 )
497 .await
498 .assert_matches();
499 cx.simulate(
500 "d j",
501 indoc! {"
502 The quick
503 brown fox
504 ˇ"},
505 )
506 .await
507 .assert_matches();
508 }
509
510 #[gpui::test]
511 async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
512 let mut cx = NeovimBackedTestContext::new(cx).await;
513 cx.simulate(
514 "d shift-g",
515 indoc! {"
516 The quick
517 brownˇ fox
518 jumps over
519 the lazy"},
520 )
521 .await
522 .assert_matches();
523 cx.simulate(
524 "d shift-g",
525 indoc! {"
526 The quick
527 brownˇ fox
528 jumps over
529 the lazy"},
530 )
531 .await
532 .assert_matches();
533 cx.simulate(
534 "d shift-g",
535 indoc! {"
536 The quick
537 brown fox
538 jumps over
539 the lˇazy"},
540 )
541 .await
542 .assert_matches();
543 cx.simulate(
544 "d shift-g",
545 indoc! {"
546 The quick
547 brown fox
548 jumps over
549 ˇ"},
550 )
551 .await
552 .assert_matches();
553 }
554
555 #[gpui::test]
556 async fn test_delete_to_line(cx: &mut gpui::TestAppContext) {
557 let mut cx = NeovimBackedTestContext::new(cx).await;
558 cx.simulate(
559 "d 3 shift-g",
560 indoc! {"
561 The quick
562 brownˇ fox
563 jumps over
564 the lazy"},
565 )
566 .await
567 .assert_matches();
568 cx.simulate(
569 "d 3 shift-g",
570 indoc! {"
571 The quick
572 brown fox
573 jumps over
574 the lˇazy"},
575 )
576 .await
577 .assert_matches();
578 cx.simulate(
579 "d 2 shift-g",
580 indoc! {"
581 The quick
582 brown fox
583 jumps over
584 ˇ"},
585 )
586 .await
587 .assert_matches();
588 }
589
590 #[gpui::test]
591 async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
592 let mut cx = NeovimBackedTestContext::new(cx).await;
593 cx.simulate(
594 "d g g",
595 indoc! {"
596 The quick
597 brownˇ fox
598 jumps over
599 the lazy"},
600 )
601 .await
602 .assert_matches();
603 cx.simulate(
604 "d g g",
605 indoc! {"
606 The quick
607 brown fox
608 jumps over
609 the lˇazy"},
610 )
611 .await
612 .assert_matches();
613 cx.simulate(
614 "d g g",
615 indoc! {"
616 The qˇuick
617 brown fox
618 jumps over
619 the lazy"},
620 )
621 .await
622 .assert_matches();
623 cx.simulate(
624 "d g g",
625 indoc! {"
626 ˇ
627 brown fox
628 jumps over
629 the lazy"},
630 )
631 .await
632 .assert_matches();
633 }
634
635 #[gpui::test]
636 async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
637 let mut cx = VimTestContext::new(cx, true).await;
638 cx.set_state(
639 indoc! {"
640 The quick brown
641 fox juˇmps over
642 the lazy dog"},
643 Mode::Normal,
644 );
645
646 // Canceling operator twice reverts to normal mode with no active operator
647 cx.simulate_keystrokes("d escape k");
648 assert_eq!(cx.active_operator(), None);
649 assert_eq!(cx.mode(), Mode::Normal);
650 cx.assert_editor_state(indoc! {"
651 The quˇick brown
652 fox jumps over
653 the lazy dog"});
654 }
655
656 #[gpui::test]
657 async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
658 let mut cx = VimTestContext::new(cx, true).await;
659 cx.set_state(
660 indoc! {"
661 The quick brown
662 fox juˇmps over
663 the lazy dog"},
664 Mode::Normal,
665 );
666
667 // Canceling operator twice reverts to normal mode with no active operator
668 cx.simulate_keystrokes("d y");
669 assert_eq!(cx.active_operator(), None);
670 assert_eq!(cx.mode(), Mode::Normal);
671 }
672
673 #[gpui::test]
674 async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
675 let mut cx = NeovimBackedTestContext::new(cx).await;
676 cx.set_shared_state(indoc! {"
677 The ˇquick brown
678 fox jumps over
679 the lazy dog"})
680 .await;
681 cx.simulate_shared_keystrokes("d 2 d").await;
682 cx.shared_state().await.assert_eq(indoc! {"
683 the ˇlazy dog"});
684
685 cx.set_shared_state(indoc! {"
686 The ˇquick brown
687 fox jumps over
688 the lazy dog"})
689 .await;
690 cx.simulate_shared_keystrokes("2 d d").await;
691 cx.shared_state().await.assert_eq(indoc! {"
692 the ˇlazy dog"});
693
694 cx.set_shared_state(indoc! {"
695 The ˇquick brown
696 fox jumps over
697 the moon,
698 a star, and
699 the lazy dog"})
700 .await;
701 cx.simulate_shared_keystrokes("2 d 2 d").await;
702 cx.shared_state().await.assert_eq(indoc! {"
703 the ˇlazy dog"});
704 }
705
706 #[gpui::test]
707 async fn test_delete_to_adjacent_character(cx: &mut gpui::TestAppContext) {
708 let mut cx = NeovimBackedTestContext::new(cx).await;
709 cx.simulate("d t x", "ˇax").await.assert_matches();
710 cx.simulate("d t x", "aˇx").await.assert_matches();
711 }
712
713 #[gpui::test]
714 async fn test_delete_sentence(cx: &mut gpui::TestAppContext) {
715 let mut cx = NeovimBackedTestContext::new(cx).await;
716 // cx.simulate(
717 // "d )",
718 // indoc! {"
719 // Fiˇrst. Second. Third.
720 // Fourth.
721 // "},
722 // )
723 // .await
724 // .assert_matches();
725
726 // cx.simulate(
727 // "d )",
728 // indoc! {"
729 // First. Secˇond. Third.
730 // Fourth.
731 // "},
732 // )
733 // .await
734 // .assert_matches();
735
736 // // Two deletes
737 // cx.simulate(
738 // "d ) d )",
739 // indoc! {"
740 // First. Second. Thirˇd.
741 // Fourth.
742 // "},
743 // )
744 // .await
745 // .assert_matches();
746
747 // Should delete whole line if done on first column
748 cx.simulate(
749 "d )",
750 indoc! {"
751 ˇFirst.
752 Fourth.
753 "},
754 )
755 .await
756 .assert_matches();
757
758 // Backwards it should also delete the whole first line
759 cx.simulate(
760 "d (",
761 indoc! {"
762 First.
763 ˇSecond.
764 Fourth.
765 "},
766 )
767 .await
768 .assert_matches();
769 }
770
771 #[gpui::test]
772 async fn test_delete_object_scope(cx: &mut gpui::TestAppContext) {
773 let mut cx = VimTestContext::new(cx, true).await;
774
775 cx.update(|_, cx| {
776 cx.bind_keys([KeyBinding::new(
777 "a",
778 PushObject {
779 around: true,
780 whitespace: false,
781 },
782 Some("VimControl && !menu"),
783 )]);
784 });
785
786 cx.set_state("some 'ˇquotes' here", Mode::Normal);
787 cx.simulate_keystrokes("d a '");
788 cx.assert_editor_state("some ˇ here");
789 }
790}