/vscode-neovim

Vim-mode for VS Code using embedded Neovim

Primary LanguageTypeScriptMIT LicenseMIT


VSCode Neovim

VSCode Neovim Integration

Neovim is a fork of VIM to allow greater extensibility and integration. This extension uses a fully embedded Neovim instance, no more half-complete VIM emulation! VSCode's native functionality is used for insert mode and editor commands, making the best use of both editors.

  • 🎉 Almost fully feature-complete VIM integration by utilizing Neovim as a backend.
  • 🔧 Supports custom init.vim and many VIM plugins.
  • 🥇 First-class and lag-free insert mode, letting VSCode do what it does best.
  • 🤝 Complete integration with VSCode features (lsp/autocompletion/snippets/multi-cursor/etc).

Table of Contents

🧰 Getting Started

Installation

  • Install the vscode-neovim extension.
  • Install Neovim 0.5.0 or greater.
    • Set the Neovim path in the extension settings. You must specify full path to Neovim, like "C:\Neovim\bin\nvim.exe" or "/usr/local/bin/nvim".
    • The setting id is "vscode-neovim.neovimExecutablePaths.win32/linux/darwin", respective to your system.
  • If you want to use Neovim from WSL, set the useWSL configuration toggle and specify Linux path to nvim binary. wsl.exe Windows binary and wslpath Linux binary are required for this. wslpath must be available through $PATH Linux env setting. Use wsl --list to check for the correct default Linux distribution.

Neovim configuration

Since many VIM plugins can cause issues in VSCode, it is recommended to start from an empty init.vim. For a guide for which types of plugins are supported, see troubleshooting.

Before creating an issue on Github, make sure you can reproduce the problem with an empty init.vim and no VSCode extensions.

To determine if Neovim is running in VSCode, add to your init.vim:

if exists('g:vscode')
    " VSCode extension
else
    " ordinary Neovim
endif

Or to your init.lua:

if vim.g.vscode then
    -- VSCode extension
else
    -- ordinary Neovim
end

To conditionally activate plugins, vim-plug has a few solutions. For example, using the Cond helper, you can conditionally activate installed plugins (source):

" inside plug#begin:
" use normal easymotion when in VIM mode
Plug 'easymotion/vim-easymotion', Cond(!exists('g:vscode'))
" use VSCode easymotion when in VSCode mode
Plug 'asvetliakov/vim-easymotion', Cond(exists('g:vscode'), { 'as': 'vsc-easymotion' })

See plugins in the wiki for tips on configuring VIM plugins.

VSCode configuration

  • On a Mac, the h, j, k and l movement keys may not repeat when held, to fix this open Terminal and execute the following command: defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false.
  • To fix remapped escape key not working in Linux, set "keyboard.dispatch": "keyCode"

Adding keybindings

Every special (control/alt) keyboard shortcut must be explicitly defined in VSCode to send to neovim. By default, only bindings that are included by Neovim by default are sent.

To pass custom bindings to Neovim, for example C-h in normal mode, add to your keybindings.json:

{
    "command": "vscode-neovim.send",
    // the key sequence to activate the binding
    "key": "ctrl+h",
    // don't activate during insert mode
    "when": "editorTextFocus && neovim.mode != insert",
    // the input to send to Neovim
    "args": "<C-h>"
}

To disable an existing shortcut, for example C-a, add to your keybindings.json:

{
    "command": "-vscode-neovim.send",
    "key": "ctrl+a"
}

The VSCode keybindings editor provides a good way to delete keybindings.

💡 Tips and Features

VSCode specific differences

  • File and editor management commands such as :e/:w/:q/:vsplit/:tabnext/etc are mapped to corresponding VSCode commands and behavior may be different (see below). Do not use vVIM commands like :w in scripts/keybindings, they won't work. If you're using them in some custom commands/mappings, you might need to rebind them to call VSCode commands from Neovim with VSCodeCall/VSCodeNotify (see below).
  • Visual modes don't produce VSCode selections, so any VSCode commands expecting selection won't work. To round the corners, invoking the VSCode command picker from visual mode through the default hotkeys (f1/ctrl/cmd+shift+p) converts VIM selection to real VSCode selection. This conversion is also done automatically for some commands like commenting and formatting. If you're using some custom mapping for calling VSCode commands that depends on real VSCode selection, you can use VSCodeNotifyRange/VSCodeNotifyRangePos/VSCodeNotifyVisual (linewise, characterwise, and automatic) which will convert VIM visual mode selection to VSCode selection before calling the command (see below).
  • When you type some commands they may be substituted for the another, like :write will be replaced by :Write.
  • Scrolling is done by VSCode. C-d/C-u/etc are slightly different.
  • Editor customization (relative line number, scrolloff, etc) is handled by VSCode.
  • Dot-repeat (.) is slightly different - moving the cursor within a change range won't break the repeat. sequence. In Neovim, if you type abc<cursor> in insert mode, then move cursor to a<cursor>bc and type 1 here the repeat sequence would be 1. However in VSCode it would be a1bc. Another difference is that when you delete some text in insert mode, dot repeat only works from right-to-left, meaning it will treat Del key as BS keys when running dot repeat.

