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