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