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