change.rs

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