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