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