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