helix.rs

  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}