neovim

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

inlay_hint.lua (12503B)


      1 local util = require('vim.lsp.util')
      2 local log = require('vim.lsp.log')
      3 local api = vim.api
      4 local M = {}
      5 
      6 ---@class (private) vim.lsp.inlay_hint.globalstate Global state for inlay hints
      7 ---@field enabled boolean Whether inlay hints are enabled for this scope
      8 ---@type vim.lsp.inlay_hint.globalstate
      9 local globalstate = {
     10  enabled = false,
     11 }
     12 
     13 ---@class (private) vim.lsp.inlay_hint.bufstate: vim.lsp.inlay_hint.globalstate Buffer local state for inlay hints
     14 ---@field version? integer
     15 ---@field client_hints? table<integer, table<integer, lsp.InlayHint[]>> client_id -> (lnum -> hints)
     16 ---@field applied table<integer, integer> Last version of hints applied to this line
     17 
     18 ---@type table<integer, vim.lsp.inlay_hint.bufstate>
     19 local bufstates = vim.defaulttable(function(_)
     20  return setmetatable({ applied = {} }, {
     21    __index = globalstate,
     22    __newindex = function(state, key, value)
     23      if globalstate[key] == value then
     24        rawset(state, key, nil)
     25      else
     26        rawset(state, key, value)
     27      end
     28    end,
     29  })
     30 end)
     31 
     32 local namespace = api.nvim_create_namespace('nvim.lsp.inlayhint')
     33 local augroup = api.nvim_create_augroup('nvim.lsp.inlayhint', {})
     34 
     35 --- |lsp-handler| for the method `textDocument/inlayHint`
     36 --- Store hints for a specific buffer and client
     37 ---@param result lsp.InlayHint[]?
     38 ---@param ctx lsp.HandlerContext
     39 ---@private
     40 function M.on_inlayhint(err, result, ctx)
     41  if err then
     42    log.error('inlayhint', err)
     43    return
     44  end
     45  local bufnr = assert(ctx.bufnr)
     46 
     47  if
     48    util.buf_versions[bufnr] ~= ctx.version
     49    or not api.nvim_buf_is_loaded(bufnr)
     50    or not bufstates[bufnr].enabled
     51  then
     52    return
     53  end
     54  local client_id = ctx.client_id
     55  local bufstate = bufstates[bufnr]
     56  if not (bufstate.client_hints and bufstate.version) then
     57    bufstate.client_hints = vim.defaulttable()
     58    bufstate.version = ctx.version
     59  end
     60  local client_hints = bufstate.client_hints
     61  local client = assert(vim.lsp.get_client_by_id(client_id))
     62 
     63  -- If there's no error but the result is nil, clear existing hints.
     64  result = result or {}
     65 
     66  local new_lnum_hints = vim.defaulttable()
     67  local num_unprocessed = #result
     68  if num_unprocessed == 0 then
     69    client_hints[client_id] = {}
     70    bufstate.version = ctx.version
     71    api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
     72    return
     73  end
     74 
     75  local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
     76 
     77  for _, hint in ipairs(result) do
     78    local lnum = hint.position.line
     79    local line = lines and lines[lnum + 1] or ''
     80    hint.position.character =
     81      vim.str_byteindex(line, client.offset_encoding, hint.position.character, false)
     82    table.insert(new_lnum_hints[lnum], hint)
     83  end
     84 
     85  client_hints[client_id] = new_lnum_hints
     86  bufstate.version = ctx.version
     87  api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
     88 end
     89 
     90 --- Refresh inlay hints, only if we have attached clients that support it
     91 ---@param bufnr (integer) Buffer handle, or 0 for current
     92 ---@param client_id? (integer) Client ID, or nil for all
     93 local function refresh(bufnr, client_id)
     94  for _, client in
     95    ipairs(vim.lsp.get_clients({
     96      bufnr = bufnr,
     97      id = client_id,
     98      method = 'textDocument/inlayHint',
     99    }))
    100  do
    101    client:request('textDocument/inlayHint', {
    102      textDocument = util.make_text_document_params(bufnr),
    103      range = util._make_line_range_params(
    104        bufnr,
    105        0,
    106        api.nvim_buf_line_count(bufnr) - 1,
    107        client.offset_encoding
    108      ),
    109    }, nil, bufnr)
    110  end
    111 end
    112 
    113 --- |lsp-handler| for the method `workspace/inlayHint/refresh`
    114 ---@param ctx lsp.HandlerContext
    115 ---@private
    116 function M.on_refresh(err, _, ctx)
    117  if err then
    118    return vim.NIL
    119  end
    120  for bufnr in pairs(vim.lsp.get_client_by_id(ctx.client_id).attached_buffers or {}) do
    121    for _, winid in ipairs(api.nvim_list_wins()) do
    122      if api.nvim_win_get_buf(winid) == bufnr then
    123        if bufstates[bufnr] and bufstates[bufnr].enabled then
    124          bufstates[bufnr].applied = {}
    125          refresh(bufnr)
    126        end
    127      end
    128    end
    129  end
    130 
    131  return vim.NIL
    132 end
    133 
    134 --- Optional filters |kwargs|:
    135 --- @class vim.lsp.inlay_hint.get.Filter
    136 --- @inlinedoc
    137 --- @field bufnr integer?
    138 --- @field range lsp.Range?
    139 
    140 --- @class vim.lsp.inlay_hint.get.ret
    141 --- @inlinedoc
    142 --- @field bufnr integer
    143 --- @field client_id integer
    144 --- @field inlay_hint lsp.InlayHint
    145 
    146 --- Get the list of inlay hints, (optionally) restricted by buffer or range.
    147 ---
    148 --- Example usage:
    149 ---
    150 --- ```lua
    151 --- local hint = vim.lsp.inlay_hint.get({ bufnr = 0 })[1] -- 0 for current buffer
    152 ---
    153 --- local client = vim.lsp.get_client_by_id(hint.client_id)
    154 --- local resp = client:request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0)
    155 --- local resolved_hint = assert(resp and resp.result, resp.err)
    156 --- vim.lsp.util.apply_text_edits(resolved_hint.textEdits, 0, client.encoding)
    157 ---
    158 --- location = resolved_hint.label[1].location
    159 --- client:request('textDocument/hover', {
    160 ---   textDocument = { uri = location.uri },
    161 ---   position = location.range.start,
    162 --- })
    163 --- ```
    164 ---
    165 --- @param filter vim.lsp.inlay_hint.get.Filter?
    166 --- @return vim.lsp.inlay_hint.get.ret[]
    167 --- @since 12
    168 function M.get(filter)
    169  vim.validate('filter', filter, 'table', true)
    170  filter = filter or {}
    171 
    172  local bufnr = filter.bufnr
    173  if not bufnr then
    174    --- @type vim.lsp.inlay_hint.get.ret[]
    175    local hints = {}
    176    --- @param buf integer
    177    vim.tbl_map(function(buf)
    178      vim.list_extend(hints, M.get(vim.tbl_extend('keep', { bufnr = buf }, filter)))
    179    end, api.nvim_list_bufs())
    180    return hints
    181  else
    182    bufnr = vim._resolve_bufnr(bufnr)
    183  end
    184 
    185  local bufstate = bufstates[bufnr]
    186  if not bufstate.client_hints then
    187    return {}
    188  end
    189 
    190  local clients = vim.lsp.get_clients({
    191    bufnr = bufnr,
    192    method = 'textDocument/inlayHint',
    193  })
    194  if #clients == 0 then
    195    return {}
    196  end
    197 
    198  local range = filter.range
    199  if not range then
    200    range = {
    201      start = { line = 0, character = 0 },
    202      ['end'] = { line = api.nvim_buf_line_count(bufnr), character = 0 },
    203    }
    204  end
    205 
    206  --- @type vim.lsp.inlay_hint.get.ret[]
    207  local result = {}
    208  for _, client in pairs(clients) do
    209    local lnum_hints = bufstate.client_hints[client.id]
    210    if lnum_hints then
    211      for lnum = range.start.line, range['end'].line do
    212        local hints = lnum_hints[lnum] or {}
    213        for _, hint in pairs(hints) do
    214          local line, char = hint.position.line, hint.position.character
    215          if
    216            (line > range.start.line or char >= range.start.character)
    217            and (line < range['end'].line or char <= range['end'].character)
    218          then
    219            table.insert(result, {
    220              bufnr = bufnr,
    221              client_id = client.id,
    222              inlay_hint = hint,
    223            })
    224          end
    225        end
    226      end
    227    end
    228  end
    229  return result
    230 end
    231 
    232 --- Clear inlay hints
    233 ---@param bufnr (integer) Buffer handle, or 0 for current
    234 local function clear(bufnr)
    235  bufnr = vim._resolve_bufnr(bufnr)
    236  local bufstate = bufstates[bufnr]
    237  local client_lens = (bufstate or {}).client_hints or {}
    238  local client_ids = vim.tbl_keys(client_lens) --- @type integer[]
    239  for _, iter_client_id in ipairs(client_ids) do
    240    if bufstate then
    241      bufstate.client_hints[iter_client_id] = {}
    242    end
    243  end
    244  api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
    245  api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
    246 end
    247 
    248 --- Disable inlay hints for a buffer
    249 ---@param bufnr (integer) Buffer handle, or 0 for current
    250 local function _disable(bufnr)
    251  bufnr = vim._resolve_bufnr(bufnr)
    252  clear(bufnr)
    253  bufstates[bufnr] = nil
    254  bufstates[bufnr].enabled = false
    255 end
    256 
    257 --- Enable inlay hints for a buffer
    258 ---@param bufnr (integer) Buffer handle, or 0 for current
    259 local function _enable(bufnr)
    260  bufnr = vim._resolve_bufnr(bufnr)
    261  bufstates[bufnr] = nil
    262  bufstates[bufnr].enabled = true
    263  refresh(bufnr)
    264 end
    265 
    266 api.nvim_create_autocmd('LspNotify', {
    267  callback = function(args)
    268    ---@type integer
    269    local bufnr = args.buf
    270 
    271    if
    272      args.data.method ~= 'textDocument/didChange'
    273      and args.data.method ~= 'textDocument/didOpen'
    274    then
    275      return
    276    end
    277    if bufstates[bufnr].enabled then
    278      refresh(bufnr, args.data.client_id)
    279    end
    280  end,
    281  group = augroup,
    282 })
    283 api.nvim_create_autocmd('LspAttach', {
    284  callback = function(args)
    285    ---@type integer
    286    local bufnr = args.buf
    287 
    288    api.nvim_buf_attach(bufnr, false, {
    289      on_reload = function(_, cb_bufnr)
    290        clear(cb_bufnr)
    291        if bufstates[cb_bufnr] and bufstates[cb_bufnr].enabled then
    292          bufstates[cb_bufnr].applied = {}
    293          refresh(cb_bufnr)
    294        end
    295      end,
    296      on_detach = function(_, cb_bufnr)
    297        _disable(cb_bufnr)
    298        bufstates[cb_bufnr] = nil
    299      end,
    300    })
    301  end,
    302  group = augroup,
    303 })
    304 api.nvim_create_autocmd('LspDetach', {
    305  callback = function(args)
    306    ---@type integer
    307    local bufnr = args.buf
    308    local clients = vim.lsp.get_clients({ bufnr = bufnr, method = 'textDocument/inlayHint' })
    309 
    310    if not vim.iter(clients):any(function(c)
    311      return c.id ~= args.data.client_id
    312    end) then
    313      _disable(bufnr)
    314    end
    315  end,
    316  group = augroup,
    317 })
    318 api.nvim_set_decoration_provider(namespace, {
    319  on_win = function(_, _, bufnr, topline, botline)
    320    ---@type vim.lsp.inlay_hint.bufstate
    321    local bufstate = rawget(bufstates, bufnr)
    322    if not bufstate then
    323      return
    324    end
    325 
    326    if bufstate.version ~= util.buf_versions[bufnr] then
    327      return
    328    end
    329 
    330    if not bufstate.client_hints then
    331      return
    332    end
    333    local client_hints = assert(bufstate.client_hints)
    334 
    335    for lnum = topline, botline do
    336      if bufstate.applied[lnum] ~= bufstate.version then
    337        api.nvim_buf_clear_namespace(bufnr, namespace, lnum, lnum + 1)
    338 
    339        local hint_virtual_texts = {} --- @type table<integer, [string, string?][]>
    340        for _, lnum_hints in pairs(client_hints) do
    341          local hints = lnum_hints[lnum] or {}
    342          for _, hint in pairs(hints) do
    343            local text = ''
    344            local label = hint.label
    345            if type(label) == 'string' then
    346              text = label
    347            else
    348              for _, part in ipairs(label) do
    349                text = text .. part.value
    350              end
    351            end
    352            local vt = hint_virtual_texts[hint.position.character] or {}
    353            if hint.paddingLeft then
    354              vt[#vt + 1] = { ' ' }
    355            end
    356            vt[#vt + 1] = { text, 'LspInlayHint' }
    357            if hint.paddingRight then
    358              vt[#vt + 1] = { ' ' }
    359            end
    360            hint_virtual_texts[hint.position.character] = vt
    361          end
    362        end
    363 
    364        for pos, vt in pairs(hint_virtual_texts) do
    365          api.nvim_buf_set_extmark(bufnr, namespace, lnum, pos, {
    366            virt_text_pos = 'inline',
    367            ephemeral = false,
    368            virt_text = vt,
    369          })
    370        end
    371 
    372        bufstate.applied[lnum] = bufstate.version
    373      end
    374    end
    375  end,
    376 })
    377 
    378 --- Query whether inlay hint is enabled in the {filter}ed scope
    379 --- @param filter? vim.lsp.inlay_hint.enable.Filter
    380 --- @return boolean
    381 --- @since 12
    382 function M.is_enabled(filter)
    383  vim.validate('filter', filter, 'table', true)
    384  filter = filter or {}
    385  local bufnr = filter.bufnr
    386 
    387  if bufnr == nil then
    388    return globalstate.enabled
    389  end
    390  return bufstates[vim._resolve_bufnr(bufnr)].enabled
    391 end
    392 
    393 --- Optional filters |kwargs|, or `nil` for all.
    394 --- @class vim.lsp.inlay_hint.enable.Filter
    395 --- @inlinedoc
    396 --- Buffer number, or 0 for current buffer, or nil for all.
    397 --- @field bufnr integer?
    398 
    399 --- Enables or disables inlay hints for the {filter}ed scope.
    400 ---
    401 --- To "toggle", pass the inverse of `is_enabled()`:
    402 ---
    403 --- ```lua
    404 --- vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled())
    405 --- ```
    406 ---
    407 --- @param enable (boolean|nil) true/nil to enable, false to disable
    408 --- @param filter vim.lsp.inlay_hint.enable.Filter?
    409 --- @since 12
    410 function M.enable(enable, filter)
    411  vim.validate('enable', enable, 'boolean', true)
    412  vim.validate('filter', filter, 'table', true)
    413  enable = enable == nil or enable
    414  filter = filter or {}
    415 
    416  if filter.bufnr == nil then
    417    globalstate.enabled = enable
    418    for _, bufnr in ipairs(api.nvim_list_bufs()) do
    419      if api.nvim_buf_is_loaded(bufnr) then
    420        if enable == false then
    421          _disable(bufnr)
    422        else
    423          _enable(bufnr)
    424        end
    425      else
    426        bufstates[bufnr] = nil
    427      end
    428    end
    429  else
    430    if enable == false then
    431      _disable(filter.bufnr)
    432    else
    433      _enable(filter.bufnr)
    434    end
    435  end
    436 end
    437 
    438 return M