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: 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 | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine
 14    );
 15    vim.update_active_editor(cx, |editor, cx| {
 16        editor.transact(cx, |editor, cx| {
 17            // We are swapping to insert mode anyway. Just set the line end clipping behavior now
 18            editor.set_clip_at_line_ends(false, cx);
 19            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 20                s.move_with(|map, selection| {
 21                    motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
 22                    {
 23                        expand_changed_word_selection(map, selection, times, ignore_punctuation)
 24                    } else {
 25                        motion.expand_selection(map, selection, times, false)
 26                    };
 27                });
 28            });
 29            copy_selections_content(editor, motion.linewise(), cx);
 30            editor.insert("", cx);
 31        });
 32    });
 33
 34    if motion_succeeded {
 35        vim.switch_mode(Mode::Insert, false, cx)
 36    } else {
 37        vim.switch_mode(Mode::Normal, false, cx)
 38    }
 39}
 40
 41pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
 42    let mut objects_found = false;
 43    vim.update_active_editor(cx, |editor, cx| {
 44        // We are swapping to insert mode anyway. Just set the line end clipping behavior now
 45        editor.set_clip_at_line_ends(false, cx);
 46        editor.transact(cx, |editor, cx| {
 47            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 48                s.move_with(|map, selection| {
 49                    objects_found |= object.expand_selection(map, selection, around);
 50                });
 51            });
 52            if objects_found {
 53                copy_selections_content(editor, false, cx);
 54                editor.insert("", cx);
 55            }
 56        });
 57    });
 58
 59    if objects_found {
 60        vim.switch_mode(Mode::Insert, false, cx);
 61    } else {
 62        vim.switch_mode(Mode::Normal, false, cx);
 63    }
 64}
 65
 66// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
 67// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
 68// on a non-blank.  This is because "cw" is interpreted as change-word, and a
 69// word does not include the following white space.  {Vi: "cw" when on a blank
 70//     followed by other blanks changes only the first blank; this is probably a
 71//     bug, because "dw" deletes all the blanks}
 72//
 73// NOT HANDLED YET
 74// Another special case: When using the "w" motion in combination with an
 75// operator and the last word moved over is at the end of a line, the end of
 76// that word becomes the end of the operated text, not the first word in the
 77// next line.
 78fn expand_changed_word_selection(
 79    map: &DisplaySnapshot,
 80    selection: &mut Selection<DisplayPoint>,
 81    times: usize,
 82    ignore_punctuation: bool,
 83) -> bool {
 84    if times == 1 {
 85        let in_word = map
 86            .chars_at(selection.head())
 87            .next()
 88            .map(|(c, _)| char_kind(c) != CharKind::Whitespace)
 89            .unwrap_or_default();
 90
 91        if in_word {
 92            selection.end = movement::find_boundary(map, selection.end, |left, right| {
 93                let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
 94                let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
 95
 96                left_kind != right_kind && left_kind != CharKind::Whitespace
 97            });
 98            true
 99        } else {
100            Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, 1, false)
101        }
102    } else {
103        Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
104    }
105}
106
107#[cfg(test)]
108mod test {
109    use indoc::indoc;
110
111    use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
112
113    #[gpui::test]
114    async fn test_change_h(cx: &mut gpui::TestAppContext) {
115        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "h"]);
116        cx.assert("Teˇst").await;
117        cx.assert("Tˇest").await;
118        cx.assert("ˇTest").await;
119        cx.assert(indoc! {"
120            Test
121            ˇtest"})
122            .await;
123    }
124
125    #[gpui::test]
126    async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
127        let mut cx = NeovimBackedTestContext::new(cx)
128            .await
129            .binding(["c", "backspace"]);
130        cx.assert("Teˇst").await;
131        cx.assert("Tˇest").await;
132        cx.assert("ˇTest").await;
133        cx.assert(indoc! {"
134            Test
135            ˇtest"})
136            .await;
137    }
138
139    #[gpui::test]
140    async fn test_change_l(cx: &mut gpui::TestAppContext) {
141        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "l"]);
142        cx.assert("Teˇst").await;
143        cx.assert("Tesˇt").await;
144    }
145
146    #[gpui::test]
147    async fn test_change_w(cx: &mut gpui::TestAppContext) {
148        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "w"]);
149        cx.assert("Teˇst").await;
150        cx.assert("Tˇest test").await;
151        cx.assert("Testˇ  test").await;
152        cx.assert(indoc! {"
153                Test teˇst
154                test"})
155            .await;
156        cx.assert(indoc! {"
157                Test tesˇt
158                test"})
159            .await;
160        cx.assert(indoc! {"
161                Test test
162                ˇ
163                test"})
164            .await;
165
166        let mut cx = cx.binding(["c", "shift-w"]);
167        cx.assert("Test teˇst-test test").await;
168    }
169
170    #[gpui::test]
171    async fn test_change_e(cx: &mut gpui::TestAppContext) {
172        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "e"]);
173        cx.assert("Teˇst Test").await;
174        cx.assert("Tˇest test").await;
175        cx.assert(indoc! {"
176                Test teˇst
177                test"})
178            .await;
179        cx.assert(indoc! {"
180                Test tesˇt
181                test"})
182            .await;
183        cx.assert(indoc! {"
184                Test test
185                ˇ
186                test"})
187            .await;
188
189        let mut cx = cx.binding(["c", "shift-e"]);
190        cx.assert("Test teˇst-test test").await;
191    }
192
193    #[gpui::test]
194    async fn test_change_b(cx: &mut gpui::TestAppContext) {
195        let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "b"]);
196        cx.assert("Teˇst Test").await;
197        cx.assert("Test ˇtest").await;
198        cx.assert("Test1 test2 ˇtest3").await;
199        cx.assert(indoc! {"
200                Test test
201                ˇtest"})
202            .await;
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}