While mapping jk/jj to esc, typing j and a motion hotkey make the j appear after the motion.
Describe the bug
I have imapped jk to <ESC>, and have some hotkeys like ctrl+a mapped to command cursorHome, ctrl+b to cursorLeft.
Press j and then ctrl+a, the j appears at the start of the line.
That is, the j changes its original location to the location after motion hotkey.
IIRC I don't remember such problem exists before.
To Reproduce
- add the following lines to user settings.
"vim.insertModeKeyBindingsNonRecursive": [
{
"before": ["j", "k"],
"after": ["<esc>"]
}
],
add such hotkeys to keybindings.json
{
"key": "ctrl+e",
"command": "cursorEnd",
"when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Insert'"
},
{
"key": "ctrl+a",
"command": "cursorHome",
"when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Insert'"
},
{
"key": "ctrl+p",
"command": "cursorUp",
"when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Insert'"
},
{
"key": "ctrl+n",
"command": "cursorDown",
"when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Insert'"
},
- Say we have a line
println($)($ is where the cursor now at) press j and it becomesprintln(j$)and waiting for a key. Then pressctrl+eto move to end of the line. The line becomesprintln()j$The j changes its location. Other such hotkeys(ctrl+a,ctrl+p, etc.) all behave like this.
Expected behavior
The line becomes println(j)$ ($ is where the cursor now at) but not println()j$.
I remember I used to play around with the variable j without any problem.
Environment (please complete the following information):
- Extension (VsCodeVim) version: 1.17.1
- VSCode version: 1.49.2
- OS: Version 2004 (OS Build 19041.264)
If this matters to you and affect your every day work flow, just downgrade to 1.16.0 from 1.17.1. They are doing a refactor and introducing some breaking changes, just wait for them to release a stable refactored version, stay in 1.17.1 if you want to help them debug their code.
Whats happening here is the following:
Since you have 'jk' mapped in insert mode, when you press 'j' in insert mode it will see that it is a potential remap and will not actually insert the 'j' waiting for another key to be pressed or timeout to finish. (this timeout is 1s by default and can be changed in settings).
For visual indication of that potential remap a "virtual" 'j' will be shown overlapping the character in front of the cursor, until a key is pressed or timeout finishes. If a key is pressed (that is not 'k') or if timeout finishes the 'j' will then be inserted on the cursor position. If instead the 'k' is pressed it doesn't insert anything and goes back to normal mode and that visual indication disappears.
The problem here is that you have those keybindings in vscode that change the cursor using a command, that means that no key is sent to Vim so there is no way to know that you pressed another key. So timeout will finish and 'j' will be inserted on the cursor position (that has since been changed).
To fix this you have a few options.
Option 1. - Wait for timeout to finish before pressing those keys:
You can wait for timeout to finish before pressing any of those keys (<C-e>, <C-a>, <C-p> or <C-n>). If you think 1 second is too much you can lower the timeout in settings to something lower like 200ms.
Option 2. - Move those bindings to VSCodeVim:
You can make those bindings directly in Vim, just make sure you have "vim.useCtrlKeys": true (it is true by default) and that you don't have any of those keys set to false on "vim.handleKeys".
Then you can use these bindings in 'settings.json':
"vim.insertModeKeyBindingsNonRecursive": [
{
"before": ["<C-e>"],
"commands": ["cursorEnd"]
},
{
"before": ["<C-a>"],
"commands": ["cursorHome"]
},
{
"before": ["<C-p>"],
"commands": ["cursorUp"]
},
{
"before": ["<C-n>"],
"commands": ["cursorDown"]
},
]
Note: I've used 'NonRecursive' here but it is not an obligation. I personally prefer to always use non recursive remaps unless I need it to be recursive
Option 3. - If you don't want your Ctrl keys to be handled by VSCodeVim:
If for some reason you don't want to or can't have your Ctrl keys being handled by VSCodeVim then you can use the vim.remap command. Although in this case you can't map the keys to those commands directly, otherwise we would have the same issue. In this case you would need to map those keys to some other keys in VSCodeVim that perform the same action. Like this for example in 'keybindings.json':
{
"key": "ctrl+e",
"command": "vim.remap",
"when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Insert'",
"args": {
"after": ["<C-o>", "A"]
}
},
{
"key": "ctrl+a",
"command": "vim.remap",
"when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Insert'",
"args": {
"after": ["<C-o>", "I"]
}
},
{
"key": "ctrl+p",
"command": "vim.remap",
"when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Insert'",
"args": {
"after": ["<C-o>", "k"]
}
},
{
"key": "ctrl+n",
"command": "vim.remap",
"when": "editorTextFocus && vim.active && !inDebugRepl && vim.mode == 'Insert'",
"args": {
"after": ["<C-o>", "j"]
}
},
You can choose any one of this options. Also you can use the bindings in option 2., but instead of mapping to the commands directly you could also map them to the "after:" ["<C-o>", "A"] like used in option 3.
Important:
Options 2 and 3 are currently not working due to a bug I just found out when writing this comment. I've already pushed a PR #5280 to fix that. So as soon as that fix is merged you can then actually choose any one of this options. For now you'll have to go with option 1.
I think choosing jk or jj for <ESC> should work well seamlessly, I used not to need to wait for the timeout, I now switched to 1.16.0, it works well, really well.
The j is inserted as-is, only if I then press k the j is erased and send a <ESC>.
I noticed that with version 1.17 the cursor is an underscore on j waiting for the next key, on version 1.16 I just have a plain line cursor, well though there was a bug in 1.16 that after only pressing jk to escape insert mode the file is considered modified.
Well, I remember I have met some problems with hotkeys in vscode-vim settings so I chose vscode builtin keybindings.json with the use of vim.active and so on conditions, and such hotkeys work well before 1.17.
I am not sure what changes are made to hotkey handling, but I remember in vim or sublime vintage or some other vim emulator I have used, they all don't have such a problem, the mapping of <ESC> is very common among developers, it's the most often operation, it should be great to use.
I think now the cause is that the j is inserted to the final cursor place, or what if we can remember where the j should be inserted, no matter what motion I do after type j? Or what if we display an underscore like 1.17 but still insert the j like in 1.16, and only if a k follows we erase them and keep the file unmodified.
I think choosing
jkorjjfor<ESC>should work well seamlessly, I used not to need to wait for the timeout, I now switched to 1.16.0, it works well, really well.
The key reason they are moving away from it is that it has been the root problem of many issues. The timeout effect does exist in the original Vim, and they want to align VSCode Vim Emulator with the actual Vim but still are struggling to have it work nicely with VSCode.
Now the real nightmare is if you choose to map jk/kj to <Esc> in visual mode, every j/k you press in visual mode will give you a lag depending on your timeout duration param in your setting.json
When I used to use vim, I remember that if I wanna type something like Dijkstra I still need to wait for the timeout, but only need to wait for a timeout when I wanna input jk. But I don't know how vim implements such functions in detail.
I see such changes may be important to solve more issues, I understand such breaking changes may cause some old functions to work not so good. We are patient to wait for the new mechanism to surpass the old way.
BTW, I choose <c-g> for <ESC> in modes other than insert mode, like visual mode or command mode😜, yes yet another emacs hotkey haha.
When I used to use vim, I remember that if I wanna type something like
DijkstraI still need to wait for the timeout, but only need to wait for a timeout when I wanna inputjk. But I don't know how vim implements such functions in detail.I see such changes may be important to solve more issues, I understand such breaking changes may cause some old functions to work not so good. We are patient to wait for the new mechanism to surpass the old way.
BTW, I choose
<c-g>for<ESC>in modes other than insert mode, like visual mode or command mode😜, yes yet another emacs hotkey haha.
Now I figured the key <Tab> is not so important so I just made it <Esc>
@Xdminsy Once #5280 is merged you can use option 2 or 3 and this will work seamlessly like it does in normal Vim. The problem with your approach is that your keybinding makes vscode move the cursor but it doesn't send that keypress to this extension.
So how are we supposed to know that you pressed a key? All we know is that you pressed 'j' and then we don't get any other input, so the timeout keeps running and when it finishes we insert the 'j'.
I can understand, but the j I inserted at one place, after the timeout it should still be at there, not fly to where now my cursor stands, right?
It's not only related to my custom keybindings. Other keybindings from other extensions will be affected as well. How can I map other extensions keybindings again in vim settings? There are many keybindings that will change the text or move the cursor. VSCode keybindings are the standard way all the extensions are using.
For example, one markdown extension provides an alt+s to wrap the current word with a strikethrough, but if I have a word ends with j. Say I wanna wrap proj with alt+s, now the result will be ~~pro~~j.
What happened? The markdown extension can't realize the j character we inserted, it wraps pro with ~~~~, and after the timeout, the vscode-vim inserts the j on the final cursor place.
It is really strange behavior, that if I map jk, the j is not inserted, every other extension can't realize that there is a j until timeout.
That means, if I map jk, I cannot do many actions that vscode-vim does not know after type j, but there are unlimited extensions, and they work well with the old version of vscode-vim in this aspect.
I'd suggest in such cases, the j should still be inserted, not to defer to timeout and insert, in most cases, my j is really a character j, inserted now when I type j, only jk cancels the j inserted, I really don't wanna my j key be a command display the j at cursor place, but insert j after a timeout or other vscode-vim actions.
I don't agree with the workaround 2 & 3, it's more likely to be an issue about the current changed keystroke handling mechanism now, I can move my custom keybinding, but it's onerous to move other extensions keybinding into vim settings manually, and it will make the settings.json too redundant, IIRC vscode now have no means to split settings.json to many smaller files, I prefer keybindings in keybindings.json, also recap, they all work well in old vscode-vim version.
@berknam I think it is only a matter of inventing an algorithm that makes it work.
Consider the following:
if user inputs something that match the beginning of any registered mapping, then cache every keystroke except the last one for timeoutlen long(and the location), insert them as-is, if timedout, do nothing to them, if matched, erase from the oldest one all the way to the lastest keystroke and fire the mapping.
Of course it is not how the original vim is doing it, but is it a must?
@JW9506
@berknam I think it is only a matter of inventing an algorithm that makes it work. Consider the following: if user inputs something that match the beginning of any registered mapping, then cache every keystroke except the last one for
timeoutlenlong(and the location), insert them as-is, if timedout, do nothing to them, if matched, erase from the oldest one all the way to the lastest keystroke and fire the mapping.Of course it is not how the original vim is doing it, but is it a must?
This is kind of the way it worked before. But this is a hack that creates a whole lot more issues. It was also one of the main reasons why this extension couldn't move their undo implementation to use the vscode undo. And the undo issue is the biggest issue yet to be fixed on this repo. This remapper overhaul will help in further changes to the undo implementation.
I can understand, but the
jI inserted at one place, after the timeout it should still be at there, not fly to where now my cursor stands, right?
It's not that it is flying somewhere else. The thing is after pressing 'j' it is not actually inserted until timeout finishes or another key is pressed. This is normal Vim behavior. Try this:
- Open Vim (with the map
imap jk <Esc>set) - Press
:splitto create a split of the current document - Change the timeout to something longer like 2s just to be easier to notice with
:set timeoutlen=2000 - Enter insert mode with
i - Now type the word "Test" one character at a time and see that character showing up straight away on the other split window
- Type 'j'. Now the 'j' will appear on the window you are currently in, but it won't show up on the other window, that is because it is not in the buffer yet it is just a visual representation. After 2s it will show up on the other window because only then will it be added to the document.
What is happening with your approach is like in Vim after typing the 'j' some other external program inserted some characters on the document and changed the cursor without Vim knowing, then the same thing would happen. Of course that doesn't happen in Vim, but this is an extension to vscode so it is different.
Previously it worked for you because the remapper wasn't correct, it had bugs and didn't even implement the timeout feature properly.
I don't agree with the workaround 2 & 3, it's more likely to be an issue about the current changed keystroke handling mechanism now, I can move my custom keybinding, but it's onerous to move other extensions keybinding into vim settings manually
VSCodeVim has its specific keybindings and the way to create them is either through 'settings.json' or a '.vimrc' file. These are the keybindings officially supported by this extension. Some people found workarounds for many of the bugs that existed in the remapper previously using the 'keybindings.json'. As with moving your custom bindings to 'settings.json' most of them are only a case of copy-pasting to the appropriate value (normalModeKeyBindings, insertModeKeyBindings, ...). If you copy them even with the 'when' clause that part will just be ignored and work properly. You only need to change keys like 'ctrl+e' to '<C-e>'.
Of course this is still annoying, I know, but it is the proper way to do this.
(I've been thinking of creating a way to allow the vim keybindings to be set like they are in vim with something like this:
"vim.remaps": [
'inoremap jk <Esc>',
'nnoremap L $',
]
But this is just an idea, haven't had the time to try it out.
In regards to other extensions keybindings, this is like in normal Vim, when you add a plugin sometimes you need to setup the keybindings for it or change conflicting remaps. In this case you could remap that markdown extension command in vim settings (like in option 2.).
Still this is all just my opinion. I'm not a maintainer of this extension. Maybe @J-Fields Can you give your opinion here?
Yes, in vim such timeout may not cause problems, vim can know and control everything, every key other than k ends the waiting, the j yes is not shown in a split window, but If I do anything manually, the j will be inserted first, the vim sees I pressed any key.
But can this extension vscode-vim interact with all commands or keybindings? Or maybe it needs to be part of vscode to work better, I guess. Or could vscode provide a way to hook before every command/keybinding? (sorry I haven't seen any vscode extension API.) As a great extension, it should regard other extensions' keybindings, not letting the user redefines. In vim, there is only one system to map keys, the key user defines not differ to what other extensions provide. Sorry again I do not know how other vim emulators implement, if anything I pointed not so true. Just from a vim emulator user standpoint.
I see such improvements can be hard, I understand, well, I just pointed the problem out now, we can wait for vscode-vim to be great again, you are still doing the right things. Now the temporary workaround could be still using 1.16 for work.
@berknam
It may worth a special treatment for key mappings just in insert mode. For users that may type fast at times, having to keep that timeoutlen in mind is tedious (not that it is impossible). For other modes, we can stay in the way it is now.
We can always reverse the state (what the user typed in) in the editor easily, but it is often hard to do in other modes, e.g. motions in visual mode and normal mode.
You don't have to always wait for timeout! We're talking about a very specific situation were the user is writing some text that ends in 'j' and then pressing a keybinding that triggers a command that makes changes to the editor without sending that key combination to extensions.
I've been using this remapper for quite a while and the only situation I've had something like this happening was when using the TAB key in some autocomplete when the last key I typed of the partial string was a 'j'. But that is a different issue because of the TAB key, as discussed here #5131 .
Most of the times the keybindings of other extensions won't be a problem, specially since the main goal when using Vim is that most of the time you'll be in normal mode and you'll preform actions in normal mode. Like that 'Toggle strike-through' command can be run without a problem in normal mode.
The problem in trying to create another hacky fix for insert mode is that we will run into the same problems again. What about undo, what about '.' (DotCommand) to repeat last action, what about macros. Even if we could somehow, just by noticing that the document changed, predict that a key must have been pressed how would we know what key was that? We could try to figure out what the result of the key was, like "it moved the cursor 3 characters to the right" but did it though?? Maybe it moved the cursor to the end of line?! or maybe it moved the cursor to the next word?! We have no way to know.
Actually, I said the TAB key was a different issue but it kind of is the same thing as this. Like with TAB where we have no way to know that it was pressed we also don't have any way to know with the current vscode API that a command ran triggered by a keypress from the user.
Since we're talking about it, maybe you could go and upvote this issue in vscode microsoft/vscode#104832. That feature request would certainly help solving this issue as well.
What I'm trying to say here is that if you press a potential remap key and then press a key combination that binds to a command that wasn't set in vim settings, vscode won't tell the extensions that a key was pressed or a command just ran. But you as the user can make Vim know about those commands by using our keybindings.
Of course you're not supposed to rebind every keybinding of every extension! You should only rebind those that at some point didn't work for you or created a situation like the one stated in this issue.
You don't have to always wait for timeout! We're talking about a very specific situation were the user is writing some text that ends in 'j' and then pressing a keybinding that triggers a command that makes changes to the editor without sending that key combination to extensions.
Now I totally understand, it lacks the ability to sendInputEvent back to VSCode. > <, kinda like it needs a bilateral relationship between it and vscode for them to work seamlessly
Recently I have time to try to move to 1.17.1, try to change my keybindings to make 1.17.1 happy, but I found the keybindings system is a so big issue.
I tried map c-p c-n in vim.insertModeKeyBindings, but both behave strangely.
{
"before": ["<c-p>"],
"commands": ["cursorUp"]
},
Well, then I press c-p, the default Go to file... command was invoked.
But I think I already let vscvim to handle c-p,
"vim.useCtrlKeys": true,
"vim.handleKeys": {
"<C-c>": false,
"<C-x>": false,
"<C-k>": false
},
But as I change <c-p> to <c-n> it works as cusorUp, but it still works in a strange way with jk.
Say now I map <c-n> to command cusorUp and jk to key <esc>, after press j and press <c-n>, the j disappears and the cursor moves up.
Well, shouldn't there be a j placed there? At least in real vim editor, there is.
I found the keybindings in 1.17.1 works so strangely, I cannot make my keys to work as I wanted, as they work well in 1.16.
Now I painfully do not know how to properly set keybindings in 1.17.1.
Why is it so finicky? I figure it is not only a breaking change, it nearly actually breaks the ability to let us use jk as <esc>.
If I stay in 1.16, there is an extension update badge numbered 1 always shows, FWIW, one workaround I found in https://github.com/microsoft/vscode/issues/58153, At least I can properly use jk with 1.16.
As an extension to power up the keyboard, I wish such many keybindings issues be solved in maybe the following versions.
Ah, I remember, maybe I choose to map keys in keybindings.json is because c-p and some other key mappings in vim.insertModeKeyBindings doesn't work, but do work in keybindings.json with a proper when clause in 1.16.
Yep, in 1.17.1 the workaround I found fails :(, not I want not to use insertModeKeyBindings, but it's really harder to use, or can someone tell me how to properly map c-p in such settings?
Say now I map
<c-n>to commandcusorUpandjkto key<esc>, after pressjand press<c-n>, the j disappears and the cursor moves up. Well, shouldn't there be ajplaced there? At least in real vim editor, there is.
This issue has been fixed on #5280 and should be out on the next version.
About the <C-p> situation, I'm not sure but I assume the developers didn't implement a complete capture of that key combination because of it being bound to such an important vscode command "Go to file..." which is similar to the usual plugins that use that key combo like the "ctrlp" plugin. Currently that key combination is only captured when in CommandLineMode or SearchInProgressMode (when using the / search) or when there is a suggestion widget open.
You can manually change that by changing the 'when' clause of the command "extension.vim_ctrl+p" on the "Keyboard Shortcuts". By default it is like this:
suggestWidgetVisible && vim.active && vim.use<C-p> && !inDebugRepl || vim.active && vim.use<C-p> && !inDebugRepl && vim.mode == 'CommandlineInProgress' || vim.active && vim.use<C-p> && !inDebugRepl && vim.mode == 'SearchInProgressMode'
You can change that when clause (right click the 'when' expression and then click "Change When Expression") to something like this:
suggestWidgetVisible && vim.active && vim.use<C-p> && !inDebugRepl || vim.active && vim.use<C-p> && !inDebugRepl && vim.mode == 'CommandlineInProgress' || vim.active && vim.use<C-p> && !inDebugRepl && vim.mode == 'SearchInProgressMode' || vim.active && vim.use<C-p> && !inDebugRepl && vim.mode == 'InsertMode'
This will allow using the <C-p> on 'insertModeKeyBindings' but only insert mode, if you want to be able to use it on all keybindings you would have to change the 'when' expression to this:
editorTextFocus && vim.active && vim.use<C-p> && !inDebugRepl || vim.active && vim.use<C-p> && !inDebugRepl && vim.mode == 'CommandlineInProgress' || vim.active && vim.use<C-p> && !inDebugRepl && vim.mode == 'SearchInProgressMode'
Note: This change is only temporary because next time VSCodeVim updates it will overwrite this 'when' expression. Try making this change and if you think this works well then open up a new issue with a feature request asking for this key combination to be captured normally, and then we can discuss if it is okay to make that change.
Haha, thank you for your advice, yes I came up with that maybe I can bind <c-p> to vim_ctrl+p manually yesterday, but those keybindings working with jk is important to me, I would like to try such solutions in the next version fixing that. 😄
Any updates on this? set timeoutlen=100 doesn't work for me
@JW9506 how to change modify timeout duration in settings.json? and also, is it possible to bind multiple keys to the same function. e.g. map both jk and JK to escape