change_list.rs

  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        if self.change_list.is_empty() {
 28            return;
 29        }
 30
 31        let prev = self.change_list_position.unwrap_or(self.change_list.len());
 32        let next = if direction == Direction::Prev {
 33            prev.saturating_sub(count)
 34        } else {
 35            (prev + count).min(self.change_list.len() - 1)
 36        };
 37        self.change_list_position = Some(next);
 38        let Some(selections) = self.change_list.get(next).cloned() else {
 39            return;
 40        };
 41        self.update_editor(window, cx, |_, editor, window, cx| {
 42            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
 43                let map = s.display_map();
 44                s.select_display_ranges(selections.into_iter().map(|a| {
 45                    let point = a.to_display_point(&map);
 46                    point..point
 47                }))
 48            })
 49        });
 50    }
 51
 52    pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 53        let Some((map, selections, buffer)) = self.update_editor(window, cx, |_, editor, _, cx| {
 54            let (map, selections) = editor.selections.all_adjusted_display(cx);
 55            let buffer = editor.buffer().clone();
 56            (map, selections, buffer)
 57        }) else {
 58            return;
 59        };
 60
 61        let pop_state = self
 62            .change_list
 63            .last()
 64            .map(|previous| {
 65                previous.len() == selections.len()
 66                    && previous.iter().enumerate().all(|(ix, p)| {
 67                        p.to_display_point(&map).row() == selections[ix].head().row()
 68                    })
 69            })
 70            .unwrap_or(false);
 71
 72        let new_positions: Vec<Anchor> = selections
 73            .into_iter()
 74            .map(|s| {
 75                let point = if self.mode == Mode::Insert {
 76                    movement::saturating_left(&map, s.head())
 77                } else {
 78                    s.head()
 79                };
 80                map.display_point_to_anchor(point, Bias::Left)
 81            })
 82            .collect();
 83
 84        self.change_list_position.take();
 85        if pop_state {
 86            self.change_list.pop();
 87        }
 88        self.change_list.push(new_positions.clone());
 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}