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