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