neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

codelens.lua (14775B)


      1 local util = require('vim.lsp.util')
      2 local log = require('vim.lsp.log')
      3 local api = vim.api
      4 local M = {}
      5 
      6 local Capability = require('vim.lsp._capability')
      7 
      8 ---@class (private) vim.lsp.codelens.ClientState
      9 ---@field row_lenses table<integer, lsp.CodeLens[]?> row -> lens
     10 ---@field namespace integer
     11 
     12 ---@class (private) vim.lsp.codelens.Provider : vim.lsp.Capability
     13 ---@field active table<integer, vim.lsp.codelens.Provider?>
     14 ---
     15 --- `TextDocument` version current state corresponds to.
     16 ---@field version? integer
     17 ---
     18 --- Last version of codelens applied to this line.
     19 ---
     20 --- Index In the form of row -> true?
     21 ---@field row_version table<integer, integer?>
     22 ---
     23 --- Index In the form of client_id -> client_state
     24 ---@field client_state? table<integer, vim.lsp.codelens.ClientState?>
     25 ---
     26 --- Timer for debouncing automatic requests.
     27 ---
     28 ---@field timer? uv.uv_timer_t
     29 local Provider = {
     30  name = 'codelens',
     31  method = 'textDocument/codeLens',
     32  active = {},
     33 }
     34 Provider.__index = Provider
     35 setmetatable(Provider, Capability)
     36 Capability.all[Provider.name] = Provider
     37 
     38 ---@package
     39 ---@param bufnr integer
     40 ---@return vim.lsp.codelens.Provider
     41 function Provider:new(bufnr)
     42  ---@type vim.lsp.codelens.Provider
     43  self = Capability.new(self, bufnr)
     44  self.client_state = {}
     45  self.row_version = {}
     46 
     47  api.nvim_buf_attach(bufnr, false, {
     48    on_lines = function(_, buf)
     49      local provider = Provider.active[buf]
     50      if not provider then
     51        return true
     52      end
     53      provider:automatic_request()
     54    end,
     55    on_reload = function(_, buf)
     56      local provider = Provider.active[buf]
     57      if provider then
     58        provider:automatic_request()
     59      end
     60    end,
     61  })
     62 
     63  return self
     64 end
     65 
     66 ---@package
     67 ---@param client_id integer
     68 function Provider:on_attach(client_id)
     69  local state = self.client_state[client_id]
     70  if not state then
     71    state = {
     72      namespace = api.nvim_create_namespace('nvim.lsp.codelens:' .. client_id),
     73      row_lenses = {},
     74    }
     75    self.client_state[client_id] = state
     76  end
     77  self:request(client_id)
     78 end
     79 
     80 ---@package
     81 ---@param client_id integer
     82 function Provider:on_detach(client_id)
     83  local state = self.client_state[client_id]
     84  if state then
     85    api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
     86    self.client_state[client_id] = nil
     87  end
     88 end
     89 
     90 --- `lsp.Handler` for `textDocument/codeLens`.
     91 ---
     92 ---@package
     93 ---@param err? lsp.ResponseError
     94 ---@param result? lsp.CodeLens[]
     95 ---@param ctx lsp.HandlerContext
     96 function Provider:handler(err, result, ctx)
     97  local state = self.client_state[ctx.client_id]
     98  if not state then
     99    return
    100  end
    101 
    102  if err then
    103    log.error('codelens', err)
    104    return
    105  end
    106 
    107  if util.buf_versions[self.bufnr] ~= ctx.version then
    108    return
    109  end
    110 
    111  ---@type table<integer, lsp.CodeLens[]>
    112  local row_lenses = {}
    113 
    114  -- Code lenses should only span a single line.
    115  for _, lens in ipairs(result or {}) do
    116    local row = lens.range.start.line
    117    local lenses = row_lenses[row] or {}
    118    table.insert(lenses, lens)
    119    row_lenses[row] = lenses
    120  end
    121 
    122  state.row_lenses = row_lenses
    123  self.version = ctx.version
    124 end
    125 
    126 ---@package
    127 ---@param client_id? integer
    128 ---@param on_response? function
    129 function Provider:request(client_id, on_response)
    130  ---@type lsp.CodeLensParams
    131  local params = { textDocument = util.make_text_document_params(self.bufnr) }
    132  for id in pairs(self.client_state) do
    133    if not client_id or client_id == id then
    134      local client = assert(vim.lsp.get_client_by_id(id))
    135      client:request('textDocument/codeLens', params, function(...)
    136        self:handler(...)
    137 
    138        if on_response then
    139          on_response()
    140        end
    141      end, self.bufnr)
    142    end
    143  end
    144 end
    145 
    146 ---@private
    147 function Provider:reset_timer()
    148  local timer = self.timer
    149  if timer then
    150    self.timer = nil
    151    if not timer:is_closing() then
    152      timer:stop()
    153      timer:close()
    154    end
    155  end
    156 end
    157 
    158 --- Automatically request with debouncing, used as callbacks in autocmd events.
    159 ---
    160 ---@package
    161 function Provider:automatic_request()
    162  self:reset_timer()
    163  self.timer = vim.defer_fn(function()
    164    self:request()
    165  end, 200)
    166 end
    167 
    168 ---@private
    169 ---@param client vim.lsp.Client
    170 ---@param unresolved_lens lsp.CodeLens
    171 function Provider:resolve(client, unresolved_lens)
    172  ---@param resolved_lens lsp.CodeLens
    173  client:request('codeLens/resolve', unresolved_lens, function(err, resolved_lens, ctx)
    174    local state = self.client_state[client.id]
    175    if not state then
    176      return
    177    end
    178 
    179    if err then
    180      log.error('codelens/resolve', err)
    181      return
    182    end
    183 
    184    if util.buf_versions[self.bufnr] ~= ctx.version then
    185      return
    186    end
    187 
    188    local row = unresolved_lens.range.start.line
    189    local lenses = assert(state.row_lenses[row])
    190    for i, lens in ipairs(lenses) do
    191      if lens == unresolved_lens then
    192        lenses[i] = resolved_lens
    193      end
    194    end
    195 
    196    self.row_version[row] = nil
    197    api.nvim__redraw({
    198      buf = self.bufnr,
    199      range = { row, row + 1 },
    200      valid = true,
    201      flush = false,
    202    })
    203  end, self.bufnr)
    204 end
    205 
    206 ---@package
    207 ---@param toprow integer
    208 ---@param botrow integer
    209 function Provider:on_win(toprow, botrow)
    210  for row = toprow, botrow do
    211    if self.row_version[row] ~= self.version then
    212      for client_id, state in pairs(self.client_state) do
    213        local bufnr = self.bufnr
    214        local namespace = state.namespace
    215 
    216        api.nvim_buf_clear_namespace(bufnr, namespace, row, row + 1)
    217 
    218        local lenses = state.row_lenses[row]
    219        if lenses then
    220          table.sort(lenses, function(a, b)
    221            return a.range.start.character < b.range.start.character
    222          end)
    223 
    224          ---@type integer
    225          local indent = api.nvim_buf_call(bufnr, function()
    226            return vim.fn.indent(row + 1)
    227          end)
    228 
    229          ---@type [string, string|integer][][]
    230          local virt_lines = { { { string.rep(' ', indent), 'LspCodeLensSeparator' } } }
    231          local virt_text = virt_lines[1]
    232          for _, lens in ipairs(lenses) do
    233            -- A code lens is unresolved when no command is associated to it.
    234            if not lens.command then
    235              local client = assert(vim.lsp.get_client_by_id(client_id)) ---@type vim.lsp.Client
    236              self:resolve(client, lens)
    237            else
    238              virt_text[#virt_text + 1] = { lens.command.title, 'LspCodeLens' }
    239              virt_text[#virt_text + 1] = { ' | ', 'LspCodeLensSeparator' }
    240            end
    241          end
    242 
    243          if #virt_text > 1 then
    244            -- Remove trailing separator.
    245            virt_text[#virt_text] = nil
    246          else
    247            -- Use a placeholder to prevent flickering caused by layout shifts.
    248            virt_text[#virt_text + 1] = { '...', 'LspCodeLens' }
    249          end
    250 
    251          api.nvim_buf_set_extmark(bufnr, namespace, row, 0, {
    252            virt_lines = virt_lines,
    253            virt_lines_above = true,
    254            virt_lines_overflow = 'scroll',
    255            hl_mode = 'combine',
    256          })
    257        end
    258        self.row_version[row] = self.version
    259      end
    260    end
    261  end
    262 
    263  if botrow == api.nvim_buf_line_count(self.bufnr) - 1 then
    264    for _, state in pairs(self.client_state) do
    265      api.nvim_buf_clear_namespace(self.bufnr, state.namespace, botrow, -1)
    266    end
    267  end
    268 end
    269 
    270 local namespace = api.nvim_create_namespace('nvim.lsp.codelens')
    271 api.nvim_set_decoration_provider(namespace, {
    272  on_win = function(_, _, bufnr, toprow, botrow)
    273    local provider = Provider.active[bufnr]
    274    if provider then
    275      provider:on_win(toprow, botrow)
    276    end
    277  end,
    278 })
    279 
    280 --- Query whether code lens is enabled in the {filter}ed scope
    281 ---
    282 ---@param filter? vim.lsp.capability.enable.Filter
    283 ---@return boolean whether code lens is enabled.
    284 function M.is_enabled(filter)
    285  return vim.lsp._capability.is_enabled('codelens', filter)
    286 end
    287 
    288 --- Enables or disables code lens for the {filter}ed scope.
    289 ---
    290 --- To "toggle", pass the inverse of `is_enabled()`:
    291 ---
    292 --- ```lua
    293 --- vim.lsp.codelens.enable(not vim.lsp.codelens.is_enabled())
    294 --- ```
    295 ---
    296 --- To run a code lens, see |vim.lsp.codelens.run()|.
    297 ---
    298 ---@param enable? boolean true/nil to enable, false to disable
    299 ---@param filter? vim.lsp.capability.enable.Filter
    300 function M.enable(enable, filter)
    301  vim.lsp._capability.enable('codelens', enable, filter)
    302 end
    303 
    304 --- Optional filters |kwargs|:
    305 ---@class vim.lsp.codelens.get.Filter
    306 ---@inlinedoc
    307 ---
    308 --- Buffer handle, or 0 for current.
    309 --- (default: 0)
    310 ---@field bufnr? integer
    311 ---
    312 --- Client ID, or nil for all.
    313 --- (default: all)
    314 ---@field client_id? integer
    315 
    316 ---@class vim.lsp.codelens.get.Result
    317 ---@inlinedoc
    318 ---@field client_id integer
    319 ---@field lens lsp.CodeLens
    320 
    321 --- Get all code lenses in the {filter}ed scope.
    322 ---
    323 ---@param filter? vim.lsp.codelens.get.Filter
    324 ---@return vim.lsp.codelens.get.Result[]
    325 function M.get(filter)
    326  if type(filter) == 'number' then
    327    vim.deprecate(
    328      'vim.lsp.codelens.get(bufnr)',
    329      'vim.lsp.codelens.get({ bufnr = bufnr })',
    330      '0.13.0'
    331    )
    332    local bufnr = vim._resolve_bufnr(filter)
    333    local provider = Provider.active[bufnr]
    334    if not provider then
    335      return {}
    336    end
    337    ---@type lsp.CodeLens[]
    338    local result = {}
    339    for _, state in pairs(provider.client_state) do
    340      for _, lenses in pairs(state.row_lenses) do
    341        result = vim.list_extend(result, lenses)
    342      end
    343    end
    344    return result
    345  end
    346 
    347  vim.validate('filter', filter, 'table', true)
    348  filter = filter or {}
    349 
    350  local bufnr = vim._resolve_bufnr(filter.bufnr)
    351  local provider = Provider.active[bufnr]
    352  if not provider then
    353    return {}
    354  end
    355 
    356  local result = {}
    357  for client_id, state in pairs(provider.client_state) do
    358    if not filter.client_id or filter.client_id == client_id then
    359      for _, lenses in pairs(state.row_lenses) do
    360        for _, lens in ipairs(lenses) do
    361          table.insert(result, { client_id = client_id, lens = lens })
    362        end
    363      end
    364    end
    365  end
    366  return result
    367 end
    368 
    369 ---@param lnum integer
    370 ---@param opts vim.lsp.codelens.run.Opts
    371 ---@param results table<integer, {err: lsp.ResponseError?, result: lsp.CodeLens[]?}>
    372 ---@param context lsp.HandlerContext
    373 local function on_lenses_run(lnum, opts, results, context)
    374  local bufnr = context.bufnr or 0
    375 
    376  ---@type {client: vim.lsp.Client, lens: lsp.CodeLens}[]
    377  local candidates = {}
    378  local pending_resolve = 1
    379  local function on_resolved()
    380    pending_resolve = pending_resolve - 1
    381    if pending_resolve > 0 then
    382      return
    383    end
    384    if #candidates == 0 then
    385      vim.notify('No codelens at current line')
    386    elseif #candidates == 1 then
    387      local candidate = candidates[1]
    388      candidate.client:exec_cmd(candidate.lens.command, { bufnr = bufnr })
    389    else
    390      local selectopts = {
    391        prompt = 'Code lenses: ',
    392        kind = 'codelens',
    393        ---@param candidate {client: vim.lsp.Client, lens: lsp.CodeLens}
    394        format_item = function(candidate)
    395          return string.format('%s [%s]', candidate.lens.command.title, candidate.client.name)
    396        end,
    397      }
    398      vim.ui.select(candidates, selectopts, function(candidate)
    399        if candidate then
    400          candidate.client:exec_cmd(candidate.lens.command, { bufnr = bufnr })
    401        end
    402      end)
    403    end
    404  end
    405  for client_id, result in pairs(results) do
    406    if opts.client_id == nil or opts.client_id == client_id then
    407      local client = assert(vim.lsp.get_client_by_id(client_id))
    408      for _, lens in ipairs(result.result or {}) do
    409        if lens.range.start.line == lnum then
    410          if lens.command then
    411            table.insert(candidates, { client = client, lens = lens })
    412          else
    413            pending_resolve = pending_resolve + 1
    414            client:request('codeLens/resolve', lens, function(_, resolved_lens)
    415              if resolved_lens then
    416                table.insert(candidates, { client = client, lens = resolved_lens })
    417              end
    418              on_resolved()
    419            end, bufnr)
    420          end
    421        end
    422      end
    423    end
    424  end
    425  on_resolved()
    426 end
    427 
    428 --- Optional parameters |kwargs|:
    429 ---@class vim.lsp.codelens.run.Opts
    430 ---@inlinedoc
    431 ---
    432 --- Client ID, or nil for all.
    433 --- (default: all)
    434 ---@field client_id? integer
    435 
    436 --- Run code lens at the current cursor position.
    437 ---
    438 ---@param opts? vim.lsp.codelens.run.Opts
    439 function M.run(opts)
    440  vim.validate('opts', opts, 'table', true)
    441  opts = opts or {}
    442 
    443  local winid = api.nvim_get_current_win()
    444  local bufnr = api.nvim_win_get_buf(winid)
    445  local pos = vim.pos.cursor(api.nvim_win_get_cursor(winid))
    446  local params = {
    447    textDocument = vim.lsp.util.make_text_document_params(bufnr),
    448  }
    449  vim.lsp.buf_request_all(bufnr, 'textDocument/codeLens', params, function(results, context)
    450    on_lenses_run(pos.row, opts, results, context)
    451  end)
    452 end
    453 
    454 --- |lsp-handler| for the method `workspace/codeLens/refresh`
    455 ---
    456 ---@private
    457 ---@type lsp.Handler
    458 function M.on_refresh(err, _, ctx)
    459  if err then
    460    return vim.NIL
    461  end
    462 
    463  for bufnr, provider in pairs(Provider.active) do
    464    for client_id in pairs(provider.client_state) do
    465      if client_id == ctx.client_id then
    466        provider:request(client_id, function()
    467          provider.row_version = {}
    468          vim.api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
    469        end)
    470      end
    471    end
    472  end
    473  return vim.NIL
    474 end
    475 
    476 ---@deprecated
    477 ---@param client_id? integer
    478 ---@param bufnr? integer
    479 function M.clear(client_id, bufnr)
    480  vim.deprecate(
    481    'vim.lsp.codelens.clear(client_id, bufnr)',
    482    'vim.lsp.codelens.enable(false, { bufnr = bufnr, client_id = client_id })',
    483    '0.13.0'
    484  )
    485  M.enable(false, { bufnr = bufnr, client_id = client_id })
    486 end
    487 
    488 ---@deprecated
    489 ---@param lenses? lsp.CodeLens[] lenses to display
    490 ---@param bufnr integer
    491 ---@param client_id integer
    492 function M.display(lenses, bufnr, client_id)
    493  vim.deprecate('vim.lsp.codelens.display()', nil, '0.13.0')
    494  local _, _, _ = lenses, bufnr, client_id
    495 end
    496 
    497 ---@deprecated
    498 ---@param lenses? lsp.CodeLens[] lenses to store
    499 ---@param bufnr integer
    500 ---@param client_id integer
    501 function M.save(lenses, bufnr, client_id)
    502  vim.deprecate('vim.lsp.codelens.save()', nil, '0.13.0')
    503  local _, _, _ = lenses, bufnr, client_id
    504 end
    505 
    506 ---@deprecated
    507 ---@param err? lsp.ResponseError
    508 ---@param result lsp.CodeLens[]
    509 ---@param ctx lsp.HandlerContext
    510 function M.on_codelens(err, result, ctx)
    511  vim.deprecate('vim.lsp.codelens.on_codelens()', nil, '0.13.0')
    512  local _, _, _ = err, result, ctx
    513 end
    514 
    515 ---@class vim.lsp.codelens.refresh.Opts
    516 ---@inlinedoc
    517 ---@field bufnr? integer
    518 
    519 ---@deprecated
    520 ---@param opts? vim.lsp.codelens.refresh.Opts Optional fields
    521 function M.refresh(opts)
    522  vim.deprecate(
    523    'vim.lsp.codelens.refresh({ bufnr = bufnr})',
    524    'vim.lsp.codelens.enable(true, { bufnr = bufnr })',
    525    '0.13.0'
    526  )
    527 
    528  vim.validate('opts', opts, 'table', true)
    529  M.enable(true, { bufnr = opts and opts.bufnr })
    530 end
    531 
    532 return M