Moving to init.lua on Neovim 0.11 and the E5422 Config Conflict

Migrating from init.vim to init.lua in Neovim 0.11: Complete Guide

* This page contains promotional content

I migrated the NeoVim configuration I set up in Reviewing my Zsh and NeoVim configuration over to Lua.

TL;DR

  • Neovim 0.11.5 assumes Lua, so moving from init.vim + vim-plug toward init.lua + lazy.nvim gives you a big return.
  • For completion, blink.cmp is a strong choice these days.
  • The 0.11 trap: init.lua and init.vim cannot live side by side (E5422: Conflicting configs). A staged migration takes some ingenuity.
  • While I was at it: NERDTree→oil.nvim, hand-written brackets→nvim-autopairs, IME→Lua autocmd, cursor shape→guicursor.

Background

In the Neovim 0.11 generation, the ecosystem of LSP, diagnostics and the surrounding plugins moves ahead on the premise of Lua. If you keep maintaining a Vimscript-centred setup,

  • you get little benefit from the new configuration APIs
  • the gap with the plugins’ samples and documentation keeps widening
  • narrowing things down when there is trouble gets slower

and you end up in that kind of slow, creeping defeat.

The 0.11 pitfall: init.lua and init.vim cannot coexist

On 0.11, if ~/.config/nvim/init.lua and ~/.config/nvim/init.vim exist at the same time, startup stops with E5422: Conflicting configs.

In other words, the classic plan of “create init.lua, source init.vim from it, and migrate in stages” falls apart as it is.

Where I landed this time:

  • Throw away init.vim (delete it on the repo side too)
  • Port the behaviour I need over to Lua
  • The legacy.vim used for temporary stashing was in the end emptied out and deleted

Structure

  • nvim/init.lua: the entry point (lazy bootstrap + loading core)
  • nvim/lua/plugins.lua: where the lazy.nvim specs are collected
  • nvim/lua/core/options.lua: options / guicursor / IME autocmd
  • nvim/lua/core/keymaps.lua: keymaps
  • nvim/lua/core/diagnostics.lua: diagnostic settings + LspAttach

Key points of the implementation

1) Bootstrapping lazy.nvim in init.lua

  • Clone lazy.nvim if it is not there
  • Load core/* and plugins
  • A simple hot reload that re-reads the Lua on save

2) Plugins managed by lazy.nvim

  • telescope.nvim
  • nvim-treesitter
  • copilot.lua
  • CopilotChat.nvim
  • blink.cmp
  • nvim-lspconfig (passes blink’s capabilities to the LSP)

And to bring legacy down to zero:

  • oil.nvim (NERDTree replacement)
  • nvim-autopairs (replacement for the hand-written bracket completion)

3) Settings that pay off on 0.11 (diagnostic/LspAttach)

  • Spell out vim.diagnostic.config in Lua
  • Buffer-local keymaps on LspAttach (hover/definition/rename/code action/diagnostic nav/format)

4) Bringing legacy to zero (Oil/Autopairs/IME/Cursor)

  • NERDTree→Oil: assign <C-t> to :Oil and the migration cost is zero
  • Bracket completion: stop the hand-written inoremap and use nvim-autopairs
  • Cursor shape: stop the t_SI/t_EI hack and use guicursor
  • IME: call im-select from a Lua autocmd (asynchronously with vim.system() in 0.11)

5) The <Tab> problem (Copilot vs completion)

<Tab> tends to collide between Copilot, the completion UI and the pum.

This time I settled on the behaviour of “accept with <Tab> only while a Copilot ghost suggestion is visible, and fall back to the pum or a normal Tab otherwise”.

Operational cleanup (the junk that tends to be left behind after migrating)

  • Delete the old copilot.vim (~/.config/nvim/pack/...) to avoid double loading
  • Delete ~/.config/nvim/plugged/ (the vim-plug leftovers) too
  • Delete the broken ~/.vimrc symlink as well (a source of noise when Vim starts)

See also