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