delete.rs

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