neovim

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

_lsp.lua (8318B)


      1 local M = {}
      2 
      3 local capabilities = {
      4  codeActionProvider = true,
      5  documentSymbolProvider = true,
      6  executeCommandProvider = { commands = { 'delete_plugin', 'update_plugin', 'skip_update_plugin' } },
      7  hoverProvider = true,
      8 }
      9 --- @type table<string,function>
     10 local methods = {}
     11 
     12 --- @param callback function
     13 function methods.initialize(_, callback)
     14  return callback(nil, { capabilities = capabilities })
     15 end
     16 
     17 --- @param callback function
     18 function methods.shutdown(_, callback)
     19  return callback(nil, nil)
     20 end
     21 
     22 local get_confirm_bufnr = function(uri)
     23  return tonumber(uri:match('^nvim%-pack://confirm#(%d+)$'))
     24 end
     25 
     26 local group_header_pattern = '^# (%S+)'
     27 local plugin_header_pattern = '^## (.+)$'
     28 
     29 --- @return { group: string?, name: string?, from: integer?, to: integer? }
     30 local get_plug_data_at_lnum = function(bufnr, lnum)
     31  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
     32  --- @type string, string, integer, integer
     33  local group, name, from, to
     34  for i = lnum, 1, -1 do
     35    group = group or lines[i]:match(group_header_pattern) --[[@as string]]
     36    -- If group is found earlier than name - `lnum` is for group header line
     37    -- If later - proper group header line.
     38    if group then
     39      break
     40    end
     41    name = name or lines[i]:match(plugin_header_pattern) --[[@as string]]
     42    from = (not from and name) and i or from --[[@as integer]]
     43  end
     44  if not (group and name and from) then
     45    return {}
     46  end
     47  --- @cast group string
     48  --- @cast from integer
     49 
     50  for i = lnum + 1, #lines do
     51    if lines[i]:match(group_header_pattern) or lines[i]:match(plugin_header_pattern) then
     52      -- Do not include blank line before next section
     53      to = i - 2
     54      break
     55    end
     56  end
     57  to = to or #lines
     58 
     59  if not (from <= lnum and lnum <= to) then
     60    return {}
     61  end
     62  return { group = group, name = name:gsub(' %(not active%)$', ''), from = from, to = to }
     63 end
     64 
     65 --- @alias vim.pack.lsp.Position { line: integer, character: integer }
     66 --- @alias vim.pack.lsp.Range { start: vim.pack.lsp.Position, end: vim.pack.lsp.Position }
     67 
     68 --- @param params { textDocument: { uri: string } }
     69 --- @param callback function
     70 methods['textDocument/documentSymbol'] = function(params, callback)
     71  local bufnr = get_confirm_bufnr(params.textDocument.uri)
     72  if bufnr == nil then
     73    return callback(nil, {})
     74  end
     75 
     76  --- @alias vim.pack.lsp.Symbol {
     77  ---   name: string,
     78  ---   kind: number,
     79  ---   range: vim.pack.lsp.Range,
     80  ---   selectionRange: vim.pack.lsp.Range,
     81  ---   children: vim.pack.lsp.Symbol[]?,
     82  --- }
     83 
     84  --- @return vim.pack.lsp.Symbol?
     85  local new_symbol = function(name, start_line, end_line, kind)
     86    if name == nil then
     87      return nil
     88    end
     89    local range = {
     90      start = { line = start_line, character = 0 },
     91      ['end'] = { line = end_line, character = 0 },
     92    }
     93    return { name = name, kind = kind, range = range, selectionRange = range }
     94  end
     95 
     96  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
     97 
     98  --- @return vim.pack.lsp.Symbol[]
     99  local parse_headers = function(pattern, start_line, end_line, kind)
    100    local res, cur_match, cur_start = {}, nil, nil
    101    for i = start_line, end_line do
    102      local m = lines[i + 1]:match(pattern)
    103      if m ~= nil and m ~= cur_match then
    104        table.insert(res, new_symbol(cur_match, cur_start, i, kind))
    105        cur_match, cur_start = m, i
    106      end
    107    end
    108    table.insert(res, new_symbol(cur_match, cur_start, end_line, kind))
    109    return res
    110  end
    111 
    112  local group_kind = vim.lsp.protocol.SymbolKind.Namespace
    113  local symbols = parse_headers(group_header_pattern, 0, #lines - 1, group_kind)
    114 
    115  local plug_kind = vim.lsp.protocol.SymbolKind.Module
    116  for _, group in ipairs(symbols) do
    117    local start_line, end_line = group.range.start.line, group.range['end'].line
    118    group.children = parse_headers(plugin_header_pattern, start_line, end_line, plug_kind)
    119  end
    120 
    121  return callback(nil, symbols)
    122 end
    123 
    124 --- @alias vim.pack.lsp.CodeActionContext { diagnostics: table, only: table?, triggerKind: integer? }
    125 
    126 --- @param params { textDocument: { uri: string }, range: vim.pack.lsp.Range, context: vim.pack.lsp.CodeActionContext }
    127 --- @param callback function
    128 methods['textDocument/codeAction'] = function(params, callback)
    129  local bufnr = get_confirm_bufnr(params.textDocument.uri)
    130  local empty_kind = vim.lsp.protocol.CodeActionKind.Empty
    131  local only = params.context.only or { empty_kind }
    132  if not (bufnr and vim.tbl_contains(only, empty_kind)) then
    133    return callback(nil, {})
    134  end
    135  local plug_data = get_plug_data_at_lnum(bufnr, params.range.start.line + 1)
    136  if not plug_data.name then
    137    return callback(nil, {})
    138  end
    139 
    140  local function new_action(title, command)
    141    return {
    142      title = ('%s `%s`'):format(title, plug_data.name),
    143      command = { title = title, command = command, arguments = { bufnr, plug_data } },
    144    }
    145  end
    146 
    147  local res = {}
    148  if plug_data.group == 'Update' then
    149    vim.list_extend(res, {
    150      new_action('Update', 'update_plugin'),
    151      new_action('Skip updating', 'skip_update_plugin'),
    152    }, 0)
    153  end
    154  if not vim.pack.get({ plug_data.name })[1].active then
    155    vim.list_extend(res, { new_action('Delete', 'delete_plugin') })
    156  end
    157  callback(nil, res)
    158 end
    159 
    160 local commands = {
    161  update_plugin = function(plug_data)
    162    vim.pack.update({ plug_data.name }, { force = true, offline = true })
    163  end,
    164  skip_update_plugin = function(_) end,
    165  delete_plugin = function(plug_data)
    166    vim.pack.del({ plug_data.name })
    167  end,
    168 }
    169 
    170 -- NOTE: Use `vim.schedule_wrap` to avoid press-enter after choosing code
    171 -- action via built-in `vim.fn.inputlist()`
    172 --- @param params { command: string, arguments: table }
    173 --- @param callback function
    174 methods['workspace/executeCommand'] = vim.schedule_wrap(function(params, callback)
    175  --- @type integer, table
    176  local bufnr, plug_data = unpack(params.arguments)
    177  local ok, err = pcall(commands[params.command], plug_data)
    178  if not ok then
    179    return callback({ code = 1, message = err }, {})
    180  end
    181 
    182  -- Remove plugin lines (including blank line) to not later act on plugin
    183  vim.bo[bufnr].modifiable = true
    184  vim.api.nvim_buf_set_lines(bufnr, plug_data.from - 2, plug_data.to, false, {})
    185  vim.bo[bufnr].modifiable, vim.bo[bufnr].modified = false, false
    186  callback(nil, {})
    187 end)
    188 
    189 --- @param params { textDocument: { uri: string }, position: vim.pack.lsp.Position }
    190 --- @param callback function
    191 methods['textDocument/hover'] = function(params, callback)
    192  local bufnr = get_confirm_bufnr(params.textDocument.uri)
    193  if bufnr == nil then
    194    return
    195  end
    196 
    197  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
    198  local lnum = params.position.line + 1
    199  local commit = lines[lnum]:match('^[<>] (%x+) │') or lines[lnum]:match('^Revision.*:%s+(%x+)')
    200  local tag = lines[lnum]:match('^• (.+)$')
    201  if commit == nil and tag == nil then
    202    return
    203  end
    204 
    205  local path, path_lnum = nil, lnum - 1
    206  while path == nil and path_lnum >= 1 do
    207    path = lines[path_lnum]:match('^Path:%s+(.+)$')
    208    path_lnum = path_lnum - 1
    209  end
    210  if path == nil then
    211    return
    212  end
    213 
    214  local cmd = { 'git', 'show', '--no-color', commit or tag }
    215  --- @param sys_out vim.SystemCompleted
    216  local on_exit = function(sys_out)
    217    local markdown = '```diff\n' .. sys_out.stdout .. '\n```'
    218    local res = { contents = { kind = vim.lsp.protocol.MarkupKind.Markdown, value = markdown } }
    219    callback(nil, res)
    220  end
    221  vim.system(cmd, { cwd = path }, vim.schedule_wrap(on_exit))
    222 end
    223 
    224 local dispatchers = {}
    225 
    226 -- TODO: Simplify after `vim.lsp.server` is a thing
    227 -- https://github.com/neovim/neovim/pull/24338
    228 local cmd = function(disp)
    229  -- Store dispatchers to use for showing progress notifications
    230  dispatchers = disp
    231  local res, closing, request_id = {}, false, 0
    232 
    233  function res.request(method, params, callback)
    234    local method_impl = methods[method]
    235    if method_impl ~= nil then
    236      method_impl(params, callback)
    237    end
    238    request_id = request_id + 1
    239    return true, request_id
    240  end
    241 
    242  function res.notify(method, _)
    243    if method == 'exit' then
    244      dispatchers.on_exit(0, 15)
    245    end
    246    return false
    247  end
    248 
    249  function res.is_closing()
    250    return closing
    251  end
    252 
    253  function res.terminate()
    254    closing = true
    255  end
    256 
    257  return res
    258 end
    259 
    260 M.client_id = assert(
    261  vim.lsp.start({ cmd = cmd, name = 'vim.pack', root_dir = vim.uv.cwd() }, { attach = false })
    262 )
    263 
    264 return M