mirror of
https://github.com/neovim/neovim.git
synced 2024-12-23 20:55:18 -07:00
Merge 6d3ff992ff
into 7121983c45
This commit is contained in:
commit
85c29d24c3
396
runtime/doc/lua-plugin.txt
Normal file
396
runtime/doc/lua-plugin.txt
Normal file
@ -0,0 +1,396 @@
|
||||
*lua-plugin.txt* Nvim
|
||||
|
||||
NVIM REFERENCE MANUAL
|
||||
|
||||
Guide to developing Lua plugins for Nvim
|
||||
|
||||
|
||||
Type |gO| to see the table of contents.
|
||||
|
||||
==============================================================================
|
||||
Introduction *lua-plugin*
|
||||
|
||||
This is a guide for getting started with Nvim plugin development. It is not
|
||||
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|.
|
||||
|
||||
==============================================================================
|
||||
Type safety *lua-plugin-type-safety*
|
||||
|
||||
Lua, as a dynamically typed language, is great for configuration. It provides
|
||||
virtually immediate feedback.
|
||||
But for larger projects, this can be a double-edged sword, leaving your plugin
|
||||
susceptible to unexpected bugs at the wrong time.
|
||||
|
||||
You can leverage LuaCATS https://luals.github.io/wiki/annotations/
|
||||
annotations, along with lua-language-server https://luals.github.io/ to catch
|
||||
potential bugs in your CI before your plugin's users do.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Tools *lua-plugin-type-safety-tools*
|
||||
|
||||
- lua-typecheck-action https://github.com/marketplace/actions/lua-typecheck-action
|
||||
- luacheck https://github.com/lunarmodules/luacheck for additional linting
|
||||
|
||||
==============================================================================
|
||||
User commands *lua-plugin-user-commands*
|
||||
|
||||
Many users rely on command completion to discover available user commands. If
|
||||
a plugin pollutes the command namespace with lots of commands, this can
|
||||
quickly become overwhelming.
|
||||
|
||||
Example:
|
||||
|
||||
- `FooAction1 {arg}`
|
||||
- `FooAction2 {arg}`
|
||||
- `FooAction3`
|
||||
- `BarAction1`
|
||||
- `BarAction2`
|
||||
|
||||
Instead of doing this, consider gathering subcommands under scoped commands
|
||||
and implementing completions for each subcommand.
|
||||
|
||||
Example:
|
||||
|
||||
- `Foo action1 {arg}`
|
||||
- `Foo action2 {arg}`
|
||||
- `Foo action3`
|
||||
- `Bar action1`
|
||||
- `Bar action2`
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Subcommand completions example *lua-plugin-user-commands-completions-example*
|
||||
|
||||
In this example, we want to provide:
|
||||
|
||||
- Subcommand completions if the user has typed `:Foo ...`
|
||||
- Argument completions if they have typed `:Foo {subcommand}`
|
||||
|
||||
First, define a type for each subcommand, which has:
|
||||
|
||||
- An implementation: A function which is called when executing the subcommand.
|
||||
- An optional command completion callback, which takes the lead of the
|
||||
subcommand's arguments.
|
||||
|
||||
>lua
|
||||
---@class FooSubcommand
|
||||
---@field impl fun(args:string[], opts: table)
|
||||
---@field complete? fun(subcmd_arg_lead: string): string[]
|
||||
<
|
||||
Next, we define a table mapping subcommands to their implementations and
|
||||
completions:
|
||||
>lua
|
||||
---@type table<string, FooSubcommand>
|
||||
local subcommand_tbl = {
|
||||
action1 = {
|
||||
impl = function(args, opts)
|
||||
-- Implementation (args is a list of strings)
|
||||
end,
|
||||
-- This subcommand has no completions
|
||||
},
|
||||
action2 = {
|
||||
impl = function(args, opts)
|
||||
-- Implementation
|
||||
end,
|
||||
complete = function(subcmd_arg_lead)
|
||||
-- Simplified example
|
||||
local install_args = {
|
||||
"first",
|
||||
"second",
|
||||
"third",
|
||||
}
|
||||
return vim.iter(install_args)
|
||||
:filter(function(install_arg)
|
||||
-- If the user has typed `:Foo action2 fi`,
|
||||
-- this will match 'first'
|
||||
return install_arg:find(subcmd_arg_lead) ~= nil
|
||||
end)
|
||||
:totable()
|
||||
end,
|
||||
-- ...
|
||||
},
|
||||
}
|
||||
<
|
||||
Then, create a Lua function to implement the main command:
|
||||
>lua
|
||||
---@param opts table :h lua-guide-commands-create
|
||||
local function foo_cmd(opts)
|
||||
local fargs = opts.fargs
|
||||
local subcommand_key = fargs[1]
|
||||
-- Get the subcommand's arguments, if any
|
||||
local args = #fargs > 1 and vim.list_slice(fargs, 2, #fargs) or {}
|
||||
local subcommand = subcommand_tbl[subcommand_key]
|
||||
if not subcommand then
|
||||
vim.notify("Foo: Unknown command: " .. subcommand_key, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
-- Invoke the subcommand
|
||||
subcommand.impl(args, opts)
|
||||
end
|
||||
<
|
||||
See also |lua-guide-commands-create|.
|
||||
|
||||
Finally, we register our command, along with the completions:
|
||||
>lua
|
||||
-- NOTE: the options will vary, based on your use case.
|
||||
vim.api.nvim_create_user_command("Foo", foo_cmd, {
|
||||
nargs = "+",
|
||||
desc = "My awesome command with subcommand completions",
|
||||
complete = function(arg_lead, cmdline, _)
|
||||
-- Get the subcommand.
|
||||
local subcmd_key, subcmd_arg_lead = cmdline:match("^Foo[!]*%s(%S+)%s(.*)$")
|
||||
if subcmd_key
|
||||
and subcmd_arg_lead
|
||||
and subcommand_tbl[subcmd_key]
|
||||
and subcommand_tbl[subcmd_key].complete
|
||||
then
|
||||
-- The subcommand has completions. Return them.
|
||||
return subcommand_tbl[subcmd_key].complete(subcmd_arg_lead)
|
||||
end
|
||||
-- Check if cmdline is a subcommand
|
||||
if cmdline:match("^Foo[!]*%s+%w*$") then
|
||||
-- Filter subcommands that match
|
||||
local subcommand_keys = vim.tbl_keys(subcommand_tbl)
|
||||
return vim.iter(subcommand_keys)
|
||||
:filter(function(key)
|
||||
return key:find(arg_lead) ~= nil
|
||||
end)
|
||||
:totable()
|
||||
end
|
||||
end,
|
||||
bang = true, -- If you want to support ! modifiers
|
||||
})
|
||||
<
|
||||
==============================================================================
|
||||
Keymaps *lua-plugin-keymaps*
|
||||
|
||||
Avoid creating keymaps automatically, unless they are not controversial. Doing
|
||||
so can easily lead to conflicts with user |mapping|s.
|
||||
|
||||
NOTE: An example for uncontroversial keymaps are buffer-local |mapping|s for
|
||||
specific file types or floating windows.
|
||||
|
||||
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.
|
||||
|
||||
However, doing so means that
|
||||
|
||||
- You will have to implement and document it yourself.
|
||||
- Users will likely face inconsistencies if another plugin has a slightly
|
||||
different DSL.
|
||||
- |init.lua| scripts that call such a `setup` function may throw an error if
|
||||
the plugin is not installed or disabled.
|
||||
|
||||
As an alternative, you can provide |<Plug>| mappings to allow users to define
|
||||
their own keymaps with |vim.keymap.set()|.
|
||||
|
||||
- This requires one line of code in user configs.
|
||||
- Even if your plugin is not installed or disabled, creating the keymap won't
|
||||
throw an error.
|
||||
|
||||
Another option is to simply expose a Lua function or |user-commands|.
|
||||
|
||||
However, some benefits of |<Plug>| mappings over this are that you can
|
||||
|
||||
- Enforce options like `expr = true`.
|
||||
- Expose functionality only for specific |map-modes|.
|
||||
- Expose different behavior for different |map-modes| with a single |<Plug>|
|
||||
mapping, without adding impurity or complexity to the underlying Lua
|
||||
implementation.
|
||||
|
||||
NOTE: If you have a function that takes a large options table, creating lots
|
||||
of |<Plug>| mappings to expose all of its uses could become
|
||||
overwhelming. It may still be beneficial to create some for the most
|
||||
common ones.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Example *lua-plugin-plug-mapping-example*
|
||||
|
||||
In your plugin:
|
||||
>lua
|
||||
vim.keymap.set("n", "<Plug>(SayHello)", function()
|
||||
print("Hello from normal mode")
|
||||
end, { noremap = true })
|
||||
|
||||
vim.keymap.set("v", "<Plug>(SayHello)", function()
|
||||
print("Hello from visual mode")
|
||||
end, { noremap = true })
|
||||
<
|
||||
In the user's config:
|
||||
>lua
|
||||
vim.keymap.set({"n", "v"}, "<leader>h", "<Plug>(SayHello)")
|
||||
<
|
||||
==============================================================================
|
||||
Initialization *lua-plugin-initialization*
|
||||
|
||||
Newcomers to Lua plugin development will often put all initialization logic in
|
||||
a single `setup` function, which takes a table of options.
|
||||
If you do this, users will be forced to call this function in order to use
|
||||
your plugin, even if they are happy with the default configuration.
|
||||
|
||||
Strictly separated configuration and smart initialization allow your plugin to
|
||||
work out of the box.
|
||||
|
||||
NOTE: A well designed plugin has minimal impact on startup time.
|
||||
See also |lua-plugin-lazy-loading|.
|
||||
|
||||
Common approaches to a strictly separated configuration are:
|
||||
|
||||
- A Lua function, e.g. `setup(opts)` or `configure(opts)`, which only overrides the
|
||||
default configuration and does not contain any initialization logic.
|
||||
- A Vimscript compatible table (e.g. in the |vim.g| or |vim.b| namespace) that your
|
||||
plugin reads from and validates at initialization time.
|
||||
See also |lua-vim-variables|.
|
||||
|
||||
Typically, automatic initialization logic is done in a |plugin| or |ftplugin|
|
||||
script. See also |'runtimepath'|.
|
||||
|
||||
==============================================================================
|
||||
Lazy loading *lua-plugin-lazy-loading*
|
||||
|
||||
When it comes to initializing your plugin, assume your users may not be using
|
||||
a plugin manager that takes care of lazy loading for you.
|
||||
Making sure your plugin does not unnecessarily impact startup time is your
|
||||
responsibility. A plugin's functionality may evolve over time, potentially
|
||||
leading to breakage if users have to hack into the loading mechanisms.
|
||||
Furthermore, a plugin that implements its own lazy initialization properly will
|
||||
likely have less overhead than the mechanisms used by a plugin manager or user
|
||||
to load that plugin lazily.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Defer `require` calls *lua-plugin-lazy-loading-defer-require*
|
||||
|
||||
|plugin| scripts should not eagerly `require` Lua modules.
|
||||
|
||||
For example, instead of:
|
||||
>lua
|
||||
local foo = require("foo")
|
||||
vim.api.nvim_create_user_command("MyCommand", function()
|
||||
foo.do_something()
|
||||
end, {
|
||||
-- ...
|
||||
})
|
||||
<
|
||||
which will eagerly load the `foo` module and any other modules it imports
|
||||
eagerly, you can lazy load it by moving the `require` into the command's
|
||||
implementation.
|
||||
>lua
|
||||
vim.api.nvim_create_user_command("MyCommand", function()
|
||||
local foo = require("foo")
|
||||
foo.do_something()
|
||||
end, {
|
||||
-- ...
|
||||
})
|
||||
<
|
||||
NOTE: For a Vimscript alternative to `require`, see |autoload|.
|
||||
|
||||
NOTE: In case you are worried about eagerly creating user commands, autocommands
|
||||
or keymaps at startup:
|
||||
Plugin managers that provide abstractions for lazy-loading plugins on
|
||||
such events will need to create these themselves.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Filetype-specific functionality *lua-plugin-lazy-loading-filetype*
|
||||
|
||||
Consider making use of |filetype| for any functionality that is specific to a
|
||||
filetype, by putting the initialization logic in a `ftplugin/{filetype}.lua`
|
||||
script.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Example *lua-plugin-lazy-loading-filetype-example*
|
||||
|
||||
A plugin tailored to Rust development might have initialization in
|
||||
`ftplugin/rust.lua`:
|
||||
>lua
|
||||
if not vim.g.loaded_my_rust_plugin then
|
||||
-- Initialize
|
||||
end
|
||||
-- NOTE: Using `vim.g.loaded_` prevents the plugin from initializing twice
|
||||
-- and allows users to prevent plugins from loading
|
||||
-- (in both Lua and Vimscript).
|
||||
vim.g.loaded_my_rust_plugin = true
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
-- do something specific to this buffer,
|
||||
-- e.g. add a |<Plug>| mapping or create a command
|
||||
vim.keymap.set("n", "<Plug>(MyPluginBufferAction)", function()
|
||||
print("Hello")
|
||||
end, { noremap = true, buffer = bufnr, })
|
||||
<
|
||||
==============================================================================
|
||||
Configuration *lua-plugin-configuration*
|
||||
|
||||
Once you have merged the default configuration with the user's config, you
|
||||
should validate configs.
|
||||
|
||||
Validations could include:
|
||||
|
||||
- Correct types, see |vim.validate()|
|
||||
- Unknown fields in the user config (e.g. due to typos).
|
||||
This can be tricky to implement, and may be better suited for a |health|
|
||||
check, to reduce overhead.
|
||||
|
||||
==============================================================================
|
||||
Troubleshooting *lua-plugin-troubleshooting*
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Health checks *lua-plugin-troubleshooting-health*
|
||||
|
||||
Provide health checks in `lua/{plugin}/health.lua`.
|
||||
|
||||
Some things to validate:
|
||||
|
||||
- User configuration
|
||||
- Proper initialization
|
||||
- Presence of Lua dependencies (e.g. other plugins)
|
||||
- Presence of external dependencies
|
||||
|
||||
See also |vim.health| and |health-dev|.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Minimal config template *lua-plugin-troubleshooting-minimal-config*
|
||||
|
||||
It can be useful to provide a template for a minimal configuration, along with
|
||||
a guide on how to use it to reproduce issues.
|
||||
|
||||
==============================================================================
|
||||
Versioning and releases *lua-plugin-versioning-releases*
|
||||
|
||||
Consider
|
||||
|
||||
- Using SemVer https://semver.org/ tags and releases to properly communicate
|
||||
bug fixes, new features, and breaking changes.
|
||||
- Automating versioning and releases in CI.
|
||||
- Publishing to luarocks https://luarocks.org, especially if your plugin
|
||||
has dependencies or components that need to be built; or if it could be a
|
||||
dependency for another plugin.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Further reading *lua-plugin-versioning-releases-further-reading*
|
||||
|
||||
- Luarocks <3 Nvim https://github.com/nvim-neorocks/sample-luarocks-plugin
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Tools *lua-plugin-versioning-releases-tools*
|
||||
|
||||
- luarocks-tag-release
|
||||
https://github.com/marketplace/actions/luarocks-tag-release
|
||||
- release-please-action
|
||||
https://github.com/marketplace/actions/release-please-action
|
||||
- semantic-release
|
||||
https://github.com/semantic-release/semantic-release
|
||||
|
||||
==============================================================================
|
||||
Documentation *lua-plugin-documentation*
|
||||
|
||||
Provide vimdoc (see |help-writing|), so that users can read your plugin's
|
||||
documentation in Nvim, by entering `:h {plugin}` in |command-mode|.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
Tools *lua-plugin-documentation-tools*
|
||||
|
||||
- panvimdoc https://github.com/kdheepak/panvimdoc
|
||||
|
||||
vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:
|
Loading…
Reference in New Issue
Block a user