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