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