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