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