delete.rs

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