case.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, ConvertToUpperCase},
 11    object::Object,
 12    state::Mode,
 13};
 14
 15pub enum CaseTarget {
 16    Lowercase,
 17    Uppercase,
 18    OppositeCase,
 19}
 20
 21impl Vim {
 22    pub fn change_case_motion(
 23        &mut self,
 24        motion: Motion,
 25        times: Option<usize>,
 26        mode: CaseTarget,
 27        window: &mut Window,
 28        cx: &mut Context<Self>,
 29    ) {
 30        self.stop_recording(cx);
 31        self.update_editor(window, cx, |_, editor, window, cx| {
 32            editor.set_clip_at_line_ends(false, cx);
 33            let text_layout_details = editor.text_layout_details(window);
 34            editor.transact(window, cx, |editor, window, cx| {
 35                let mut selection_starts: HashMap<_, _> = Default::default();
 36                editor.change_selections(None, window, cx, |s| {
 37                    s.move_with(|map, selection| {
 38                        let anchor = map.display_point_to_anchor(selection.head(), Bias::Left);
 39                        selection_starts.insert(selection.id, anchor);
 40                        motion.expand_selection(map, selection, times, &text_layout_details);
 41                    });
 42                });
 43                match mode {
 44                    CaseTarget::Lowercase => {
 45                        editor.convert_to_lower_case(&Default::default(), window, cx)
 46                    }
 47                    CaseTarget::Uppercase => {
 48                        editor.convert_to_upper_case(&Default::default(), window, cx)
 49                    }
 50                    CaseTarget::OppositeCase => {
 51                        editor.convert_to_opposite_case(&Default::default(), window, cx)
 52                    }
 53                }
 54                editor.change_selections(None, window, cx, |s| {
 55                    s.move_with(|map, selection| {
 56                        let anchor = selection_starts.remove(&selection.id).unwrap();
 57                        selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
 58                    });
 59                });
 60            });
 61            editor.set_clip_at_line_ends(true, cx);
 62        });
 63    }
 64
 65    pub fn change_case_object(
 66        &mut self,
 67        object: Object,
 68        around: bool,
 69        mode: CaseTarget,
 70        window: &mut Window,
 71        cx: &mut Context<Self>,
 72    ) {
 73        self.stop_recording(cx);
 74        self.update_editor(window, cx, |_, editor, window, cx| {
 75            editor.transact(window, cx, |editor, window, cx| {
 76                editor.set_clip_at_line_ends(false, cx);
 77                let mut original_positions: HashMap<_, _> = Default::default();
 78                editor.change_selections(None, window, cx, |s| {
 79                    s.move_with(|map, selection| {
 80                        object.expand_selection(map, selection, around);
 81                        original_positions.insert(
 82                            selection.id,
 83                            map.display_point_to_anchor(selection.start, Bias::Left),
 84                        );
 85                    });
 86                });
 87                match mode {
 88                    CaseTarget::Lowercase => {
 89                        editor.convert_to_lower_case(&Default::default(), window, cx)
 90                    }
 91                    CaseTarget::Uppercase => {
 92                        editor.convert_to_upper_case(&Default::default(), window, cx)
 93                    }
 94                    CaseTarget::OppositeCase => {
 95                        editor.convert_to_opposite_case(&Default::default(), window, cx)
 96                    }
 97                }
 98                editor.change_selections(None, window, cx, |s| {
 99                    s.move_with(|map, selection| {
100                        let anchor = original_positions.remove(&selection.id).unwrap();
101                        selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
102                    });
103                });
104                editor.set_clip_at_line_ends(true, cx);
105            });
106        });
107    }
108
109    pub fn change_case(&mut self, _: &ChangeCase, window: &mut Window, cx: &mut Context<Self>) {
110        self.manipulate_text(window, cx, |c| {
111            if c.is_lowercase() {
112                c.to_uppercase().collect::<Vec<char>>()
113            } else {
114                c.to_lowercase().collect::<Vec<char>>()
115            }
116        })
117    }
118
119    pub fn convert_to_upper_case(
120        &mut self,
121        _: &ConvertToUpperCase,
122        window: &mut Window,
123        cx: &mut Context<Self>,
124    ) {
125        self.manipulate_text(window, cx, |c| c.to_uppercase().collect::<Vec<char>>())
126    }
127
128    pub fn convert_to_lower_case(
129        &mut self,
130        _: &ConvertToLowerCase,
131        window: &mut Window,
132        cx: &mut Context<Self>,
133    ) {
134        self.manipulate_text(window, cx, |c| c.to_lowercase().collect::<Vec<char>>())
135    }
136
137    fn manipulate_text<F>(&mut self, window: &mut Window, cx: &mut Context<Self>, transform: F)
138    where
139        F: Fn(char) -> Vec<char> + Copy,
140    {
141        self.record_current_action(cx);
142        self.store_visual_marks(window, cx);
143        let count = Vim::take_count(cx).unwrap_or(1) as u32;
144
145        self.update_editor(window, cx, |vim, editor, window, cx| {
146            let mut ranges = Vec::new();
147            let mut cursor_positions = Vec::new();
148            let snapshot = editor.buffer().read(cx).snapshot(cx);
149            for selection in editor.selections.all_adjusted(cx) {
150                match vim.mode {
151                    Mode::Visual | Mode::VisualLine => {
152                        ranges.push(selection.start..selection.end);
153                        cursor_positions.push(selection.start..selection.start);
154                    }
155                    Mode::VisualBlock => {
156                        ranges.push(selection.start..selection.end);
157                        if cursor_positions.is_empty() {
158                            cursor_positions.push(selection.start..selection.start);
159                        }
160                    }
161
162                    Mode::HelixNormal => {}
163                    Mode::Insert | Mode::Normal | Mode::Replace => {
164                        let start = selection.start;
165                        let mut end = start;
166                        for _ in 0..count {
167                            end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
168                        }
169                        ranges.push(start..end);
170
171                        if end.column == snapshot.line_len(MultiBufferRow(end.row)) {
172                            end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
173                        }
174                        cursor_positions.push(end..end)
175                    }
176                }
177            }
178            editor.transact(window, cx, |editor, window, cx| {
179                for range in ranges.into_iter().rev() {
180                    let snapshot = editor.buffer().read(cx).snapshot(cx);
181                    let text = snapshot
182                        .text_for_range(range.start..range.end)
183                        .flat_map(|s| s.chars())
184                        .flat_map(transform)
185                        .collect::<String>();
186                    editor.edit([(range, text)], cx)
187                }
188                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
189                    s.select_ranges(cursor_positions)
190                })
191            });
192        });
193        self.switch_mode(Mode::Normal, true, window, cx)
194    }
195}
196
197#[cfg(test)]
198mod test {
199    use crate::{state::Mode, test::NeovimBackedTestContext};
200
201    #[gpui::test]
202    async fn test_change_case(cx: &mut gpui::TestAppContext) {
203        let mut cx = NeovimBackedTestContext::new(cx).await;
204        cx.set_shared_state("ˇabC\n").await;
205        cx.simulate_shared_keystrokes("~").await;
206        cx.shared_state().await.assert_eq("AˇbC\n");
207        cx.simulate_shared_keystrokes("2 ~").await;
208        cx.shared_state().await.assert_eq("ABˇc\n");
209
210        // works in visual mode
211        cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
212        cx.simulate_shared_keystrokes("~").await;
213        cx.shared_state().await.assert_eq("a😀CˇDé1*F\n");
214
215        // works with multibyte characters
216        cx.simulate_shared_keystrokes("~").await;
217        cx.set_shared_state("aˇC😀é1*F\n").await;
218        cx.simulate_shared_keystrokes("4 ~").await;
219        cx.shared_state().await.assert_eq("ac😀É1ˇ*F\n");
220
221        // works with line selections
222        cx.set_shared_state("abˇC\n").await;
223        cx.simulate_shared_keystrokes("shift-v ~").await;
224        cx.shared_state().await.assert_eq("ˇABc\n");
225
226        // works in visual block mode
227        cx.set_shared_state("ˇaa\nbb\ncc").await;
228        cx.simulate_shared_keystrokes("ctrl-v j ~").await;
229        cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
230
231        // works with multiple cursors (zed only)
232        cx.set_state("aˇßcdˇe\n", Mode::Normal);
233        cx.simulate_keystrokes("~");
234        cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
235    }
236
237    #[gpui::test]
238    async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) {
239        let mut cx = NeovimBackedTestContext::new(cx).await;
240        // works in visual mode
241        cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
242        cx.simulate_shared_keystrokes("shift-u").await;
243        cx.shared_state().await.assert_eq("a😀CˇDÉ1*F\n");
244
245        // works with line selections
246        cx.set_shared_state("abˇC\n").await;
247        cx.simulate_shared_keystrokes("shift-v shift-u").await;
248        cx.shared_state().await.assert_eq("ˇABC\n");
249
250        // works in visual block mode
251        cx.set_shared_state("ˇaa\nbb\ncc").await;
252        cx.simulate_shared_keystrokes("ctrl-v j shift-u").await;
253        cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
254    }
255
256    #[gpui::test]
257    async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) {
258        let mut cx = NeovimBackedTestContext::new(cx).await;
259        // works in visual mode
260        cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await;
261        cx.simulate_shared_keystrokes("u").await;
262        cx.shared_state().await.assert_eq("A😀cˇdé1*f\n");
263
264        // works with line selections
265        cx.set_shared_state("ABˇc\n").await;
266        cx.simulate_shared_keystrokes("shift-v u").await;
267        cx.shared_state().await.assert_eq("ˇabc\n");
268
269        // works in visual block mode
270        cx.set_shared_state("ˇAa\nBb\nCc").await;
271        cx.simulate_shared_keystrokes("ctrl-v j u").await;
272        cx.shared_state().await.assert_eq("ˇaa\nbb\nCc");
273    }
274
275    #[gpui::test]
276    async fn test_change_case_motion(cx: &mut gpui::TestAppContext) {
277        let mut cx = NeovimBackedTestContext::new(cx).await;
278
279        cx.set_shared_state("ˇabc def").await;
280        cx.simulate_shared_keystrokes("g shift-u w").await;
281        cx.shared_state().await.assert_eq("ˇABC def");
282
283        cx.simulate_shared_keystrokes("g u w").await;
284        cx.shared_state().await.assert_eq("ˇabc def");
285
286        cx.simulate_shared_keystrokes("g ~ w").await;
287        cx.shared_state().await.assert_eq("ˇABC def");
288
289        cx.simulate_shared_keystrokes(".").await;
290        cx.shared_state().await.assert_eq("ˇabc def");
291
292        cx.set_shared_state("abˇc def").await;
293        cx.simulate_shared_keystrokes("g ~ i w").await;
294        cx.shared_state().await.assert_eq("ˇABC def");
295
296        cx.simulate_shared_keystrokes(".").await;
297        cx.shared_state().await.assert_eq("ˇabc def");
298
299        cx.simulate_shared_keystrokes("g shift-u $").await;
300        cx.shared_state().await.assert_eq("ˇABC DEF");
301    }
302
303    #[gpui::test]
304    async fn test_change_case_motion_object(cx: &mut gpui::TestAppContext) {
305        let mut cx = NeovimBackedTestContext::new(cx).await;
306
307        cx.set_shared_state("abc dˇef\n").await;
308        cx.simulate_shared_keystrokes("g shift-u i w").await;
309        cx.shared_state().await.assert_eq("abc ˇDEF\n");
310    }
311}