Open-source project
lukas-reineke/lsp-format.nvim avatar
lukas-reineke/lsp-format.nvim

lsp-format.nvim: format on save through your LSP servers without blocking the editor

A wrapper around Neovims native LSP formatting.

593 stars30 forksLuaLicense varies

At a glance

What is it?
lsp-format.nvim is a Neovim plugin that wraps the editor's native LSP formatting so a buffer is formatted when you save it, asynchronously by default, with per-server exclusion, server ordering and an explicit opt-in to synchronous formatting. It formats nothing itself: you still need an LSP server that does the actual work.
Who is it for?
lsp-format.nvim suits a Neovim user who already runs one or more language servers and wants format on save without the editor blocking, since the default asynchronous path only writes the result back when the buffer has not changed since the request. It does not suit a setup with no LSP server, because the plugin formats nothing itself, and it does not suit someone who wants a single consistent formatter across languages when several servers can format the same buffer.
Can I use it commercially?
Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
Is it still maintained?
Yes. The repository last received commits 135 days ago.
What is it written in?
Mainly Lua, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The plugin formats nothing, it only schedules the requests

lsp-format.nvim is described as a wrapper around Neovim's native LSP formatting, and the single most important line in the README is the one under what it does not do: it does not provide any formatting by itself, and you still need to use an LSP server.

That determines what the project is and is not. There is no embedded formatter, no language detection, no fallback to a generic tool like prettier or gofmt. Everything the plugin does is deciding when to ask your attached language servers to format, and what to do with what they return.

What it does do is listed as five items. Asynchronous or synchronous formatting on save. Sequential formatting with all attached LSP servers. Commands for disabling formatting globally or per filetype. Making it easier to send format options to the LSP. And allowing you to exclude specific LSP servers from formatting.

The first item is the reason the plugin exists, and the FAQ explains why with the most direct comparison in the repository. The main difference between this plugin and an autocmd on BufWritePre calling vim.lsp.buf.format() is that LSP-format.nvim is async by default: it will format on save without blocking the editor.

Then the behaviour that follows from being async. When the formatting is done, LSP-format.nvim will only change the buffer if it did not change since the time formatting was called. That single rule is what makes async safe. If you typed while the formatter was working, the result is discarded rather than applied on top of your newer edits.

The FAQ is also honest that the end result is the same as the builtin approach. The plugin adds convenience with disable commands and format options, and it changes the blocking behaviour. That is the whole delta.

The licence is not identified in the project metadata, which is worth noting for anyone planning to ship it in a product, and the primary language is Lua.

Setup is two pieces, and the autocmd is the one people forget

To use LSP-format you have to run the setup function and register an LspAttach autocmd.

lua
require("lsp-format").setup {}

vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
    require("lsp-format").on_attach(client, args.buf)
  end,
})

The callback looks the client up by id from the autocmd arguments, asserts it exists, and hands both the client and the buffer to on_attach. That is the mechanism: on_attach is what registers the per-buffer formatting, and it has to be called for each client that attaches.

The README notes that is it, saving a buffer will format it now. The setup call takes an empty table in the minimal case, so the defaults are the configuration.

Installation is by whatever plugin manager you already use. With Packer it goes in init.lua.

lua
-- init.lua
require("packer").startup(function()
    use "lukas-reineke/lsp-format.nvim"
end)

With vim-plug it goes in init.vim.

vim
" init.vim
call plug#begin('~/.vim/plugged')
Plug 'lukas-reineke/lsp-format.nvim'
call plug#end()

The version floor is Neovim 0.7 or newer, which is a sensible requirement given the plugin depends on the LSP client API and on autocmds that arrived in that era.

The repository layout confirms this is a small, focused plugin: a lua/ directory for the source, a specs/ directory for tests, a doc/ directory, a Makefile for the test and lint targets, and configuration files for stylua, luacheck, the Lua language server and a cbfmt formatter config.

Four format options do most of the work

The README calls these special format options, and they are the entire configuration surface that most users need.

The exclude option is a table of LSP servers that should not format the buffer. The alternative is simply not calling on_attach for the clients you do not want used for formatting, which is a neat consequence of the API shape. If a project has a language server attached for diagnostics and a second one for formatting, exclude is the switch that separates them.

The order option is a table that determines the order formatting is requested from the LSP server. This matters when more than one server can format the same buffer, because the last one to respond wins the buffer, and the order option makes that deterministic instead of a race.

