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