docs: Lua plugin development guide

This commit is contained in:
Justin M. Keyes
2025-09-01 18:52:35 -04:00
parent 28ab656122
commit a5e7ccc329
2 changed files with 116 additions and 86 deletions

View File

@@ -10,19 +10,18 @@
============================================================================== ==============================================================================
Introduction *lua-guide* Introduction *lua-guide*
This guide will go through the basics of using Lua in Nvim. It is not meant This guide introduces the basics of everyday usage of Lua to configure and
to be a comprehensive encyclopedia of all available features, nor will it operate Nvim. It assumes some familiarity with the (non-Lua) basics of Nvim
detail all intricacies. Think of it as a survival kit -- the bare minimum
needed to know to comfortably get started on using Lua in Nvim.
An important thing to note is that this isn't a guide to the Lua language
itself. Rather, this is a guide on how to configure and modify Nvim through
the Lua language and the functions we provide to help with this. Take a look
at |luaref| and |lua-concepts| if you'd like to learn more about Lua itself.
Similarly, this guide assumes some familiarity with the basics of Nvim
(commands, options, mappings, autocommands), which are covered in the (commands, options, mappings, autocommands), which are covered in the
|user-manual|. |user-manual|.
This is not a comprehensive encyclopedia of all available features. Think of
it as a survival kit: the bare minimum needed to comfortably get started on
using Lua in Nvim.
See |lua-plugin| for guidance on developing Lua plugins.
See |luaref| and |lua-concepts| for details on the Lua programming language.
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
Some words on the API *lua-guide-api* Some words on the API *lua-guide-api*

View File

