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