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