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    scroll::Autoscroll,
 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(window, cx, |vim, editor, window, 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(Some(Autoscroll::fit()), 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(Some(Autoscroll::fit()), window, cx, |s| {
 75                    s.move_with(|map, selection| {
 76                        let mut cursor = selection.head();
 77                        if kind.linewise() {
 78                            if let Some(column) = original_columns.get(&selection.id) {
 79                                *cursor.column_mut() = *column
 80                            }
 81                        }
 82                        cursor = map.clip_point(cursor, Bias::Left);
 83                        selection.collapse_to(cursor, selection.goal)
 84                    });
 85                });
 86                editor.refresh_inline_completion(true, false, window, cx);
 87            });
 88        });
 89    }
 90
 91    pub fn delete_object(
 92        &mut self,
 93        object: Object,
 94        around: bool,
 95        window: &mut Window,
 96        cx: &mut Context<Self>,
 97    ) {
 98        self.stop_recording(cx);
 99        self.update_editor(window, cx, |vim, editor, window, 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(Some(Autoscroll::fit()), window, cx, |s| {
106                    s.move_with(|map, selection| {
107                        object.expand_selection(map, selection, around);
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(Some(Autoscroll::fit()), 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_inline_completion(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_0(cx: &mut gpui::TestAppContext) {
356        let mut cx = NeovimBackedTestContext::new(cx).await;
357        cx.simulate(
358            "d 0",
359            indoc! {"
360            The qˇuick
361            brown fox"},
362        )
363        .await
364        .assert_matches();
365        cx.simulate(
366            "d 0",
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_k(cx: &mut gpui::TestAppContext) {
378        let mut cx = NeovimBackedTestContext::new(cx).await;
379        cx.simulate(
380            "d k",
381            indoc! {"
382            The quick
383            brown ˇfox
384            jumps over"},
385        )
386        .await
387        .assert_matches();
388        cx.simulate(
389            "d k",
390            indoc! {"
391            The quick
392            brown fox
393            jumps ˇover"},
394        )
395        .await
396        .assert_matches();
397        cx.simulate(
398            "d k",
399            indoc! {"
400            The qˇuick
401            brown fox
402            jumps over"},
403        )
404        .await
405        .assert_matches();
406        cx.simulate(
407            "d k",
408            indoc! {"
409            ˇbrown fox
410            jumps over"},
411        )
412        .await
413        .assert_matches();
414    }
415
416    #[gpui::test]
417    async fn test_delete_j(cx: &mut gpui::TestAppContext) {
418        let mut cx = NeovimBackedTestContext::new(cx).await;
419        cx.simulate(
420            "d j",
421            indoc! {"
422            The quick
423            brown ˇfox
424            jumps over"},
425        )
426        .await
427        .assert_matches();
428        cx.simulate(
429            "d j",
430            indoc! {"
431            The quick
432            brown fox
433            jumps ˇover"},
434        )
435        .await
436        .assert_matches();
437        cx.simulate(
438            "d j",
439            indoc! {"
440            The qˇuick
441            brown fox
442            jumps over"},
443        )
444        .await
445        .assert_matches();
446        cx.simulate(
447            "d j",
448            indoc! {"
449            The quick
450            brown fox
451            ˇ"},
452        )
453        .await
454        .assert_matches();
455    }
456
457    #[gpui::test]
458    async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
459        let mut cx = NeovimBackedTestContext::new(cx).await;
460        cx.simulate(
461            "d shift-g",
462            indoc! {"
463            The quick
464            brownˇ fox
465            jumps over
466            the lazy"},
467        )
468        .await
469        .assert_matches();
470        cx.simulate(
471            "d shift-g",
472            indoc! {"
473            The quick
474            brownˇ fox
475            jumps over
476            the lazy"},
477        )
478        .await
479        .assert_matches();
480        cx.simulate(
481            "d shift-g",
482            indoc! {"
483            The quick
484            brown fox
485            jumps over
486            the lˇazy"},
487        )
488        .await
489        .assert_matches();
490        cx.simulate(
491            "d shift-g",
492            indoc! {"
493            The quick
494            brown fox
495            jumps over
496            ˇ"},
497        )
498        .await
499        .assert_matches();
500    }
501
502    #[gpui::test]
503    async fn test_delete_to_line(cx: &mut gpui::TestAppContext) {
504        let mut cx = NeovimBackedTestContext::new(cx).await;
505        cx.simulate(
506            "d 3 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 3 shift-g",
517            indoc! {"
518            The quick
519            brown fox
520            jumps over
521            the lˇazy"},
522        )
523        .await
524        .assert_matches();
525        cx.simulate(
526            "d 2 shift-g",
527            indoc! {"
528            The quick
529            brown fox
530            jumps over
531            ˇ"},
532        )
533        .await
534        .assert_matches();
535    }
536
537    #[gpui::test]
538    async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
539        let mut cx = NeovimBackedTestContext::new(cx).await;
540        cx.simulate(
541            "d g g",
542            indoc! {"
543            The quick
544            brownˇ fox
545            jumps over
546            the lazy"},
547        )
548        .await
549        .assert_matches();
550        cx.simulate(
551            "d g g",
552            indoc! {"
553            The quick
554            brown fox
555            jumps over
556            the lˇazy"},
557        )
558        .await
559        .assert_matches();
560        cx.simulate(
561            "d g g",
562            indoc! {"
563            The qˇuick
564            brown fox
565            jumps over
566            the lazy"},
567        )
568        .await
569        .assert_matches();
570        cx.simulate(
571            "d g g",
572            indoc! {"
573            ˇ
574            brown fox
575            jumps over
576            the lazy"},
577        )
578        .await
579        .assert_matches();
580    }
581
582    #[gpui::test]
583    async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
584        let mut cx = VimTestContext::new(cx, true).await;
585        cx.set_state(
586            indoc! {"
587                The quick brown
588                fox juˇmps over
589                the lazy dog"},
590            Mode::Normal,
591        );
592
593        // Canceling operator twice reverts to normal mode with no active operator
594        cx.simulate_keystrokes("d escape k");
595        assert_eq!(cx.active_operator(), None);
596        assert_eq!(cx.mode(), Mode::Normal);
597        cx.assert_editor_state(indoc! {"
598            The quˇick brown
599            fox jumps over
600            the lazy dog"});
601    }
602
603    #[gpui::test]
604    async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
605        let mut cx = VimTestContext::new(cx, true).await;
606        cx.set_state(
607            indoc! {"
608                The quick brown
609                fox juˇmps over
610                the lazy dog"},
611            Mode::Normal,
612        );
613
614        // Canceling operator twice reverts to normal mode with no active operator
615        cx.simulate_keystrokes("d y");
616        assert_eq!(cx.active_operator(), None);
617        assert_eq!(cx.mode(), Mode::Normal);
618    }
619
620    #[gpui::test]
621    async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
622        let mut cx = NeovimBackedTestContext::new(cx).await;
623        cx.set_shared_state(indoc! {"
624                The ˇquick brown
625                fox jumps over
626                the lazy dog"})
627            .await;
628        cx.simulate_shared_keystrokes("d 2 d").await;
629        cx.shared_state().await.assert_eq(indoc! {"
630        the ˇlazy dog"});
631
632        cx.set_shared_state(indoc! {"
633                The ˇquick brown
634                fox jumps over
635                the lazy dog"})
636            .await;
637        cx.simulate_shared_keystrokes("2 d d").await;
638        cx.shared_state().await.assert_eq(indoc! {"
639        the ˇlazy dog"});
640
641        cx.set_shared_state(indoc! {"
642                The ˇquick brown
643                fox jumps over
644                the moon,
645                a star, and
646                the lazy dog"})
647            .await;
648        cx.simulate_shared_keystrokes("2 d 2 d").await;
649        cx.shared_state().await.assert_eq(indoc! {"
650        the ˇlazy dog"});
651    }
652
653    #[gpui::test]
654    async fn test_delete_to_adjacent_character(cx: &mut gpui::TestAppContext) {
655        let mut cx = NeovimBackedTestContext::new(cx).await;
656        cx.simulate("d t x", "ˇax").await.assert_matches();
657        cx.simulate("d t x", "aˇx").await.assert_matches();
658    }
659
660    #[gpui::test]
661    async fn test_delete_sentence(cx: &mut gpui::TestAppContext) {
662        let mut cx = NeovimBackedTestContext::new(cx).await;
663        // cx.simulate(
664        //     "d )",
665        //     indoc! {"
666        //     Fiˇrst. Second. Third.
667        //     Fourth.
668        //     "},
669        // )
670        // .await
671        // .assert_matches();
672
673        // cx.simulate(
674        //     "d )",
675        //     indoc! {"
676        //     First. Secˇond. Third.
677        //     Fourth.
678        //     "},
679        // )
680        // .await
681        // .assert_matches();
682
683        // // Two deletes
684        // cx.simulate(
685        //     "d ) d )",
686        //     indoc! {"
687        //     First. Second. Thirˇd.
688        //     Fourth.
689        //     "},
690        // )
691        // .await
692        // .assert_matches();
693
694        // Should delete whole line if done on first column
695        cx.simulate(
696            "d )",
697            indoc! {"
698            ˇFirst.
699            Fourth.
700            "},
701        )
702        .await
703        .assert_matches();
704
705        // Backwards it should also delete the whole first line
706        cx.simulate(
707            "d (",
708            indoc! {"
709            First.
710            ˇSecond.
711            Fourth.
712            "},
713        )
714        .await
715        .assert_matches();
716    }
717}