convert.rs

  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}