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