Troubleshooting

If you get "Unable to init vscode-neovim: command 'type' already exists" message, uninstall other VSCode extensions that register the type command (like VSCodeVim or Overtype).

Performance problems

If you have any performance problems (cursor jitter usually) make sure you're not using these kinds of extensions:

  • Anything that renders decorators very often:
    • Line number extensions (VSCode has built-in support for normal/relative line numbers)
    • Indent guide extensions (VSCode has built-in indent guides)
    • Brackets highlighter extensions (VSCode has built-in feature)
  • VSCode extensions that delay the extension host like "Bracket Pair Colorizer"
  • VIM plugins that increase latency and cause performance problems.
    • Make sure to disable unneeded plugins, as many of them don't make sense with VSCode and may cause problems.
    • You don't need any code, highlighting, completion, LSP plugins as well any plugins that spawn windows/buffers (nerdtree and similar), fuzzy-finders, etc.
    • Many navigation/textobject/editing plugins should be fine.

If you're not sure, disable all other extensions, reload VSCode window, and see if the problem persists before reporting it.

Composite escape keys

Since VSCode is responsible for insert mode, custom insert-mode VIM mappings don't work. To map composite escape keys, put into your keybindings.json:

for jj

{
    "command": "vscode-neovim.compositeEscape1",
    "key": "j",
    "when": "neovim.mode == insert && editorTextFocus",
    "args": "j"
}

to enable jk add also:

{
    "command": "vscode-neovim.compositeEscape2",
    "key": "k",
    "when": "neovim.mode == insert && editorTextFocus",
    "args": "k"
}

Currently, there is no way to map both jk and kj, or to map jk without also mapping jj.

Jumplist

VSCode's jumplist is used instead of Neovim's. This is to make VSCode native navigation (mouse click, jump to definition, etc) navigable through the jumplist.

Make sure to bind to workbench.action.navigateBack / workbench.action.navigateForward if you're using custom mappings. Marks (both upper & lowercased) should work fine.

Wildmenu completion

Command menu has the wildmenu completion on type. The completion options appear after 1.5s (to not bother you when you write :w or :noh). Up/Down selects the option and Tab accepts it. See the gif:

wildmenu

Multiple cursors

Multiple cursors work in:

  1. Insert mode
  2. Visual line mode
  3. Visual block mode

To spawn multiple cursors from visual line/block modes type ma/mA or mi/mI (by default). The effect differs:

  • For visual line mode, mi will start insert mode on each selected line on the first non whitespace character and ma will on the end of line.
  • For visual block mode, mi will start insert on each selected line before the cursor block and ma after.
  • mA/mI versions accounts for empty lines (only for visual line mode, for visual block mode they're same as ma/mi).

See gif in action:

multicursors

Keyboard Quickfix

By default, the quickfix menu can be opened using z= or C-.. However, it is currently not possible to add mappings to the quickfix menu, so it can only be navigated with arrow keys. A workaround vscode extension has been made to use the quick open menu, which can be navigated with custom bindings.

To use, install the keyboard-quickfix extension, and add to your keybindings.json:

{
    "key": "ctrl+.",
    "command": "keyboard-quickfix.openQuickFix",
    "when": "editorHasCodeActionsProvider && editorTextFocus && !editorReadonly"
},

and add to your init.vim:

nnoremap z= <Cmd>call VSCodeNotify('keyboard-quickfix.openQuickFix')<CR>

Invoking VSCode actions from neovim

There are a few helper functions that are used to invoke VSCode commands from Neovim:

Command Description
VSCodeNotify(command, ...)
VSCodeCall(command, ...)
Invoke VSCode command with optional arguments.
VSCodeNotifyRange(command, line1, line2, leaveSelection ,...)
VSCodeCallRange(command, line1, line2, leaveSelection, ...)
Produce linewise VSCode selection from line1 to line2 and invoke VSCode command. Setting leaveSelection to 1 keeps VSCode selection active after invoking the command.
VSCodeNotifyRangePos(command, line1, line2, pos1, pos2, leaveSelection ,...)
VSCodeCallRangePos(command, line1, line2, pos1, pos2, leaveSelection, ...)
Produce characterwise VSCode selection from line1.pos1 to line2.pos2 and invoke VSCode command.
VSCodeNotifyVisual(command, leaveSelection, ...)
VSCodeCallVisual(command, leaveSelection, ...)
Produce linewise (visual line) or characterwise (visual and visual block) selection from visual mode selection and invoke VSCode command. Behaves like VSCodeNotify/Call when visual mode is not active.

💡 Functions with Notify in their name are non-blocking, the ones with Call are blocking. Generally use Notify unless you really need a blocking call.

Examples

Open command picker (default binding):

xnoremap <C-S-P> <Cmd>call VSCodeNotifyVisual('workbench.action.showCommands', 1)<CR>

Open definition aside (default binding):

nnoremap <C-w>gd <Cmd>call VSCodeNotify('editor.action.revealDefinitionAside')<CR>

Find in files for word under cursor:

nnoremap ? <Cmd>call VSCodeNotify('workbench.action.findInFiles', { 'query': expand('<cword>')})<CR>

More advanced examples can be found here.

⌨️ Bindings

These are the default commands and bindings available for file/scroll/window/tab management.

💡 "With bang" refers to adding a "!" to the end of a command.

VSCode specific bindings

Editor command

Key VSCode Command
= / == editor.action.formatSelection
gh / K editor.action.showHover
gd / C-] editor.action.revealDefinition
Also works in vim help.
gf editor.action.revealDeclaration
gH editor.action.referenceSearch.trigger
gO workbench.action.gotoSymbol
C-w gd / C-w gf editor.action.revealDefinitionAside
gD editor.action.peekDefinition
gF editor.action.peekDeclaration
Tab togglePeekWidgetFocus
Switch between peek editor and reference list.
C-n / C-p Navigate lists, parameter hints, suggestions, quick-open, cmdline history, peek reference list

