mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 16:04:57 +03:00
vim cl (#11641)
Release Notes: - vim: Added support for the changelist. `g;` and `g,` to the previous/next change - vim: Added support for the `'.` mark - vim: Added support for `gi` to resume the previous insert
This commit is contained in:
parent
4f9ba28a25
commit
45f12b9426
@ -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);
|
||||
|
233
crates/vim/src/change_list.rs
Normal file
233
crates/vim/src/change_list.rs
Normal file
@ -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;
|
||||
|
16
crates/vim/test_data/test_change_list_delete.json
Normal file
16
crates/vim/test_data/test_change_list_delete.json
Normal file
@ -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"}}
|
32
crates/vim/test_data/test_change_list_insert.json
Normal file
32
crates/vim/test_data/test_change_list_insert.json
Normal file
@ -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":";"}
|
8
crates/vim/test_data/test_dot_mark.json
Normal file
8
crates/vim/test_data/test_dot_mark.json
Normal file
@ -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"}}
|
10
crates/vim/test_data/test_gi.json
Normal file
10
crates/vim/test_data/test_gi.json
Normal file
@ -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"}}
|
Loading…
Reference in New Issue
Block a user