1use editor::{movement, scroll::Autoscroll, DisplayPoint, Editor};
2use gpui::{actions, Action};
3use gpui::{Context, Window};
4use language::{CharClassifier, CharKind};
5
6use crate::{motion::Motion, state::Mode, Vim};
7
8actions!(vim, [HelixNormalAfter, HelixDelete]);
9
10pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
11 Vim::action(editor, cx, Vim::helix_normal_after);
12 Vim::action(editor, cx, Vim::helix_delete);
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(Some(Autoscroll::fit()), window, cx, |s| {
51 s.move_with(|map, selection| {
52 let times = times.unwrap_or(1);
53
54 if selection.head() == map.max_point() {
55 return;
56 }
57
58 // collapse to block cursor
59 if selection.tail() < selection.head() {
60 selection.set_tail(movement::left(map, selection.head()), selection.goal);
61 } else {
62 selection.set_tail(selection.head(), selection.goal);
63 selection.set_head(movement::right(map, selection.head()), selection.goal);
64 }
65
66 // create a classifier
67 let classifier = map
68 .buffer_snapshot
69 .char_classifier_at(selection.head().to_point(map));
70
71 let mut last_selection = selection.clone();
72 for _ in 0..times {
73 let (new_tail, new_head) =
74 movement::find_boundary_trail(map, selection.head(), |left, right| {
75 is_boundary(left, right, &classifier)
76 });
77
78 selection.set_head(new_head, selection.goal);
79 if let Some(new_tail) = new_tail {
80 selection.set_tail(new_tail, selection.goal);
81 }
82
83 if selection.head() == last_selection.head()
84 && selection.tail() == last_selection.tail()
85 {
86 break;
87 }
88 last_selection = selection.clone();
89 }
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(Some(Autoscroll::fit()), window, cx, |s| {
104 s.move_with(|map, selection| {
105 let times = times.unwrap_or(1);
106
107 if selection.head() == DisplayPoint::zero() {
108 return;
109 }
110
111 // collapse to block cursor
112 if selection.tail() < selection.head() {
113 selection.set_tail(movement::left(map, selection.head()), selection.goal);
114 } else {
115 selection.set_tail(selection.head(), selection.goal);
116 selection.set_head(movement::right(map, selection.head()), selection.goal);
117 }
118
119 // flip the selection
120 selection.swap_head_tail();
121
122 // create a classifier
123 let classifier = map
124 .buffer_snapshot
125 .char_classifier_at(selection.head().to_point(map));
126
127 let mut last_selection = selection.clone();
128 for _ in 0..times {
129 let (new_tail, new_head) = movement::find_preceding_boundary_trail(
130 map,
131 selection.head(),
132 |left, right| is_boundary(left, right, &classifier),
133 );
134
135 selection.set_head(new_head, selection.goal);
136 if let Some(new_tail) = new_tail {
137 selection.set_tail(new_tail, selection.goal);
138 }
139
140 if selection.head() == last_selection.head()
141 && selection.tail() == last_selection.tail()
142 {
143 break;
144 }
145 last_selection = selection.clone();
146 }
147 });
148 })
149 });
150 }
151
152 pub fn helix_move_and_collapse(
153 &mut self,
154 motion: Motion,
155 times: Option<usize>,
156 window: &mut Window,
157 cx: &mut Context<Self>,
158 ) {
159 self.update_editor(window, cx, |_, editor, window, cx| {
160 let text_layout_details = editor.text_layout_details(window);
161 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
162 s.move_with(|map, selection| {
163 let goal = selection.goal;
164 let cursor = if selection.is_empty() || selection.reversed {
165 selection.head()
166 } else {
167 movement::left(map, selection.head())
168 };
169
170 let (point, goal) = motion
171 .move_point(map, cursor, selection.goal, times, &text_layout_details)
172 .unwrap_or((cursor, goal));
173
174 selection.collapse_to(point, goal)
175 })
176 });
177 });
178 }
179
180 pub fn helix_move_cursor(
181 &mut self,
182 motion: Motion,
183 times: Option<usize>,
184 window: &mut Window,
185 cx: &mut Context<Self>,
186 ) {
187 match motion {
188 Motion::NextWordStart { ignore_punctuation } => {
189 self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
190 let left_kind = classifier.kind_with(left, ignore_punctuation);
191 let right_kind = classifier.kind_with(right, ignore_punctuation);
192 let at_newline = right == '\n';
193
194 let found =
195 left_kind != right_kind && right_kind != CharKind::Whitespace || at_newline;
196
197 found
198 })
199 }
200 Motion::NextWordEnd { ignore_punctuation } => {
201 self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
202 let left_kind = classifier.kind_with(left, ignore_punctuation);
203 let right_kind = classifier.kind_with(right, ignore_punctuation);
204 let at_newline = right == '\n';
205
206 let found = left_kind != right_kind
207 && (left_kind != CharKind::Whitespace || at_newline);
208
209 found
210 })
211 }
212 Motion::PreviousWordStart { ignore_punctuation } => {
213 self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
214 let left_kind = classifier.kind_with(left, ignore_punctuation);
215 let right_kind = classifier.kind_with(right, ignore_punctuation);
216 let at_newline = right == '\n';
217
218 let found = left_kind != right_kind
219 && (left_kind != CharKind::Whitespace || at_newline);
220
221 found
222 })
223 }
224 Motion::PreviousWordEnd { ignore_punctuation } => {
225 self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
226 let left_kind = classifier.kind_with(left, ignore_punctuation);
227 let right_kind = classifier.kind_with(right, ignore_punctuation);
228 let at_newline = right == '\n';
229
230 let found = left_kind != right_kind
231 && right_kind != CharKind::Whitespace
232 && !at_newline;
233
234 found
235 })
236 }
237 _ => self.helix_move_and_collapse(motion, times, window, cx),
238 }
239 }
240
241 pub fn helix_delete(&mut self, _: &HelixDelete, window: &mut Window, cx: &mut Context<Self>) {
242 self.store_visual_marks(window, cx);
243 self.update_editor(window, cx, |vim, editor, window, cx| {
244 // Fixup selections so they have helix's semantics.
245 // Specifically:
246 // - Make sure that each cursor acts as a 1 character wide selection
247 editor.transact(window, cx, |editor, window, cx| {
248 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
249 s.move_with(|map, selection| {
250 if selection.is_empty() && !selection.reversed {
251 selection.end = movement::right(map, selection.end);
252 }
253 });
254 });
255 });
256
257 vim.copy_selections_content(editor, false, window, cx);
258 editor.insert("", window, cx);
259 });
260 }
261}
262
263#[cfg(test)]
264mod test {
265 use indoc::indoc;
266
267 use crate::{state::Mode, test::VimTestContext};
268
269 #[gpui::test]
270 async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
271 let mut cx = VimTestContext::new(cx, true).await;
272 // «
273 // ˇ
274 // »
275 cx.set_state(
276 indoc! {"
277 The quˇick brown
278 fox jumps over
279 the lazy dog."},
280 Mode::HelixNormal,
281 );
282
283 cx.simulate_keystrokes("w");
284
285 cx.assert_state(
286 indoc! {"
287 The qu«ick ˇ»brown
288 fox jumps over
289 the lazy dog."},
290 Mode::HelixNormal,
291 );
292
293 cx.simulate_keystrokes("w");
294
295 cx.assert_state(
296 indoc! {"
297 The quick «brownˇ»
298 fox jumps over
299 the lazy dog."},
300 Mode::HelixNormal,
301 );
302 }
303
304 #[gpui::test]
305 async fn test_delete(cx: &mut gpui::TestAppContext) {
306 let mut cx = VimTestContext::new(cx, true).await;
307
308 // test delete a selection
309 cx.set_state(
310 indoc! {"
311 The qu«ick ˇ»brown
312 fox jumps over
313 the lazy dog."},
314 Mode::HelixNormal,
315 );
316
317 cx.simulate_keystrokes("d");
318
319 cx.assert_state(
320 indoc! {"
321 The quˇbrown
322 fox jumps over
323 the lazy dog."},
324 Mode::HelixNormal,
325 );
326
327 // test deleting a single character
328 cx.simulate_keystrokes("d");
329
330 cx.assert_state(
331 indoc! {"
332 The quˇrown
333 fox jumps over
334 the lazy dog."},
335 Mode::HelixNormal,
336 );
337 }
338
339 #[gpui::test]
340 async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
341 let mut cx = VimTestContext::new(cx, true).await;
342
343 cx.set_state(
344 indoc! {"
345 The quick brownˇ
346 fox jumps over
347 the lazy dog."},
348 Mode::HelixNormal,
349 );
350
351 cx.simulate_keystrokes("d");
352
353 cx.assert_state(
354 indoc! {"
355 The quick brownˇfox jumps over
356 the lazy dog."},
357 Mode::HelixNormal,
358 );
359 }
360
361 #[gpui::test]
362 async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
363 let mut cx = VimTestContext::new(cx, true).await;
364
365 cx.set_state(
366 indoc! {"
367 The quick brown
368 fox jumps over
369 the lazy dog.ˇ"},
370 Mode::HelixNormal,
371 );
372
373 cx.simulate_keystrokes("d");
374
375 cx.assert_state(
376 indoc! {"
377 The quick brown
378 fox jumps over
379 the lazy dog.ˇ"},
380 Mode::HelixNormal,
381 );
382 }
383}