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