Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/code_action.lua

Raw
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,
}