delete.rs

  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}