change.rs

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