delete.rs

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