Neovim Plugins

Overview

This article describes how to implement and share a custom plugin.

Resources

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:

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 "g" already in use?

vim.keymap.set("n", "<leader>g", Greet)

Building a Plugin

To build a Neovim plugin that can be shared with others:

  1. Create a directory for the plugin. It is common for Neovim plugin repositories to have names that end in ".nvim". For example, "greet.nvim"

  2. Create a README.md file in this directory that describes the functionality of the plugin and the steps to install it.

  3. Create the directories lua and plugin directory inside it.

  4. Create the file {plugin-name}.lua inside the lua directory. Alternatively, create a subdirectory whose name is the plugin name and create the file init.lua inside it.

  5. 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
  6. Create the file {plugin-name}.lua inside the plugin 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,
    {}
    )
  7. 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 mappingx to write and source the current buffer. This is useful when developing a Neovim plugin.

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:

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:

  1. 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
    }
  2. Modify the file ~/.config/nvim/lua/user/plugins/treesitter.lua to contain the following. Note the playground 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
    }
  3. Restart nvim.

  4. Enter :TSInstall query

  5. Open any source file.

  6. 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.

  7. Move the cursor to any AST node to highlight the corresponding source code token.

  8. Move the cursor to any source code token to highlight the corresponding AST node.

  9. With focus in the AST buffer, press o to toggle display of a query editor buffer below the AST.

  10. Enter a query like (comment) @comment and exit insert mode.

  11. 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:

KeyOperation
otoggles display of query editor buffer below the AST buffer
atoggles display of anonymous nodes such as keywords and operators
itoggles display of highlight groups
Itoggles 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

FunctionDescription
nvim_clear_autocmds(*opts)
nvim_create_augroup(name, *opts)crates an auto-command group
nvim_create_autocmd(event, *opts)
nvim_del_augroup_by_id(id)
nvim_del_augroup_by_name(name)
nvim_del_autocmd(id)
nvim_create_autocmd()
nvim_get_autocmds(*opts)

Buffer Functions

FunctionDescription
nvim_buf_attach(buffer, send_buffer, opts)
nvim_buf_call(buffer, fun)
nvim_buf_del_keymap(buffer, mode, lhs)
nvim_buf_del_mark(buffer, name)
nvim_buf_del_var(buffer, name)
nvim_buf_delete(buffer, opts)
nvim_buf_detach(buffer)
nvim_buf_get_changedtick(buffer)
nvim_buf_get_keymap(buffer, mode)
nvim_buf_get_lines(buffer, start, end, strick_indexing)
nvim_buf_get_mark(buffer, name)
nvim_buf_get_name(buffer)
nvim_buf_get_offset(buffer, index)
nvim_buf_get_text(buffer, start_row, start_col, end_row, end_col, opts)
nvim_buf_get_var(buffer, name)
nvim_buf_is_loaded(buffer)
nvim_buf_is_valid(buffer)
nvim_buf_line_count(buffer)
nvim_buf_set_keymap(buffer, mode, lhs, rhs, *opt)
nvim_buf_set_lines(buffer, start, end, strict_indexing, replacement)adds text in a given buffer
nvim_buf_set_mark(buffer, name, line, col, opts)
nvim_buf_set_name(buffer, name)
nvim_buf_set_text(buffer, start_row, start_col, end_row, end_col, replacement)
nvim_buf_set_var(buffer, name, value)

Command Functions

FunctionDescription
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

FunctionDescription
nvim_buf_add_highlight(buffer, ns_id, hl_group, line, col_start, col_end)
nvim_buf_clear_namespace(buffer, ns_id, line_start, line_end)
nvim_buf_del_extmark(buffer, ns_id, id)
nvim_buf_get_extmark_by_id(buffer, ns_id, id, opts)
nvim_buf_set_extmark(buffer, ns_id, line, col, *opts)
nvim_create_namespace(name)
nvim_get_namespaces()
nvim_set_decoration_provider(ns_id, *opts)

Global Functions

