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