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