FunctionDescription
nvim__get_runtime(pat, all, *opts)
nvim__id(obj)
nvim__id_array(arr)
nvim__id_dictionary(dct)
nvim__id_float(flt)
nvim__inspect_cell(grid, row, col)
nvim__stats()
nvim_call_atomic(calls)
nvim_chan_send(chan, data)
nvim_create_buf(listed, scratch)
nvim_del_current_line()
nvim_del_keymap(mode, lhs)
nvim_del_mark(name)
nvim_del_var(name)
nvim_echo(chunks, history, *opts)
nvim_err_write(str)
nvim_err_writeln(str)
nvim_eval_statusline(str, *opts)
nvim_exec_lua(code, args)
nvim_feedkeys(keys, mode, escape_ks)
nvim_get_api_info()
nvim_get_chan_info(chan)
nvim_get_context(*opts)
nvim_get_current_buf()
nvim_get_current_tabpage()
nvim_get_current_win()
nvim_get_hl(ns_id, *opts)
nvim_get_hl_id_by_name(name)
nvim_get_keymap(mode)
nvim_get_mark(name, opts)
nvim_get_mode()
nvim_get_proc(pid)
nvim_get_proc_children(pid)
nvim_get_runtime_file(name, all)
nvim_get_var(name)
nvim_get_vvar(name)
nvim_input(keys)
nvim_input_mouse(button, action, modifier, grid, row, col)
nvim_list_bufs()
nvim_list_chans()
nvim_list_runtime_paths()
nvim_list_tabpages()
nvim_list_uis()
nvim_list_wins()
nvim_load_context(dict)
nvim_notify(msg, log_level, opts)
nvim_open_term(buffer, opts)
nvim_out_write(str)
nvim_paste(data, crlf, phase)
nvim_put(lines, type, after, follow)
nvim_replace_termcodes(str, from_part, do_lt)
nvim_select_popupmenu_item(item, insert, finish, opts)
nvim_set_client_info(name, version, type, methods, attributes)
nvim_set_current_buf(buffer)
nvim_set_current_line(line)
nvim_set_current_tabpage(tabpage)
nvim_set_current_win(window)
nvim_set_hl(ns_id, name, *val)
nvim_set_hl_ns(ns_id)
nvim_set_hl_ns_fast(ns_id)
nvim_set_keymap(mode, lhs, rhs, *opts)
nvim_set_var(name, value)
nvim_set_vvar(name, value)
nvim_strwidth(text)
nvim_subscribe(event)
nvim_unsubscribe(event)

Options Functions

FunctionDescription
nvim_buf_get_option(buffer, name)
nvim_buf_set_option(buffer, name, value)
nvim_get_all_options_info()
nvim_get_option(name)
nvim_get_option_info2(name, *opts)
nvim_get_option_value(name, value, *opts)
nvim_win_get_option(window, name)
nvim_win_set_option(window, name, value)

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".

FunctionDescription
nvim_ui_atttach(width, height, options)
nvim_ui_detach()
nvim_ui_pum_set_bounds(width, height, row, col)
nvim_ui_pum_set_height(height)
nvim_ui_set_focus(gained)
nvim_ui_set_option(name, value)
nvim_ui_try_resize(width, height)
nvim_ui_try_resize_grid(grid, width, height)

Vimscript Functions

FunctionDescription
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

FunctionDescription
nvim_win_call(window, fun)
nvim_win_close(window, force)
nvim_win_del_var(window, name)
nvim_win_get_buf(window)
nvim_win_get_cursor(window)
nvim_win_get_height(window)
nvim_win_get_position(window)
nvim_win_get_tabpage(window)
nvim_win_get_var(window, name)
nvim_win_get_width(window)
nvim_win_hide(window)
nvim_win_is_valid(window)
nvim_win_set_buf(window, buffer)
nvim_win_set_cursor(window, pos)
nvim_win_set_height(window, height)
nvim_win_set_hl_ns(window, ns_id)
nvim_win_set_var(window, name, value)
nvim_win_set_width(window, width)

Win_Config Functions

FunctionDescription
nvim_open_win(buffer, enter, *config)
nvim_win_get_config(window, *config)