Detailed changes
@@ -240,6 +240,9 @@
],
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
+ "g i": ["workspace::SendKeystrokes", "` ^ i"],
+ "g ,": "vim::ChangeListNewer",
+ "g ;": "vim::ChangeListOlder",
"shift-h": "vim::WindowTop",
"shift-m": "vim::WindowMiddle",
"shift-l": "vim::WindowBottom",
@@ -477,6 +477,11 @@ impl DisplaySnapshot {
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
}
+ pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor {
+ self.buffer_snapshot
+ .anchor_at(point.to_offset(&self, bias), bias)
+ }
+
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
@@ -0,0 +1,233 @@
+use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, Bias, Direction, Editor};
+use gpui::{actions, View};
+use ui::{ViewContext, WindowContext};
+use workspace::Workspace;
+
+use crate::{state::Mode, Vim};
+
+actions!(vim, [ChangeListOlder, ChangeListNewer]);
+
+pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(|_, _: &ChangeListOlder, cx| {
+ Vim::update(cx, |vim, cx| {
+ move_to_change(vim, Direction::Prev, cx);
+ })
+ });
+ workspace.register_action(|_, _: &ChangeListNewer, cx| {
+ Vim::update(cx, |vim, cx| {
+ move_to_change(vim, Direction::Next, cx);
+ })
+ });
+}
+
+fn move_to_change(vim: &mut Vim, direction: Direction, cx: &mut WindowContext) {
+ let count = vim.take_count(cx).unwrap_or(1);
+ let selections = vim.update_state(|state| {
+ if state.change_list.is_empty() {
+ return None;
+ }
+
+ let prev = state
+ .change_list_position
+ .unwrap_or(state.change_list.len());
+ let next = if direction == Direction::Prev {
+ prev.saturating_sub(count)
+ } else {
+ (prev + count).min(state.change_list.len() - 1)
+ };
+ state.change_list_position = Some(next);
+ state.change_list.get(next).cloned()
+ });
+
+ let Some(selections) = selections else {
+ return;
+ };
+ vim.update_active_editor(cx, |_, editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let map = s.display_map();
+ s.select_display_ranges(selections.into_iter().map(|a| {
+ let point = a.to_display_point(&map);
+ point..point
+ }))
+ })
+ });
+}
+
+pub(crate) fn push_to_change_list(vim: &mut Vim, editor: View<Editor>, cx: &mut WindowContext) {
+ let (map, selections) =
+ editor.update(cx, |editor, cx| editor.selections.all_adjusted_display(cx));
+
+ let pop_state =
+ vim.state()
+ .change_list
+ .last()
+ .map(|previous| {
+ previous.len() == selections.len()
+ && previous.iter().enumerate().all(|(ix, p)| {
+ p.to_display_point(&map).row() == selections[ix].head().row()
+ })
+ })
+ .unwrap_or(false);
+
+ let new_positions = selections
+ .into_iter()
+ .map(|s| {
+ let point = if vim.state().mode == Mode::Insert {
+ movement::saturating_left(&map, s.head())
+ } else {
+ s.head()
+ };
+ map.display_point_to_anchor(point, Bias::Left)
+ })
+ .collect();
+
+ vim.update_state(|state| {
+ state.change_list_position.take();
+ if pop_state {
+ state.change_list.pop();
+ }
+ state.change_list.push(new_positions);
+ })
+}
+
+#[cfg(test)]
+mod test {
+ use indoc::indoc;
+
+ use crate::{state::Mode, test::NeovimBackedTestContext};
+
+ #[gpui::test]
+ async fn test_change_list_insert(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state("ˇ").await;
+
+ cx.simulate_shared_keystrokes([
+ "i", "1", "1", "escape", "shift-o", "2", "2", "escape", "shift-g", "o", "3", "3",
+ "escape",
+ ])
+ .await;
+
+ cx.assert_shared_state(indoc! {
+ "22
+ 11
+ 3ˇ3"
+ })
+ .await;
+
+ cx.simulate_shared_keystrokes(["g", ";"]).await;
+ // NOTE: this matches nvim when I type it into it
+ // but in tests, nvim always reports the column as 0...
+ cx.assert_state(
+ indoc! {
+ "22
+ 11
+ 3ˇ3"
+ },
+ Mode::Normal,
+ );
+ cx.simulate_shared_keystrokes(["g", ";"]).await;
+ cx.assert_state(
+ indoc! {
+ "2ˇ2
+ 11
+ 33"
+ },
+ Mode::Normal,
+ );
+ cx.simulate_shared_keystrokes(["g", ";"]).await;
+ cx.assert_state(
+ indoc! {
+ "22
+ 1ˇ1
+ 33"
+ },
+ Mode::Normal,
+ );
+ cx.simulate_shared_keystrokes(["g", ","]).await;
+ cx.assert_state(
+ indoc! {
+ "2ˇ2
+ 11
+ 33"
+ },
+ Mode::Normal,
+ );
+ cx.simulate_shared_keystrokes(["shift-g", "i", "4", "4", "escape"])
+ .await;
+ cx.simulate_shared_keystrokes(["g", ";"]).await;
+ cx.assert_state(
+ indoc! {
+ "22
+ 11
+ 34ˇ43"
+ },
+ Mode::Normal,
+ );
+ cx.simulate_shared_keystrokes(["g", ";"]).await;
+ cx.assert_state(
+ indoc! {
+ "2ˇ2
+ 11
+ 3443"
+ },
+ Mode::Normal,
+ );
+ }
+
+ #[gpui::test]
+ async fn test_change_list_delete(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.set_shared_state(indoc! {
+ "one two
+ three fˇour"})
+ .await;
+ cx.simulate_shared_keystrokes(["x", "k", "d", "i", "w", "^", "x"])
+ .await;
+ cx.assert_shared_state(indoc! {
+ "ˇne•
+ three fur"})
+ .await;
+ cx.simulate_shared_keystrokes(["2", "g", ";"]).await;
+ cx.assert_shared_state(indoc! {
+ "ne•
+ three fˇur"})
+ .await;
+ cx.simulate_shared_keystrokes(["g", ","]).await;
+ cx.assert_shared_state(indoc! {
+ "ˇne•
+ three fur"})
+ .await;
+ }
+
+ #[gpui::test]
+ async fn test_gi(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.set_shared_state(indoc! {
+ "one two
+ three fˇr"})
+ .await;
+ cx.simulate_shared_keystrokes(["i", "o", "escape", "k", "g", "i"])
+ .await;
+ cx.simulate_shared_keystrokes(["u", "escape"]).await;
+ cx.assert_shared_state(indoc! {
+ "one two
+ three foˇur"})
+ .await;
+ }
+
+ #[gpui::test]
+ async fn test_dot_mark(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.set_shared_state(indoc! {
+ "one two
+ three fˇr"})
+ .await;
+ cx.simulate_shared_keystrokes(["i", "o", "escape", "k", "`", "."])
+ .await;
+ cx.assert_shared_state(indoc! {
+ "one two
+ three fˇor"})
+ .await;
+ }
+}
@@ -68,9 +68,11 @@ pub fn create_mark_before(vim: &mut Vim, text: Arc<str>, cx: &mut WindowContext)
}
pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) {
- let anchors = match &*text {
- "{" | "}" => Vim::update(cx, |vim, cx| {
- vim.update_active_editor(cx, |_, editor, cx| {
+ let anchors = Vim::update(cx, |vim, cx| {
+ vim.pop_operator(cx);
+
+ match &*text {
+ "{" | "}" => vim.update_active_editor(cx, |_, editor, cx| {
let (map, selections) = editor.selections.all_display(cx);
selections
.into_iter()
@@ -84,13 +86,10 @@ pub fn jump(text: Arc<str>, line: bool, cx: &mut WindowContext) {
.anchor_before(point.to_offset(&map, Bias::Left))
})
.collect::<Vec<Anchor>>()
- })
- }),
- _ => Vim::read(cx).state().marks.get(&*text).cloned(),
- };
-
- Vim::update(cx, |vim, cx| {
- vim.pop_operator(cx);
+ }),
+ "." => vim.state().change_list.last().cloned(),
+ _ => vim.state().marks.get(&*text).cloned(),
+ }
});
let Some(anchors) = anchors else { return };
@@ -77,6 +77,8 @@ pub struct EditorState {
pub replacements: Vec<(Range<editor::Anchor>, String)>,
pub marks: HashMap<String, Vec<Anchor>>,
+ pub change_list: Vec<Vec<Anchor>>,
+ pub change_list_position: Option<usize>,
pub current_tx: Option<TransactionId>,
pub current_anchor: Option<Selection<Anchor>>,
@@ -3,6 +3,7 @@
#[cfg(test)]
mod test;
+mod change_list;
mod command;
mod editor_events;
mod insert;
@@ -17,6 +18,7 @@ mod utils;
mod visual;
use anyhow::Result;
+use change_list::push_to_change_list;
use collections::HashMap;
use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
use editor::{
@@ -159,6 +161,7 @@ fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
replace::register(workspace, cx);
object::register(workspace, cx);
visual::register(workspace, cx);
+ change_list::register(workspace, cx);
}
/// Called whenever an keystroke is typed so vim can observe all actions
@@ -264,6 +267,7 @@ impl Vim {
EditorEvent::TransactionUndone { transaction_id } => Vim::update(cx, |vim, cx| {
vim.transaction_undone(transaction_id, cx);
}),
+ EditorEvent::Edited => Vim::update(cx, |vim, cx| vim.transaction_ended(editor, cx)),
_ => {}
}));
@@ -618,6 +622,10 @@ impl Vim {
self.switch_mode(Mode::Normal, true, cx)
}
+ fn transaction_ended(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
+ push_to_change_list(self, editor, cx)
+ }
+
fn local_selections_changed(&mut self, editor: View<Editor>, cx: &mut WindowContext) {
let newest = editor.read(cx).selections.newest_anchor().clone();
let is_multicursor = editor.read(cx).selections.count() > 1;
@@ -0,0 +1,16 @@
+{"Put":{"state":"one two\nthree fˇour"}}
+{"Key":"x"}
+{"Key":"k"}
+{"Key":"d"}
+{"Key":"i"}
+{"Key":"w"}
+{"Key":"^"}
+{"Key":"x"}
+{"Get":{"state":"ˇne \nthree fur","mode":"Normal"}}
+{"Key":"2"}
+{"Key":"g"}
+{"Key":";"}
+{"Get":{"state":"ne \nthree fˇur","mode":"Normal"}}
+{"Key":"g"}
+{"Key":","}
+{"Get":{"state":"ˇne \nthree fur","mode":"Normal"}}
@@ -0,0 +1,32 @@
+{"Put":{"state":"ˇ"}}
+{"Key":"i"}
+{"Key":"1"}
+{"Key":"1"}
+{"Key":"escape"}
+{"Key":"shift-o"}
+{"Key":"2"}
+{"Key":"2"}
+{"Key":"escape"}
+{"Key":"shift-g"}
+{"Key":"o"}
+{"Key":"3"}
+{"Key":"3"}
+{"Key":"escape"}
+{"Get":{"state":"22\n11\n3ˇ3","mode":"Normal"}}
+{"Key":"g"}
+{"Key":";"}
+{"Key":"g"}
+{"Key":";"}
+{"Key":"g"}
+{"Key":";"}
+{"Key":"g"}
+{"Key":","}
+{"Key":"shift-g"}
+{"Key":"i"}
+{"Key":"4"}
+{"Key":"4"}
+{"Key":"escape"}
+{"Key":"g"}
+{"Key":";"}
+{"Key":"g"}
+{"Key":";"}
@@ -0,0 +1,8 @@
+{"Put":{"state":"one two\nthree fˇr"}}
+{"Key":"i"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Key":"k"}
+{"Key":"`"}
+{"Key":"."}
+{"Get":{"state":"one two\nthree fˇor","mode":"Normal"}}
@@ -0,0 +1,10 @@
+{"Put":{"state":"one two\nthree fˇr"}}
+{"Key":"i"}
+{"Key":"o"}
+{"Key":"escape"}
+{"Key":"k"}
+{"Key":"g"}
+{"Key":"i"}
+{"Key":"u"}
+{"Key":"escape"}
+{"Get":{"state":"one two\nthree foˇur","mode":"Normal"}}