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