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::{ExemptionFeatures, 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.binding(["c", "0"]);
243 cx.assert(indoc! {"
244 The qˇuick
245 brown fox"})
246 .await;
247 cx.assert(indoc! {"
248 The quick
249 ˇ
250 brown fox"})
251 .await;
252 }
253
254 #[gpui::test]
255 async fn test_change_k(cx: &mut gpui::TestAppContext) {
256 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "k"]);
257 cx.assert(indoc! {"
258 The quick
259 brown ˇfox
260 jumps over"})
261 .await;
262 cx.assert(indoc! {"
263 The quick
264 brown fox
265 jumps ˇover"})
266 .await;
267 cx.assert_exempted(
268 indoc! {"
269 The qˇuick
270 brown fox
271 jumps over"},
272 ExemptionFeatures::OperatorAbortsOnFailedMotion,
273 )
274 .await;
275 cx.assert_exempted(
276 indoc! {"
277 ˇ
278 brown fox
279 jumps over"},
280 ExemptionFeatures::OperatorAbortsOnFailedMotion,
281 )
282 .await;
283 }
284
285 #[gpui::test]
286 async fn test_change_j(cx: &mut gpui::TestAppContext) {
287 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "j"]);
288 cx.assert(indoc! {"
289 The quick
290 brown ˇfox
291 jumps over"})
292 .await;
293 cx.assert_exempted(
294 indoc! {"
295 The quick
296 brown fox
297 jumps ˇover"},
298 ExemptionFeatures::OperatorAbortsOnFailedMotion,
299 )
300 .await;
301 cx.assert(indoc! {"
302 The qˇuick
303 brown fox
304 jumps over"})
305 .await;
306 cx.assert_exempted(
307 indoc! {"
308 The quick
309 brown fox
310 ˇ"},
311 ExemptionFeatures::OperatorAbortsOnFailedMotion,
312 )
313 .await;
314 }
315
316 #[gpui::test]
317 async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
318 let mut cx = NeovimBackedTestContext::new(cx)
319 .await
320 .binding(["c", "shift-g"]);
321 cx.assert(indoc! {"
322 The quick
323 brownˇ fox
324 jumps over
325 the lazy"})
326 .await;
327 cx.assert(indoc! {"
328 The quick
329 brownˇ fox
330 jumps over
331 the lazy"})
332 .await;
333 cx.assert_exempted(
334 indoc! {"
335 The quick
336 brown fox
337 jumps over
338 the lˇazy"},
339 ExemptionFeatures::OperatorAbortsOnFailedMotion,
340 )
341 .await;
342 cx.assert_exempted(
343 indoc! {"
344 The quick
345 brown fox
346 jumps over
347 ˇ"},
348 ExemptionFeatures::OperatorAbortsOnFailedMotion,
349 )
350 .await;
351 }
352
353 #[gpui::test]
354 async fn test_change_gg(cx: &mut gpui::TestAppContext) {
355 let mut cx = NeovimBackedTestContext::new(cx)
356 .await
357 .binding(["c", "g", "g"]);
358 cx.assert(indoc! {"
359 The quick
360 brownˇ fox
361 jumps over
362 the lazy"})
363 .await;
364 cx.assert(indoc! {"
365 The quick
366 brown fox
367 jumps over
368 the lˇazy"})
369 .await;
370 cx.assert_exempted(
371 indoc! {"
372 The qˇuick
373 brown fox
374 jumps over
375 the lazy"},
376 ExemptionFeatures::OperatorAbortsOnFailedMotion,
377 )
378 .await;
379 cx.assert_exempted(
380 indoc! {"
381 ˇ
382 brown fox
383 jumps over
384 the lazy"},
385 ExemptionFeatures::OperatorAbortsOnFailedMotion,
386 )
387 .await;
388 }
389
390 #[gpui::test]
391 async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
392 let mut cx = NeovimBackedTestContext::new(cx).await;
393
394 for count in 1..=5 {
395 cx.assert_binding_matches_all(
396 ["c", &count.to_string(), "j"],
397 indoc! {"
398 ˇThe quˇickˇ browˇn
399 ˇ
400 ˇfox ˇjumpsˇ-ˇoˇver
401 ˇthe lazy dog
402 "},
403 )
404 .await;
405 }
406 }
407
408 #[gpui::test]
409 async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
410 let mut cx = NeovimBackedTestContext::new(cx).await;
411
412 for count in 1..=5 {
413 cx.assert_binding_matches_all(
414 ["c", &count.to_string(), "l"],
415 indoc! {"
416 ˇThe quˇickˇ browˇn
417 ˇ
418 ˇfox ˇjumpsˇ-ˇoˇver
419 ˇthe lazy dog
420 "},
421 )
422 .await;
423 }
424 }
425
426 #[gpui::test]
427 async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
428 let mut cx = NeovimBackedTestContext::new(cx).await;
429
430 cx.add_initial_state_exemptions(
431 indoc! {"
432 ˇThe quick brown
433
434 fox jumps-over
435 the lazy dog
436 "},
437 ExemptionFeatures::OperatorAbortsOnFailedMotion,
438 );
439
440 for count in 1..=5 {
441 cx.assert_binding_matches_all(
442 ["c", &count.to_string(), "b"],
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_ce(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(), "e"],
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}