The sync option turns on synchronous formatting, and the editor will block until the formatting is done. That is the escape hatch for the case the README warns about below, and also for scripts and macros that need the formatted result to be present when the command returns.

The force option writes the format result to the buffer even if the buffer changed after the format request started. That is the opposite of the safe default described earlier, and it exists for the case where you would rather have the formatter's output win than your own mid-flight edit.

The two remaining pieces are the disable commands, which work globally or per filetype, and the format options that are passed through to the server.

The options mechanism is where the plugin earns its keep, and it is more general than it looks. You can pass format options into the setup function, or as arguments to the :Format command, and how the options look depends on the LSP server you are using. They can be a string, a number, a boolean, or a function that resolves to one of those.

The example given uses the efm-langserver, which uses template syntax with which you can define your own options.

lua
require("lsp-format").setup {
    typescript = {
        tab_width = function()
            return vim.opt.shiftwidth:get()
        end,
    },
    yaml = { tab_width = 2 },
}

So the width for TypeScript is computed at format time from the current shiftwidth setting, and YAML is formatted with two spaces. And you can run :Format tab_width=8 to overwrite the setting and format with 8 spaces.

The two mistakes that break format on save for everyone

The Notes section names two problems that produce the same symptom, a buffer that is not formatted, and both are worth reading before you configure the plugin.

The first is running formatting twice. You don't want to run formatting twice, and if you had setup formatting on save before, remove it. The README suggests checking whether something is listening on buffer write events with the :autocmd BufWritePre and :autocmd BufWritePost commands. That check is the diagnostic, and it is the thing to run if you install this plugin and nothing appears to happen.

The second is that :wq will not format when not using sync. Because default formatting is async, you can't save and quit in the same command: the formatting results will not get back in time and Neovim will close without applying the changes.

The fix is either to use the sync format option, or to add an abbreviation into your dotfiles that does the right thing when doing :wq.

lua
vim.cmd [[cabbrev wq execute "Format sync" <bar> wq]]

The abbreviation runs the Format command in sync mode before the write and quit. That is a small piece of configuration that solves a real annoyance, and it is the kind of detail that separates a plugin that works out of the box from one that works once you have read the notes.

The same problem appears in scripts and in any place where a command is expected to leave the buffer formatted when it returns. Async is the right default for interactive editing, where blocking on a network round trip to a language server would make the editor feel broken, but it is the wrong default for automation, and the plugin gives you both.

There is a broader lesson in the FAQ entry comparing this plugin to vim.lsp.buf.format(). The end result is the same. If you already have a working format-on-save autocommand and it does not bother you, this plugin's main contribution is the async default and the convenience commands, and the convenience commands are the reason to switch.

The test suite runs headless Neovim against Plenary

The Makefile shows how the project is verified, and it is a modern Neovim plugin setup.

make
test: dependencies
	@echo "Running lsp-format tests..."
	timeout 300 nvim -e \
		--headless \
		--noplugin \
		-u specs/spec.lua \
		-c "PlenaryBustedDirectory specs/features {minimal_init = 'specs/spec.lua'}"

The test target depends on a dependencies target that clones two plugins with depth 1 if the vendor directory does not already exist: plenary.nvim into .ci/vendor/pack/vendor/start/plenary.nvim and neodev.nvim into the same tree. The tests then run headless Neovim with the plenary busted directory runner over specs/features.

That is a standard arrangement for testing Neovim plugins, and the choice of plenary tells you the assertions are written in the busted style. The timeout of 300 seconds is a bound on the whole run, which matters for a plugin whose behaviour depends on an asynchronous language server responding.

The other Makefile targets are the quality gates. luacheck runs luacheck over the tree. stylua runs stylua in check mode, so the formatting is verified rather than applied. And the lua-language-server target runs the server in check mode against the project using a config file selected by version, .luarc.stable.json or .luarc.nightly.json, then fails the build if the server produced any diagnostics, by checking a JSON log with jq.

Two supporting files confirm the setup. There is a .luacheckrc for the linter and a stylua.toml plus a .styluaignore for the formatter. The .luarc.stable.json and .luarc.nightly.json pair is the interesting one: it lets the type check run against either the stable or the nightly Lua language server, which for a plugin that targets Neovim 0.7 and newer is the way to catch breakage before users hit it.

The CI workflow files live under .github/, and the plugin's own source is in the lua/ directory with the tests in specs/.

