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 _ => self.helix_move_and_collapse(motion, times, window, cx),
239 }
240 }
241
242 pub fn helix_delete(&mut self, _: &HelixDelete, window: &mut Window, cx: &mut Context<Self>) {
243 self.store_visual_marks(window, cx);
244 self.update_editor(window, cx, |vim, editor, window, cx| {
245 // Fixup selections so they have helix's semantics.
246 // Specifically:
247 // - Make sure that each cursor acts as a 1 character wide selection
248 editor.transact(window, cx, |editor, window, cx| {
249 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
250 s.move_with(|map, selection| {
251 if selection.is_empty() && !selection.reversed {
252 selection.end = movement::right(map, selection.end);
253 }
254 });
255 });
256 });
257
258 vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx);
259 editor.insert("", window, cx);
260 });
261 }
262}
263
264#[cfg(test)]
265mod test {
266 use indoc::indoc;
267
268 use crate::{state::Mode, test::VimTestContext};
269
270 #[gpui::test]
271 async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
272 let mut cx = VimTestContext::new(cx, true).await;
273 // «
274 // ˇ
275 // »
276 cx.set_state(
277 indoc! {"
278 The quˇick brown
279 fox jumps over
280 the lazy dog."},
281 Mode::HelixNormal,
282 );
283
284 cx.simulate_keystrokes("w");
285
286 cx.assert_state(
287 indoc! {"
288 The qu«ick ˇ»brown
289 fox jumps over
290 the lazy dog."},
291 Mode::HelixNormal,
292 );
293
294 cx.simulate_keystrokes("w");
295
296 cx.assert_state(
297 indoc! {"
298 The quick «brownˇ»
299 fox jumps over
300 the lazy dog."},
301 Mode::HelixNormal,
302 );
303 }
304
305 // #[gpui::test]
306 // async fn test_delete(cx: &mut gpui::TestAppContext) {
307 // let mut cx = VimTestContext::new(cx, true).await;
308
309 // // test delete a selection
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("d");
319
320 // cx.assert_state(
321 // indoc! {"
322 // The quˇbrown
323 // fox jumps over
324 // the lazy dog."},
325 // Mode::HelixNormal,
326 // );
327
328 // // test deleting a single character
329 // cx.simulate_keystrokes("d");
330
331 // cx.assert_state(
332 // indoc! {"
333 // The quˇrown
334 // fox jumps over
335 // the lazy dog."},
336 // Mode::HelixNormal,
337 // );
338 // }
339
340 // #[gpui::test]
341 // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
342 // let mut cx = VimTestContext::new(cx, true).await;
343
344 // cx.set_state(
345 // indoc! {"
346 // The quick 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 quick brownˇfox jumps over
357 // the lazy dog."},
358 // Mode::HelixNormal,
359 // );
360 // }
361
362 // #[gpui::test]
363 // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
364 // let mut cx = VimTestContext::new(cx, true).await;
365
366 // cx.set_state(
367 // indoc! {"
368 // The quick brown
369 // fox jumps over
370 // the lazy dog.ˇ"},
371 // Mode::HelixNormal,
372 // );
373
374 // cx.simulate_keystrokes("d");
375
376 // cx.assert_state(
377 // indoc! {"
378 // The quick brown
379 // fox jumps over
380 // the lazy dog.ˇ"},
381 // Mode::HelixNormal,
382 // );
383 // }
384}