Neovim to VS Code
I finally switched from Neovim to Visual Studio Code.
This is a vim power user’s guide to getting started with VS Code. It’s particularly relevant for front-end developers. I’ll cover the main reasons I switched, the joy of using the VSCode Neovim extension, some VS Code features, and how to use vim and VS Code in parallel. If you want to skip the backstory, jump to Getting started.
Motivation
I’d been talking about switching from vim to a graphical editor for years because I envied the ease with which other devs working on the front end could get set up with new tools. For any new code base or framework, I found that in the vim ecosystem it was a project just to get things working. Here are some specific areas…
Vim plugins and support
Edit: Front-end plugins often don’t exist in the vim ecosystem or they require particular (hard-to-find and hard-to-configure) settings to work for your setup. The plugins and their documentation/wikis are sprawled across GitHub wiki, READMEs, vim :help
, and so on. Debugging vim plugins and using vimscript is a pain compared to the wealth of support in VS Code community and relative ease of reading JavaScript.
Vim and language servers
Anything depending on language servers like linting, formatting, and tags
was awkward and difficult in vim. Coc seemed like the main option for quite a while and I found it difficult to wrangle (but there are better, newer options than Coc now that I never explored). Tom MacWright writes about the pain of TypeScript with Neovim in his post, A day using zed.
Notably, I found that my back-end oriented colleagues didn’t have as many issues using vim in a VS Code company, and my hunch is that TypeScript was a big factor.
Extensions
Meanwhile, my VS Code-using colleagues would switch seamlessly between code bases, and maybe install an extension here or there. There was a very well standardised and searchable extension ecosystem with huge numbers of people using and contributing to the extensions.
The size of the community and ecosystem in VS Code is appealing. Working with Neovim has always been the niche option. My coworkers would occasionally share neat extensions like Import Cost and Tailwind’s CSS Intellisense and prettier plugin, and I would just miss out.
Switching projects and tech stack as a vim user
To give you an idea, each of the following required some amount of manual hackery to work in vim: styled-components, Emotion, Tailwind, eslint, prettier, JSX and React, TypeScript, properly structured monorepos, poorly structured monorepos, and so on. Not always huge or insurmountable tasks, but certainly more than my colleagues faced.
Similarly, repos and onboarding docs often came with a .vscode
folder with settings, so being the odd one out would take extra time to get set up.
Switching editor
All in all, I was eager to try a graphical editor popular among my coworkers that would support vim style modal editing. I didn’t consider any other editors because one goal was to be able to pair program with my coworkers and I wanted to be able to use the same editor as them. But with the power of vim!
Switching costs
Every time I tried to switch previously, however, I found the cost of switching too high. As an extremely efficient power user of vim, using VS Code felt like typing with sausages for fingers. Every operation that I previously did with my hands without my brain consciously instructing them would suddenly require multiple new steps:
- make some guesses about what the operation was called
- see if there was a tool built into VS Code for it
- see if there was a setting to operate it
- see if there was an extension for it
- configure each of these
- find the keyboard shortcut or create a key binding for it
- write down the keyboard shortcut to help me learn it
… then finally do the thing I was trying to do!
When you’re used to doing things with your hands without thinking, it feels arduous to use a mouse to perform tasks and to search for commands, let alone work out how to do it all.
Why I finally switched
The main prompt to finally switch was my new work place was using devcontainers in VS Code and I didn’t want to waste time figuring out how to make that work with my vim setup.
Now I’ll get into how I actually made the switch!
Getting started
VSCode Neovim extension
Firstly, I set up the VSCode Neovim extension. It uses “a fully embedded Neovim instance”, not just emulation. It uses VSCode’s native functionality for insert mode and editor commands, “making the best use of both editors”.
I created a separate Neovim init file just for VS Code called vscode.vim
next to my actual Neovim init file for vim, which is called .vimrc
(for historical reasons, but yours might be init.vim
).
A lot of vim behaviours work seamlessly out of the box with the VSCode Neovim extension, and it does a good job of documenting where it does not and what differences you need to consider.
When I started using it, I commented out most of my new vscode.vim
file then walked through and turned parts on as needed.
I kept my leader key defined as space:
" Leader key is space bar:
let mapleader = " "
Vim plugins in VS Code
I disabled most of my usual vim plugins in vscode.vim
so I didn’t have to worry too much about conflicts and to push myself to work out the native VS Code alternatives.
I kept my vim Plug setup, tpope
plugins like vim-surround
and vim-subvert
, and junegunn
plugins like vim-easy-align
. Over time when I missed a vim plugin with vim specific behaviour, I would toggle them back on one at a time.
Using VS Code behaviour in vim code
I have a handful of vim mappings defined in vscode.vim
to help overcome my hard-wired muscle memory. For example:
" If I reflexively hit space a, run ⌘⇧f instead:
nmap <leader>a <Cmd>call VSCodeCall('workbench.action.findInFiles')<CR>
" If I reflexively hit space e, run ⌘⇧e instead:
nmap <leader>e <Cmd>call VSCodeCall('workbench.view.explorer')<CR>
" If I reflexively hit space s, run ⌥⇧f instead:
nmap <leader>s <Cmd>call VSCodeCall('editor.action.formatDocument')<CR>
This is handy for key bindings that use vim behaviour like the leader key, or for incorporating into more complex vim commands.
To figure out what to put in the VSCodeCall()
, you can search the VS Code command palette. Once you have found the name of the command, search for the command name in the VS Code Keyboard Shortcuts (⌘K ⌘S). Then right-click and pick “Copy Command ID”. Pop that text into your vscode.vim
inside the VSCodeCall()
.
Migrating simple shortcuts to VS Code
For simpler behaviour, you can add VS Code key bindings using VS Code directly.
For example, I used to use ⌥j in Neovim in iTerm to move a line down:
" ⌥j on macOS types ∆. We use it here to move a line down:
nnoremap ∆ :m .+1<CR>
To add this shortcut in VS Code:
- I can use ⌘K ⌘S to open Keyboard Shortcuts.
- I search for the command with “move lines down” and discover it’s called
moveLinesDownAction
. - With that command highlighted, I press ⌘K ⌘A to add a new key binding.
- VS Code asks me to type the keyboard shortcut (⌥j) and press Enter.
- I can then “Change When Expression” ⌘K ⌘E if I like e.g.
editorTextFocus && !editorReadonly
.
To add multiple shortcuts to a single command, you can select a command and press ⌘K ⌘A again.
If you prefer editing JSON, you can also search the command palette for Preferences: Open Keyboard Shortcuts (JSON)
to open the keybindings.json
file and add your key binding there:
[
{
"key": "alt+j",
"command": "editor.action.moveLinesDownAction",
"when": "editorTextFocus && !editorReadonly"
}
]
Reducing the UI clutter
If you’re used to a very minimal vim setup, you might find the VS Code User Interface (and its myriad unlabelled icons) overwhelming.
I turned off the activity bar (toggleActivityBarVisibility
) and instead learned shortcuts to each of the sections inside the side bar that I need to make them appear on demand:
View: Toggle Primary Side Bar Visibility
: ⌘BView: Show Explorer
: ⌘⇧ESearch: Find in Files
: ⌘⇧FView: Show Extensions
: ⌘⇧X
For the rare cases that I want the activity bar back, I added ⌃\ as a shortcut to toggle the “Activity Bar” visibility:
[
{
"key": "ctrl+\\",
"command": "workbench.action.toggleActivityBarVisibility"
}
]
I toggle on View: Toggle Panel Visibility
(⌘J) whenever I need to see the terminal and then collapse it again.
Because of how I toggle these panels, I tend to keep them obscenely large so that when the panels are open, I can read full file paths in the explorer and see the full terminal output.
I also leave the sticky scroll (@command:editor.action.toggleStickyScroll
) off most of the time and toggle it on using a custom shortcut ⌘Y when I need it:
[
{
"key": "cmd+y",
"command": "editor.action.toggleStickyScroll"
}
]
Enjoying VS Code features
I want to share some of my favourite features in VS Code so far!
Refactoring shortcuts mostly just work, including refactoring TypeScript. Symbol renaming works across files. Extract function can be handy. I had previously had mixed success achieving such things in vim with Coc.
Auto Imports are super convenient e.g. ⌘. to bring up the “Quick Fix” list of JS/TS refactorings:
Organize Imports (⇧⌥O) is like Prettier for imports.
I also enjoy how keyboard shortcuts are listed in the command palette and popovers. The visibility is handy for learning or reminding yourself of new shortcuts.
I don’t need to manually create tags
files to navigate codebases. Shortcuts like go to definition, implementation, and references work well, and I can even use vim shortcuts like gd
or ⌃]
to operate them.
Modal editing and Copilot
GitHub Copilot came to VS Code as the Copilot extension before Copilot came to Neovim but it did get to Neovim too, eventually.
If you’ve read this far you’re probably aware that vim has modal editing meaning in “normal” mode I can navigate around using regular keys, delete or copy text, and so on, while in “insert” mode I can type new text.
Using GitHub Copilot to predict text can sometimes be distracting if it’s predicting something irrelevant while I’m trying to think. So I hit escape to leave insert mode while I ponder what I need to write in peace. When I’m ready to type, I go back to insert mode. I understand that other people just ignore copilot’s suggestions. Some folk also like to pop Copilot out into a new tab with multiple additional options using Ctrl+Enter.
VS Code reload window
Sometimes if the VSCode Neovim extension hits an error for whatever reason, it can get confused and the vim behaviour gets out of sync so you’re effectively typing all over the place. At this point, absolutely do not press the vim undo shortcut as it also has no idea what’s going on and mangles the text further. Instead, call VS Code’s reload window command. It reboots the extension without losing what you have typed, even if the file is unsaved. Or maybe try restarting the Neovim extension from the command palette.
The reload window command does not affect any processes you have running in any VS Code terminals. I’ve also seen VS Code users run this command at the drop of a hat any time something is vaguely misbehaving so I don’t think this is necessarily a failure of the Neovim extension.
Power users
I’ve noticed with Neovim that the tool itself attracts more of a keyboard-oriented, power user community. The learning curve requires that you are really committed to figuring out how to work efficiently. While I know a lot of people that use VS Code, I don’t know many that have delved deeply into all of its features. So when I ask for ideas on how to do something that I’m used to doing fast in vim, they often don’t know how to do that task efficiently in VS Code. They’re content with using their mouse, searching the command palette every time, navigating manually through the file explorer, or just typing things out slowly.
This contributed to the high switching cost for me that meant I didn’t make the switch years ago. I hope this post can help you make the switch faster if you want.
Using both in parallel
Sometimes things are still just easier in Neovim. For substitutions or complex macros, I sometimes switch to Neovim.
I also continue to use Neovim for editing my personal notes files. No one is stopping you from using multiple editors!
VS Code features I don’t use
I still use git/tig in iTerm for the majority of git
operations. I like the single-line staging you can do with tig
. I’ve also yet to see any compelling reason to use the VS Code git features, and skipping them cuts down on how much VS Code I have to learn at once.
By certain flukes, I haven’t needed to resolve any merge conflicts in the last few months. I expect I’d still use vimdiff
or opendiff
or whatever, but it might depend on how much time/capacity I have to learn and navigate a new interface next time I have to deal with large conflicts.
Settings
Here are some settings I’ve changed in VS Code to get you started:
- Set
Editor: Line Numbers
(@id:editor.lineNumbers
) to “relative” in Settings to make vim line navigation easier. - Set Terminal › Integrated: Confirm On Exit to control whether to confirm when the window closes if there are active terminal sessions.
Extensions
Here are some extensions I’ve found useful:
- VSCode Neovim extension (obviously)
- Code Spell Checker to spell check code and prose
- Git Lens to see git blame and history
- Import Cost to see the size of imports
- Rainbow CSV for working with CSV/TSV files
Random tips
- ⌘K ⌘S to open Keyboard Shortcuts
- ⌘. to bring up the quick fix suggestions
- K to show the hover widget, which can be handy for seeing documentation, types, function signature, or errors
- ⌃space to “invoke Intellisense” and bring up a suggestions window
- You can ⌘ click all sorts of things to follow them somewhere useful.
I also collected a bunch more shortcuts in that format (shortcut then concise, personal description) to refer back to when I forget them. I encourage you to do the same!
I like using space 1, space 2, and so on to jump to specific editor tabs. You can set this up in your vscode.vim
:
" Leader key is space bar:
let mapleader = " "
nmap <leader>1 <Cmd>call VSCodeCall('workbench.action.openEditorAtIndex1')<CR>
nmap <leader>2 <Cmd>call VSCodeCall('workbench.action.openEditorAtIndex2')<CR>
nmap <leader>3 <Cmd>call VSCodeCall('workbench.action.openEditorAtIndex3')<CR>
Depending on your setup, you might consider globally ignoring .vscode/settings.json
files in your global config .gitignore
file so that you can customise whatever you like without affecting your team’s settings:
# Ignore VS Code workspace settings
.vscode/settings.json
Some things I haven’t worked out yet
If you have solutions to these problems, please let me know!
- In VS Code the Neovim extension lets you run substitution commands, but if you want to edit a previously run substitution you can’t just press the up key to see the previous substitution and tweak it. If you know how to make this work, please let me know! Update! ⌃p/⌃n work for this. Thank you, weaksauce on Lobsters.
- I used to toggle relative line numbers on and off when screen sharing with colleagues to make it easier for them to follow along. While I can adjust the setting in VS Code settings, I haven’t worked out how to toggle it quickly with a key binding yet. Update! There are some extensions to toggle or cycle settings, such as Toggle by Peng Lv that Brian Schiller shared with me. Here’s how I have configured the Toggle extension to switch the line numbers setting from “relative” to “on”:
{ "key": "alt-l", "command": "toggle", "when": "editorTextFocus", "args": { "id": "lineNumbers", "value": [ { "editor.lineNumbers": "on" }, { "editor.lineNumbers": "relative" } ] } },
- On rare occasions,
cit
to change inside a HTML tag eats up more than just the current tag but a level or two higher. I haven’t worked out why yet. - Can I have a cspell user dictionary that doesn’t clutter up my user
settings.json
file with words, and doesn’t live in a specific project/workspace? - Only in MDX files, sometimes an unsaved duplicate of the current file will appear in a new tab. I’m not sure how and when this happens!
- My colleagues say “press F12” or whatever and I have no idea what that means because I use vim shortcuts. Just kidding!
If you have feedback, send me an email at [email protected]