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