Writing an extra
Extras are the right place for any capability that doesn’t belong in core — language support, optional plugins, alternative providers, opinionated tweaks. The contract is small and the reward is automatic: enable / disable from :BlakExtras works for free.
When something should be an extra
Use the philosophy rules:
- Most users benefit? → core (
lua/blak/plugins/orlua/blak/core/). - Some users benefit, and the cost of carrying it is non-zero? → extra.
- Just personal taste? → your own
user.luaorplugins.specs.
Anatomy
Create a file at lua/blak/extras/<group>/<name>.lua. Groups today: lang, ui, git, ai, editor. Add a new group folder if your extra doesn’t fit.
The file returns a single table:
return { id = "lang.zig", -- unique, dotted, matches path label = "Zig", -- short human label description = "zls + treesitter", -- one line shown in :BlakExtras
-- Any of these are optional; include only the ones your extra needs. treesitter = { "zig" }, -- merged into ensure_installed mason = { "zls" }, -- merged into mason.ensure_installed lsp = { servers = { zls = { settings = { zls = { enable_inlay_hints = true } }, }, }, }, format = { formatters_by_ft = { zig = { "zigfmt" } }, }, lint = { linters_by_ft = { zig = { "ziggrep" } }, }, snacks = { dim = { enabled = true }, -- deep-merged into snacks opts }, keys = { -- show up in :BlakKeys { lhs = "<leader>cc", rhs = ":!zig build<CR>", desc = "zig build" }, }, plugins = { -- lazy.nvim specs added to the set { "ziglang/zig.vim", ft = "zig" }, }, apply = function(config) -- runs after merge, before plugin load -- escape hatch for anything the declarative fields don't cover config.snacks.scroll = { enabled = false } end,}Performance contract
Extras are opt-in, but they still must not become surprise startup cost. Every plugin spec in an extra must lazy-load with one of these:
cmdfor command-driven tools.ftfor language or filetype-specific behavior.eventfor UI that can appear after startup, such asVeryLazy.keyswhen lazy.nvim owns the mapping.lazy = trueonly when Blak keymaps or provider code explicitly load the plugin on demand.
Avoid lazy = false in extras. If a feature truly has to load at startup, it probably belongs in core or needs a design discussion first.
Register it
Add the require path to the module list in lua/blak/extras/init.lua:
local modules = { -- ... "blak.extras.lang.zig",}That’s the entire registration. The registry caches results and looks up extras by their id field, so the order of modules doesn’t matter.
Test it locally
./dev-install.shblak-dev:BlakExtras:BlakExtras enable lang.zig:Lazy sync " if plugins changed:BlakToolsInstall " if mason changed:BlakTreesitterInstall " if parsers changed:BlakDoctor " confirm everything resolvedThen exercise the actual behavior — open a .zig file, check LSP attaches (:lua = vim.lsp.get_clients()), confirm formatting works.
Validate
make validateThe static checker enforces:
- The require path in
modulesmatches an actual file. - The file’s
idfield is unique across all extras. - Plugin specs lazy-load with
cmd,event,ft,keys, or explicitlazy = true. - The file’s Lua syntax balances delimiters and keywords.
Then a smoke run:
make smokeBoots Neovim headless, calls require("blak").setup(), and runs :checkhealth blak.
Document it
A few places, all in this repo:
- Add a dedicated page under
docs/src/content/docs/extras/<group>/<name>.md. - Link that page from the Extras guide and the docs sidebar in
docs/astro.config.mjs. - If the extra adds a keymap, mention it in the Keymaps page under “Keymaps added by extras.”
Open a PR
Describe what the extra adds and why it isn’t a core feature. The bar for core is high — extras are the path of least resistance.
Reversibility checklist
When the extra is disabled, every one of these should be undone automatically because the runtime composes contributions from enabled() on each setup:
- ✅ Treesitter parsers — removed from the install list (already-installed parsers stay until you uninstall manually; that’s fine).
- ✅ Mason tools — removed from the install list.
- ✅ LSP servers — the
vim.lsp.config()entry isn’t registered. - ✅ Formatters / linters — not added to the by-filetype maps.
- ✅ Keymaps — not registered.
- ✅ Plugin specs — lazy.nvim removes them on the next
:Lazy sync.
If your extra has side effects beyond these (writes a file, mutates a global), undo them in a second function or document them in the extras guide so users know.