delete.rs

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