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}