1use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, Bias, Direction, Editor};
2use gpui::{actions, View};
3use ui::{ViewContext, WindowContext};
4use workspace::Workspace;
5
6use crate::{state::Mode, Vim};
7
8actions!(vim, [ChangeListOlder, ChangeListNewer]);
9
10pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
11 workspace.register_action(|_, _: &ChangeListOlder, cx| {
12 Vim::update(cx, |vim, cx| {
13 move_to_change(vim, Direction::Prev, cx);
14 })
15 });
16 workspace.register_action(|_, _: &ChangeListNewer, cx| {
17 Vim::update(cx, |vim, cx| {
18 move_to_change(vim, Direction::Next, cx);
19 })
20 });
21}
22
23fn move_to_change(vim: &mut Vim, direction: Direction, cx: &mut WindowContext) {
24 let count = vim.take_count(cx).unwrap_or(1);
25 let selections = vim.update_state(|state| {
26 if state.change_list.is_empty() {
27 return None;
28 }
29
30 let prev = state
31 .change_list_position
32 .unwrap_or(state.change_list.len());
33 let next = if direction == Direction::Prev {
34 prev.saturating_sub(count)
35 } else {
36 (prev + count).min(state.change_list.len() - 1)
37 };
38 state.change_list_position = Some(next);
39 state.change_list.get(next).cloned()
40 });
41
42 let Some(selections) = selections else {
43 return;
44 };
45 vim.update_active_editor(cx, |_, editor, cx| {
46 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
47 let map = s.display_map();
48 s.select_display_ranges(selections.into_iter().map(|a| {
49 let point = a.to_display_point(&map);
50 point..point
51 }))
52 })
53 });
54}
55
56pub(crate) fn push_to_change_list(vim: &mut Vim, editor: View<Editor>, cx: &mut WindowContext) {
57 let (map, selections) =
58 editor.update(cx, |editor, cx| editor.selections.all_adjusted_display(cx));
59
60 let pop_state =
61 vim.state()
62 .change_list
63 .last()
64 .map(|previous| {
65 previous.len() == selections.len()
66 && previous.iter().enumerate().all(|(ix, p)| {
67 p.to_display_point(&map).row() == selections[ix].head().row()
68 })
69 })
70 .unwrap_or(false);
71
72 let new_positions = selections
73 .into_iter()
74 .map(|s| {
75 let point = if vim.state().mode == Mode::Insert {
76 movement::saturating_left(&map, s.head())
77 } else {
78 s.head()
79 };
80 map.display_point_to_anchor(point, Bias::Left)
81 })
82 .collect();
83
84 vim.update_state(|state| {
85 state.change_list_position.take();
86 if pop_state {
87 state.change_list.pop();
88 }
89 state.change_list.push(new_positions);
90 })
91}
92
93#[cfg(test)]
94mod test {
95 use indoc::indoc;
96
97 use crate::{state::Mode, test::NeovimBackedTestContext};
98
99 #[gpui::test]
100 async fn test_change_list_insert(cx: &mut gpui::TestAppContext) {
101 let mut cx = NeovimBackedTestContext::new(cx).await;
102
103 cx.set_shared_state("ˇ").await;
104
105 cx.simulate_shared_keystrokes([
106 "i", "1", "1", "escape", "shift-o", "2", "2", "escape", "shift-g", "o", "3", "3",
107 "escape",
108 ])
109 .await;
110
111 cx.assert_shared_state(indoc! {
112 "22
113 11
114 3ˇ3"
115 })
116 .await;
117
118 cx.simulate_shared_keystrokes(["g", ";"]).await;
119 // NOTE: this matches nvim when I type it into it
120 // but in tests, nvim always reports the column as 0...
121 cx.assert_state(
122 indoc! {
123 "22
124 11
125 3ˇ3"
126 },
127 Mode::Normal,
128 );
129 cx.simulate_shared_keystrokes(["g", ";"]).await;
130 cx.assert_state(
131 indoc! {
132 "2ˇ2
133 11
134 33"
135 },
136 Mode::Normal,
137 );
138 cx.simulate_shared_keystrokes(["g", ";"]).await;
139 cx.assert_state(
140 indoc! {
141 "22
142 1ˇ1
143 33"
144 },
145 Mode::Normal,
146 );
147 cx.simulate_shared_keystrokes(["g", ","]).await;
148 cx.assert_state(
149 indoc! {
150 "2ˇ2
151 11
152 33"
153 },
154 Mode::Normal,
155 );
156 cx.simulate_shared_keystrokes(["shift-g", "i", "4", "4", "escape"])
157 .await;
158 cx.simulate_shared_keystrokes(["g", ";"]).await;
159 cx.assert_state(
160 indoc! {
161 "22
162 11
163 34ˇ43"
164 },
165 Mode::Normal,
166 );
167 cx.simulate_shared_keystrokes(["g", ";"]).await;
168 cx.assert_state(
169 indoc! {
170 "2ˇ2
171 11
172 3443"
173 },
174 Mode::Normal,
175 );
176 }
177
178 #[gpui::test]
179 async fn test_change_list_delete(cx: &mut gpui::TestAppContext) {
180 let mut cx = NeovimBackedTestContext::new(cx).await;
181 cx.set_shared_state(indoc! {
182 "one two
183 three fˇour"})
184 .await;
185 cx.simulate_shared_keystrokes(["x", "k", "d", "i", "w", "^", "x"])
186 .await;
187 cx.assert_shared_state(indoc! {
188 "ˇne•
189 three fur"})
190 .await;
191 cx.simulate_shared_keystrokes(["2", "g", ";"]).await;
192 cx.assert_shared_state(indoc! {
193 "ne•
194 three fˇur"})
195 .await;
196 cx.simulate_shared_keystrokes(["g", ","]).await;
197 cx.assert_shared_state(indoc! {
198 "ˇne•
199 three fur"})
200 .await;
201 }
202
203 #[gpui::test]
204 async fn test_gi(cx: &mut gpui::TestAppContext) {
205 let mut cx = NeovimBackedTestContext::new(cx).await;
206 cx.set_shared_state(indoc! {
207 "one two
208 three fˇr"})
209 .await;
210 cx.simulate_shared_keystrokes(["i", "o", "escape", "k", "g", "i"])
211 .await;
212 cx.simulate_shared_keystrokes(["u", "escape"]).await;
213 cx.assert_shared_state(indoc! {
214 "one two
215 three foˇur"})
216 .await;
217 }
218
219 #[gpui::test]
220 async fn test_dot_mark(cx: &mut gpui::TestAppContext) {
221 let mut cx = NeovimBackedTestContext::new(cx).await;
222 cx.set_shared_state(indoc! {
223 "one two
224 three fˇr"})
225 .await;
226 cx.simulate_shared_keystrokes(["i", "o", "escape", "k", "`", "."])
227 .await;
228 cx.assert_shared_state(indoc! {
229 "one two
230 three fˇor"})
231 .await;
232 }
233}