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