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