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