From 6d3ff992ffef6b2eaf2cbe133a25e165cf0ae9a8 Mon Sep 17 00:00:00 2001 From: Marc Jakobi Date: Tue, 28 May 2024 20:47:49 +0200 Subject: [PATCH] docs: add guide for developing Lua plugins --- runtime/doc/lua-plugin.txt | 396 +++++++++++++++++++++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 runtime/doc/lua-plugin.txt diff --git a/runtime/doc/lua-plugin.txt b/runtime/doc/lua-plugin.txt new file mode 100644 index 0000000000..9aa93ef96a --- /dev/null +++ b/runtime/doc/lua-plugin.txt @@ -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 + 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 || 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 || 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 || + 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 || 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", "(SayHello)", function() + print("Hello from normal mode") + end, { noremap = true }) + + vim.keymap.set("v", "(SayHello)", function() + print("Hello from visual mode") + end, { noremap = true }) +< +In the user's config: +>lua + vim.keymap.set({"n", "v"}, "h", "(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 || mapping or create a command + vim.keymap.set("n", "(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: