{"id":13752938,"url":"https://github.com/nvim-neorocks/nvim-best-practices","last_synced_at":"2025-04-09T06:07:51.339Z","repository":{"id":240097165,"uuid":"800343592","full_name":"nvim-neorocks/nvim-best-practices","owner":"nvim-neorocks","description":"Collection of DOs and DON'Ts for modern Neovim Lua plugin development","archived":false,"fork":false,"pushed_at":"2024-10-07T18:10:42.000Z","size":42,"stargazers_count":333,"open_issues_count":3,"forks_count":7,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-10-29T21:36:27.615Z","etag":null,"topics":["best-practices","development","guide","lua","neovim","nvim","plugin","plugin-development"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"cc0-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nvim-neorocks.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-14T06:45:21.000Z","updated_at":"2024-10-27T22:24:50.000Z","dependencies_parsed_at":"2024-05-21T21:23:08.878Z","dependency_job_id":"44415e04-4cfa-42e5-bb9f-9b55be6d618d","html_url":"https://github.com/nvim-neorocks/nvim-best-practices","commit_stats":{"total_commits":56,"total_committers":8,"mean_commits":7.0,"dds":0.3392857142857143,"last_synced_commit":"1af067a787a557093c27a1ba631b37dfbfdd24e1"},"previous_names":["nvim-neorocks/nvim-best-practices"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nvim-neorocks%2Fnvim-best-practices","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nvim-neorocks%2Fnvim-best-practices/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nvim-neorocks%2Fnvim-best-practices/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nvim-neorocks%2Fnvim-best-practices/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nvim-neorocks","download_url":"https://codeload.github.com/nvim-neorocks/nvim-best-practices/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247731115,"owners_count":20986601,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["best-practices","development","guide","lua","neovim","nvim","plugin","plugin-development"],"created_at":"2024-08-03T09:01:12.893Z","updated_at":"2025-04-09T06:07:51.323Z","avatar_url":"https://github.com/nvim-neorocks.png","language":null,"readme":"# :white_check_mark: :x: :moon: DOs and DON'Ts for modern Neovim Lua plugin development\n\n\u003e [!IMPORTANT]\n\u003e\n\u003e The code snippets in this document use the Neovim 0.10.0 API.\n\n\u003e [!NOTE]\n\u003e\n\u003e - For a guide to using Lua in Neovim,\n\u003e   please refer to [`:h lua-intro`](https://neovim.io/doc/user/lua.html).\n\u003e\n\u003e - For an example based on this guide, check out\n    [@ColinKennedy's plugin template](https://github.com/ColinKennedy/nvim-best-practices-plugin-template).\n\n## :safety_vest: Type safety\n\nLua, as a dynamically typed language, is great for configuration.\nIt provides virtually immediate feedback.\n\n### :x: DON'T\n\n...make your plugin susceptible to unexpected bugs at the wrong time.\n\n### :white_check_mark: DO\n\n...leverage [LuaCATS annotations](https://luals.github.io/wiki/annotations/),\nalong with [lua-language-server](https://github.com/LuaLS/lua-language-server) to\ncatch potential bugs in your CI before your plugin's users do.\n\n#### :hammer_and_wrench: Tools\n\n- [lua-typecheck-action](https://github.com/marketplace/actions/lua-typecheck-action)\n- [llscheck](https://github.com/jeffzi/llscheck)\n- [luacheck](https://github.com/mpeterv/luacheck) for additional linting.\n- [lazydev.nvim](https://github.com/folke/lazydev.nvim)\n\nFor Nix users:\n\n- [nix-gen-luarc-json](https://github.com/mrcjkb/nix-gen-luarc-json),\n  which can be used to integrate with [git-hooks.nix](https://github.com/cachix/git-hooks.nix).\n\n#### :books: Further reading\n\n- [LuaCATS documentation](https://luals.github.io/wiki/annotations/)\n- [Algebraic data types in Lua (Almost)](https://mrcjkb.dev/posts/2023-08-17-lua-adts.html)\n\n## :speaking_head: User Commands\n\n### :x: DON'T\n\n...pollute the command namespace with a command for each action.\n\n**Example:**\n\n- `:RocksInstall {arg}`\n- `:RocksPrune {arg}`\n- `:RocksUpdate`\n- `:RocksSync`\n\nThis can quickly become overwhelming when users rely on\ncommand completion.\n\n### :white_check_mark: DO\n\n...gather subcommands under scoped commands\nand implement completions for each subcommand.\n\n**Example:**\n\n- `:Rocks install {arg}`\n- `:Rocks prune {arg}`\n- `:Rocks update`\n- `:Rocks sync`\n\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cb\u003eScreenshots\u003c/b\u003e\n  \u003c/summary\u003e\n\n  Subcommand completions:\n\n  ![](https://github.com/mrcjkb/nvim-best-practices/assets/12857160/33bf7825-b08b-427b-aa55-c51b8b10a361)\n\n  Argument completions:\n\n  ![](https://github.com/mrcjkb/nvim-best-practices/assets/12857160/e8c8de05-f2aa-477b-82e5-f983775d5fd3)\n\u003c/details\u003e\n\n\u003e [!TIP]\n\u003e\n\u003e There exists a Lua library, [`mega.cmdparse`](https://github.com/ColinKennedy/mega.cmdparse),\n\u003e which can take care of much of the boilerplate for you\n\u003e and comes with many features.\n\nHere's a basic example of how to implement completions manually.\nIn this example, we want to \n\n- provide *subcommand completions* if the user has typed \n  `:Rocks ...`\n- *argument completions* if they have typed `:Rocks {subcommand} ...`\n\nFirst, define a type for each subcommand, which has:\n\n- An implementation, a function which is called when executing\n  the subcommand.\n- Optional completions for the subcommand's arguments.\n\n```lua\n---@class MyCmdSubcommand\n---@field impl fun(args:string[], opts: table) The command implementation\n---@field complete? fun(subcmd_arg_lead: string): string[] (optional) Command completions callback, taking the lead of the subcommand's arguments\n```\n\nNext, we define a table mapping subcommands to their\nimplementations and completions:\n\n```lua\n---@type table\u003cstring, MyCmdSubcommand\u003e\nlocal subcommand_tbl = {\n    update = {\n        impl = function(args, opts)\n          -- Implementation (args is a list of strings)\n        end,\n        -- This subcommand has no completions\n    },\n    install = {\n        impl = function(args, opts)\n            -- Implementation\n        end,\n        complete = function(subcmd_arg_lead)\n            -- Simplified example\n            local install_args = {\n                \"neorg\",\n                \"rest.nvim\",\n                \"rustaceanvim\",\n            }\n            return vim.iter(install_args)\n                :filter(function(install_arg)\n                    -- If the user has typed `:Rocks install ne`,\n                    -- this will match 'neorg'\n                    return install_arg:find(subcmd_arg_lead) ~= nil\n                end)\n                :totable()\n        end,\n        -- ...\n    },\n}\n```\n\nThen, create a lua function to implement the main command:\n\n```lua\n---@param opts table :h lua-guide-commands-create\nlocal function my_cmd(opts)\n    local fargs = opts.fargs\n    local subcommand_key = fargs[1]\n    -- Get the subcommand's arguments, if any\n    local args = #fargs \u003e 1 and vim.list_slice(fargs, 2, #fargs) or {}\n    local subcommand = subcommand_tbl[subcommand_key]\n    if not subcommand then\n        vim.notify(\"Rocks: Unknown command: \" .. subcommand_key, vim.log.levels.ERROR)\n        return\n    end\n    -- Invoke the subcommand\n    subcommand.impl(args, opts)\nend\n```\n\nFinally, we register our command, along with the completions:\n\n```lua\n-- NOTE: the options will vary, based on your use case.\nvim.api.nvim_create_user_command(\"Rocks\", my_cmd, {\n    nargs = \"+\",\n    desc = \"My awesome command with subcommand completions\",\n    complete = function(arg_lead, cmdline, _)\n        -- Get the subcommand.\n        local subcmd_key, subcmd_arg_lead = cmdline:match(\"^['\u003c,'\u003e]*Rocks[!]*%s(%S+)%s(.*)$\")\n        if subcmd_key \n            and subcmd_arg_lead \n            and subcommand_tbl[subcmd_key] \n            and subcommand_tbl[subcmd_key].complete\n        then\n            -- The subcommand has completions. Return them.\n            return subcommand_tbl[subcmd_key].complete(subcmd_arg_lead)\n        end\n        -- Check if cmdline is a subcommand\n        if cmdline:match(\"^['\u003c,'\u003e]*Rocks[!]*%s+%w*$\") then\n            -- Filter subcommands that match\n            local subcommand_keys = vim.tbl_keys(subcommand_tbl)\n            return vim.iter(subcommand_keys)\n                :filter(function(key)\n                    return key:find(arg_lead) ~= nil\n                end)\n                :totable()\n        end\n    end,\n    bang = true, -- If you want to support ! modifiers\n})\n```\n\n#### :books: Further reading\n\n- `:h lua-guide-commands-create`\n\n## :keyboard: Keymaps\n\n### :x: DON'T\n\n...create any keymaps automatically (unless they are not controversial).\nThis can easily lead to conflicts.\n\n### :x: DON'T\n\n...define a fancy DSL for enabling keymaps via a `setup` function.\n\n- You will have to implement and document it yourself.\n- What if someone else comes up with a slightly different DSL?\n  This will lead to inconsistencies and confusion.\n  Here are 3 differing implementations:\n    - [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim/tree/96610122a40f0bb4ce2e452c6d2429bf093d6700?tab=readme-ov-file#telescope-setup-structure)\n    - [nvim-treesitter-textobjects (legacy)](https://github.com/nvim-treesitter/nvim-treesitter-textobjects/tree/84cc9ed772f1fee2f47c1e076f518829583d8347?tab=readme-ov-file#text-objects-select)\n    - [nvim-treesitter-refactor (legacy)](https://github.com/nvim-treesitter/nvim-treesitter-refactor/tree/65ad2eca822dfaec2a3603119ec3cc8826a7859e?tab=readme-ov-file#smart-rename)\n- If a user has to call a `setup` function to set keymaps,\n  it will cause an error if your plugin is not installed or disabled.\n- Neovim already has a built-in API for this.\n\n### :white_check_mark: DO\n\n...provide `:h \u003cPlug\u003e` mappings to allow users to define their own keymaps.\n\n- It requires one line of code in user configs.\n- Even if your plugin is not installed or disabled,\n  creating the keymap won't lead to an error.\n\n**Example:**\n\nIn your plugin:\n\n```lua\nvim.keymap.set(\"n\", \"\u003cPlug\u003e(MyPluginAction)\", function() print(\"Hello\") end, { noremap = true })\n```\n\nIn the user's config:\n\n```lua\nvim.keymap.set(\"n\", \"\u003cleader\u003eh\", \"\u003cPlug\u003e(MyPluginAction)\")\n```\n\n\u003e [!TIP]\n\u003e\n\u003e Some benefits of `\u003cPlug\u003e` mappings over exposing a lua function:\n\u003e \n\u003e - You can enforce options like `expr = true`.\n\u003e - Expose functionality only or specific modes (`:h map-modes`).\n\u003e - Expose different behaviour for different modes with\n\u003e   a single `\u003cPlug\u003e` mapping.\n\nFor example, in your plugin:\n\n```lua\nvim.keymap.set(\"n\", \"\u003cPlug\u003e(SayHello)\", function() \n    print(\"Hello from normal mode\") \nend, { noremap = true })\n\nvim.keymap.set(\"v\", \"\u003cPlug\u003e(SayHello)\", function() \n    print(\"Hello from visual mode\") \nend, { noremap = true })\n```\n\nIn the user's config:\n\n```lua\nvim.keymap.set({\"n\", \"v\"}, \"\u003cleader\u003eh\", \"\u003cPlug\u003e(SayHello)\")\n```\n\n### :white_check_mark: DO\n\n...just expose a Lua API that people can use to define keymaps, if\n\n- You have a function that takes a large options table,\n  and it would require lots of `\u003cPlug\u003e` mappings to expose all of its uses\n  (You could still create some for the most common ones).\n- You don't care about people who prefer Vimscript for configuration.\n\nAnother alternative is just to expose user commands.\n\n## :zap: Initialization\n\n### :x: DON'T\n\n...force users to call a `setup` function\nin order to be able to use your plugin.\n\n\u003e [!WARNING]\n\u003e\n\u003e This one often sparks heated debates.\n\u003e I have written in detail about the various reasons \n\u003e why this is an anti pattern [here](https://mrcjkb.dev/posts/2023-08-22-setup.html).\n\u003e\n\u003e - If you still disagree, feel free to open an issue.\n\nThese are the rare cases in which a `setup` function \nfor initialization could be useful:\n\n- You want your plugin to be compatible with Neovim \u003c= 0.6.\n- *And* your plugin is actually multiple plugins in a monorepo.\n- *Or* you're integrating with another plugin that *forces* you to do so.\n\n### :white_check_mark: DO\n\n- Cleanly separate configuration and initialization.\n- Automatically initialize your plugin *(smartly)*,\n  with minimal impact on startup time (see the next section).\n\n## :sleeping_bed: Lazy loading\n\n### :x: DON'T\n\n...rely on plugin managers to take care of lazy loading for you.\n\n- Making sure a plugin doesn't unnecessarily impact startup time\n  should be the responsibility of plugin authors, not users.\n- A plugin's functionality may evolve over time, potentially\n  leading to breakage if it's the user who has to worry about\n  lazy loading.\n- A plugin that implements its own lazy initialization properly\n  will likely have less overhead than the mechanisms used by a\n  plugin manager or user to load that plugin lazily.\n\n### :white_check_mark: DO\n\n...think carefully about when which parts of your plugin need to be loaded.\n\n#### Is there any functionality that is specific to a filetype?\n\n- Put your initialization logic in a `ftplugin/{filetype}.lua` script.\n- See [`:h filetype`](https://neovim.io/doc/user/filetype.html#%3Afiletype).\n\n**Example:**\n\n```lua\n-- ftplugin/rust.lua\nif not vim.g.loaded_my_rust_plugin then\n    -- Initialise\nend\n-- NOTE: Using vim.g.loaded_ prevents the plugin from initializing twice\n-- and allows users to prevent plugins from loading (in both Lua and Vimscript).\nvim.g.loaded_my_rust_plugin = true\n\nlocal bufnr = vim.api.nvim_get_current_buf()\n-- do something specific to this buffer, e.g. add a \u003cPlug\u003e mapping or create a command\nvim.keymap.set(\"n\", \"\u003cPlug\u003e(MyPluginBufferAction)\", function() \n    print(\"Hello\")\nend, { noremap = true, buffer = bufnr, })\n```\n\n#### Is your plugin *not* filetype-specific, but it likely won't be needed every single time a user opens a Neovim session?\n\nDon't eagerly `require` your lua modules.\n\n**Example:**\n\nInstead of:\n\n```lua\nlocal foo = require(\"foo\")\nvim.api.nvim_create_user_command(\"MyCommand\", function()\n    foo.do_something()\nend, {\n  -- ...\n})\n```\n\n...which will eagerly load the `foo` module,\nand any modules it eagerly imports, you can lazy load it\nby moving the `require` into the command's implementation.\n\n```lua\nvim.api.nvim_create_user_command(\"MyCommand\", function()\n    local foo = require(\"foo\")\n    foo.do_something()\nend, {\n  -- ...\n})\n```\n\n\u003e [!TIP]\n\u003e\n\u003e For a Vimscript equivalent to `require`, see `:h autoload`.\n\n\u003e [!NOTE]\n\u003e\n\u003e - What about eagerly creating user commands at startup?\n\u003e - Wouldn't it be better to rely on a plugin manager to lazy load my plugin\n\u003e   via a user command and/or autocommand?\n\u003e\n\u003e No! To be able to lazy load your plugin with a user command,\n\u003e a plugin manager [has to itself create a user command](https://github.com/folke/lazy.nvim/blob/e44636a43376e8a1e851958f7e9cbe996751d59f/lua/lazy/core/handler/cmd.lua#L16).\n\u003e This helps for plugins that don't implement proper lazy loading,\n\u003e but it just adds overhead for those that do.\n\u003e The same applies to [autocommands](https://github.com/folke/lazy.nvim/blob/e44636a43376e8a1e851958f7e9cbe996751d59f/lua/lazy/core/handler/event.lua#L68),\n\u003e [keymaps](https://github.com/folke/lazy.nvim/blob/e44636a43376e8a1e851958f7e9cbe996751d59f/lua/lazy/core/handler/keys.lua#L112),\n\u003e etc.\n\n## :wrench: Configuration\n\n### :white_check_mark: DO\n\n...use [LuaCATS annotations](https://luals.github.io/wiki/annotations/)\nto make your API play nicely with lua-language-server, while\nproviding type safety.\n\nOne of the largest foot guns in Lua is `nil`.\nYou should avoid it in your internal configuration.\nOn the other hand, users don't want to have to set every possible field.\nIt is convenient for them to provide a default configuration and merge it\nwith an override table.\n\nThis is a common practice:\n\n```lua\n---@class myplugin.Config\n---@field do_something_cool boolean\n---@field strategy \"random\" | \"periodic\"\n\n---@type myplugin.Config\nlocal default_config = {\n    do_something_cool = true,\n    strategy = \"random\",\n}\n\n-- could also be passed in via a function. But there's no real downside to using `vim.g` or `vim.b`.\nlocal user_config = ...\nlocal config = vim.tbl_deep_extend(\"force\", default_config, user_config or {})\nreturn config\n```\n\nIn this example, a user can override only individual configuration fields:\n\n```lua\n{\n    strategy = \"periodic\"\n}\n```\n\n...leaving the unset fields as their default.\nHowever, if they have lua-language-server configured to pick up your plugin\n(for example, using [neodev.nvim](https://github.com/folke/neodev.nvim)),\nit will show them a warning like this:\n\n```lua\n{ -- ⚠ Missing required fields in type `myplugin.Config`: `do_something_cool`\n    strategy = \"periodic\"\n}\n```\n\nTo mitigate this, you can split configuration *option* declarations\nand internal configuration *values*.\n\nThis is how I like to do it:\n\n```lua\n-- config/meta.lua\n\n---@class myplugin.Config\n---@field do_something_cool? boolean (optional) Notice the `?`\n---@field strategy? \"random\" | \"periodic\" (optional)\n\n-- Side note: I prefer to use `vim.g` or `vim.b` tables (:h lua-vim-variables).\n-- You can also use a lua function but there's no real downside to using `vim.g` or `vim.b`\n-- and it doesn't throw an error if your plugin is not installed.\n-- This annotation says that`vim.g.my_plugin` can either be a `myplugin.Config` table, or\n-- a function that returns one, or `nil` (union type).\n---@type myplugin.Config | fun():myplugin.Config | nil\nvim.g.my_plugin = vim.g.my_plugin\n\n--------------------------------------------------------------\n-- config/internal.lua\n\n---@class myplugin.InternalConfig\nlocal default_config = {\n    ---@type boolean\n    do_something_cool = true,\n    ---@type \"random\" | \"periodic\"\n    strategy = \"random\",\n}\n\nlocal user_config = type(vim.g.my_plugin) == \"function\" and vim.g.my_plugin() or vim.g.my_plugin or {}\n\n---@type myplugin.InternalConfig\nlocal config = -- ...merge configs\n```\n\n\u003e [!IMPORTANT]\n\u003e\n\u003e This does have some downsides:\n\u003e\n\u003e - You have to maintain two configuration types.\n\u003e - As this is fairly uncommon, first time contributors will often overlook one\n\u003e   of the configuration types.\n\u003e\n\u003e Since this provides increased type safety for both the plugin\n\u003e and the user's config, I believe it is well worth the slight inconvenience.\n\n### :white_check_mark: DO\n\n...validate configs.\n\nOnce you have merged the default configuration with the user's config,\nyou should validate configs.\n\nValidations could include:\n\n- Correct types, see `:h vim.validate`\n- Unknown fields in the user config (e.g. due to typos).\n  This can be tricky to implement, and may be better suited for\n  a health check, to reduce overhead.\n\n\u003e [!WARNING]\n\u003e\n\u003e `vim.validate` will `error` if it fails a validation.\n\nBecause of this, I like to wrap it with `pcall`,\nand add the path to the field in the config\ntable to the error message:\n\n```lua\n---@param path string The path to the field being validated\n---@param tbl table The table to validate\n---@see vim.validate\n---@return boolean is_valid\n---@return string|nil error_message\nlocal function validate_path(path, tbl)\n  local ok, err = pcall(vim.validate, tbl)\n  return ok, err and path .. \".\" .. err\nend\n```\n\nThe function can be called like this:\n\n```lua\n---@param cfg myplugin.InternalConfig\n---@return boolean is_valid\n---@return string|nil error_message\nfunction validate(cfg)\n    return validate_path(\"vim.g.my_plugin\", {\n        do_something_cool = { cfg.do_something_cool, \"boolean\" },\n        strategy = { cfg.strategy, \"string\" },\n    })\nend\n```\n\nAnd invalid config will result in an error message like\n`\"vim.g.my_plugin.strategy: expected string, got number\"`.\n\nBy doing this, you can use the validation with both \n[`:h vim.notify`](https://neovim.io/doc/user/lua.html#vim.notify()) and [`:h vim.health`](https://neovim.io/doc/user/pi_health.html#health-functions).\n\n## :stethoscope: Troubleshooting\n\n### :white_check_mark: DO\n\n...provide health checks in `lua/{plugin}/health.lua`.\n\nSome things to validate:\n\n- User configuration\n- Proper initialization\n- Presence of lua dependencies\n- Presence of external dependencies\n\n#### :books: Further reading\n\n- [`:h vim.health`](https://neovim.io/doc/user/pi_health.html#health-functions)\n\n## :hash: Versioning and releases\n\n### :x: DON'T\n\n...use [0ver](https://0ver.org/) or omit versioning completely,\ne.g. because you believe doing so is a commitment to stability.\n\n\u003e [!TIP]\n\u003e\n\u003e Doing this won't make people any happier about breaking changes.\n\n### :white_check_mark: DO\n\n...use [SemVer](https://semver.org/) to properly communicate\nbug fixes, new features, and breaking changes.\n\n#### :books: Further reading\n\n- [Just use Semantic Versioning](https://vhyrro.github.io/posts/versioning/).\n\n### :white_check_mark: DO\n\n...automate versioning and releases, and publish to luarocks.org.\n\n#### :books: Further reading\n\n- [rocks.nvim/Introduction](https://github.com/nvim-neorocks/rocks.nvim?tab=readme-ov-file#moon-introduction)\n- [Luarocks :purple_heart: Neovim](https://github.com/nvim-neorocks/sample-luarocks-plugin)\n\n#### :hammer_and_wrench: Tools\n\n- [luarocks-tag-release](https://github.com/marketplace/actions/luarocks-tag-release)\n- [release-please-action](https://github.com/googleapis/release-please-action)\n- [semantic-release](https://github.com/semantic-release/semantic-release)\n\n## :notebook: Documentation\n\n### :white_check_mark: DO\n\n...provide vimdoc, so that users can read your plugin's documentation in Neovim,\nby entering `:h {plugin}`.\n\n### :x: DON'T\n\n...simply dump generated references in your `doc` directory.\n\n#### :hammer_and_wrench: Tools\n\n- [vimCATS](https://github.com/mrcjkb/vimcats)\n- [panvimdoc](https://github.com/kdheepak/panvimdoc)\n\n#### :books: Further reading\n\n- [Diátaxis - A systematic approach to technical documentation authoring](https://diataxis.fr/)\n\n## :test_tube: Testing\n\n### :white_check_mark: DO\n\n...automate testing as much as you can.\n\n### :x: DON'T\n\n...use plenary.nvim for testing.\n\nHistorically, `plenary.test` has been very popular for testing,\nbecause there was no convenient way for using Neovim as a lua interpreter.\nThat has changed with the introduction of `nvim -l` in Neovim 0.9.\n\nWhile plenary.nvim is still being maintained, much of its functionality\nis [gradually being upstreamed into Neovim or moved into other libraries](https://github.com/nvim-telescope/telescope.nvim/issues/2552).\n\n### :white_check_mark: DO\n\n...use [busted](https://github.com/lunarmodules/busted) for testing,\nwhich is a lot more powerful. \n\n\u003e [!NOTE]\n\u003e \n\u003e plenary.nvim bundles a *limited subset* of luassert.\n\nWe advocate for using luarocks + busted\nfor testing, primarily for the following reasons:\n\n- **Familiarity:**\n  It is the de facto tried and tested standard in the Lua community,\n  with a familiar API modeled after [rspec](https://rspec.info/).\n- **Consistency:**\n  Having a consistent API makes life easier for\n    - Contributors to your project.\n    - Package managers, who [run test suites](https://github.com/NixOS/nixpkgs/blob/7feaafb5f1dea9eb74186d8c40c2f6c095904429/pkgs/development/lua-modules/overrides.nix#L581)\n    to ensure they don't deliver a broken version of your plugin.\n- **Better reproducibility:**\n  By using luarocks to manage your test dependencies, you can easily\n  pin them. Checking out git repositories is prone to flakes in CI\n  and \"it works on my machine\" issues.\n- **Dependency management**:\n  With luarocks, you have the whole\n  ecosystem of Lua modules at your\n  fingertips, making it easy to manage and\n  integrate dependencies for your project.\n\n\u003e [!TIP]\n\u003e\n\u003e For combining busted with other test frameworks,\n\u003e check out our [busted interop examples](https://github.com/nvim-neorocks/busted-interop-examples).\n\n#### :page_facing_up: Template\n\n- [`nvim-lua/nvim-lua-plugin-template`](https://github.com/nvim-lua/nvim-lua-plugin-template/)\n\n#### :books: Further reading\n\n- [Using Neovim as Lua interpreter with Luarocks](https://zignar.net/2023/01/21/using-luarocks-as-lua-interpreter-with-luarocks/)\n- [Testing Neovim plugins with Busted](https://hiphish.github.io/blog/2024/01/29/testing-neovim-plugins-with-busted/)\n- [Test your Neovim plugins with luarocks \u0026 busted](https://mrcjkb.dev/posts/2023-06-06-luarocks-test.html)\n- [Debugging Lua in Neovim](https://zignar.net/2023/06/10/debugging-lua-in-neovim/)\n\n#### :hammer_and_wrench: Tools\n\n- [`nvim-busted-action`](https://github.com/marketplace/actions/nvim-busted-action)\n- [`nlua`](https://github.com/mfussenegger/nlua)\n- [`neorocksTest`](https://github.com/nvim-neorocks/neorocks) (for Nix users)\n- [Neotest](https://github.com/nvim-neotest/neotest) adapters:\n  - [`HiPhish/neotest-busted`](https://gitlab.com/HiPhish/neotest-busted)\n  - [`MisanthropicBit/neotest-busted`](https://github.com/MisanthropicBit/neotest-busted)\n\n## :electric_plug: Integrating with other plugins\n\n### :white_check_mark: DO\n\n...consider integrating with other plugins.\n\nFor example, it might be useful to add\na [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) extension\nor a [lualine](https://github.com/nvim-lualine/lualine.nvim) component.\n\n\u003e [!TIP]\n\u003e\n\u003e If you don't want to commit to maintaining compatibility with another plugin's API,\n\u003e you can expose your own API for others to hook into.\n","funding_links":[],"categories":["Others"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnvim-neorocks%2Fnvim-best-practices","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnvim-neorocks%2Fnvim-best-practices","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnvim-neorocks%2Fnvim-best-practices/lists"}