1use editor::{DisplayPoint, Editor, movement};
2use gpui::{Action, actions};
3use gpui::{Context, Window};
4use language::{CharClassifier, CharKind};
5use text::SelectionGoal;
6
7use crate::{Vim, motion::Motion, state::Mode};
8
9actions!(
10 vim,
11 [
12 /// Switches to normal mode after the cursor (Helix-style).
13 HelixNormalAfter
14 ]
15);
16
17pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
18 Vim::action(editor, cx, Vim::helix_normal_after);
19}
20
21impl Vim {
22 pub fn helix_normal_after(
23 &mut self,
24 action: &HelixNormalAfter,
25 window: &mut Window,
26 cx: &mut Context<Self>,
27 ) {
28 if self.active_operator().is_some() {
29 self.operator_stack.clear();
30 self.sync_vim_settings(window, cx);
31 return;
32 }
33 self.stop_recording_immediately(action.boxed_clone(), cx);
34 self.switch_mode(Mode::HelixNormal, false, window, cx);
35 return;
36 }
37
38 pub fn helix_normal_motion(
39 &mut self,
40 motion: Motion,
41 times: Option<usize>,
42 window: &mut Window,
43 cx: &mut Context<Self>,
44 ) {
45 self.helix_move_cursor(motion, times, window, cx);
46 }
47
48 fn helix_find_range_forward(
49 &mut self,
50 times: Option<usize>,
51 window: &mut Window,
52 cx: &mut Context<Self>,
53 mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
54 ) {
55 self.update_editor(window, cx, |_, editor, window, cx| {
56 editor.change_selections(Default::default(), window, cx, |s| {
57 s.move_with(|map, selection| {
58 let times = times.unwrap_or(1);
59 let new_goal = SelectionGoal::None;
60 let mut head = selection.head();
61 let mut tail = selection.tail();
62
63 if head == map.max_point() {
64 return;
65 }
66
67 // collapse to block cursor
68 if tail < head {
69 tail = movement::left(map, head);
70 } else {
71 tail = head;
72 head = movement::right(map, head);
73 }
74
75 // create a classifier
76 let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
77
78 for _ in 0..times {
79 let (maybe_next_tail, next_head) =
80 movement::find_boundary_trail(map, head, |left, right| {
81 is_boundary(left, right, &classifier)
82 });
83
84 if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
85 break;
86 }
87
88 head = next_head;
89 if let Some(next_tail) = maybe_next_tail {
90 tail = next_tail;
91 }
92 }
93
94 selection.set_tail(tail, new_goal);
95 selection.set_head(head, new_goal);
96 });
97 });
98 });
99 }
100
101 fn helix_find_range_backward(
102 &mut self,
103 times: Option<usize>,
104 window: &mut Window,
105 cx: &mut Context<Self>,
106 mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
107 ) {
108 self.update_editor(window, cx, |_, editor, window, cx| {
109 editor.change_selections(Default::default(), window, cx, |s| {
110 s.move_with(|map, selection| {
111 let times = times.unwrap_or(1);
112 let new_goal = SelectionGoal::None;
113 let mut head = selection.head();
114 let mut tail = selection.tail();
115
116 if head == DisplayPoint::zero() {
117 return;
118 }
119
120 // collapse to block cursor
121 if tail < head {
122 tail = movement::left(map, head);
123 } else {
124 tail = head;
125 head = movement::right(map, head);
126 }
127
128 selection.set_head(head, new_goal);
129 selection.set_tail(tail, new_goal);
130 // flip the selection
131 selection.swap_head_tail();
132 head = selection.head();
133 tail = selection.tail();
134
135 // create a classifier
136 let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
137
138 for _ in 0..times {
139 let (maybe_next_tail, next_head) =
140 movement::find_preceding_boundary_trail(map, head, |left, right| {
141 is_boundary(left, right, &classifier)
142 });
143
144 if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
145 break;
146 }
147
148 head = next_head;
149 if let Some(next_tail) = maybe_next_tail {
150 tail = next_tail;
151 }
152 }
153
154 selection.set_tail(tail, new_goal);
155 selection.set_head(head, new_goal);
156 });
157 })
158 });
159 }
160
161 pub fn helix_move_and_collapse(
162 &mut self,
163 motion: Motion,
164 times: Option<usize>,
165 window: &mut Window,
166 cx: &mut Context<Self>,
167 ) {
168 self.update_editor(window, cx, |_, editor, window, cx| {
169 let text_layout_details = editor.text_layout_details(window);
170 editor.change_selections(Default::default(), window, cx, |s| {
171 s.move_with(|map, selection| {
172 let goal = selection.goal;
173 let cursor = if selection.is_empty() || selection.reversed {
174 selection.head()
175 } else {
176 movement::left(map, selection.head())
177 };
178
179 let (point, goal) = motion
180 .move_point(map, cursor, selection.goal, times, &text_layout_details)
181 .unwrap_or((cursor, goal));
182
183 selection.collapse_to(point, goal)
184 })
185 });
186 });
187 }
188
189 pub fn helix_move_cursor(
190 &mut self,
191 motion: Motion,
192 times: Option<usize>,
193 window: &mut Window,
194 cx: &mut Context<Self>,
195 ) {
196 match motion {
197 Motion::NextWordStart { ignore_punctuation } => {
198 self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
199 let left_kind = classifier.kind_with(left, ignore_punctuation);
200 let right_kind = classifier.kind_with(right, ignore_punctuation);
201 let at_newline = (left == '\n') ^ (right == '\n');
202
203 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
204 || at_newline;
205
206 found
207 })
208 }
209 Motion::NextWordEnd { ignore_punctuation } => {
210 self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
211 let left_kind = classifier.kind_with(left, ignore_punctuation);
212 let right_kind = classifier.kind_with(right, ignore_punctuation);
213 let at_newline = (left == '\n') ^ (right == '\n');
214
215 let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)
216 || at_newline;
217
218 found
219 })
220 }
221 Motion::PreviousWordStart { ignore_punctuation } => {
222 self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
223 let left_kind = classifier.kind_with(left, ignore_punctuation);
224 let right_kind = classifier.kind_with(right, ignore_punctuation);
225 let at_newline = (left == '\n') ^ (right == '\n');
226
227 let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)
228 || at_newline;
229
230 found
231 })
232 }
233 Motion::PreviousWordEnd { ignore_punctuation } => {
234 self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
235 let left_kind = classifier.kind_with(left, ignore_punctuation);
236 let right_kind = classifier.kind_with(right, ignore_punctuation);
237 let at_newline = (left == '\n') ^ (right == '\n');
238
239 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
240 || at_newline;
241
242 found
243 })
244 }
245 Motion::FindForward { .. } => {
246 self.update_editor(window, cx, |_, editor, window, cx| {
247 let text_layout_details = editor.text_layout_details(window);
248 editor.change_selections(Default::default(), window, cx, |s| {
249 s.move_with(|map, selection| {
250 let goal = selection.goal;
251 let cursor = if selection.is_empty() || selection.reversed {
252 selection.head()
253 } else {
254 movement::left(map, selection.head())
255 };
256
257 let (point, goal) = motion
258 .move_point(
259 map,
260 cursor,
261 selection.goal,
262 times,
263 &text_layout_details,
264 )
265 .unwrap_or((cursor, goal));
266 selection.set_tail(selection.head(), goal);
267 selection.set_head(movement::right(map, point), goal);
268 })
269 });
270 });
271 }
272 Motion::FindBackward { .. } => {
273 self.update_editor(window, cx, |_, editor, window, cx| {
274 let text_layout_details = editor.text_layout_details(window);
275 editor.change_selections(Default::default(), window, cx, |s| {
276 s.move_with(|map, selection| {
277 let goal = selection.goal;
278 let cursor = if selection.is_empty() || selection.reversed {
279 selection.head()
280 } else {
281 movement::left(map, selection.head())
282 };
283
284 let (point, goal) = motion
285 .move_point(
286 map,
287 cursor,
288 selection.goal,
289 times,
290 &text_layout_details,
291 )
292 .unwrap_or((cursor, goal));
293 selection.set_tail(selection.head(), goal);
294 selection.set_head(point, goal);
295 })
296 });
297 });
298 }
299 _ => self.helix_move_and_collapse(motion, times, window, cx),
300 }
301 }
302}
303
304#[cfg(test)]
305mod test {
306 use indoc::indoc;
307
308 use crate::{state::Mode, test::VimTestContext};
309
310 #[gpui::test]
311 async fn test_word_motions(cx: &mut gpui::TestAppContext) {
312 let mut cx = VimTestContext::new(cx, true).await;
313 // «
314 // ˇ
315 // »
316 cx.set_state(
317 indoc! {"
318 Th«e quiˇ»ck brown
319 fox jumps over
320 the lazy dog."},
321 Mode::HelixNormal,
322 );
323
324 cx.simulate_keystrokes("w");
325
326 cx.assert_state(
327 indoc! {"
328 The qu«ick ˇ»brown
329 fox jumps over
330 the lazy dog."},
331 Mode::HelixNormal,
332 );
333
334 cx.simulate_keystrokes("w");
335
336 cx.assert_state(
337 indoc! {"
338 The quick «brownˇ»
339 fox jumps over
340 the lazy dog."},
341 Mode::HelixNormal,
342 );
343
344 cx.simulate_keystrokes("2 b");
345
346 cx.assert_state(
347 indoc! {"
348 The «ˇquick »brown
349 fox jumps over
350 the lazy dog."},
351 Mode::HelixNormal,
352 );
353
354 cx.simulate_keystrokes("down e up");
355
356 cx.assert_state(
357 indoc! {"
358 The quicˇk brown
359 fox jumps over
360 the lazy dog."},
361 Mode::HelixNormal,
362 );
363
364 cx.set_state("aa\n «ˇbb»", Mode::HelixNormal);
365
366 cx.simulate_keystroke("b");
367
368 cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal);
369 }
370
371 #[gpui::test]
372 async fn test_delete(cx: &mut gpui::TestAppContext) {
373 let mut cx = VimTestContext::new(cx, true).await;
374
375 // test delete a selection
376 cx.set_state(
377 indoc! {"
378 The qu«ick ˇ»brown
379 fox jumps over
380 the lazy dog."},
381 Mode::HelixNormal,
382 );
383
384 cx.simulate_keystrokes("d");
385
386 cx.assert_state(
387 indoc! {"
388 The quˇbrown
389 fox jumps over
390 the lazy dog."},
391 Mode::HelixNormal,
392 );
393
394 // test deleting a single character
395 cx.simulate_keystrokes("d");
396
397 cx.assert_state(
398 indoc! {"
399 The quˇrown
400 fox jumps over
401 the lazy dog."},
402 Mode::HelixNormal,
403 );
404 }
405
406 // #[gpui::test]
407 // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
408 // let mut cx = VimTestContext::new(cx, true).await;
409
410 // cx.set_state(
411 // indoc! {"
412 // The quick brownˇ
413 // fox jumps over
414 // the lazy dog."},
415 // Mode::HelixNormal,
416 // );
417
418 // cx.simulate_keystrokes("d");
419
420 // cx.assert_state(
421 // indoc! {"
422 // The quick brownˇfox jumps over
423 // the lazy dog."},
424 // Mode::HelixNormal,
425 // );
426 // }
427
428 // #[gpui::test]
429 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
430 // let mut cx = VimTestContext::new(cx, true).await;
431
432 // cx.set_state(
433 // indoc! {"
434 // The quick brown
435 // fox jumps over
436 // the lazy dog.ˇ"},
437 // Mode::HelixNormal,
438 // );
439
440 // cx.simulate_keystrokes("d");
441
442 // cx.assert_state(
443 // indoc! {"
444 // The quick brown
445 // fox jumps over
446 // the lazy dog.ˇ"},
447 // Mode::HelixNormal,
448 // );
449 // }
450
451 #[gpui::test]
452 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
453 let mut cx = VimTestContext::new(cx, true).await;
454
455 cx.set_state(
456 indoc! {"
457 The quˇick brown
458 fox jumps over
459 the lazy dog."},
460 Mode::HelixNormal,
461 );
462
463 cx.simulate_keystrokes("f z");
464
465 cx.assert_state(
466 indoc! {"
467 The qu«ick brown
468 fox jumps over
469 the lazˇ»y dog."},
470 Mode::HelixNormal,
471 );
472
473 cx.simulate_keystrokes("2 T r");
474
475 cx.assert_state(
476 indoc! {"
477 The quick br«ˇown
478 fox jumps over
479 the laz»y dog."},
480 Mode::HelixNormal,
481 );
482 }
483
484 #[gpui::test]
485 async fn test_newline_char(cx: &mut gpui::TestAppContext) {
486 let mut cx = VimTestContext::new(cx, true).await;
487
488 cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
489
490 cx.simulate_keystroke("w");
491
492 cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
493
494 cx.set_state("aa«\nˇ»", Mode::HelixNormal);
495
496 cx.simulate_keystroke("b");
497
498 cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
499 }
500}