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