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: 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: usize,
82 ignore_punctuation: bool,
83) -> bool {
84 if times == 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 }.expand_selection(map, selection, 1, false)
101 }
102 } else {
103 Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
104 }
105}
106
107#[cfg(test)]
108mod test {
109 use indoc::indoc;
110
111 use crate::test::{ExemptionFeatures, NeovimBackedTestContext};
112
113 #[gpui::test]
114 async fn test_change_h(cx: &mut gpui::TestAppContext) {
115 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "h"]);
116 cx.assert("Teˇst").await;
117 cx.assert("Tˇest").await;
118 cx.assert("ˇTest").await;
119 cx.assert(indoc! {"
120 Test
121 ˇtest"})
122 .await;
123 }
124
125 #[gpui::test]
126 async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
127 let mut cx = NeovimBackedTestContext::new(cx)
128 .await
129 .binding(["c", "backspace"]);
130 cx.assert("Teˇst").await;
131 cx.assert("Tˇest").await;
132 cx.assert("ˇTest").await;
133 cx.assert(indoc! {"
134 Test
135 ˇtest"})
136 .await;
137 }
138
139 #[gpui::test]
140 async fn test_change_l(cx: &mut gpui::TestAppContext) {
141 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "l"]);
142 cx.assert("Teˇst").await;
143 cx.assert("Tesˇt").await;
144 }
145
146 #[gpui::test]
147 async fn test_change_w(cx: &mut gpui::TestAppContext) {
148 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "w"]);
149 cx.assert("Teˇst").await;
150 cx.assert("Tˇest test").await;
151 cx.assert("Testˇ test").await;
152 cx.assert(indoc! {"
153 Test teˇst
154 test"})
155 .await;
156 cx.assert(indoc! {"
157 Test tesˇt
158 test"})
159 .await;
160 cx.assert(indoc! {"
161 Test test
162 ˇ
163 test"})
164 .await;
165
166 let mut cx = cx.binding(["c", "shift-w"]);
167 cx.assert("Test teˇst-test test").await;
168 }
169
170 #[gpui::test]
171 async fn test_change_e(cx: &mut gpui::TestAppContext) {
172 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "e"]);
173 cx.assert("Teˇst Test").await;
174 cx.assert("Tˇest test").await;
175 cx.assert(indoc! {"
176 Test teˇst
177 test"})
178 .await;
179 cx.assert(indoc! {"
180 Test tesˇt
181 test"})
182 .await;
183 cx.assert(indoc! {"
184 Test test
185 ˇ
186 test"})
187 .await;
188
189 let mut cx = cx.binding(["c", "shift-e"]);
190 cx.assert("Test teˇst-test test").await;
191 }
192
193 #[gpui::test]
194 async fn test_change_b(cx: &mut gpui::TestAppContext) {
195 let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "b"]);
196 cx.assert("Teˇst Test").await;
197 cx.assert("Test ˇtest").await;
198 cx.assert("Test1 test2 ˇtest3").await;
199 cx.assert(indoc! {"
200 Test test
201 ˇtest"})
202 .await;
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}