aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2024-11-25 19:15:05 +0000
committerJosh Rahm <joshuarahm@gmail.com>2024-11-25 19:27:38 +0000
commitc5d770d311841ea5230426cc4c868e8db27300a8 (patch)
treedd21f70127b4b8b5f109baefc8ecc5016f507c91 /runtime/lua/vim/lsp
parent9be89f131f87608f224f0ee06d199fcd09d32176 (diff)
parent081beb3659bd6d8efc3e977a160b1e72becbd8a2 (diff)
downloadrneovim-c5d770d311841ea5230426cc4c868e8db27300a8.tar.gz
rneovim-c5d770d311841ea5230426cc4c868e8db27300a8.tar.bz2
rneovim-c5d770d311841ea5230426cc4c868e8db27300a8.zip
Merge remote-tracking branch 'upstream/master' into mix_20240309
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/_dynamic.lua110
-rw-r--r--runtime/lua/vim/lsp/_meta.lua3
-rw-r--r--runtime/lua/vim/lsp/_tagfunc.lua49
-rw-r--r--runtime/lua/vim/lsp/_watchfiles.lua10
-rw-r--r--runtime/lua/vim/lsp/buf.lua702
-rw-r--r--runtime/lua/vim/lsp/client.lua255
-rw-r--r--runtime/lua/vim/lsp/codelens.lua4
-rw-r--r--runtime/lua/vim/lsp/completion.lua70
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua109
-rw-r--r--runtime/lua/vim/lsp/handlers.lua308
-rw-r--r--runtime/lua/vim/lsp/health.lua23
-rw-r--r--runtime/lua/vim/lsp/inlay_hint.lua33
-rw-r--r--runtime/lua/vim/lsp/log.lua9
-rw-r--r--runtime/lua/vim/lsp/protocol.lua112
-rw-r--r--runtime/lua/vim/lsp/rpc.lua103
-rw-r--r--runtime/lua/vim/lsp/semantic_tokens.lua110
-rw-r--r--runtime/lua/vim/lsp/sync.lua59
-rw-r--r--runtime/lua/vim/lsp/util.lua1101
18 files changed, 1708 insertions, 1462 deletions
diff --git a/runtime/lua/vim/lsp/_dynamic.lua b/runtime/lua/vim/lsp/_dynamic.lua
deleted file mode 100644
index 27113c0e74..0000000000
--- a/runtime/lua/vim/lsp/_dynamic.lua
+++ /dev/null
@@ -1,110 +0,0 @@
-local glob = vim.glob
-
---- @class lsp.DynamicCapabilities
---- @field capabilities table<string, lsp.Registration[]>
---- @field client_id number
-local M = {}
-
---- @param client_id number
---- @return lsp.DynamicCapabilities
-function M.new(client_id)
- return setmetatable({
- capabilities = {},
- client_id = client_id,
- }, { __index = M })
-end
-
-function M:supports_registration(method)
- local client = vim.lsp.get_client_by_id(self.client_id)
- if not client then
- return false
- end
- local capability = vim.tbl_get(client.capabilities, unpack(vim.split(method, '/')))
- return type(capability) == 'table' and capability.dynamicRegistration
-end
-
---- @param registrations lsp.Registration[]
-function M:register(registrations)
- -- remove duplicates
- self:unregister(registrations)
- for _, reg in ipairs(registrations) do
- local method = reg.method
- if not self.capabilities[method] then
- self.capabilities[method] = {}
- end
- table.insert(self.capabilities[method], reg)
- end
-end
-
---- @param unregisterations lsp.Unregistration[]
-function M:unregister(unregisterations)
- for _, unreg in ipairs(unregisterations) do
- local method = unreg.method
- if not self.capabilities[method] then
- return
- end
- local id = unreg.id
- for i, reg in ipairs(self.capabilities[method]) do
- if reg.id == id then
- table.remove(self.capabilities[method], i)
- break
- end
- end
- end
-end
-
---- @param method string
---- @param opts? {bufnr: integer?}
---- @return lsp.Registration? (table|nil) the registration if found
-function M:get(method, opts)
- opts = opts or {}
- opts.bufnr = opts.bufnr or vim.api.nvim_get_current_buf()
- for _, reg in ipairs(self.capabilities[method] or {}) do
- if not reg.registerOptions then
- return reg
- end
- local documentSelector = reg.registerOptions.documentSelector
- if not documentSelector then
- return reg
- end
- if self:match(opts.bufnr, documentSelector) then
- return reg
- end
- end
-end
-
---- @param method string
---- @param opts? {bufnr: integer?}
-function M:supports(method, opts)
- return self:get(method, opts) ~= nil
-end
-
---- @param bufnr number
---- @param documentSelector lsp.DocumentSelector
---- @private
-function M:match(bufnr, documentSelector)
- local client = vim.lsp.get_client_by_id(self.client_id)
- if not client then
- return false
- end
- local language = client.get_language_id(bufnr, vim.bo[bufnr].filetype)
- local uri = vim.uri_from_bufnr(bufnr)
- local fname = vim.uri_to_fname(uri)
- for _, filter in ipairs(documentSelector) do
- local matches = true
- if filter.language and language ~= filter.language then
- matches = false
- end
- if matches and filter.scheme and not vim.startswith(uri, filter.scheme .. ':') then
- matches = false
- end
- if matches and filter.pattern and not glob.to_lpeg(filter.pattern):match(fname) then
- matches = false
- end
- if matches then
- return true
- end
- end
-end
-
-return M
diff --git a/runtime/lua/vim/lsp/_meta.lua b/runtime/lua/vim/lsp/_meta.lua
index be3222828d..bf693ccc57 100644
--- a/runtime/lua/vim/lsp/_meta.lua
+++ b/runtime/lua/vim/lsp/_meta.lua
@@ -1,7 +1,8 @@
---@meta
error('Cannot require a meta file')
----@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext, config?: table): ...any
+---@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext): ...any
+---@alias lsp.MultiHandler fun(results: table<integer,{err: lsp.ResponseError?, result: any}>, context: lsp.HandlerContext): ...any
---@class lsp.HandlerContext
---@field method string
diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua
index 4ad50e4a58..f75d43f373 100644
--- a/runtime/lua/vim/lsp/_tagfunc.lua
+++ b/runtime/lua/vim/lsp/_tagfunc.lua
@@ -1,4 +1,5 @@
local lsp = vim.lsp
+local api = vim.api
local util = lsp.util
local ms = lsp.protocol.Methods
@@ -21,32 +22,48 @@ end
---@param pattern string
---@return table[]
local function query_definition(pattern)
- local params = util.make_position_params()
- local results_by_client, err = lsp.buf_request_sync(0, ms.textDocument_definition, params, 1000)
- if err then
+ local bufnr = api.nvim_get_current_buf()
+ local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_definition })
+ if not next(clients) then
return {}
end
+ local win = api.nvim_get_current_win()
local results = {}
+
+ --- @param range lsp.Range
+ --- @param uri string
+ --- @param offset_encoding string
local add = function(range, uri, offset_encoding)
table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding))
end
- for client_id, lsp_results in pairs(assert(results_by_client)) do
- local client = lsp.get_client_by_id(client_id)
- local offset_encoding = client and client.offset_encoding or 'utf-16'
- local result = lsp_results.result or {}
- if result.range then -- Location
- add(result.range, result.uri)
- else
- result = result --[[@as (lsp.Location[]|lsp.LocationLink[])]]
- for _, item in pairs(result) do
- if item.range then -- Location
- add(item.range, item.uri, offset_encoding)
- else -- LocationLink
- add(item.targetSelectionRange, item.targetUri, offset_encoding)
+
+ local remaining = #clients
+ for _, client in ipairs(clients) do
+ ---@param result nil|lsp.Location|lsp.Location[]|lsp.LocationLink[]
+ local function on_response(_, result)
+ if result then
+ local encoding = client.offset_encoding
+ -- single Location
+ if result.range then
+ add(result.range, result.uri, encoding)
+ else
+ for _, location in ipairs(result) do
+ if location.range then -- Location
+ add(location.range, location.uri, encoding)
+ else -- LocationLink
+ add(location.targetSelectionRange, location.targetUri, encoding)
+ end
+ end
end
end
+ remaining = remaining - 1
end
+ local params = util.make_position_params(win, client.offset_encoding)
+ client.request(ms.textDocument_definition, params, on_response, bufnr)
end
+ vim.wait(1000, function()
+ return remaining == 0
+ end)
return results
end
diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua
index 98e9818bcd..c4cdb5aea8 100644
--- a/runtime/lua/vim/lsp/_watchfiles.lua
+++ b/runtime/lua/vim/lsp/_watchfiles.lua
@@ -44,9 +44,8 @@ M._poll_exclude_pattern = glob.to_lpeg('**/.git/{objects,subtree-cache}/**')
--- Registers the workspace/didChangeWatchedFiles capability dynamically.
---
---@param reg lsp.Registration LSP Registration object.
----@param ctx lsp.HandlerContext Context from the |lsp-handler|.
-function M.register(reg, ctx)
- local client_id = ctx.client_id
+---@param client_id integer Client ID.
+function M.register(reg, client_id)
local client = assert(vim.lsp.get_client_by_id(client_id), 'Client must be running')
-- Ill-behaved servers may not honor the client capability and try to register
-- anyway, so ignore requests when the user has opted out of the feature.
@@ -155,9 +154,8 @@ end
--- Unregisters the workspace/didChangeWatchedFiles capability dynamically.
---
---@param unreg lsp.Unregistration LSP Unregistration object.
----@param ctx lsp.HandlerContext Context from the |lsp-handler|.
-function M.unregister(unreg, ctx)
- local client_id = ctx.client_id
+---@param client_id integer Client ID.
+function M.unregister(unreg, client_id)
local client_cancels = cancels[client_id]
local reg_cancels = client_cancels[unreg.id]
while #reg_cancels > 0 do
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 301c1f0cb6..6383855a30 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -1,4 +1,5 @@
local api = vim.api
+local lsp = vim.lsp
local validate = vim.validate
local util = require('vim.lsp.util')
local npcall = vim.F.npcall
@@ -6,28 +7,24 @@ local ms = require('vim.lsp.protocol').Methods
local M = {}
---- Sends an async request to all active clients attached to the current
---- buffer.
----
----@param method (string) LSP method name
----@param params (table|nil) Parameters to send to the server
----@param handler lsp.Handler? See |lsp-handler|. Follows |lsp-handler-resolution|
----
----@return table<integer, integer> client_request_ids Map of client-id:request-id pairs
----for all successful requests.
----@return function _cancel_all_requests Function which can be used to
----cancel all the requests. You could instead
----iterate all clients and call their `cancel_request()` methods.
----
----@see |vim.lsp.buf_request()|
-local function request(method, params, handler)
- validate({
- method = { method, 's' },
- handler = { handler, 'f', true },
- })
- return vim.lsp.buf_request(0, method, params, handler)
+--- @param params? table
+--- @return fun(client: vim.lsp.Client): lsp.TextDocumentPositionParams
+local function client_positional_params(params)
+ local win = api.nvim_get_current_win()
+ return function(client)
+ local ret = util.make_position_params(win, client.offset_encoding)
+ if params then
+ ret = vim.tbl_extend('force', ret, params)
+ end
+ return ret
+ end
end
+local hover_ns = api.nvim_create_namespace('vim_lsp_hover_range')
+
+--- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts
+--- @field silent? boolean
+
--- Displays hover information about the symbol under the cursor in a floating
--- window. The window will be dismissed on cursor move.
--- Calling the function twice will jump into the floating window
@@ -35,21 +32,210 @@ end
--- In the floating window, all commands and mappings are available as usual,
--- except that "q" dismisses the window.
--- You can scroll the contents the same as you would any other buffer.
-function M.hover()
- local params = util.make_position_params()
- request(ms.textDocument_hover, params)
+---
+--- Note: to disable hover highlights, add the following to your config:
+---
+--- ```lua
+--- vim.api.nvim_create_autocmd('ColorScheme', {
+--- callback = function()
+--- vim.api.nvim_set_hl(0, 'LspReferenceTarget', {})
+--- end,
+--- })
+--- ```
+--- @param config? vim.lsp.buf.hover.Opts
+function M.hover(config)
+ config = config or {}
+ config.focus_id = ms.textDocument_hover
+
+ lsp.buf_request_all(0, ms.textDocument_hover, client_positional_params(), function(results, ctx)
+ local bufnr = assert(ctx.bufnr)
+ if api.nvim_get_current_buf() ~= bufnr then
+ -- Ignore result since buffer changed. This happens for slow language servers.
+ return
+ end
+
+ -- Filter errors from results
+ local results1 = {} --- @type table<integer,lsp.Hover>
+
+ for client_id, resp in pairs(results) do
+ local err, result = resp.err, resp.result
+ if err then
+ lsp.log.error(err.code, err.message)
+ elseif result then
+ results1[client_id] = result
+ end
+ end
+
+ if vim.tbl_isempty(results1) then
+ if config.silent ~= true then
+ vim.notify('No information available')
+ end
+ return
+ end
+
+ local contents = {} --- @type string[]
+
+ local nresults = #vim.tbl_keys(results1)
+
+ local format = 'markdown'
+
+ for client_id, result in pairs(results1) do
+ local client = assert(lsp.get_client_by_id(client_id))
+ if nresults > 1 then
+ -- Show client name if there are multiple clients
+ contents[#contents + 1] = string.format('# %s', client.name)
+ end
+ if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then
+ if #results1 == 1 then
+ format = 'plaintext'
+ contents = vim.split(result.contents.value or '', '\n', { trimempty = true })
+ else
+ -- Surround plaintext with ``` to get correct formatting
+ contents[#contents + 1] = '```'
+ vim.list_extend(
+ contents,
+ vim.split(result.contents.value or '', '\n', { trimempty = true })
+ )
+ contents[#contents + 1] = '```'
+ end
+ else
+ vim.list_extend(contents, util.convert_input_to_markdown_lines(result.contents))
+ end
+ local range = result.range
+ if range then
+ local start = range.start
+ local end_ = range['end']
+ local start_idx = util._get_line_byte_from_position(bufnr, start, client.offset_encoding)
+ local end_idx = util._get_line_byte_from_position(bufnr, end_, client.offset_encoding)
+
+ vim.hl.range(
+ bufnr,
+ hover_ns,
+ 'LspReferenceTarget',
+ { start.line, start_idx },
+ { end_.line, end_idx },
+ { priority = vim.hl.priorities.user }
+ )
+ end
+ contents[#contents + 1] = '---'
+ end
+
+ -- Remove last linebreak ('---')
+ contents[#contents] = nil
+
+ if vim.tbl_isempty(contents) then
+ if config.silent ~= true then
+ vim.notify('No information available')
+ end
+ return
+ end
+
+ local _, winid = lsp.util.open_floating_preview(contents, format, config)
+
+ api.nvim_create_autocmd('WinClosed', {
+ pattern = tostring(winid),
+ once = true,
+ callback = function()
+ api.nvim_buf_clear_namespace(bufnr, hover_ns, 0, -1)
+ return true
+ end,
+ })
+ end)
end
local function request_with_opts(name, params, opts)
local req_handler --- @type function?
if opts then
req_handler = function(err, result, ctx, config)
- local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
- local handler = client.handlers[name] or vim.lsp.handlers[name]
+ local client = assert(lsp.get_client_by_id(ctx.client_id))
+ local handler = client.handlers[name] or lsp.handlers[name]
handler(err, result, ctx, vim.tbl_extend('force', config or {}, opts))
end
end
- request(name, params, req_handler)
+ lsp.buf_request(0, name, params, req_handler)
+end
+
+---@param method string
+---@param opts? vim.lsp.LocationOpts
+local function get_locations(method, opts)
+ opts = opts or {}
+ local bufnr = api.nvim_get_current_buf()
+ local clients = lsp.get_clients({ method = method, bufnr = bufnr })
+ if not next(clients) then
+ vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN)
+ return
+ end
+ local win = api.nvim_get_current_win()
+ local from = vim.fn.getpos('.')
+ from[1] = bufnr
+ local tagname = vim.fn.expand('<cword>')
+ local remaining = #clients
+
+ ---@type vim.quickfix.entry[]
+ local all_items = {}
+
+ ---@param result nil|lsp.Location|lsp.Location[]
+ ---@param client vim.lsp.Client
+ local function on_response(_, result, client)
+ local locations = {}
+ if result then
+ locations = vim.islist(result) and result or { result }
+ end
+ local items = util.locations_to_items(locations, client.offset_encoding)
+ vim.list_extend(all_items, items)
+ remaining = remaining - 1
+ if remaining == 0 then
+ if vim.tbl_isempty(all_items) then
+ vim.notify('No locations found', vim.log.levels.INFO)
+ return
+ end
+
+ local title = 'LSP locations'
+ if opts.on_list then
+ assert(vim.is_callable(opts.on_list), 'on_list is not a function')
+ opts.on_list({
+ title = title,
+ items = all_items,
+ context = { bufnr = bufnr, method = method },
+ })
+ return
+ end
+
+ if #all_items == 1 then
+ local item = all_items[1]
+ local b = item.bufnr or vim.fn.bufadd(item.filename)
+
+ -- Save position in jumplist
+ vim.cmd("normal! m'")
+ -- Push a new item into tagstack
+ local tagstack = { { tagname = tagname, from = from } }
+ vim.fn.settagstack(vim.fn.win_getid(win), { items = tagstack }, 't')
+
+ vim.bo[b].buflisted = true
+ local w = opts.reuse_win and vim.fn.win_findbuf(b)[1] or win
+ api.nvim_win_set_buf(w, b)
+ api.nvim_win_set_cursor(w, { item.lnum, item.col - 1 })
+ vim._with({ win = w }, function()
+ -- Open folds under the cursor
+ vim.cmd('normal! zv')
+ end)
+ return
+ end
+ if opts.loclist then
+ vim.fn.setloclist(0, {}, ' ', { title = title, items = all_items })
+ vim.cmd.lopen()
+ else
+ vim.fn.setqflist({}, ' ', { title = title, items = all_items })
+ vim.cmd('botright copen')
+ end
+ end
+ end
+ for _, client in ipairs(clients) do
+ local params = util.make_position_params(win, client.offset_encoding)
+ client.request(method, params, function(_, result)
+ on_response(_, result, client)
+ end)
+ end
end
--- @class vim.lsp.ListOpts
@@ -89,39 +275,145 @@ end
--- @note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead.
--- @param opts? vim.lsp.LocationOpts
function M.declaration(opts)
- local params = util.make_position_params()
- request_with_opts(ms.textDocument_declaration, params, opts)
+ get_locations(ms.textDocument_declaration, opts)
end
--- Jumps to the definition of the symbol under the cursor.
--- @param opts? vim.lsp.LocationOpts
function M.definition(opts)
- local params = util.make_position_params()
- request_with_opts(ms.textDocument_definition, params, opts)
+ get_locations(ms.textDocument_definition, opts)
end
--- Jumps to the definition of the type of the symbol under the cursor.
--- @param opts? vim.lsp.LocationOpts
function M.type_definition(opts)
- local params = util.make_position_params()
- request_with_opts(ms.textDocument_typeDefinition, params, opts)
+ get_locations(ms.textDocument_typeDefinition, opts)
end
--- Lists all the implementations for the symbol under the cursor in the
--- quickfix window.
--- @param opts? vim.lsp.LocationOpts
function M.implementation(opts)
- local params = util.make_position_params()
- request_with_opts(ms.textDocument_implementation, params, opts)
+ get_locations(ms.textDocument_implementation, opts)
end
+--- @param results table<integer,{err: lsp.ResponseError?, result: lsp.SignatureHelp?}>
+local function process_signature_help_results(results)
+ local signatures = {} --- @type [vim.lsp.Client,lsp.SignatureInformation][]
+
+ -- Pre-process results
+ for client_id, r in pairs(results) do
+ local err = r.err
+ local client = assert(lsp.get_client_by_id(client_id))
+ if err then
+ vim.notify(
+ client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message,
+ vim.log.levels.ERROR
+ )
+ api.nvim_command('redraw')
+ else
+ local result = r.result --- @type lsp.SignatureHelp
+ if result and result.signatures and result.signatures[1] then
+ for _, sig in ipairs(result.signatures) do
+ signatures[#signatures + 1] = { client, sig }
+ end
+ end
+ end
+ end
+
+ return signatures
+end
+
+local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help')
+
+--- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts
+--- @field silent? boolean
+
+-- TODO(lewis6991): support multiple clients
--- Displays signature information about the symbol under the cursor in a
--- floating window.
-function M.signature_help()
- local params = util.make_position_params()
- request(ms.textDocument_signatureHelp, params)
+--- @param config? vim.lsp.buf.signature_help.Opts
+function M.signature_help(config)
+ local method = ms.textDocument_signatureHelp
+
+ config = config and vim.deepcopy(config) or {}
+ config.focus_id = method
+
+ lsp.buf_request_all(0, method, client_positional_params(), function(results, ctx)
+ if api.nvim_get_current_buf() ~= ctx.bufnr then
+ -- Ignore result since buffer changed. This happens for slow language servers.
+ return
+ end
+
+ local signatures = process_signature_help_results(results)
+
+ if not next(signatures) then
+ if config.silent ~= true then
+ print('No signature help available')
+ end
+ return
+ end
+
+ local ft = vim.bo[ctx.bufnr].filetype
+ local total = #signatures
+ local idx = 0
+
+ --- @param update_win? integer
+ local function show_signature(update_win)
+ idx = (idx % total) + 1
+ local client, result = signatures[idx][1], signatures[idx][2]
+ --- @type string[]?
+ local triggers =
+ vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters')
+ local lines, hl =
+ util.convert_signature_help_to_markdown_lines({ signatures = { result } }, ft, triggers)
+ if not lines then
+ return
+ end
+
+ local sfx = total > 1 and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or ''
+ local title = string.format('Signature Help: %s%s', client.name, sfx)
+ if config.border then
+ config.title = title
+ else
+ table.insert(lines, 1, '# ' .. title)
+ if hl then
+ hl[1] = hl[1] + 1
+ hl[3] = hl[3] + 1
+ end
+ end
+
+ config._update_win = update_win
+
+ local buf, win = util.open_floating_preview(lines, 'markdown', config)
+
+ if hl then
+ vim.api.nvim_buf_clear_namespace(buf, sig_help_ns, 0, -1)
+ vim.hl.range(
+ buf,
+ sig_help_ns,
+ 'LspSignatureActiveParameter',
+ { hl[1], hl[2] },
+ { hl[3], hl[4] }
+ )
+ end
+ return buf, win
+ end
+
+ local fbuf, fwin = show_signature()
+
+ if total > 1 then
+ vim.keymap.set('n', '<C-s>', function()
+ show_signature(fwin)
+ end, {
+ buffer = fbuf,
+ desc = 'Cycle next signature',
+ })
+ end
+ end)
end
+--- @deprecated
--- Retrieves the completion items at the current cursor position. Can only be
--- called in Insert mode.
---
@@ -131,9 +423,14 @@ end
---
---@see vim.lsp.protocol.CompletionTriggerKind
function M.completion(context)
- local params = util.make_position_params()
- params.context = context
- return request(ms.textDocument_completion, params)
+ vim.depends('vim.lsp.buf.completion', 'vim.lsp.commpletion.trigger', '0.12')
+ return lsp.buf_request(
+ 0,
+ ms.textDocument_completion,
+ client_positional_params({
+ context = context,
+ })
+ )
end
---@param bufnr integer
@@ -240,7 +537,7 @@ function M.format(opts)
method = ms.textDocument_formatting
end
- local clients = vim.lsp.get_clients({
+ local clients = lsp.get_clients({
id = opts.id,
bufnr = bufnr,
name = opts.name,
@@ -277,7 +574,7 @@ function M.format(opts)
end
local params = set_range(client, util.make_formatting_params(opts.formatting_options))
client.request(method, params, function(...)
- local handler = client.handlers[method] or vim.lsp.handlers[method]
+ local handler = client.handlers[method] or lsp.handlers[method]
handler(...)
do_format(next(clients, idx))
end, bufnr)
@@ -319,7 +616,7 @@ end
function M.rename(new_name, opts)
opts = opts or {}
local bufnr = opts.bufnr or api.nvim_get_current_buf()
- local clients = vim.lsp.get_clients({
+ local clients = lsp.get_clients({
bufnr = bufnr,
name = opts.name,
-- Clients must at least support rename, prepareRename is optional
@@ -338,6 +635,8 @@ function M.rename(new_name, opts)
-- Compute early to account for cursor movements after going async
local cword = vim.fn.expand('<cword>')
+ --- @param range lsp.Range
+ --- @param offset_encoding string
local function get_text_at_range(range, offset_encoding)
return api.nvim_buf_get_text(
bufnr,
@@ -359,7 +658,7 @@ function M.rename(new_name, opts)
local params = util.make_position_params(win, client.offset_encoding)
params.newName = name
local handler = client.handlers[ms.textDocument_rename]
- or vim.lsp.handlers[ms.textDocument_rename]
+ or lsp.handlers[ms.textDocument_rename]
client.request(ms.textDocument_rename, params, function(...)
handler(...)
try_use_client(next(clients, idx))
@@ -437,12 +736,60 @@ end
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
---@param opts? vim.lsp.ListOpts
function M.references(context, opts)
- validate({ context = { context, 't', true } })
- local params = util.make_position_params()
- params.context = context or {
- includeDeclaration = true,
- }
- request_with_opts(ms.textDocument_references, params, opts)
+ validate('context', context, 'table', true)
+ local bufnr = api.nvim_get_current_buf()
+ local clients = lsp.get_clients({ method = ms.textDocument_references, bufnr = bufnr })
+ if not next(clients) then
+ return
+ end
+ local win = api.nvim_get_current_win()
+ opts = opts or {}
+
+ local all_items = {}
+ local title = 'References'
+
+ local function on_done()
+ if not next(all_items) then
+ vim.notify('No references found')
+ else
+ local list = {
+ title = title,
+ items = all_items,
+ context = {
+ method = ms.textDocument_references,
+ bufnr = bufnr,
+ },
+ }
+ if opts.loclist then
+ vim.fn.setloclist(0, {}, ' ', list)
+ vim.cmd.lopen()
+ elseif opts.on_list then
+ assert(vim.is_callable(opts.on_list), 'on_list is not a function')
+ opts.on_list(list)
+ else
+ vim.fn.setqflist({}, ' ', list)
+ vim.cmd('botright copen')
+ end
+ end
+ end
+
+ local remaining = #clients
+ for _, client in ipairs(clients) do
+ local params = util.make_position_params(win, client.offset_encoding)
+
+ ---@diagnostic disable-next-line: inject-field
+ params.context = context or {
+ includeDeclaration = true,
+ }
+ client.request(ms.textDocument_references, params, function(_, result)
+ local items = util.locations_to_items(result or {}, client.offset_encoding)
+ vim.list_extend(all_items, items)
+ remaining = remaining - 1
+ if remaining == 0 then
+ on_done()
+ end
+ end)
+ end
end
--- Lists all symbols in the current buffer in the quickfix window.
@@ -452,65 +799,116 @@ function M.document_symbol(opts)
request_with_opts(ms.textDocument_documentSymbol, params, opts)
end
---- @param call_hierarchy_items lsp.CallHierarchyItem[]
---- @return lsp.CallHierarchyItem?
-local function pick_call_hierarchy_item(call_hierarchy_items)
- if #call_hierarchy_items == 1 then
- return call_hierarchy_items[1]
- end
- local items = {}
- for i, item in pairs(call_hierarchy_items) do
- local entry = item.detail or item.name
- table.insert(items, string.format('%d. %s', i, entry))
- end
- local choice = vim.fn.inputlist(items)
- if choice < 1 or choice > #items then
+--- @param client_id integer
+--- @param method string
+--- @param params table
+--- @param handler? lsp.Handler
+--- @param bufnr? integer
+local function request_with_id(client_id, method, params, handler, bufnr)
+ local client = lsp.get_client_by_id(client_id)
+ if not client then
+ vim.notify(
+ string.format('Client with id=%d disappeared during hierarchy request', client_id),
+ vim.log.levels.WARN
+ )
return
end
- return call_hierarchy_items[choice]
+ client.request(method, params, handler, bufnr)
+end
+
+--- @param item lsp.TypeHierarchyItem|lsp.CallHierarchyItem
+local function format_hierarchy_item(item)
+ if not item.detail or #item.detail == 0 then
+ return item.name
+ end
+ return string.format('%s %s', item.name, item.detail)
end
+local hierarchy_methods = {
+ [ms.typeHierarchy_subtypes] = 'type',
+ [ms.typeHierarchy_supertypes] = 'type',
+ [ms.callHierarchy_incomingCalls] = 'call',
+ [ms.callHierarchy_outgoingCalls] = 'call',
+}
+
--- @param method string
-local function call_hierarchy(method)
- local params = util.make_position_params()
- --- @param result lsp.CallHierarchyItem[]?
- request(ms.textDocument_prepareCallHierarchy, params, function(err, result, ctx)
- if err then
- vim.notify(err.message, vim.log.levels.WARN)
- return
- end
- if not result or vim.tbl_isempty(result) then
+local function hierarchy(method)
+ local kind = hierarchy_methods[method]
+ if not kind then
+ error('unsupported method ' .. method)
+ end
+
+ local prepare_method = kind == 'type' and ms.textDocument_prepareTypeHierarchy
+ or ms.textDocument_prepareCallHierarchy
+
+ local bufnr = api.nvim_get_current_buf()
+ local clients = lsp.get_clients({ bufnr = bufnr, method = prepare_method })
+ if not next(clients) then
+ vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN)
+ return
+ end
+
+ local win = api.nvim_get_current_win()
+
+ --- @param results [integer, lsp.TypeHierarchyItem|lsp.CallHierarchyItem][]
+ local function on_response(results)
+ if #results == 0 then
vim.notify('No item resolved', vim.log.levels.WARN)
- return
- end
- local call_hierarchy_item = pick_call_hierarchy_item(result)
- if not call_hierarchy_item then
- return
- e