2026.04.27: By forge+kimi2.6

Lua LSP Not Auto-Attaching: Root Cause Analysis

Symptom

When opening a .lua file directly from the command line (nvim main.lua), the Lua LSP (emmylua_ls) did not attach automatically, even after waiting. However, running :e inside Neovim caused the LSP to attach immediately.

How the Cause Was Found

1. Verified the obvious first

  • emmylua_ls binary was present and executable in the nix wrapper PATH.
  • vim.lsp.config('emmylua_ls') was registered with correct settings.
  • vim.lsp.enable('emmylua_ls') was called in the active init file.
  • Running nvim main.lua in headless mode showed 0 clients attached initially.
  • Running :e and then checking showed emmylua_ls attached.

This confirmed the issue was specifically about startup timing, not missing configuration or a broken LSP binary.

2. Isolated the config with a minimal reproducer

Created a minimal init.lua that only did vim.lsp.config(...) + vim.lsp.enable(...) and tested it with nvim -u /tmp/minimal_init.lua main.lua. In this minimal setup, the LSP attached correctly.

This proved the issue was caused by something else in the user's full configuration.

3. Bisected the vimscript config

The user's init.lua is generated by home-manager, which sources a large vimscript block (extraConfig) before running the Lua block (extraLuaConfig).

By sourcing chunks of the vimscript config incrementally, the trigger was isolated to a single line in the vimscript block.

4. Identified the exact trigger

The trigger was syntax on in the vimscript extraConfig.

Root Cause

syntax on sources Neovim's runtime/syntax/syntax.vim, which contains:

" runtime/syntax/syntax.vim:26
filetype on
" ...
" runtime/syntax/syntax.vim:43-44
if !s:did_ft
  doautoall filetypedetect BufRead
endif

doautoall filetypedetect BufRead fires FileType for all already-existing buffers during init.lua sourcing.

Because home-manager places extraConfig (vimscript) before extraLuaConfig (Lua), the startup sequence becomes:

  1. Buffer main.lua is created during startup.
  2. syntax on triggers doautoall filetypedetect BufRead.
  3. FileType lua fires for main.lua.
  4. The nvim.lsp.enable autocmd does not exist yet, so nothing attaches.
  5. Later, vim.lsp.enable('emmylua_ls') runs in the Lua section.
  6. runtime/lua/vim/lsp.lua:635 checks vim.v.vim_did_enter == 1. It is 0 during init, so the doautoall catch-up is skipped.
  7. The FileType autocmd is created, but only fires for future FileType events.
  8. main.lua is permanently missed until :e re-triggers FileType.

Why :e Fixed It

:e re-triggers BufRead -> FileType for the current buffer. By then, vim.lsp.enable() has already been called, the nvim.lsp.enable autocmd exists, and the LSP attaches normally.

Why Removing syntax on Is the Correct Fix

Neovim's startup code in src/nvim/main.c:454-462 is explicitly designed to enable syntax after init.lua finishes:

source_startup_scripts(&params);
// ...
filetype_maybe_enable();
// Sources syntax/syntax.vim. We do this *after* the user startup scripts
// so that users can disable syntax highlighting with `:syntax off` if they wish.
syn_maybe_enable();

By putting syntax on in init.lua, you fight against Neovim's design:

  • It is redundant — Neovim enables syntax automatically after init.
  • It is harmful — it causes premature FileType events before Lua config runs.

References

  • runtime/syntax/syntax.vim:26,43-44
  • runtime/lua/vim/lsp.lua:610-637
  • src/nvim/main.c:454-462
  • src/nvim/syntax.c:3195-3202

Note on the "Neovim Bug" Misconception

This behavior is sometimes described as a "Neovim 0.11 bug" (e.g. issue #32910), but issue #32910 is actually an unrelated PR about codeAction/resolve support. The real issue here is a configuration ordering problem caused by syntax on in init, not a bug in vim.lsp.enable() itself.