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