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 = (left == '\n') ^ (right == '\n');
192
193                    let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
194                        || 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 = (left == '\n') ^ (right == '\n');
204
205                    let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)
206                        || 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 = (left == '\n') ^ (right == '\n');
216
217                    let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)
218                        || 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 = (left == '\n') ^ (right == '\n');
228
229                    let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
230                        || at_newline;
231
232                    found
233                })
234            }
235            Motion::FindForward { .. } => {
236                self.update_editor(window, cx, |_, editor, window, cx| {
237                    let text_layout_details = editor.text_layout_details(window);
238                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
239                        s.move_with(|map, selection| {
240                            let goal = selection.goal;
241                            let cursor = if selection.is_empty() || selection.reversed {
242                                selection.head()
243                            } else {
244                                movement::left(map, selection.head())
245                            };
246
247                            let (point, goal) = motion
248                                .move_point(
249                                    map,
250                                    cursor,
251                                    selection.goal,
252                                    times,
253                                    &text_layout_details,
254                                )
255                                .unwrap_or((cursor, goal));
256                            selection.set_tail(selection.head(), goal);
257                            selection.set_head(movement::right(map, point), goal);
258                        })
259                    });
260                });
261            }
262            Motion::FindBackward { .. } => {
263                self.update_editor(window, cx, |_, editor, window, cx| {
264                    let text_layout_details = editor.text_layout_details(window);
265                    editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
266                        s.move_with(|map, selection| {
267                            let goal = selection.goal;
268                            let cursor = if selection.is_empty() || selection.reversed {
269                                selection.head()
270                            } else {
271                                movement::left(map, selection.head())
272                            };
273
274                            let (point, goal) = motion
275                                .move_point(
276                                    map,
277                                    cursor,
278                                    selection.goal,
279                                    times,
280                                    &text_layout_details,
281                                )
282                                .unwrap_or((cursor, goal));
283                            selection.set_tail(selection.head(), goal);
284                            selection.set_head(point, goal);
285                        })
286                    });
287                });
288            }
289            _ => self.helix_move_and_collapse(motion, times, window, cx),
290        }
291    }
292}
293
294#[cfg(test)]
295mod test {
296    use indoc::indoc;
297
298    use crate::{state::Mode, test::VimTestContext};
299
300    #[gpui::test]
301    async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
302        let mut cx = VimTestContext::new(cx, true).await;
303        // «
304        // ˇ
305        // »
306        cx.set_state(
307            indoc! {"
308            The quˇick brown
309            fox jumps over
310            the lazy dog."},
311            Mode::HelixNormal,
312        );
313
314        cx.simulate_keystrokes("w");
315
316        cx.assert_state(
317            indoc! {"
318            The qu«ick ˇ»brown
319            fox jumps over
320            the lazy dog."},
321            Mode::HelixNormal,
322        );
323
324        cx.simulate_keystrokes("w");
325
326        cx.assert_state(
327            indoc! {"
328            The quick «brownˇ»
329            fox jumps over
330            the lazy dog."},
331            Mode::HelixNormal,
332        );
333    }
334
335    // #[gpui::test]
336    // async fn test_delete(cx: &mut gpui::TestAppContext) {
337    //     let mut cx = VimTestContext::new(cx, true).await;
338
339    //     // test delete a selection
340    //     cx.set_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("d");
349
350    //     cx.assert_state(
351    //         indoc! {"
352    //         The quˇbrown
353    //         fox jumps over
354    //         the lazy dog."},
355    //         Mode::HelixNormal,
356    //     );
357
358    //     // test deleting a single character
359    //     cx.simulate_keystrokes("d");
360
361    //     cx.assert_state(
362    //         indoc! {"
363    //         The quˇrown
364    //         fox jumps over
365    //         the lazy dog."},
366    //         Mode::HelixNormal,
367    //     );
368    // }
369
370    // #[gpui::test]
371    // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
372    //     let mut cx = VimTestContext::new(cx, true).await;
373
374    //     cx.set_state(
375    //         indoc! {"
376    //         The quick brownˇ
377    //         fox jumps over
378    //         the lazy dog."},
379    //         Mode::HelixNormal,
380    //     );
381
382    //     cx.simulate_keystrokes("d");
383
384    //     cx.assert_state(
385    //         indoc! {"
386    //         The quick brownˇfox jumps over
387    //         the lazy dog."},
388    //         Mode::HelixNormal,
389    //     );
390    // }
391
392    // #[gpui::test]
393    // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
394    //     let mut cx = VimTestContext::new(cx, true).await;
395
396    //     cx.set_state(
397    //         indoc! {"
398    //         The quick brown
399    //         fox jumps over
400    //         the lazy dog.ˇ"},
401    //         Mode::HelixNormal,
402    //     );
403
404    //     cx.simulate_keystrokes("d");
405
406    //     cx.assert_state(
407    //         indoc! {"
408    //         The quick brown
409    //         fox jumps over
410    //         the lazy dog.ˇ"},
411    //         Mode::HelixNormal,
412    //     );
413    // }
414
415    #[gpui::test]
416    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
417        let mut cx = VimTestContext::new(cx, true).await;
418
419        cx.set_state(
420            indoc! {"
421            The quˇick brown
422            fox jumps over
423            the lazy dog."},
424            Mode::HelixNormal,
425        );
426
427        cx.simulate_keystrokes("f z");
428
429        cx.assert_state(
430            indoc! {"
431                The qu«ick brown
432                fox jumps over
433                the lazˇ»y dog."},
434            Mode::HelixNormal,
435        );
436
437        cx.simulate_keystrokes("2 T r");
438
439        cx.assert_state(
440            indoc! {"
441                The quick br«ˇown
442                fox jumps over
443                the laz»y dog."},
444            Mode::HelixNormal,
445        );
446    }
447}