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_next_word_start(cx: &mut gpui::TestAppContext) {
306 let mut cx = VimTestContext::new(cx, true).await;
307 // «
308 // ˇ
309 // »
310 cx.set_state(
311 indoc! {"
312 The quˇick 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
339 // #[gpui::test]
340 // async fn test_delete(cx: &mut gpui::TestAppContext) {
341 // let mut cx = VimTestContext::new(cx, true).await;
342
343 // // test delete a selection
344 // cx.set_state(
345 // indoc! {"
346 // The qu«ick ˇ»brown
347 // fox jumps over
348 // the lazy dog."},
349 // Mode::HelixNormal,
350 // );
351
352 // cx.simulate_keystrokes("d");
353
354 // cx.assert_state(
355 // indoc! {"
356 // The quˇbrown
357 // fox jumps over
358 // the lazy dog."},
359 // Mode::HelixNormal,
360 // );
361
362 // // test deleting a single character
363 // cx.simulate_keystrokes("d");
364
365 // cx.assert_state(
366 // indoc! {"
367 // The quˇrown
368 // fox jumps over
369 // the lazy dog."},
370 // Mode::HelixNormal,
371 // );
372 // }
373
374 // #[gpui::test]
375 // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
376 // let mut cx = VimTestContext::new(cx, true).await;
377
378 // cx.set_state(
379 // indoc! {"
380 // The quick brownˇ
381 // fox jumps over
382 // the lazy dog."},
383 // Mode::HelixNormal,
384 // );
385
386 // cx.simulate_keystrokes("d");
387
388 // cx.assert_state(
389 // indoc! {"
390 // The quick brownˇfox jumps over
391 // the lazy dog."},
392 // Mode::HelixNormal,
393 // );
394 // }
395
396 // #[gpui::test]
397 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
398 // let mut cx = VimTestContext::new(cx, true).await;
399
400 // cx.set_state(
401 // indoc! {"
402 // The quick brown
403 // fox jumps over
404 // the lazy dog.ˇ"},
405 // Mode::HelixNormal,
406 // );
407
408 // cx.simulate_keystrokes("d");
409
410 // cx.assert_state(
411 // indoc! {"
412 // The quick brown
413 // fox jumps over
414 // the lazy dog.ˇ"},
415 // Mode::HelixNormal,
416 // );
417 // }
418
419 #[gpui::test]
420 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
421 let mut cx = VimTestContext::new(cx, true).await;
422
423 cx.set_state(
424 indoc! {"
425 The quˇick brown
426 fox jumps over
427 the lazy dog."},
428 Mode::HelixNormal,
429 );
430
431 cx.simulate_keystrokes("f z");
432
433 cx.assert_state(
434 indoc! {"
435 The qu«ick brown
436 fox jumps over
437 the lazˇ»y dog."},
438 Mode::HelixNormal,
439 );
440
441 cx.simulate_keystrokes("2 T r");
442
443 cx.assert_state(
444 indoc! {"
445 The quick br«ˇown
446 fox jumps over
447 the laz»y dog."},
448 Mode::HelixNormal,
449 );
450 }
451}