change.rs

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