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