delete.rs

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