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 = right == '\n';
192
193 let found =
194 left_kind != right_kind && right_kind != CharKind::Whitespace || 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 = right == '\n';
204
205 let found = left_kind != right_kind
206 && (left_kind != CharKind::Whitespace || 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 = right == '\n';
216
217 let found = left_kind != right_kind
218 && (left_kind != CharKind::Whitespace || 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 = right == '\n';
228
229 let found = left_kind != right_kind
230 && right_kind != CharKind::Whitespace
231 && !at_newline;
232
233 found
234 })
235 }
236 Motion::FindForward { .. } => {
237 self.update_editor(window, cx, |_, editor, window, cx| {
238 let text_layout_details = editor.text_layout_details(window);
239 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
240 s.move_with(|map, selection| {
241 let goal = selection.goal;
242 let cursor = if selection.is_empty() || selection.reversed {
243 selection.head()
244 } else {
245 movement::left(map, selection.head())
246 };
247
248 let (point, goal) = motion
249 .move_point(
250 map,
251 cursor,
252 selection.goal,
253 times,
254 &text_layout_details,
255 )
256 .unwrap_or((cursor, goal));
257 selection.set_tail(selection.head(), goal);
258 selection.set_head(movement::right(map, point), goal);
259 })
260 });
261 });
262 }
263 Motion::FindBackward { .. } => {
264 self.update_editor(window, cx, |_, editor, window, cx| {
265 let text_layout_details = editor.text_layout_details(window);
266 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
267 s.move_with(|map, selection| {
268 let goal = selection.goal;
269 let cursor = if selection.is_empty() || selection.reversed {
270 selection.head()
271 } else {
272 movement::left(map, selection.head())
273 };
274
275 let (point, goal) = motion
276 .move_point(
277 map,
278 cursor,
279 selection.goal,
280 times,
281 &text_layout_details,
282 )
283 .unwrap_or((cursor, goal));
284 selection.set_tail(selection.head(), goal);
285 selection.set_head(point, goal);
286 })
287 });
288 });
289 }
290 _ => self.helix_move_and_collapse(motion, times, window, cx),
291 }
292 }
293}
294
295#[cfg(test)]
296mod test {
297 use indoc::indoc;
298
299 use crate::{state::Mode, test::VimTestContext};
300
301 #[gpui::test]
302 async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
303 let mut cx = VimTestContext::new(cx, true).await;
304 // «
305 // ˇ
306 // »
307 cx.set_state(
308 indoc! {"
309 The quˇick brown
310 fox jumps over
311 the lazy dog."},
312 Mode::HelixNormal,
313 );
314
315 cx.simulate_keystrokes("w");
316
317 cx.assert_state(
318 indoc! {"
319 The qu«ick ˇ»brown
320 fox jumps over
321 the lazy dog."},
322 Mode::HelixNormal,
323 );
324
325 cx.simulate_keystrokes("w");
326
327 cx.assert_state(
328 indoc! {"
329 The quick «brownˇ»
330 fox jumps over
331 the lazy dog."},
332 Mode::HelixNormal,
333 );
334 }
335
336 // #[gpui::test]
337 // async fn test_delete(cx: &mut gpui::TestAppContext) {
338 // let mut cx = VimTestContext::new(cx, true).await;
339
340 // // test delete a selection
341 // cx.set_state(
342 // indoc! {"
343 // The qu«ick ˇ»brown
344 // fox jumps over
345 // the lazy dog."},
346 // Mode::HelixNormal,
347 // );
348
349 // cx.simulate_keystrokes("d");
350
351 // cx.assert_state(
352 // indoc! {"
353 // The quˇbrown
354 // fox jumps over
355 // the lazy dog."},
356 // Mode::HelixNormal,
357 // );
358
359 // // test deleting a single character
360 // cx.simulate_keystrokes("d");
361
362 // cx.assert_state(
363 // indoc! {"
364 // The quˇrown
365 // fox jumps over
366 // the lazy dog."},
367 // Mode::HelixNormal,
368 // );
369 // }
370
371 // #[gpui::test]
372 // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
373 // let mut cx = VimTestContext::new(cx, true).await;
374
375 // cx.set_state(
376 // indoc! {"
377 // The quick brownˇ
378 // fox jumps over
379 // the lazy dog."},
380 // Mode::HelixNormal,
381 // );
382
383 // cx.simulate_keystrokes("d");
384
385 // cx.assert_state(
386 // indoc! {"
387 // The quick brownˇfox jumps over
388 // the lazy dog."},
389 // Mode::HelixNormal,
390 // );
391 // }
392
393 // #[gpui::test]
394 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
395 // let mut cx = VimTestContext::new(cx, true).await;
396
397 // cx.set_state(
398 // indoc! {"
399 // The quick brown
400 // fox jumps over
401 // the lazy dog.ˇ"},
402 // Mode::HelixNormal,
403 // );
404
405 // cx.simulate_keystrokes("d");
406
407 // cx.assert_state(
408 // indoc! {"
409 // The quick brown
410 // fox jumps over
411 // the lazy dog.ˇ"},
412 // Mode::HelixNormal,
413 // );
414 // }
415
416 #[gpui::test]
417 async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
418 let mut cx = VimTestContext::new(cx, true).await;
419
420 cx.set_state(
421 indoc! {"
422 The quˇick brown
423 fox jumps over
424 the lazy dog."},
425 Mode::HelixNormal,
426 );
427
428 cx.simulate_keystrokes("f z");
429
430 cx.assert_state(
431 indoc! {"
432 The qu«ick brown
433 fox jumps over
434 the lazˇ»y dog."},
435 Mode::HelixNormal,
436 );
437
438 cx.simulate_keystrokes("2 T r");
439
440 cx.assert_state(
441 indoc! {"
442 The quick br«ˇown
443 fox jumps over
444 the laz»y dog."},
445 Mode::HelixNormal,
446 );
447 }
448}