💡 To specify the default peek mode, modify editor.peekWidgetDefaultFocus in your settings.

Explorer/list navigation

Key VSCode Command
j / k list.focusDown/Up
h / l list.collapse/select
Enter list.select
gg list.focusFirst
G list.focusLast
o list.toggleExpand
C-u / C-d list.focusPageUp/Down
/ / Escape list.toggleKeyboardNavigation

Explorer file manipulation

Key VSCode Command
r renameFile
d deleteFile
y filesExplorer.copy
x filesExplorer.cut
p filesExplorer.paste
v explorer.openToSide
a explorer.newFile
A explorer.newFolder

File management

Command Description
e[dit] / ex Open quickopen.
With filename, e.g. :e $MYVIMRC: open the file in new tab. The file must exist.
With bang: revert file to last saved version.
With filename and bang e.g. :e! $MYVIMRC: close current file (discard any changes) and open the file. The file must exist.
ene[w] Create new untitled document in VSCode.
With bang: close current file (discard any changes) and create new document.
fin[d] Open VSCode's quick open window. Arguments and count are not supported.
w[rite] Save current file. With bang: open 'save as' dialog.
sav[eas] Open 'save as' dialog.
wa[ll] Save all files.
q[uit] / C-w q / C-w c / ZQ Close the active editor. With bang: revert changes and close the active editor.
wq / ZZ Save and close the active editor.
qa[ll] Close all editors, but don't quit VSCode. Acts like qall!, so beware of unsaved changes.
wqa[ll] / xa[ll] Save all editors & close.

Tab management

Command Description
tabe[dit] Similar to e[dit]. Open quickopen.
With argument: open the file in new tab.
tabnew Open new untitled file.
tabf[ind] Open quickopen window.
tab/tabs Not supported. Doesn't make sense with VSCode.
tabc[lose] Close active editor (tab).
tabo[nly] Close other tabs in VSCode group (pane). This differs from VIM where a tab is a like a new window, but doesn't make sense in VSCode.
tabn[ext] / gt Switch to next (or count tabs if argument is given) in the active VSCode group (pane).
tabp[revious] / gT Switch to previous (or count tabs if argument is given) in the active VSCode group (pane).
tabfir[st] Switch to the first tab in the active editor group.
tabl[ast] Switch to the last tab in the active editor group.
tabm[ove] Not supported yet.

Buffer/window management

