delete.rs

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