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}
 79//
 80// NOT HANDLED YET
 81// Another special case: When using the "w" motion in combination with an
 82// operator and the last word moved over is at the end of a line, the end of
 83// that word becomes the end of the operated text, not the first word in the
 84// next line.
 85fn expand_changed_word_selection(
 86    map: &DisplaySnapshot,
 87    selection: &mut Selection<DisplayPoint>,
 88    times: Option<usize>,
 89    ignore_punctuation: bool,
 90) -> bool {
 91    if times.is_none() || times.unwrap() == 1 {
 92        let scope = map
 93            .buffer_snapshot
 94            .language_scope_at(selection.start.to_point(map));
 95        let in_word = map
 96            .chars_at(selection.head())
 97            .next()
 98            .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
 99            .unwrap_or_default();
100
101        if in_word {
102            selection.end =
103                movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
104                    let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
105                    let right_kind =
106                        char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
107
108                    left_kind != right_kind && left_kind != CharKind::Whitespace
109                });
110            true
111        } else {
112            Motion::NextWordStart { ignore_punctuation }
113                .expand_selection(map, selection, None, false)
114        }
115    } else {
116        Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
117    }
118}
119
120#[cfg(test)]
121mod test {
122    use indoc::indoc;
123
124    use crate::test::NeovimBackedTestContext;
125
126    #[gpui::test]
127    async fn test_change_h(cx: &mut gpui::TestAppContext) {
128        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "h"]);
129        cx.assert("Teˇst").await;
130        cx.assert("Tˇest").await;
131        cx.assert("ˇTest").await;
132        cx.assert(indoc! {"
133            Test
134            ˇtest"})
135            .await;
136    }
137
138    #[gpui::test]
139    async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
140        let mut cx = NeovimBackedTestContext::new(cx)
141            .await
142            .binding(["c", "backspace"]);
143        cx.assert("Teˇst").await;
144        cx.assert("Tˇest").await;
145        cx.assert("ˇTest").await;
146        cx.assert(indoc! {"
147            Test
148            ˇtest"})
149            .await;
150    }
151
152    #[gpui::test]
153    async fn test_change_l(cx: &mut gpui::TestAppContext) {
154        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "l"]);
155        cx.assert("Teˇst").await;
156        cx.assert("Tesˇt").await;
157    }
158
159    #[gpui::test]
160    async fn test_change_w(cx: &mut gpui::TestAppContext) {
161        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "w"]);
162        cx.assert("Teˇst").await;
163        cx.assert("Tˇest test").await;
164        cx.assert("Testˇ  test").await;
165        cx.assert(indoc! {"
166                Test teˇst
167                test"})
168            .await;
169        cx.assert(indoc! {"
170                Test tesˇt
171                test"})
172            .await;
173        cx.assert(indoc! {"
174                Test test
175                ˇ
176                test"})
177            .await;
178
179        let mut cx = cx.binding(["c", "shift-w"]);
180        cx.assert("Test teˇst-test test").await;
181    }
182
183    #[gpui::test]
184    async fn test_change_e(cx: &mut gpui::TestAppContext) {
185        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "e"]);
186        cx.assert("Teˇst Test").await;
187        cx.assert("Tˇest test").await;
188        cx.assert(indoc! {"
189                Test teˇst
190                test"})
191            .await;
192        cx.assert(indoc! {"
193                Test tesˇt
194                test"})
195            .await;
196        cx.assert(indoc! {"
197                Test test
198                ˇ
199                test"})
200            .await;
201
202        let mut cx = cx.binding(["c", "shift-e"]);
203        cx.assert("Test teˇst-test test").await;
204    }
205
206    #[gpui::test]
207    async fn test_change_b(cx: &mut gpui::TestAppContext) {
208        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "b"]);
209        cx.assert("Teˇst Test").await;
210        cx.assert("Test ˇtest").await;
211        cx.assert("Test1 test2 ˇtest3").await;
212        cx.assert(indoc! {"
213                Test test
214                ˇtest"})
215            .await;
216        cx.assert(indoc! {"
217                Test test
218                ˇ
219                test"})
220            .await;
221
222        let mut cx = cx.binding(["c", "shift-b"]);
223        cx.assert("Test test-test ˇtest").await;
224    }
225
226    #[gpui::test]
227    async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
228        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "$"]);
229        cx.assert(indoc! {"
230            The qˇuick
231            brown fox"})
232            .await;
233        cx.assert(indoc! {"
234            The quick
235            ˇ
236            brown fox"})
237            .await;
238    }
239
240    #[gpui::test]
241    async fn test_change_0(cx: &mut gpui::TestAppContext) {
242        let mut cx = NeovimBackedTestContext::new(cx).await;
243
244        cx.assert_neovim_compatible(
245            indoc! {"
246            The qˇuick
247            brown fox"},
248            ["c", "0"],
249        )
250        .await;
251        cx.assert_neovim_compatible(
252            indoc! {"
253            The quick
254            ˇ
255            brown fox"},
256            ["c", "0"],
257        )
258        .await;
259    }
260
261    #[gpui::test]
262    async fn test_change_k(cx: &mut gpui::TestAppContext) {
263        let mut cx = NeovimBackedTestContext::new(cx).await;
264
265        cx.assert_neovim_compatible(
266            indoc! {"
267            The quick
268            brown ˇfox
269            jumps over"},
270            ["c", "k"],
271        )
272        .await;
273        cx.assert_neovim_compatible(
274            indoc! {"
275            The quick
276            brown fox
277            jumps ˇover"},
278            ["c", "k"],
279        )
280        .await;
281        cx.assert_neovim_compatible(
282            indoc! {"
283            The qˇuick
284            brown fox
285            jumps over"},
286            ["c", "k"],
287        )
288        .await;
289        cx.assert_neovim_compatible(
290            indoc! {"
291            ˇ
292            brown fox
293            jumps over"},
294            ["c", "k"],
295        )
296        .await;
297    }
298
299    #[gpui::test]
300    async fn test_change_j(cx: &mut gpui::TestAppContext) {
301        let mut cx = NeovimBackedTestContext::new(cx).await;
302        cx.assert_neovim_compatible(
303            indoc! {"
304            The quick
305            brown ˇfox
306            jumps over"},
307            ["c", "j"],
308        )
309        .await;
310        cx.assert_neovim_compatible(
311            indoc! {"
312            The quick
313            brown fox
314            jumps ˇover"},
315            ["c", "j"],
316        )
317        .await;
318        cx.assert_neovim_compatible(
319            indoc! {"
320            The qˇuick
321            brown fox
322            jumps over"},
323            ["c", "j"],
324        )
325        .await;
326        cx.assert_neovim_compatible(
327            indoc! {"
328            The quick
329            brown fox
330            ˇ"},
331            ["c", "j"],
332        )
333        .await;
334    }
335
336    #[gpui::test]
337    async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
338        let mut cx = NeovimBackedTestContext::new(cx).await;
339        cx.assert_neovim_compatible(
340            indoc! {"
341            The quick
342            brownˇ fox
343            jumps over
344            the lazy"},
345            ["c", "shift-g"],
346        )
347        .await;
348        cx.assert_neovim_compatible(
349            indoc! {"
350            The quick
351            brownˇ fox
352            jumps over
353            the lazy"},
354            ["c", "shift-g"],
355        )
356        .await;
357        cx.assert_neovim_compatible(
358            indoc! {"
359            The quick
360            brown fox
361            jumps over
362            the lˇazy"},
363            ["c", "shift-g"],
364        )
365        .await;
366        cx.assert_neovim_compatible(
367            indoc! {"
368            The quick
369            brown fox
370            jumps over
371            ˇ"},
372            ["c", "shift-g"],
373        )
374        .await;
375    }
376
377    #[gpui::test]
378    async fn test_change_gg(cx: &mut gpui::TestAppContext) {
379        let mut cx = NeovimBackedTestContext::new(cx).await;
380        cx.assert_neovim_compatible(
381            indoc! {"
382            The quick
383            brownˇ fox
384            jumps over
385            the lazy"},
386            ["c", "g", "g"],
387        )
388        .await;
389        cx.assert_neovim_compatible(
390            indoc! {"
391            The quick
392            brown fox
393            jumps over
394            the lˇazy"},
395            ["c", "g", "g"],
396        )
397        .await;
398        cx.assert_neovim_compatible(
399            indoc! {"
400            The qˇuick
401            brown fox
402            jumps over
403            the lazy"},
404            ["c", "g", "g"],
405        )
406        .await;
407        cx.assert_neovim_compatible(
408            indoc! {"
409            ˇ
410            brown fox
411            jumps over
412            the lazy"},
413            ["c", "g", "g"],
414        )
415        .await;
416    }
417
418    #[gpui::test]
419    async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
420        let mut cx = NeovimBackedTestContext::new(cx).await;
421
422        for count in 1..=5 {
423            cx.assert_binding_matches_all(
424                ["c", &count.to_string(), "j"],
425                indoc! {"
426                    ˇThe quˇickˇ browˇn
427                    ˇ
428                    ˇfox ˇjumpsˇ-ˇoˇver
429                    ˇthe lazy dog
430                    "},
431            )
432            .await;
433        }
434    }
435
436    #[gpui::test]
437    async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
438        let mut cx = NeovimBackedTestContext::new(cx).await;
439
440        for count in 1..=5 {
441            cx.assert_binding_matches_all(
442                ["c", &count.to_string(), "l"],
443                indoc! {"
444                    ˇThe quˇickˇ browˇn
445                    ˇ
446                    ˇfox ˇjumpsˇ-ˇoˇver
447                    ˇthe lazy dog
448                    "},
449            )
450            .await;
451        }
452    }
453
454    #[gpui::test]
455    async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
456        let mut cx = NeovimBackedTestContext::new(cx).await;
457
458        for count in 1..=5 {
459            for marked_text in cx.each_marked_position(indoc! {"
460                ˇThe quˇickˇ browˇn
461                ˇ
462                ˇfox ˇjumpsˇ-ˇoˇver
463                ˇthe lazy dog
464                "})
465            {
466                cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
467                    .await;
468            }
469        }
470    }
471
472    #[gpui::test]
473    async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
474        let mut cx = NeovimBackedTestContext::new(cx).await;
475
476        for count in 1..=5 {
477            cx.assert_binding_matches_all(
478                ["c", &count.to_string(), "e"],
479                indoc! {"
480                    ˇThe quˇickˇ browˇn
481                    ˇ
482                    ˇfox ˇjumpsˇ-ˇoˇver
483                    ˇthe lazy dog
484                    "},
485            )
486            .await;
487        }
488    }
489}