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 = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
225 }
226 cursor_positions.push(end..end)
227 }
228 }
229 }
230 editor.transact(window, cx, |editor, window, cx| {
231 for range in ranges.into_iter().rev() {
232 let snapshot = editor.buffer().read(cx).snapshot(cx);
233 let text = snapshot
234 .text_for_range(range.start..range.end)
235 .flat_map(|s| s.chars())
236 .flat_map(transform)
237 .collect::<String>();
238 editor.edit([(range, text)], cx)
239 }
240 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
241 s.select_ranges(cursor_positions)
242 })
243 });
244 });
245 self.switch_mode(Mode::Normal, true, window, cx)
246 }
247}
248
249#[cfg(test)]
250mod test {
251 use crate::{state::Mode, test::NeovimBackedTestContext};
252
253 #[gpui::test]
254 async fn test_change_case(cx: &mut gpui::TestAppContext) {
255 let mut cx = NeovimBackedTestContext::new(cx).await;
256 cx.set_shared_state("ˇabC\n").await;
257 cx.simulate_shared_keystrokes("~").await;
258 cx.shared_state().await.assert_eq("AˇbC\n");
259 cx.simulate_shared_keystrokes("2 ~").await;
260 cx.shared_state().await.assert_eq("ABˇc\n");
261
262 // works in visual mode
263 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
264 cx.simulate_shared_keystrokes("~").await;
265 cx.shared_state().await.assert_eq("a😀CˇDé1*F\n");
266
267 // works with multibyte characters
268 cx.simulate_shared_keystrokes("~").await;
269 cx.set_shared_state("aˇC😀é1*F\n").await;
270 cx.simulate_shared_keystrokes("4 ~").await;
271 cx.shared_state().await.assert_eq("ac😀É1ˇ*F\n");
272
273 // works with line selections
274 cx.set_shared_state("abˇC\n").await;
275 cx.simulate_shared_keystrokes("shift-v ~").await;
276 cx.shared_state().await.assert_eq("ˇABc\n");
277
278 // works in visual block mode
279 cx.set_shared_state("ˇaa\nbb\ncc").await;
280 cx.simulate_shared_keystrokes("ctrl-v j ~").await;
281 cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
282
283 // works with multiple cursors (zed only)
284 cx.set_state("aˇßcdˇe\n", Mode::Normal);
285 cx.simulate_keystrokes("~");
286 cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
287 }
288
289 #[gpui::test]
290 async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) {
291 let mut cx = NeovimBackedTestContext::new(cx).await;
292 // works in visual mode
293 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
294 cx.simulate_shared_keystrokes("shift-u").await;
295 cx.shared_state().await.assert_eq("a😀CˇDÉ1*F\n");
296
297 // works with line selections
298 cx.set_shared_state("abˇC\n").await;
299 cx.simulate_shared_keystrokes("shift-v shift-u").await;
300 cx.shared_state().await.assert_eq("ˇABC\n");
301
302 // works in visual block mode
303 cx.set_shared_state("ˇaa\nbb\ncc").await;
304 cx.simulate_shared_keystrokes("ctrl-v j shift-u").await;
305 cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
306 }
307
308 #[gpui::test]
309 async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) {
310 let mut cx = NeovimBackedTestContext::new(cx).await;
311 // works in visual mode
312 cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await;
313 cx.simulate_shared_keystrokes("u").await;
314 cx.shared_state().await.assert_eq("A😀cˇdé1*f\n");
315
316 // works with line selections
317 cx.set_shared_state("ABˇc\n").await;
318 cx.simulate_shared_keystrokes("shift-v u").await;
319 cx.shared_state().await.assert_eq("ˇabc\n");
320
321 // works in visual block mode
322 cx.set_shared_state("ˇAa\nBb\nCc").await;
323 cx.simulate_shared_keystrokes("ctrl-v j u").await;
324 cx.shared_state().await.assert_eq("ˇaa\nbb\nCc");
325 }
326
327 #[gpui::test]
328 async fn test_change_case_motion(cx: &mut gpui::TestAppContext) {
329 let mut cx = NeovimBackedTestContext::new(cx).await;
330
331 cx.set_shared_state("ˇabc def").await;
332 cx.simulate_shared_keystrokes("g shift-u w").await;
333 cx.shared_state().await.assert_eq("ˇABC def");
334
335 cx.simulate_shared_keystrokes("g u w").await;
336 cx.shared_state().await.assert_eq("ˇabc def");
337
338 cx.simulate_shared_keystrokes("g ~ w").await;
339 cx.shared_state().await.assert_eq("ˇABC def");
340
341 cx.simulate_shared_keystrokes(".").await;
342 cx.shared_state().await.assert_eq("ˇabc def");
343
344 cx.set_shared_state("abˇc def").await;
345 cx.simulate_shared_keystrokes("g ~ i w").await;
346 cx.shared_state().await.assert_eq("ˇABC def");
347
348 cx.simulate_shared_keystrokes(".").await;
349 cx.shared_state().await.assert_eq("ˇabc def");
350
351 cx.simulate_shared_keystrokes("g shift-u $").await;
352 cx.shared_state().await.assert_eq("ˇABC DEF");
353 }
354
355 #[gpui::test]
356 async fn test_change_case_motion_object(cx: &mut gpui::TestAppContext) {
357 let mut cx = NeovimBackedTestContext::new(cx).await;
358
359 cx.set_shared_state("abc dˇef\n").await;
360 cx.simulate_shared_keystrokes("g shift-u i w").await;
361 cx.shared_state().await.assert_eq("abc ˇDEF\n");
362 }
363
364 #[gpui::test]
365 async fn test_convert_to_rot13(cx: &mut gpui::TestAppContext) {
366 let mut cx = NeovimBackedTestContext::new(cx).await;
367 // works in visual mode
368 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
369 cx.simulate_shared_keystrokes("g ?").await;
370 cx.shared_state().await.assert_eq("a😀CˇqÉ1*s\n");
371
372 // works with line selections
373 cx.set_shared_state("abˇC\n").await;
374 cx.simulate_shared_keystrokes("shift-v g ?").await;
375 cx.shared_state().await.assert_eq("ˇnoP\n");
376
377 // works in visual block mode
378 cx.set_shared_state("ˇaa\nbb\ncc").await;
379 cx.simulate_shared_keystrokes("ctrl-v j g ?").await;
380 cx.shared_state().await.assert_eq("ˇna\nob\ncc");
381 }
382
383 #[gpui::test]
384 async fn test_change_rot13_motion(cx: &mut gpui::TestAppContext) {
385 let mut cx = NeovimBackedTestContext::new(cx).await;
386
387 cx.set_shared_state("ˇabc def").await;
388 cx.simulate_shared_keystrokes("g ? w").await;
389 cx.shared_state().await.assert_eq("ˇnop def");
390
391 cx.simulate_shared_keystrokes("g ? w").await;
392 cx.shared_state().await.assert_eq("ˇabc def");
393
394 cx.simulate_shared_keystrokes(".").await;
395 cx.shared_state().await.assert_eq("ˇnop def");
396
397 cx.set_shared_state("abˇc def").await;
398 cx.simulate_shared_keystrokes("g ? i w").await;
399 cx.shared_state().await.assert_eq("ˇnop def");
400
401 cx.simulate_shared_keystrokes(".").await;
402 cx.shared_state().await.assert_eq("ˇabc def");
403
404 cx.simulate_shared_keystrokes("g ? $").await;
405 cx.shared_state().await.assert_eq("ˇnop qrs");
406 }
407
408 #[gpui::test]
409 async fn test_change_rot13_object(cx: &mut gpui::TestAppContext) {
410 let mut cx = NeovimBackedTestContext::new(cx).await;
411
412 cx.set_shared_state("ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
413 .await;
414 cx.simulate_shared_keystrokes("g ? i w").await;
415 cx.shared_state()
416 .await
417 .assert_eq("ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM");
418 }
419}