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}