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