Command Key Description
sp[lit] C-w s Split editor horizontally.
With argument: open the specified file, e.g. :sp $MYVIMRC. File must exist.
vs[plit] C-w v Split editor vertically.
With argument: open the specified file. File must exist.
new C-w n Like sp[lit] but create new untitled file if no argument given.
vne[w] Like vs[plit] but create new untitled file if no argument given.
C-w = Align all editors to have the same width.
C-w _ Toggle maximized editor size. Pressing again will restore the size.
[count] C-w + Increase editor height by (optional) count.
[count] C-w - Decrease editor height by (optional) count.
[count] C-w > Increase editor width by (optional) count.
[count] C-w < Decrease editor width by (optional) count.
on[ly] C-w o Without bang: merge all editor groups into the one. Don't close editors.
With bang: close all editors from all groups except current one.
C-w j/k/h/l Focus group below/above/left/right.
C-w C-j/i/h/l Move editor to group below/above/left/right.
Note: C-w C-i moves editor up. Ideally it should be C-w C-k but VSCode has many commands mapped to C-k [key] and doesn't allow using C-w C-k without unbinding them first.
C-w J/K/H/L Move whole editor group below/above/left/right.
C-w w or C-w C-w Focus next group. The behavior may differ than in vim.
C-w W or C-w p Focus previous group. The behavior may differ than in vim. C-w p is completely different from vim.
C-w b Focus last editor group (most bottom-right).
C-w r/R/x Not supported, use C-w C-j and similar to move editors.

💡 Split size distribution is controlled by workbench.editor.splitSizing setting. By default, it's distribute, which is equal to VIM's equalalways and eadirection = 'both' (default).

To use VSCode command 'Increase/decrease current view size' instead of separate bindings for width and height:

  • workbench.action.increaseViewSize
  • workbench.action.decreaseViewSize
Copy this into init.vim
function! s:manageEditorSize(...)
    let count = a:1
    let to = a:2
    for i in range(1, count ? count : 1)
        call VSCodeNotify(to ==# 'increase' ? 'workbench.action.increaseViewSize' : 'workbench.action.decreaseViewSize')
    endfor
endfunction

" Sample keybindings. Note these override default keybindings mentioned above.
nnoremap <C-w>> <Cmd>call <SID>manageEditorSize(v:count, 'increase')<CR>
xnoremap <C-w>> <Cmd>call <SID>manageEditorSize(v:count, 'increase')<CR>
nnoremap <C-w>+ <Cmd>call <SID>manageEditorSize(v:count, 'increase')<CR>
xnoremap <C-w>+ <Cmd>call <SID>manageEditorSize(v:count, 'increase')<CR>
nnoremap <C-w>< <Cmd>call <SID>manageEditorSize(v:count, 'decrease')<CR>
xnoremap <C-w>< <Cmd>call <SID>manageEditorSize(v:count, 'decrease')<CR>
nnoremap <C-w>- <Cmd>call <SID>manageEditorSize(v:count, 'decrease')<CR>
xnoremap <C-w>- <Cmd>call <SID>manageEditorSize(v:count, 'decrease')<CR>

Insert mode special keys

Enabled by useCtrlKeysForInsertMode (default true).

Refer to VIM's manual for their use.

  • C-c
  • C-o
  • C-u
  • C-w
  • C-h
  • C-t
  • C-d
  • C-j
  • C-a
  • C-r

Normal mode control keys

Enabled by useCtrlKeysForNormalMode (default true).

Refer to VIM's manual for their use.

  • C-a
  • C-b
  • C-c
  • C-d
  • C-e
  • C-f
  • C-i
  • C-o
  • C-r
  • C-u
  • C-v
  • C-w
  • C-x
  • C-y
  • C-z
  • C-]
  • C-j
  • C-k
  • C-l
  • C-h
  • C-/

Cmdline special keys

Always enabled.

Refer to VIM's manual for their use.

  • C-h
  • C-w
  • C-u
  • C-r (including C-rC-w and others)
  • C-n
  • C-p
  • C-l
  • C-g
  • C-t
  • Tab

🔧 Build

How to build (and install) from source:

  1. Clone the repo locally.

    git clone https://github.com/vscode-neovim/vscode-neovim
    
  2. Install the dependencies.

    yarn install
    
  3. Build the VSIX package:

    ./node_modules/.bin/yarn run vsce package -o vscode-neovim.vsix
    
  4. From VSCode, use the Extensions: Install from VSIX command to install the package.

How to develop:

  1. Open the repo in VSCode.
  2. Go to debug view and click Run Extension (F5).

How to run tests:

  1. Open the repo in VSCode.
  2. Go to debug view and click Extension Tests (F5).
  3. To run individual tests, modify grep: ".*" in src/test/suite/index.ts.

📑 How it works

  • VScode connects to Neovim instance.
  • When opening a file, a scratch buffer is created within Neovim and being initialized with text content from VSCode.
  • Normal/visual mode commands are being sent directly to Neovim. The extension listens for buffer events and applies edits from Neovim.
  • When entering the insert mode, the extensions stops listen for keystroke events and delegates typing mode to VSCode (no Neovim communication is being performed here).
  • After pressing escape key from the insert mode, extension sends changes obtained from the insert mode to Neovim.

❤️ Credits & External Resources