Vim mode for VSCode, powered by 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.
init.vim
and many Vim plugins.Table of Contents
Install the vscode-neovim extension.
Install Neovim 0.9.0 or greater.
C:\Neovim\bin\nvim.exe
" or "/usr/local/bin/nvim
".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 the Linux path to the 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.
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.
defaults write com.microsoft.VSCode ApplePressAndHoldEnabled -bool false
."keyboard.dispatch": "keyCode"
:e
/:w
/:q
/:vsplit
/:tabnext
/etc are mapped to corresponding
VSCode commands and behavior may be different (see below).
: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
require('vscode-neovim').call()
(see API).:write
will be replaced by :Write
.abc<cursor>
in insert mode, then move the 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.vscode-neovim.logLevel
setting to "info", then restart vscode. View the logs via
Output: Focus on Output View
and select vscode-neovim
.vscode-neovim.neovimClean
in VSCode settings, which starts Nvim without your plugins (nvim --clean
).
Nvim plugins can do anything. Visual effects in particular can cause visual artifacts. vscode-neovim does its best
to merge the visual effects of Nvim and VSCode, but it's far from perfect. You may need to disable some Nvim plugins
that cause visual effects.Unable to init vscode-neovim: command 'type' already exists
message, uninstall other VSCode
extensions that use registerTextEditorCommand("type", …)
(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:
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:
To spawn multiple cursors from visual line/block modes type ma/mA or mi/mI (by default). The effect differs:
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
Load the module:
local vscode = require('vscode-neovim')
vscode.action()
: asynchronously executes a vscode command.vscode.call()
: synchronously executes a vscode command.vscode.on()
: defines a handler for some Nvim UI events.vscode.has_config()
: checks if a vscode setting exists.vscode.get_config()
: gets a vscode setting value.vscode.update_config()
: sets a vscode setting.vscode.notify()
: shows a vscode message (see also Nvim's vim.notify
).vscode.to_op()
: A helper for map-operator
. See code_actions.lua for the
usageg:vscode_clipboard
: Clipboard provider using VSCode's clipboard API. Used by default when in WSL. See
:h g:clipboard
for more details. Usage: let g:clipboard = g:vscode_clipboard
vscode.eval()
: evaluate javascript synchronously in vscode and return the resultvscode.eval_async()
: evaluate javascript asynchronously in vscodeAsynchronously executes a vscode command. See Examples.
Parameters:
name
(string): The name of the action, generally a vscode command.opts
(table): Map of optional parameters:
args
(table): List of arguments passed to the vscode command. If the command only requires a single object
parameter, you can directly pass in a map-like table.
action('foo', { args = { 'foo', 'bar', … } })
action('foo', { args = { foo = bar, … } })
range
(table): Specific range for the action. Implicitly passed in visual mode. Has three possible forms (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 when range
is specified.
Defaults to true
.callback
: Function to handle the action result. Must have this signature:
function(err: string|nil, ret: any)
err
is the error message, if anyret
is the resultSynchronously executes a vscode command. See Examples.
Parameters:
name
(string): The name of the action, generally a vscode command.opts
(table): Same as vscode.action().timeout
(number): Timeout in milliseconds. The default value is -1, which means there is no timeout.Returns: the result of the action
xnoremap = <Cmd>lua require('vscode-neovim').call('editor.action.formatSelection')<CR>
nnoremap = <Cmd>lua require('vscode-neovim').call('editor.action.formatSelection')<CR><Esc>
nnoremap == <Cmd>lua require('vscode-neovim').call('editor.action.formatSelection')<CR>
nnoremap <C-w>gd <Cmd>lua require('vscode-neovim').action('editor.action.revealDefinitionAside')<CR>
nnoremap ? <Cmd>lua require('vscode-neovim').action('workbench.action.findInFiles', { args = { query = vim.fn.expand('<cword>') } })<CR>
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
vscode.action("workbench.action.findInFiles", {
args = { query = vim.fn.expand('<cword>') }
})
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[]
: Returns true
if the configuration has a certain value, false
otherwise. If name
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. If name
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. OptionalExamples:
------------------
--- 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 function.
vim.notify = vscode.notify
Evaluate javascript inside vscode and return the result. The code is executed in an async function context (so await
can be used). Use a return
statement to return a value back to lua. Arguments passed from lua are available as the
args
variable. The evaluated code has access to the
VSCode API through the vscode
global.
Tips:
await
on asynchronous functions when accessing the API.logger
(e.g. logger.info(...)
) to log messages to the output of vscode-neovim (logging level
controlled with the vscode-neovim.logLevel
setting).{foo: 123}
are
converted to strings and returned as "[object Object]"
. JSON.stringify()
can be used to serialize plain objects
to JSON that can be deserialized with vim.json.decode()
in lua.globalThis['some_name'] = ...
can be used to persist values between calls.Parameters:
code
(string): The javascript to execute.opts
(table): Map of optional parameters:
args
(any): a value to make available as the args
variable in javascript. Can be a single value such as a
string or a table of multiple values.timeout
(number): The number of milliseconds to wait for the evalution to complete before cancelling. By default
there is no timeout.Returns:
Examples:
local current_file = vscode.eval("return vscode.window.activeTextEditor.document.fileName")
local current_tab_is_pinned = vscode.eval("return vscode.window.tabGroups.activeTabGroup.activeTab.isPinned")
vscode.eval("await vscode.env.clipboard.writeText(args.text)", { args = { text = "some text" } })
Like vscode.eval()
but returns immediately and evaluates in the background instead.
Parameters:
code
(string): The javascript to execute.opts
(table): Map of optional parameters:
args
(any): a value to make available as the args
variable in javascript. Can be a single value such as a
string or a table of multiple values.callback
: Function to handle the eval result. Must have this signature:
function(err: string|nil, ret: any)
err
is the error message, if anyret
is the resultNote: Since 1.0.0, vimscript functions are deprecated. Use the Lua api instead.
VSCodeNotify()
/VSCodeCall()
: deprecated, use Lua require('vscode-neovim').call()
instead.VSCodeNotifyRange()
/VSCodeCallRange()
: deprecated, use Lua
require('vscode-neovim').call(…, {range:…})
instead.VSCodeNotifyRangePos()
/VSCodeCallRangePos()
: deprecated, use Lua
require('vscode-neovim').call(…, {range:…})
instead.Default commands and bindings available for file/scroll/window/tab management:
💡 "With bang" refers to adding a "!" to the end of a command.
This document only mentions some special cases, it is not an exhaustive list of keybindings and commands. Use VSCode and Nvim features to see documentation and all defined shortcuts:
Preferences: Open Keyboard Shortcuts
vscode command and search for "neovim" to see all keybindings.:help
command to see the documentation for a given command or keybinding. For example try
:help :split
or :help zo
.
:help
for <C-…>
bindings is spelled CTRL-…
. For example to see the help for <c-w>
, run
:help CTRL-W
.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>"
}
There are three configurations for toggling keybindings:
ctrlKeysForInsertMode
: toggle ctrl keys for insert mode.ctrlKeysForNormalMode
: toggle ctrl keys for normal mode.editorLangIdExclusions
: disable keybindings defined by this extension in certain filetypes. Please note that this
will not affect all keybindings.If you find that these options are not working, you can manually modify the keybindings in VSCode (see below).
settings.json
, or use the VSCode keybindings editor:
💡 See Keybindings help to see all defined shortcuts and their documentation.
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.
💡 See Keybindings help to see all defined shortcuts and their documentation.
Key | VSCode Command |
---|---|
j or k | list.focusDown/Up |
h or l | list.collapse/select |
Enter | list.select |
gg | list.focusFirst |
G | list.focusLast |
o | list.toggleExpand |
C-u or C-d | list.focusPageUp/Down |
zo or zO | list.expand |
zc | list.collapse |
zC | list.collapseAllToFocus |
za or zA | list.toggleExpand |
zm or zM | list.collapseAll |
/ or Escape | list.toggleKeyboardNavigation |
💡 See Keybindings help to see all defined shortcuts and their documentation.
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 |
R | workbench.files.action.refreshFilesExplorer |
💡 See Keybindings help to see all defined shortcuts and their documentation.
The following keybinding is set by default: When hover is invisible, K is sent to nvim(show hover); when hover is visible, press K again to focus the hover widget.
{
"command": "editor.action.showHover",
"key": "shift+k",
"when": "neovim.init && neovim.mode == normal && editorTextFocus && editorHoverVisible"
}
Key | VSCode Command |
---|---|
h | editor.action.scrollLeftHover |
j | editor.action.scrollDownHover |
k | editor.action.scrollUpHover |
l | editor.action.scrollRightHover |
gg | editor.action.goToTopHover |
G | editor.action.goToBottomHover |
C-f | editor.action.pageDownHover |
C-b | editor.action.pageUpHover |
The extension aliases various Nvim commands (:edit
, :enew
, :find
, :write
, :saveas
, :wall
, :quit
, etc.) to
equivalent vscode commands. Also their normal-mode equivalents (where applicable) such as C-w q, etc.
💡 See Keybindings help to see all defined shortcuts and their documentation.
The extension aliases various Nvim tab commands (:tabedit
, :tabnew
, :tabfind
, :tabclose
, :tabnext
,
:tabprevious
, :tabfirst
, :tablast
) to equivalent vscode commands. Also their normal-mode equivalents (where
applicable) such as gt, etc.
💡 See Keybindings help to see all defined shortcuts and their documentation.
The extension aliases various Nvim buffer/window commands (:split
, :vsplit
, :new
, :vnew
, :only
) to equivalent
vscode commands. Also their normal-mode equivalents (where applicable) such as C-w s, etc.
💡 See Keybindings help to see all defined shortcuts and their documentation.
💡 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
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", "m", "o", "r", "t", "u", "w"]
💡 See Keybindings help to see all defined shortcuts and their documentation.
Enabled by ctrlKeysForNormalMode
Default:
["a", "b", "d", "e", "f", "h", "i", "j", "k", "l", "m", "o", "r", "t", "u", "v", "w", "x", "y", "z", "/", "]"]
💡 See Keybindings help to see all defined shortcuts and their documentation.
Always enabled.
<C-h>
<C-w>
<C-u>
<C-n>
<C-p>
<C-l>
<C-g>
<C-t>
<C-r>
prefixed keys💡 See Keybindings help to see all defined shortcuts and their documentation.
There are two ways to customize colors:
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:
Please see CONTRIBUTING.md for details on how to contribute to this project.