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