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