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