change.rs

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