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