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