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
- 💡 Tips and Features
- ⚡️ API
- ⌨️ Bindings
- 🎨 Highlights
- 🔧 Build
- 📑 How it works
- ❤️ Credits & External Resources
-
Install the vscode-neovim extension.
-
Install Neovim 0.9.0 or greater.
- Set the Neovim path in the extension settings. You must specify the 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.
- Set the Neovim path in the extension settings. You must specify the full path to Neovim, like
"
-
If you want to use Neovim from WSL, set the
useWSL
configuration toggle and specify the Linux path to the nvim binary.wsl.exe
Windows binary andwslpath
Linux binary are required for this.wslpath
must be available through$PATH
Linux env setting. Usewsl --list
to check for the correct default Linux distribution. -
Assign affinity value for performance improvement.
-
Go to Settings > Features > Extensions > Experimental Affinity.
Add an entry with item name
asvetliakov.vscode-neovim
and value 1.OR
-
Add to your
settings.json
:"extensions.experimental.affinity": { "asvetliakov.vscode-neovim": 1 },
-
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
In lua:
if vim.g.vscode then
-- VSCode extension
else
-- ordinary Neovim
end
To conditionally activate plugins, vim-plug
has a
few solutions. packer.nvim
and lazy.nvim
have built-in support for cond = vim.g.vscode
. See
plugins in the wiki for tips on configuring VIM plugins.
- 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 the remapped escape key not working in Linux, set
"keyboard.dispatch": "keyCode"
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.
- 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 withVSCodeCall/VSCodeNotify
(see below). - When you type some commands they may be substituted for 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 the cursor toa<cursor>bc
and type1
here the repeat sequence would be1
. However, in VSCode, it would bea1bc
. 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.
If you get the "Unable to init vscode-neovim: command 'type' already exists" message, uninstall other VSCode extensions
that register the type
command (like VSCodeVim or
Overtype).
Make sure you have the extension running in its own thread using affinity (see installation).
Extensions that share the same affinity value are associated with a shared extension host (extension manager from VSCode). Performance issues arise when a number of extensions have the same host. On-going operations of one extension may slow down the operations of another. However, if an extension is assigned an affinity, its extension host runs in a separate worker thread. The operations of an extension with the host in one thread don't directly affect the operations of the extension with its host running in another.
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.
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
.
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.
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:
Multiple cursors work in:
- Insert mode
- Visual line mode
- 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:
The built-in multi-cursor support may not meet your needs. Please refer to the plugin vscode-multi-cursor.nvim for more multi-cursor features
There are a few helper functions that are used to invoke VSCode commands from Neovim.
VSCodeNotify(command, ...)
/VSCodeCall
: Invoke VSCode command with optional arguments.VSCodeNotifyRange(command, line1, line2, leaveSelection, ...)
/VSCodeCallRange
: Produce linewise VSCode selection fromline1
toline2
and invoke VSCode command. SettingleaveSelection
to 1 keeps VSCode selection active after invoking the command. Line is 1-based.VSCodeNotifyRangePos(command, line1, line2, pos1, pos2, leaveSelection ,...)
/VSCodeCallRangePos
: Produce characterwise VSCode selection fromline1.pos1
toline2.pos2
and invoke VSCode command. Pos is (1, 1)-based.
💡 Functions with
Notify
in their name are non-blocking, the ones withCall
are blocking. Generally use Notify unless you really need a blocking call. One example of a blocking call is wanting VSCode to process a visual selection when running a command before exiting visual mode.
Format selection (default binding):
xnoremap = <Cmd>call VSCodeCall('editor.action.formatSelection')<CR>
nnoremap = <Cmd>call VSCodeCall('editor.action.formatSelection')<CR><Esc>
nnoremap == <Cmd>call VSCodeCall('editor.action.formatSelection')<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.
Load module: local vscode = require("vscode-neovim")
vscode.action
for asynchronous execution of actions.vscode.call
for synchronous execution of actions.vscode.on
for adding hook functions.vscode.has_config
check if a configuration existsvscode.get_config
get a configuration valuevscode.update_config
update a configurationvscode.notify
likevim.notify
, but use vscode notification to show the messagevscode.to_op
A helper formap-operator
. See code_actions.lua for the usagevscode.get_status_item
Creates a status item
This function is used to run an action asynchronously.
Parameters:
name
(string): The name of the action, generally a vscode command.opts
(table): Optional table of options. All fields in the table are optional.args
(table): Optional arguments for the action.range
(table): Specific range for the action. In visual mode, this parameter is generally not needed. There are three supported formats for defining the range (all values are 0-indexed):[start_line, end_line]
[start_line, start_character, end_line, end_character]
{start = { line = start_line, character = start_character}, end = { line = end_line, character = end_character}}
restore_selection
(boolean): Whether to preserve the current selection. Only valid whenrange
is specified. Defaults totrue
.callback
: Optional callback function to handle the action result. The callback function should have the following signature:function(err: string|nil, ret: any)
. The first argument is the error message, and the second is the result. If no callback is provided, any error message will be shown as a notification in VSCode.
This function is used to run an action synchronously.
Parameters:
name
(string): The name of the action, generally a vscode command.opts
(table): Optional table of options. All fields in the table are optional.args
(table): Optional arguments for the action.range
(table): Specific range for the action. In visual mode, this parameter is generally not needed. There are three supported formats for defining the range (all values are 0-indexed):[start_line, end_line]
[start_line, start_character, end_line, end_character]
{start = { line = start_line, character = start_character}, end = { line = end_line, character = end_character}}
restore_selection
(boolean): Whether to preserve the current selection. Only valid whenrange
is specified. Defaults totrue
.
timeout
(number): Timeout in milliseconds. The default value is -1, which means there is no timeout.
Returns: the result of the action
Currently, two built-in actions are provided for testing purposes:
_ping
returns"pong"
_wait
waits for the specified milliseconds and then returns"ok"
do -- Execute _ping asynchronously and print the result
vscode.action("_ping", {
callback = function(err, res)
if err == nil then
print(res) -- outputs: pong
end
end,
})
end
-- Format current document
vscode.action("editor.action.formatDocument")
do -- Comment the three lines below the cursor
local curr_line = vim.fn.line(".") - 1 -- 0-indexed
vscode.action("editor.action.commentLine", {
range = { curr_line + 1, curr_line + 3 },
})
end
do -- Comment the previous line
local curr_line = vim.fn.line(".") - 1 -- 0-indexed
local prev_line = curr_line - 1
if prev_line >= 0 then
vscode.action("editor.action.commentLine", {
range = { prev_line , prev_line },
})
end
end
do -- Find in files for word under cursor
local arg = { query = vim.fn.expand('<cword>') }
vscode.action("workbench.action.findInFiles", { args = { arg } })
end
-- Execute _ping synchronously and print the result
print(vscode.call("_ping")) -- outputs: pong
-- Wait for 1 second and print the return value 'ok'
print(vscode.call("_wait", { args = { 1000 } })) -- outputs: ok
-- Wait for 2 seconds with a timeout of 1 second
print(vscode.call("_wait", { args = { 2000 } }), 1000)
-- error: Call '_wait' timed out
Currently no available events for user use.
Check if configuration has a certain value.
Parameters:
name
(string|string[]): The configuration name or an array of configuration names.
Returns:
boolean|boolean[]
: Returnstrue
if the configuration has a certain value,false
otherwise. Ifname
is an array, returns an array of booleans indicating whether each configuration has a certain value or not.
Get configuration value.
Parameters:
name
(string|string[]): The configuration name or an array of configuration names.
Returns:
unknown|unknown[]
: The value of the configuration. Ifname
is an array, returns an array of values corresponding to each configuration.
Update configuration value.
Parameters:
name
(string|string[]): The configuration name or an array of configuration names.value
(unknown|unknown[]): The new value for the configuration.target
("global"|"workspace"): The configuration target. Optional
Examples:
------------------
--- has_config ---
------------------
-- Check if the configuration "not.exist" exists
print(vscode.has_config("not.exist"))
-- Should return: false
-- Check multiple configurations
vim.print(vscode.has_config({ "not.exist", "existing.config" }))
-- Should return: { false, true }
------------------
--- get_config ---
------------------
-- Get the value of "editor.tabSize"
print(vscode.get_config("editor.tabSize")) -- a number
-- Get multiple configurations
vim.print(vscode.get_config({ "editor.fontFamily", "editor.tabSize" }))
-- Should return: { "the font family", "the editor tabSizse" }
---------------------
--- update_config ---
---------------------
-- Update the value of "editor.tabSize"
vscode.update_config("editor.tabSize", 16, "global")
-- Update multiple configurations
vscode.update_config({ "editor.fontFamily", "editor.tabSize" }, { "Fira Code", 14 })
Show a vscode notification
You can set vscode.notify
as your default notify functions.
vim.notify = vscode.notify
Creates a status item
id
(string): The identifier of the item
local test = vscode.get_status_item('test')
test.text = 'hello' -- Show the text
test.text = '' -- Hide the item
test.text = nil -- Close the item
test.text = '' -- error: The status item "test" has been closed
These are the default commands and bindings available for file/scroll/window/tab management.
- See vscode-scrolling.vim for scrolling commands reference
- See vscode-file-commands.vim for file commands reference
- See vscode-tab-commands.vim for tab commands reference
- See vscode-window-commands.vim for window commands reference
💡 "With bang" refers to adding a "!" to the end of a 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.
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 |
z o / z O | list.expand |
z c | list.collapse |
z C | list.collapseAllToFocus |
z a / z A | list.toggleExpand |
z m / z M | list.collapseAll |
/ / Escape | list.toggleKeyboardNavigation |
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 |
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. |
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. |
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/k/h/l | Move editor to group below/above/left/right. | |
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'sdistribute
, which is equal to VIM'sequalalways
andeadirection = '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>
Enabled by ctrlKeysForInsertMode
Default: ["a", "d", "h", "j", "o", "r", "t", "u", "w"]
Refer to VIM's manual for their use.
Enabled by ctrlKeysForNormalMode
Default: ["a", "b", "d", "e", "f", "h", "i", "j", "k", "l", "o", "r", "t", "u", "v", "w", "x", "y", "z", "/", "]"]
Refer to VIM's manual for their use.
Always enabled.
- Tab
- Ctrl keys:
<C-h>
<C-w>
<C-u>
<C-n>
<C-p>
<C-l>
<C-g>
<C-t>
- All
<C-r>
prefixed keys
Refer to VIM's manual for their use.
To disable keybindings defined by this extension in certain filetypes, you can use the editorLangIdExclusions
configuration. Please note that this will not affect all keybindings. If you find that this option is not working, you
can manually modify the keybindings in VSCode.
There are two ways to customize colors:
- Set colors in nvim
Note: Due to the support for the syntax
option requiring processing of syntax highlights, all built-in highlight
groups may be overridden or cleared. Therefore, please do not link any highlights to the built-in highlight groups.
-
Set colors in vscode
References:
How to build (and install) from source:
-
Clone the repo locally.
git clone https://github.com/vscode-neovim/vscode-neovim
-
Install the dependencies.
npm install
-
Build the VSIX package:
npx vsce package -o vscode-neovim.vsix
-
From VSCode, use the
Extensions: Install from VSIX
command to install the package.
How to develop:
- Open the repo in VSCode.
- Go to debug view and click
Run Extension
(F5).
How to run tests:
- Open the repo in VSCode.
- Go to debug view and click
Extension Tests
(F5). - To run individual tests, modify
grep: ".*"
insrc/test/suite/index.ts
.
- 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. Changes are synced to neovim in periodic intervals.
- After pressing escape key from the insert mode, extension sends changes obtained from the insert mode to Neovim.
- vim-altercmd - Used for rebinding default commands to call VSCode command.
- neovim nodejs client - NodeJS library for communicating with Neovim.
- VSCodeVim - Used for various inspiration.