helix.rs

  1use editor::{movement, scroll::Autoscroll, DisplayPoint, Editor};
  2use gpui::{actions, Action};
  3use language::{CharClassifier, CharKind};
  4use ui::ViewContext;
  5
  6use crate::{motion::Motion, state::Mode, Vim};
  7
  8actions!(vim, [HelixNormalAfter]);
  9
 10pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
 11    Vim::action(editor, cx, Vim::helix_normal_after);
 12}
 13
 14impl Vim {
 15    pub fn helix_normal_after(&mut self, action: &HelixNormalAfter, cx: &mut ViewContext<Self>) {
 16        if self.active_operator().is_some() {
 17            self.operator_stack.clear();
 18            self.sync_vim_settings(cx);
 19            return;
 20        }
 21        self.stop_recording_immediately(action.boxed_clone(), cx);
 22        self.switch_mode(Mode::HelixNormal, false, cx);
 23        return;
 24    }
 25
 26    pub fn helix_normal_motion(
 27        &mut self,
 28        motion: Motion,
 29        times: Option<usize>,
 30        cx: &mut ViewContext<Self>,
 31    ) {
 32        self.helix_move_cursor(motion, times, cx);
 33    }
 34
 35    fn helix_find_range_forward(
 36        &mut self,
 37        times: Option<usize>,
 38        cx: &mut ViewContext<Self>,
 39        mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
 40    ) {
 41        self.update_editor(cx, |_, editor, cx| {
 42            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 43                s.move_with(|map, selection| {
 44                    let times = times.unwrap_or(1);
 45
 46                    if selection.head() == map.max_point() {
 47                        return;
 48                    }
 49
 50                    // collapse to block cursor
 51                    if selection.tail() < selection.head() {
 52                        selection.set_tail(movement::left(map, selection.head()), selection.goal);
 53                    } else {
 54                        selection.set_tail(selection.head(), selection.goal);
 55                        selection.set_head(movement::right(map, selection.head()), selection.goal);
 56                    }
 57
 58                    // create a classifier
 59                    let classifier = map
 60                        .buffer_snapshot
 61                        .char_classifier_at(selection.head().to_point(map));
 62
 63                    let mut last_selection = selection.clone();
 64                    for _ in 0..times {
 65                        let (new_tail, new_head) =
 66                            movement::find_boundary_trail(map, selection.head(), |left, right| {
 67                                is_boundary(left, right, &classifier)
 68                            });
 69
 70                        selection.set_head(new_head, selection.goal);
 71                        if let Some(new_tail) = new_tail {
 72                            selection.set_tail(new_tail, selection.goal);
 73                        }
 74
 75                        if selection.head() == last_selection.head()
 76                            && selection.tail() == last_selection.tail()
 77                        {
 78                            break;
 79                        }
 80                        last_selection = selection.clone();
 81                    }
 82                });
 83            });
 84        });
 85    }
 86
 87    fn helix_find_range_backward(
 88        &mut self,
 89        times: Option<usize>,
 90        cx: &mut ViewContext<Self>,
 91        mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
 92    ) {
 93        self.update_editor(cx, |_, editor, cx| {
 94            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 95                s.move_with(|map, selection| {
 96                    let times = times.unwrap_or(1);
 97
 98                    if selection.head() == DisplayPoint::zero() {
 99                        return;
100                    }
101
102                    // collapse to block cursor
103                    if selection.tail() < selection.head() {
104                        selection.set_tail(movement::left(map, selection.head()), selection.goal);
105                    } else {
106                        selection.set_tail(selection.head(), selection.goal);
107                        selection.set_head(movement::right(map, selection.head()), selection.goal);
108                    }
109
110                    // flip the selection
111                    selection.swap_head_tail();
112
113                    // create a classifier
114                    let classifier = map
115                        .buffer_snapshot
116                        .char_classifier_at(selection.head().to_point(map));
117
118                    let mut last_selection = selection.clone();
119                    for _ in 0..times {
120                        let (new_tail, new_head) = movement::find_preceding_boundary_trail(
121                            map,
122                            selection.head(),
123                            |left, right| is_boundary(left, right, &classifier),
124                        );
125
126                        selection.set_head(new_head, selection.goal);
127                        if let Some(new_tail) = new_tail {
128                            selection.set_tail(new_tail, selection.goal);
129                        }
130
131                        if selection.head() == last_selection.head()
132                            && selection.tail() == last_selection.tail()
133                        {
134                            break;
135                        }
136                        last_selection = selection.clone();
137                    }
138                });
139            })
140        });
141    }
142
143    pub fn helix_move_and_collapse(
144        &mut self,
145        motion: Motion,
146        times: Option<usize>,
147        cx: &mut ViewContext<Self>,
148    ) {
149        self.update_editor(cx, |_, editor, cx| {
150            let text_layout_details = editor.text_layout_details(cx);
151            editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
152                s.move_with(|map, selection| {
153                    let goal = selection.goal;
154                    let cursor = if selection.is_empty() || selection.reversed {
155                        selection.head()
156                    } else {
157                        movement::left(map, selection.head())
158                    };
159
160                    let (point, goal) = motion
161                        .move_point(map, cursor, selection.goal, times, &text_layout_details)
162                        .unwrap_or((cursor, goal));
163
164                    selection.collapse_to(point, goal)
165                })
166            });
167        });
168    }
169
170    pub fn helix_move_cursor(
171        &mut self,
172        motion: Motion,
173        times: Option<usize>,
174        cx: &mut ViewContext<Self>,
175    ) {
176        match motion {
177            Motion::NextWordStart { ignore_punctuation } => {
178                self.helix_find_range_forward(times, cx, |left, right, classifier| {
179                    let left_kind = classifier.kind_with(left, ignore_punctuation);
180                    let right_kind = classifier.kind_with(right, ignore_punctuation);
181                    let at_newline = right == '\n';
182
183                    let found =
184                        left_kind != right_kind && right_kind != CharKind::Whitespace || at_newline;
185
186                    found
187                })
188            }
189            Motion::NextWordEnd { ignore_punctuation } => {
190                self.helix_find_range_forward(times, 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 = left_kind != right_kind
196                        && (left_kind != CharKind::Whitespace || at_newline);
197
198                    found
199                })
200            }
201            Motion::PreviousWordStart { ignore_punctuation } => {
202                self.helix_find_range_backward(times, 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::PreviousWordEnd { ignore_punctuation } => {
214                self.helix_find_range_backward(times, 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                        && right_kind != CharKind::Whitespace
221                        && !at_newline;
222
223                    found
224                })
225            }
226            _ => self.helix_move_and_collapse(motion, times, cx),
227        }
228    }
229}
230
231#[cfg(test)]
232mod test {
233    use indoc::indoc;
234
235    use crate::{state::Mode, test::VimTestContext};
236
237    #[gpui::test]
238    async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
239        let mut cx = VimTestContext::new(cx, true).await;
240        // «
241        // ˇ
242        // »
243        cx.set_state(
244            indoc! {"
245            The quˇick brown
246            fox jumps over
247            the lazy dog."},
248            Mode::HelixNormal,
249        );
250
251        cx.simulate_keystrokes("w");
252
253        cx.assert_state(
254            indoc! {"
255            The qu«ick ˇ»brown
256            fox jumps over
257            the lazy dog."},
258            Mode::HelixNormal,
259        );
260
261        cx.simulate_keystrokes("w");
262
263        cx.assert_state(
264            indoc! {"
265            The quick «brownˇ»
266            fox jumps over
267            the lazy dog."},
268            Mode::HelixNormal,
269        );
270    }
271}