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