1use editor::{
2 char_kind,
3 display_map::{DisplaySnapshot, ToDisplayPoint},
4 movement, Bias, CharKind, DisplayPoint,
5};
6use gpui::{actions, impl_actions, MutableAppContext};
7use language::{Selection, SelectionGoal};
8use serde::Deserialize;
9use workspace::Workspace;
10
11use crate::{
12 normal::normal_motion,
13 state::{Mode, Operator},
14 visual::visual_motion,
15 Vim,
16};
17
18#[derive(Copy, Clone, Debug)]
19pub enum Motion {
20 Left,
21 Down,
22 Up,
23 Right,
24 NextWordStart { ignore_punctuation: bool },
25 NextWordEnd { ignore_punctuation: bool },
26 PreviousWordStart { ignore_punctuation: bool },
27 FirstNonWhitespace,
28 CurrentLine,
29 StartOfLine,
30 EndOfLine,
31 StartOfDocument,
32 EndOfDocument,
33 Matching,
34}
35
36#[derive(Clone, Deserialize, PartialEq)]
37#[serde(rename_all = "camelCase")]
38struct NextWordStart {
39 #[serde(default)]
40 ignore_punctuation: bool,
41}
42
43#[derive(Clone, Deserialize, PartialEq)]
44#[serde(rename_all = "camelCase")]
45struct NextWordEnd {
46 #[serde(default)]
47 ignore_punctuation: bool,
48}
49
50#[derive(Clone, Deserialize, PartialEq)]
51#[serde(rename_all = "camelCase")]
52struct PreviousWordStart {
53 #[serde(default)]
54 ignore_punctuation: bool,
55}
56
57actions!(
58 vim,
59 [
60 Left,
61 Down,
62 Up,
63 Right,
64 FirstNonWhitespace,
65 StartOfLine,
66 EndOfLine,
67 CurrentLine,
68 StartOfDocument,
69 EndOfDocument,
70 Matching,
71 ]
72);
73impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
74
75pub fn init(cx: &mut MutableAppContext) {
76 cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
77 cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
78 cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
79 cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
80 cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
81 motion(Motion::FirstNonWhitespace, cx)
82 });
83 cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
84 cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
85 cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
86 cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
87 motion(Motion::StartOfDocument, cx)
88 });
89 cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
90 cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
91
92 cx.add_action(
93 |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
94 motion(Motion::NextWordStart { ignore_punctuation }, cx)
95 },
96 );
97 cx.add_action(
98 |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
99 motion(Motion::NextWordEnd { ignore_punctuation }, cx)
100 },
101 );
102 cx.add_action(
103 |_: &mut Workspace,
104 &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
105 cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
106 );
107}
108
109fn motion(motion: Motion, cx: &mut MutableAppContext) {
110 Vim::update(cx, |vim, cx| {
111 if let Some(Operator::Namespace(_)) = vim.active_operator() {
112 vim.pop_operator(cx);
113 }
114 });
115 match Vim::read(cx).state.mode {
116 Mode::Normal => normal_motion(motion, cx),
117 Mode::Visual { .. } => visual_motion(motion, cx),
118 Mode::Insert => {
119 // Shouldn't execute a motion in insert mode. Ignoring
120 }
121 }
122}
123
124// Motion handling is specified here:
125// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
126impl Motion {
127 pub fn linewise(self) -> bool {
128 use Motion::*;
129 matches!(
130 self,
131 Down | Up | StartOfDocument | EndOfDocument | CurrentLine
132 )
133 }
134
135 pub fn inclusive(self) -> bool {
136 use Motion::*;
137 if self.linewise() {
138 return true;
139 }
140
141 match self {
142 EndOfLine | NextWordEnd { .. } | Matching => true,
143 Left | Right | StartOfLine | NextWordStart { .. } | PreviousWordStart { .. } => false,
144 _ => panic!("Exclusivity not defined for {self:?}"),
145 }
146 }
147
148 pub fn move_point(
149 self,
150 map: &DisplaySnapshot,
151 point: DisplayPoint,
152 goal: SelectionGoal,
153 ) -> (DisplayPoint, SelectionGoal) {
154 use Motion::*;
155 match self {
156 Left => (left(map, point), SelectionGoal::None),
157 Down => movement::down(map, point, goal, true),
158 Up => movement::up(map, point, goal, true),
159 Right => (right(map, point), SelectionGoal::None),
160 NextWordStart { ignore_punctuation } => (
161 next_word_start(map, point, ignore_punctuation),
162 SelectionGoal::None,
163 ),
164 NextWordEnd { ignore_punctuation } => (
165 next_word_end(map, point, ignore_punctuation),
166 SelectionGoal::None,
167 ),
168 PreviousWordStart { ignore_punctuation } => (
169 previous_word_start(map, point, ignore_punctuation),
170 SelectionGoal::None,
171 ),
172 FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
173 StartOfLine => (start_of_line(map, point), SelectionGoal::None),
174 EndOfLine => (end_of_line(map, point), SelectionGoal::None),
175 CurrentLine => (end_of_line(map, point), SelectionGoal::None),
176 StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
177 EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
178 Matching => (matching(map, point), SelectionGoal::None),
179 }
180 }
181
182 // Expands a selection using self motion for an operator
183 pub fn expand_selection(
184 self,
185 map: &DisplaySnapshot,
186 selection: &mut Selection<DisplayPoint>,
187 expand_to_surrounding_newline: bool,
188 ) {
189 let (head, goal) = self.move_point(map, selection.head(), selection.goal);
190 selection.set_head(head, goal);
191
192 if self.linewise() {
193 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
194
195 if expand_to_surrounding_newline {
196 if selection.end.row() < map.max_point().row() {
197 *selection.end.row_mut() += 1;
198 *selection.end.column_mut() = 0;
199 selection.end = map.clip_point(selection.end, Bias::Right);
200 // Don't reset the end here
201 return;
202 } else if selection.start.row() > 0 {
203 *selection.start.row_mut() -= 1;
204 *selection.start.column_mut() = map.line_len(selection.start.row());
205 selection.start = map.clip_point(selection.start, Bias::Left);
206 }
207 }
208
209 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
210 } else {
211 // If the motion is exclusive and the end of the motion is in column 1, the
212 // end of the motion is moved to the end of the previous line and the motion
213 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
214 // but "d}" will not include that line.
215 let mut inclusive = self.inclusive();
216 if !inclusive
217 && selection.end.row() > selection.start.row()
218 && selection.end.column() == 0
219 && selection.end.row() > 0
220 {
221 inclusive = true;
222 *selection.end.row_mut() -= 1;
223 *selection.end.column_mut() = 0;
224 selection.end = map.clip_point(
225 map.next_line_boundary(selection.end.to_point(map)).1,
226 Bias::Left,
227 );
228 }
229
230 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
231 *selection.end.column_mut() += 1;
232 }
233 }
234 }
235}
236
237fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
238 *point.column_mut() = point.column().saturating_sub(1);
239 map.clip_point(point, Bias::Left)
240}
241
242fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
243 *point.column_mut() += 1;
244 map.clip_point(point, Bias::Right)
245}
246
247fn next_word_start(
248 map: &DisplaySnapshot,
249 point: DisplayPoint,
250 ignore_punctuation: bool,
251) -> DisplayPoint {
252 let mut crossed_newline = false;
253 movement::find_boundary(map, point, |left, right| {
254 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
255 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
256 let at_newline = right == '\n';
257
258 let found = (left_kind != right_kind && !right.is_whitespace())
259 || at_newline && crossed_newline
260 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
261
262 if at_newline {
263 crossed_newline = true;
264 }
265 found
266 })
267}
268
269fn next_word_end(
270 map: &DisplaySnapshot,
271 mut point: DisplayPoint,
272 ignore_punctuation: bool,
273) -> DisplayPoint {
274 *point.column_mut() += 1;
275 point = movement::find_boundary(map, point, |left, right| {
276 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
277 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
278
279 left_kind != right_kind && !left.is_whitespace()
280 });
281 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
282 // we have backtraced already
283 if !map
284 .chars_at(point)
285 .nth(1)
286 .map(|c| c == '\n')
287 .unwrap_or(true)
288 {
289 *point.column_mut() = point.column().saturating_sub(1);
290 }
291 map.clip_point(point, Bias::Left)
292}
293
294fn previous_word_start(
295 map: &DisplaySnapshot,
296 mut point: DisplayPoint,
297 ignore_punctuation: bool,
298) -> DisplayPoint {
299 // This works even though find_preceding_boundary is called for every character in the line containing
300 // cursor because the newline is checked only once.
301 point = movement::find_preceding_boundary(map, point, |left, right| {
302 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
303 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
304
305 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
306 });
307 point
308}
309
310fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
311 let mut column = 0;
312 for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
313 if ch == '\n' {
314 return point;
315 }
316
317 if char_kind(ch) != CharKind::Whitespace {
318 break;
319 }
320
321 column += ch.len_utf8() as u32;
322 }
323
324 *point.column_mut() = column;
325 map.clip_point(point, Bias::Left)
326}
327
328fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
329 map.prev_line_boundary(point.to_point(map)).1
330}
331
332fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
333 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
334}
335
336fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
337 let mut new_point = 0usize.to_display_point(map);
338 *new_point.column_mut() = point.column();
339 map.clip_point(new_point, Bias::Left)
340}
341
342fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
343 let mut new_point = map.max_point();
344 *new_point.column_mut() = point.column();
345 map.clip_point(new_point, Bias::Left)
346}
347
348fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
349 let offset = point.to_offset(map, Bias::Left);
350 if let Some((open_range, close_range)) =
351 map.buffer_snapshot.enclosing_bracket_ranges(offset..offset)
352 {
353 if open_range.contains(&offset) {
354 close_range.start.to_display_point(map)
355 } else {
356 open_range.start.to_display_point(map)
357 }
358 } else {
359 point
360 }
361}