change.rs

  1use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
  2use editor::{
  3    char_kind,
  4    display_map::DisplaySnapshot,
  5    movement::{self, FindRange, TextLayoutDetails},
  6    scroll::autoscroll::Autoscroll,
  7    CharKind, DisplayPoint,
  8};
  9use gpui::WindowContext;
 10use language::Selection;
 11
 12pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
 13    // Some motions ignore failure when switching to normal mode
 14    let mut motion_succeeded = matches!(
 15        motion,
 16        Motion::Left
 17            | Motion::Right
 18            | Motion::EndOfLine { .. }
 19            | Motion::Backspace
 20            | Motion::StartOfLine { .. }
 21    );
 22    vim.update_active_editor(cx, |editor, cx| {
 23        let text_layout_details = editor.text_layout_details(cx);
 24        editor.transact(cx, |editor, cx| {
 25            // We are swapping to insert mode anyway. Just set the line end clipping behavior now
 26            editor.set_clip_at_line_ends(false, cx);
 27            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 28                s.move_with(|map, selection| {
 29                    motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
 30                    {
 31                        expand_changed_word_selection(
 32                            map,
 33                            selection,
 34                            times,
 35                            ignore_punctuation,
 36                            &text_layout_details,
 37                        )
 38                    } else {
 39                        motion.expand_selection(map, selection, times, false, &text_layout_details)
 40                    };
 41                });
 42            });
 43            copy_selections_content(editor, motion.linewise(), cx);
 44            editor.insert("", cx);
 45        });
 46    });
 47
 48    if motion_succeeded {
 49        vim.switch_mode(Mode::Insert, false, cx)
 50    } else {
 51        vim.switch_mode(Mode::Normal, false, cx)
 52    }
 53}
 54
 55pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
 56    let mut objects_found = false;
 57    vim.update_active_editor(cx, |editor, cx| {
 58        // We are swapping to insert mode anyway. Just set the line end clipping behavior now
 59        editor.set_clip_at_line_ends(false, cx);
 60        editor.transact(cx, |editor, cx| {
 61            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 62                s.move_with(|map, selection| {
 63                    objects_found |= object.expand_selection(map, selection, around);
 64                });
 65            });
 66            if objects_found {
 67                copy_selections_content(editor, false, cx);
 68                editor.insert("", cx);
 69            }
 70        });
 71    });
 72
 73    if objects_found {
 74        vim.switch_mode(Mode::Insert, false, cx);
 75    } else {
 76        vim.switch_mode(Mode::Normal, false, cx);
 77    }
 78}
 79
 80// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
 81// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
 82// on a non-blank.  This is because "cw" is interpreted as change-word, and a
 83// word does not include the following white space.  {Vi: "cw" when on a blank
 84//     followed by other blanks changes only the first blank; this is probably a
 85//     bug, because "dw" deletes all the blanks}
 86fn expand_changed_word_selection(
 87    map: &DisplaySnapshot,
 88    selection: &mut Selection<DisplayPoint>,
 89    times: Option<usize>,
 90    ignore_punctuation: bool,
 91    text_layout_details: &TextLayoutDetails,
 92) -> bool {
 93    if times.is_none() || times.unwrap() == 1 {
 94        let scope = map
 95            .buffer_snapshot
 96            .language_scope_at(selection.start.to_point(map));
 97        let in_word = map
 98            .chars_at(selection.head())
 99            .next()
100            .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
101            .unwrap_or_default();
102
103        if in_word {
104            selection.end =
105                movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
106                    let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
107                    let right_kind =
108                        char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
109
110                    left_kind != right_kind && left_kind != CharKind::Whitespace
111                });
112            true
113        } else {
114            Motion::NextWordStart { ignore_punctuation }.expand_selection(
115                map,
116                selection,
117                None,
118                false,
119                &text_layout_details,
120            )
121        }
122    } else {
123        Motion::NextWordStart { ignore_punctuation }.expand_selection(
124            map,
125            selection,
126            times,
127            false,
128            &text_layout_details,
129        )
130    }
131}
132
133// #[cfg(test)]
134// mod test {
135//     use indoc::indoc;
136
137//     use crate::test::NeovimBackedTestContext;
138
139//     #[gpui::test]
140//     async fn test_change_h(cx: &mut gpui::TestAppContext) {
141//         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "h"]);
142//         cx.assert("Teˇst").await;
143//         cx.assert("Tˇest").await;
144//         cx.assert("ˇTest").await;
145//         cx.assert(indoc! {"
146//             Test
147//             ˇtest"})
148//             .await;
149//     }
150
151//     #[gpui::test]
152//     async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
153//         let mut cx = NeovimBackedTestContext::new(cx)
154//             .await
155//             .binding(["c", "backspace"]);
156//         cx.assert("Teˇst").await;
157//         cx.assert("Tˇest").await;
158//         cx.assert("ˇTest").await;
159//         cx.assert(indoc! {"
160//             Test
161//             ˇtest"})
162//             .await;
163//     }
164
165//     #[gpui::test]
166//     async fn test_change_l(cx: &mut gpui::TestAppContext) {
167//         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "l"]);
168//         cx.assert("Teˇst").await;
169//         cx.assert("Tesˇt").await;
170//     }
171
172//     #[gpui::test]
173//     async fn test_change_w(cx: &mut gpui::TestAppContext) {
174//         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "w"]);
175//         cx.assert("Teˇst").await;
176//         cx.assert("Tˇest test").await;
177//         cx.assert("Testˇ  test").await;
178//         cx.assert(indoc! {"
179//                 Test teˇst
180//                 test"})
181//             .await;
182//         cx.assert(indoc! {"
183//                 Test tesˇt
184//                 test"})
185//             .await;
186//         cx.assert(indoc! {"
187//                 Test test
188//                 ˇ
189//                 test"})
190//             .await;
191
192//         let mut cx = cx.binding(["c", "shift-w"]);
193//         cx.assert("Test teˇst-test test").await;
194//     }
195
196//     #[gpui::test]
197//     async fn test_change_e(cx: &mut gpui::TestAppContext) {
198//         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "e"]);
199//         cx.assert("Teˇst Test").await;
200//         cx.assert("Tˇest test").await;
201//         cx.assert(indoc! {"
202//                 Test teˇst
203//                 test"})
204//             .await;
205//         cx.assert(indoc! {"
206//                 Test tesˇt
207//                 test"})
208//             .await;
209//         cx.assert(indoc! {"
210//                 Test test
211//                 ˇ
212//                 test"})
213//             .await;
214
215//         let mut cx = cx.binding(["c", "shift-e"]);
216//         cx.assert("Test teˇst-test test").await;
217//     }
218
219//     #[gpui::test]
220//     async fn test_change_b(cx: &mut gpui::TestAppContext) {
221//         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "b"]);
222//         cx.assert("Teˇst Test").await;
223//         cx.assert("Test ˇtest").await;
224//         cx.assert("Test1 test2 ˇtest3").await;
225//         cx.assert(indoc! {"
226//                 Test test
227//                 ˇtest"})
228//             .await;
229//         cx.assert(indoc! {"
230//                 Test test
231//                 ˇ
232//                 test"})
233//             .await;
234
235//         let mut cx = cx.binding(["c", "shift-b"]);
236//         cx.assert("Test test-test ˇtest").await;
237//     }
238
239//     #[gpui::test]
240//     async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
241//         let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "$"]);
242//         cx.assert(indoc! {"
243//             The qˇuick
244//             brown fox"})
245//             .await;
246//         cx.assert(indoc! {"
247//             The quick
248//             ˇ
249//             brown fox"})
250//             .await;
251//     }
252
253//     #[gpui::test]
254//     async fn test_change_0(cx: &mut gpui::TestAppContext) {
255//         let mut cx = NeovimBackedTestContext::new(cx).await;
256
257//         cx.assert_neovim_compatible(
258//             indoc! {"
259//             The qˇuick
260//             brown fox"},
261//             ["c", "0"],
262//         )
263//         .await;
264//         cx.assert_neovim_compatible(
265//             indoc! {"
266//             The quick
267//             ˇ
268//             brown fox"},
269//             ["c", "0"],
270//         )
271//         .await;
272//     }
273
274//     #[gpui::test]
275//     async fn test_change_k(cx: &mut gpui::TestAppContext) {
276//         let mut cx = NeovimBackedTestContext::new(cx).await;
277
278//         cx.assert_neovim_compatible(
279//             indoc! {"
280//             The quick
281//             brown ˇfox
282//             jumps over"},
283//             ["c", "k"],
284//         )
285//         .await;
286//         cx.assert_neovim_compatible(
287//             indoc! {"
288//             The quick
289//             brown fox
290//             jumps ˇover"},
291//             ["c", "k"],
292//         )
293//         .await;
294//         cx.assert_neovim_compatible(
295//             indoc! {"
296//             The qˇuick
297//             brown fox
298//             jumps over"},
299//             ["c", "k"],
300//         )
301//         .await;
302//         cx.assert_neovim_compatible(
303//             indoc! {"
304//             ˇ
305//             brown fox
306//             jumps over"},
307//             ["c", "k"],
308//         )
309//         .await;
310//     }
311
312//     #[gpui::test]
313//     async fn test_change_j(cx: &mut gpui::TestAppContext) {
314//         let mut cx = NeovimBackedTestContext::new(cx).await;
315//         cx.assert_neovim_compatible(
316//             indoc! {"
317//             The quick
318//             brown ˇfox
319//             jumps over"},
320//             ["c", "j"],
321//         )
322//         .await;
323//         cx.assert_neovim_compatible(
324//             indoc! {"
325//             The quick
326//             brown fox
327//             jumps ˇover"},
328//             ["c", "j"],
329//         )
330//         .await;
331//         cx.assert_neovim_compatible(
332//             indoc! {"
333//             The qˇuick
334//             brown fox
335//             jumps over"},
336//             ["c", "j"],
337//         )
338//         .await;
339//         cx.assert_neovim_compatible(
340//             indoc! {"
341//             The quick
342//             brown fox
343//             ˇ"},
344//             ["c", "j"],
345//         )
346//         .await;
347//     }
348
349//     #[gpui::test]
350//     async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
351//         let mut cx = NeovimBackedTestContext::new(cx).await;
352//         cx.assert_neovim_compatible(
353//             indoc! {"
354//             The quick
355//             brownˇ fox
356//             jumps over
357//             the lazy"},
358//             ["c", "shift-g"],
359//         )
360//         .await;
361//         cx.assert_neovim_compatible(
362//             indoc! {"
363//             The quick
364//             brownˇ fox
365//             jumps over
366//             the lazy"},
367//             ["c", "shift-g"],
368//         )
369//         .await;
370//         cx.assert_neovim_compatible(
371//             indoc! {"
372//             The quick
373//             brown fox
374//             jumps over
375//             the lˇazy"},
376//             ["c", "shift-g"],
377//         )
378//         .await;
379//         cx.assert_neovim_compatible(
380//             indoc! {"
381//             The quick
382//             brown fox
383//             jumps over
384//             ˇ"},
385//             ["c", "shift-g"],
386//         )
387//         .await;
388//     }
389
390//     #[gpui::test]
391//     async fn test_change_gg(cx: &mut gpui::TestAppContext) {
392//         let mut cx = NeovimBackedTestContext::new(cx).await;
393//         cx.assert_neovim_compatible(
394//             indoc! {"
395//             The quick
396//             brownˇ fox
397//             jumps over
398//             the lazy"},
399//             ["c", "g", "g"],
400//         )
401//         .await;
402//         cx.assert_neovim_compatible(
403//             indoc! {"
404//             The quick
405//             brown fox
406//             jumps over
407//             the lˇazy"},
408//             ["c", "g", "g"],
409//         )
410//         .await;
411//         cx.assert_neovim_compatible(
412//             indoc! {"
413//             The qˇuick
414//             brown fox
415//             jumps over
416//             the lazy"},
417//             ["c", "g", "g"],
418//         )
419//         .await;
420//         cx.assert_neovim_compatible(
421//             indoc! {"
422//             ˇ
423//             brown fox
424//             jumps over
425//             the lazy"},
426//             ["c", "g", "g"],
427//         )
428//         .await;
429//     }
430
431//     #[gpui::test]
432//     async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
433//         let mut cx = NeovimBackedTestContext::new(cx).await;
434
435//         for count in 1..=5 {
436//             cx.assert_binding_matches_all(
437//                 ["c", &count.to_string(), "j"],
438//                 indoc! {"
439//                     ˇThe quˇickˇ browˇn
440//                     ˇ
441//                     ˇfox ˇjumpsˇ-ˇoˇver
442//                     ˇthe lazy dog
443//                     "},
444//             )
445//             .await;
446//         }
447//     }
448
449//     #[gpui::test]
450//     async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
451//         let mut cx = NeovimBackedTestContext::new(cx).await;
452
453//         for count in 1..=5 {
454//             cx.assert_binding_matches_all(
455//                 ["c", &count.to_string(), "l"],
456//                 indoc! {"
457//                     ˇThe quˇickˇ browˇn
458//                     ˇ
459//                     ˇfox ˇjumpsˇ-ˇoˇver
460//                     ˇthe lazy dog
461//                     "},
462//             )
463//             .await;
464//         }
465//     }
466
467//     #[gpui::test]
468//     async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
469//         let mut cx = NeovimBackedTestContext::new(cx).await;
470
471//         for count in 1..=5 {
472//             for marked_text in cx.each_marked_position(indoc! {"
473//                 ˇThe quˇickˇ browˇn
474//                 ˇ
475//                 ˇfox ˇjumpsˇ-ˇoˇver
476//                 ˇthe lazy dog
477//                 "})
478//             {
479//                 cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
480//                     .await;
481//             }
482//         }
483//     }
484
485//     #[gpui::test]
486//     async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
487//         let mut cx = NeovimBackedTestContext::new(cx).await;
488
489//         for count in 1..=5 {
490//             cx.assert_binding_matches_all(
491//                 ["c", &count.to_string(), "e"],
492//                 indoc! {"
493//                     ˇThe quˇickˇ browˇn
494//                     ˇ
495//                     ˇfox ˇjumpsˇ-ˇoˇver
496//                     ˇthe lazy dog
497//                     "},
498//             )
499//             .await;
500//         }
501//     }
502// }