diff --git a/lua/plugins/language-manager.lua b/lua/plugins/language-manager.lua index 622a228..2b65136 100644 --- a/lua/plugins/language-manager.lua +++ b/lua/plugins/language-manager.lua @@ -359,11 +359,162 @@ function M.lint.setup() }) end +local biome_save_action_kinds = { + 'source.organizeImports.biome', + 'source.biome', +} + +local function matches_code_action_kind(action, action_kind) + local kind = action.kind or '' + return kind == action_kind or vim.startswith(kind, action_kind .. '.') +end + +local function get_client_diagnostics(bufnr, client) + local diagnostics = {} + + for _, pull in ipairs({ false, true }) do + local namespace = vim.lsp.diagnostic.get_namespace(client.id, pull) + + for _, diagnostic in ipairs(vim.diagnostic.get(bufnr, { namespace = namespace })) do + local lsp_diagnostic = diagnostic.user_data and diagnostic.user_data.lsp + if lsp_diagnostic then + table.insert(diagnostics, lsp_diagnostic) + end + end + end + + return diagnostics +end + +local function get_document_range(bufnr, client) + local last_line = math.max(vim.api.nvim_buf_line_count(bufnr) - 1, 0) + local line = vim.api.nvim_buf_get_lines(bufnr, last_line, last_line + 1, true)[1] or '' + local last_character = vim.str_utfindex(line, client.offset_encoding or 'utf-16', #line) + + return { + start = { line = 0, character = 0 }, + ['end'] = { line = last_line, character = last_character }, + } +end + +local function apply_code_action(bufnr, client, action) + local methods = vim.lsp.protocol.Methods + + if + not (action.edit and action.command) and client:supports_method(methods.codeAction_resolve) + then + local resolved, resolve_error = + client:request_sync(methods.codeAction_resolve, action, 1000, bufnr) + + if resolved and resolved.err then + vim.notify(resolved.err.message, vim.log.levels.ERROR) + return false + end + + if resolved and resolved.result then + action = resolved.result + elseif resolve_error then + vim.notify(resolve_error, vim.log.levels.ERROR) + return false + end + end + + local applied = false + + if action.edit then + vim.lsp.util.apply_workspace_edit(action.edit, client.offset_encoding) + applied = true + end + + if action.command then + local command = type(action.command) == 'table' and action.command or action + client:exec_cmd(command, { bufnr = bufnr }) + applied = true + end + + return applied +end + +local function apply_biome_save_actions(bufnr) + local methods = vim.lsp.protocol.Methods + local clients = vim.lsp.get_clients({ + bufnr = bufnr, + name = 'biome', + method = methods.textDocument_codeAction, + }) + + for _, client in ipairs(clients) do + for _, action_kind in ipairs(biome_save_action_kinds) do + local remaining_passes = 20 + local applied_action_kinds = {} + local noop_action_kinds = {} + + while remaining_passes > 0 do + remaining_passes = remaining_passes - 1 + + local result, request_error = client:request_sync(methods.textDocument_codeAction, { + textDocument = vim.lsp.util.make_text_document_params(bufnr), + range = get_document_range(bufnr, client), + context = { + diagnostics = get_client_diagnostics(bufnr, client), + only = { action_kind }, + triggerKind = vim.lsp.protocol.CodeActionTriggerKind.Automatic, + }, + }, 1000, bufnr) + + local applied = false + + if result and result.err then + vim.notify(result.err.message, vim.log.levels.ERROR) + elseif request_error then + vim.notify(request_error, vim.log.levels.ERROR) + elseif result and result.result then + for _, action in ipairs(result.result) do + local kind = action.kind or '' + if + matches_code_action_kind(action, action_kind) + and not applied_action_kinds[kind] + and not noop_action_kinds[kind] + then + applied = apply_code_action(bufnr, client, action) + if applied then + applied_action_kinds[kind] = true + break + else + noop_action_kinds[kind] = true + end + end + end + end + + if not applied then + break + end + end + + if remaining_passes == 0 then + vim.notify('Biome source actions did not converge on save', vim.log.levels.ERROR) + end + end + end +end + function M.format.setup() require('conform').setup({ formatters_by_ft = (M.general and M.general.formatters_by_ft) or {}, default_format_opts = { stop_after_first = true, lsp_format = 'fallback' }, - format_on_save = { timeout_ms = 500 }, + }) + + vim.api.nvim_create_autocmd('BufWritePre', { + group = vim.api.nvim_create_augroup('language-manager.format', { clear = true }), + callback = function(args) + if not vim.bo[args.buf].modifiable then + return + end + + apply_biome_save_actions(args.buf) + require('conform').format({ bufnr = args.buf, timeout_ms = 500, lsp_format = 'fallback' }) + end, }) end