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