An in-process LSP formatting server for Neovim. Runs as a native LSP client — no external binary needed.
Supports CLI formatters (biome, prettier, stylua), LSP code actions (organize imports), and chaining multiple steps in a pipeline. Automatically resolves local node_modules/.bin binaries and detects config files.
Requires Neovim 0.12+.
With vim.pack:
vim.pack.add("https://github.com/sindrip/formatls.nvim")With lazy.nvim:
{ "sindrip/formatls.nvim" }vim.lsp.config("formatls", {
init_options = {
formatters_by_ft = {
typescript = {
-- try biome first
{ "biome" },
-- fall back to organize imports + prettier
{ "source.organizeImports", "prettier" },
},
go = {
-- organize imports + format via gopls
{ "source.organizeImports", "textDocument/formatting" },
},
lua = {
{ "stylua" },
},
},
},
})
vim.lsp.enable("formatls")Then format with vim.lsp.buf.format() or your preferred keymap.
formatls registers as an LSP server that advertises documentFormattingProvider. It proxies formatting capabilities from other LSP servers on the buffer so that all format requests go through formatls.
Each filetype maps to a list of groups. Groups are tried in order — the first group where all CLI formatters are available is used.
A group is a list of steps, executed sequentially:
| Step | Description |
|---|---|
"biome", "prettier", "stylua" |
CLI formatter — resolved via spec |
"source.organizeImports", "source.fixAll" |
LSP code action — sent via textDocument/codeAction |
"textDocument/formatting" |
LSP formatting — delegated to a server's formatter |
For each CLI formatter, formatls:
- Looks up the formatter spec by name (e.g.
require("formatls.formatters.biome")) - Searches for a local binary in
node_modules/.bin/(walking up the directory tree) - Falls back to a global binary on
$PATH - Checks for required config files (e.g.
biome.jsonfor biome)
If any step fails, the group is skipped and the next group is tried.
When no groups are configured for a filetype (or none are viable), formatls delegates directly to the original LSP server's formatter.
Define custom formatter specs inline:
vim.lsp.config("formatls", {
init_options = {
formatters = {
my_formatter = {
cmd = "my-formatter",
args = function(path)
return { "--stdin-filepath", path }
end,
-- optional: required config files (skip if none found)
config_files = { ".my-formatter.json" },
},
},
formatters_by_ft = {
python = {
{ "my_formatter" },
},
},
},
})- conform.nvim — a more mature formatting plugin with a large collection of built-in formatter definitions. The built-in formatter specs in formatls are based on the hard work done by conform and its maintainers.