change.rs

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