indent.rs

  1use crate::{Vim, motion::Motion, object::Object, state::Mode};
  2use collections::HashMap;
  3use editor::SelectionEffects;
  4use editor::{Bias, Editor, display_map::ToDisplayPoint};
  5use gpui::actions;
  6use gpui::{Context, Window};
  7use language::SelectionGoal;
  8use settings::Settings;
  9use vim_mode_setting::HelixModeSetting;
 10
 11#[derive(PartialEq, Eq)]
 12pub(crate) enum IndentDirection {
 13    In,
 14    Out,
 15    Auto,
 16}
 17
 18actions!(
 19    vim,
 20    [
 21        /// Increases indentation of selected lines.
 22        Indent,
 23        /// Decreases indentation of selected lines.
 24        Outdent,
 25        /// Automatically adjusts indentation based on syntax.
 26        AutoIndent
 27    ]
 28);
 29
 30pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 31    Vim::action(editor, cx, |vim, _: &Indent, window, cx| {
 32        vim.record_current_action(cx);
 33        let count = Vim::take_count(cx).unwrap_or(1);
 34        Vim::take_forced_motion(cx);
 35        vim.store_visual_marks(window, cx);
 36        vim.update_editor(cx, |vim, editor, cx| {
 37            editor.transact(window, cx, |editor, window, cx| {
 38                let original_positions = vim.save_selection_starts(editor, cx);
 39                for _ in 0..count {
 40                    editor.indent(&Default::default(), window, cx);
 41                }
 42                if !HelixModeSetting::get_global(cx).0 {
 43                    vim.restore_selection_cursors(editor, window, cx, original_positions);
 44                }
 45            });
 46        });
 47        if vim.mode.is_visual() {
 48            vim.switch_mode(Mode::Normal, true, window, cx)
 49        }
 50    });
 51
 52    Vim::action(editor, cx, |vim, _: &Outdent, window, cx| {
 53        vim.record_current_action(cx);
 54        let count = Vim::take_count(cx).unwrap_or(1);
 55        Vim::take_forced_motion(cx);
 56        vim.store_visual_marks(window, cx);
 57        vim.update_editor(cx, |vim, editor, cx| {
 58            editor.transact(window, cx, |editor, window, cx| {
 59                let original_positions = vim.save_selection_starts(editor, cx);
 60                for _ in 0..count {
 61                    editor.outdent(&Default::default(), window, cx);
 62                }
 63                if !HelixModeSetting::get_global(cx).0 {
 64                    vim.restore_selection_cursors(editor, window, cx, original_positions);
 65                }
 66            });
 67        });
 68        if vim.mode.is_visual() {
 69            vim.switch_mode(Mode::Normal, true, window, cx)
 70        }
 71    });
 72
 73    Vim::action(editor, cx, |vim, _: &AutoIndent, window, cx| {
 74        vim.record_current_action(cx);
 75        let count = Vim::take_count(cx).unwrap_or(1);
 76        Vim::take_forced_motion(cx);
 77        vim.store_visual_marks(window, cx);
 78        vim.update_editor(cx, |vim, editor, cx| {
 79            editor.transact(window, cx, |editor, window, cx| {
 80                let original_positions = vim.save_selection_starts(editor, cx);
 81                for _ in 0..count {
 82                    editor.autoindent(&Default::default(), window, cx);
 83                }
 84                vim.restore_selection_cursors(editor, window, cx, original_positions);
 85            });
 86        });
 87        if vim.mode.is_visual() {
 88            vim.switch_mode(Mode::Normal, true, window, cx)
 89        }
 90    });
 91}
 92
 93impl Vim {
 94    pub(crate) fn indent_motion(
 95        &mut self,
 96        motion: Motion,
 97        times: Option<usize>,
 98        forced_motion: bool,
 99        dir: IndentDirection,
100        window: &mut Window,
101        cx: &mut Context<Self>,
102    ) {
103        self.stop_recording(cx);
104        self.update_editor(cx, |_, editor, cx| {
105            let text_layout_details = editor.text_layout_details(window);
106            editor.transact(window, cx, |editor, window, cx| {
107                let mut selection_starts: HashMap<_, _> = Default::default();
108                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
109                    s.move_with(|map, selection| {
110                        let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
111                        selection_starts.insert(selection.id, anchor);
112                        motion.expand_selection(
113                            map,
114                            selection,
115                            times,
116                            &text_layout_details,
117                            forced_motion,
118                        );
119                    });
120                });
121                match dir {
122                    IndentDirection::In => editor.indent(&Default::default(), window, cx),
123                    IndentDirection::Out => editor.outdent(&Default::default(), window, cx),
124                    IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx),
125                }
126                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
127                    s.move_with(|map, selection| {
128                        let anchor = selection_starts.remove(&selection.id).unwrap();
129                        selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
130                    });
131                });
132            });
133        });
134    }
135
136    pub(crate) fn indent_object(
137        &mut self,
138        object: Object,
139        around: bool,
140        dir: IndentDirection,
141        times: Option<usize>,
142        window: &mut Window,
143        cx: &mut Context<Self>,
144    ) {
145        self.stop_recording(cx);
146        self.update_editor(cx, |_, editor, cx| {
147            editor.transact(window, cx, |editor, window, cx| {
148                let mut original_positions: HashMap<_, _> = Default::default();
149                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
150                    s.move_with(|map, selection| {
151                        let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
152                        original_positions.insert(selection.id, anchor);
153                        object.expand_selection(map, selection, around, times);
154                    });
155                });
156                match dir {
157                    IndentDirection::In => editor.indent(&Default::default(), window, cx),
158                    IndentDirection::Out => editor.outdent(&Default::default(), window, cx),
159                    IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx),
160                }
161                editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
162                    s.move_with(|map, selection| {
163                        let anchor = original_positions.remove(&selection.id).unwrap();
164                        selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
165                    });
166                });
167            });
168        });
169    }
170}
171
172#[cfg(test)]
173mod test {
174    use crate::{
175        state::Mode,
176        test::{NeovimBackedTestContext, VimTestContext},
177    };
178    use indoc::indoc;
179
180    #[gpui::test]
181    async fn test_indent_gv(cx: &mut gpui::TestAppContext) {
182        let mut cx = NeovimBackedTestContext::new(cx).await;
183        cx.set_neovim_option("shiftwidth=4").await;
184
185        cx.set_shared_state("ˇhello\nworld\n").await;
186        cx.simulate_shared_keystrokes("v j > g v").await;
187        cx.shared_state()
188            .await
189            .assert_eq("«    hello\n ˇ»   world\n");
190    }
191
192    #[gpui::test]
193    async fn test_indent_hx(cx: &mut gpui::TestAppContext) {
194        let mut cx = VimTestContext::new(cx, true).await;
195        cx.enable_helix();
196
197        cx.set_state("«Hello\nWorldˇ»\n", Mode::HelixNormal);
198
199        cx.simulate_keystrokes(">");
200        cx.assert_state("    «Hello\n    Worldˇ»\n", Mode::HelixNormal);
201
202        cx.simulate_keystrokes("<");
203        cx.assert_state("«Hello\nWorldˇ»\n", Mode::HelixNormal);
204    }
205
206    #[gpui::test]
207    async fn test_autoindent_op(cx: &mut gpui::TestAppContext) {
208        let mut cx = VimTestContext::new(cx, true).await;
209
210        cx.set_state(
211            indoc!(
212                "
213            fn a() {
214                b();
215                c();
216
217                    d();
218                    ˇe();
219                    f();
220
221                g();
222            }
223        "
224            ),
225            Mode::Normal,
226        );
227
228        cx.simulate_keystrokes("= a p");
229        cx.assert_state(
230            indoc!(
231                "
232                fn a() {
233                    b();
234                    c();
235
236                    d();
237                    ˇe();
238                    f();
239
240                    g();
241                }
242            "
243            ),
244            Mode::Normal,
245        );
246    }
247}