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