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