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