1use gpui::WindowContext;
2use language::Point;
3
4use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
5
6pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
7 let line_mode = vim.state().mode == Mode::VisualLine;
8 vim.update_active_editor(cx, |editor, cx| {
9 editor.set_clip_at_line_ends(false, cx);
10 editor.transact(cx, |editor, cx| {
11 editor.change_selections(None, cx, |s| {
12 s.move_with(|map, selection| {
13 if selection.start == selection.end {
14 Motion::Right.expand_selection(map, selection, count, true);
15 }
16 if line_mode {
17 Motion::CurrentLine.expand_selection(map, selection, None, false);
18 if let Some((point, _)) = Motion::FirstNonWhitespace.move_point(
19 map,
20 selection.start,
21 selection.goal,
22 None,
23 ) {
24 selection.start = point;
25 }
26 }
27 })
28 });
29 copy_selections_content(editor, line_mode, cx);
30 let selections = editor.selections.all::<Point>(cx).into_iter();
31 let edits = selections.map(|selection| (selection.start..selection.end, ""));
32 editor.edit(edits, cx);
33 });
34 });
35 vim.switch_mode(Mode::Insert, true, cx);
36}
37
38#[cfg(test)]
39mod test {
40 use crate::{
41 state::Mode,
42 test::{NeovimBackedTestContext, VimTestContext},
43 };
44 use indoc::indoc;
45
46 #[gpui::test]
47 async fn test_substitute(cx: &mut gpui::TestAppContext) {
48 let mut cx = VimTestContext::new(cx, true).await;
49
50 // supports a single cursor
51 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
52 cx.simulate_keystrokes(["s", "x"]);
53 cx.assert_editor_state("xˇbc\n");
54
55 // supports a selection
56 cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
57 cx.assert_editor_state("a«bcˇ»\n");
58 cx.simulate_keystrokes(["s", "x"]);
59 cx.assert_editor_state("axˇ\n");
60
61 // supports counts
62 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
63 cx.simulate_keystrokes(["2", "s", "x"]);
64 cx.assert_editor_state("xˇc\n");
65
66 // supports multiple cursors
67 cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
68 cx.simulate_keystrokes(["2", "s", "x"]);
69 cx.assert_editor_state("axˇdexˇg\n");
70
71 // does not read beyond end of line
72 cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
73 cx.simulate_keystrokes(["5", "s", "x"]);
74 cx.assert_editor_state("xˇ\n");
75
76 // it handles multibyte characters
77 cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
78 cx.simulate_keystrokes(["4", "s"]);
79 cx.assert_editor_state("ˇ\n");
80
81 // should transactionally undo selection changes
82 cx.simulate_keystrokes(["escape", "u"]);
83 cx.assert_editor_state("ˇcàfé\n");
84
85 // it handles visual line mode
86 cx.set_state(
87 indoc! {"
88 alpha
89 beˇta
90 gamma"},
91 Mode::Normal,
92 );
93 cx.simulate_keystrokes(["shift-v", "s"]);
94 cx.assert_editor_state(indoc! {"
95 alpha
96 ˇ
97 gamma"});
98 }
99
100 #[gpui::test]
101 async fn test_visual_change(cx: &mut gpui::TestAppContext) {
102 let mut cx = NeovimBackedTestContext::new(cx).await;
103
104 cx.set_shared_state("The quick ˇbrown").await;
105 cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
106 cx.assert_shared_state("The quick ˇ").await;
107
108 cx.set_shared_state(indoc! {"
109 The ˇquick brown
110 fox jumps over
111 the lazy dog"})
112 .await;
113 cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
114 cx.assert_shared_state(indoc! {"
115 The ˇver
116 the lazy dog"})
117 .await;
118
119 let cases = cx.each_marked_position(indoc! {"
120 The ˇquick brown
121 fox jumps ˇover
122 the ˇlazy dog"});
123 for initial_state in cases {
124 cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
125 .await;
126 cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
127 .await;
128 }
129 }
130
131 #[gpui::test]
132 async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
133 let mut cx = NeovimBackedTestContext::new(cx)
134 .await
135 .binding(["shift-v", "c"]);
136 cx.assert(indoc! {"
137 The quˇick brown
138 fox jumps over
139 the lazy dog"})
140 .await;
141 // Test pasting code copied on change
142 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
143 cx.assert_state_matches().await;
144
145 cx.assert_all(indoc! {"
146 The quick brown
147 fox juˇmps over
148 the laˇzy dog"})
149 .await;
150 let mut cx = cx.binding(["shift-v", "j", "c"]);
151 cx.assert(indoc! {"
152 The quˇick brown
153 fox jumps over
154 the lazy dog"})
155 .await;
156 // Test pasting code copied on delete
157 cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
158 cx.assert_state_matches().await;
159
160 cx.assert_all(indoc! {"
161 The quick brown
162 fox juˇmps over
163 the laˇzy dog"})
164 .await;
165 }
166}