change_list.rs

  1use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, Bias, Direction, Editor};
  2use gpui::{actions, View};
  3use ui::{ViewContext, WindowContext};
  4use workspace::Workspace;
  5
  6use crate::{state::Mode, Vim};
  7
  8actions!(vim, [ChangeListOlder, ChangeListNewer]);
  9
 10pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 11    workspace.register_action(|_, _: &ChangeListOlder, cx| {
 12        Vim::update(cx, |vim, cx| {
 13            move_to_change(vim, Direction::Prev, cx);
 14        })
 15    });
 16    workspace.register_action(|_, _: &ChangeListNewer, cx| {
 17        Vim::update(cx, |vim, cx| {
 18            move_to_change(vim, Direction::Next, cx);
 19        })
 20    });
 21}
 22
 23fn move_to_change(vim: &mut Vim, direction: Direction, cx: &mut WindowContext) {
 24    let count = vim.take_count(cx).unwrap_or(1);
 25    let selections = vim.update_state(|state| {
 26        if state.change_list.is_empty() {
 27            return None;
 28        }
 29
 30        let prev = state
 31            .change_list_position
 32            .unwrap_or(state.change_list.len());
 33        let next = if direction == Direction::Prev {
 34            prev.saturating_sub(count)
 35        } else {
 36            (prev + count).min(state.change_list.len() - 1)
 37        };
 38        state.change_list_position = Some(next);
 39        state.change_list.get(next).cloned()
 40    });
 41
 42    let Some(selections) = selections else {
 43        return;
 44    };
 45    vim.update_active_editor(cx, |_, editor, cx| {
 46        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
 47            let map = s.display_map();
 48            s.select_display_ranges(selections.into_iter().map(|a| {
 49                let point = a.to_display_point(&map);
 50                point..point
 51            }))
 52        })
 53    });
 54}
 55
 56pub(crate) fn push_to_change_list(vim: &mut Vim, editor: View<Editor>, cx: &mut WindowContext) {
 57    let (map, selections) =
 58        editor.update(cx, |editor, cx| editor.selections.all_adjusted_display(cx));
 59
 60    let pop_state =
 61        vim.state()
 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 = selections
 73        .into_iter()
 74        .map(|s| {
 75            let point = if vim.state().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    vim.update_state(|state| {
 85        state.change_list_position.take();
 86        if pop_state {
 87            state.change_list.pop();
 88        }
 89        state.change_list.push(new_positions);
 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}