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