1use editor::scroll::Autoscroll;
2use gpui::ViewContext;
3use language::{Bias, Point};
4use multi_buffer::MultiBufferRow;
5use workspace::Workspace;
6
7use crate::{
8 normal::ChangeCase, normal::ConvertToLowerCase, normal::ConvertToUpperCase, state::Mode, Vim,
9};
10
11pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
12 manipulate_text(cx, |c| {
13 if c.is_lowercase() {
14 c.to_uppercase().collect::<Vec<char>>()
15 } else {
16 c.to_lowercase().collect::<Vec<char>>()
17 }
18 })
19}
20
21pub fn convert_to_upper_case(
22 _: &mut Workspace,
23 _: &ConvertToUpperCase,
24 cx: &mut ViewContext<Workspace>,
25) {
26 manipulate_text(cx, |c| c.to_uppercase().collect::<Vec<char>>())
27}
28
29pub fn convert_to_lower_case(
30 _: &mut Workspace,
31 _: &ConvertToLowerCase,
32 cx: &mut ViewContext<Workspace>,
33) {
34 manipulate_text(cx, |c| c.to_lowercase().collect::<Vec<char>>())
35}
36
37fn manipulate_text<F>(cx: &mut ViewContext<Workspace>, transform: F)
38where
39 F: Fn(char) -> Vec<char> + Copy,
40{
41 Vim::update(cx, |vim, cx| {
42 vim.record_current_action(cx);
43 let count = vim.take_count(cx).unwrap_or(1) as u32;
44 vim.update_active_editor(cx, |vim, editor, cx| {
45 let mut ranges = Vec::new();
46 let mut cursor_positions = Vec::new();
47 let snapshot = editor.buffer().read(cx).snapshot(cx);
48 for selection in editor.selections.all::<Point>(cx) {
49 match vim.state().mode {
50 Mode::VisualLine => {
51 let start = Point::new(selection.start.row, 0);
52 let end = Point::new(
53 selection.end.row,
54 snapshot.line_len(MultiBufferRow(selection.end.row)),
55 );
56 ranges.push(start..end);
57 cursor_positions.push(start..start);
58 }
59 Mode::Visual => {
60 ranges.push(selection.start..selection.end);
61 cursor_positions.push(selection.start..selection.start);
62 }
63 Mode::VisualBlock => {
64 ranges.push(selection.start..selection.end);
65 if cursor_positions.len() == 0 {
66 cursor_positions.push(selection.start..selection.start);
67 }
68 }
69 Mode::Insert | Mode::Normal | Mode::Replace => {
70 let start = selection.start;
71 let mut end = start;
72 for _ in 0..count {
73 end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
74 }
75 ranges.push(start..end);
76
77 if end.column == snapshot.line_len(MultiBufferRow(end.row)) {
78 end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
79 }
80 cursor_positions.push(end..end)
81 }
82 }
83 }
84 editor.transact(cx, |editor, cx| {
85 for range in ranges.into_iter().rev() {
86 let snapshot = editor.buffer().read(cx).snapshot(cx);
87 editor.buffer().update(cx, |buffer, cx| {
88 let text = snapshot
89 .text_for_range(range.start..range.end)
90 .flat_map(|s| s.chars())
91 .flat_map(|c| transform(c))
92 .collect::<String>();
93
94 buffer.edit([(range, text)], None, cx)
95 })
96 }
97 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
98 s.select_ranges(cursor_positions)
99 })
100 });
101 });
102 vim.switch_mode(Mode::Normal, true, cx)
103 })
104}
105
106#[cfg(test)]
107mod test {
108 use crate::{state::Mode, test::NeovimBackedTestContext};
109
110 #[gpui::test]
111 async fn test_change_case(cx: &mut gpui::TestAppContext) {
112 let mut cx = NeovimBackedTestContext::new(cx).await;
113 cx.set_shared_state("ˇabC\n").await;
114 cx.simulate_shared_keystrokes("~").await;
115 cx.shared_state().await.assert_eq("AˇbC\n");
116 cx.simulate_shared_keystrokes("2 ~").await;
117 cx.shared_state().await.assert_eq("ABˇc\n");
118
119 // works in visual mode
120 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
121 cx.simulate_shared_keystrokes("~").await;
122 cx.shared_state().await.assert_eq("a😀CˇDé1*F\n");
123
124 // works with multibyte characters
125 cx.simulate_shared_keystrokes("~").await;
126 cx.set_shared_state("aˇC😀é1*F\n").await;
127 cx.simulate_shared_keystrokes("4 ~").await;
128 cx.shared_state().await.assert_eq("ac😀É1ˇ*F\n");
129
130 // works with line selections
131 cx.set_shared_state("abˇC\n").await;
132 cx.simulate_shared_keystrokes("shift-v ~").await;
133 cx.shared_state().await.assert_eq("ˇABc\n");
134
135 // works in visual block mode
136 cx.set_shared_state("ˇaa\nbb\ncc").await;
137 cx.simulate_shared_keystrokes("ctrl-v j ~").await;
138 cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
139
140 // works with multiple cursors (zed only)
141 cx.set_state("aˇßcdˇe\n", Mode::Normal);
142 cx.simulate_keystrokes("~");
143 cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
144 }
145
146 #[gpui::test]
147 async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) {
148 let mut cx = NeovimBackedTestContext::new(cx).await;
149 // works in visual mode
150 cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
151 cx.simulate_shared_keystrokes("U").await;
152 cx.shared_state().await.assert_eq("a😀CˇDÉ1*F\n");
153
154 // works with line selections
155 cx.set_shared_state("abˇC\n").await;
156 cx.simulate_shared_keystrokes("shift-v U").await;
157 cx.shared_state().await.assert_eq("ˇABC\n");
158
159 // works in visual block mode
160 cx.set_shared_state("ˇaa\nbb\ncc").await;
161 cx.simulate_shared_keystrokes("ctrl-v j U").await;
162 cx.shared_state().await.assert_eq("ˇAa\nBb\ncc");
163 }
164
165 #[gpui::test]
166 async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) {
167 let mut cx = NeovimBackedTestContext::new(cx).await;
168 // works in visual mode
169 cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await;
170 cx.simulate_shared_keystrokes("u").await;
171 cx.shared_state().await.assert_eq("A😀cˇdé1*f\n");
172
173 // works with line selections
174 cx.set_shared_state("ABˇc\n").await;
175 cx.simulate_shared_keystrokes("shift-v u").await;
176 cx.shared_state().await.assert_eq("ˇabc\n");
177
178 // works in visual block mode
179 cx.set_shared_state("ˇAa\nBb\nCc").await;
180 cx.simulate_shared_keystrokes("ctrl-v j u").await;
181 cx.shared_state().await.assert_eq("ˇaa\nbb\nCc");
182 }
183}