delete.rs

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