1use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
2use editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
3use gpui::MutableAppContext;
4use language::Selection;
5
6pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
7 vim.update_active_editor(cx, |editor, cx| {
8 editor.transact(cx, |editor, cx| {
9 // We are swapping to insert mode anyway. Just set the line end clipping behavior now
10 editor.set_clip_at_line_ends(false, cx);
11 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
12 s.move_with(|map, selection| {
13 if let Motion::NextWordStart { ignore_punctuation } = motion {
14 expand_changed_word_selection(map, selection, times, ignore_punctuation);
15 } else {
16 motion.expand_selection(map, selection, times, false);
17 }
18 });
19 });
20 copy_selections_content(editor, motion.linewise(), cx);
21 editor.insert("", cx);
22 });
23 });
24 vim.switch_mode(Mode::Insert, false, cx)
25}
26
27pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
28 let mut objects_found = false;
29 vim.update_active_editor(cx, |editor, cx| {
30 // We are swapping to insert mode anyway. Just set the line end clipping behavior now
31 editor.set_clip_at_line_ends(false, cx);
32 editor.transact(cx, |editor, cx| {
33 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
34 s.move_with(|map, selection| {
35 objects_found |= object.expand_selection(map, selection, around);
36 });
37 });
38 if objects_found {
39 copy_selections_content(editor, false, cx);
40 editor.insert("", cx);
41 }
42 });
43 });
44
45 if objects_found {
46 vim.switch_mode(Mode::Insert, false, cx);
47 } else {
48 vim.switch_mode(Mode::Normal, false, cx);
49 }
50}
51
52// From the docs https://vimhelp.org/change.txt.html#cw
53// Special case: When the cursor is in a word, "cw" and "cW" do not include the
54// white space after a word, they only change up to the end of the word. This is
55// because Vim interprets "cw" as change-word, and a word does not include the
56// following white space.
57fn expand_changed_word_selection(
58 map: &DisplaySnapshot,
59 selection: &mut Selection<DisplayPoint>,
60 times: usize,
61 ignore_punctuation: bool,
62) {
63 if times > 1 {
64 Motion::NextWordStart { ignore_punctuation }.expand_selection(
65 map,
66 selection,
67 times - 1,
68 false,
69 );
70 }
71
72 if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
73 return;
74 }
75
76 selection.end = movement::find_boundary(map, selection.end, |left, right| {
77 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
78 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
79
80 left_kind != right_kind || left == '\n' || right == '\n'
81 });
82}
83
84#[cfg(test)]
85mod test {
86 use indoc::indoc;
87
88 use crate::{
89 state::Mode,
90 test::{NeovimBackedTestContext, VimTestContext},
91 };
92
93 #[gpui::test]
94 async fn test_change_h(cx: &mut gpui::TestAppContext) {
95 let cx = VimTestContext::new(cx, true).await;
96 let mut cx = cx.binding(["c", "h"]).mode_after(Mode::Insert);
97 cx.assert("Teˇst", "Tˇst");
98 cx.assert("Tˇest", "ˇest");
99 cx.assert("ˇTest", "ˇTest");
100 cx.assert(
101 indoc! {"
102 Test
103 ˇtest"},
104 indoc! {"
105 Test
106 ˇtest"},
107 );
108 }
109
110 #[gpui::test]
111 async fn test_change_l(cx: &mut gpui::TestAppContext) {
112 let cx = VimTestContext::new(cx, true).await;
113 let mut cx = cx.binding(["c", "l"]).mode_after(Mode::Insert);
114 cx.assert("Teˇst", "Teˇt");
115 cx.assert("Tesˇt", "Tesˇ");
116 }
117
118 #[gpui::test]
119 async fn test_change_w(cx: &mut gpui::TestAppContext) {
120 let cx = VimTestContext::new(cx, true).await;
121 let mut cx = cx.binding(["c", "w"]).mode_after(Mode::Insert);
122 cx.assert("Teˇst", "Teˇ");
123 cx.assert("Tˇest test", "Tˇ test");
124 cx.assert("Testˇ test", "Testˇtest");
125 cx.assert(
126 indoc! {"
127 Test teˇst
128 test"},
129 indoc! {"
130 Test teˇ
131 test"},
132 );
133 cx.assert(
134 indoc! {"
135 Test tesˇt
136 test"},
137 indoc! {"
138 Test tesˇ
139 test"},
140 );
141 cx.assert(
142 indoc! {"
143 Test test
144 ˇ
145 test"},
146 indoc! {"
147 Test test
148 ˇ
149 test"},
150 );
151
152 let mut cx = cx.binding(["c", "shift-w"]);
153 cx.assert("Test teˇst-test test", "Test teˇ test");
154 }
155
156 #[gpui::test]
157 async fn test_change_e(cx: &mut gpui::TestAppContext) {
158 let cx = VimTestContext::new(cx, true).await;
159 let mut cx = cx.binding(["c", "e"]).mode_after(Mode::Insert);
160 cx.assert("Teˇst Test", "Teˇ Test");
161 cx.assert("Tˇest test", "Tˇ test");
162 cx.assert(
163 indoc! {"
164 Test teˇst
165 test"},
166 indoc! {"
167 Test teˇ
168 test"},
169 );
170 cx.assert(
171 indoc! {"
172 Test tesˇt
173 test"},
174 "Test tesˇ",
175 );
176 cx.assert(
177 indoc! {"
178 Test test
179 ˇ
180 test"},
181 indoc! {"
182 Test test
183 ˇ"},
184 );
185
186 let mut cx = cx.binding(["c", "shift-e"]);
187 cx.assert("Test teˇst-test test", "Test teˇ test");
188 }
189
190 #[gpui::test]
191 async fn test_change_b(cx: &mut gpui::TestAppContext) {
192 let cx = VimTestContext::new(cx, true).await;
193 let mut cx = cx.binding(["c", "b"]).mode_after(Mode::Insert);
194 cx.assert("Teˇst Test", "ˇst Test");
195 cx.assert("Test ˇtest", "ˇtest");
196 cx.assert("Test1 test2 ˇtest3", "Test1 ˇtest3");
197 cx.assert(
198 indoc! {"
199 Test test
200 ˇtest"},
201 indoc! {"
202 Test ˇ
203 test"},
204 );
205 println!("Marker");
206 cx.assert(
207 indoc! {"
208 Test test
209 ˇ
210 test"},
211 indoc! {"
212 Test ˇ
213
214 test"},
215 );
216
217 let mut cx = cx.binding(["c", "shift-b"]);
218 cx.assert("Test test-test ˇtest", "Test ˇtest");
219 }
220
221 #[gpui::test]
222 async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
223 let cx = VimTestContext::new(cx, true).await;
224 let mut cx = cx.binding(["c", "$"]).mode_after(Mode::Insert);
225 cx.assert(
226 indoc! {"
227 The qˇuick
228 brown fox"},
229 indoc! {"
230 The qˇ
231 brown fox"},
232 );
233 cx.assert(
234 indoc! {"
235 The quick
236 ˇ
237 brown fox"},
238 indoc! {"
239 The quick
240 ˇ
241 brown fox"},
242 );
243 }
244
245 #[gpui::test]
246 async fn test_change_0(cx: &mut gpui::TestAppContext) {
247 let cx = VimTestContext::new(cx, true).await;
248 let mut cx = cx.binding(["c", "0"]).mode_after(Mode::Insert);
249 cx.assert(
250 indoc! {"
251 The qˇuick
252 brown fox"},
253 indoc! {"
254 ˇuick
255 brown fox"},
256 );
257 cx.assert(
258 indoc! {"
259 The quick
260 ˇ
261 brown fox"},
262 indoc! {"
263 The quick
264 ˇ
265 brown fox"},
266 );
267 }
268
269 #[gpui::test]
270 async fn test_change_k(cx: &mut gpui::TestAppContext) {
271 let cx = VimTestContext::new(cx, true).await;
272 let mut cx = cx.binding(["c", "k"]).mode_after(Mode::Insert);
273 cx.assert(
274 indoc! {"
275 The quick
276 brown ˇfox
277 jumps over"},
278 indoc! {"
279 ˇ
280 jumps over"},
281 );
282 cx.assert(
283 indoc! {"
284 The quick
285 brown fox
286 jumps ˇover"},
287 indoc! {"
288 The quick
289 ˇ"},
290 );
291 cx.assert(
292 indoc! {"
293 The qˇuick
294 brown fox
295 jumps over"},
296 indoc! {"
297 ˇ
298 brown fox
299 jumps over"},
300 );
301 cx.assert(
302 indoc! {"
303 ˇ
304 brown fox
305 jumps over"},
306 indoc! {"
307 ˇ
308 brown fox
309 jumps over"},
310 );
311 }
312
313 #[gpui::test]
314 async fn test_change_j(cx: &mut gpui::TestAppContext) {
315 let cx = VimTestContext::new(cx, true).await;
316 let mut cx = cx.binding(["c", "j"]).mode_after(Mode::Insert);
317 cx.assert(
318 indoc! {"
319 The quick
320 brown ˇfox
321 jumps over"},
322 indoc! {"
323 The quick
324 ˇ"},
325 );
326 cx.assert(
327 indoc! {"
328 The quick
329 brown fox
330 jumps ˇover"},
331 indoc! {"
332 The quick
333 brown fox
334 ˇ"},
335 );
336 cx.assert(
337 indoc! {"
338 The qˇuick
339 brown fox
340 jumps over"},
341 indoc! {"
342 ˇ
343 jumps over"},
344 );
345 cx.assert(
346 indoc! {"
347 The quick
348 brown fox
349 ˇ"},
350 indoc! {"
351 The quick
352 brown fox
353 ˇ"},
354 );
355 }
356
357 #[gpui::test]
358 async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
359 let cx = VimTestContext::new(cx, true).await;
360 let mut cx = cx.binding(["c", "shift-g"]).mode_after(Mode::Insert);
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 lazy"},
377 indoc! {"
378 The quick
379 ˇ"},
380 );
381 cx.assert(
382 indoc! {"
383 The quick
384 brown fox
385 jumps over
386 the lˇazy"},
387 indoc! {"
388 The quick
389 brown fox
390 jumps over
391 ˇ"},
392 );
393 cx.assert(
394 indoc! {"
395 The quick
396 brown fox
397 jumps over
398 ˇ"},
399 indoc! {"
400 The quick
401 brown fox
402 jumps over
403 ˇ"},
404 );
405 }
406
407 #[gpui::test]
408 async fn test_change_gg(cx: &mut gpui::TestAppContext) {
409 let cx = VimTestContext::new(cx, true).await;
410 let mut cx = cx.binding(["c", "g", "g"]).mode_after(Mode::Insert);
411 cx.assert(
412 indoc! {"
413 The quick
414 brownˇ fox
415 jumps over
416 the lazy"},
417 indoc! {"
418 ˇ
419 jumps over
420 the lazy"},
421 );
422 cx.assert(
423 indoc! {"
424 The quick
425 brown fox
426 jumps over
427 the lˇazy"},
428 "ˇ",
429 );
430 cx.assert(
431 indoc! {"
432 The qˇuick
433 brown fox
434 jumps over
435 the lazy"},
436 indoc! {"
437 ˇ
438 brown fox
439 jumps over
440 the lazy"},
441 );
442 cx.assert(
443 indoc! {"
444 ˇ
445 brown fox
446 jumps over
447 the lazy"},
448 indoc! {"
449 ˇ
450 brown fox
451 jumps over
452 the lazy"},
453 );
454 }
455
456 #[gpui::test]
457 async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
458 let mut cx = NeovimBackedTestContext::new(cx).await;
459
460 for count in 1..=5 {
461 cx.assert_binding_matches_all(
462 ["c", &count.to_string(), "j"],
463 indoc! {"
464 ˇThe quˇickˇ browˇn
465 ˇ
466 ˇfox ˇjumpsˇ-ˇoˇver
467 ˇthe lazy dog
468 "},
469 )
470 .await;
471 }
472 }
473
474 #[gpui::test]
475 async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
476 let mut cx = NeovimBackedTestContext::new(cx).await;
477
478 for count in 1..=5 {
479 cx.assert_binding_matches_all(
480 ["c", &count.to_string(), "l"],
481 indoc! {"
482 ˇThe quˇickˇ browˇn
483 ˇ
484 ˇfox ˇjumpsˇ-ˇoˇver
485 ˇthe lazy dog
486 "},
487 )
488 .await;
489 }
490 }
491
492 #[gpui::test]
493 async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
494 let mut cx = NeovimBackedTestContext::new(cx).await;
495
496 // Changing back any number of times from the start of the file doesn't
497 // switch to insert mode in vim. This is weird and painful to implement
498 cx.add_initial_state_exemption(indoc! {"
499 ˇThe quick brown
500
501 fox jumps-over
502 the lazy dog
503 "});
504
505 for count in 1..=5 {
506 cx.assert_binding_matches_all(
507 ["c", &count.to_string(), "b"],
508 indoc! {"
509 ˇThe quˇickˇ browˇn
510 ˇ
511 ˇfox ˇjumpsˇ-ˇoˇver
512 ˇthe lazy dog
513 "},
514 )
515 .await;
516 }
517 }
518
519 #[gpui::test]
520 async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
521 let mut cx = NeovimBackedTestContext::new(cx).await;
522
523 for count in 1..=5 {
524 cx.assert_binding_matches_all(
525 ["c", &count.to_string(), "e"],
526 indoc! {"
527 ˇThe quˇickˇ browˇn
528 ˇ
529 ˇfox ˇjumpsˇ-ˇoˇver
530 ˇthe lazy dog
531 "},
532 )
533 .await;
534 }
535 }
536}