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}