Configuring Neovim
This skill provides patterns for modifying and testing Neovim configuration safely and efficiently.
What This Skill Does
- Tests nvim configuration changes without manual launch
- Verifies plugins load correctly via headless testing
- Configures LSP servers with mason.nvim
- Debugs startup errors and plugin loading issues
- Tests LSP attachment and functionality
- Manages plugin configuration with lazy.nvim
Key Technique: Headless Testing
The critical pattern for working with nvim configuration is testing headlessly before manually launching nvim. This catches errors immediately without disrupting your workflow.
Basic Headless Test
# Test if nvim loads without errors
nvim --headless -c "echo 'Config loaded successfully'" -c "quit" 2>&1
What this does:
--headless: Run without UI-c "command": Execute vim command-c "quit": Exit after commands2>&1: Capture stderr to see errors
Success output:
Config loaded successfully
Error output shows:
- File and line number where error occurred
- Stack trace
- Specific error message
Testing Specific Features
# Test plugin loading
nvim --headless -c "lua print('Plugins loaded')" -c "sleep 2" -c "quit" 2>&1
# Test LSP configuration
nvim --headless -c "lua print('LSP config:', vim.inspect(vim.lsp))" -c "quit" 2>&1
# Test with a specific file type
cd /tmp && echo 'print("test")' > test.lua && \
nvim --headless test.lua -c "lua print('Buffer filetype:', vim.bo.filetype)" -c "quit" 2>&1
Testing LSP Server Attachment
# Test if LSP attaches to a buffer (requires servers installed)
cd /tmp && echo 'print("hello")' > test.lua && \
nvim --headless test.lua \
-c "lua vim.defer_fn(function() print('LSP clients:', vim.inspect(vim.lsp.get_clients())) end, 2000)" \
-c "sleep 3" \
-c "quit" 2>&1 | grep -A5 "LSP clients"
Why defer_fn? LSP servers need time to start and attach. The 2000ms delay allows initialization.
Prerequisites
Required Tools
- nvim - Neovim 0.9+
- git - For plugin installation
Configuration Location
- Main config:
~/.config/nvim/init.lua - Plugins:
~/.config/nvim/lua/plugins/*.lua - User config:
~/.config/nvim/lua/user/*.lua - Plugin manager:
~/.config/nvim/lua/config/lazy.lua
Common Configuration Tasks
Adding a New Plugin
- Create plugin file in
~/.config/nvim/lua/plugins/:
-- ~/.config/nvim/lua/plugins/my-plugin.lua
return {
"username/plugin-name",
lazy = false, -- Load immediately (or set to true for lazy loading)
config = function()
require("plugin-name").setup({
-- Plugin configuration here
})
end,
}
- Test the plugin loads without errors:
nvim --headless -c "lua print('Testing plugin load')" -c "sleep 2" -c "quit" 2>&1
- Launch nvim normally - lazy.nvim will auto-install the plugin
Configuring LSP with Mason
The modern approach uses three plugins:
mason.nvim- Installs language serversmason-lspconfig.nvim- Bridges mason and lspconfignvim-lspconfig- Configures LSP clients
Recommended structure (~/.config/nvim/lua/plugins/lsp.lua):
return {
-- Mason for installing language servers
{
"williamboman/mason.nvim",
lazy = false,
priority = 1000,
config = function()
require("mason").setup()
end,
},
-- LSP configuration (load early as dependency)
{
"neovim/nvim-lspconfig",
lazy = false,
},
-- Mason-lspconfig bridge
{
"williamboman/mason-lspconfig.nvim",
lazy = false,
dependencies = {
"williamboman/mason.nvim",
"neovim/nvim-lspconfig",
"hrsh7th/cmp-nvim-lsp",
},
config = function()
-- Setup mason-lspconfig
require("mason-lspconfig").setup({
ensure_installed = {
"lua_ls", -- Lua
"pyright", -- Python
"ts_ls", -- TypeScript/JavaScript
"rust_analyzer", -- Rust
-- Add more servers as needed
},
})
-- Define on_attach for keymaps
local on_attach = function(_, bufnr)
local map = function(mode, lhs, rhs, desc)
vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, silent = true, desc = desc })
end
-- Navigation
map("n", "gd", vim.lsp.buf.definition, "Go to definition")
map("n", "gD", vim.lsp.buf.declaration, "Go to declaration")
map("n", "gr", vim.lsp.buf.references, "Go to references")
map("n", "K", vim.lsp.buf.hover, "Hover documentation")
end
-- Get capabilities
local capabilities = require("cmp_nvim_lsp").default_capabilities()
-- Configure each server
local lspconfig = require("lspconfig")
lspconfig.lua_ls.setup({
on_attach = on_attach,
capabilities = capabilities,
settings = {
Lua = {
diagnostics = { globals = { "vim" } },
},
},
})
-- Add more server configs as needed
end,
},
}
Test LSP config loads:
nvim --headless -c "lua print('Testing LSP')" -c "sleep 2" -c "quit" 2>&1
Common errors:
attempt to call field 'setup_handlers'- Don't usesetup_handlers, configure servers manuallymodule 'cmp_nvim_lsp' not found- Add nvim-cmp as dependency- Priority issues - Set explicit
priorityvalues (1000, 900, 800)
Modifying Existing Configuration
- Read the current config:
cat ~/.config/nvim/lua/plugins/target-plugin.lua
-
Make your changes using the Edit tool
-
Test headlessly immediately:
nvim --headless -c "lua print('Config updated')" -c "quit" 2>&1
-
If errors appear, fix them before launching nvim normally
-
Launch nvim to verify behavior
Verification Workflows
After Plugin Changes
# Check plugin loads
nvim --headless -c "lua print('Plugins OK')" -c "sleep 2" -c "quit" 2>&1
# Check for errors (should be empty)
nvim --headless -c "quit" 2>&1 | grep -i error
After LSP Changes
# Verify LSP config loads
nvim --headless -c "lua print('LSP:', vim.inspect(vim.lsp))" -c "quit" 2>&1
# Test with actual file (after servers installed)
cd /tmp && echo 'def hello(): pass' > test.py && \
nvim --headless test.py -c "sleep 3" -c "lua print('Clients:', #vim.lsp.get_clients())" -c "quit" 2>&1
After Keymap Changes
# Check specific keymap exists
nvim --headless -c "lua print('Leader key:', vim.g.mapleader)" -c "quit" 2>&1
Troubleshooting
Error: Failed to run config for plugin
Symptom: Error during plugin config function execution
Cause: Lua error in plugin's config = function() block
Solution:
- Look at the stack trace for file:line number
- Read the specific line in the config file
- Common issues:
- Calling function that doesn't exist yet (dependency loading order)
- Typo in module name
- Missing
require()statement
Fix dependency loading:
-- Add explicit dependencies and priority
{
"my-plugin",
dependencies = { "required-plugin" },
priority = 900, -- Lower number = loads later
}
Error: Module not found
Symptom: module 'xyz' not found
Cause: Plugin not installed or wrong name
Solution:
# Check if plugin directory exists
ls ~/.local/share/nvim/lazy/
# Launch nvim normally to let lazy.nvim install
nvim
# Or force plugin sync
nvim --headless -c "lua require('lazy').sync()" -c "sleep 5" -c "quit"
Error: Attempt to call field (a nil value)
Symptom: attempt to call field 'setup' (a nil value)
Cause:
- Function doesn't exist in that module
- Module not loaded yet
- Wrong module name
Solution:
- Verify correct module name in plugin docs
- Check if module needs to be required first
- Ensure dependencies load before this plugin
LSP Not Attaching
Symptom: LSP features don't work, no diagnostics
Diagnosis:
# Check if LSP clients exist
nvim some-file.lua -c "lua vim.defer_fn(function() print(vim.inspect(vim.lsp.get_clients())) end, 2000)" -c "sleep 3" -c "quit"
Common causes:
- Server not installed - Run
:Masonin nvim to install - Server not configured - Add to lspconfig setup
- File type not detected - Check
:set filetype?in nvim - Server crashed - Check
:LspInfofor errors
Solution:
# Verify mason installed servers
ls ~/.local/share/nvim/mason/bin/
# Check server executable works
~/.local/share/nvim/mason/bin/lua-language-server --version
Deprecation Warning: lspconfig framework deprecated
Symptom: Warning about require('lspconfig') being deprecated
Cause: Neovim 0.11+ has new LSP config API
Impact: Warning only - still works fine
Future fix: Migration guide will be available when nvim-lspconfig v3.0.0 releases
Performance: Slow Startup
Diagnosis:
# Profile startup time
nvim --startuptime startup.log -c "quit"
cat startup.log | tail -20
Common causes:
- Too many plugins loading at startup
- Heavy plugins not lazy-loaded
- Expensive config functions
Solution:
-- Lazy load plugins
{
"heavy-plugin",
lazy = true, -- Don't load at startup
event = "VeryLazy", -- Load after UI renders
-- or
cmd = "PluginCommand", -- Load on command
-- or
ft = "python", -- Load on filetype
}
Best Practices
Always Test Headlessly First
# GOOD: Test before manual launch
nvim --headless -c "echo 'OK'" -c "quit" 2>&1 && echo "Safe to launch nvim"
# BAD: Edit config, launch nvim, hit error, can't see anything
nvim # Might crash immediately
Use Explicit Loading Order
-- For plugins that depend on each other, set priorities
{
"base-plugin",
priority = 1000, -- Loads first
}
{
"dependent-plugin",
priority = 900, -- Loads second
dependencies = { "base-plugin" },
}
Keep Configs Modular
~/.config/nvim/
├── init.lua # Entry point (minimal)
├── lua/
│ ├── config/
│ │ └── lazy.lua # Plugin manager setup
│ ├── plugins/ # One file per plugin
│ │ ├── lsp.lua
│ │ ├── telescope.lua
│ │ └── treesitter.lua
│ └── user/ # User settings
│ ├── mappings.lua
│ └── settings.lua
Benefits:
- Easy to find/edit specific plugin config
- Can remove plugins by deleting one file
- Clear separation of concerns
Document Your Configurations
-- Good: Explain why
{
"williamboman/mason.nvim",
priority = 1000, -- Must load before mason-lspconfig
lazy = false, -- Required at startup for LSP
config = function()
require("mason").setup()
end,
}
-- Bad: No context
{
"williamboman/mason.nvim",
config = function()
require("mason").setup()
end,
}
Test in Clean Environment
When debugging mysterious issues:
# Start nvim with no config
nvim -u NONE
# Start with minimal config
echo "vim.opt.number = true" > /tmp/minimal.lua
nvim -u /tmp/minimal.lua
Back Up Before Major Changes
# Before restructuring LSP config
cp ~/.config/nvim/lua/plugins/lsp.lua ~/.config/nvim/lua/plugins/lsp.lua.backup
# Or use git
cd ~/.config/nvim
git diff # Review changes
git checkout -- file.lua # Revert if needed
Common Patterns
Testing a Complete Config Change
# 1. Make your changes
# 2. Test headlessly
nvim --headless -c "quit" 2>&1
# 3. If errors, see the stack trace and fix
# 4. When clean, test with a file
cd /tmp && echo 'test' > test.txt && nvim --headless test.txt -c "quit" 2>&1
# 5. Launch normally
nvim
Adding a Language Server
# 1. Add to ensure_installed in lsp.lua
# 2. Add server config in lsp.lua
# 3. Test config loads
nvim --headless -c "quit" 2>&1
# 4. Launch nvim - mason auto-installs
nvim
# 5. Wait for mason to finish installing
# 6. Open a file of that language
# 7. Verify LSP attached: :LspInfo
Removing a Plugin
# 1. Delete plugin file
rm ~/.config/nvim/lua/plugins/unwanted-plugin.lua
# 2. Test config loads
nvim --headless -c "quit" 2>&1
# 3. Launch nvim
nvim
# 4. Clean up plugin directory
# In nvim: :Lazy clean
Quick Reference Commands
# Test config loads
nvim --headless -c "quit" 2>&1
# Test with delay for async operations
nvim --headless -c "sleep 2" -c "quit" 2>&1
# Test LSP configuration
nvim --headless -c "lua print(vim.inspect(vim.lsp))" -c "quit" 2>&1
# Test with specific file type
nvim --headless test.lua -c "quit" 2>&1
# Check plugin installation
ls ~/.local/share/nvim/lazy/
# Check LSP servers installed
ls ~/.local/share/nvim/mason/bin/
# Profile startup time
nvim --startuptime startup.log -c "quit" && tail -20 startup.log
# Launch with no config (debugging)
nvim -u NONE
# Force plugin sync
nvim --headless -c "lua require('lazy').sync()" -c "sleep 10" -c "quit"
Related Skills
- Creating Claude Code Skills - For documenting reusable nvim patterns as skills
- Git Workflows - For version controlling nvim config changes
Advanced Topics
Testing Specific Plugin Loading
# Test if specific plugin is loaded
nvim --headless -c "lua print('Telescope:', require('telescope') ~= nil)" -c "quit" 2>&1
Debugging Plugin Load Order
# Add prints in config functions to trace loading
config = function()
print("Loading plugin X...")
require("plugin").setup()
print("Plugin X loaded")
end
Testing Keymaps Work
# Open file and try keymap programmatically
nvim --headless test.lua \
-c "normal gd" \
-c "echo 'Keymap executed'" \
-c "quit" 2>&1
This pattern lets you verify keymaps trigger without manual testing.
Summary
The most important practice: always test nvim config changes headlessly before launching nvim manually. This catches errors immediately and shows you exactly where the problem is.
The basic workflow:
- Edit config
nvim --headless -c "quit" 2>&1- Fix any errors shown
- Repeat until clean
- Launch nvim normally
This saves enormous amounts of time and frustration compared to trial-and-error with manual launches.
Scan to join WeChat group