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