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