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 selection.end = map.clip_point(selection.end, Bias::Right);
196 // Don't reset the end here
197 return;
198 } else if selection.start.row() > 0 {
199 *selection.start.row_mut() -= 1;
200 *selection.start.column_mut() = map.line_len(selection.start.row());
201 selection.start = map.clip_point(selection.start, Bias::Left);
202 }
203 }
204
205 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
206 } else {
207 // If the motion is exclusive and the end of the motion is in column 1, the
208 // end of the motion is moved to the end of the previous line and the motion
209 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
210 // but "d}" will not include that line.
211 let mut inclusive = self.inclusive();
212 if !inclusive
213 && selection.end.row() > selection.start.row()
214 && selection.end.column() == 0
215 && selection.end.row() > 0
216 {
217 inclusive = true;
218 *selection.end.row_mut() -= 1;
219 *selection.end.column_mut() = 0;
220 selection.end = map.clip_point(
221 map.next_line_boundary(selection.end.to_point(map)).1,
222 Bias::Left,
223 );
224 }
225
226 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
227 *selection.end.column_mut() += 1;
228 }
229 }
230 }
231}
232
233fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
234 *point.column_mut() = point.column().saturating_sub(1);
235 map.clip_point(point, Bias::Left)
236}
237
238fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
239 *point.column_mut() += 1;
240 map.clip_point(point, Bias::Right)
241}
242
243fn next_word_start(
244 map: &DisplaySnapshot,
245 point: DisplayPoint,
246 ignore_punctuation: bool,
247) -> DisplayPoint {
248 let mut crossed_newline = false;
249 movement::find_boundary(map, point, |left, right| {
250 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
251 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
252 let at_newline = right == '\n';
253
254 let found = (left_kind != right_kind && !right.is_whitespace())
255 || at_newline && crossed_newline
256 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
257
258 if at_newline {
259 crossed_newline = true;
260 }
261 found
262 })
263}
264
265fn next_word_end(
266 map: &DisplaySnapshot,
267 mut point: DisplayPoint,
268 ignore_punctuation: bool,
269) -> DisplayPoint {
270 *point.column_mut() += 1;
271 point = movement::find_boundary(map, point, |left, right| {
272 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
273 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
274
275 left_kind != right_kind && !left.is_whitespace()
276 });
277 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
278 // we have backtraced already
279 if !map
280 .chars_at(point)
281 .skip(1)
282 .next()
283 .map(|c| c == '\n')
284 .unwrap_or(true)
285 {
286 *point.column_mut() = point.column().saturating_sub(1);
287 }
288 map.clip_point(point, Bias::Left)
289}
290
291fn previous_word_start(
292 map: &DisplaySnapshot,
293 mut point: DisplayPoint,
294 ignore_punctuation: bool,
295) -> DisplayPoint {
296 // This works even though find_preceding_boundary is called for every character in the line containing
297 // cursor because the newline is checked only once.
298 point = movement::find_preceding_boundary(map, point, |left, right| {
299 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
300 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
301
302 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
303 });
304 point
305}
306
307fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
308 let mut column = 0;
309 for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
310 if ch == '\n' {
311 return point;
312 }
313
314 if char_kind(ch) != CharKind::Whitespace {
315 break;
316 }
317
318 column += ch.len_utf8() as u32;
319 }
320
321 *point.column_mut() = column;
322 map.clip_point(point, Bias::Left)
323}
324
325fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
326 map.prev_line_boundary(point.to_point(map)).1
327}
328
329fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
330 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
331}
332
333fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
334 let mut new_point = 0usize.to_display_point(map);
335 *new_point.column_mut() = point.column();
336 map.clip_point(new_point, Bias::Left)
337}
338
339fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
340 let mut new_point = map.max_point();
341 *new_point.column_mut() = point.column();
342 map.clip_point(new_point, Bias::Left)
343}