Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vim mode multi-key mappings #5337

Closed
cjfuller opened this issue Jun 29, 2022 · 36 comments
Closed

Vim mode multi-key mappings #5337

cjfuller opened this issue Jun 29, 2022 · 36 comments
Labels
enhancement [core label] vim

Comments

@cjfuller
Copy link

Is your feature request related to a problem? Please describe.
I normally remap keys in vim or vim-mode in other editors so that I can use jk in insert mode to exit insert mode instead of escape, but I don't see any way to do this from the docs, and it did not work to try to hack it with:

    {
        "context": "Editor && vim_mode == insert && vim_operator == j",
        "bindings": {
            "k": "vim::NormalBefore"
        }
    }

Describe the solution you'd like
I'd like to somehow be able to bind j then k to Escape in insert mode.

Describe alternatives you've considered
I tried the config hack above to no avail, and I also considered retraining to ctrl-c, which is the default alternate binding, but I'd rather be able to use something on the home row.

Mockup
For example, I'd love to be able to write the following:

    {
        "context": "Editor && vim_mode == insert",
        "bindings": {
            "jk": "vim::NormalBefore"
        }
    }
@cjfuller cjfuller added enhancement [core label] triage Maintainer needs to classify the issue labels Jun 29, 2022
@JosephTLyons JosephTLyons removed the triage Maintainer needs to classify the issue label Jun 29, 2022
@albertorestifo
Copy link

Vim mode without the ability to remap multi-key bindings is very limiting indeed.

Another use-case: In normal mode, I want to remap gd to the Go To Definition command.

@caliguIa
Copy link

caliguIa commented Mar 10, 2023

@albertorestifo you may have already figured this out but you can achieve go to definition already with the following snippet:

[
    {
        "context": "Editor && vim_operator == g",
        "bindings": {
            "d": "editor::GoToDefinition"
        }
    }
]

@mauscoelho
Copy link

