1use collections::HashMap;
2use editor::{Autoscroll, Bias};
3use gpui::{actions, MutableAppContext, ViewContext};
4use workspace::Workspace;
5
6use crate::{motion::Motion, state::Mode, Vim};
7
8actions!(
9 vim,
10 [
11 VisualDelete,
12 VisualChange,
13 VisualLineDelete,
14 VisualLineChange
15 ]
16);
17
18pub fn init(cx: &mut MutableAppContext) {
19 cx.add_action(change);
20 cx.add_action(change_line);
21 cx.add_action(delete);
22 cx.add_action(delete_line);
23}
24
25pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
26 Vim::update(cx, |vim, cx| {
27 vim.update_active_editor(cx, |editor, cx| {
28 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
29 s.move_with(|map, selection| {
30 let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
31 let new_head = map.clip_at_line_end(new_head);
32 let was_reversed = selection.reversed;
33 selection.set_head(new_head, goal);
34
35 if was_reversed && !selection.reversed {
36 // Head was at the start of the selection, and now is at the end. We need to move the start
37 // back by one if possible in order to compensate for this change.
38 *selection.start.column_mut() = selection.start.column().saturating_sub(1);
39 selection.start = map.clip_point(selection.start, Bias::Left);
40 } else if !was_reversed && selection.reversed {
41 // Head was at the end of the selection, and now is at the start. We need to move the end
42 // forward by one if possible in order to compensate for this change.
43 *selection.end.column_mut() = selection.end.column() + 1;
44 selection.end = map.clip_point(selection.end, Bias::Left);
45 }
46 });
47 });
48 });
49 });
50}
51
52pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspace>) {
53 Vim::update(cx, |vim, cx| {
54 vim.update_active_editor(cx, |editor, cx| {
55 editor.set_clip_at_line_ends(false, cx);
56 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
57 s.move_with(|map, selection| {
58 if !selection.reversed {
59 // Head was at the end of the selection, and now is at the start. We need to move the end
60 // forward by one if possible in order to compensate for this change.
61 *selection.end.column_mut() = selection.end.column() + 1;
62 selection.end = map.clip_point(selection.end, Bias::Left);
63 }
64 });
65 });
66 editor.insert("", cx);
67 });
68 vim.switch_mode(Mode::Insert, cx);
69 });
70}
71
72pub fn change_line(_: &mut Workspace, _: &VisualLineChange, cx: &mut ViewContext<Workspace>) {
73 Vim::update(cx, |vim, cx| {
74 vim.update_active_editor(cx, |editor, cx| {
75 editor.set_clip_at_line_ends(false, cx);
76 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
77 s.move_with(|map, selection| {
78 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
79 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
80 });
81 });
82 editor.insert("", cx);
83 });
84 vim.switch_mode(Mode::Insert, cx);
85 });
86}
87
88pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
89 Vim::update(cx, |vim, cx| {
90 vim.switch_mode(Mode::Normal, cx);
91 vim.update_active_editor(cx, |editor, cx| {
92 editor.set_clip_at_line_ends(false, cx);
93 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
94 s.move_with(|map, selection| {
95 if !selection.reversed {
96 // Head was at the end of the selection, and now is at the start. We need to move the end
97 // forward by one if possible in order to compensate for this change.
98 *selection.end.column_mut() = selection.end.column() + 1;
99 selection.end = map.clip_point(selection.end, Bias::Left);
100 }
101 });
102 });
103 editor.insert("", cx);
104
105 // Fixup cursor position after the deletion
106 editor.set_clip_at_line_ends(true, cx);
107 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
108 s.move_with(|map, selection| {
109 let mut cursor = selection.head();
110 cursor = map.clip_point(cursor, Bias::Left);
111 selection.collapse_to(cursor, selection.goal)
112 });
113 });
114 });
115 });
116}
117
118pub fn delete_line(_: &mut Workspace, _: &VisualLineDelete, cx: &mut ViewContext<Workspace>) {
119 Vim::update(cx, |vim, cx| {
120 vim.update_active_editor(cx, |editor, cx| {
121 editor.set_clip_at_line_ends(false, cx);
122 let mut original_columns: HashMap<_, _> = Default::default();
123 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
124 s.move_with(|map, selection| {
125 original_columns.insert(selection.id, selection.head().column());
126 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
127
128 if selection.end.row() < map.max_point().row() {
129 *selection.end.row_mut() += 1;
130 *selection.end.column_mut() = 0;
131 // Don't reset the end here
132 return;
133 } else if selection.start.row() > 0 {
134 *selection.start.row_mut() -= 1;
135 *selection.start.column_mut() = map.line_len(selection.start.row());
136 }
137
138 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
139 });
140 });
141 editor.insert("", cx);
142
143 // Fixup cursor position after the deletion
144 editor.set_clip_at_line_ends(true, cx);
145 editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
146 s.move_with(|map, selection| {
147 let mut cursor = selection.head();
148 if let Some(column) = original_columns.get(&selection.id) {
149 *cursor.column_mut() = *column
150 }
151 cursor = map.clip_point(cursor, Bias::Left);
152 selection.collapse_to(cursor, selection.goal)
153 });
154 });
155 });
156 vim.switch_mode(Mode::Normal, cx);
157 });
158}
159
160#[cfg(test)]
161mod test {
162 use indoc::indoc;
163
164 use crate::{state::Mode, vim_test_context::VimTestContext};
165
166 #[gpui::test]
167 async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) {
168 let cx = VimTestContext::new(cx, true).await;
169 let mut cx = cx.binding(["v", "w", "j"]).mode_after(Mode::Visual);
170 cx.assert(
171 indoc! {"
172 The |quick brown
173 fox jumps over
174 the lazy dog"},
175 indoc! {"
176 The [quick brown
177 fox jumps }over
178 the lazy dog"},
179 );
180 cx.assert(
181 indoc! {"
182 The quick brown
183 fox jumps over
184 the |lazy dog"},
185 indoc! {"
186 The quick brown
187 fox jumps over
188 the [lazy }dog"},
189 );
190 cx.assert(
191 indoc! {"
192 The quick brown
193 fox jumps |over
194 the lazy dog"},
195 indoc! {"
196 The quick brown
197 fox jumps [over
198 }the lazy dog"},
199 );
200 let mut cx = cx.binding(["v", "b", "k"]).mode_after(Mode::Visual);
201 cx.assert(
202 indoc! {"
203 The |quick brown
204 fox jumps over
205 the lazy dog"},
206 indoc! {"
207 {The q]uick brown
208 fox jumps over
209 the lazy dog"},
210 );
211 cx.assert(
212 indoc! {"
213 The quick brown
214 fox jumps over
215 the |lazy dog"},
216 indoc! {"
217 The quick brown
218 {fox jumps over
219 the l]azy dog"},
220 );
221 cx.assert(
222 indoc! {"
223 The quick brown
224 fox jumps |over
225 the lazy dog"},
226 indoc! {"
227 The {quick brown
228 fox jumps o]ver
229 the lazy dog"},
230 );
231 }
232
233 #[gpui::test]
234 async fn test_visual_delete(cx: &mut gpui::TestAppContext) {
235 let cx = VimTestContext::new(cx, true).await;
236 let mut cx = cx.binding(["v", "w", "x"]);
237 cx.assert("The quick |brown", "The quick| ");
238 let mut cx = cx.binding(["v", "w", "j", "x"]);
239 cx.assert(
240 indoc! {"
241 The |quick brown
242 fox jumps over
243 the lazy dog"},
244 indoc! {"
245 The |ver
246 the lazy dog"},
247 );
248 cx.assert(
249 indoc! {"
250 The quick brown
251 fox jumps over
252 the |lazy dog"},
253 indoc! {"
254 The quick brown
255 fox jumps over
256 the |og"},
257 );
258 cx.assert(
259 indoc! {"
260 The quick brown
261 fox jumps |over
262 the lazy dog"},
263 indoc! {"
264 The quick brown
265 fox jumps |he lazy dog"},
266 );
267 let mut cx = cx.binding(["v", "b", "k", "x"]);
268 cx.assert(
269 indoc! {"
270 The |quick brown
271 fox jumps over
272 the lazy dog"},
273 indoc! {"
274 |uick brown
275 fox jumps over
276 the lazy dog"},
277 );
278 cx.assert(
279 indoc! {"
280 The quick brown
281 fox jumps over
282 the |lazy dog"},
283 indoc! {"
284 The quick brown
285 |azy dog"},
286 );
287 cx.assert(
288 indoc! {"
289 The quick brown
290 fox jumps |over
291 the lazy dog"},
292 indoc! {"
293 The |ver
294 the lazy dog"},
295 );
296 }
297
298 #[gpui::test]
299 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
300 let cx = VimTestContext::new(cx, true).await;
301 let mut cx = cx.binding(["v", "w", "c"]).mode_after(Mode::Insert);
302 cx.assert("The quick |brown", "The quick |");
303 let mut cx = cx.binding(["v", "w", "j", "c"]).mode_after(Mode::Insert);
304 cx.assert(
305 indoc! {"
306 The |quick brown
307 fox jumps over
308 the lazy dog"},
309 indoc! {"
310 The |ver
311 the lazy dog"},
312 );
313 cx.assert(
314 indoc! {"
315 The quick brown
316 fox jumps over
317 the |lazy dog"},
318 indoc! {"
319 The quick brown
320 fox jumps over
321 the |og"},
322 );
323 cx.assert(
324 indoc! {"
325 The quick brown
326 fox jumps |over
327 the lazy dog"},
328 indoc! {"
329 The quick brown
330 fox jumps |he lazy dog"},
331 );
332 let mut cx = cx.binding(["v", "b", "k", "c"]).mode_after(Mode::Insert);
333 cx.assert(
334 indoc! {"
335 The |quick brown
336 fox jumps over
337 the lazy dog"},
338 indoc! {"
339 |uick brown
340 fox jumps over
341 the lazy dog"},
342 );
343 cx.assert(
344 indoc! {"
345 The quick brown
346 fox jumps over
347 the |lazy dog"},
348 indoc! {"
349 The quick brown
350 |azy dog"},
351 );
352 cx.assert(
353 indoc! {"
354 The quick brown
355 fox jumps |over
356 the lazy dog"},
357 indoc! {"
358 The |ver
359 the lazy dog"},
360 );
361 }
362}