1use crate::{
2 motion::{self, Motion},
3 object::Object,
4 state::Mode,
5 utils::copy_selections_content,
6 Vim,
7};
8use editor::{
9 display_map::{DisplaySnapshot, ToDisplayPoint},
10 movement::TextLayoutDetails,
11 scroll::Autoscroll,
12 Bias, DisplayPoint,
13};
14use gpui::WindowContext;
15use language::{char_kind, CharKind, Selection};
16
17pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
18 // Some motions ignore failure when switching to normal mode
19 let mut motion_succeeded = matches!(
20 motion,
21 Motion::Left
22 | Motion::Right
23 | Motion::EndOfLine { .. }
24 | Motion::Backspace
25 | Motion::StartOfLine { .. }
26 );
27 vim.update_active_editor(cx, |vim, editor, cx| {
28 let text_layout_details = editor.text_layout_details(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 motion_succeeded |= match motion {
35 Motion::NextWordStart { ignore_punctuation }
36 | Motion::NextSubwordStart { ignore_punctuation } => {
37 expand_changed_word_selection(
38 map,
39 selection,
40 times,
41 ignore_punctuation,
42 &text_layout_details,
43 motion == Motion::NextSubwordStart { ignore_punctuation },
44 )
45 }
46 _ => {
47 let result = motion.expand_selection(
48 map,
49 selection,
50 times,
51 false,
52 &text_layout_details,
53 );
54 if let Motion::CurrentLine = motion {
55 let mut start_offset = selection.start.to_offset(map, Bias::Left);
56 let scope = map
57 .buffer_snapshot
58 .language_scope_at(selection.start.to_point(&map));
59 for (ch, offset) in map.buffer_chars_at(start_offset) {
60 if ch == '\n' || char_kind(&scope, ch) != CharKind::Whitespace {
61 break;
62 }
63 start_offset = offset + ch.len_utf8();
64 }
65 selection.start = start_offset.to_display_point(map);
66 }
67 result
68 }
69 }
70 });
71 });
72 copy_selections_content(vim, editor, motion.linewise(), cx);
73 editor.insert("", cx);
74 });
75 });
76
77 if motion_succeeded {
78 vim.switch_mode(Mode::Insert, false, cx)
79 } else {
80 vim.switch_mode(Mode::Normal, false, cx)
81 }
82}
83
84pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
85 let mut objects_found = false;
86 vim.update_active_editor(cx, |vim, editor, cx| {
87 // We are swapping to insert mode anyway. Just set the line end clipping behavior now
88 editor.set_clip_at_line_ends(false, cx);
89 editor.transact(cx, |editor, cx| {
90 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
91 s.move_with(|map, selection| {
92 objects_found |= object.expand_selection(map, selection, around);
93 });
94 });
95 if objects_found {
96 copy_selections_content(vim, editor, false, cx);
97 editor.insert("", cx);
98 }
99 });
100 });
101
102 if objects_found {
103 vim.switch_mode(Mode::Insert, false, cx);
104 } else {
105 vim.switch_mode(Mode::Normal, false, cx);
106 }
107}
108
109// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
110// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
111// on a non-blank. This is because "cw" is interpreted as change-word, and a
112// word does not include the following white space. {Vi: "cw" when on a blank
113// followed by other blanks changes only the first blank; this is probably a
114// bug, because "dw" deletes all the blanks}
115fn expand_changed_word_selection(
116 map: &DisplaySnapshot,
117 selection: &mut Selection<DisplayPoint>,
118 times: Option<usize>,
119 ignore_punctuation: bool,
120 text_layout_details: &TextLayoutDetails,
121 use_subword: bool,
122) -> bool {
123 let is_in_word = || {
124 let scope = map
125 .buffer_snapshot
126 .language_scope_at(selection.start.to_point(map));
127 let in_word = map
128 .buffer_chars_at(selection.head().to_offset(map, Bias::Left))
129 .next()
130 .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
131 .unwrap_or_default();
132 return in_word;
133 };
134 if (times.is_none() || times.unwrap() == 1) && is_in_word() {
135 let next_char = map
136 .buffer_chars_at(
137 motion::next_char(map, selection.end, false).to_offset(map, Bias::Left),
138 )
139 .next();
140 match next_char {
141 Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false),
142 _ => {
143 if use_subword {
144 selection.end =
145 motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false);
146 } else {
147 selection.end =
148 motion::next_word_end(map, selection.end, ignore_punctuation, 1, false);
149 }
150 selection.end = motion::next_char(map, selection.end, false);
151 }
152 }
153 true
154 } else {
155 let motion = if use_subword {
156 Motion::NextSubwordStart { ignore_punctuation }
157 } else {
158 Motion::NextWordStart { ignore_punctuation }
159 };
160 motion.expand_selection(map, selection, times, false, &text_layout_details)
161 }
162}
163
164#[cfg(test)]
165mod test {
166 use indoc::indoc;
167
168 use crate::test::NeovimBackedTestContext;
169
170 #[gpui::test]
171 async fn test_change_h(cx: &mut gpui::TestAppContext) {
172 let mut cx = NeovimBackedTestContext::new(cx).await;
173 cx.simulate("c h", "Teˇst").await.assert_matches();
174 cx.simulate("c h", "Tˇest").await.assert_matches();
175 cx.simulate("c h", "ˇTest").await.assert_matches();
176 cx.simulate(
177 "c h",
178 indoc! {"
179 Test
180 ˇtest"},
181 )
182 .await
183 .assert_matches();
184 }
185
186 #[gpui::test]
187 async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
188 let mut cx = NeovimBackedTestContext::new(cx).await;
189 cx.simulate("c backspace", "Teˇst").await.assert_matches();
190 cx.simulate("c backspace", "Tˇest").await.assert_matches();
191 cx.simulate("c backspace", "ˇTest").await.assert_matches();
192 cx.simulate(
193 "c backspace",
194 indoc! {"
195 Test
196 ˇtest"},
197 )
198 .await
199 .assert_matches();
200 }
201
202 #[gpui::test]
203 async fn test_change_l(cx: &mut gpui::TestAppContext) {
204 let mut cx = NeovimBackedTestContext::new(cx).await;
205 cx.simulate("c l", "Teˇst").await.assert_matches();
206 cx.simulate("c l", "Tesˇt").await.assert_matches();
207 }
208
209 #[gpui::test]
210 async fn test_change_w(cx: &mut gpui::TestAppContext) {
211 let mut cx = NeovimBackedTestContext::new(cx).await;
212 cx.simulate("c w", "Teˇst").await.assert_matches();
213 cx.simulate("c w", "Tˇest test").await.assert_matches();
214 cx.simulate("c w", "Testˇ test").await.assert_matches();
215 cx.simulate("c w", "Tesˇt test").await.assert_matches();
216 cx.simulate(
217 "c w",
218 indoc! {"
219 Test teˇst
220 test"},
221 )
222 .await
223 .assert_matches();
224 cx.simulate(
225 "c w",
226 indoc! {"
227 Test tesˇt
228 test"},
229 )
230 .await
231 .assert_matches();
232 cx.simulate(
233 "c w",
234 indoc! {"
235 Test test
236 ˇ
237 test"},
238 )
239 .await
240 .assert_matches();
241
242 cx.simulate("c shift-w", "Test teˇst-test test")
243 .await
244 .assert_matches();
245 }
246
247 #[gpui::test]
248 async fn test_change_e(cx: &mut gpui::TestAppContext) {
249 let mut cx = NeovimBackedTestContext::new(cx).await;
250 cx.simulate("c e", "Teˇst Test").await.assert_matches();
251 cx.simulate("c e", "Tˇest test").await.assert_matches();
252 cx.simulate(
253 "c e",
254 indoc! {"
255 Test teˇst
256 test"},
257 )
258 .await
259 .assert_matches();
260 cx.simulate(
261 "c e",
262 indoc! {"
263 Test tesˇt
264 test"},
265 )
266 .await
267 .assert_matches();
268 cx.simulate(
269 "c e",
270 indoc! {"
271 Test test
272 ˇ
273 test"},
274 )
275 .await
276 .assert_matches();
277
278 cx.simulate("c shift-e", "Test teˇst-test test")
279 .await
280 .assert_matches();
281 }
282
283 #[gpui::test]
284 async fn test_change_b(cx: &mut gpui::TestAppContext) {
285 let mut cx = NeovimBackedTestContext::new(cx).await;
286 cx.simulate("c b", "Teˇst Test").await.assert_matches();
287 cx.simulate("c b", "Test ˇtest").await.assert_matches();
288 cx.simulate("c b", "Test1 test2 ˇtest3")
289 .await
290 .assert_matches();
291 cx.simulate(
292 "c b",
293 indoc! {"
294 Test test
295 ˇtest"},
296 )
297 .await
298 .assert_matches();
299 cx.simulate(
300 "c b",
301 indoc! {"
302 Test test
303 ˇ
304 test"},
305 )
306 .await
307 .assert_matches();
308
309 cx.simulate("c shift-b", "Test test-test ˇtest")
310 .await
311 .assert_matches();
312 }
313
314 #[gpui::test]
315 async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
316 let mut cx = NeovimBackedTestContext::new(cx).await;
317 cx.simulate(
318 "c $",
319 indoc! {"
320 The qˇuick
321 brown fox"},
322 )
323 .await
324 .assert_matches();
325 cx.simulate(
326 "c $",
327 indoc! {"
328 The quick
329 ˇ
330 brown fox"},
331 )
332 .await
333 .assert_matches();
334 }
335
336 #[gpui::test]
337 async fn test_change_0(cx: &mut gpui::TestAppContext) {
338 let mut cx = NeovimBackedTestContext::new(cx).await;
339
340 cx.simulate(
341 "c 0",
342 indoc! {"
343 The qˇuick
344 brown fox"},
345 )
346 .await
347 .assert_matches();
348 cx.simulate(
349 "c 0",
350 indoc! {"
351 The quick
352 ˇ
353 brown fox"},
354 )
355 .await
356 .assert_matches();
357 }
358
359 #[gpui::test]
360 async fn test_change_k(cx: &mut gpui::TestAppContext) {
361 let mut cx = NeovimBackedTestContext::new(cx).await;
362
363 cx.simulate(
364 "c k",
365 indoc! {"
366 The quick
367 brown ˇfox
368 jumps over"},
369 )
370 .await
371 .assert_matches();
372 cx.simulate(
373 "c k",
374 indoc! {"
375 The quick
376 brown fox
377 jumps ˇover"},
378 )
379 .await
380 .assert_matches();
381 cx.simulate(
382 "c k",
383 indoc! {"
384 The qˇuick
385 brown fox
386 jumps over"},
387 )
388 .await
389 .assert_matches();
390 cx.simulate(
391 "c k",
392 indoc! {"
393 ˇ
394 brown fox
395 jumps over"},
396 )
397 .await
398 .assert_matches();
399 }
400
401 #[gpui::test]
402 async fn test_change_j(cx: &mut gpui::TestAppContext) {
403 let mut cx = NeovimBackedTestContext::new(cx).await;
404 cx.simulate(
405 "c j",
406 indoc! {"
407 The quick
408 brown ˇfox
409 jumps over"},
410 )
411 .await
412 .assert_matches();
413 cx.simulate(
414 "c j",
415 indoc! {"
416 The quick
417 brown fox
418 jumps ˇover"},
419 )
420 .await
421 .assert_matches();
422 cx.simulate(
423 "c j",
424 indoc! {"
425 The qˇuick
426 brown fox
427 jumps over"},
428 )
429 .await
430 .assert_matches();
431 cx.simulate(
432 "c j",
433 indoc! {"
434 The quick
435 brown fox
436 ˇ"},
437 )
438 .await
439 .assert_matches();
440 }
441
442 #[gpui::test]
443 async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
444 let mut cx = NeovimBackedTestContext::new(cx).await;
445 cx.simulate(
446 "c shift-g",
447 indoc! {"
448 The quick
449 brownˇ fox
450 jumps over
451 the lazy"},
452 )
453 .await
454 .assert_matches();
455 cx.simulate(
456 "c shift-g",
457 indoc! {"
458 The quick
459 brownˇ fox
460 jumps over
461 the lazy"},
462 )
463 .await
464 .assert_matches();
465 cx.simulate(
466 "c shift-g",
467 indoc! {"
468 The quick
469 brown fox
470 jumps over
471 the lˇazy"},
472 )
473 .await
474 .assert_matches();
475 cx.simulate(
476 "c shift-g",
477 indoc! {"
478 The quick
479 brown fox
480 jumps over
481 ˇ"},
482 )
483 .await
484 .assert_matches();
485 }
486
487 #[gpui::test]
488 async fn test_change_cc(cx: &mut gpui::TestAppContext) {
489 let mut cx = NeovimBackedTestContext::new(cx).await;
490 cx.simulate(
491 "c c",
492 indoc! {"
493 The quick
494 brownˇ fox
495 jumps over
496 the lazy"},
497 )
498 .await
499 .assert_matches();
500
501 cx.simulate(
502 "c c",
503 indoc! {"
504 ˇThe quick
505 brown fox
506 jumps over
507 the lazy"},
508 )
509 .await
510 .assert_matches();
511
512 cx.simulate(
513 "c c",
514 indoc! {"
515 The quick
516 broˇwn fox
517 jumps over
518 the lazy"},
519 )
520 .await
521 .assert_matches();
522 }
523
524 #[gpui::test]
525 async fn test_change_gg(cx: &mut gpui::TestAppContext) {
526 let mut cx = NeovimBackedTestContext::new(cx).await;
527 cx.simulate(
528 "c g g",
529 indoc! {"
530 The quick
531 brownˇ fox
532 jumps over
533 the lazy"},
534 )
535 .await
536 .assert_matches();
537 cx.simulate(
538 "c g g",
539 indoc! {"
540 The quick
541 brown fox
542 jumps over
543 the lˇazy"},
544 )
545 .await
546 .assert_matches();
547 cx.simulate(
548 "c g g",
549 indoc! {"
550 The qˇuick
551 brown fox
552 jumps over
553 the lazy"},
554 )
555 .await
556 .assert_matches();
557 cx.simulate(
558 "c g g",
559 indoc! {"
560 ˇ
561 brown fox
562 jumps over
563 the lazy"},
564 )
565 .await
566 .assert_matches();
567 }
568
569 #[gpui::test]
570 async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
571 let mut cx = NeovimBackedTestContext::new(cx).await;
572
573 for count in 1..=5 {
574 cx.simulate_at_each_offset(
575 &format!("c {count} j"),
576 indoc! {"
577 ˇThe quˇickˇ browˇn
578 ˇ
579 ˇfox ˇjumpsˇ-ˇoˇver
580 ˇthe lazy dog
581 "},
582 )
583 .await
584 .assert_matches();
585 }
586 }
587
588 #[gpui::test]
589 async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
590 let mut cx = NeovimBackedTestContext::new(cx).await;
591
592 for count in 1..=5 {
593 cx.simulate_at_each_offset(
594 &format!("c {count} l"),
595 indoc! {"
596 ˇThe quˇickˇ browˇn
597 ˇ
598 ˇfox ˇjumpsˇ-ˇoˇver
599 ˇthe lazy dog
600 "},
601 )
602 .await
603 .assert_matches();
604 }
605 }
606
607 #[gpui::test]
608 async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
609 let mut cx = NeovimBackedTestContext::new(cx).await;
610
611 for count in 1..=5 {
612 cx.simulate_at_each_offset(
613 &format!("c {count} b"),
614 indoc! {"
615 ˇThe quˇickˇ browˇn
616 ˇ
617 ˇfox ˇjumpsˇ-ˇoˇver
618 ˇthe lazy dog
619 "},
620 )
621 .await
622 .assert_matches()
623 }
624 }
625
626 #[gpui::test]
627 async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
628 let mut cx = NeovimBackedTestContext::new(cx).await;
629
630 for count in 1..=5 {
631 cx.simulate_at_each_offset(
632 &format!("c {count} e"),
633 indoc! {"
634 ˇThe quˇickˇ browˇn
635 ˇ
636 ˇfox ˇjumpsˇ-ˇoˇver
637 ˇthe lazy dog
638 "},
639 )
640 .await
641 .assert_matches();
642 }
643 }
644}