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