Reference guide for developing Neovim plugins using Lua, Nix, and plenary.nvim. Load this skill when working on an existing Neovim plugin project.
This skill provides guidance for ongoing development of Neovim plugins using Lua, Nix flakes, and plenary.nvim for testing.
Use this skill when:
Lua modules should follow this pattern:
local M = {}
---@type string|nil
M.property = nil
--- Setup the module
---@param opts table?
---@return table
function M.setup(opts)
opts = opts or {}
-- initialization logic
return M
end
return M
Key points:
M.setup() returns M (the module itself)M after setupRun tests with:
nix run .#nvim-test -- -u scripts/minimal-init.lua --headless -c 'PlenaryBustedFile tests/lua/path/to_spec.lua' -c 'qa!'
Always use:
-u scripts/minimal-init.lua - minimal init for testingPlenaryBustedFile - run specific test filePlenaryBustedDirectory - run all tests in directory-c 'qa!' - exit after tests complete---
-- Tests for modulename.core
-- @module modulename.core_spec
local lua_module = require("modulename.core")
describe("modulename.core", function()
before_each(function()
lua_module.property = nil -- reset state
end)
it("should do something", function()
lua_module.setup({ option = "value" })
assert.are.equal("expected", lua_module.property)
end)
end)
Key points:
tests/lua/ mirroring lua/ structurelua/modulename/core.lua → tests/lua/modulename/core_spec.luabefore_eachlua_module as the require result variable nameluacheck before committing (vim global warnings are expected)stylua before committingLinting command:
nix develop -c luacheck lua/ tests/
Formatting command:
nix develop -c stylua lua/ tests/
Requiring third-party modules at import time
Over-mocking in unit tests
test_root/)Placing test files outside tests/lua/ structure
lua/ directory structurelua/modulename/file.lua → tests/lua/modulename/file_spec.luaWhen building a Neovim plugin, you often need to reference internal APIs, vim functions, and configuration options. Here are the best ways to access Neovim documentation:
If you know the Neovim installation location, documentation files are stored under runtime/doc/:
# Find Neovim runtime path
nix run .#nvim-test -- --headless -c 'echo $VIMRUNTIME' -c 'qa!' 2>&1 | head -1
# Common locations in Nix store
ls $(nix build .#nvim-test --no-link --print-out-paths)/share/nvim/runtime/doc/
Documentation files use the .txt extension (e.g., api.txt, lua.txt, options.txt).
For quick reference without starting Neovim:
Vim documentation (HTML): https://vimdoc.sourceforge.net/htmldoc/
Browse categories like:
eval.html - Vimscript functions and expressionsoptions.html - Configuration optionsautocmd.html - Autocommandsmap.html - Key mappingsNeovim-specific docs: https://neovim.io/doc/user/
api.html - Lua API functionslua.html - Lua scripting guidelsp.html - LSP client APItreesitter.html - Treesitter integrationQuery documentation directly from the command line:
# View help for a specific topic and output to stdout
nix run .#nvim-test -- --headless -c "h quickfix" -c ":!cat %" -c "qa" 2>/dev/null
# Search for a pattern in help files
nix run .#nvim-test -- --headless -c "helpgrep lua_api" -c "cfirst" -c ":!cat %" -c "qa" 2>/dev/null
# List all functions matching a pattern
nix run .#nvim-test -- --headless -c "h vim.api.nvim_" -c ":!cat %" -c "qa" 2>/dev/null
Common help topics for plugin development:
:h api - Lua API reference:h lua-guide - Lua scripting in Neovim:h autocommand - Event handling:h map - Key mapping functions:h vim.opt - Option manipulation:h vim.fn - Vimscript function access:h lsp - Language Server Protocol:h treesitter - Syntax tree parsingFor Lua-specific development:
# List all vim.api functions
nix run .#nvim-test -- --headless -c "lua print(vim.inspect(vim.api))" -c "qa!" 2>&1
# Check a specific API function signature
nix run .#nvim-test -- --headless -c "lua print(vim.inspect(vim.api.nvim_create_autocmd))" -c "qa!" 2>&1
The lua-language-server included in the dev shell provides inline documentation:
@class and @field annotations for custom typesvim.* types for autocomplete and documentation---@param opts table See :h nvim_create_autocmd for options
function M.setup_autocmd(opts)
-- lua-language-server will show nvim_create_autocmd signature
vim.api.nvim_create_autocmd("BufWritePost", opts)
end
Use vim.notify() for logging:
vim.notify("Debug: " .. vim.inspect(value), vim.log.levels.DEBUG)
Enable verbose mode for tracing:
nix run .#nvim-test -- -V15log.txt -u scripts/minimal-init.lua
Test in isolation: Always use the minimal init when debugging to rule out conflicts with other plugins.
Test against real scenarios:
test_root/Mock only when necessary:
Organize tests logically:
describe() blocksGlobal namespace pollution:
local M = {} patternvim.g. for intentional globals onlyLazy loading issues:
pcall(require, "plugin") for optional dependenciesAutocmd cleanup:
M.cleanup()augroup to prevent duplicate registrationsbefore_each for tests# Run all tests
nix run .#nvim-test -- -u scripts/minimal-init.lua --headless -c 'PlenaryBustedDirectory tests/lua' -c 'qa!'
# Run specific test file
nix run .#nvim-test -- -u scripts/minimal-init.lua --headless -c 'PlenaryBustedFile tests/lua/modulename/core_spec.lua' -c 'qa!'
# Lint code
nix develop -c luacheck lua/ tests/
# Format code
nix develop -c stylua lua/ tests/
# Check docs for API function
nix run .#nvim-test -- --headless -c "h nvim_create_autocmd" -c ":!cat %" -c "qa" 2>/dev/null