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