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