indent.rs

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