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