indent.rs

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