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