change.rs

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