From VS Code to Neovim

A summary of my switch to Neovim as my main editor

December 15, 2024

For the past eight or so years, I had been using VS Code as my only editor both personally and professionally. VS Code is built on Electron , and while I’m generally not a fan of desktop software being built on web technologies, I think Microsoft did a pretty good job of squeezing as much performance as they reasonably could out of Electron. There are also some pretty clear advantages to building a code editor on Electron, such as strong cross-platform support, high development speed, and a very accessible environment for community-made extensions.

That last point is especially important. There are extensions to support almost any type of programming workload. Because of that, I got very comfortable using it for a wide range of purposes, including:

  • Full-stack web development with HTML, JS, CSS, and PHP
  • C
  • C++ (including Unreal Engine integration)
  • C# (including Unity integration)
  • Godot
  • Rust
  • Odin

That’s without mentioning the various other formats and languages I probably worked with on occasion. It was very nice not to have to think about being proficient in multiple editors!

Why I switched

The main reason I felt the need to switch to something else was performance. In my job, I work mainly with C++ in a moderately large codebase. It’s nowhere near the size of some of the codebases in Big Tech, but big enough to present a challenge to C++ tooling in terms of efficiency. There came a point where the speed of C++ features provided by Microsoft’s C/C++ extension gradually degraded from acceptable to prohibitively slow (for example, CTRL-clicking a function name to navigate to its definition would often take around 10 seconds or longer).

Looking into the issue, I found that some part of MSVC was generating a huge database of symbols, and updating it to match changes I made in the code was taking a matter of seconds rather than milliseconds. Whether this was the actual problem or only a symptom of it, I’m not sure, but it definitely didn’t seem right.

Not wanting to spend too much time identifying the exact problem, I decided to try the clangd extension instead. This appeared to perform very well initially only to slow down just as much as the MSVC extension after working with the code for a while. At one point I noticed it had generated a compile_commands.json over 1 GB in size! I’m sure you can imagine 1 GB of JSON is a bit of a bottleneck no matter how fast the tools using it may be…

To give some context, in order for a code editor to understand a C++ project and provide features such as autocompletion, it relies on external tools to analyse the code and generate metadata that describes it. One such example is the compile_commands.json specification , a compilation database format for describing how each translation unit in a given project is compiled. A compile_commands.json file can be generated in various ways (e.g. via CMake ) and can then be ingested by tools to understand things like include paths.

What I use now

While I would love to use Neovim at work, setting it up to work nicely with the build workflows we use there would take more time than I could justify investing, and I’m simply not proficient enough with the vim motions yet to work at a good pace. So, as a compromise, I switched to Rider with the IdeaVim plugin , which lets me dip into vim motions where I feel comfortable and still be able to fall back to the conventions of a graphical editor that vaguely resembles VS Code.

Interestingly, the performance issues went away with Rider. I’m not sure why, but whatever it does differently to achieve the same functionality I was getting in VS Code seems to be much more efficient. Maybe one day I’ll experiment with a large open source project in my spare time to see if I can figure out those differences.

For everything else, I went all in on Neovim. I don’t care so much if I work slowly on personal projects as long as the editor I’m using has good support for them, so I went on a mission to create a configuration that would support all of the languages and game engines I mentioned at the start of the post using kickstart.nvim as a starting point:

My configuration

I won’t cover my config in depth - it’s here if you want to see it in detail. Some of it hasn’t changed much (or at all) from kickstart anyway. Instead, I’ll go over some key plugins and external tools to add both language-agnostic features as well as environment-specific ones.

First, the language-agnostic stuff:

Below is an overview of the environments I mentioned at the start of the post and how I added support for them.

C and C++

For most of my standalone C and C++ projects, I use CMake, clangd, and LLDB. To manage common CMake tasks, I use Shatur/neovim-tasks with some keybinds for the configure/debug/run tasks:

vim.keymap.set('n', '<leader>cc', ':Task start cmake configure<CR>')
vim.keymap.set('n', '<leader>cd', ':Task start cmake debug<CR>')
vim.keymap.set('n', '<leader>cr', ':Task start cmake run<CR>')

For LLDB support, I use codelldb - this is a VS Code extension, but you can configure an nvim-dap adapter for it. See this nvim-dap wiki page for specifics.

HTML, CSS, JS, and PHP

HTML, CSS, and JS are nice to work with out of the box with kickstart. I don’t have anything fancy configured for them - not even completion, just syntax highlighting. I’ll probably expand my config when I next work on a big JS/TS project to provide better support for it, especially for debugging.

For PHP specifically, I use php-debug-adapter (installed via Mason with jay-babu/nason-nvim-dap.nvim ) for debugging and phpactor (via neovim/nvim-lspconfig ) for completion and linting.

Unreal Engine

I have Unreal Engine support partially working on Windows mainly thanks to two excellent projects:

The main issue I have with this setup is the limited debugger support. “Attach debugger” seems to be the only launch configuration that works, but it requires the full path to UnrealEditor.exe to be entered every time it’s used. Here are a few observations I made while investigating this:

  • nvim-dap doesn’t read launch configurations embedded in VS Code workspace files, only those defined in .vscode/launch.json.
  • I verified that copying the existing DebugGame launch configuration generated by Unreal, changing its type to lldb (from the codelldb extension), and setting request to attach does work. The codelldb binary spams the log with [ERROR codelldb::debug_event_listener] Event listener: Could not send event: "Full(..)" - this seems to have something to do with the number of modules being loaded, but it works.
  • Copying the modified configuration into .vscode/launch.json and using it in Neovim does work. There is a delay before it attaches to UnrealEditor.exe, and there’s some weirdness around breakpoints not being verified or hit initially, but it does start working after that.

So it’s not perfect, but good enough to be productive. Nonetheless I may end up deciding to use Rider for Unreal Engine since it became free for personal use.

Unity

For Unity, I have code analysis and completion via seblj/roslyn.nvim . Unfortunately I haven’t yet found a working debugger integration, which is a bit of a dealbreaker for me. I’ll look into this again soon. Thankfully I don’t work with Unity much at the moment, so it’s not a high priority.

Godot

Since Godot ships with both an LSP server and DAP server, it’s fairly easy to configure Neovim to use them via nvim-lspconfig and nvim-dap. I basically followed this excellent guide from the r/neovim subreddit: Godot/GDScript in Neovim with LSP and debugging in 2024 - the right way

Odin

The Odin compiler produces debug binaries that are compatible with common C debuggers, so my setup for C works for Odin too.

For code analysis/completion, while there is a decent community-developed Odin language server , I’ve opted not to use it. This choice is motivated by the views of Odin’s author . He believes relying on a language server gets in the way of learning the language (particularly with respect to learning how to write in it idiomatically rather than just writing code that works). I think there’s probably some truth in that, so I’ve been writing Odin armed only with the official docs and demo code.

Rust

There’s good support for Rust and its tooling via nvim-lspconfig (rust_analyzer). For debugging, since Rust also follows the same formats as C and C++ for debugging information, I use LLDB with the same configuration.

Although I haven’t tried it yet, there’s also mrcjkb/rustaceanvim , which seems to provide excellent Rust support with minimal effort.


That covers everything. Now to get back to Advent of Code 2024 , which has greatly accelerated my adjusting to Neovim!