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