Overview
This article describes how to implement and share a custom plugin.
Resources
- Writing Plugins - It 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:
BufAddBufDeleteBufEnterBufFilePostBufFilePreBufHiddenBufLeaveBufModifiedSetBufNewFileBufNewBufReadCmdBufReadPreBufReadofBufReadPostBufUnloadBufWinEnterBufWinLeaveBufWipeoutBufWriteCmdBufWritePost: after a buffer has been writtenBufWriteorBufWritePreChanInfoChanOpenChanUndefinedCmdLineChangeCmdLineEnterCmdLineLeaveCmdwinEnterCmdwinLeaveColorSchemePreColorSchemeCompleteChangedCompleteDonePreCompleteDoneCursorHoldI: 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 modeDiffChangedPreDiffChangedDiffUpdatedExitPreFileAppendedCmdFileAppendedPostFileAppendedPreFileChangedROFileChangedShellPostFileChangedShellFileReadCmdFileReadPostFileReadPreFileTypeFileWriteCmdFileWritePostFileWritePreFocusGainedFocusLostFuncUndefinedInsertChangeInsertCharPreInsertEnterInsertLeavePreInsertLeaveMenuPopupModeChangedOptionSetQuickFixCmdPostQuickFixCmdPreQuitPreRecordingEnterRecordingLeaveRemoteReplySearchWrappedSessionLoadPostShellCmdPostShellFilterPostSignalSourceCmdSourcePostSourcePreSpellFileMissingStdinReadPostStdinReadPreSwapExistsSyntaxTabCosedTabEnterTabLeaveTabNewEnteredTabNewTermCloseTermEnterTermLeaveTermOpenTermResponseTextChangedITextChangedPTextChangedTTextChangedTextYankPostUIEnterUILeaveUserGettingBoredUserVimEnterVimLeavePreVimLeaveVimResizedVimResumeVimSuspendWinClosedWinEnterWinNewWinResizedWinScrolledWinleavenvim_buf_attachnvim_buf_changedtick_eventnvim_buf_detach_eventnvim_buf_lines_eventnvim_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.mdfile in this directory that describes the functionality of the plugin and the steps to install it. -
Create the directories
luaandplugindirectory inside it. -
Create the file
{plugin-name}.luainside theluadirectory. Alternatively, create a subdirectory whose name is the plugin name and create the fileinit.luainside 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 M -
Create the file
{plugin-name}.luainside theplugindirectory. 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.luacontaining the following:return { "nvim-treesitter/playground", lazy = false -- load on startup, not just when required } -
Modify the file
~/.config/nvim/lua/user/plugins/treesitter.luato contain the following. Note theplaygroundvalue.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
:TSPlaygroundToggleto 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
oto toggle display of a query editor buffer below the AST. -
Enter a query like
(comment) @commentand 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) |