local auto = require 'std.auto' local table = require 'std.table' local NS = vim.api.nvim_create_namespace 'bugabinga.code_action' local kind_icons = { ['source.fixAll'] = '●', ['quickfix'] = '◉', ['refactor.extract'] = '◎', ['source.organizeImports'] = '◎', ['source'] = '◎', ['refactor.move'] = '○', ['refactor'] = '○', [''] = '○', } local kind_priority = { ['source.fixAll'] = 1, ['quickfix'] = 2, ['refactor.extract'] = 3, ['source.organizeImports'] = 3, ['source'] = 3, ['refactor.move'] = 4, ['refactor'] = 4, [''] = 5, } local function resolve_kind(kind) local k = kind or '' while k ~= '' do if kind_icons[k] then return k end k = k:match '^(.+)%.[^%.]+$' or '' end return '' end local function get_kind_hl(kind) local k = resolve_kind(kind) return 'BugabingaCodeAction' .. (k == '' and 'Default' or k:gsub('%.', '_'):gsub('^%l', string.upper)) end local function get_line_diagnostics(bufnr) local row = vim.api.nvim_win_get_cursor(0)[1] - 1 local diagnostics = vim.diagnostic.get(bufnr, { lnum = row }) local lsp_diagnostics = {} for _, d in ipairs(diagnostics) do if d.user_data and d.user_data.lsp then table.insert(lsp_diagnostics, d.user_data.lsp) end end return lsp_diagnostics end local function update(bufnr) bufnr = bufnr or vim.api.nvim_get_current_buf() local clients = vim.lsp.get_clients({ bufnr = bufnr, method = 'textDocument/codeAction' }) if #clients == 0 then vim.api.nvim_buf_clear_namespace(bufnr, NS, 0, -1) return end local encoding = clients[1].offset_encoding or 'utf-16' local params = vim.lsp.util.make_range_params(0, encoding) ---@cast params lsp.CodeActionParams params.context = { diagnostics = get_line_diagnostics(bufnr) } if vim.b[bufnr].bugabinga_code_action_cancel then vim.b[bufnr].bugabinga_code_action_cancel() end local cancel = vim.lsp.buf_request_all(bufnr, 'textDocument/codeAction', params, function(results) if not vim.api.nvim_buf_is_valid(bufnr) then return end local best_kind, best_prio = '', math.huge for _, resp in pairs(results) do if resp.result then for _, action in ipairs(resp.result) do local kind = action.kind or '' local prio = kind_priority[kind] or 99 if prio < best_prio then best_prio = prio best_kind = kind end end end end vim.api.nvim_buf_clear_namespace(bufnr, NS, 0, -1) if best_kind ~= '' or best_prio ~= math.huge then local icon = kind_icons[resolve_kind(best_kind)] or '○' local hl = get_kind_hl(best_kind) local line = params.range.start.line vim.api.nvim_buf_set_extmark(bufnr, NS, line, 0, { sign_text = icon, sign_hl_group = hl, priority = 10, }) end end) vim.b[bufnr].bugabinga_code_action_cancel = cancel end local function setup(bufnr) local group = vim.api.nvim_create_augroup( ('bugabinga.code_action.%d'):format(bufnr), { clear = true } ) vim.api.nvim_create_autocmd({ 'CursorHold', 'TextChanged', 'DiagnosticChanged' }, { group = group, buf = bufnr, callback = function() update(bufnr) end, }) vim.api.nvim_create_autocmd('InsertEnter', { group = group, buf = bufnr, callback = function() vim.api.nvim_buf_clear_namespace(bufnr, NS, 0, -1) end, }) vim.api.nvim_create_autocmd('InsertLeave', { group = group, buf = bufnr, callback = function() update(bufnr) end, }) vim.api.nvim_create_autocmd('LspDetach', { group = group, buf = bufnr, callback = function() vim.api.nvim_buf_clear_namespace(bufnr, NS, 0, -1) end, }) end auto 'code_action' { description = 'code action: show kind-specific circle sign when actions available', events = 'LspAttach', command = function(args) setup(args.buf) end, }