Vim shortcuts (#2760)
Refactors some of the vim bindings to make the vim.json file less obtuse. Release Notes: - vim: add `;` and `,` to repeat last `{f,F,t,T}` - vim: add zed-specific shortcuts for common IDE actions: - - `g A` to find all references - - `g .` to open the code actions menu. - - `c d` for rename
This commit is contained in:
commit
56c657fe79
6 changed files with 135 additions and 85 deletions
|
@ -2,12 +2,6 @@
|
||||||
{
|
{
|
||||||
"context": "Editor && VimControl && !VimWaiting && !menu",
|
"context": "Editor && VimControl && !VimWaiting && !menu",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"g": [
|
|
||||||
"vim::PushOperator",
|
|
||||||
{
|
|
||||||
"Namespace": "G"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"i": [
|
"i": [
|
||||||
"vim::PushOperator",
|
"vim::PushOperator",
|
||||||
{
|
{
|
||||||
|
@ -110,6 +104,32 @@
|
||||||
"*": "vim::MoveToNext",
|
"*": "vim::MoveToNext",
|
||||||
"#": "vim::MoveToPrev",
|
"#": "vim::MoveToPrev",
|
||||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||||
|
// "g" commands
|
||||||
|
"g g": "vim::StartOfDocument",
|
||||||
|
"g h": "editor::Hover",
|
||||||
|
"g t": "pane::ActivateNextItem",
|
||||||
|
"g shift-t": "pane::ActivatePrevItem",
|
||||||
|
"g d": "editor::GoToDefinition",
|
||||||
|
"g shift-d": "editor::GoToTypeDefinition",
|
||||||
|
"g .": "editor::ToggleCodeActions", // zed specific
|
||||||
|
"g shift-a": "editor::FindAllReferences", // zed specific
|
||||||
|
"g *": [
|
||||||
|
"vim::MoveToNext",
|
||||||
|
{
|
||||||
|
"partialWord": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"g #": [
|
||||||
|
"vim::MoveToPrev",
|
||||||
|
{
|
||||||
|
"partialWord": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// z commands
|
||||||
|
"z t": "editor::ScrollCursorTop",
|
||||||
|
"z z": "editor::ScrollCursorCenter",
|
||||||
|
"z b": "editor::ScrollCursorBottom",
|
||||||
|
// Count support
|
||||||
"1": [
|
"1": [
|
||||||
"vim::Number",
|
"vim::Number",
|
||||||
1
|
1
|
||||||
|
@ -234,12 +254,6 @@
|
||||||
"vim::PushOperator",
|
"vim::PushOperator",
|
||||||
"Yank"
|
"Yank"
|
||||||
],
|
],
|
||||||
"z": [
|
|
||||||
"vim::PushOperator",
|
|
||||||
{
|
|
||||||
"Namespace": "Z"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"i": [
|
"i": [
|
||||||
"vim::SwitchMode",
|
"vim::SwitchMode",
|
||||||
"Insert"
|
"Insert"
|
||||||
|
@ -278,6 +292,13 @@
|
||||||
"backwards": true
|
"backwards": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
";": "vim::RepeatFind",
|
||||||
|
",": [
|
||||||
|
"vim::RepeatFind",
|
||||||
|
{
|
||||||
|
"backwards": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"ctrl-f": "vim::PageDown",
|
"ctrl-f": "vim::PageDown",
|
||||||
"pagedown": "vim::PageDown",
|
"pagedown": "vim::PageDown",
|
||||||
"ctrl-b": "vim::PageUp",
|
"ctrl-b": "vim::PageUp",
|
||||||
|
@ -306,33 +327,11 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "Editor && vim_operator == g",
|
|
||||||
"bindings": {
|
|
||||||
"g": "vim::StartOfDocument",
|
|
||||||
"h": "editor::Hover",
|
|
||||||
"t": "pane::ActivateNextItem",
|
|
||||||
"shift-t": "pane::ActivatePrevItem",
|
|
||||||
"d": "editor::GoToDefinition",
|
|
||||||
"shift-d": "editor::GoToTypeDefinition",
|
|
||||||
"*": [
|
|
||||||
"vim::MoveToNext",
|
|
||||||
{
|
|
||||||
"partialWord": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"#": [
|
|
||||||
"vim::MoveToPrev",
|
|
||||||
{
|
|
||||||
"partialWord": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_operator == c",
|
"context": "Editor && vim_operator == c",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"c": "vim::CurrentLine"
|
"c": "vim::CurrentLine",
|
||||||
|
"d": "editor::Rename" // zed specific
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -347,14 +346,6 @@
|
||||||
"y": "vim::CurrentLine"
|
"y": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"context": "Editor && vim_operator == z",
|
|
||||||
"bindings": {
|
|
||||||
"t": "editor::ScrollCursorTop",
|
|
||||||
"z": "editor::ScrollCursorCenter",
|
|
||||||
"b": "editor::ScrollCursorBottom"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"context": "Editor && VimObject",
|
"context": "Editor && VimObject",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -62,6 +62,12 @@ struct PreviousWordStart {
|
||||||
ignore_punctuation: bool,
|
ignore_punctuation: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
struct RepeatFind {
|
||||||
|
#[serde(default)]
|
||||||
|
backwards: bool,
|
||||||
|
}
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
vim,
|
vim,
|
||||||
[
|
[
|
||||||
|
@ -82,7 +88,10 @@ actions!(
|
||||||
NextLineStart,
|
NextLineStart,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
|
impl_actions!(
|
||||||
|
vim,
|
||||||
|
[NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind]
|
||||||
|
);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
|
||||||
|
@ -123,13 +132,15 @@ pub fn init(cx: &mut AppContext) {
|
||||||
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
|
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
|
||||||
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
||||||
);
|
);
|
||||||
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx))
|
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
|
||||||
|
cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
|
||||||
|
repeat_motion(action.backwards, cx)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
|
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
|
||||||
if let Some(Operator::Namespace(_))
|
if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
|
||||||
| Some(Operator::FindForward { .. })
|
Vim::read(cx).active_operator()
|
||||||
| Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
|
|
||||||
{
|
{
|
||||||
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
|
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
|
||||||
}
|
}
|
||||||
|
@ -146,6 +157,35 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
|
||||||
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
|
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
|
||||||
|
let find = match Vim::read(cx).state.last_find.clone() {
|
||||||
|
Some(Motion::FindForward { before, text }) => {
|
||||||
|
if backwards {
|
||||||
|
Motion::FindBackward {
|
||||||
|
after: before,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Motion::FindForward { before, text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Motion::FindBackward { after, text }) => {
|
||||||
|
if backwards {
|
||||||
|
Motion::FindForward {
|
||||||
|
before: after,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Motion::FindBackward { after, text }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
motion(find, cx)
|
||||||
|
}
|
||||||
|
|
||||||
// Motion handling is specified here:
|
// Motion handling is specified here:
|
||||||
// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
|
// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
|
||||||
impl Motion {
|
impl Motion {
|
||||||
|
@ -743,4 +783,23 @@ mod test {
|
||||||
cx.simulate_shared_keystrokes(["%"]).await;
|
cx.simulate_shared_keystrokes(["%"]).await;
|
||||||
cx.assert_shared_state("func boop(ˇ) {\n}").await;
|
cx.assert_shared_state("func boop(ˇ) {\n}").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.set_shared_state("ˇone two three four").await;
|
||||||
|
cx.simulate_shared_keystrokes(["f", "o"]).await;
|
||||||
|
cx.assert_shared_state("one twˇo three four").await;
|
||||||
|
cx.simulate_shared_keystrokes([","]).await;
|
||||||
|
cx.assert_shared_state("ˇone two three four").await;
|
||||||
|
cx.simulate_shared_keystrokes(["2", ";"]).await;
|
||||||
|
cx.assert_shared_state("one two three fˇour").await;
|
||||||
|
cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
|
||||||
|
cx.assert_shared_state("one two threeˇ four").await;
|
||||||
|
cx.simulate_shared_keystrokes(["3", ";"]).await;
|
||||||
|
cx.assert_shared_state("oneˇ two three four").await;
|
||||||
|
cx.simulate_shared_keystrokes([","]).await;
|
||||||
|
cx.assert_shared_state("one two thˇree four").await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ pub fn normal_motion(
|
||||||
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
|
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
|
||||||
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
|
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
|
||||||
Some(operator) => {
|
Some(operator) => {
|
||||||
// Can't do anything for text objects or namespace operators. Ignoring
|
// Can't do anything for text objects, Ignoring
|
||||||
error!("Unexpected normal mode motion operator: {:?}", operator)
|
error!("Unexpected normal mode motion operator: {:?}", operator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -441,11 +441,8 @@ mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
state::{
|
state::Mode::{self},
|
||||||
Mode::{self, *},
|
test::{ExemptionFeatures, NeovimBackedTestContext},
|
||||||
Namespace, Operator,
|
|
||||||
},
|
|
||||||
test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -610,22 +607,6 @@ mod test {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
|
|
||||||
let mut cx = VimTestContext::new(cx, true).await;
|
|
||||||
|
|
||||||
// Can abort with escape to get back to normal mode
|
|
||||||
cx.simulate_keystroke("g");
|
|
||||||
assert_eq!(cx.mode(), Normal);
|
|
||||||
assert_eq!(
|
|
||||||
cx.active_operator(),
|
|
||||||
Some(Operator::Namespace(Namespace::G))
|
|
||||||
);
|
|
||||||
cx.simulate_keystroke("escape");
|
|
||||||
assert_eq!(cx.mode(), Normal);
|
|
||||||
assert_eq!(cx.active_operator(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_gg(cx: &mut gpui::TestAppContext) {
|
async fn test_gg(cx: &mut gpui::TestAppContext) {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
|
@ -3,6 +3,8 @@ use language::CursorShape;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use workspace::searchable::Direction;
|
use workspace::searchable::Direction;
|
||||||
|
|
||||||
|
use crate::motion::Motion;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
Normal,
|
Normal,
|
||||||
|
@ -16,16 +18,9 @@ impl Default for Mode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
|
||||||
pub enum Namespace {
|
|
||||||
G,
|
|
||||||
Z,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||||
pub enum Operator {
|
pub enum Operator {
|
||||||
Number(usize),
|
Number(usize),
|
||||||
Namespace(Namespace),
|
|
||||||
Change,
|
Change,
|
||||||
Delete,
|
Delete,
|
||||||
Yank,
|
Yank,
|
||||||
|
@ -40,6 +35,8 @@ pub struct VimState {
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
pub operator_stack: Vec<Operator>,
|
pub operator_stack: Vec<Operator>,
|
||||||
pub search: SearchState,
|
pub search: SearchState,
|
||||||
|
|
||||||
|
pub last_find: Option<Motion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SearchState {
|
pub struct SearchState {
|
||||||
|
@ -126,8 +123,6 @@ impl Operator {
|
||||||
pub fn id(&self) -> &'static str {
|
pub fn id(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Operator::Number(_) => "n",
|
Operator::Number(_) => "n",
|
||||||
Operator::Namespace(Namespace::G) => "g",
|
|
||||||
Operator::Namespace(Namespace::Z) => "z",
|
|
||||||
Operator::Object { around: false } => "i",
|
Operator::Object { around: false } => "i",
|
||||||
Operator::Object { around: true } => "a",
|
Operator::Object { around: true } => "a",
|
||||||
Operator::Change => "c",
|
Operator::Change => "c",
|
||||||
|
|
|
@ -14,8 +14,8 @@ use anyhow::Result;
|
||||||
use collections::CommandPaletteFilter;
|
use collections::CommandPaletteFilter;
|
||||||
use editor::{Bias, Editor, EditorMode, Event};
|
use editor::{Bias, Editor, EditorMode, Event};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, impl_actions, keymap_matcher::KeymapContext, AppContext, Subscription, ViewContext,
|
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
|
||||||
ViewHandle, WeakViewHandle, WindowContext,
|
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||||
};
|
};
|
||||||
use language::CursorShape;
|
use language::CursorShape;
|
||||||
use motion::Motion;
|
use motion::Motion;
|
||||||
|
@ -90,7 +90,10 @@ pub fn init(cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn observe_keystrokes(cx: &mut WindowContext) {
|
pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||||
cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| {
|
cx.observe_keystrokes(|_keystroke, result, handled_by, cx| {
|
||||||
|
if result == &MatchResult::Pending {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if let Some(handled_by) = handled_by {
|
if let Some(handled_by) = handled_by {
|
||||||
// Keystroke is handled by the vim system, so continue forward
|
// Keystroke is handled by the vim system, so continue forward
|
||||||
if handled_by.namespace() == "vim" {
|
if handled_by.namespace() == "vim" {
|
||||||
|
@ -243,10 +246,14 @@ impl Vim {
|
||||||
|
|
||||||
match Vim::read(cx).active_operator() {
|
match Vim::read(cx).active_operator() {
|
||||||
Some(Operator::FindForward { before }) => {
|
Some(Operator::FindForward { before }) => {
|
||||||
motion::motion(Motion::FindForward { before, text }, cx)
|
let find = Motion::FindForward { before, text };
|
||||||
|
Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
|
||||||
|
motion::motion(find, cx)
|
||||||
}
|
}
|
||||||
Some(Operator::FindBackward { after }) => {
|
Some(Operator::FindBackward { after }) => {
|
||||||
motion::motion(Motion::FindBackward { after, text }, cx)
|
let find = Motion::FindBackward { after, text };
|
||||||
|
Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
|
||||||
|
motion::motion(find, cx)
|
||||||
}
|
}
|
||||||
Some(Operator::Replace) => match Vim::read(cx).state.mode {
|
Some(Operator::Replace) => match Vim::read(cx).state.mode {
|
||||||
Mode::Normal => normal_replace(text, cx),
|
Mode::Normal => normal_replace(text, cx),
|
||||||
|
|
17
crates/vim/test_data/test_comma_semicolon.json
Normal file
17
crates/vim/test_data/test_comma_semicolon.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{"Put":{"state":"ˇone two three four"}}
|
||||||
|
{"Key":"f"}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Get":{"state":"one twˇo three four","mode":"Normal"}}
|
||||||
|
{"Key":","}
|
||||||
|
{"Get":{"state":"ˇone two three four","mode":"Normal"}}
|
||||||
|
{"Key":"2"}
|
||||||
|
{"Key":";"}
|
||||||
|
{"Get":{"state":"one two three fˇour","mode":"Normal"}}
|
||||||
|
{"Key":"shift-t"}
|
||||||
|
{"Key":"e"}
|
||||||
|
{"Get":{"state":"one two threeˇ four","mode":"Normal"}}
|
||||||
|
{"Key":"3"}
|
||||||
|
{"Key":";"}
|
||||||
|
{"Get":{"state":"oneˇ two three four","mode":"Normal"}}
|
||||||
|
{"Key":","}
|
||||||
|
{"Get":{"state":"one two thˇree four","mode":"Normal"}}
|
Loading…
Add table
Add a link
Reference in a new issue