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}