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