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