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