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(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}