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