| name | neovim-plugin-development |
| description | Write Neovim plugins in Lua. Use in conjunction with LuaLS LSP and activate any available Lua skills as well. This skill provides extra context on Neovim specifics like vim.api, custom plugin logic, buffer/window management, the nvim event system, and where to find deeper documentation as needed. |
Neovim Plugin Development
Write Neovim plugins from scratch in Lua, working with Neovim's internal APIs at a low level.
When to Use This Skill
- Writing custom Neovim plugin logic (not just configuration)
- Working with vim.api, vim.fn, vim.opt directly
- Understanding how existing plugins work internally
- Creating buffer manipulation, window management, or custom UI
- Implementing autocommands, user commands, or highlight groups
- Debugging Lua code running inside Neovim
Core APIs
vim.api (Neovim API)
Primary interface for Neovim internals:
vim.api.nvim_get_current_buf()
vim.api.nvim_buf_get_lines(buf, start, end_, strict)
vim.api.nvim_buf_set_lines(buf, start, end_, strict, lines)
vim.api.nvim_buf_get_name(buf)
vim.api.nvim_buf_set_option(buf, name, value)
vim.api.nvim_buf_get_mark(buf, name)
vim.api.nvim_get_current_win()
vim.api.nvim_win_get_buf(win)
vim.api.nvim_win_set_cursor(win, {row, col})
vim.api.nvim_win_get_cursor(win)
vim.api.nvim_open_win(buf, enter, config)
vim.api.nvim_create_user_command(name, command, opts)
vim.api.nvim_create_autocmd(event, opts)
vim.api.nvim_set_keymap(mode, lhs, rhs, opts)
vim.api.nvim_create_namespace(name)
vim.api.nvim_buf_add_highlight(buf, ns, hl_group, line, col_start, col_end)
vim.api.nvim_buf_set_extmark(buf, ns, line, col, opts)
vim.fn (Vimscript Functions)
Access Vimscript functions from Lua:
vim.fn.expand("%:p")
vim.fn.fnamemodify(path, ":t")
vim.fn.filereadable(path)
vim.fn.glob(pattern)
vim.fn.system(cmd)
vim.fn.json_decode(str)
vim.fn.json_encode(table)
vim.fn.input("Prompt: ")
vim.fn.confirm("Question?", "&Yes\n&No")
vim.opt / vim.o / vim.bo / vim.wo
vim.opt.number = true
vim.o.number = true
vim.bo.filetype = "lua"
vim.bo[bufnr].modifiable = false
vim.wo.wrap = false
vim.wo[winnr].signcolumn = "yes"
vim.opt.wildignore:append({ "*.o", "*.a" })
vim.opt.listchars = { tab = ">> ", trail = "-" }
vim.keymap
vim.keymap.set("n", "<leader>x", function()
end, { desc = "Description", buffer = bufnr, silent = true })
vim.keymap.del("n", "<leader>x")
Plugin Structure
Minimal Plugin
local M = {}
M.setup = function(opts)
opts = opts or {}
end
return M
Full Plugin Structure
my-plugin.nvim/
โโโ lua/
โ โโโ my-plugin/
โ โโโ init.lua -- Main entry, exports M.setup()
โ โโโ config.lua -- Default config, merged with user opts
โ โโโ commands.lua -- User commands
โ โโโ util.lua -- Helper functions
โโโ plugin/
โ โโโ my-plugin.lua -- Auto-loaded, can call setup if no config needed
โโโ doc/
โโโ my-plugin.txt -- Help documentation
Config Pattern
local M = {}
M.defaults = {
option1 = true,
option2 = "default",
}
M.options = {}
M.setup = function(opts)
M.options = vim.tbl_deep_extend("force", M.defaults, opts or {})
end
return M
Common Patterns
Autocommands
local group = vim.api.nvim_create_augroup("MyPlugin", { clear = true })
vim.api.nvim_create_autocmd("BufWritePre", {
group = group,
pattern = "*.lua",
callback = function(args)
end,
})
vim.api.nvim_create_autocmd("User", {
group = group,
pattern = "MyPluginEvent",
callback = function() ... end,
})
vim.api.nvim_exec_autocmds("User", { pattern = "MyPluginEvent" })
User Commands
vim.api.nvim_create_user_command("MyCommand", function(opts)
print(opts.args)
end, {
nargs = "*",
bang = true,
range = true,
complete = function(arglead, cmdline, cursorpos)
return { "option1", "option2" }
end,
})
Floating Windows
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "Line 1", "Line 2" })
local win = vim.api.nvim_open_win(buf, true, {
relative = "editor",
width = 40,
height = 10,
row = 5,
col = 10,
style = "minimal",
border = "rounded",
})
vim.keymap.set("n", "q", function()
vim.api.nvim_win_close(win, true)
end, { buffer = buf })
Extmarks and Virtual Text
local ns = vim.api.nvim_create_namespace("my-plugin")
vim.api.nvim_buf_set_extmark(buf, ns, line, 0, {
virt_text = { { "virtual text", "Comment" } },
virt_text_pos = "eol",
})
vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)
Async with vim.schedule
vim.schedule(function()
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
end)
local timer = vim.loop.new_timer()
local function debounce(fn, ms)
return function(...)
local args = { ... }
timer:stop()
timer:start(ms, 0, vim.schedule_wrap(function()
fn(unpack(args))
end))
end
end
Debugging
print(vim.inspect(table))
vim.print(table)
vim.notify("Message", vim.log.levels.INFO)
vim.notify("Error!", vim.log.levels.ERROR)
assert(condition, "Error message")
local f = io.open("/tmp/nvim-debug.log", "a")
f:write(vim.inspect(data) .. "\n")
f:close()
Guidelines
- Use
vim.schedule when modifying buffers from async callbacks
- Clear autocommand groups before recreating to avoid duplicates
- Use namespaces for highlights/extmarks to enable clean removal
- Prefer
vim.keymap.set over vim.api.nvim_set_keymap
- Use
vim.tbl_deep_extend for merging config tables
- Check
vim.fn.has("nvim-0.10") for version-specific features
- Test with
:luafile % or :source % during development
- Use
:messages and :checkhealth for debugging