delete.rs

  1use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
  2use collections::{HashMap, HashSet};
  3use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
  4use gpui::MutableAppContext;
  5
  6pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
  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 MutableAppContext) {
 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::{state::Mode, test_contexts::VimTestContext};
100
101    #[gpui::test]
102    async fn test_delete_h(cx: &mut gpui::TestAppContext) {
103        let cx = VimTestContext::new(cx, true).await;
104        let mut cx = cx.binding(["d", "h"]);
105        cx.assert("Teˇst", "Tˇst");
106        cx.assert("Tˇest", "ˇest");
107        cx.assert("ˇTest", "ˇTest");
108        cx.assert(
109            indoc! {"
110                Test
111                ˇtest"},
112            indoc! {"
113                Test
114                ˇtest"},
115        );
116    }
117
118    #[gpui::test]
119    async fn test_delete_l(cx: &mut gpui::TestAppContext) {
120        let cx = VimTestContext::new(cx, true).await;
121        let mut cx = cx.binding(["d", "l"]);
122        cx.assert("ˇTest", "ˇest");
123        cx.assert("Teˇst", "Teˇt");
124        cx.assert("Tesˇt", "Teˇs");
125        cx.assert(
126            indoc! {"
127                Tesˇt
128                test"},
129            indoc! {"
130                Teˇs
131                test"},
132        );
133    }
134
135    #[gpui::test]
136    async fn test_delete_w(cx: &mut gpui::TestAppContext) {
137        let cx = VimTestContext::new(cx, true).await;
138        let mut cx = cx.binding(["d", "w"]);
139        cx.assert("Teˇst", "Tˇe");
140        cx.assert("Tˇest test", "Tˇtest");
141        cx.assert(
142            indoc! {"
143                Test teˇst
144                test"},
145            indoc! {"
146                Test tˇe
147                test"},
148        );
149        cx.assert(
150            indoc! {"
151                Test tesˇt
152                test"},
153            indoc! {"
154                Test teˇs
155                test"},
156        );
157        cx.assert(
158            indoc! {"
159                Test test
160                ˇ
161                test"},
162            indoc! {"
163                Test test
164                ˇ
165                test"},
166        );
167
168        let mut cx = cx.binding(["d", "shift-w"]);
169        cx.assert("Test teˇst-test test", "Test teˇtest");
170    }
171
172    #[gpui::test]
173    async fn test_delete_e(cx: &mut gpui::TestAppContext) {
174        let cx = VimTestContext::new(cx, true).await;
175        let mut cx = cx.binding(["d", "e"]);
176        cx.assert("Teˇst Test", "Teˇ Test");
177        cx.assert("Tˇest test", "Tˇ test");
178        cx.assert(
179            indoc! {"
180                Test teˇst
181                test"},
182            indoc! {"
183                Test tˇe
184                test"},
185        );
186        cx.assert(
187            indoc! {"
188                Test tesˇt
189                test"},
190            "Test teˇs",
191        );
192        cx.assert(
193            indoc! {"
194                Test test
195                ˇ
196                test"},
197            indoc! {"
198                Test test
199                ˇ"},
200        );
201
202        let mut cx = cx.binding(["d", "shift-e"]);
203        cx.assert("Test teˇst-test test", "Test teˇ test");
204    }
205
206    #[gpui::test]
207    async fn test_delete_b(cx: &mut gpui::TestAppContext) {
208        let cx = VimTestContext::new(cx, true).await;
209        let mut cx = cx.binding(["d", "b"]);
210        cx.assert("Teˇst Test", "ˇst Test");
211        cx.assert("Test ˇtest", "ˇtest");
212        cx.assert("Test1 test2 ˇtest3", "Test1 ˇtest3");
213        cx.assert(
214            indoc! {"
215                Test test
216                ˇtest"},
217            // Trailing whitespace after cursor
218            indoc! {"
219                Testˇ 
220                test"},
221        );
222        cx.assert(
223            indoc! {"
224                Test test
225                ˇ
226                test"},
227            // Trailing whitespace after cursor
228            indoc! {"
229                Testˇ 
230                
231                test"},
232        );
233
234        let mut cx = cx.binding(["d", "shift-b"]);
235        cx.assert("Test test-test ˇtest", "Test ˇtest");
236    }
237
238    #[gpui::test]
239    async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
240        let cx = VimTestContext::new(cx, true).await;
241        let mut cx = cx.binding(["d", "$"]);
242        cx.assert(
243            indoc! {"
244                The qˇuick
245                brown fox"},
246            indoc! {"
247                The ˇq
248                brown fox"},
249        );
250        cx.assert(
251            indoc! {"
252                The quick
253                ˇ
254                brown fox"},
255            indoc! {"
256                The quick
257                ˇ
258                brown fox"},
259        );
260    }
261
262    #[gpui::test]
263    async fn test_delete_0(cx: &mut gpui::TestAppContext) {
264        let cx = VimTestContext::new(cx, true).await;
265        let mut cx = cx.binding(["d", "0"]);
266        cx.assert(
267            indoc! {"
268                The qˇuick
269                brown fox"},
270            indoc! {"
271                ˇuick
272                brown fox"},
273        );
274        cx.assert(
275            indoc! {"
276                The quick
277                ˇ
278                brown fox"},
279            indoc! {"
280                The quick
281                ˇ
282                brown fox"},
283        );
284    }
285
286    #[gpui::test]
287    async fn test_delete_k(cx: &mut gpui::TestAppContext) {
288        let cx = VimTestContext::new(cx, true).await;
289        let mut cx = cx.binding(["d", "k"]);
290        cx.assert(
291            indoc! {"
292                The quick
293                brown ˇfox
294                jumps over"},
295            "jumps ˇover",
296        );
297        cx.assert(
298            indoc! {"
299                The quick
300                brown fox
301                jumps ˇover"},
302            "The quˇick",
303        );
304        cx.assert(
305            indoc! {"
306                The qˇuick
307                brown fox
308                jumps over"},
309            indoc! {"
310                brownˇ fox
311                jumps over"},
312        );
313        cx.assert(
314            indoc! {"
315                ˇbrown fox
316                jumps over"},
317            "ˇjumps over",
318        );
319    }
320
321    #[gpui::test]
322    async fn test_delete_j(cx: &mut gpui::TestAppContext) {
323        let cx = VimTestContext::new(cx, true).await;
324        let mut cx = cx.binding(["d", "j"]);
325        cx.assert(
326            indoc! {"
327                The quick
328                brown ˇfox
329                jumps over"},
330            "The quˇick",
331        );
332        cx.assert(
333            indoc! {"
334                The quick
335                brown fox
336                jumps ˇover"},
337            indoc! {"
338                The quick
339                brown ˇfox"},
340        );
341        cx.assert(
342            indoc! {"
343                The qˇuick
344                brown fox
345                jumps over"},
346            "jumpsˇ over",
347        );
348        cx.assert(
349            indoc! {"
350                The quick
351                brown fox
352                ˇ"},
353            indoc! {"
354                The quick
355                ˇbrown fox"},
356        );
357    }
358
359    #[gpui::test]
360    async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
361        let cx = VimTestContext::new(cx, true).await;
362        let mut cx = cx.binding(["d", "shift-g"]);
363        cx.assert(
364            indoc! {"
365                The quick
366                brownˇ fox
367                jumps over
368                the lazy"},
369            "The qˇuick",
370        );
371        cx.assert(
372            indoc! {"
373                The quick
374                brownˇ fox
375                jumps over
376                the lazy"},
377            "The qˇuick",
378        );
379        cx.assert(
380            indoc! {"
381                The quick
382                brown fox
383                jumps over
384                the lˇazy"},
385            indoc! {"
386                The quick
387                brown fox
388                jumpsˇ over"},
389        );
390        cx.assert(
391            indoc! {"
392                The quick
393                brown fox
394                jumps over
395                ˇ"},
396            indoc! {"
397                The quick
398                brown fox
399                ˇjumps over"},
400        );
401    }
402
403    #[gpui::test]
404    async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
405        let cx = VimTestContext::new(cx, true).await;
406        let mut cx = cx.binding(["d", "g", "g"]);
407        cx.assert(
408            indoc! {"
409                The quick
410                brownˇ fox
411                jumps over
412                the lazy"},
413            indoc! {"
414                jumpsˇ over
415                the lazy"},
416        );
417        cx.assert(
418            indoc! {"
419                The quick
420                brown fox
421                jumps over
422                the lˇazy"},
423            "ˇ",
424        );
425        cx.assert(
426            indoc! {"
427                The qˇuick
428                brown fox
429                jumps over
430                the lazy"},
431            indoc! {"
432                brownˇ fox
433                jumps over
434                the lazy"},
435        );
436        cx.assert(
437            indoc! {"
438                ˇ
439                brown fox
440                jumps over
441                the lazy"},
442            indoc! {"
443                ˇbrown fox
444                jumps over
445                the lazy"},
446        );
447    }
448
449    #[gpui::test]
450    async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
451        let mut cx = VimTestContext::new(cx, true).await;
452        cx.set_state(
453            indoc! {"
454                The quick brown
455                fox juˇmps over
456                the lazy dog"},
457            Mode::Normal,
458        );
459
460        // Canceling operator twice reverts to normal mode with no active operator
461        cx.simulate_keystrokes(["d", "escape", "k"]);
462        assert_eq!(cx.active_operator(), None);
463        assert_eq!(cx.mode(), Mode::Normal);
464        cx.assert_editor_state(indoc! {"
465            The quˇick brown
466            fox jumps over
467            the lazy dog"});
468    }
469
470    #[gpui::test]
471    async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
472        let mut cx = VimTestContext::new(cx, true).await;
473        cx.set_state(
474            indoc! {"
475                The quick brown
476                fox juˇmps over
477                the lazy dog"},
478            Mode::Normal,
479        );
480
481        // Canceling operator twice reverts to normal mode with no active operator
482        cx.simulate_keystrokes(["d", "y"]);
483        assert_eq!(cx.active_operator(), None);
484        assert_eq!(cx.mode(), Mode::Normal);
485    }
486}