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