helix.rs

  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}