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