case.rs

  1use editor::scroll::Autoscroll;
  2use gpui::ViewContext;
  3use language::{Bias, Point};
  4use workspace::Workspace;
  5
  6use crate::{
  7    normal::ChangeCase, normal::ConvertToLowerCase, normal::ConvertToUpperCase, state::Mode, Vim,
  8};
  9
 10pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
 11    manipulate_text(cx, |c| {
 12        if c.is_lowercase() {
 13            c.to_uppercase().collect::<Vec<char>>()
 14        } else {
 15            c.to_lowercase().collect::<Vec<char>>()
 16        }
 17    })
 18}
 19
 20pub fn convert_to_upper_case(
 21    _: &mut Workspace,
 22    _: &ConvertToUpperCase,
 23    cx: &mut ViewContext<Workspace>,
 24) {
 25    manipulate_text(cx, |c| c.to_uppercase().collect::<Vec<char>>())
 26}
 27
 28pub fn convert_to_lower_case(
 29    _: &mut Workspace,
 30    _: &ConvertToLowerCase,
 31    cx: &mut ViewContext<Workspace>,
 32) {
 33    manipulate_text(cx, |c| c.to_lowercase().collect::<Vec<char>>())
 34}
 35
 36fn manipulate_text<F>(cx: &mut ViewContext<Workspace>, transform: F)
 37where
 38    F: Fn(char) -> Vec<char> + Copy,
 39{
 40    Vim::update(cx, |vim, cx| {
 41        vim.record_current_action(cx);
 42        let count = vim.take_count(cx).unwrap_or(1) as u32;
 43        vim.update_active_editor(cx, |vim, editor, cx| {
 44            let mut ranges = Vec::new();
 45            let mut cursor_positions = Vec::new();
 46            let snapshot = editor.buffer().read(cx).snapshot(cx);
 47            for selection in editor.selections.all::<Point>(cx) {
 48                match vim.state().mode {
 49                    Mode::VisualLine => {
 50                        let start = Point::new(selection.start.row, 0);
 51                        let end =
 52                            Point::new(selection.end.row, snapshot.line_len(selection.end.row));
 53                        ranges.push(start..end);
 54                        cursor_positions.push(start..start);
 55                    }
 56                    Mode::Visual => {
 57                        ranges.push(selection.start..selection.end);
 58                        cursor_positions.push(selection.start..selection.start);
 59                    }
 60                    Mode::VisualBlock => {
 61                        ranges.push(selection.start..selection.end);
 62                        if cursor_positions.len() == 0 {
 63                            cursor_positions.push(selection.start..selection.start);
 64                        }
 65                    }
 66                    Mode::Insert | Mode::Normal | Mode::Replace => {
 67                        let start = selection.start;
 68                        let mut end = start;
 69                        for _ in 0..count {
 70                            end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
 71                        }
 72                        ranges.push(start..end);
 73
 74                        if end.column == snapshot.line_len(end.row) {
 75                            end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
 76                        }
 77                        cursor_positions.push(end..end)
 78                    }
 79                }
 80            }
 81            editor.transact(cx, |editor, cx| {
 82                for range in ranges.into_iter().rev() {
 83                    let snapshot = editor.buffer().read(cx).snapshot(cx);
 84                    editor.buffer().update(cx, |buffer, cx| {
 85                        let text = snapshot
 86                            .text_for_range(range.start..range.end)
 87                            .flat_map(|s| s.chars())
 88                            .flat_map(|c| transform(c))
 89                            .collect::<String>();
 90
 91                        buffer.edit([(range, text)], None, cx)
 92                    })
 93                }
 94                editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 95                    s.select_ranges(cursor_positions)
 96                })
 97            });
 98        });
 99        vim.switch_mode(Mode::Normal, true, cx)
100    })
101}
102
103#[cfg(test)]
104mod test {
105    use crate::{state::Mode, test::NeovimBackedTestContext};
106
107    #[gpui::test]
108    async fn test_change_case(cx: &mut gpui::TestAppContext) {
109        let mut cx = NeovimBackedTestContext::new(cx).await;
110        cx.set_shared_state("ˇabC\n").await;
111        cx.simulate_shared_keystrokes(["~"]).await;
112        cx.assert_shared_state("AˇbC\n").await;
113        cx.simulate_shared_keystrokes(["2", "~"]).await;
114        cx.assert_shared_state("ABˇc\n").await;
115
116        // works in visual mode
117        cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
118        cx.simulate_shared_keystrokes(["~"]).await;
119        cx.assert_shared_state("a😀CˇDé1*F\n").await;
120
121        // works with multibyte characters
122        cx.simulate_shared_keystrokes(["~"]).await;
123        cx.set_shared_state("aˇC😀é1*F\n").await;
124        cx.simulate_shared_keystrokes(["4", "~"]).await;
125        cx.assert_shared_state("ac😀É1ˇ*F\n").await;
126
127        // works with line selections
128        cx.set_shared_state("abˇC\n").await;
129        cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
130        cx.assert_shared_state("ˇABc\n").await;
131
132        // works in visual block mode
133        cx.set_shared_state("ˇaa\nbb\ncc").await;
134        cx.simulate_shared_keystrokes(["ctrl-v", "j", "~"]).await;
135        cx.assert_shared_state("ˇAa\nBb\ncc").await;
136
137        // works with multiple cursors (zed only)
138        cx.set_state("aˇßcdˇe\n", Mode::Normal);
139        cx.simulate_keystroke("~");
140        cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
141    }
142
143    #[gpui::test]
144    async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) {
145        let mut cx = NeovimBackedTestContext::new(cx).await;
146        // works in visual mode
147        cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
148        cx.simulate_shared_keystrokes(["U"]).await;
149        cx.assert_shared_state("a😀CˇDÉ1*F\n").await;
150
151        // works with line selections
152        cx.set_shared_state("abˇC\n").await;
153        cx.simulate_shared_keystrokes(["shift-v", "U"]).await;
154        cx.assert_shared_state("ˇABC\n").await;
155
156        // works in visual block mode
157        cx.set_shared_state("ˇaa\nbb\ncc").await;
158        cx.simulate_shared_keystrokes(["ctrl-v", "j", "U"]).await;
159        cx.assert_shared_state("ˇAa\nBb\ncc").await;
160    }
161
162    #[gpui::test]
163    async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) {
164        let mut cx = NeovimBackedTestContext::new(cx).await;
165        // works in visual mode
166        cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await;
167        cx.simulate_shared_keystrokes(["u"]).await;
168        cx.assert_shared_state("A😀cˇdé1*f\n").await;
169
170        // works with line selections
171        cx.set_shared_state("ABˇc\n").await;
172        cx.simulate_shared_keystrokes(["shift-v", "u"]).await;
173        cx.assert_shared_state("ˇabc\n").await;
174
175        // works in visual block mode
176        cx.set_shared_state("ˇAa\nBb\nCc").await;
177        cx.simulate_shared_keystrokes(["ctrl-v", "j", "u"]).await;
178        cx.assert_shared_state("ˇaa\nbb\nCc").await;
179    }
180}