helix.rs

  1use editor::{DisplayPoint, Editor, movement};
  2use gpui::{Action, actions};
  3use gpui::{Context, Window};
  4use language::{CharClassifier, CharKind};
  5use text::SelectionGoal;
  6
  7use crate::{Vim, motion::Motion, state::Mode};
  8
  9actions!(vim, [HelixNormalAfter]);
 10
 11pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 12    Vim::action(editor, cx, Vim::helix_normal_after);
 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(Default::default(), window, cx, |s| {
 51                s.move_with(|map, selection| {
 52                    let times = times.unwrap_or(1);
 53                    let new_goal = SelectionGoal::None;
 54                    let mut head = selection.head();
 55                    let mut tail = selection.tail();
 56
 57                    if head == map.max_point() {
 58                        return;
 59                    }
 60
 61                    // collapse to block cursor
 62                    if tail < head {
 63                        tail = movement::left(map, head);
 64                    } else {
 65                        tail = head;
 66                        head = movement::right(map, head);
 67                    }
 68
 69                    // create a classifier
 70                    let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
 71
 72                    for _ in 0..times {
 73                        let (maybe_next_tail, next_head) =
 74                            movement::find_boundary_trail(map, head, |left, right| {
 75                                is_boundary(left, right, &classifier)
 76                            });
 77
 78                        if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
 79                            break;
 80                        }
 81
 82                        head = next_head;
 83                        if let Some(next_tail) = maybe_next_tail {
 84                            tail = next_tail;
 85                        }
 86                    }
 87
 88                    selection.set_tail(tail, new_goal);
 89                    selection.set_head(head, new_goal);
 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(Default::default(), window, cx, |s| {
104                s.move_with(|map, selection| {
105                    let times = times.unwrap_or(1);
106                    let new_goal = SelectionGoal::None;
107                    let mut head = selection.head();
108                    let mut tail = selection.tail();
109
110                    if head == DisplayPoint::zero() {
111                        return;
112                    }
113
114                    // collapse to block cursor
115                    if tail < head {
116                        tail = movement::left(map, head);
117                    } else {
118                        tail = head;
119                        head = movement::right(map, head);
120                    }
121
122                    selection.set_head(head, new_goal);
123                    selection.set_tail(tail, new_goal);
124                    // flip the selection
125                    selection.swap_head_tail();
126                    head = selection.head();
127                    tail = selection.tail();
128
129                    // create a classifier
130                    let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
131
132                    for _ in 0..times {
133                        let (maybe_next_tail, next_head) =
134                            movement::find_preceding_boundary_trail(map, head, |left, right| {
135                                is_boundary(left, right, &classifier)
136                            });
137
138                        if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
139                            break;
140                        }
141
142                        head = next_head;
143                        if let Some(next_tail) = maybe_next_tail {
144                            tail = next_tail;
145                        }
146                    }
147
148                    selection.set_tail(tail, new_goal);
149                    selection.set_head(head, new_goal);
150                });
151            })
152        });
153    }
154
155    pub fn helix_move_and_collapse(
156        &mut self,
157        motion: Motion,
158        times: Option<usize>,
159        window: &mut Window,
160        cx: &mut Context<Self>,
161    ) {
162        self.update_editor(window, cx, |_, editor, window, cx| {
163            let text_layout_details = editor.text_layout_details(window);
164            editor.change_selections(Default::default(), window, cx, |s| {
165                s.move_with(|map, selection| {
166                    let goal = selection.goal;
167                    let cursor = if selection.is_empty() || selection.reversed {
168                        selection.head()
169                    } else {
170                        movement::left(map, selection.head())
171                    };
172
173                    let (point, goal) = motion
174                        .move_point(map, cursor, selection.goal, times, &text_layout_details)
175                        .unwrap_or((cursor, goal));
176
177                    selection.collapse_to(point, goal)
178                })
179            });
180        });
181    }
182
183    pub fn helix_move_cursor(
184        &mut self,
185        motion: Motion,
186        times: Option<usize>,
187        window: &mut Window,
188        cx: &mut Context<Self>,
189    ) {
190        match motion {
191            Motion::NextWordStart { ignore_punctuation } => {
192                self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
193                    let left_kind = classifier.kind_with(left, ignore_punctuation);
194                    let right_kind = classifier.kind_with(right, ignore_punctuation);
195                    let at_newline = (left == '\n') ^ (right == '\n');
196
197                    let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
198                        || at_newline;
199
200                    found
201                })
202            }
203            Motion::NextWordEnd { ignore_punctuation } => {
204                self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
205                    let left_kind = classifier.kind_with(left, ignore_punctuation);
206                    let right_kind = classifier.kind_with(right, ignore_punctuation);
207                    let at_newline = (left == '\n') ^ (right == '\n');
208
209                    let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)
210                        || at_newline;
211
212                    found
213                })
214            }
215            Motion::PreviousWordStart { ignore_punctuation } => {
216                self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
217                    let left_kind = classifier.kind_with(left, ignore_punctuation);
218                    let right_kind = classifier.kind_with(right, ignore_punctuation);
219                    let at_newline = (left == '\n') ^ (right == '\n');
220
221                    let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)
222                        || at_newline;
223
224                    found
225                })
226            }
227            Motion::PreviousWordEnd { ignore_punctuation } => {
228                self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
229                    let left_kind = classifier.kind_with(left, ignore_punctuation);
230                    let right_kind = classifier.kind_with(right, ignore_punctuation);
231                    let at_newline = (left == '\n') ^ (right == '\n');
232
233                    let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
234                        || at_newline;
235
236                    found
237                })
238            }
239            Motion::FindForward { .. } => {
240                self.update_editor(window, cx, |_, editor, window, cx| {
241                    let text_layout_details = editor.text_layout_details(window);
242                    editor.change_selections(Default::default(), window, cx, |s| {
243                        s.move_with(|map, selection| {
244                            let goal = selection.goal;
245                            let cursor = if selection.is_empty() || selection.reversed {
246                                selection.head()
247                            } else {
248                                movement::left(map, selection.head())
249                            };
250
251                            let (point, goal) = motion
252                                .move_point(
253                                    map,
254                                    cursor,
255                                    selection.goal,
256                                    times,
257                                    &text_layout_details,
258                                )
259                                .unwrap_or((cursor, goal));
260                            selection.set_tail(selection.head(), goal);
261                            selection.set_head(movement::right(map, point), goal);
262                        })
263                    });
264                });
265            }
266            Motion::FindBackward { .. } => {
267                self.update_editor(window, cx, |_, editor, window, cx| {
268                    let text_layout_details = editor.text_layout_details(window);
269                    editor.change_selections(Default::default(), window, cx, |s| {
270                        s.move_with(|map, selection| {
271                            let goal = selection.goal;
272                            let cursor = if selection.is_empty() || selection.reversed {
273                                selection.head()
274                            } else {
275                                movement::left(map, selection.head())
276                            };
277
278                            let (point, goal) = motion
279                                .move_point(
280                                    map,
281                                    cursor,
282                                    selection.goal,
283                                    times,
284                                    &text_layout_details,
285                                )
286                                .unwrap_or((cursor, goal));
287                            selection.set_tail(selection.head(), goal);
288                            selection.set_head(point, goal);
289                        })
290                    });
291                });
292            }
293            _ => self.helix_move_and_collapse(motion, times, window, cx),
294        }
295    }
296}
297
298#[cfg(test)]
299mod test {
300    use indoc::indoc;
301
302    use crate::{state::Mode, test::VimTestContext};
303
304    #[gpui::test]
305    async fn test_word_motions(cx: &mut gpui::TestAppContext) {
306        let mut cx = VimTestContext::new(cx, true).await;
307        // «
308        // ˇ
309        // »
310        cx.set_state(
311            indoc! {"
312            Th«e quiˇ»ck brown
313            fox jumps over
314            the lazy dog."},
315            Mode::HelixNormal,
316        );
317
318        cx.simulate_keystrokes("w");
319
320        cx.assert_state(
321            indoc! {"
322            The qu«ick ˇ»brown
323            fox jumps over
324            the lazy dog."},
325            Mode::HelixNormal,
326        );
327
328        cx.simulate_keystrokes("w");
329
330        cx.assert_state(
331            indoc! {"
332            The quick «brownˇ»
333            fox jumps over
334            the lazy dog."},
335            Mode::HelixNormal,
336        );
337
338        cx.simulate_keystrokes("2 b");
339
340        cx.assert_state(
341            indoc! {"
342            The «ˇquick »brown
343            fox jumps over
344            the lazy dog."},
345            Mode::HelixNormal,
346        );
347
348        cx.simulate_keystrokes("down e up");
349
350        cx.assert_state(
351            indoc! {"
352            The quicˇk brown
353            fox jumps over
354            the lazy dog."},
355            Mode::HelixNormal,
356        );
357
358        cx.set_state("aa\n  «ˇbb»", Mode::HelixNormal);
359
360        cx.simulate_keystroke("b");
361
362        cx.assert_state("aa\n«ˇ  »bb", Mode::HelixNormal);
363    }
364
365    // #[gpui::test]
366    // async fn test_delete(cx: &mut gpui::TestAppContext) {
367    //     let mut cx = VimTestContext::new(cx, true).await;
368
369    //     // test delete a selection
370    //     cx.set_state(
371    //         indoc! {"
372    //         The qu«ick ˇ»brown
373    //         fox jumps over
374    //         the lazy dog."},
375    //         Mode::HelixNormal,
376    //     );
377
378    //     cx.simulate_keystrokes("d");
379
380    //     cx.assert_state(
381    //         indoc! {"
382    //         The quˇbrown
383    //         fox jumps over
384    //         the lazy dog."},
385    //         Mode::HelixNormal,
386    //     );
387
388    //     // test deleting a single character
389    //     cx.simulate_keystrokes("d");
390
391    //     cx.assert_state(
392    //         indoc! {"
393    //         The quˇrown
394    //         fox jumps over
395    //         the lazy dog."},
396    //         Mode::HelixNormal,
397    //     );
398    // }
399
400    // #[gpui::test]
401    // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
402    //     let mut cx = VimTestContext::new(cx, true).await;
403
404    //     cx.set_state(
405    //         indoc! {"
406    //         The quick brownˇ
407    //         fox jumps over
408    //         the lazy dog."},
409    //         Mode::HelixNormal,
410    //     );
411
412    //     cx.simulate_keystrokes("d");
413
414    //     cx.assert_state(
415    //         indoc! {"
416    //         The quick brownˇfox jumps over
417    //         the lazy dog."},
418    //         Mode::HelixNormal,
419    //     );
420    // }
421
422    // #[gpui::test]
423    // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
424    //     let mut cx = VimTestContext::new(cx, true).await;
425
426    //     cx.set_state(
427    //         indoc! {"
428    //         The quick brown
429    //         fox jumps over
430    //         the lazy dog.ˇ"},
431    //         Mode::HelixNormal,
432    //     );
433
434    //     cx.simulate_keystrokes("d");
435
436    //     cx.assert_state(
437    //         indoc! {"
438    //         The quick brown
439    //         fox jumps over
440    //         the lazy dog.ˇ"},
441    //         Mode::HelixNormal,
442    //     );
443    // }
444
445    #[gpui::test]
446    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
447        let mut cx = VimTestContext::new(cx, true).await;
448
449        cx.set_state(
450            indoc! {"
451            The quˇick brown
452            fox jumps over
453            the lazy dog."},
454            Mode::HelixNormal,
455        );
456
457        cx.simulate_keystrokes("f z");
458
459        cx.assert_state(
460            indoc! {"
461                The qu«ick brown
462                fox jumps over
463                the lazˇ»y dog."},
464            Mode::HelixNormal,
465        );
466
467        cx.simulate_keystrokes("2 T r");
468
469        cx.assert_state(
470            indoc! {"
471                The quick br«ˇown
472                fox jumps over
473                the laz»y dog."},
474            Mode::HelixNormal,
475        );
476    }
477
478    #[gpui::test]
479    async fn test_newline_char(cx: &mut gpui::TestAppContext) {
480        let mut cx = VimTestContext::new(cx, true).await;
481
482        cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal);
483
484        cx.simulate_keystroke("w");
485
486        cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal);
487
488        cx.set_state("aa«\nˇ»", Mode::HelixNormal);
489
490        cx.simulate_keystroke("b");
491
492        cx.assert_state("«ˇaa»\n", Mode::HelixNormal);
493    }
494}