change.rs

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