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