change_list.rs

  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}