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