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