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