change.rs

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