change_list.rs

  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}