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