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