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