Where lsp-format.nvim is the wrong tool

The first wrong-tool case is a configuration with no language server. The plugin formats nothing itself, so without an attached LSP server that can format, installing it changes nothing. Some languages have good LSP servers with formatting, and some do not, and for the ones that do not you need a different tool entirely.

The second case is a project that wants one deterministic formatter per language. This plugin asks every attached server that is not excluded to format, and the order option makes the request order deterministic, but the result is still whichever server answers last. If a project has a language server attached for completion and diagnostics and a separate formatting tool, the design that fits is a single formatting entry point rather than a negotiation between servers.

The third case is a team that has already standardised on the built-in autocommand and is happy with it. The FAQ says the end result is the same, and the differences are the async default, the disable commands and the format options. If you have none of those needs, the plugin adds a dependency to your dotfiles for no behavioural gain, and the async default would be the one thing you actively do not want if your current setup is synchronous and predictable.

The fourth case is a Neovim version older than 0.7, which is a hard requirement rather than a recommendation.

There is a licensing point to settle before this goes into a distributed configuration. The project metadata records the licence as unknown, and the repository listing at the root does not show a LICENSE file among the entries, which is consistent with that. A dotfiles repository used by one person is not a distribution problem. A plugin bundled into a product, a company-wide dotfiles template published to a registry, or a fork you distribute carries the question of what licence you have the right to redistribute under, and that question needs an answer from the author rather than from the metadata.

On maintenance, the release cadence is worth noting. v2.7.3 was tagged on 2026-06-10, v2.7.2 on 2025-03-12 and v2.7.1 on 2024-08-29, and the last push was on 2026-06-10. So the plugin is still being released, roughly annually at the minor version, with the same version numbers moving for a long time now. That is the pattern of a stable, low-churn plugin, which is what you want from something that sits in the write path of every save.

Editorial conclusion

lsp-format.nvim suits a Neovim user who already runs one or more language servers and wants format on save without the editor blocking, since the default asynchronous path only writes the result back when the buffer has not changed since the request. It does not suit a setup with no LSP server, because the plugin formats nothing itself, and it does not suit someone who wants a single consistent formatter across languages when several servers can format the same buffer. Before adopting, remove any older format-on-save autocommand you already have, and add the wq abbreviation from the README if you save and quit often, because the asynchronous default cannot apply the formatting before the buffer closes.

Frequently asked questions

What is lsp-format.nvim?

It is a Neovim plugin that wraps the editor's native LSP formatting to format buffers on save, asynchronously by default, with support for excluding specific LSP servers, setting the order servers are asked in, and turning on synchronous formatting. It provides no formatting itself, so you still need an LSP server.

How do I set up lsp-format.nvim?

Run require("lsp-format").setup {} and register an LspAttach autocmd that looks the client up with vim.lsp.get_client_by_id and calls require("lsp-format").on_attach(client, args.buf). Saving a buffer then formats it. The plugin requires Neovim 0.7 or newer.

How is lsp-format.nvim different from autocmd BufWritePre with vim.lsp.buf.format()?

The main difference is that lsp-format.nvim is async by default, so it formats on save without blocking the editor, and it only changes the buffer if the buffer did not change since formatting was called. It also adds disable commands and format options. The end result is the same.

Why does :wq not format with lsp-format.nvim?

Because default formatting is async, the formatting results will not get back in time and Neovim will close without applying the changes. Either enable the sync format option, or add the abbreviation vim.cmd [[cabbrev wq execute "Format sync" <bar> wq]] to your dotfiles.

What format options does lsp-format.nvim support?

There are four special ones: exclude, a table of LSP servers that should not format the buffer; order, a table determining the order formatting is requested from the servers; sync, which turns on synchronous formatting; and force, which writes the result even if the buffer changed after the request started. Format options can be a string, number, boolean or a function resolving to one of those.

How are lsp-format.nvim tests run?

The Makefile test target clones plenary.nvim and neodev.nvim into .ci/vendor if they are not already there, then runs headless Neovim with the PlenaryBustedDirectory runner over specs/features, with a 300 second timeout. Separate targets run luacheck, stylua in check mode, and the Lua language server in check mode.

Official sources

  1. Issues
  2. lukas-reineke/lsp-format.nvim on GitHub
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lukas-reineke-lsp-format-nvim.svg)](https://hysenlabs.com/projects/lukas-reineke-lsp-format-nvim)

Was this useful?