1use collections::HashMap;
2use editor::{display_map::ToDisplayPoint, scroll::Autoscroll};
3use gpui::{Context, Window};
4use language::{Bias, Point, SelectionGoal};
5use multi_buffer::MultiBufferRow;
6
7use crate::{
8 Vim,
9 motion::Motion,
10 normal::{ChangeCase, ConvertToLowerCase, ConvertToRot13, ConvertToRot47, ConvertToUpperCase},
11 object::Object,
12 state::Mode,
13};
14
15pub enum ConvertTarget {
16 LowerCase,
17 UpperCase,
18 OppositeCase,
19 Rot13,
20 Rot47,
21}
22
23impl Vim {
24 pub fn convert_motion(
25 &mut self,
26 motion: Motion,
27 times: Option<usize>,
28 forced_motion: bool,
29 mode: ConvertTarget,
30 window: &mut Window,
31 cx: &mut Context<Self>,
32 ) {
33 self.stop_recording(cx);
34 self.update_editor(window, cx, |_, editor, window, cx| {
35 editor.set_clip_at_line_ends(false, cx);
36 let text_layout_details = editor.text_layout_details(window);
37 editor.transact(window, cx, |editor, window, cx| {
38 let mut selection_starts: HashMap<_, _> = Default::default();
39 editor.change_selections(None, window, cx, |s| {
40 s.move_with(|map, selection| {
41 let anchor = map.display_point_to_anchor(selection.head(), Bias::Left);
42 selection_starts.insert(selection.id, anchor);
43 motion.expand_selection(
44 map,
45 selection,
46 times,
47 &text_layout_details,
48 forced_motion,
49 );
50 });
51 });
52 match mode {
53 ConvertTarget::LowerCase => {
54 editor.convert_to_lower_case(&Default::default(), window, cx)
55 }
56 ConvertTarget::UpperCase => {
57 editor.convert_to_upper_case(&Default::default(), window, cx)
58 }
59 ConvertTarget::OppositeCase => {
60 editor.convert_to_opposite_case(&Default::default(), window, cx)
61 }
62 ConvertTarget::Rot13 => {
63 editor.convert_to_rot13(&Default::default(), window, cx)
64 }
65 ConvertTarget::Rot47 => {
66 editor.convert_to_rot47(&Default::default(), window, cx)
67 }
68 }
69 editor.change_selections(None, window, cx, |s| {
70 s.move_with(|map, selection| {
71 let anchor = selection_starts.remove(&selection.id).unwrap();
72 selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
73 });
74 });
75 });
76 editor.set_clip_at_line_ends(true, cx);
77 });
78 }
79
80 pub fn convert_object(
81 &mut self,
82 object: Object,
83 around: bool,
84 mode: ConvertTarget,
85 window: &mut Window,
86 cx: &mut Context<Self>,
87 ) {
88 self.stop_recording(cx);
89 self.update_editor(window, cx, |_, editor, window, cx| {
90 editor.transact(window, cx, |editor, window, cx| {
91 editor.set_clip_at_line_ends(false, cx);
92 let mut original_positions: HashMap<_, _> = Default::default();
93 editor.change_selections(None, window, cx, |s| {
94 s.move_with(|map, selection| {
95 object.expand_selection(map, selection, around);
96 original_positions.insert(
97 selection.id,
98 map.display_point_to_anchor(selection.start, Bias::Left),
99 );
100 });
101 });
102 match mode {
103 ConvertTarget::LowerCase => {
104 editor.convert_to_lower_case(&Default::default(), window, cx)
105 }
106 ConvertTarget::UpperCase => {
107 editor.convert_to_upper_case(&Default::default(), window, cx)
108 }
109 ConvertTarget::OppositeCase => {
110 editor.convert_to_opposite_case(&Default::default(), window, cx)
111 }
112 ConvertTarget::Rot13 => {
113 editor.convert_to_rot13(&Default::default(), window, cx)
114 }
115 ConvertTarget::Rot47 => {
116 editor.convert_to_rot47(&Default::default(), window, cx)
117 }
118 }
119 editor.change_selections(None, window, cx, |s| {
120 s.move_with(|map, selection| {
121 let anchor = original_positions.remove(&selection.id).unwrap();
122 selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
123 });
124 });
125 editor.set_clip_at_line_ends(true, cx);
126 });
127 });
128 }
129
130 pub fn change_case(&mut self, _: &ChangeCase, window: &mut Window, cx: &mut Context<Self>) {
131 self.manipulate_text(window, cx, |c| {
132 if c.is_lowercase() {
133 c.to_uppercase().collect::<Vec<char>>()
134 } else {
135 c.to_lowercase().collect::<Vec<char>>()
136 }
137 })
138 }
139
140 pub fn convert_to_upper_case(
141 &mut self,
142 _: &ConvertToUpperCase,
143 window: &mut Window,
144 cx: &mut Context<Self>,
145 ) {
146 self.manipulate_text(window, cx, |c| c.to_uppercase().collect::<Vec<char>>())
147 }
148
149 pub fn convert_to_lower_case(
150 &mut self,
151 _: &ConvertToLowerCase,
152 window: &mut Window,
153 cx: &mut Context<Self>,
154 ) {
155 self.manipulate_text(window, cx, |c| c.to_lowercase().collect::<Vec<char>>())
156 }
157
158 pub fn convert_to_rot13(
159 &mut self,
160 _: &ConvertToRot13,
161 window: &mut Window,
162 cx: &mut Context<Self>,
163 ) {
164 self.manipulate_text(window, cx, |c| {
165 vec![match c {
166 'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char,
167 'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char,
168 _ => c,
169 }]
170 })
171 }
172
173 pub fn convert_to_rot47(
174 &mut self,
175 _: &ConvertToRot47,
176 window: &mut Window,
177 cx: &mut Context<Self>,
178 ) {
179 self.manipulate_text(window, cx, |c| {
180 let code_point = c as u32;
181 if code_point >= 33 && code_point <= 126 {
182 return vec![char::from_u32(33 + ((code_point + 14) % 94)).unwrap()];
183 }
184 vec![c]
185 })
186 }
187
188 fn manipulate_text<F>(&mut self, window: &mut Window, cx: &mut Context<Self>, transform: F)
189 where
190 F: Fn(char) -> Vec<char> + Copy,
191 {
192 self.record_current_action(cx);
193 self.store_visual_marks(window, cx);
194 let count = Vim::take_count(cx).unwrap_or(1) as u32;
195 Vim::take_forced_motion(cx);
196
197 self.update_editor(window, cx, |vim, editor, window, cx| {
198 let mut ranges = Vec::new();
199 let mut cursor_positions = Vec::new();
200 let snapshot = editor.buffer().read(cx).snapshot(cx);
201 for selection in editor.selections.all_adjusted(cx) {
202 match vim.mode {
203 Mode::Visual | Mode::VisualLine => {
204 ranges.push(selection.start..selection.end);
205 cursor_positions.push(selection.start..selection.start);
206 }
207 Mode::VisualBlock => {
208 ranges.push(selection.start..selection.end);
209 if cursor_positions.is_empty() {
210 cursor_positions.push(selection.start..selection.start);
211 }
212 }
213
214 Mode::HelixNormal => {}
215 Mode::Insert | Mode::Normal | Mode::Replace => {
216 let start = selection.start;
217 let mut end = start;
218 for _ in 0..count {
219 end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
220 }
221 ranges.push(start..end);
222
223 if end.column == snapshot.line_len(MultiBufferRow(end.row))
224 && end.column > 0
225 {
226 end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
227 }
228 cursor_positions.push(end..end)
229 }
230 }
231 }
232 editor.transact(window, cx, |editor, window, cx| {
233 for range in ranges.into_iter().rev() {
234 let snapshot = editor.buffer().read(cx).snapshot(cx);
235 let text = snapshot
236 .text_for_range(range.start..range.end)
237 .flat_map(|s| s.chars())
238 .flat_map(transform)
239 .collect::<String>();
240 editor.edit([(range, text)], cx)
241 }
242 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
243 s.select_ranges(cursor_positions)
244 })
245 });
246 });
247 self.switch_mode(Mode::Normal, true, window, cx)
248 }
249}
250
251#[cfg(test)]
252mod test {
253 use crate::{state::Mode, test::NeovimBackedTestContext};
254
255 #[gpui::test]
256 async fn test_change_case(cx: &mut gpui::TestAppContext) {
257 let mut cx = NeovimBackedTestContext::new(cx).await;
258 cx.set_shared_state("ˇabC\n").await;
259 cx.simulate_shared_keystrokes("~").await;
260 cx.shared_state().await.assert_eq("AˇbC\n");
261 cx.simulate_shared_keystrokes("2 ~").await;
262 cx.shared_state().await.assert_eq("ABˇc\n");
263
264 // works in visual mode
265 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
266 cx.simulate_shared_keystrokes("~").await;
267 cx.shared_state().await.assert_eq("a😀CˇDé1*F\n");
268
269 // works with multibyte characters
270 cx.simulate_shared_keystrokes("~").await;
271 cx.set_shared_state("aˇC😀é1*F\n").await;
272 cx.simulate_shared_keystrokes("4 ~").await;
273 cx.shared_state().await.assert_eq("ac😀É1ˇ*F\n");
274
275 // works with line selections
276 cx.set_shared_state("abˇC\n").await;
277 cx.simulate_shared_keystrokes("shift-v ~").await;
278 cx.shared_state().await.assert_eq("ˇABc\n");
279
280 // works in visual block mode
281 cx.set_shared_state("ˇaa\nbb\ncc").await;
282 cx.simulate_shared_keystrokes("ctrl-v j ~").await;
283 cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
284
285 // works with multiple cursors (zed only)
286 cx.set_state("aˇßcdˇe\n", Mode::Normal);
287 cx.simulate_keystrokes("~");
288 cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
289 }
290
291 #[gpui::test]
292 async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) {
293 let mut cx = NeovimBackedTestContext::new(cx).await;
294 // works in visual mode
295 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
296 cx.simulate_shared_keystrokes("shift-u").await;
297 cx.shared_state().await.assert_eq("a😀CˇDÉ1*F\n");
298
299 // works with line selections
300 cx.set_shared_state("abˇC\n").await;
301 cx.simulate_shared_keystrokes("shift-v shift-u").await;
302 cx.shared_state().await.assert_eq("ˇABC\n");
303
304 // works in visual block mode
305 cx.set_shared_state("ˇaa\nbb\ncc").await;
306 cx.simulate_shared_keystrokes("ctrl-v j shift-u").await;
307 cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
308 }
309
310 #[gpui::test]
311 async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) {
312 let mut cx = NeovimBackedTestContext::new(cx).await;
313 // works in visual mode
314 cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await;
315 cx.simulate_shared_keystrokes("u").await;
316 cx.shared_state().await.assert_eq("A😀cˇdé1*f\n");
317
318 // works with line selections
319 cx.set_shared_state("ABˇc\n").await;
320 cx.simulate_shared_keystrokes("shift-v u").await;
321 cx.shared_state().await.assert_eq("ˇabc\n");
322
323 // works in visual block mode
324 cx.set_shared_state("ˇAa\nBb\nCc").await;
325 cx.simulate_shared_keystrokes("ctrl-v j u").await;
326 cx.shared_state().await.assert_eq("ˇaa\nbb\nCc");
327 }
328
329 #[gpui::test]
330 async fn test_change_case_motion(cx: &mut gpui::TestAppContext) {
331 let mut cx = NeovimBackedTestContext::new(cx).await;
332
333 cx.set_shared_state("ˇabc def").await;
334 cx.simulate_shared_keystrokes("g shift-u w").await;
335 cx.shared_state().await.assert_eq("ˇABC def");
336
337 cx.simulate_shared_keystrokes("g u w").await;
338 cx.shared_state().await.assert_eq("ˇabc def");
339
340 cx.simulate_shared_keystrokes("g ~ w").await;
341 cx.shared_state().await.assert_eq("ˇABC def");
342
343 cx.simulate_shared_keystrokes(".").await;
344 cx.shared_state().await.assert_eq("ˇabc def");
345
346 cx.set_shared_state("abˇc def").await;
347 cx.simulate_shared_keystrokes("g ~ i w").await;
348 cx.shared_state().await.assert_eq("ˇABC def");
349
350 cx.simulate_shared_keystrokes(".").await;
351 cx.shared_state().await.assert_eq("ˇabc def");
352
353 cx.simulate_shared_keystrokes("g shift-u $").await;
354 cx.shared_state().await.assert_eq("ˇABC DEF");
355 }
356
357 #[gpui::test]
358 async fn test_change_case_motion_object(cx: &mut gpui::TestAppContext) {
359 let mut cx = NeovimBackedTestContext::new(cx).await;
360
361 cx.set_shared_state("abc dˇef\n").await;
362 cx.simulate_shared_keystrokes("g shift-u i w").await;
363 cx.shared_state().await.assert_eq("abc ˇDEF\n");
364 }
365
366 #[gpui::test]
367 async fn test_convert_to_rot13(cx: &mut gpui::TestAppContext) {
368 let mut cx = NeovimBackedTestContext::new(cx).await;
369 // works in visual mode
370 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
371 cx.simulate_shared_keystrokes("g ?").await;
372 cx.shared_state().await.assert_eq("a😀CˇqÉ1*s\n");
373
374 // works with line selections
375 cx.set_shared_state("abˇC\n").await;
376 cx.simulate_shared_keystrokes("shift-v g ?").await;
377 cx.shared_state().await.assert_eq("ˇnoP\n");
378
379 // works in visual block mode
380 cx.set_shared_state("ˇaa\nbb\ncc").await;
381 cx.simulate_shared_keystrokes("ctrl-v j g ?").await;
382 cx.shared_state().await.assert_eq("ˇna\nob\ncc");
383 }
384
385 #[gpui::test]
386 async fn test_change_rot13_motion(cx: &mut gpui::TestAppContext) {
387 let mut cx = NeovimBackedTestContext::new(cx).await;
388
389 cx.set_shared_state("ˇabc def").await;
390 cx.simulate_shared_keystrokes("g ? w").await;
391 cx.shared_state().await.assert_eq("ˇnop def");
392
393 cx.simulate_shared_keystrokes("g ? w").await;
394 cx.shared_state().await.assert_eq("ˇabc def");
395
396 cx.simulate_shared_keystrokes(".").await;
397 cx.shared_state().await.assert_eq("ˇnop def");
398
399 cx.set_shared_state("abˇc def").await;
400 cx.simulate_shared_keystrokes("g ? i w").await;
401 cx.shared_state().await.assert_eq("ˇnop def");
402
403 cx.simulate_shared_keystrokes(".").await;
404 cx.shared_state().await.assert_eq("ˇabc def");
405
406 cx.simulate_shared_keystrokes("g ? $").await;
407 cx.shared_state().await.assert_eq("ˇnop qrs");
408 }
409
410 #[gpui::test]
411 async fn test_change_rot13_object(cx: &mut gpui::TestAppContext) {
412 let mut cx = NeovimBackedTestContext::new(cx).await;
413
414 cx.set_shared_state("ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
415 .await;
416 cx.simulate_shared_keystrokes("g ? i w").await;
417 cx.shared_state()
418 .await
419 .assert_eq("ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM");
420 }
421}