mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
vim gigv (#13028)
Release Notes: - vim: Fix `gi` when the insert ended at the end of a line (#12162) - vim: Add `gv` to restore previous visual selection (#12888) - vim: Fix `gl` when the first match is at the end of a line
This commit is contained in:
parent
3539a7c04a
commit
3b84b106e2
@ -246,9 +246,10 @@
|
||||
"displayLines": true
|
||||
}
|
||||
],
|
||||
"g v": "vim::RestoreVisualSelection",
|
||||
"g ]": "editor::GoToDiagnostic",
|
||||
"g [": "editor::GoToPrevDiagnostic",
|
||||
"g i": ["workspace::SendKeystrokes", "` ^ i"],
|
||||
"g i": "vim::InsertAtPrevious",
|
||||
"g ,": "vim::ChangeListNewer",
|
||||
"g ;": "vim::ChangeListOlder",
|
||||
"shift-h": "vim::WindowTop",
|
||||
|
@ -450,6 +450,7 @@ impl PromptLibrary {
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Box::new(
|
||||
SlashCommandCompletionProvider::new(commands, None, None),
|
||||
|
@ -1,4 +1,8 @@
|
||||
use crate::{normal::repeat, state::Mode, Vim};
|
||||
use crate::{
|
||||
normal::{mark::create_mark, repeat},
|
||||
state::Mode,
|
||||
Vim,
|
||||
};
|
||||
use editor::{scroll::Autoscroll, Bias};
|
||||
use gpui::{actions, Action, ViewContext};
|
||||
use language::SelectionGoal;
|
||||
@ -15,6 +19,7 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
vim.stop_recording_immediately(action.boxed_clone());
|
||||
if count <= 1 || vim.workspace_state.replaying {
|
||||
create_mark(vim, "^".into(), false, cx);
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.dismiss_menus_and_popups(false, cx);
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
|
@ -51,6 +51,7 @@ actions!(
|
||||
InsertEndOfLine,
|
||||
InsertLineAbove,
|
||||
InsertLineBelow,
|
||||
InsertAtPrevious,
|
||||
DeleteLeft,
|
||||
DeleteRight,
|
||||
ChangeToEndOfLine,
|
||||
@ -73,6 +74,7 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace
|
||||
workspace.register_action(insert_end_of_line);
|
||||
workspace.register_action(insert_line_above);
|
||||
workspace.register_action(insert_line_below);
|
||||
workspace.register_action(insert_at_previous);
|
||||
workspace.register_action(change_case);
|
||||
workspace.register_action(convert_to_upper_case);
|
||||
workspace.register_action(convert_to_lower_case);
|
||||
@ -341,6 +343,20 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_at_previous(_: &mut Workspace, _: &InsertAtPrevious, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
if let Some(marks) = vim.state().marks.get("^") {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
|
@ -11,6 +11,7 @@ use language::SelectionGoal;
|
||||
|
||||
use crate::{
|
||||
motion::{self, Motion},
|
||||
state::Mode,
|
||||
Vim,
|
||||
};
|
||||
|
||||
@ -29,41 +30,32 @@ pub fn create_mark(vim: &mut Vim, text: Arc<str>, tail: bool, cx: &mut WindowCon
|
||||
vim.clear_operator(cx);
|
||||
}
|
||||
|
||||
pub fn create_mark_after(vim: &mut Vim, text: Arc<str>, cx: &mut WindowContext) {
|
||||
let Some(anchors) = vim.update_active_editor(cx, |_, editor, cx| {
|
||||
pub fn create_visual_marks(vim: &mut Vim, mode: Mode, cx: &mut WindowContext) {
|
||||
let mut starts = vec![];
|
||||
let mut ends = vec![];
|
||||
let mut reversed = vec![];
|
||||
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
let (map, selections) = editor.selections.all_display(cx);
|
||||
selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let point = movement::saturating_right(&map, selection.tail());
|
||||
for selection in selections {
|
||||
let end = movement::saturating_left(&map, selection.end);
|
||||
ends.push(
|
||||
map.buffer_snapshot
|
||||
.anchor_before(point.to_offset(&map, Bias::Left))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
vim.update_state(|state| state.marks.insert(text.to_string(), anchors));
|
||||
vim.clear_operator(cx);
|
||||
}
|
||||
|
||||
pub fn create_mark_before(vim: &mut Vim, text: Arc<str>, cx: &mut WindowContext) {
|
||||
let Some(anchors) = vim.update_active_editor(cx, |_, editor, cx| {
|
||||
let (map, selections) = editor.selections.all_display(cx);
|
||||
selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
let point = movement::saturating_left(&map, selection.head());
|
||||
.anchor_before(end.to_offset(&map, Bias::Left)),
|
||||
);
|
||||
starts.push(
|
||||
map.buffer_snapshot
|
||||
.anchor_before(point.to_offset(&map, Bias::Left))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
.anchor_after(selection.start.to_offset(&map, Bias::Right)),
|
||||
);
|
||||
reversed.push(selection.reversed)
|
||||
}
|
||||
});
|
||||
|
||||
vim.update_state(|state| state.marks.insert(text.to_string(), anchors));
|
||||
vim.update_state(|state| {
|
||||
state.marks.insert("<".to_string(), starts);
|
||||
state.marks.insert(">".to_string(), ends);
|
||||
state.stored_visual_mode.replace((mode, reversed));
|
||||
});
|
||||
vim.clear_operator(cx);
|
||||
}
|
||||
|
||||
|
@ -84,6 +84,7 @@ pub struct EditorState {
|
||||
pub replacements: Vec<(Range<editor::Anchor>, String)>,
|
||||
|
||||
pub marks: HashMap<String, Vec<Anchor>>,
|
||||
pub stored_visual_mode: Option<(Mode, Vec<bool>)>,
|
||||
pub change_list: Vec<Vec<Anchor>>,
|
||||
pub change_list_position: Option<usize>,
|
||||
|
||||
|
@ -1127,6 +1127,26 @@ async fn test_lt_gt_marks(cx: &mut TestAppContext) {
|
||||
Line five
|
||||
"
|
||||
});
|
||||
|
||||
cx.simulate_shared_keystrokes("v i w o escape").await;
|
||||
cx.simulate_shared_keystrokes("` >").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
Line one
|
||||
Line two
|
||||
Line three
|
||||
Line fouˇr
|
||||
Line five
|
||||
"
|
||||
});
|
||||
cx.simulate_shared_keystrokes("` <").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
Line one
|
||||
Line two
|
||||
Line three
|
||||
Line ˇfour
|
||||
Line five
|
||||
"
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@ -1166,4 +1186,14 @@ async fn test_caret_mark(cx: &mut TestAppContext) {
|
||||
Line five
|
||||
"
|
||||
});
|
||||
|
||||
cx.simulate_shared_keystrokes("k a ! escape k g i ?").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {"
|
||||
Line one
|
||||
Line two
|
||||
Line three!?ˇ
|
||||
Straight thing four
|
||||
Line five
|
||||
"
|
||||
});
|
||||
}
|
||||
|
@ -31,10 +31,7 @@ use gpui::{
|
||||
use language::{CursorShape, Point, SelectionGoal, TransactionId};
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
use motion::Motion;
|
||||
use normal::{
|
||||
mark::{create_mark, create_mark_after, create_mark_before},
|
||||
normal_replace,
|
||||
};
|
||||
use normal::{mark::create_visual_marks, normal_replace};
|
||||
use replace::multi_replace;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
@ -431,8 +428,8 @@ impl Vim {
|
||||
// Sync editor settings like clip mode
|
||||
self.sync_vim_settings(cx);
|
||||
|
||||
if mode != Mode::Insert && last_mode == Mode::Insert {
|
||||
create_mark_after(self, "^".into(), cx)
|
||||
if !mode.is_visual() && last_mode.is_visual() {
|
||||
create_visual_marks(self, last_mode, cx);
|
||||
}
|
||||
|
||||
if leave_selections {
|
||||
@ -790,7 +787,6 @@ impl Vim {
|
||||
let is_multicursor = editor.read(cx).selections.count() > 1;
|
||||
|
||||
let state = self.state();
|
||||
let mut is_visual = state.mode.is_visual();
|
||||
if state.mode == Mode::Insert && state.current_tx.is_some() {
|
||||
if state.current_anchor.is_none() {
|
||||
self.update_state(|state| state.current_anchor = Some(newest));
|
||||
@ -807,18 +803,11 @@ impl Vim {
|
||||
} else {
|
||||
self.switch_mode(Mode::Visual, false, cx)
|
||||
}
|
||||
is_visual = true;
|
||||
} else if newest.start == newest.end
|
||||
&& !is_multicursor
|
||||
&& [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&state.mode)
|
||||
{
|
||||
self.switch_mode(Mode::Normal, true, cx);
|
||||
is_visual = false;
|
||||
}
|
||||
|
||||
if is_visual {
|
||||
create_mark_before(self, ">".into(), cx);
|
||||
create_mark(self, "<".into(), true, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ use editor::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
movement,
|
||||
scroll::Autoscroll,
|
||||
Bias, DisplayPoint, Editor,
|
||||
Bias, DisplayPoint, Editor, ToOffset,
|
||||
};
|
||||
use gpui::{actions, ViewContext, WindowContext};
|
||||
use language::{Point, Selection, SelectionGoal};
|
||||
@ -16,8 +16,8 @@ use workspace::{searchable::Direction, Workspace};
|
||||
|
||||
use crate::{
|
||||
motion::{start_of_line, Motion},
|
||||
normal::substitute::substitute,
|
||||
normal::yank::{copy_selections_content, yank_selections_content},
|
||||
normal::{mark::create_visual_marks, substitute::substitute},
|
||||
object::Object,
|
||||
state::{Mode, Operator},
|
||||
Vim,
|
||||
@ -37,6 +37,7 @@ actions!(
|
||||
SelectPrevious,
|
||||
SelectNextMatch,
|
||||
SelectPreviousMatch,
|
||||
RestoreVisualSelection,
|
||||
]
|
||||
);
|
||||
|
||||
@ -83,6 +84,52 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
select_match(workspace, vim, Direction::Prev, cx);
|
||||
});
|
||||
});
|
||||
|
||||
workspace.register_action(|_, _: &RestoreVisualSelection, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let Some((stored_mode, reversed)) =
|
||||
vim.update_state(|state| state.stored_visual_mode.take())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some((start, end)) = vim.state().marks.get("<").zip(vim.state().marks.get(">"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let ranges = start
|
||||
.into_iter()
|
||||
.zip(end)
|
||||
.zip(reversed)
|
||||
.map(|((start, end), reversed)| (*start, *end, reversed))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if vim.state().mode.is_visual() {
|
||||
create_visual_marks(vim, vim.state().mode, cx);
|
||||
}
|
||||
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let map = s.display_map();
|
||||
let ranges = ranges
|
||||
.into_iter()
|
||||
.map(|(start, end, reversed)| {
|
||||
let new_end =
|
||||
movement::saturating_right(&map, end.to_display_point(&map));
|
||||
Selection {
|
||||
id: s.new_selection_id(),
|
||||
start: start.to_offset(&map.buffer_snapshot),
|
||||
end: new_end.to_offset(&map, Bias::Left),
|
||||
reversed,
|
||||
goal: SelectionGoal::None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
s.select(ranges);
|
||||
})
|
||||
});
|
||||
vim.switch_mode(stored_mode, true, cx)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
@ -483,6 +530,7 @@ pub fn select_next(_: &mut Workspace, _: &SelectNext, cx: &mut ViewContext<Works
|
||||
vim.take_count(cx)
|
||||
.unwrap_or_else(|| if vim.state().mode.is_visual() { 1 } else { 2 });
|
||||
vim.update_active_editor(cx, |_, editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
for _ in 0..count {
|
||||
if editor
|
||||
.select_next(&Default::default(), cx)
|
||||
@ -1166,6 +1214,17 @@ mod test {
|
||||
cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_gl(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("aaˇ aa\naa", Mode::Normal);
|
||||
cx.simulate_keystrokes("g l");
|
||||
cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual);
|
||||
cx.simulate_keystrokes("g >");
|
||||
cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
@ -1247,4 +1306,56 @@ mod test {
|
||||
"
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_gv(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {
|
||||
"The ˇquick brown"
|
||||
})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes("v i w escape g v").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"The «quickˇ» brown"
|
||||
});
|
||||
|
||||
cx.simulate_shared_keystrokes("o escape g v").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"The «ˇquick» brown"
|
||||
});
|
||||
|
||||
cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"«Thˇ»e quick brown"
|
||||
});
|
||||
cx.simulate_shared_keystrokes("g v").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"The «ˇquick» brown"
|
||||
});
|
||||
cx.simulate_shared_keystrokes("g v").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"«Thˇ»e quick brown"
|
||||
});
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
fiˇsh one
|
||||
fish two
|
||||
fish red
|
||||
fish blue
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("4 g l escape escape g v");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«fishˇ» one
|
||||
«fishˇ» two
|
||||
«fishˇ» red
|
||||
«fishˇ» blue
|
||||
"},
|
||||
Mode::Visual,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -24,3 +24,12 @@
|
||||
{"Key":"`"}
|
||||
{"Key":"^"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three\nStraight thingˇ four\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"k"}
|
||||
{"Key":"a"}
|
||||
{"Key":"!"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"k"}
|
||||
{"Key":"g"}
|
||||
{"Key":"i"}
|
||||
{"Key":"?"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three!?ˇ\nStraight thing four\nLine five\n","mode":"Insert"}}
|
||||
|
24
crates/vim/test_data/test_gv.json
Normal file
24
crates/vim/test_data/test_gv.json
Normal file
@ -0,0 +1,24 @@
|
||||
{"Put":{"state":"The ˇquick brown"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"g"}
|
||||
{"Key":"v"}
|
||||
{"Get":{"state":"The «quickˇ» brown","mode":"Visual"}}
|
||||
{"Key":"o"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"g"}
|
||||
{"Key":"v"}
|
||||
{"Get":{"state":"The «ˇquick» brown","mode":"Visual"}}
|
||||
{"Key":"escape"}
|
||||
{"Key":"^"}
|
||||
{"Key":"ctrl-v"}
|
||||
{"Key":"l"}
|
||||
{"Get":{"state":"«Thˇ»e quick brown","mode":"VisualBlock"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"v"}
|
||||
{"Get":{"state":"The «ˇquick» brown","mode":"Visual"}}
|
||||
{"Key":"g"}
|
||||
{"Key":"v"}
|
||||
{"Get":{"state":"«Thˇ»e quick brown","mode":"VisualBlock"}}
|
@ -16,3 +16,14 @@
|
||||
{"Key":"`"}
|
||||
{"Key":">"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"v"}
|
||||
{"Key":"i"}
|
||||
{"Key":"w"}
|
||||
{"Key":"o"}
|
||||
{"Key":"escape"}
|
||||
{"Key":"`"}
|
||||
{"Key":">"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three\nLine fouˇr\nLine five\n","mode":"Normal"}}
|
||||
{"Key":"`"}
|
||||
{"Key":"<"}
|
||||
{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}}
|
||||
|
Loading…
Reference in New Issue
Block a user