change.rs

  1use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
  2use editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
  3use gpui::MutableAppContext;
  4use language::Selection;
  5
  6pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
  7    vim.update_active_editor(cx, |editor, cx| {
  8        editor.transact(cx, |editor, cx| {
  9            // We are swapping to insert mode anyway. Just set the line end clipping behavior now
 10            editor.set_clip_at_line_ends(false, cx);
 11            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
 12                s.move_with(|map, selection| {
 13                    if let Motion::NextWordStart { ignore_punctuation } = motion {
 14                        expand_changed_word_selection(map, selection, times, ignore_punctuation);
 15                    } else {
 16                        motion.expand_selection(map, selection, times, false);
 17                    }
 18                });
 19            });
 20            copy_selections_content(editor, motion.linewise(), cx);
 21            editor.insert("", cx);
 22        });
 23    });
 24    vim.switch_mode(Mode::Insert, false, cx)
 25}
 26
 27pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
 28    let mut objects_found = false;
 29    vim.update_active_editor(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.transact(cx, |editor, cx| {
 33            editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
 34                s.move_with(|map, selection| {
 35                    objects_found |= object.expand_selection(map, selection, around);
 36                });
 37            });
 38            if objects_found {
 39                copy_selections_content(editor, false, cx);
 40                editor.insert("", cx);
 41            }
 42        });
 43    });
 44
 45    if objects_found {
 46        vim.switch_mode(Mode::Insert, false, cx);
 47    } else {
 48        vim.switch_mode(Mode::Normal, false, cx);
 49    }
 50}
 51
 52// From the docs https://vimhelp.org/change.txt.html#cw
 53// Special case: When the cursor is in a word, "cw" and "cW" do not include the
 54// white space after a word, they only change up to the end of the word. This is
 55// because Vim interprets "cw" as change-word, and a word does not include the
 56// following white space.
 57fn expand_changed_word_selection(
 58    map: &DisplaySnapshot,
 59    selection: &mut Selection<DisplayPoint>,
 60    times: usize,
 61    ignore_punctuation: bool,
 62) {
 63    if times > 1 {
 64        Motion::NextWordStart { ignore_punctuation }.expand_selection(
 65            map,
 66            selection,
 67            times - 1,
 68            false,
 69        );
 70    }
 71
 72    if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
 73        return;
 74    }
 75
 76    selection.end = movement::find_boundary(map, selection.end, |left, right| {
 77        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
 78        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
 79
 80        left_kind != right_kind || left == '\n' || right == '\n'
 81    });
 82}
 83
 84#[cfg(test)]
 85mod test {
 86    use indoc::indoc;
 87
 88    use crate::{
 89        state::Mode,
 90        test::{NeovimBackedTestContext, VimTestContext},
 91    };
 92
 93    #[gpui::test]
 94    async fn test_change_h(cx: &mut gpui::TestAppContext) {
 95        let cx = VimTestContext::new(cx, true).await;
 96        let mut cx = cx.binding(["c", "h"]).mode_after(Mode::Insert);
 97        cx.assert("Teˇst", "Tˇst");
 98        cx.assert("Tˇest", "ˇest");
 99        cx.assert("ˇTest", "ˇTest");
100        cx.assert(
101            indoc! {"
102                Test
103                ˇtest"},
104            indoc! {"
105                Test
106                ˇtest"},
107        );
108    }
109
110    #[gpui::test]
111    async fn test_change_l(cx: &mut gpui::TestAppContext) {
112        let cx = VimTestContext::new(cx, true).await;
113        let mut cx = cx.binding(["c", "l"]).mode_after(Mode::Insert);
114        cx.assert("Teˇst", "Teˇt");
115        cx.assert("Tesˇt", "Tesˇ");
116    }
117
118    #[gpui::test]
119    async fn test_change_w(cx: &mut gpui::TestAppContext) {
120        let cx = VimTestContext::new(cx, true).await;
121        let mut cx = cx.binding(["c", "w"]).mode_after(Mode::Insert);
122        cx.assert("Teˇst", "Teˇ");
123        cx.assert("Tˇest test", "Tˇ test");
124        cx.assert("Testˇ  test", "Testˇtest");
125        cx.assert(
126            indoc! {"
127                Test teˇst
128                test"},
129            indoc! {"
130                Test teˇ
131                test"},
132        );
133        cx.assert(
134            indoc! {"
135                Test tesˇt
136                test"},
137            indoc! {"
138                Test tesˇ
139                test"},
140        );
141        cx.assert(
142            indoc! {"
143                Test test
144                ˇ
145                test"},
146            indoc! {"
147                Test test
148                ˇ
149                test"},
150        );
151
152        let mut cx = cx.binding(["c", "shift-w"]);
153        cx.assert("Test teˇst-test test", "Test teˇ test");
154    }
155
156    #[gpui::test]
157    async fn test_change_e(cx: &mut gpui::TestAppContext) {
158        let cx = VimTestContext::new(cx, true).await;
159        let mut cx = cx.binding(["c", "e"]).mode_after(Mode::Insert);
160        cx.assert("Teˇst Test", "Teˇ Test");
161        cx.assert("Tˇest test", "Tˇ test");
162        cx.assert(
163            indoc! {"
164                Test teˇst
165                test"},
166            indoc! {"
167                Test teˇ
168                test"},
169        );
170        cx.assert(
171            indoc! {"
172                Test tesˇt
173                test"},
174            "Test tesˇ",
175        );
176        cx.assert(
177            indoc! {"
178                Test test
179                ˇ
180                test"},
181            indoc! {"
182                Test test
183                ˇ"},
184        );
185
186        let mut cx = cx.binding(["c", "shift-e"]);
187        cx.assert("Test teˇst-test test", "Test teˇ test");
188    }
189
190    #[gpui::test]
191    async fn test_change_b(cx: &mut gpui::TestAppContext) {
192        let cx = VimTestContext::new(cx, true).await;
193        let mut cx = cx.binding(["c", "b"]).mode_after(Mode::Insert);
194        cx.assert("Teˇst Test", "ˇst Test");
195        cx.assert("Test ˇtest", "ˇtest");
196        cx.assert("Test1 test2 ˇtest3", "Test1 ˇtest3");
197        cx.assert(
198            indoc! {"
199                Test test
200                ˇtest"},
201            indoc! {"
202                Test ˇ
203                test"},
204        );
205        println!("Marker");
206        cx.assert(
207            indoc! {"
208                Test test
209                ˇ
210                test"},
211            indoc! {"
212                Test ˇ
213                
214                test"},
215        );
216
217        let mut cx = cx.binding(["c", "shift-b"]);
218        cx.assert("Test test-test ˇtest", "Test ˇtest");
219    }
220
221    #[gpui::test]
222    async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
223        let cx = VimTestContext::new(cx, true).await;
224        let mut cx = cx.binding(["c", "$"]).mode_after(Mode::Insert);
225        cx.assert(
226            indoc! {"
227                The qˇuick
228                brown fox"},
229            indoc! {"
230                The qˇ
231                brown fox"},
232        );
233        cx.assert(
234            indoc! {"
235                The quick
236                ˇ
237                brown fox"},
238            indoc! {"
239                The quick
240                ˇ
241                brown fox"},
242        );
243    }
244
245    #[gpui::test]
246    async fn test_change_0(cx: &mut gpui::TestAppContext) {
247        let cx = VimTestContext::new(cx, true).await;
248        let mut cx = cx.binding(["c", "0"]).mode_after(Mode::Insert);
249        cx.assert(
250            indoc! {"
251                The qˇuick
252                brown fox"},
253            indoc! {"
254                ˇuick
255                brown fox"},
256        );
257        cx.assert(
258            indoc! {"
259                The quick
260                ˇ
261                brown fox"},
262            indoc! {"
263                The quick
264                ˇ
265                brown fox"},
266        );
267    }
268
269    #[gpui::test]
270    async fn test_change_k(cx: &mut gpui::TestAppContext) {
271        let cx = VimTestContext::new(cx, true).await;
272        let mut cx = cx.binding(["c", "k"]).mode_after(Mode::Insert);
273        cx.assert(
274            indoc! {"
275                The quick
276                brown ˇfox
277                jumps over"},
278            indoc! {"
279                ˇ
280                jumps over"},
281        );
282        cx.assert(
283            indoc! {"
284                The quick
285                brown fox
286                jumps ˇover"},
287            indoc! {"
288                The quick
289                ˇ"},
290        );
291        cx.assert(
292            indoc! {"
293                The qˇuick
294                brown fox
295                jumps over"},
296            indoc! {"
297                ˇ
298                brown fox
299                jumps over"},
300        );
301        cx.assert(
302            indoc! {"
303                ˇ
304                brown fox
305                jumps over"},
306            indoc! {"
307                ˇ
308                brown fox
309                jumps over"},
310        );
311    }
312
313    #[gpui::test]
314    async fn test_change_j(cx: &mut gpui::TestAppContext) {
315        let cx = VimTestContext::new(cx, true).await;
316        let mut cx = cx.binding(["c", "j"]).mode_after(Mode::Insert);
317        cx.assert(
318            indoc! {"
319                The quick
320                brown ˇfox
321                jumps over"},
322            indoc! {"
323                The quick
324                ˇ"},
325        );
326        cx.assert(
327            indoc! {"
328                The quick
329                brown fox
330                jumps ˇover"},
331            indoc! {"
332                The quick
333                brown fox
334                ˇ"},
335        );
336        cx.assert(
337            indoc! {"
338                The qˇuick
339                brown fox
340                jumps over"},
341            indoc! {"
342                ˇ
343                jumps over"},
344        );
345        cx.assert(
346            indoc! {"
347                The quick
348                brown fox
349                ˇ"},
350            indoc! {"
351                The quick
352                brown fox
353                ˇ"},
354        );
355    }
356
357    #[gpui::test]
358    async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
359        let cx = VimTestContext::new(cx, true).await;
360        let mut cx = cx.binding(["c", "shift-g"]).mode_after(Mode::Insert);
361        cx.assert(
362            indoc! {"
363                The quick
364                brownˇ fox
365                jumps over
366                the lazy"},
367            indoc! {"
368                The quick
369                ˇ"},
370        );
371        cx.assert(
372            indoc! {"
373                The quick
374                brownˇ fox
375                jumps over
376                the lazy"},
377            indoc! {"
378                The quick
379                ˇ"},
380        );
381        cx.assert(
382            indoc! {"
383                The quick
384                brown fox
385                jumps over
386                the lˇazy"},
387            indoc! {"
388                The quick
389                brown fox
390                jumps over
391                ˇ"},
392        );
393        cx.assert(
394            indoc! {"
395                The quick
396                brown fox
397                jumps over
398                ˇ"},
399            indoc! {"
400                The quick
401                brown fox
402                jumps over
403                ˇ"},
404        );
405    }
406
407    #[gpui::test]
408    async fn test_change_gg(cx: &mut gpui::TestAppContext) {
409        let cx = VimTestContext::new(cx, true).await;
410        let mut cx = cx.binding(["c", "g", "g"]).mode_after(Mode::Insert);
411        cx.assert(
412            indoc! {"
413                The quick
414                brownˇ fox
415                jumps over
416                the lazy"},
417            indoc! {"
418                ˇ
419                jumps over
420                the lazy"},
421        );
422        cx.assert(
423            indoc! {"
424                The quick
425                brown fox
426                jumps over
427                the lˇazy"},
428            "ˇ",
429        );
430        cx.assert(
431            indoc! {"
432                The qˇuick
433                brown fox
434                jumps over
435                the lazy"},
436            indoc! {"
437                ˇ
438                brown fox
439                jumps over
440                the lazy"},
441        );
442        cx.assert(
443            indoc! {"
444                ˇ
445                brown fox
446                jumps over
447                the lazy"},
448            indoc! {"
449                ˇ
450                brown fox
451                jumps over
452                the lazy"},
453        );
454    }
455
456    #[gpui::test]
457    async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
458        let mut cx = NeovimBackedTestContext::new(cx).await;
459
460        for count in 1..=5 {
461            cx.assert_binding_matches_all(
462                ["c", &count.to_string(), "j"],
463                indoc! {"
464                    ˇThe quˇickˇ browˇn
465                    ˇ
466                    ˇfox ˇjumpsˇ-ˇoˇver
467                    ˇthe lazy dog
468                    "},
469            )
470            .await;
471        }
472    }
473
474    #[gpui::test]
475    async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
476        let mut cx = NeovimBackedTestContext::new(cx).await;
477
478        for count in 1..=5 {
479            cx.assert_binding_matches_all(
480                ["c", &count.to_string(), "l"],
481                indoc! {"
482                    ˇThe quˇickˇ browˇn
483                    ˇ
484                    ˇfox ˇjumpsˇ-ˇoˇver
485                    ˇthe lazy dog
486                    "},
487            )
488            .await;
489        }
490    }
491
492    #[gpui::test]
493    async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
494        let mut cx = NeovimBackedTestContext::new(cx).await;
495
496        // Changing back any number of times from the start of the file doesn't
497        // switch to insert mode in vim. This is weird and painful to implement
498        cx.add_initial_state_exemption(indoc! {"
499            ˇThe quick brown
500            
501            fox jumps-over
502            the lazy dog
503            "});
504
505        for count in 1..=5 {
506            cx.assert_binding_matches_all(
507                ["c", &count.to_string(), "b"],
508                indoc! {"
509                    ˇThe quˇickˇ browˇn
510                    ˇ
511                    ˇfox ˇjumpsˇ-ˇoˇver
512                    ˇthe lazy dog
513                    "},
514            )
515            .await;
516        }
517    }
518
519    #[gpui::test]
520    async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
521        let mut cx = NeovimBackedTestContext::new(cx).await;
522
523        for count in 1..=5 {
524            cx.assert_binding_matches_all(
525                ["c", &count.to_string(), "e"],
526                indoc! {"
527                    ˇThe quˇickˇ browˇn
528                    ˇ
529                    ˇfox ˇjumpsˇ-ˇoˇver
530                    ˇthe lazy dog
531                    "},
532            )
533            .await;
534        }
535    }
536}