indent.rs

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