1use editor::movement;
2use gpui::{actions, AppContext, WindowContext};
3use language::Point;
4use workspace::Workspace;
5
6use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
7
8actions!(vim, [Substitute, SubstituteLine]);
9
10pub(crate) fn init(cx: &mut AppContext) {
11 cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
12 Vim::update(cx, |vim, cx| {
13 vim.start_recording(cx);
14 let count = vim.take_count(cx);
15 substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
16 })
17 });
18
19 cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
20 Vim::update(cx, |vim, cx| {
21 vim.start_recording(cx);
22 if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
23 vim.switch_mode(Mode::VisualLine, false, cx)
24 }
25 let count = vim.take_count(cx);
26 substitute(vim, count, true, cx)
27 })
28 });
29}
30
31pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
32 vim.update_active_editor(cx, |editor, cx| {
33 editor.set_clip_at_line_ends(false, cx);
34 editor.transact(cx, |editor, cx| {
35 editor.change_selections(None, cx, |s| {
36 s.move_with(|map, selection| {
37 if selection.start == selection.end {
38 Motion::Right.expand_selection(map, selection, count, true);
39 }
40 if line_mode {
41 // in Visual mode when the selection contains the newline at the end
42 // of the line, we should exclude it.
43 if !selection.is_empty() && selection.end.column() == 0 {
44 selection.end = movement::left(map, selection.end);
45 }
46 Motion::CurrentLine.expand_selection(map, selection, None, false);
47 if let Some((point, _)) = (Motion::FirstNonWhitespace {
48 display_lines: false,
49 })
50 .move_point(
51 map,
52 selection.start,
53 selection.goal,
54 None,
55 ) {
56 selection.start = point;
57 }
58 }
59 })
60 });
61 copy_selections_content(editor, line_mode, cx);
62 let selections = editor.selections.all::<Point>(cx).into_iter();
63 let edits = selections.map(|selection| (selection.start..selection.end, ""));
64 editor.edit(edits, cx);
65 });
66 });
67 vim.switch_mode(Mode::Insert, true, cx);
68}
69
70#[cfg(test)]
71mod test {
72 use crate::{
73 state::Mode,
74 test::{NeovimBackedTestContext, VimTestContext},
75 };
76 use indoc::indoc;
77
78 #[gpui::test]
79 async fn test_substitute(cx: &mut gpui::TestAppContext) {
80 let mut cx = VimTestContext::new(cx, true).await;
81
82 // supports a single cursor
83 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
84 cx.simulate_keystrokes(["s", "x"]);
85 cx.assert_editor_state("xˇbc\n");
86
87 // supports a selection
88 cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
89 cx.assert_editor_state("a«bcˇ»\n");
90 cx.simulate_keystrokes(["s", "x"]);
91 cx.assert_editor_state("axˇ\n");
92
93 // supports counts
94 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
95 cx.simulate_keystrokes(["2", "s", "x"]);
96 cx.assert_editor_state("xˇc\n");
97
98 // supports multiple cursors
99 cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
100 cx.simulate_keystrokes(["2", "s", "x"]);
101 cx.assert_editor_state("axˇdexˇg\n");
102
103 // does not read beyond end of line
104 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
105 cx.simulate_keystrokes(["5", "s", "x"]);
106 cx.assert_editor_state("xˇ\n");
107
108 // it handles multibyte characters
109 cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
110 cx.simulate_keystrokes(["4", "s"]);
111 cx.assert_editor_state("ˇ\n");
112
113 // should transactionally undo selection changes
114 cx.simulate_keystrokes(["escape", "u"]);
115 cx.assert_editor_state("ˇcàfé\n");
116
117 // it handles visual line mode
118 cx.set_state(
119 indoc! {"
120 alpha
121 beˇta
122 gamma"},
123 Mode::Normal,
124 );
125 cx.simulate_keystrokes(["shift-v", "s"]);
126 cx.assert_editor_state(indoc! {"
127 alpha
128 ˇ
129 gamma"});
130 }
131
132 #[gpui::test]
133 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
134 let mut cx = NeovimBackedTestContext::new(cx).await;
135
136 cx.set_shared_state("The quick ˇbrown").await;
137 cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
138 cx.assert_shared_state("The quick ˇ").await;
139
140 cx.set_shared_state(indoc! {"
141 The ˇquick brown
142 fox jumps over
143 the lazy dog"})
144 .await;
145 cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
146 cx.assert_shared_state(indoc! {"
147 The ˇver
148 the lazy dog"})
149 .await;
150
151 let cases = cx.each_marked_position(indoc! {"
152 The ˇquick brown
153 fox jumps ˇover
154 the ˇlazy dog"});
155 for initial_state in cases {
156 cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
157 .await;
158 cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
159 .await;
160 }
161 }
162
163 #[gpui::test]
164 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
165 let mut cx = NeovimBackedTestContext::new(cx)
166 .await
167 .binding(["shift-v", "c"]);
168 cx.assert(indoc! {"
169 The quˇick brown
170 fox jumps over
171 the lazy dog"})
172 .await;
173 // Test pasting code copied on change
174 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
175 cx.assert_state_matches().await;
176
177 cx.assert_all(indoc! {"
178 The quick brown
179 fox juˇmps over
180 the laˇzy dog"})
181 .await;
182 let mut cx = cx.binding(["shift-v", "j", "c"]);
183 cx.assert(indoc! {"
184 The quˇick brown
185 fox jumps over
186 the lazy dog"})
187 .await;
188 // Test pasting code copied on delete
189 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
190 cx.assert_state_matches().await;
191
192 cx.assert_all(indoc! {"
193 The quick brown
194 fox juˇmps over
195 the laˇzy dog"})
196 .await;
197 }
198
199 #[gpui::test]
200 async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
201 let mut cx = NeovimBackedTestContext::new(cx).await;
202
203 let initial_state = indoc! {"
204 The quick brown
205 fox juˇmps over
206 the lazy dog
207 "};
208
209 // normal mode
210 cx.set_shared_state(initial_state).await;
211 cx.simulate_shared_keystrokes(["shift-s", "o"]).await;
212 cx.assert_shared_state(indoc! {"
213 The quick brown
214 oˇ
215 the lazy dog
216 "})
217 .await;
218
219 // visual mode
220 cx.set_shared_state(initial_state).await;
221 cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
222 .await;
223 cx.assert_shared_state(indoc! {"
224 oˇ
225 the lazy dog
226 "})
227 .await;
228
229 // visual block mode
230 cx.set_shared_state(initial_state).await;
231 cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
232 .await;
233 cx.assert_shared_state(indoc! {"
234 The quick brown
235 oˇ
236 "})
237 .await;
238
239 // visual mode including newline
240 cx.set_shared_state(initial_state).await;
241 cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
242 .await;
243 cx.assert_shared_state(indoc! {"
244 The quick brown
245 oˇ
246 the lazy dog
247 "})
248 .await;
249
250 // indentation
251 cx.set_neovim_option("shiftwidth=4").await;
252 cx.set_shared_state(initial_state).await;
253 cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
254 .await;
255 cx.assert_shared_state(indoc! {"
256 The quick brown
257 oˇ
258 the lazy dog
259 "})
260 .await;
261 }
262}