delete.rs

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