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_next_word_start(cx: &mut gpui::TestAppContext) {
306        let mut cx = VimTestContext::new(cx, true).await;
307        // «
308        // ˇ
309        // »
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("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
339    // #[gpui::test]
340    // async fn test_delete(cx: &mut gpui::TestAppContext) {
341    //     let mut cx = VimTestContext::new(cx, true).await;
342
343    //     // test delete a selection
344    //     cx.set_state(
345    //         indoc! {"
346    //         The qu«ick ˇ»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 quˇbrown
357    //         fox jumps over
358    //         the lazy dog."},
359    //         Mode::HelixNormal,
360    //     );
361
362    //     // test deleting a single character
363    //     cx.simulate_keystrokes("d");
364
365    //     cx.assert_state(
366    //         indoc! {"
367    //         The quˇrown
368    //         fox jumps over
369    //         the lazy dog."},
370    //         Mode::HelixNormal,
371    //     );
372    // }
373
374    // #[gpui::test]
375    // async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
376    //     let mut cx = VimTestContext::new(cx, true).await;
377
378    //     cx.set_state(
379    //         indoc! {"
380    //         The quick brownˇ
381    //         fox jumps over
382    //         the lazy dog."},
383    //         Mode::HelixNormal,
384    //     );
385
386    //     cx.simulate_keystrokes("d");
387
388    //     cx.assert_state(
389    //         indoc! {"
390    //         The quick brownˇfox jumps over
391    //         the lazy dog."},
392    //         Mode::HelixNormal,
393    //     );
394    // }
395
396    // #[gpui::test]
397    // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) {
398    //     let mut cx = VimTestContext::new(cx, true).await;
399
400    //     cx.set_state(
401    //         indoc! {"
402    //         The quick brown
403    //         fox jumps over
404    //         the lazy dog.ˇ"},
405    //         Mode::HelixNormal,
406    //     );
407
408    //     cx.simulate_keystrokes("d");
409
410    //     cx.assert_state(
411    //         indoc! {"
412    //         The quick brown
413    //         fox jumps over
414    //         the lazy dog.ˇ"},
415    //         Mode::HelixNormal,
416    //     );
417    // }
418
419    #[gpui::test]
420    async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
421        let mut cx = VimTestContext::new(cx, true).await;
422
423        cx.set_state(
424            indoc! {"
425            The quˇick brown
426            fox jumps over
427            the lazy dog."},
428            Mode::HelixNormal,
429        );
430
431        cx.simulate_keystrokes("f z");
432
433        cx.assert_state(
434            indoc! {"
435                The qu«ick brown
436                fox jumps over
437                the lazˇ»y dog."},
438            Mode::HelixNormal,
439        );
440
441        cx.simulate_keystrokes("2 T r");
442
443        cx.assert_state(
444            indoc! {"
445                The quick br«ˇown
446                fox jumps over
447                the laz»y dog."},
448            Mode::HelixNormal,
449        );
450    }
451}