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.binding(["c", "h"]);
173 cx.assert("Teˇst").await;
174 cx.assert("Tˇest").await;
175 cx.assert("ˇTest").await;
176 cx.assert(indoc! {"
177 Test
178 ˇtest"})
179 .await;
180 }
181
182 #[gpui::test]
183 async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
184 let mut cx = NeovimBackedTestContext::new(cx)
185 .await
186 .binding(["c", "backspace"]);
187 cx.assert("Teˇst").await;
188 cx.assert("Tˇest").await;
189 cx.assert("ˇTest").await;
190 cx.assert(indoc! {"
191 Test
192 ˇtest"})
193 .await;
194 }
195
196 #[gpui::test]
197 async fn test_change_l(cx: &mut gpui::TestAppContext) {
198 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "l"]);
199 cx.assert("Teˇst").await;
200 cx.assert("Tesˇt").await;
201 }
202
203 #[gpui::test]
204 async fn test_change_w(cx: &mut gpui::TestAppContext) {
205 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "w"]);
206 cx.assert("Teˇst").await;
207 cx.assert("Tˇest test").await;
208 cx.assert("Testˇ test").await;
209 cx.assert("Tesˇt test").await;
210 cx.assert(indoc! {"
211 Test teˇst
212 test"})
213 .await;
214 cx.assert(indoc! {"
215 Test tesˇt
216 test"})
217 .await;
218 cx.assert(indoc! {"
219 Test test
220 ˇ
221 test"})
222 .await;
223
224 let mut cx = cx.binding(["c", "shift-w"]);
225 cx.assert("Test teˇst-test test").await;
226 }
227
228 #[gpui::test]
229 async fn test_change_e(cx: &mut gpui::TestAppContext) {
230 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "e"]);
231 cx.assert("Teˇst Test").await;
232 cx.assert("Tˇest test").await;
233 cx.assert(indoc! {"
234 Test teˇst
235 test"})
236 .await;
237 cx.assert(indoc! {"
238 Test tesˇt
239 test"})
240 .await;
241 cx.assert(indoc! {"
242 Test test
243 ˇ
244 test"})
245 .await;
246
247 let mut cx = cx.binding(["c", "shift-e"]);
248 cx.assert("Test teˇst-test test").await;
249 }
250
251 #[gpui::test]
252 async fn test_change_b(cx: &mut gpui::TestAppContext) {
253 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "b"]);
254 cx.assert("Teˇst Test").await;
255 cx.assert("Test ˇtest").await;
256 cx.assert("Test1 test2 ˇtest3").await;
257 cx.assert(indoc! {"
258 Test test
259 ˇtest"})
260 .await;
261 cx.assert(indoc! {"
262 Test test
263 ˇ
264 test"})
265 .await;
266
267 let mut cx = cx.binding(["c", "shift-b"]);
268 cx.assert("Test test-test ˇtest").await;
269 }
270
271 #[gpui::test]
272 async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
273 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "$"]);
274 cx.assert(indoc! {"
275 The qˇuick
276 brown fox"})
277 .await;
278 cx.assert(indoc! {"
279 The quick
280 ˇ
281 brown fox"})
282 .await;
283 }
284
285 #[gpui::test]
286 async fn test_change_0(cx: &mut gpui::TestAppContext) {
287 let mut cx = NeovimBackedTestContext::new(cx).await;
288
289 cx.assert_neovim_compatible(
290 indoc! {"
291 The qˇuick
292 brown fox"},
293 ["c", "0"],
294 )
295 .await;
296 cx.assert_neovim_compatible(
297 indoc! {"
298 The quick
299 ˇ
300 brown fox"},
301 ["c", "0"],
302 )
303 .await;
304 }
305
306 #[gpui::test]
307 async fn test_change_k(cx: &mut gpui::TestAppContext) {
308 let mut cx = NeovimBackedTestContext::new(cx).await;
309
310 cx.assert_neovim_compatible(
311 indoc! {"
312 The quick
313 brown ˇfox
314 jumps over"},
315 ["c", "k"],
316 )
317 .await;
318 cx.assert_neovim_compatible(
319 indoc! {"
320 The quick
321 brown fox
322 jumps ˇover"},
323 ["c", "k"],
324 )
325 .await;
326 cx.assert_neovim_compatible(
327 indoc! {"
328 The qˇuick
329 brown fox
330 jumps over"},
331 ["c", "k"],
332 )
333 .await;
334 cx.assert_neovim_compatible(
335 indoc! {"
336 ˇ
337 brown fox
338 jumps over"},
339 ["c", "k"],
340 )
341 .await;
342 }
343
344 #[gpui::test]
345 async fn test_change_j(cx: &mut gpui::TestAppContext) {
346 let mut cx = NeovimBackedTestContext::new(cx).await;
347 cx.assert_neovim_compatible(
348 indoc! {"
349 The quick
350 brown ˇfox
351 jumps over"},
352 ["c", "j"],
353 )
354 .await;
355 cx.assert_neovim_compatible(
356 indoc! {"
357 The quick
358 brown fox
359 jumps ˇover"},
360 ["c", "j"],
361 )
362 .await;
363 cx.assert_neovim_compatible(
364 indoc! {"
365 The qˇuick
366 brown fox
367 jumps over"},
368 ["c", "j"],
369 )
370 .await;
371 cx.assert_neovim_compatible(
372 indoc! {"
373 The quick
374 brown fox
375 ˇ"},
376 ["c", "j"],
377 )
378 .await;
379 }
380
381 #[gpui::test]
382 async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
383 let mut cx = NeovimBackedTestContext::new(cx).await;
384 cx.assert_neovim_compatible(
385 indoc! {"
386 The quick
387 brownˇ fox
388 jumps over
389 the lazy"},
390 ["c", "shift-g"],
391 )
392 .await;
393 cx.assert_neovim_compatible(
394 indoc! {"
395 The quick
396 brownˇ fox
397 jumps over
398 the lazy"},
399 ["c", "shift-g"],
400 )
401 .await;
402 cx.assert_neovim_compatible(
403 indoc! {"
404 The quick
405 brown fox
406 jumps over
407 the lˇazy"},
408 ["c", "shift-g"],
409 )
410 .await;
411 cx.assert_neovim_compatible(
412 indoc! {"
413 The quick
414 brown fox
415 jumps over
416 ˇ"},
417 ["c", "shift-g"],
418 )
419 .await;
420 }
421
422 #[gpui::test]
423 async fn test_change_cc(cx: &mut gpui::TestAppContext) {
424 let mut cx = NeovimBackedTestContext::new(cx).await;
425 cx.assert_neovim_compatible(
426 indoc! {"
427 The quick
428 brownˇ fox
429 jumps over
430 the lazy"},
431 ["c", "c"],
432 )
433 .await;
434
435 cx.assert_neovim_compatible(
436 indoc! {"
437 ˇThe quick
438 brown fox
439 jumps over
440 the lazy"},
441 ["c", "c"],
442 )
443 .await;
444
445 cx.assert_neovim_compatible(
446 indoc! {"
447 The quick
448 broˇwn fox
449 jumˇps over
450 the lazy"},
451 ["c", "c"],
452 )
453 .await;
454 }
455
456 #[gpui::test]
457 async fn test_change_gg(cx: &mut gpui::TestAppContext) {
458 let mut cx = NeovimBackedTestContext::new(cx).await;
459 cx.assert_neovim_compatible(
460 indoc! {"
461 The quick
462 brownˇ fox
463 jumps over
464 the lazy"},
465 ["c", "g", "g"],
466 )
467 .await;
468 cx.assert_neovim_compatible(
469 indoc! {"
470 The quick
471 brown fox
472 jumps over
473 the lˇazy"},
474 ["c", "g", "g"],
475 )
476 .await;
477 cx.assert_neovim_compatible(
478 indoc! {"
479 The qˇuick
480 brown fox
481 jumps over
482 the lazy"},
483 ["c", "g", "g"],
484 )
485 .await;
486 cx.assert_neovim_compatible(
487 indoc! {"
488 ˇ
489 brown fox
490 jumps over
491 the lazy"},
492 ["c", "g", "g"],
493 )
494 .await;
495 }
496
497 #[gpui::test]
498 async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
499 let mut cx = NeovimBackedTestContext::new(cx).await;
500
501 for count in 1..=5 {
502 cx.assert_binding_matches_all(
503 ["c", &count.to_string(), "j"],
504 indoc! {"
505 ˇThe quˇickˇ browˇn
506 ˇ
507 ˇfox ˇjumpsˇ-ˇoˇver
508 ˇthe lazy dog
509 "},
510 )
511 .await;
512 }
513 }
514
515 #[gpui::test]
516 async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
517 let mut cx = NeovimBackedTestContext::new(cx).await;
518
519 for count in 1..=5 {
520 cx.assert_binding_matches_all(
521 ["c", &count.to_string(), "l"],
522 indoc! {"
523 ˇThe quˇickˇ browˇn
524 ˇ
525 ˇfox ˇjumpsˇ-ˇoˇver
526 ˇthe lazy dog
527 "},
528 )
529 .await;
530 }
531 }
532
533 #[gpui::test]
534 async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
535 let mut cx = NeovimBackedTestContext::new(cx).await;
536
537 for count in 1..=5 {
538 for marked_text in cx.each_marked_position(indoc! {"
539 ˇThe quˇickˇ browˇn
540 ˇ
541 ˇfox ˇjumpsˇ-ˇoˇver
542 ˇthe lazy dog
543 "})
544 {
545 cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
546 .await;
547 }
548 }
549 }
550
551 #[gpui::test]
552 async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
553 let mut cx = NeovimBackedTestContext::new(cx).await;
554
555 for count in 1..=5 {
556 cx.assert_binding_matches_all(
557 ["c", &count.to_string(), "e"],
558 indoc! {"
559 ˇThe quˇickˇ browˇn
560 ˇ
561 ˇfox ˇjumpsˇ-ˇoˇver
562 ˇthe lazy dog
563 "},
564 )
565 .await;
566 }
567 }
568}