The vim_operator == g but when trying to do something like vim_operator == [ it does not work, is it a special character? How would it translate?

Not working example:

  {
    "context": "Editor && vim_operator == [",
    "bindings": {
      "d": "editor::GoToPrevDiagnostic"      
    }
  }

ConradIrwin referenced this issue Jul 21, 2023
This previously enabled things like `d g g` to work, but we can
fix that instead by not clearing out pending vim state on change.

Either way, it is unnecessary and causes some user-confusion
(zed-industries/community#176), so remove this code for now; and use
comments to organize the file a bit instead.
@ConradIrwin
Copy link
Member

Multi-character key-bindings should mostly "just work" if you separate keys with a space in the key.

The main thing I know is broken is that you can't (in insert mode) map j k to escape, because the way the keybinding system currently works you can't then type a j. I do want to try and fix that.

You should not need to specify the first key in the context for the second. This is only required in the builtin vim.json because we want to support c <motion> and vim special cases c to be the "entire line" motion if the operator is c (or d if the operator is d, etc.).

That said, it is quite fiddly to get the context correct at the moment (which I'd like to work on) but in the meantime:

The main context for things is:

{[{
  "context": "Editor && VimControl && !VimWaiting && !menu"
  "bindings": [
   // put key-bindings here if you want them to work in normal & visual mode
   "c d": "editor::Change
  ]
}, {
  "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
   "bindings": [
    // put key-bindings here if you want them to work only in normal mode
   ]
}, {
  "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject"
  "bindings": [
   // visual, visual line & visual block modes
  ],
}, {
    "context": "Editor && vim_mode == insert",
    "bindings": [
     // insert mode *CAVEAT* about using printable characters above.
   ],
]}

Also please do open issues if there are common plugins you use that define keybindings you want (or just keybindings for vim stuff that we're missing) – ideally you don't have to make too many changes to the file!

@washanhanzi
Copy link

This worked for jk:

  {
    "context": "Editor && vim_mode == insert",
    "bindings": {
      "j k": "vim::NormalBefore"
    }
  }

but if you want to type j, it's jj.

@mauscoelho
Copy link

For reference this works for me:

{
    "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
    "bindings": {
      // put key-bindings here if you want them to work only in normal mode
      "g d": "editor::GoToDefinition",
      "g r": "editor::FindAllReferences",
      "[ d": "editor::GoToPrevDiagnostic",
      "] d": "editor::GoToDiagnostic"
    }
  }

@ConradIrwin
Copy link
Member

Nice! I like [ d and ] d. I recently added g A for "find all references" to the default vim keybindings (although I think it's unlikely that vim's g r support is coming anytime soon, I wanted to not overwrite any defaults).

@calebmeyer
Copy link

I added a bunch of keybinds, and they're all working (amazingly fast and well compared to vs code!). My only issue is what @washanhanzi said, I now have to double type js.

My new keybinds!
[
    {
        // normal & visual mode
        "context": "Editor && VimControl && !VimWaiting && !menu",
        "bindings": {}
    },
    {
        // normal mode
        "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
        "bindings": {
            "g d": "editor::GoToDefinition",
            "space space": "command_palette::Toggle",
            "space b d": "pane::CloseActiveItem",
            "space b n": "pane::ActivateNextItem",
            "space b p": "pane::ActivatePrevItem",
            "space c l": "editor::ToggleComments",
            "space f s": "workspace::Save",
            "space f S": "workspace::SaveAll",
            "space j f": "project_symbols::Toggle", // not quite right, since it's full project
            "space p s": "TODO", // go to project
            "space q a": "pane::CloseAllItems",
            "space q o": "pane::CloseInactiveItems",
            "space q w": "workspace::CloseWindow",
            "space q q": "zed::Quit",
            "space s d": "pane::SplitDown",
            "space s l": "pane::SplitLeft",
            "space s r": "pane::SplitRight",
            "space s u": "pane::SplitUp",
            "space t i": "editor::ToggleInlayHints",
            "space t n": "workspace::NewTerminal",
            "space t t": "terminal_panel::ToggleFocus",
            "space w d": [
                "workspace::ActivatePaneInDirection",
                "Down"
            ],
            "space w l": [
                "workspace::ActivatePaneInDirection",
                "Left"
            ],
            "space w r": [
                "workspace::ActivatePaneInDirection",
                "Right"
            ],
            "space w u": [
                "workspace::ActivatePaneInDirection",
                "Up"
            ],
            "space w w": "workspace::ActivateNextPane",
        }
    },
    {
        // visual, visual line & visual block modes
        "context": "Editor && vim_mode == visual && !VimWaiting && !VimObject",
        "bindings": {},
    },
    {
        // insert mode
        "context": "Editor && vim_mode == insert",
        "bindings": {
            "j k": "vim::NormalBefore"
        }
    }
]

@mauscoelho
Copy link

"space j f": "project_symbols::Toggle", // not quite right, since it's full project

You can use cmd-shift-O for buffer symbols @calebmeyer. I could not find the action name for it as well

@JosephTLyons
Copy link
Collaborator

JosephTLyons commented Sep 13, 2023

"space j f": "project_symbols::Toggle", // not quite right, since it's full project

You can use cmd-shift-O for buffer symbols @calebmeyer. I could not find the action name for it as well

We call buffer symbols an outline, so the action name for this would be outline: toggle.

@ConradIrwin
Copy link
Member

@calebmeyer thanks for sharing these!

I'm going to add outline::Toggle and project_symbols::Toggle to the default map. Vim doesn't use g s for anything useful ("sleep for count seconds??!") and doesn't use g S at all.

I've already begun repurposing the g namespace for zed specific stuff, so that would give us:

      "g d": "editor::GoToDefinition",  // in vim "go to local declaration" 
      "g shift-d": "editor::GoToTypeDefinition", // in vim "go to global declaration"
      "g .": "editor::ToggleCodeActions", // zed specific
      "g shift-a": "editor::FindAllReferences", // zed specific
      "g s": "outline::Toggle",  // zed specific
      "g shift-s": "project_symbols::Toggle",  // zed specific
      // ... 
      "c d": "editor::Rename", // zed specific

Also a quick note that in your file "space f S" needs to be spelt "space f shift-s" for zed's keyboard handler to understand it.

@justDeeevin
Copy link

In the preview version of Zed, doing something like this:

    {
        "context": "Editor && vim_mode == insert",
        "bindings": {
            "j k": "vim::NormalBefore",
            "k j": "vim::NormalBefore"
        }
    },

Actually ends up typing k or j in the file before executing the command. So I have to get rid of the extra letter every time I use the keybind. Not fun.

@d1y
Copy link
Contributor

d1y commented Jan 12, 2024

In the preview version of Zed, doing something like this:

    {
        "context": "Editor && vim_mode == insert",
        "bindings": {
            "j k": "vim::NormalBefore",
            "k j": "vim::NormalBefore"
        }
    },

Actually ends up typing k or j in the file before executing the command. So I have to get rid of the extra letter every time I use the keybind. Not fun.

Same problem, in zed-preview
zed-bug

@ConradIrwin
Copy link
Member

Thanks for the report on Preview. I'll at least revert to the gpui1 behavior, but I wonder if I can figure out how to teach the input handler about pending keys so we can have our cake and eat it to.

@ConradIrwin ConradIrwin mentioned this issue Jan 22, 2024
Merged
ConradIrwin referenced this issue Jan 22, 2024
Add support for mapping `jk` to escape in vim mode.

This changes the behaviour of the keymatches when there are pending
matches.

Before: Even if there was a pending match, any complete matches would be
triggered and the pending state lost.

After: If there is a pending match, any complete matches are delayed by
1s, or until more keys are typed.

Release Notes:

- Added support for mapping `jk` in vim mode
([#2378](https://github.com/zed-industries/community/issues/2378)),
([#176](https://github.com/zed-industries/community/issues/176))
@JosephTLyons
Copy link
Collaborator

This should be shipped in v0.119.16-pre.

@ConradIrwin
Copy link
Member

I've added support for jk in insert-mode in the latest nightly:

  • You'll need to set up bindings like:
[
  {
    "context": "Editor && vim_mode == insert",
    "bindings": {
      "j k": "vim::NormalBefore"
    }
  }
]
  • Once you have that, typing jk in insert mode will act like <esc>
  • If you type j and then wait 1000ms, it will type j
  • If you type j and then any other key, it will type the j just before the next key.

This works similarly in normal mode, for example if you have:

[
  {
    "bindings": {
      "shift-escape": null,
      "ctrl-enter": "menu::ShowContextMenu",
      ", w": "workspace::Save"
    }
  }
]
  • If you type ,w then it will save.
  • If you type , and then wait 1000ms it will do the default (repeat the most recent tf command in reverse).
  • If you type , and then another key, it will do the default just before the next key

With that done, I think we can close this out.

There is a related feature request to support mapping from one sequence of keys to another sequence of keys, but we should track that separately!

@JosephTLyons JosephTLyons transferred this issue from zed-industries/community Jan 24, 2024
@LORE-D-NATO
Copy link

I have kj set as my escape binding and it makes exiting insert mode after typing a k sort of awkward with the 1000ms delay. Shouldn't kk type a k and then wait for another input?

@ConradIrwin
Copy link
Member

@LORE-D-NATO If you just keep typing it should do the right thing, for example if you type koala without pausing after the k it should insert the k correctly. Please let me know if you're not observing that!

There does seem to be a small bug right now where if you type kk it doesn't type the first k until the timeout has passed either; I'll take a look at that.

@LORE-D-NATO
Copy link

@ConradIrwin Yes, typing a second letter other than k works as intended. The bug when typing kk is what I was referencing.

@jparr721
Copy link

jparr721 commented Jan 25, 2024

I didn't want to resurrect this for another individual issue, so let me know if this is the inappropriate place to put this, but I am having trouble getting certain chained functionality similar to above, specifically ciw or "change inner word". I grabbed a keymap a year or so ago and have been using that, but the one critical piece keeping me in regular vim/vscode is the ability to use the c commands as I expect, and most specifically ciw. Given the following config, I am trying to understand how I might implement this? I have been googling this issue for months to avoid adding yet-another one-off request, but I am truly stumped as to how to do this with the present functionality. Is there any way I can get this to work as I expect? You can see my half-hearted attempt to add the feature below, but it's definitely not right. If there's docs I'm happy to read those. I've been using the provided bindings, but I am not sure how I would replace the current word.

Something like this:

"c i w": "editor::DeleteToNextWordEnd",

Works great, but something like this:

      "c i w": [
        "editor::MoveToPreviousWordStart",
        "editor::DeleteToNextWordStart",
        "vim::SwitchMode",
        "Insert"
      ],

does not. Am I doing something horribly wrong here?

@ConradIrwin
Copy link
Member

@jparr721 There should be (but I don't think there is) an issue for what you're talking about, which is being able to bind one keystroke to many actions (or more keystrokes). It is in the backlog of the Vim channel notes https://zed.dev/channel/vim-393, but would be good to have tracked on Github too.

That said, we do already support things like ciw. If you're looking at the binding file, you can see that c pushes the "Change" operator: https://github.com/zed-industries/zed/blob/main/assets/keymaps/vim.json#L296

Then i pushes the around operator: https://github.com/zed-industries/zed/blob/main/assets/keymaps/vim.json#L459, and finally w selects the word object: https://github.com/zed-industries/zed/blob/main/assets/keymaps/vim.json#L376.

Its a little convoluted, but that way we don't have to have separate bindings for each action and object.

In order for any of this work you do have to use Zed in Vim mode "vim_mode": true in your settings in addition to having the keymap; as that enables all the bookkeeping machinery. (Also if you copied the keymap a year ago, I'd suggest deleting it and starting afresh!)

@jparr721
Copy link

The new keymaps worked like a charm! Thanks for the followup here.

@eneiford
Copy link

eneiford commented Feb 18, 2024

is it possible to override the 1000ms delay time waiting on a second keypress (especially for a specific context)? using jk is really awkward with the 1000ms delay when actually typing a "j" or "k". For example, i would normally escape from visual mode with jk, but i also need to be able to navigate lines inside visual mode, so I set the delay interval very small to enable jk escape while keeping the ability to navigate visual blocks

@ConradIrwin
Copy link
Member

@eneiford not right now, but if you’d like to pair with me on building it, feel free to book time at https://calendly.com/conradirwin/pairing.

We should just be able to add a new setting for this.

@mre
Copy link

mre commented Feb 19, 2024

@ConradIrwin, I have a similar (but slightly different) use-case for reducing the 1000ms delay time.
(Did not want to open a new issue right away.)

I normally use cmd-j and cmd-k to switch between panes.

"bindings": {
    "cmd-j": "pane::ActivatePrevItem",
    "cmd-k": "pane::ActivateNextItem",
}

While cmd-j works as expected, cmd-k is slow because it waits for a second key press. I think if I set the delay time to 0, it would be instant. Alternatively, is there a way to disable multi-key mappings and/or set all the other cmd-k X mappings to null?

My current workaround is

"bindings": {
    "cmd-[": "pane::ActivatePrevItem",
    "cmd-]": "pane::ActivateNextItem",
}

@ConradIrwin
Copy link
Member

There's no currently easy way to do this unfortunately.

A few things we could consider improving on the Zed side that might help:

  • Moving all the cmd-k X bindings to a top-level context so that user-bindings at a lower-level context would take priority
  • Have a way to give user-bindings precedence over internal bindings
  • Make the delay so much shorter that you don't care anymore

I think 3. should be easy, and is probably a good place to start; but 2. would be nice to have too. (1. seems further away from feasible)

@mre
Copy link

mre commented Feb 20, 2024

Yes, each of these solutions would work fine for me.
As this issue is closed, would you like me to create a new issue mentioning your points?

@ConradIrwin
Copy link
Member

Please do!

Or if you'd like to just fix them PRs are also welcome – if you'd like to work together you can book time here: https://calendly.com/conradirwin/pairing

@cyfyifanchen
Copy link

  {
    "context": "Editor && vim_mode == normal && !menu",
    "bindings": {
      "j": ["g j"],
      "k": ["g", "k"]
    }
  }

Anyone knows how to make the above mapping work? I've tried various combination, don't seem to work at all.

@ConradIrwin
Copy link
Member

@cyfyifanchen You either need to use the workspace::SendKeystrokes action to convert one set of keys to another: https://zed.dev/docs/key-bindings#remapping-keys

Or (for this specific case), you can use the target actions directly (

"g j": [
):

      "j": [
        "vim::Down",
        {
          "displayLines": true
        }
      ],
      "k": [
        "vim::Up",
        {
          "displayLines": true
        }
      ],

@cyfyifanchen
Copy link

@ConradIrwin beautiful!

@venkatb-zelar
Copy link

is it possible to save the file on leaving the insert mode?

Basically i want to leave the insert mode to Normal with "j k" and the file to be save automatically.

@ConradIrwin
Copy link
Member

@venkatb-zelar yes!

You could do something like this:

{
  "context": "vim_mode == insert",
  "bindings": {
    "j k": ["workspace::SendKeystrokes", "escape command-s"]
  }
}

You could also look into the Zed "autosave": "on_focus_change" setting, which I use to avoid having to think about saving too hard.

@nmsobri
Copy link

nmsobri commented Sep 11, 2024

I've added support for jk in insert-mode in the latest nightly:

  • You'll need to set up bindings like:
[
  {
    "context": "Editor && vim_mode == insert",
    "bindings": {
      "j k": "vim::NormalBefore"
    }
  }
]
  • Once you have that, typing jk in insert mode will act like <esc>
  • If you type j and then wait 1000ms, it will type j
  • If you type j and then any other key, it will type the j just before the next key.

This works similarly in normal mode, for example if you have:

[
  {
    "bindings": {
      "shift-escape": null,
      "ctrl-enter": "menu::ShowContextMenu",
      ", w": "workspace::Save"
    }
  }
]
  • If you type ,w then it will save.
  • If you type , and then wait 1000ms it will do the default (repeat the most recent tf command in reverse).
  • If you type , and then another key, it will do the default just before the next key

With that done, I think we can close this out.

There is a related feature request to support mapping from one sequence of keys to another sequence of keys, but we should track that separately!

still not a viable solution.. I mean, if you really want to type jk, you need to type j and wait for 1 sec to type k, or else Zed will interpret as escape to normal mode.

is there a way to reduce vim delay timeout? normally i set this value in .vimrc to 400ms

@tednaaa
Copy link

tednaaa commented Sep 16, 2024

any updates? in neovim I use 250 timeout, and default 1000 is too much for me

@ConradIrwin
Copy link
Member

If you want to work on this with me: https://calendly.com/conradirwin/pairing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement [core label] vim
Projects
None yet
Development

No branches or pull requests