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::VisualLine => visual_motion(motion, cx),
116 Mode::Insert => {
117 // Shouldn't execute a motion in insert mode. Ignoring
118 }
119 }
120}
121
122// Motion handling is specified here:
123// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
124impl Motion {
125 pub fn linewise(self) -> bool {
126 use Motion::*;
127 match self {
128 Down | Up | StartOfDocument | EndOfDocument | CurrentLine => true,
129 _ => false,
130 }
131 }
132
133 pub fn inclusive(self) -> bool {
134 use Motion::*;
135 if self.linewise() {
136 return true;
137 }
138
139 match self {
140 EndOfLine | NextWordEnd { .. } => true,
141 Left | Right | StartOfLine | NextWordStart { .. } | PreviousWordStart { .. } => false,
142 _ => panic!("Exclusivity not defined for {self:?}"),
143 }
144 }
145
146 pub fn move_point(
147 self,
148 map: &DisplaySnapshot,
149 point: DisplayPoint,
150 goal: SelectionGoal,
151 ) -> (DisplayPoint, SelectionGoal) {
152 use Motion::*;
153 match self {
154 Left => (left(map, point), SelectionGoal::None),
155 Down => movement::down(map, point, goal, true),
156 Up => movement::up(map, point, goal, true),
157 Right => (right(map, point), SelectionGoal::None),
158 NextWordStart { ignore_punctuation } => (
159 next_word_start(map, point, ignore_punctuation),
160 SelectionGoal::None,
161 ),
162 NextWordEnd { ignore_punctuation } => (
163 next_word_end(map, point, ignore_punctuation),
164 SelectionGoal::None,
165 ),
166 PreviousWordStart { ignore_punctuation } => (
167 previous_word_start(map, point, ignore_punctuation),
168 SelectionGoal::None,
169 ),
170 FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
171 StartOfLine => (start_of_line(map, point), SelectionGoal::None),
172 EndOfLine => (end_of_line(map, point), SelectionGoal::None),
173 CurrentLine => (end_of_line(map, point), SelectionGoal::None),
174 StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
175 EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
176 }
177 }
178
179 // Expands a selection using self motion for an operator
180 pub fn expand_selection(
181 self,
182 map: &DisplaySnapshot,
183 selection: &mut Selection<DisplayPoint>,
184 expand_to_surrounding_newline: bool,
185 ) {
186 let (head, goal) = self.move_point(map, selection.head(), selection.goal);
187 selection.set_head(head, goal);
188
189 if self.linewise() {
190 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
191
192 if expand_to_surrounding_newline {
193 if selection.end.row() < map.max_point().row() {
194 *selection.end.row_mut() += 1;
195 *selection.end.column_mut() = 0;
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 }
202 }
203
204 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
205 } else {
206 // If the motion is exclusive and the end of the motion is in column 1, the
207 // end of the motion is moved to the end of the previous line and the motion
208 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
209 // but "d}" will not include that line.
210 let mut inclusive = self.inclusive();
211 if !inclusive
212 && selection.end.row() > selection.start.row()
213 && selection.end.column() == 0
214 && selection.end.row() > 0
215 {
216 inclusive = true;
217 *selection.end.row_mut() -= 1;
218 *selection.end.column_mut() = 0;
219 selection.end = map.clip_point(
220 map.next_line_boundary(selection.end.to_point(map)).1,
221 Bias::Left,
222 );
223 }
224
225 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
226 *selection.end.column_mut() += 1;
227 }
228 }
229 }
230}
231
232fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
233 *point.column_mut() = point.column().saturating_sub(1);
234 map.clip_point(point, Bias::Left)
235}
236
237fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
238 *point.column_mut() += 1;
239 map.clip_point(point, Bias::Right)
240}
241
242fn next_word_start(
243 map: &DisplaySnapshot,
244 point: DisplayPoint,
245 ignore_punctuation: bool,
246) -> DisplayPoint {
247 let mut crossed_newline = false;
248 movement::find_boundary(map, point, |left, right| {
249 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
250 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
251 let at_newline = right == '\n';
252
253 let found = (left_kind != right_kind && !right.is_whitespace())
254 || at_newline && crossed_newline
255 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
256
257 if at_newline {
258 crossed_newline = true;
259 }
260 found
261 })
262}
263
264fn next_word_end(
265 map: &DisplaySnapshot,
266 mut point: DisplayPoint,
267 ignore_punctuation: bool,
268) -> DisplayPoint {
269 *point.column_mut() += 1;
270 point = movement::find_boundary(map, point, |left, right| {
271 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
272 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
273
274 left_kind != right_kind && !left.is_whitespace()
275 });
276 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
277 // we have backtraced already
278 if !map
279 .chars_at(point)
280 .skip(1)
281 .next()
282 .map(|c| c == '\n')
283 .unwrap_or(true)
284 {
285 *point.column_mut() = point.column().saturating_sub(1);
286 }
287 map.clip_point(point, Bias::Left)
288}
289
290fn previous_word_start(
291 map: &DisplaySnapshot,
292 mut point: DisplayPoint,
293 ignore_punctuation: bool,
294) -> DisplayPoint {
295 // This works even though find_preceding_boundary is called for every character in the line containing
296 // cursor because the newline is checked only once.
297 point = movement::find_preceding_boundary(map, point, |left, right| {
298 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
299 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
300
301 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
302 });
303 point
304}
305
306fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
307 let mut column = 0;
308 for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
309 if ch == '\n' {
310 return point;
311 }
312
313 if char_kind(ch) != CharKind::Whitespace {
314 break;
315 }
316
317 column += ch.len_utf8() as u32;
318 }
319
320 *point.column_mut() = column;
321 map.clip_point(point, Bias::Left)
322}
323
324fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
325 map.prev_line_boundary(point.to_point(map)).1
326}
327
328fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
329 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
330}
331
332fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
333 let mut new_point = 0usize.to_display_point(map);
334 *new_point.column_mut() = point.column();
335 map.clip_point(new_point, Bias::Left)
336}
337
338fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
339 let mut new_point = map.max_point();
340 *new_point.column_mut() = point.column();
341 map.clip_point(new_point, Bias::Left)
342}