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}