| name | Configuring Neovim |
| description | This skill guides working with Neovim configuration including testing changes headlessly, managing plugins with lazy.nvim, configuring LSP servers, and troubleshooting startup errors. Use this when modifying nvim config files or debugging nvim issues. |
| allowed-tools | Bash, Read, Write, Edit |
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.