@@ -10,11 +10,32 @@
============================================================================== ==============================================================================
Introduction *lua-plugin* Introduction *lua-plugin*
This is a guide for getting started with Nvim plugin development. It is not This document provides guidance for developing Nvim (Lua) plugins:
intended as a set of rules, but as a collection of recommendations for good
practices.
For a guide to using Lua in Nvim, please refer to |lua-guide|. See |lua-guide| for guidance on using Lua to configure and operate Nvim.
See |luaref| and |lua-concepts| for details on the Lua programming language.
==============================================================================
Creating your first plugin *lua-plugin-new*
Any Vimscript or Lua code file that lives in the right directory,
automatically is a "plugin". There's no maniest or "registration" required.
You can try it right now:
1. Visit your config directory: >
:exe 'edit' stdpath('config')
<
2. Create a `plugin/foo.lua` file in there.
3. Add something to it, like: >lua
vim.print('Hello World')
<
4. Start `nvim` and notice that it prints "Hello World" in the messages area.
Check `:messages` if you don't see it.
Besides `plugin/foo.lua`, which is always run at startup, you can define Lua
modules in the `lua/` directory. Those modules aren't loaded until your
`plugin/foo.lua`, the user, calls `require(…)`.
============================================================================== ==============================================================================
Type safety *lua-plugin-type-safety* Type safety *lua-plugin-type-safety*
@@ -24,12 +45,12 @@ virtually immediate feedback.
But for larger projects, this can be a double-edged sword, leaving your plugin But for larger projects, this can be a double-edged sword, leaving your plugin
susceptible to unexpected bugs at the wrong time. susceptible to unexpected bugs at the wrong time.
You can leverage LuaCATS https://luals.github.io/wiki/annotations/ You can leverage LuaCATS or "emmylua" annotations https://luals.github.io/wiki/annotations/
annotations, along with lua-language-server https://luals.github.io/ to catch along with lua-language-server ("LuaLS") https://luals.github.io/ to catch
potential bugs in your CI before your plugin's users do. potential bugs in your CI before your plugin's users do. The Nvim codebase
uses these annotations extensively.
------------------------------------------------------------------------------ TOOLS
Tools *lua-plugin-type-safety-tools*
- lua-typecheck-action https://github.com/marketplace/actions/lua-typecheck-action - lua-typecheck-action https://github.com/marketplace/actions/lua-typecheck-action
- lua-language-server https://luals.github.io - lua-language-server https://luals.github.io
@@ -37,11 +58,11 @@ Tools *lua-plugin-type-safety-tools*
============================================================================== ==============================================================================
Keymaps *lua-plugin-keymaps* Keymaps *lua-plugin-keymaps*
Avoid creating keymaps automatically, unless they are not controversial. Doing Avoid creating excessive keymaps automatically. Doing so can conflict with
so can easily lead to conflicts with user |mapping|s. user |mapping|s.
NOTE: An example for uncontroversial keymaps are buffer-local |mapping|s for NOTE: An example for uncontroversial keymaps are buffer-local |mapping|s for
specific file types or floating windows. specific file types or floating windows, or <Plug> mappings.
A common approach to allow keymap configuration is to define a declarative DSL A common approach to allow keymap configuration is to define a declarative DSL
https://en.wikipedia.org/wiki/Domain-specific_language via a `setup` function. https://en.wikipedia.org/wiki/Domain-specific_language via a `setup` function.
@@ -81,25 +102,24 @@ Some benefits of exposing a Lua function are:
NOTE: If your function takes an options table, users may still benefit NOTE: If your function takes an options table, users may still benefit
from |<Plug>| mappings for the most common combinations. from |<Plug>| mappings for the most common combinations.
------------------------------------------------------------------------------ KEYMAP EXAMPLE
Example *lua-plugin-plug-mapping-example*
In your plugin: In your plugin:
>lua >lua
vim.keymap.set("n", "<Plug>(SayHello)", function() vim.keymap.set('n', '<Plug>(SayHello)', function()
print("Hello from normal mode") print('Hello from normal mode')
end, { noremap = true }) end, { noremap = true })
vim.keymap.set("v", "<Plug>(SayHello)", function() vim.keymap.set('v', '<Plug>(SayHello)', function()
print("Hello from visual mode") print('Hello from visual mode')
end, { noremap = true }) end, { noremap = true })
< <
In the user's config: In the user's config:
>lua >lua
vim.keymap.set({"n", "v"}, "<leader>h", "<Plug>(SayHello)") vim.keymap.set({'n', 'v'}, '<leader>h', '<Plug>(SayHello)')
< <
============================================================================== ==============================================================================
Initialization *lua-plugin-initialization* Initialization *lua-plugin-init*
Newcomers to Lua plugin development will often put all initialization logic in Newcomers to Lua plugin development will often put all initialization logic in
a single `setup` function, which takes a table of options. a single `setup` function, which takes a table of options.
@@ -109,8 +129,8 @@ your plugin, even if they are happy with the default configuration.
Strictly separated configuration and smart initialization allow your plugin to Strictly separated configuration and smart initialization allow your plugin to
work out of the box. work out of the box.
NOTE: A well designed plugin has minimal impact on startup time. NOTE: A well designed plugin has minimal impact on startup time. See also
See also |lua-plugin-lazy-loading|. |lua-plugin-lazy|.
Common approaches to a strictly separated configuration are: Common approaches to a strictly separated configuration are:
@@ -124,37 +144,45 @@ Typically, automatic initialization logic is done in a |plugin| or |ftplugin|
script. See also |'runtimepath'|. script. See also |'runtimepath'|.
============================================================================== ==============================================================================
Lazy loading *lua-plugin-lazy-loading* Lazy loading *lua-plugin-lazy*
When it comes to initializing your plugin, assume your users may not be using Some users like to micro-manage "lazy loading" of plugins by explicitly
a plugin manager that takes care of lazy loading for you. configuring which commands and key mappings load the plugin.
Making sure your plugin does not unnecessarily impact startup time is your
responsibility. A plugin's functionality may evolve over time, potentially Your plugin should not depend on every user micro-managing their configuration
leading to breakage if users have to hack into the loading mechanisms. in such a way. Nvim has a mechanism for every plugin to do its own implicit
Furthermore, a plugin that implements its own lazy initialization properly will lazy-loading (in Vimscript it's called |autoload|), via `autoload/`
likely have less overhead than the mechanisms used by a plugin manager or user (Vimscript) and `lua/` (Lua). Plugin authors can provide "lazy loading" by
to load that plugin lazily. providing a `plugin/<name>.lua` file which defines their commands and
keymappings. This file should be small, and should not eagerly `require()` the
rest of your plugin. Commands and mappings should do the `require()`.
Guidance:
- Plugins should arrange their "lazy" behavior once, instead of expecting every user to micromanage it.
- Keep `plugin/<name>.lua` small, avoid eagerly calling `require()` on modules
until a command or mapping is actually used.
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
Defer `require` calls *lua-plugin-lazy-loading-defer-require* Defer require() calls *lua-plugin-defer-require*
|plugin| scripts should not eagerly `require` Lua modules. `plugin/<name>.lua` scripts (|plugin|) are eagerly run at startup; this is
intentional, so that plugins can setup the (minimal) commands and keymappings
that users will use to invoke the plugin. This also means these "plugin/"
files should NOT eagerly `require` Lua modules.
For example, instead of: For example, instead of:
>lua >lua
local foo = require("foo") local foo = require('foo')
vim.api.nvim_create_user_command("MyCommand", function() vim.api.nvim_create_user_command('MyCommand', function()
foo.do_something() foo.do_something()
end, { end, { -- ... })
-- ...
})
< <
which will eagerly load the `foo` module and any other modules it imports which calls `require('foo')` as soon as the module is loaded, you can
eagerly, you can lazy load it by moving the `require` into the command's lazy-load it by moving the `require` into the command's implementation:
implementation.
>lua >lua
vim.api.nvim_create_user_command("MyCommand", function() vim.api.nvim_create_user_command('MyCommand', function()
local foo = require("foo") local foo = require('foo')
foo.do_something() foo.do_something()
end, { end, {
-- ... -- ...
@@ -165,23 +193,23 @@ defer `require` calls too.
NOTE: For a Vimscript alternative to `require`, see |autoload|. NOTE: For a Vimscript alternative to `require`, see |autoload|.
NOTE: In case you are worried about eagerly creating user commands, autocommands NOTE: If you are worried about eagerly creating user commands, autocommands or
or keymaps at startup: keymaps at startup: Plugin managers that provide abstractions for lazy-loading
Plugin managers that provide abstractions for lazy-loading plugins on plugins on such events do the same amount of work. There is no performance
such events will need to create these themselves. benefit for users to define lazy-loading entrypoints in their configuration
instead of plugins defining it in `plugin/<name>.lua`.
NOTE: You can use |--startuptime| to |profile| the impact a plugin has on NOTE: You can use |--startuptime| to |profile| the impact a plugin has on
startup time. startup time.
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
Filetype-specific functionality *lua-plugin-lazy-loading-filetype* Filetype-specific functionality *lua-plugin-filetype*
Consider making use of |filetype| for any functionality that is specific to a Consider making use of 'filetype' for any functionality that is specific to
filetype, by putting the initialization logic in a `ftplugin/{filetype}.lua` a filetype, by putting the initialization logic in a `ftplugin/{filetype}.lua`
script. script.
------------------------------------------------------------------------------ FILETYPE EXAMPLE
Example *lua-plugin-lazy-loading-filetype-example*
A plugin tailored to Rust development might have initialization in A plugin tailored to Rust development might have initialization in
`ftplugin/rust.lua`: `ftplugin/rust.lua`:
@@ -197,12 +225,12 @@ A plugin tailored to Rust development might have initialization in
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
-- do something specific to this buffer, -- do something specific to this buffer,
-- e.g. add a |<Plug>| mapping or create a command -- e.g. add a |<Plug>| mapping or create a command
vim.keymap.set("n", "<Plug>(MyPluginBufferAction)", function() vim.keymap.set('n', '<Plug>(MyPluginBufferAction)', function()
print("Hello") print('Hello')
end, { noremap = true, buffer = bufnr, }) end, { noremap = true, buffer = bufnr, })
< <
============================================================================== ==============================================================================
Configuration *lua-plugin-configuration* Configuration *lua-plugin-config*
Once you have merged the default configuration with the user's config, you Once you have merged the default configuration with the user's config, you
should validate configs. should validate configs.
@@ -217,10 +245,16 @@ Validations could include:
============================================================================== ==============================================================================
Troubleshooting *lua-plugin-troubleshooting* Troubleshooting *lua-plugin-troubleshooting*
------------------------------------------------------------------------------ While developing a plugin, you can use the |:restart| command to see the
Health *lua-plugin-troubleshooting-health* result of code changes in your plugin.
Provide health checks in `lua/{plugin}/health.lua`. HEALTH
Nvim's "health" framework gives plugins a simple way to report status checks
to users. See |health-dev| for an example.
Basically, this just means your plugin will have a `lua/{plugin}/health.lua`
file. |:checkhealth| will automatically find this file when it runs.
Some things to validate: Some things to validate:
@@ -229,19 +263,18 @@ Some things to validate:
- Presence of Lua dependencies (e.g. other plugins) - Presence of Lua dependencies (e.g. other plugins)
- Presence of external dependencies - Presence of external dependencies
See also |vim.health| and |health-dev|. MINIMAL CONFIG TEMPLATE
------------------------------------------------------------------------------
Minimal config template *lua-plugin-troubleshooting-minimal-config*
It can be useful to provide a template for a minimal configuration, along with It can be useful to provide a template for a minimal configuration, along with
a guide on how to use it to reproduce issues. a guide on how to use it to reproduce issues.
============================================================================== ==============================================================================
Versioning and releases *lua-plugin-versioning-releases* Versioning and releases *lua-plugin-versioning*
Consider Consider:
- Use |vim.deprecate()| or a `---@deprecate` annotation when you need to
communicate a (future) breaking change or discourged practice.
- Using SemVer https://semver.org/ tags and releases to properly communicate - Using SemVer https://semver.org/ tags and releases to properly communicate
bug fixes, new features, and breaking changes. bug fixes, new features, and breaking changes.
- Automating versioning and releases in CI. - Automating versioning and releases in CI.
@@ -249,13 +282,11 @@ Consider
has dependencies or components that need to be built; or if it could be a has dependencies or components that need to be built; or if it could be a
dependency for another plugin. dependency for another plugin.
------------------------------------------------------------------------------ FURTHER READING
Further reading *lua-plugin-versioning-releases-further-reading*
- Luarocks <3 Nvim https://github.com/nvim-neorocks/sample-luarocks-plugin - Luarocks ❤️ Nvim https://github.com/nvim-neorocks/sample-luarocks-plugin
------------------------------------------------------------------------------ VERSIONING TOOLS
Tools *lua-plugin-versioning-releases-tools*
- luarocks-tag-release - luarocks-tag-release
https://github.com/marketplace/actions/luarocks-tag-release https://github.com/marketplace/actions/luarocks-tag-release
@@ -265,14 +296,14 @@ Tools *lua-plugin-versioning-releases-tools*
https://github.com/semantic-release/semantic-release https://github.com/semantic-release/semantic-release
============================================================================== ==============================================================================
Documentation *lua-plugin-documentation* Documentation *lua-plugin-doc*
Provide vimdoc (see |help-writing|), so that users can read your plugin's Provide vimdoc (see |help-writing|), so that users can read your plugin's
documentation in Nvim, by entering `:h {plugin}` in |command-mode|. documentation in Nvim, by entering `:h {plugin}` in |command-mode|.
------------------------------------------------------------------------------ DOCUMENTATION TOOLS
Tools *lua-plugin-documentation-tools*
- panvimdoc https://github.com/kdheepak/panvimdoc - panvimdoc https://github.com/kdheepak/panvimdoc
vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl: vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl: