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 indoc::indoc;
218
219 use crate::{
220 state::Mode,
221 test::{NeovimBackedTestContext, VimTestContext},
222 };
223
224 #[gpui::test]
225 async fn test_delete_h(cx: &mut gpui::TestAppContext) {
226 let mut cx = NeovimBackedTestContext::new(cx).await;
227 cx.simulate("d h", "Teˇst").await.assert_matches();
228 cx.simulate("d h", "Tˇest").await.assert_matches();
229 cx.simulate("d h", "ˇTest").await.assert_matches();
230 cx.simulate(
231 "d h",
232 indoc! {"
233 Test
234 ˇtest"},
235 )
236 .await
237 .assert_matches();
238 }
239
240 #[gpui::test]
241 async fn test_delete_l(cx: &mut gpui::TestAppContext) {
242 let mut cx = NeovimBackedTestContext::new(cx).await;
243 cx.simulate("d l", "ˇTest").await.assert_matches();
244 cx.simulate("d l", "Teˇst").await.assert_matches();
245 cx.simulate("d l", "Tesˇt").await.assert_matches();
246 cx.simulate(
247 "d l",
248 indoc! {"
249 Tesˇt
250 test"},
251 )
252 .await
253 .assert_matches();
254 }
255
256 #[gpui::test]
257 async fn test_delete_w(cx: &mut gpui::TestAppContext) {
258 let mut cx = NeovimBackedTestContext::new(cx).await;
259 cx.simulate(
260 "d w",
261 indoc! {"
262 Test tesˇt
263 test"},
264 )
265 .await
266 .assert_matches();
267
268 cx.simulate("d w", "Teˇst").await.assert_matches();
269 cx.simulate("d w", "Tˇest test").await.assert_matches();
270 cx.simulate(
271 "d w",
272 indoc! {"
273 Test teˇst
274 test"},
275 )
276 .await
277 .assert_matches();
278 cx.simulate(
279 "d w",
280 indoc! {"
281 Test tesˇt
282 test"},
283 )
284 .await
285 .assert_matches();
286
287 cx.simulate(
288 "d w",
289 indoc! {"
290 Test test
291 ˇ
292 test"},
293 )
294 .await
295 .assert_matches();
296
297 cx.simulate("d shift-w", "Test teˇst-test test")
298 .await
299 .assert_matches();
300 }
301
302 #[gpui::test]
303 async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) {
304 let mut cx = NeovimBackedTestContext::new(cx).await;
305 cx.simulate("d e", "Teˇst Test\n").await.assert_matches();
306 cx.simulate("d e", "Tˇest test\n").await.assert_matches();
307 cx.simulate(
308 "d e",
309 indoc! {"
310 Test teˇst
311 test"},
312 )
313 .await
314 .assert_matches();
315 cx.simulate(
316 "d e",
317 indoc! {"
318 Test tesˇt
319 test"},
320 )
321 .await
322 .assert_matches();
323
324 cx.simulate("d e", "Test teˇst-test test")
325 .await
326 .assert_matches();
327 }
328
329 #[gpui::test]
330 async fn test_delete_b(cx: &mut gpui::TestAppContext) {
331 let mut cx = NeovimBackedTestContext::new(cx).await;
332 cx.simulate("d b", "Teˇst Test").await.assert_matches();
333 cx.simulate("d b", "Test ˇtest").await.assert_matches();
334 cx.simulate("d b", "Test1 test2 ˇtest3")
335 .await
336 .assert_matches();
337 cx.simulate(
338 "d b",
339 indoc! {"
340 Test test
341 ˇtest"},
342 )
343 .await
344 .assert_matches();
345 cx.simulate(
346 "d b",
347 indoc! {"
348 Test test
349 ˇ
350 test"},
351 )
352 .await
353 .assert_matches();
354
355 cx.simulate("d shift-b", "Test test-test ˇtest")
356 .await
357 .assert_matches();
358 }
359
360 #[gpui::test]
361 async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
362 let mut cx = NeovimBackedTestContext::new(cx).await;
363 cx.simulate(
364 "d $",
365 indoc! {"
366 The qˇuick
367 brown fox"},
368 )
369 .await
370 .assert_matches();
371 cx.simulate(
372 "d $",
373 indoc! {"
374 The quick
375 ˇ
376 brown fox"},
377 )
378 .await
379 .assert_matches();
380 }
381
382 #[gpui::test]
383 async fn test_delete_end_of_paragraph(cx: &mut gpui::TestAppContext) {
384 let mut cx = NeovimBackedTestContext::new(cx).await;
385 cx.simulate(
386 "d }",
387 indoc! {"
388 ˇhello world.
389
390 hello world."},
391 )
392 .await
393 .assert_matches();
394
395 cx.simulate(
396 "d }",
397 indoc! {"
398 ˇhello world.
399 hello world."},
400 )
401 .await
402 .assert_matches();
403 }
404
405 #[gpui::test]
406 async fn test_delete_0(cx: &mut gpui::TestAppContext) {
407 let mut cx = NeovimBackedTestContext::new(cx).await;
408 cx.simulate(
409 "d 0",
410 indoc! {"
411 The qˇuick
412 brown fox"},
413 )
414 .await
415 .assert_matches();
416 cx.simulate(
417 "d 0",
418 indoc! {"
419 The quick
420 ˇ
421 brown fox"},
422 )
423 .await
424 .assert_matches();
425 }
426
427 #[gpui::test]
428 async fn test_delete_k(cx: &mut gpui::TestAppContext) {
429 let mut cx = NeovimBackedTestContext::new(cx).await;
430 cx.simulate(
431 "d k",
432 indoc! {"
433 The quick
434 brown ˇfox
435 jumps over"},
436 )
437 .await
438 .assert_matches();
439 cx.simulate(
440 "d k",
441 indoc! {"
442 The quick
443 brown fox
444 jumps ˇover"},
445 )
446 .await
447 .assert_matches();
448 cx.simulate(
449 "d k",
450 indoc! {"
451 The qˇuick
452 brown fox
453 jumps over"},
454 )
455 .await
456 .assert_matches();
457 cx.simulate(
458 "d k",
459 indoc! {"
460 ˇbrown fox
461 jumps over"},
462 )
463 .await
464 .assert_matches();
465 }
466
467 #[gpui::test]
468 async fn test_delete_j(cx: &mut gpui::TestAppContext) {
469 let mut cx = NeovimBackedTestContext::new(cx).await;
470 cx.simulate(
471 "d j",
472 indoc! {"
473 The quick
474 brown ˇfox
475 jumps over"},
476 )
477 .await
478 .assert_matches();
479 cx.simulate(
480 "d j",
481 indoc! {"
482 The quick
483 brown fox
484 jumps ˇover"},
485 )
486 .await
487 .assert_matches();
488 cx.simulate(
489 "d j",
490 indoc! {"
491 The qˇuick
492 brown fox
493 jumps over"},
494 )
495 .await
496 .assert_matches();
497 cx.simulate(
498 "d j",
499 indoc! {"
500 The quick
501 brown fox
502 ˇ"},
503 )
504 .await
505 .assert_matches();
506 }
507
508 #[gpui::test]
509 async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
510 let mut cx = NeovimBackedTestContext::new(cx).await;
511 cx.simulate(
512 "d shift-g",
513 indoc! {"
514 The quick
515 brownˇ fox
516 jumps over
517 the lazy"},
518 )
519 .await
520 .assert_matches();
521 cx.simulate(
522 "d shift-g",
523 indoc! {"
524 The quick
525 brownˇ fox
526 jumps over
527 the lazy"},
528 )
529 .await
530 .assert_matches();
531 cx.simulate(
532 "d shift-g",
533 indoc! {"
534 The quick
535 brown fox
536 jumps over
537 the lˇazy"},
538 )
539 .await
540 .assert_matches();
541 cx.simulate(
542 "d shift-g",
543 indoc! {"
544 The quick
545 brown fox
546 jumps over
547 ˇ"},
548 )
549 .await
550 .assert_matches();
551 }
552
553 #[gpui::test]
554 async fn test_delete_to_line(cx: &mut gpui::TestAppContext) {
555 let mut cx = NeovimBackedTestContext::new(cx).await;
556 cx.simulate(
557 "d 3 shift-g",
558 indoc! {"
559 The quick
560 brownˇ fox
561 jumps over
562 the lazy"},
563 )
564 .await
565 .assert_matches();
566 cx.simulate(
567 "d 3 shift-g",
568 indoc! {"
569 The quick
570 brown fox
571 jumps over
572 the lˇazy"},
573 )
574 .await
575 .assert_matches();
576 cx.simulate(
577 "d 2 shift-g",
578 indoc! {"
579 The quick
580 brown fox
581 jumps over
582 ˇ"},
583 )
584 .await
585 .assert_matches();
586 }
587
588 #[gpui::test]
589 async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
590 let mut cx = NeovimBackedTestContext::new(cx).await;
591 cx.simulate(
592 "d g g",
593 indoc! {"
594 The quick
595 brownˇ fox
596 jumps over
597 the lazy"},
598 )
599 .await
600 .assert_matches();
601 cx.simulate(
602 "d g g",
603 indoc! {"
604 The quick
605 brown fox
606 jumps over
607 the lˇazy"},
608 )
609 .await
610 .assert_matches();
611 cx.simulate(
612 "d g g",
613 indoc! {"
614 The qˇuick
615 brown fox
616 jumps over
617 the lazy"},
618 )
619 .await
620 .assert_matches();
621 cx.simulate(
622 "d g g",
623 indoc! {"
624 ˇ
625 brown fox
626 jumps over
627 the lazy"},
628 )
629 .await
630 .assert_matches();
631 }
632
633 #[gpui::test]
634 async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
635 let mut cx = VimTestContext::new(cx, true).await;
636 cx.set_state(
637 indoc! {"
638 The quick brown
639 fox juˇmps over
640 the lazy dog"},
641 Mode::Normal,
642 );
643
644 // Canceling operator twice reverts to normal mode with no active operator
645 cx.simulate_keystrokes("d escape k");
646 assert_eq!(cx.active_operator(), None);
647 assert_eq!(cx.mode(), Mode::Normal);
648 cx.assert_editor_state(indoc! {"
649 The quˇick brown
650 fox jumps over
651 the lazy dog"});
652 }
653
654 #[gpui::test]
655 async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
656 let mut cx = VimTestContext::new(cx, true).await;
657 cx.set_state(
658 indoc! {"
659 The quick brown
660 fox juˇmps over
661 the lazy dog"},
662 Mode::Normal,
663 );
664
665 // Canceling operator twice reverts to normal mode with no active operator
666 cx.simulate_keystrokes("d y");
667 assert_eq!(cx.active_operator(), None);
668 assert_eq!(cx.mode(), Mode::Normal);
669 }
670
671 #[gpui::test]
672 async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
673 let mut cx = NeovimBackedTestContext::new(cx).await;
674 cx.set_shared_state(indoc! {"
675 The ˇquick brown
676 fox jumps over
677 the lazy dog"})
678 .await;
679 cx.simulate_shared_keystrokes("d 2 d").await;
680 cx.shared_state().await.assert_eq(indoc! {"
681 the ˇlazy dog"});
682
683 cx.set_shared_state(indoc! {"
684 The ˇquick brown
685 fox jumps over
686 the lazy dog"})
687 .await;
688 cx.simulate_shared_keystrokes("2 d d").await;
689 cx.shared_state().await.assert_eq(indoc! {"
690 the ˇlazy dog"});
691
692 cx.set_shared_state(indoc! {"
693 The ˇquick brown
694 fox jumps over
695 the moon,
696 a star, and
697 the lazy dog"})
698 .await;
699 cx.simulate_shared_keystrokes("2 d 2 d").await;
700 cx.shared_state().await.assert_eq(indoc! {"
701 the ˇlazy dog"});
702 }
703
704 #[gpui::test]
705 async fn test_delete_to_adjacent_character(cx: &mut gpui::TestAppContext) {
706 let mut cx = NeovimBackedTestContext::new(cx).await;
707 cx.simulate("d t x", "ˇax").await.assert_matches();
708 cx.simulate("d t x", "aˇx").await.assert_matches();
709 }
710
711 #[gpui::test]
712 async fn test_delete_sentence(cx: &mut gpui::TestAppContext) {
713 let mut cx = NeovimBackedTestContext::new(cx).await;
714 // cx.simulate(
715 // "d )",
716 // indoc! {"
717 // Fiˇrst. Second. Third.
718 // Fourth.
719 // "},
720 // )
721 // .await
722 // .assert_matches();
723
724 // cx.simulate(
725 // "d )",
726 // indoc! {"
727 // First. Secˇond. Third.
728 // Fourth.
729 // "},
730 // )
731 // .await
732 // .assert_matches();
733
734 // // Two deletes
735 // cx.simulate(
736 // "d ) d )",
737 // indoc! {"
738 // First. Second. Thirˇd.
739 // Fourth.
740 // "},
741 // )
742 // .await
743 // .assert_matches();
744
745 // Should delete whole line if done on first column
746 cx.simulate(
747 "d )",
748 indoc! {"
749 ˇFirst.
750 Fourth.
751 "},
752 )
753 .await
754 .assert_matches();
755
756 // Backwards it should also delete the whole first line
757 cx.simulate(
758 "d (",
759 indoc! {"
760 First.
761 ˇSecond.
762 Fourth.
763 "},
764 )
765 .await
766 .assert_matches();
767 }
768}