composer-ls is a language server for recipe group files. It runs
beside your Elixir language server (ElixirLS, Next LS, Expert), handles
only recipe references, and replaces nothing. It keeps your cookbooks
loaded in one BEAM, so there is no mix startup on each save.
It gives the editor:
- Diagnostics on open and on save: the same diagnostics as
mix composer.cookbook.lint --file(see the lint guide), in the saved file and in the files that include its recipes. When you fix a typo inlocal:ash/base, the "has 1 problem" summary disappears fromapp.exswithout opening it. - Hover on a reference: a step's metadata and docs, a recipe's
shortdoc, flags and entries, what a
hex:package (or an unqualified name that falls through to Hex) does, or an alias's target cookbook. A reference that doesn't resolve shows why. - Goto-definition: a step's source (also under
deps/), a recipe's key in its group file, or an alias's entry inaliases:(or the top-level binding it names).hex:references link to HexDocs. - Completion of step ids, recipe ids and the recipe's alias prefixes
in
steps:, and of recipe ids in{:use, ", even while the buffer doesn't parse. - Quick fixes that replace a typo with lint's suggestion.
Install
composer-ls needs the gen_lsp package, an optional dependency of
composer. Add it to the host project, next to composer:
{:composer, "~> 0.1", only: :dev},
{:gen_lsp, "~> 0.11.4", only: :dev}then mix deps.get. Hosts without it pay nothing; there,
mix composer.lsp says what to add and exits.
Start: the working directory is the host project
mix composer.lsp
starts the server on stdio. Your editor runs this for you.
Recipes refer to the cookbooks installed in a project, so the server needs one: the host project is the directory the editor starts the server in. Start your editor from the host project:
cd ~/src/my_app && nvim ~/.config/composer/recipes/auth.exs
There is no settings file or environment variable for this. A reference
valid in one host may be invalid in another, and that is expected: the
server names the host in a message at startup
(composer-ls: host project my_app at /home/you/src/my_app) and at the
foot of every hover.
Started outside a Mix project, the server exits at once with "must be run inside a Mix project".
Neovim
This uses vim.lsp.config and vim.lsp.enable, from Neovim 0.11:
vim.lsp.config("composer_ls", {
cmd = { "mix", "composer.lsp" },
-- Keep Mix's own output (compiling dependencies) off stdout.
cmd_env = { MIX_QUIET = "1" },
filetypes = { "elixir" },
-- Attach to recipe files only: local recipes and a cookbook's recipes.
-- The root is Neovim's working directory, the host project, so every
-- recipe buffer shares one client per host.
root_dir = function(bufnr, on_dir)
local name = vim.api.nvim_buf_get_name(bufnr)
if name:match("/composer/recipes/[^/]+%.exs$")
or name:match("/lib/composer/[^/]+/recipes/[^/]+%.exs$")
then
on_dir(vim.fn.getcwd())
end
end,
})
vim.lsp.enable("composer_ls")root_dir calls on_dir only for the two kinds of recipe file, so other
.exs files don't start the server. Neovim reuses a client whose name
and root match, so a local recipe and a cookbook's recipe opened in one
session share one composer_ls client. :checkhealth vim.lsp lists it.
Neovim's LSP mappings apply: K hovers and gra lists the quick fixes.
Map vim.lsp.buf.definition() (commonly to gd) for goto-definition.
Completion arrives through your completion plugin or
vim.lsp.completion.enable(). The HexDocs links of hex: references are
LSP document links, for clients and plugins that show them.
Turn off nvim-lint for these buffers
If you followed the lint guide and run composer.cookbook.lint
through nvim-lint on save, remove that autocmd: the server reports the
same diagnostics, and both together show each problem twice.
Reload after editing step code
Recipe edits need nothing: group files are read again on every lint.
Steps are compiled code, though, so after editing a step in a path-dep
cookbook, run the composer.reload command. It recompiles the project
and rediscovers the cookbooks, then reports "composer-ls: reloaded N
cookbooks" (or the compile error):
vim.api.nvim_create_user_command("ComposerReload", function()
for _, client in ipairs(vim.lsp.get_clients({ name = "composer_ls" })) do
client:exec_cmd({ title = "Reload cookbooks", command = "composer.reload" })
end
end, {})The server compiles only at startup and on reload, never while idle.
Restarting the server (:lsp restart in Neovim 0.12) does the same and
more; restart after changing composer itself.
Troubleshooting
Nothing attaches. Check :checkhealth vim.lsp. The buffer's path
must match one of the two root_dir patterns, and mix composer.lsp
must start in the host project: run it in a terminal there. It should
print nothing and wait for input; a message about gen_lsp means the
dependency is missing.
The wrong project, or unknown cookbooks. The startup message and the
hover footer name the host. Things that move Neovim's working directory
(:cd, autochdir, project-root plugins) before the first recipe buffer
opens give the server another project, or none.
The server died at startup, or garbage in the log. Only protocol
messages may go to stdout. The server sends its own logging, and any
output from steps, cookbooks or recipe group files, to stderr, which
Neovim writes to its LSP log (:lua vim.cmd.tabnew(vim.lsp.log.get_filename())).
Mix itself prints dependency compilation on stdout before the server
starts; MIX_QUIET=1 (in cmd_env above) silences it.
Stale step metadata or docs. Run composer.reload, or restart the
server. A reload while mix composer.recipe.run compiles in a terminal
may contend for _build; wait for the run to finish.
Diagnostics differ from the buffer. Diagnostics come from the file on disk, on open and on save, as lint's do. Hover and completion follow the unsaved buffer.