I am probably not the only one using biome in neovim (with neovim’s lsp), so I figure i’m probably not the only person that has run into this issue. So I thought i might as well share this in case someone else embarks on their own journey trying to get biome to not run into race conditions running both fixing and formatting at the same time as an on-save hook, in the hopes that they find this before banging their heads into their mechanical keyboards.
The problem:
Say you have this piece of code in your neovim lsp stuff (for biome)
function format_and_fix(event)
local bufnr = event.buf
local clients = vim.lsp.get_active_clients({ bufnr = bufnr })
for _, client in ipairs(clients) do
local name = client.name
-- format client filter, to only format items for one single client, ...
I am probably not the only one using biome in neovim (with neovim’s lsp), so I figure i’m probably not the only person that has run into this issue. So I thought i might as well share this in case someone else embarks on their own journey trying to get biome to not run into race conditions running both fixing and formatting at the same time as an on-save hook, in the hopes that they find this before banging their heads into their mechanical keyboards.
The problem:
Say you have this piece of code in your neovim lsp stuff (for biome)
function format_and_fix(event)
local bufnr = event.buf
local clients = vim.lsp.get_active_clients({ bufnr = bufnr })
for _, client in ipairs(clients) do
local name = client.name
-- format client filter, to only format items for one single client, so a format operation for biome doesn't drag tsserver in.
local client_filter = function(formatter) return formatter.name == client.name end
if name == "biome" and valid_biome then
-- 1. Fix all auto-fixable biome errors
vim.lsp.buf.code_action({
async = false,
context = { only = { "source.fixAll.biome" }, diagnostics = {} },
timeout_ms = 1000,
apply = true,
})
end
-- 2. Format all biome errors
vim.lsp.buf.format({ async = false, filter = client_filter, timeout_ms = 3000 })
end
...
end
vim.api.nvim_create_autocmd("BufWritePre", {
group = vim.api.nvim_create_augroup("UnifiedLspFormatFix", { clear = true }),
callback = format_and_fix,
})
Then it might look like these would run after each other since their async flag is false, but that idea doesn’t really fly. The result if you have unsorted imports and at the same time faulty indentation further down the page (to give an example); is that you get extremely consistent race conditions turning your files into garbled mess of jumping or missing characters and sometimes whole lines jumping around.
The solution that seems to be working for me is to run a very targeted buf_request_sync function. this function will return a list of a list of suggested auto-fix actions that then get applied one by one in a synchronous manner. then doing a redraw for good measure and only then doing the format action as seen below:
function format_and_fix(event)
local bufnr = event.buf
local clients = vim.lsp.get_active_clients({ bufnr = bufnr })
for _, client in ipairs(clients) do
local name = client.name
-- format client filter, to only format items for one single client, so a format operation for biome doesn't drag tsserver in.
local client_filter = function(formatter) return formatter.name == client.name end
if name == "biome" and valid_biome then
-- Step 1: Apply fixAll synchronously
local params = vim.lsp.util.make_range_params(nil, client.offset_encoding)
params.context = {
only = { "source.fixAll.biome" },
diagnostics = {},
}
local fixall_results = vim.lsp.buf_request_sync(
bufnr,
'textDocument/codeAction',
params,
1000 -- 1 second timeout
)
if fixall_results then
-- Don't need client id, since only biome is running this action
---@diagnostic disable-next-line: unused-local
for _client_id, result in pairs(fixall_results) do
if result.result then
for _, action in ipairs(result.result) do
if action.edit then
vim.lsp.util.apply_workspace_edit(action.edit, client.offset_encoding)
elseif action.command then
client:exec_cmd(action.command)
end
end
end
end
end
-- Step 2: Wait a tiny bit for edits to settle
vim.cmd('redraw')
-- Step 3: Format synchronously
vim.lsp.buf.format({ async = false, filter = client_filter, timeout_ms = 3000 })
end
end
...
end
If you do this, then do make sure that other formatting actions explicitly run after this one (in case of other LSPs) (or that only one formatter is running on a certain file type as different formatters collide with each other which is the entire reason i do the filter = client_filter thing)
Source file of the full implementation: https://git.hendrikpeter.net/hendrikpeter/peva-nvim-lazy/-/blob/main/lua/utils/lsp/format-and-fix.lua
I hope this helps someone!