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}
79//
80// NOT HANDLED YET
81// Another special case: When using the "w" motion in combination with an
82// operator and the last word moved over is at the end of a line, the end of
83// that word becomes the end of the operated text, not the first word in the
84// next line.
85fn expand_changed_word_selection(
86 map: &DisplaySnapshot,
87 selection: &mut Selection<DisplayPoint>,
88 times: Option<usize>,
89 ignore_punctuation: bool,
90) -> bool {
91 if times.is_none() || times.unwrap() == 1 {
92 let scope = map
93 .buffer_snapshot
94 .language_scope_at(selection.start.to_point(map));
95 let in_word = map
96 .chars_at(selection.head())
97 .next()
98 .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
99 .unwrap_or_default();
100
101 if in_word {
102 selection.end =
103 movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
104 let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
105 let right_kind =
106 char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
107
108 left_kind != right_kind && left_kind != CharKind::Whitespace
109 });
110 true
111 } else {
112 Motion::NextWordStart { ignore_punctuation }
113 .expand_selection(map, selection, None, false)
114 }
115 } else {
116 Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
117 }
118}
119
120#[cfg(test)]
121mod test {
122 use indoc::indoc;
123
124 use crate::test::NeovimBackedTestContext;
125
126 #[gpui::test]
127 async fn test_change_h(cx: &mut gpui::TestAppContext) {
128 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "h"]);
129 cx.assert("Teˇst").await;
130 cx.assert("Tˇest").await;
131 cx.assert("ˇTest").await;
132 cx.assert(indoc! {"
133 Test
134 ˇtest"})
135 .await;
136 }
137
138 #[gpui::test]
139 async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
140 let mut cx = NeovimBackedTestContext::new(cx)
141 .await
142 .binding(["c", "backspace"]);
143 cx.assert("Teˇst").await;
144 cx.assert("Tˇest").await;
145 cx.assert("ˇTest").await;
146 cx.assert(indoc! {"
147 Test
148 ˇtest"})
149 .await;
150 }
151
152 #[gpui::test]
153 async fn test_change_l(cx: &mut gpui::TestAppContext) {
154 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "l"]);
155 cx.assert("Teˇst").await;
156 cx.assert("Tesˇt").await;
157 }
158
159 #[gpui::test]
160 async fn test_change_w(cx: &mut gpui::TestAppContext) {
161 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "w"]);
162 cx.assert("Teˇst").await;
163 cx.assert("Tˇest test").await;
164 cx.assert("Testˇ test").await;
165 cx.assert(indoc! {"
166 Test teˇst
167 test"})
168 .await;
169 cx.assert(indoc! {"
170 Test tesˇt
171 test"})
172 .await;
173 cx.assert(indoc! {"
174 Test test
175 ˇ
176 test"})
177 .await;
178
179 let mut cx = cx.binding(["c", "shift-w"]);
180 cx.assert("Test teˇst-test test").await;
181 }
182
183 #[gpui::test]
184 async fn test_change_e(cx: &mut gpui::TestAppContext) {
185 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "e"]);
186 cx.assert("Teˇst Test").await;
187 cx.assert("Tˇest test").await;
188 cx.assert(indoc! {"
189 Test teˇst
190 test"})
191 .await;
192 cx.assert(indoc! {"
193 Test tesˇt
194 test"})
195 .await;
196 cx.assert(indoc! {"
197 Test test
198 ˇ
199 test"})
200 .await;
201
202 let mut cx = cx.binding(["c", "shift-e"]);
203 cx.assert("Test teˇst-test test").await;
204 }
205
206 #[gpui::test]
207 async fn test_change_b(cx: &mut gpui::TestAppContext) {
208 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "b"]);
209 cx.assert("Teˇst Test").await;
210 cx.assert("Test ˇtest").await;
211 cx.assert("Test1 test2 ˇtest3").await;
212 cx.assert(indoc! {"
213 Test test
214 ˇtest"})
215 .await;
216 cx.assert(indoc! {"
217 Test test
218 ˇ
219 test"})
220 .await;
221
222 let mut cx = cx.binding(["c", "shift-b"]);
223 cx.assert("Test test-test ˇtest").await;
224 }
225
226 #[gpui::test]
227 async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
228 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "$"]);
229 cx.assert(indoc! {"
230 The qˇuick
231 brown fox"})
232 .await;
233 cx.assert(indoc! {"
234 The quick
235 ˇ
236 brown fox"})
237 .await;
238 }
239
240 #[gpui::test]
241 async fn test_change_0(cx: &mut gpui::TestAppContext) {
242 let mut cx = NeovimBackedTestContext::new(cx).await;
243
244 cx.assert_neovim_compatible(
245 indoc! {"
246 The qˇuick
247 brown fox"},
248 ["c", "0"],
249 )
250 .await;
251 cx.assert_neovim_compatible(
252 indoc! {"
253 The quick
254 ˇ
255 brown fox"},
256 ["c", "0"],
257 )
258 .await;
259 }
260
261 #[gpui::test]
262 async fn test_change_k(cx: &mut gpui::TestAppContext) {
263 let mut cx = NeovimBackedTestContext::new(cx).await;
264
265 cx.assert_neovim_compatible(
266 indoc! {"
267 The quick
268 brown ˇfox
269 jumps over"},
270 ["c", "k"],
271 )
272 .await;
273 cx.assert_neovim_compatible(
274 indoc! {"
275 The quick
276 brown fox
277 jumps ˇover"},
278 ["c", "k"],
279 )
280 .await;
281 cx.assert_neovim_compatible(
282 indoc! {"
283 The qˇuick
284 brown fox
285 jumps over"},
286 ["c", "k"],
287 )
288 .await;
289 cx.assert_neovim_compatible(
290 indoc! {"
291 ˇ
292 brown fox
293 jumps over"},
294 ["c", "k"],
295 )
296 .await;
297 }
298
299 #[gpui::test]
300 async fn test_change_j(cx: &mut gpui::TestAppContext) {
301 let mut cx = NeovimBackedTestContext::new(cx).await;
302 cx.assert_neovim_compatible(
303 indoc! {"
304 The quick
305 brown ˇfox
306 jumps over"},
307 ["c", "j"],
308 )
309 .await;
310 cx.assert_neovim_compatible(
311 indoc! {"
312 The quick
313 brown fox
314 jumps ˇover"},
315 ["c", "j"],
316 )
317 .await;
318 cx.assert_neovim_compatible(
319 indoc! {"
320 The qˇuick
321 brown fox
322 jumps over"},
323 ["c", "j"],
324 )
325 .await;
326 cx.assert_neovim_compatible(
327 indoc! {"
328 The quick
329 brown fox
330 ˇ"},
331 ["c", "j"],
332 )
333 .await;
334 }
335
336 #[gpui::test]
337 async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
338 let mut cx = NeovimBackedTestContext::new(cx).await;
339 cx.assert_neovim_compatible(
340 indoc! {"
341 The quick
342 brownˇ fox
343 jumps over
344 the lazy"},
345 ["c", "shift-g"],
346 )
347 .await;
348 cx.assert_neovim_compatible(
349 indoc! {"
350 The quick
351 brownˇ fox
352 jumps over
353 the lazy"},
354 ["c", "shift-g"],
355 )
356 .await;
357 cx.assert_neovim_compatible(
358 indoc! {"
359 The quick
360 brown fox
361 jumps over
362 the lˇazy"},
363 ["c", "shift-g"],
364 )
365 .await;
366 cx.assert_neovim_compatible(
367 indoc! {"
368 The quick
369 brown fox
370 jumps over
371 ˇ"},
372 ["c", "shift-g"],
373 )
374 .await;
375 }
376
377 #[gpui::test]
378 async fn test_change_gg(cx: &mut gpui::TestAppContext) {
379 let mut cx = NeovimBackedTestContext::new(cx).await;
380 cx.assert_neovim_compatible(
381 indoc! {"
382 The quick
383 brownˇ fox
384 jumps over
385 the lazy"},
386 ["c", "g", "g"],
387 )
388 .await;
389 cx.assert_neovim_compatible(
390 indoc! {"
391 The quick
392 brown fox
393 jumps over
394 the lˇazy"},
395 ["c", "g", "g"],
396 )
397 .await;
398 cx.assert_neovim_compatible(
399 indoc! {"
400 The qˇuick
401 brown fox
402 jumps over
403 the lazy"},
404 ["c", "g", "g"],
405 )
406 .await;
407 cx.assert_neovim_compatible(
408 indoc! {"
409 ˇ
410 brown fox
411 jumps over
412 the lazy"},
413 ["c", "g", "g"],
414 )
415 .await;
416 }
417
418 #[gpui::test]
419 async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
420 let mut cx = NeovimBackedTestContext::new(cx).await;
421
422 for count in 1..=5 {
423 cx.assert_binding_matches_all(
424 ["c", &count.to_string(), "j"],
425 indoc! {"
426 ˇThe quˇickˇ browˇn
427 ˇ
428 ˇfox ˇjumpsˇ-ˇoˇver
429 ˇthe lazy dog
430 "},
431 )
432 .await;
433 }
434 }
435
436 #[gpui::test]
437 async fn test_repeated_cl(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(), "l"],
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_cb(cx: &mut gpui::TestAppContext) {
456 let mut cx = NeovimBackedTestContext::new(cx).await;
457
458 for count in 1..=5 {
459 for marked_text in cx.each_marked_position(indoc! {"
460 ˇThe quˇickˇ browˇn
461 ˇ
462 ˇfox ˇjumpsˇ-ˇoˇver
463 ˇthe lazy dog
464 "})
465 {
466 cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
467 .await;
468 }
469 }
470 }
471
472 #[gpui::test]
473 async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
474 let mut cx = NeovimBackedTestContext::new(cx).await;
475
476 for count in 1..=5 {
477 cx.assert_binding_matches_all(
478 ["c", &count.to_string(), "e"],
479 indoc! {"
480 ˇThe quˇickˇ browˇn
481 ˇ
482 ˇfox ˇjumpsˇ-ˇoˇver
483 ˇthe lazy dog
484 "},
485 )
486 .await;
487 }
488 }
489}