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