Automatically formats files when saving using either LSP or shell utilities like prettierd or shfmt.
It also restores the cursor of any window showing the current file (useful when editing the same file in a split).
NOTE: This is a work in progress and the API might change.
Until now I've used LSP-only formatting with a simple auto command with null-ls to cover non-LSP formatters. However, now that null-ls is being archived I needed to find an alternative.
I tried several plugins but:
This plugin has a few core principles to keep it simple:
:Format
command
you can call whenever you want but the purpose of this plugin is to format when
saving.There are currently 3 types of formatters:
vim.lsp.buf.format
to format the file, you can pass
a client_name
option to use a specific client in case there are several ones
that support formatting.prettierd
or shfmt
) and replaces the buffer's contents with the result.tempfile
field
which can be either "random"
(uses vim.fn.tempname()
) or a function that
returns a string to be used as the tempfile and then the plugin will write the
buffer to this file, run the command on it (the "%" value will be expanded to
the tempfile) and read it back and fill in the buffer).cmd
array will be expanded by default using the vim.fn.exepath
function in order to detect mason binaries. To opt-out set the expand_executable
field to false
.cmd
argument can also be a function that is evaluated each time we format.When updating all of the lines in a buffer all of the extmarks get removed. This plugin currently includes two experimental methods to only partially update the buffer:
experiments.partial_update = 'line-by-line'
- a very simple algorithm that
goes line-by-line and compares them, only updates modified lines.experiments.partial_update = 'diff'
- an awesome upgrade added by @faergeek
that uses vim.diff
to compare the old and new buffer lines and only update
the modified hunks.You can enable either of them like this:
require('format-on-save').setup({
experiments = {
partial_update = 'diff', -- or 'line-by-line'
}
})
Add the following to your package manager:
{ "elentok/format-on-save.nvim" },
By default this plugin doesn't enable any format on save, you have to specify what you want:
local format_on_save = require("format-on-save")
local formatters = require("format-on-save.formatters")
format_on_save.setup({
exclude_path_patterns = {
"/node_modules/",
".local/share/nvim/lazy",
},
formatter_by_ft = {
css = formatters.lsp,
html = formatters.lsp,
java = formatters.lsp,
javascript = formatters.lsp,
json = formatters.lsp,
lua = formatters.lsp,
markdown = formatters.prettierd,
openscad = formatters.lsp,
python = formatters.black,
rust = formatters.lsp,
scad = formatters.lsp,
scss = formatters.lsp,
sh = formatters.shfmt,
terraform = formatters.lsp,
typescript = formatters.prettierd,
typescriptreact = formatters.prettierd,
yaml = formatters.lsp,
-- Add your own shell formatters:
myfiletype = formatters.shell({ cmd = { "myformatter", "%" } }),
-- Add lazy formatter that will only run when formatting:
my_custom_formatter = function()
if vim.api.nvim_buf_get_name(0):match("/README.md$") then
return formatters.prettierd
else
return formatters.lsp()
end
end,
-- Add custom formatter
filetype1 = formatters.remove_trailing_whitespace,
filetype2 = formatters.custom({ format = function(lines)
return vim.tbl_map(function(line)
return line:gsub("true", "false")
end, lines)
end}),
-- Concatenate formatters
python = {
formatters.remove_trailing_whitespace,
formatters.shell({ cmd = "tidy-imports" }),
formatters.black,
formatters.ruff,
},
-- Use a tempfile instead of stdin
go = {
formatters.shell({
cmd = { "goimports-reviser", "-rm-unused", "-set-alias", "-format", "%" },
tempfile = function()
return vim.fn.expand("%") .. '.formatter-temp'
end
}),
formatters.shell({ cmd = { "gofmt" } }),
},
-- Add conditional formatter that only runs if a certain file exists
-- in one of the parent directories.
javascript = {
formatters.if_file_exists({
pattern = ".eslintrc.*",
formatter = formatters.eslint_d_fix)
}),
formatters.if_file_exists({
pattern = { ".prettierrc", ".prettierrc.*", "prettier.config.*" },
formatter = formatters.prettierd,
}),
-- By default it stops at the git repo root (or "/" if git repo not found)
-- but it can be customized with the `stop_path` option:
formatters.if_file_exists({
pattern = ".prettierrc",
formatter = formatters.prettierd,
stop_path = function()
return "/my/custom/stop/path"
end
}),
},
},
-- Optional: fallback formatter to use when no formatters match the current filetype
fallback_formatter = {
formatters.remove_trailing_whitespace,
formatters.remove_trailing_newlines,
formatters.prettierd,
},
-- By default, all shell commands are prefixed with "sh -c" (see PR #3)
-- To prevent that set `run_with_sh` to `false`.
run_with_sh = false,
})
By default the plugin uses another buffer to show error messages, you can
customize it by setting the error_notifier
configuration flag, e.g. to use
vim.notify()
:
local format_on_save = require("format-on-save")
local vim_notify = require("format-on-save.error-notifiers.vim-notify")
format_on_save.setup({
error_notifier = vim_notify,
})
Alternatively you can implement your own:
local format_on_save = require("format-on-save")
---@type ErrorNotifier
local error_notifier = {
show = function(opts)
-- use opts.title and opts.body
end,
hide = function()
-- hide the error when it's been resolved
end,
}
format_on_save.setup({
error_notifier = error_notifier,
})
When a formatter exits with exitcode 0 but its stderr has contents we show a
warning message (via vim.notify
).
The default level is vim.log.levels.WARN
. To disable this message entirely set
the stderr_loglevel
config key to vim.log.levels.OFF
:
require('format-on-save').setup({
stderr_loglevel = vim.log.levels.OFF,
})
By default it will add the BufWritePre
and BufWritePost
autocommands and the Format
,
FormatOn
and FormatOff
user commands. If you prefer to avoid it and define
your own you can disable it:
require('format-on-save').setup({
auto_commands = false,
user_commands = false,
})
To trigger the format call:
require('format-on-save').format()
To restore the cursor positions after the format:
require('format-on-save').restore_cursors()