Overview
This article describes how to implement and share a custom plugin.
Resources
- Writing Plugins - It's Never Been Easier by DevOnDuty
- Automatically Execute Anything in Nvim
- Execute anything in neovim (now customizable)
- Neovim Plugins - Enhancing your Neovim editor with awesome plugins
Basics
There are four ways to trigger a Lua function in Neovim. Begin by defining a function in any buffer such as:
function Greet(name)
name = name or "World"
print("Hello, " .. name .. "!")
end
The first way to run the function is to source the current buffer and use the lua
command to call the function.
:so
:lua Greet("Mark")
By default the source
command sources the current buffer which can also be specified by :so %
.
The second way to run the function is to create a user command. This requires changing the definition of the function to accept an options table as shown below. This contains the keys args
and fargs
. The value of the args
key is a single string containing the values of all arguments passed to the command. The value of the fargs
key is a sequence table containing the values of all arguments.
function Greet(opts)
-- Also see vim.inspect(value).
local name = #opts.fargs > 0 and opts.args or "World"
name = name:gsub('"', '') -- removes double quotes
print("Hello, " .. name .. "!")
end
Add the following after the function definition inside the buffer. The last argument is a table of options and is required.
vim.api.nvim_create_user_command("Greet", Greet, {})
Source the buffer again and then use the new command. For example:
:so
:Greet
:Greet "Mark"
The third way to run the function is to create an autocommand. An autocommand associates a function with an event. Any number of autocommands can be created for the same event. When the event occurs, all of them are run.
Autocommands can be defined to only run when their event occurs in a specific buffer.
For this approach we can return to a simpler version of the Greet
function. The event in this example is writing any buffer whose file name ends in .lua
.
Placing the autocommands inside an augroup allows us to clear existing autocommands in the group every time the group is recreated. This is done by specifying "clear = true" (the default) when creating an augroup. This allows code that creates an augroup and defines autocommands in the group to be idempotent. We need to do this so sourcing this file multiple times doesn't register multiple callbacks to run when the event occurs.
To see a list of currently defined augroups, enter :au
.
To display the augroup with a given name, enter :au {group-name}
To delete the augroup with a given name, enter :au! {group-name}
.
To see a list of augroups defined for a given event, enter :au {event-name}
.
function Greet(name)
name = name or "World"
print("Hello, " .. name .. "!")
end
vim.api.nvim_create_autocmd("BufWritePost", {
group = vim.api.nvim_create_augroup("autocmd", { clear = true }),
pattern = "lua",
callback = function() Greet("Mark") end
})
To see a list of the supported events, enter :h events
. The supported event names include:
BufAdd
BufDelete
BufEnter
BufFilePost
BufFilePre
BufHidden
BufLeave
BufModifiedSet
BufNewFile
BufNew
BufReadCmd
BufReadPre
BufRead
ofBufReadPost
BufUnload
BufWinEnter
BufWinLeave
BufWipeout
BufWriteCmd
BufWritePost
: after a buffer has been writtenBufWrite
orBufWritePre
ChanInfo
ChanOpen
ChanUndefined
CmdLineChange
CmdLineEnter
CmdLineLeave
CmdwinEnter
CmdwinLeave
ColorSchemePre
ColorScheme
CompleteChanged
CompleteDonePre
CompleteDone
CursorHoldI
: when no key has been pressed for some amount of time in insert modeCursorHold
: when no key has been pressed for some amount of time in normal modeCursorMovedI
: when cursor is moved in insert modeCursorMoved
: when cursor is moved in normal or visual modeDiffChangedPre
DiffChanged
DiffUpdated
ExitPre
FileAppendedCmd
FileAppendedPost
FileAppendedPre
FileChangedRO
FileChangedShellPost
FileChangedShell
FileReadCmd
FileReadPost
FileReadPre
FileType
FileWriteCmd
FileWritePost
FileWritePre
FocusGained
FocusLost
FuncUndefined
InsertChange
InsertCharPre
InsertEnter
InsertLeavePre
InsertLeave
MenuPopup
ModeChanged
OptionSet
QuickFixCmdPost
QuickFixCmdPre
QuitPre
RecordingEnter
RecordingLeave
RemoteReply
SearchWrapped
SessionLoadPost
ShellCmdPost
ShellFilterPost
Signal
SourceCmd
SourcePost
SourcePre
SpellFileMissing
StdinReadPost
StdinReadPre
SwapExists
Syntax
TabCosed
TabEnter
TabLeave
TabNewEntered
TabNew
TermClose
TermEnter
TermLeave
TermOpen
TermResponse
TextChangedI
TextChangedP
TextChangedT
TextChanged
TextYankPost
UIEnter
UILeave
UserGettingBored
User
VimEnter
VimLeavePre
VimLeave
VimResized
VimResume
VimSuspend
WinClosed
WinEnter
WinNew
WinResized
WinScrolled
Winleave
nvim_buf_attach
nvim_buf_changedtick_event
nvim_buf_detach_event
nvim_buf_lines_event
nvim_error_event
This can be triggered multiple times. To see all the output, enter :messages
.
The fourth way to run the function is to define a key mapping and type the key sequence. Add the following after the function definition inside the buffer. "n" is for normal mode. TODO: Is "
vim.keymap.set("n", "<leader>g", Greet)
Building a Plugin
To build a Neovim plugin that can be shared with others:
Create a directory for the plugin. It is common for Neovim plugin repositories to have names that end in ".nvim". For example, "greet.nvim"
Create a
README.md
file in this directory that describes the functionality of the plugin and the steps to install it.Create the directories
lua
andplugin
directory inside it.Create the file
{plugin-name}.lua
inside thelua
directory. Alternatively, create a subdirectory whose name is the plugin name and create the fileinit.lua
inside it.In this file, define and return a Lua module. For example:
local M = {}
M.greet = function(name)
name = name or "World"
print("Hello, " .. name .. "!")
end
-- Some plugins define a `setup` function
-- that users can call to configure the plugin.
-- This is typically passed a table of options.
M.setup = function(opts)
-- Configure the plugin here.
end
return MCreate the file
{plugin-name}.lua
inside theplugin
directory. This is a good place to configure user commands and autocommands related to the plugin that do no require options provided by the user. Key mappings can also be defined here, but generally this is left to users so they can select the mappings they prefer. The code in this file is run when the plugin is required.vim.api.nvim_create_user_command(
"Greet",
function(opts)
-- vim.print(opts) -- for debugging
local greet = require("greet").greet
-- Get the first argument.
local arg1 = opts.fargs[1]
-- Remove surrounding quotes.
local unquoted = arg1 and arg1:gsub('"(%w*)"', "%1")
greet(unquoted or arg1)
end,
{}
)Create a GitHub repository for the plugin and push this directory to it.
Installing a Plugin
When nvim
is started it runs all .lua
files found in the ~/.config/nvim/lua/plugins
and ~/.config/nvim/lua/user/plugins
directories. A popular approach for configuring plugins is to create the file ~/.config/nvim/lua/user/plugins/{plugin-name}.lua
for each plugin to be configured.
For example, to configure a plugin named "greet", create the file ~/.config/nvim/lua/user/plugins/greet.lua
containing the following:
return {
-- "{github-account}/{repository-name}"
"mvolkmann/greet.nvim"
}
There are several popular plugin managers for Neovim. One that is highly recommended is lazy.nvim. If this is being used, enter :Lazy sync
to install any missing plugins that were configured, update any that have a new version, and remove any that are no longer being used.
Testing a Plugin
A single command can be entered inside Neovim to load a plugin and exercise one of the functions it defines. For example, to verify that the "greet.nvim" plugin was installed, enter the following in Neovim:
:lua require("greet").greet("World")`
This should display "Hello, World!" in the message area at the bottom.
I configured the key mapping
Debugging
When debugging a plugin it is helpful to print values of variables. The function vim.print
performs pretty-printing of all kinds of values including tables. The function vim.inspect
is similar, but returns a pretty-printed string rather than printing anything.
Auto Commands
An autocmd registers a function to run when a given event occurs. For more information, enter :help autocmd
The "FileType" event is fired every time the file type of a buffer is set. This happens every time a file is opened if the "filetype" option is on, which it is by default. For more information, enter ":h FileType".
The following example runs a function every time the "FileType" event occurs.
vim.api.nvim_create_autocmd("FileType", {
group = group,
-- Only run when one of these file types is opened.
-- To run for all file types, use "*".
pattern = { "javascript", "lua", "text" },
callback = function()
-- vim.schedule is like setImmediate in JavaScript.
-- defer_fn is like setTimeout in JavaScript.
-- vim.wait is like setInterval in JavaScript.
vim.defer_fn(function()
print("I was deferred.")
end, 1000) -- milliseconds
-- See ":h expand" for a list of available data.
local data = {
buf = vim.fn.expand("<abuf>"), -- buffer number
file = vim.fn.expand("<afile>"), -- file name
match = vim.fn.expand("<amatch>") -- matched file type
}
vim.print(data)
end
})
autorun Example
See an example Neovim plugin in GitHub. This opens a new buffer in a vertical split, prompts for a file matching pattern (ex. *.lua
), and prompts for a command to run if any matching files are saved (ex. lua demo.lua
). Each time the command is run, the contents of the new buffer are replaced anything the command writes to stdout or stderr. This is great for debugging apps that have command-line output.
To configure use of this plugin, create the file ~/.config/nvim/lua/user/plugins/autorun.lua
containing the following:
return {
"mvolkmann/autorun.nvim",
lazy = false, -- load on startup, not just when required
config = true -- require the plugin and call its setup function
}
Setting config
to true
is the equivalent of the following:
config = function()
require("autorun").setup()
end
Library/Plugin Caching
Lua caches all libraries loaded by the require
function. Additional calls to require
for a loaded library will find it in the cache and return it without reloading it. To force a library to reload, perhaps because its code has changed, remove it from the cache and then call require
as follows:
package.loaded.{plugin-name} = nil
require "plugin-name"
LUA_PATH
To see the places that the Lua require
function looks for .lua
files, enter lua
to start it in interactive mode and then enter print(package.path)
. By default this will include the following:
/usr/local/share/lua/5.4/?.lua
/usr/local/share/lua/5.4/?/init.lua
/usr/local/lib/lua/5.4/?.lua
/usr/local/lib/lua/5.4/?/init.lua
/?.lua
/?/init.lua
To add more paths to the beginning of this list, define the environment variable LUA_PATH
. For example, when using zsh, add the following in ~/.zshrc
:
export LUA_PATH="${HOME}/lua/?.lua;;"
The second semicolon at the end is replaced by the current value of package.path
.
For me this adds /Users/volkmannm/lua/?.lua;
to the beginning. This makes it so searches for .lua
files begins in the lua
subdirectory of my home directory.
To automatically require files from this directory on startup of Neovim, add calls to the require
function in ~/.config/nvim/lua/user/init.lua
.
Neovim API
The Neovim API provides functions in many categories. Each of these are summarized in the following subsections.
Treesitter Playground
Treesitter parses source code into an AST.
For help on Treesitter, enter :h nvim-treesitter
.
The plugin nvim-treesitter/playground
displays ASTs generated by Treesitter. This is helpful when developing plugins that act on specific AST nodes.
The instructions below assume that the Treesitter plugin is already configured.
To use the playground plugin:
Create the file
~/.config/nvim/lua/user/plugins/playground.lua
containing the following:return {
"nvim-treesitter/playground",
lazy = false -- load on startup, not just when required
}Modify the file
~/.config/nvim/lua/user/plugins/treesitter.lua
to contain the following. Note theplayground
value.local treesitter = {
"nvim-treesitter/nvim-treesitter",
opts = function()
require 'nvim-treesitter.configs'.setup {
incremental_selection = {
enable = true,
keymaps = {
init_selection = "<leader>sw", -- select word
node_incremental = "<leader>sn", -- incremental select node
scope_incremental = "<leader>ss", -- incremental select scope
node_decremental = "<leader>su" -- incremental select undo
}
},
playground = {
enable = true,
disable = {},
updatetime = 25, -- Debounced time for highlighting nodes in the playground from source code
persist_queries = false, -- Whether the query persists across vim sessions
keybindings = {
toggle_query_editor = 'o',
toggle_hl_groups = 'i',
toggle_injected_languages = 't',
toggle_anonymous_nodes = 'a',
toggle_language_display = 'I',
focus_language = 'f',
unfocus_language = 'F',
update = 'R',
goto_node = '<cr>',
show_help = '?',
},
},
query_linter = {
enable = true,
use_virtual_text = true,
lint_events = {"BufWrite", "CursorHold"},
}
}
end
}Restart
nvim
.Enter
:TSInstall query
Open any source file.
Enter
:TSPlaygroundToggle
to toggle display of the AST for code in the current buffer. This will open a new vertical split and display the AST there.Move the cursor to any AST node to highlight the corresponding source code token.
Move the cursor to any source code token to highlight the corresponding AST node.
With focus in the AST buffer, press
o
to toggle display of a query editor buffer below the AST.Enter a query like
(comment) @comment
and exit insert mode.Matching nodes in the AST and matching tokens in the source code will be highlighted.
The following key mappings can be used when focus is in the AST buffer:
Key | Operation |
---|---|
o | toggles display of query editor buffer below the AST buffer |
a | toggles display of anonymous nodes such as keywords and operators |
i | toggles display of highlight groups |
I | toggles display of the language to which each node belongs |
<cr> | moves cursor to corresponding source code token |
Plugin Using Treesitter
See an example Neovim plugin that uses Treesitter in GitHub. This parses the source code in the current buffer, populates the quickfix list will all comment lines that contain "TODO", and opens the quickfix list.
To configure use of this plugin, create the file ~/.config/nvim/lua/user/plugins/todo-quickfix.lua
containing the following:
return {
"mvolkmann/todo-quickfix.nvim",
lazy = false, -- load on startup, not just when required
config = true -- require the plugin and call its setup function
}
Open a source file containing TODO comments and enter :TodoQF
.
Highlight Groups
A highlight group associate a name with a foreground color, background color, and style such a bold.
To see a list of defined highlight groups, enter :hi
.
Neovim API
The Neovim API provides functions in many categories. Each of these are summarized in the following subsections.
To see help for a given function, enter :h {function-name}
.
Autocmd Functions
Buffer Functions
Command Functions
Function | Description |
---|---|
nvim_buf_create_user_command(buffer, name) | creates a global user command |
nvim_buf_del_user_command(buffer, name) | |
nvim_buf_get_commands(buffer, *opts) | |
nvim_cmd(*cmd, *opts) | |
nvim_create_user_command(name, command, *opts) | creates a user command; command can be a Lua function |
nvim_del_user_command(name) | |
nvim_get_commands(*opts) | |
nvim_parse_cmd(str, opts) |
Extmark Functions
Global Functions
Options Functions
Tabpage Functions
| Function | Description | | ----------------------------------------------------------------------------------------------------- | nvim_tabpage_del_var(tabpage, name) | | | nvim_tabpage_get_number(tabpage) | | | nvim_tabpage_get_var(tabpage, name) | | | nvim_tabpage_get_win(tabpage) | | | nvim_tabpage_is_valid(tabpage) | | | nvim_tabpage_list_wins(tabpage) | | | nvim_tabpage_set_var(tabpage, name, value) | |
UI Functions
"pum" is an acronym for "popup menu".
Vimscript Functions
Function | Description |
---|---|
nvim_call_dict_function(dict, fn, args) | |
nvim_call_function(fn, args) | |
nvim_command(command) | |
nvim_eval(expr) | |
nvim_exec2(src, *opts) | |
nvim_parse_expression(expr, flags, highlight) |
Window Functions
Win_Config Functions
Function | Description |
---|---|
nvim_open_win(buffer, enter, *config) | |
nvim_win_get_config(window, *config) |