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 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::<Point>(cx) {
150 match vim.mode {
151 Mode::VisualLine => {
152 let start = Point::new(selection.start.row, 0);
153 let end = Point::new(
154 selection.end.row,
155 snapshot.line_len(MultiBufferRow(selection.end.row)),
156 );
157 ranges.push(start..end);
158 cursor_positions.push(start..start);
159 }
160 Mode::Visual => {
161 ranges.push(selection.start..selection.end);
162 cursor_positions.push(selection.start..selection.start);
163 }
164 Mode::VisualBlock => {
165 ranges.push(selection.start..selection.end);
166 if cursor_positions.is_empty() {
167 cursor_positions.push(selection.start..selection.start);
168 }
169 }
170
171 Mode::HelixNormal => {}
172 Mode::Insert | Mode::Normal | Mode::Replace => {
173 let start = selection.start;
174 let mut end = start;
175 for _ in 0..count {
176 end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
177 }
178 ranges.push(start..end);
179
180 if end.column == snapshot.line_len(MultiBufferRow(end.row)) {
181 end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
182 }
183 cursor_positions.push(end..end)
184 }
185 }
186 }
187 editor.transact(window, cx, |editor, window, cx| {
188 for range in ranges.into_iter().rev() {
189 let snapshot = editor.buffer().read(cx).snapshot(cx);
190 let text = snapshot
191 .text_for_range(range.start..range.end)
192 .flat_map(|s| s.chars())
193 .flat_map(transform)
194 .collect::<String>();
195 editor.edit([(range, text)], cx)
196 }
197 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
198 s.select_ranges(cursor_positions)
199 })
200 });
201 });
202 self.switch_mode(Mode::Normal, true, window, cx)
203 }
204}
205
206#[cfg(test)]
207mod test {
208 use crate::{state::Mode, test::NeovimBackedTestContext};
209
210 #[gpui::test]
211 async fn test_change_case(cx: &mut gpui::TestAppContext) {
212 let mut cx = NeovimBackedTestContext::new(cx).await;
213 cx.set_shared_state("ˇabC\n").await;
214 cx.simulate_shared_keystrokes("~").await;
215 cx.shared_state().await.assert_eq("AˇbC\n");
216 cx.simulate_shared_keystrokes("2 ~").await;
217 cx.shared_state().await.assert_eq("ABˇc\n");
218
219 // works in visual mode
220 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
221 cx.simulate_shared_keystrokes("~").await;
222 cx.shared_state().await.assert_eq("a😀CˇDé1*F\n");
223
224 // works with multibyte characters
225 cx.simulate_shared_keystrokes("~").await;
226 cx.set_shared_state("aˇC😀é1*F\n").await;
227 cx.simulate_shared_keystrokes("4 ~").await;
228 cx.shared_state().await.assert_eq("ac😀É1ˇ*F\n");
229
230 // works with line selections
231 cx.set_shared_state("abˇC\n").await;
232 cx.simulate_shared_keystrokes("shift-v ~").await;
233 cx.shared_state().await.assert_eq("ˇABc\n");
234
235 // works in visual block mode
236 cx.set_shared_state("ˇaa\nbb\ncc").await;
237 cx.simulate_shared_keystrokes("ctrl-v j ~").await;
238 cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
239
240 // works with multiple cursors (zed only)
241 cx.set_state("aˇßcdˇe\n", Mode::Normal);
242 cx.simulate_keystrokes("~");
243 cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
244 }
245
246 #[gpui::test]
247 async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) {
248 let mut cx = NeovimBackedTestContext::new(cx).await;
249 // works in visual mode
250 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
251 cx.simulate_shared_keystrokes("shift-u").await;
252 cx.shared_state().await.assert_eq("a😀CˇDÉ1*F\n");
253
254 // works with line selections
255 cx.set_shared_state("abˇC\n").await;
256 cx.simulate_shared_keystrokes("shift-v shift-u").await;
257 cx.shared_state().await.assert_eq("ˇABC\n");
258
259 // works in visual block mode
260 cx.set_shared_state("ˇaa\nbb\ncc").await;
261 cx.simulate_shared_keystrokes("ctrl-v j shift-u").await;
262 cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
263 }
264
265 #[gpui::test]
266 async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) {
267 let mut cx = NeovimBackedTestContext::new(cx).await;
268 // works in visual mode
269 cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await;
270 cx.simulate_shared_keystrokes("u").await;
271 cx.shared_state().await.assert_eq("A😀cˇdé1*f\n");
272
273 // works with line selections
274 cx.set_shared_state("ABˇc\n").await;
275 cx.simulate_shared_keystrokes("shift-v u").await;
276 cx.shared_state().await.assert_eq("ˇabc\n");
277
278 // works in visual block mode
279 cx.set_shared_state("ˇAa\nBb\nCc").await;
280 cx.simulate_shared_keystrokes("ctrl-v j u").await;
281 cx.shared_state().await.assert_eq("ˇaa\nbb\nCc");
282 }
283
284 #[gpui::test]
285 async fn test_change_case_motion(cx: &mut gpui::TestAppContext) {
286 let mut cx = NeovimBackedTestContext::new(cx).await;
287
288 cx.set_shared_state("ˇabc def").await;
289 cx.simulate_shared_keystrokes("g shift-u w").await;
290 cx.shared_state().await.assert_eq("ˇABC def");
291
292 cx.simulate_shared_keystrokes("g u w").await;
293 cx.shared_state().await.assert_eq("ˇabc def");
294
295 cx.simulate_shared_keystrokes("g ~ w").await;
296 cx.shared_state().await.assert_eq("ˇABC def");
297
298 cx.simulate_shared_keystrokes(".").await;
299 cx.shared_state().await.assert_eq("ˇabc def");
300
301 cx.set_shared_state("abˇc def").await;
302 cx.simulate_shared_keystrokes("g ~ i w").await;
303 cx.shared_state().await.assert_eq("ˇABC def");
304
305 cx.simulate_shared_keystrokes(".").await;
306 cx.shared_state().await.assert_eq("ˇabc def");
307
308 cx.simulate_shared_keystrokes("g shift-u $").await;
309 cx.shared_state().await.assert_eq("ˇABC DEF");
310 }
311
312 #[gpui::test]
313 async fn test_change_case_motion_object(cx: &mut gpui::TestAppContext) {
314 let mut cx = NeovimBackedTestContext::new(cx).await;
315
316 cx.set_shared_state("abc dˇef\n").await;
317 cx.simulate_shared_keystrokes("g shift-u i w").await;
318 cx.shared_state().await.assert_eq("abc ˇDEF\n");
319 }
320}