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