Skip to content

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.

Use the philosophy rules:

  • Most users benefit? → core (lua/blak/plugins/ or lua/blak/core/).
  • Some users benefit, and the cost of carrying it is non-zero? → extra.
  • Just personal taste? → your own user.lua or plugins.specs.

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,
}

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:

  • cmd for command-driven tools.
  • ft for language or filetype-specific behavior.
  • event for UI that can appear after startup, such as VeryLazy.
  • keys when lazy.nvim owns the mapping.
  • lazy = true only 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.

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.

Terminal window
./dev-install.sh
blak-dev
:BlakExtras
:BlakExtras enable lang.zig
:Lazy sync " if plugins changed
:BlakToolsInstall " if mason changed
:BlakTreesitterInstall " if parsers changed
:BlakDoctor " confirm everything resolved

Then exercise the actual behavior — open a .zig file, check LSP attaches (:lua = vim.lsp.get_clients()), confirm formatting works.

Terminal window
make validate

The static checker enforces:

  • The require path in modules matches an actual file.
  • The file’s id field is unique across all extras.
  • Plugin specs lazy-load with cmd, event, ft, keys, or explicit lazy = true.
  • The file’s Lua syntax balances delimiters and keywords.

Then a smoke run:

Terminal window
make smoke

Boots Neovim headless, calls require("blak").setup(), and runs :checkhealth blak.

A few places, all in this repo:

  1. Add a dedicated page under docs/src/content/docs/extras/<group>/<name>.md.
  2. Link that page from the Extras guide and the docs sidebar in docs/astro.config.mjs.
  3. If the extra adds a keymap, mention it in the Keymaps page under “Keymaps added by extras.”

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.

After disabling an extra and restarting, its contributions should be absent unless another enabled extra or the user configuration also supplies them:

  • ✅ 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.