change.rs

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