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 = 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}