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}