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