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([
106            "i", "1", "1", "escape", "shift-o", "2", "2", "escape", "shift-g", "o", "3", "3",
107            "escape",
108        ])
109        .await;
110
111        cx.assert_shared_state(indoc! {
112            "22
113             11
114             3ˇ3"
115        })
116        .await;
117
118        cx.simulate_shared_keystrokes(["g", ";"]).await;
119        // NOTE: this matches nvim when I type it into it
120        // but in tests, nvim always reports the column as 0...
121        cx.assert_state(
122            indoc! {
123            "22
124             11
125             3ˇ3"
126            },
127            Mode::Normal,
128        );
129        cx.simulate_shared_keystrokes(["g", ";"]).await;
130        cx.assert_state(
131            indoc! {
132            "2ˇ2
133             11
134             33"
135            },
136            Mode::Normal,
137        );
138        cx.simulate_shared_keystrokes(["g", ";"]).await;
139        cx.assert_state(
140            indoc! {
141            "22
142             1ˇ1
143             33"
144            },
145            Mode::Normal,
146        );
147        cx.simulate_shared_keystrokes(["g", ","]).await;
148        cx.assert_state(
149            indoc! {
150            "2ˇ2
151             11
152             33"
153            },
154            Mode::Normal,
155        );
156        cx.simulate_shared_keystrokes(["shift-g", "i", "4", "4", "escape"])
157            .await;
158        cx.simulate_shared_keystrokes(["g", ";"]).await;
159        cx.assert_state(
160            indoc! {
161            "22
162             11
163             34ˇ43"
164            },
165            Mode::Normal,
166        );
167        cx.simulate_shared_keystrokes(["g", ";"]).await;
168        cx.assert_state(
169            indoc! {
170            "2ˇ2
171             11
172             3443"
173            },
174            Mode::Normal,
175        );
176    }
177
178    #[gpui::test]
179    async fn test_change_list_delete(cx: &mut gpui::TestAppContext) {
180        let mut cx = NeovimBackedTestContext::new(cx).await;
181        cx.set_shared_state(indoc! {
182        "one two
183        three fˇour"})
184            .await;
185        cx.simulate_shared_keystrokes(["x", "k", "d", "i", "w", "^", "x"])
186            .await;
187        cx.assert_shared_state(indoc! {
188        "ˇne•
189        three fur"})
190            .await;
191        cx.simulate_shared_keystrokes(["2", "g", ";"]).await;
192        cx.assert_shared_state(indoc! {
193        "ne•
194        three fˇur"})
195            .await;
196        cx.simulate_shared_keystrokes(["g", ","]).await;
197        cx.assert_shared_state(indoc! {
198        "ˇne•
199        three fur"})
200            .await;
201    }
202
203    #[gpui::test]
204    async fn test_gi(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", "g", "i"])
211            .await;
212        cx.simulate_shared_keystrokes(["u", "escape"]).await;
213        cx.assert_shared_state(indoc! {
214        "one two
215        three foˇur"})
216            .await;
217    }
218
219    #[gpui::test]
220    async fn test_dot_mark(cx: &mut gpui::TestAppContext) {
221        let mut cx = NeovimBackedTestContext::new(cx).await;
222        cx.set_shared_state(indoc! {
223        "one two
224        three fˇr"})
225            .await;
226        cx.simulate_shared_keystrokes(["i", "o", "escape", "k", "`", "."])
227            .await;
228        cx.assert_shared_state(indoc! {
229        "one two
230        three fˇor"})
231            .await;
232    }
233}