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