Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:36:05.324Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

substitute.rs

316 lines · 10.2 KB · rust
1use std::sync::Arc;
2
3use crate::{
4    Mode, Vim,
5    motion::{Motion, MotionKind},
6};
7use editor::{Editor, SelectionEffects, movement};
8use gpui::{Context, Window, actions};
9use language::Point;
10
11actions!(
12    vim,
13    [
14        /// Substitutes characters in the current selection.
15        Substitute,
16        /// Substitutes the entire line.
17        SubstituteLine
18    ]
19);
20
21pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
22    Vim::action(editor, cx, |vim, _: &Substitute, window, cx| {
23        vim.start_recording(cx);
24        let count = Vim::take_count(cx);
25        Vim::take_forced_motion(cx);
26        vim.substitute(count, vim.mode == Mode::VisualLine, window, cx);
27    });
28
29    Vim::action(editor, cx, |vim, _: &SubstituteLine, window, cx| {
30        vim.start_recording(cx);
31        if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) {
32            vim.switch_mode(Mode::VisualLine, false, window, cx)
33        }
34        let count = Vim::take_count(cx);
35        Vim::take_forced_motion(cx);
36        vim.substitute(count, true, window, cx)
37    });
38}
39
40impl Vim {
41    pub fn substitute(
42        &mut self,
43        count: Option<usize>,
44        line_mode: bool,
45        window: &mut Window,
46        cx: &mut Context<Self>,
47    ) {
48        self.store_visual_marks(window, cx);
49        self.update_editor(cx, |vim, editor, cx| {
50            editor.set_clip_at_line_ends(false, cx);
51            editor.transact(window, cx, |editor, window, cx| {
52                let text_layout_details = editor.text_layout_details(window, cx);
53                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
54                    s.move_with(&mut |map, selection| {
55                        if selection.start == selection.end {
56                            Motion::Right.expand_selection(
57                                map,
58                                selection,
59                                count,
60                                &text_layout_details,
61                                false,
62                            );
63                        }
64                        if line_mode {
65                            // in Visual mode when the selection contains the newline at the end
66                            // of the line, we should exclude it.
67                            if !selection.is_empty() && selection.end.column() == 0 {
68                                selection.end = movement::left(map, selection.end);
69                            }
70                            Motion::CurrentLine.expand_selection(
71                                map,
72                                selection,
73                                None,
74                                &text_layout_details,
75                                false,
76                            );
77                            if let Some((point, _)) = (Motion::FirstNonWhitespace {
78                                display_lines: false,
79                            })
80                            .move_point(
81                                map,
82                                selection.start,
83                                selection.goal,
84                                None,
85                                &text_layout_details,
86                            ) {
87                                selection.start = point;
88                            }
89                        }
90                    })
91                });
92                let kind = if line_mode {
93                    MotionKind::Linewise
94                } else {
95                    MotionKind::Exclusive
96                };
97                vim.copy_selections_content(editor, kind, window, cx);
98                let linked_edits = editor.linked_edits_for_selections(Arc::from(""), cx);
99                let selections = editor
100                    .selections
101                    .all::<Point>(&editor.display_snapshot(cx))
102                    .into_iter();
103                let edits = selections.map(|selection| (selection.start..selection.end, ""));
104                editor.edit(edits, cx);
105                linked_edits.apply(cx);
106            });
107        });
108        self.switch_mode(Mode::Insert, true, window, cx);
109    }
110}
111
112#[cfg(test)]
113mod test {
114    use crate::{
115        state::Mode,
116        test::{NeovimBackedTestContext, VimTestContext},
117    };
118    use indoc::indoc;
119
120    #[gpui::test]
121    async fn test_substitute(cx: &mut gpui::TestAppContext) {
122        let mut cx = VimTestContext::new(cx, true).await;
123
124        // supports a single cursor
125        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
126        cx.simulate_keystrokes("s x");
127        cx.assert_editor_state("xˇbc\n");
128
129        // supports a selection
130        cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
131        cx.assert_editor_state("a«bcˇ»\n");
132        cx.simulate_keystrokes("s x");
133        cx.assert_editor_state("axˇ\n");
134
135        // supports counts
136        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
137        cx.simulate_keystrokes("2 s x");
138        cx.assert_editor_state("xˇc\n");
139
140        // supports multiple cursors
141        cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
142        cx.simulate_keystrokes("2 s x");
143        cx.assert_editor_state("axˇdexˇg\n");
144
145        // does not read beyond end of line
146        cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
147        cx.simulate_keystrokes("5 s x");
148        cx.assert_editor_state("xˇ\n");
149
150        // it handles multibyte characters
151        cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
152        cx.simulate_keystrokes("4 s");
153        cx.assert_editor_state(\n");
154
155        // should transactionally undo selection changes
156        cx.simulate_keystrokes("escape u");
157        cx.assert_editor_state("ˇcàfé\n");
158
159        // it handles visual line mode
160        cx.set_state(
161            indoc! {"
162            alpha
163              beˇta
164            gamma"},
165            Mode::Normal,
166        );
167        cx.simulate_keystrokes("shift-v s");
168        cx.assert_editor_state(indoc! {"
169            alpha
170              ˇ
171            gamma"});
172    }
173
174    #[gpui::test]
175    async fn test_visual_change(cx: &mut gpui::TestAppContext) {
176        let mut cx = NeovimBackedTestContext::new(cx).await;
177
178        cx.set_shared_state("The quick ˇbrown").await;
179        cx.simulate_shared_keystrokes("v w c").await;
180        cx.shared_state().await.assert_eq("The quick ˇ");
181
182        cx.set_shared_state(indoc! {"
183            The ˇquick brown
184            fox jumps over
185            the lazy dog"})
186            .await;
187        cx.simulate_shared_keystrokes("v w j c").await;
188        cx.shared_state().await.assert_eq(indoc! {"
189            The ˇver
190            the lazy dog"});
191
192        cx.simulate_at_each_offset(
193            "v w j c",
194            indoc! {"
195                    The ˇquick brown
196                    fox jumps ˇover
197                    the ˇlazy dog"},
198        )
199        .await
200        .assert_matches();
201        cx.simulate_at_each_offset(
202            "v w k c",
203            indoc! {"
204                    The ˇquick brown
205                    fox jumps ˇover
206                    the ˇlazy dog"},
207        )
208        .await
209        .assert_matches();
210    }
211
212    #[gpui::test]
213    async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
214        let mut cx = NeovimBackedTestContext::new(cx).await;
215        cx.simulate(
216            "shift-v c",
217            indoc! {"
218            The quˇick brown
219            fox jumps over
220            the lazy dog"},
221        )
222        .await
223        .assert_matches();
224        // Test pasting code copied on change
225        cx.simulate_shared_keystrokes("escape j p").await;
226        cx.shared_state().await.assert_matches();
227
228        cx.simulate_at_each_offset(
229            "shift-v c",
230            indoc! {"
231            The quick brown
232            fox juˇmps over
233            the laˇzy dog"},
234        )
235        .await
236        .assert_matches();
237        cx.simulate(
238            "shift-v j c",
239            indoc! {"
240            The quˇick brown
241            fox jumps over
242            the lazy dog"},
243        )
244        .await
245        .assert_matches();
246        // Test pasting code copied on delete
247        cx.simulate_shared_keystrokes("escape j p").await;
248        cx.shared_state().await.assert_matches();
249
250        cx.simulate_at_each_offset(
251            "shift-v j c",
252            indoc! {"
253            The quick brown
254            fox juˇmps over
255            the laˇzy dog"},
256        )
257        .await
258        .assert_matches();
259    }
260
261    #[gpui::test]
262    async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
263        let mut cx = NeovimBackedTestContext::new(cx).await;
264
265        let initial_state = indoc! {"
266                    The quick brown
267                    fox juˇmps over
268                    the lazy dog
269                    "};
270
271        // normal mode
272        cx.set_shared_state(initial_state).await;
273        cx.simulate_shared_keystrokes("shift-s o").await;
274        cx.shared_state().await.assert_eq(indoc! {"
275            The quick brown
276
277            the lazy dog
278            "});
279
280        // visual mode
281        cx.set_shared_state(initial_state).await;
282        cx.simulate_shared_keystrokes("v k shift-s o").await;
283        cx.shared_state().await.assert_eq(indoc! {"
284
285            the lazy dog
286            "});
287
288        // visual block mode
289        cx.set_shared_state(initial_state).await;
290        cx.simulate_shared_keystrokes("ctrl-v j shift-s o").await;
291        cx.shared_state().await.assert_eq(indoc! {"
292            The quick brown
293
294            "});
295
296        // visual mode including newline
297        cx.set_shared_state(initial_state).await;
298        cx.simulate_shared_keystrokes("v $ shift-s o").await;
299        cx.shared_state().await.assert_eq(indoc! {"
300            The quick brown
301
302            the lazy dog
303            "});
304
305        // indentation
306        cx.set_neovim_option("shiftwidth=4").await;
307        cx.set_shared_state(initial_state).await;
308        cx.simulate_shared_keystrokes("> > shift-s o").await;
309        cx.shared_state().await.assert_eq(indoc! {"
310            The quick brown
311
312            the lazy dog
313            "});
314    }
315}
316
Served at tenant.openagents/omega Member data and write actions are omitted.