change.rs

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