change.rs

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