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