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