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