neovim/runtime/lua/vim/lsp/buf.lua
2023-09-14 08:23:01 -05:00

782 lines
27 KiB
Lua

local api = vim.api
local validate = vim.validate
local util = require('vim.lsp.util')
local npcall = vim.F.npcall
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 (function|nil) 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)
end
--- Checks whether the language servers attached to the current buffer are
--- ready.
---
---@return boolean if server responds.
---@deprecated
function M.server_ready()
vim.deprecate('vim.lsp.buf.server_ready', nil, '0.10.0')
return not not vim.lsp.buf_notify(0, 'window/progress', {})
end
--- Displays hover information about the symbol under the cursor in a floating
--- window. Calling the function twice will jump into the floating window.
function M.hover()
local params = util.make_position_params()
request(ms.textDocument_hover, params)
end
local function request_with_options(name, params, options)
local req_handler
if options then
req_handler = function(err, result, ctx, config)
local client = vim.lsp.get_client_by_id(ctx.client_id)
local handler = client.handlers[name] or vim.lsp.handlers[name]
handler(err, result, ctx, vim.tbl_extend('force', config or {}, options))
end
end
request(name, params, req_handler)
end
--- Jumps to the declaration of the symbol under the cursor.
---@note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead.
---
---@param options table|nil additional options
--- - reuse_win: (boolean) Jump to existing window if buffer is already open.
--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.declaration(options)
local params = util.make_position_params()
request_with_options(ms.textDocument_declaration, params, options)
end
--- Jumps to the definition of the symbol under the cursor.
---
---@param options table|nil additional options
--- - reuse_win: (boolean) Jump to existing window if buffer is already open.
--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.definition(options)
local params = util.make_position_params()
request_with_options(ms.textDocument_definition, params, options)
end
--- Jumps to the definition of the type of the symbol under the cursor.
---
---@param options table|nil additional options
--- - reuse_win: (boolean) Jump to existing window if buffer is already open.
--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.type_definition(options)
local params = util.make_position_params()
request_with_options(ms.textDocument_typeDefinition, params, options)
end
--- Lists all the implementations for the symbol under the cursor in the
--- quickfix window.
---
---@param options table|nil additional options
--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.implementation(options)
local params = util.make_position_params()
request_with_options(ms.textDocument_implementation, params, options)
end
--- 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)
end
--- Retrieves the completion items at the current cursor position. Can only be
--- called in Insert mode.
---
---@param context table (context support not yet implemented) Additional information
--- about the context in which a completion was triggered (how it was triggered,
--- and by which trigger character, if applicable)
---
---@see vim.lsp.protocol.CompletionTriggerKind
function M.completion(context)
local params = util.make_position_params()
params.context = context
return request(ms.textDocument_completion, params)
end
---@param bufnr integer
---@param mode "v"|"V"
---@return table {start={row,col}, end={row,col}} using (1, 0) indexing
local function range_from_selection(bufnr, mode)
-- TODO: Use `vim.region()` instead https://github.com/neovim/neovim/pull/13896
-- [bufnum, lnum, col, off]; both row and column 1-indexed
local start = vim.fn.getpos('v')
local end_ = vim.fn.getpos('.')
local start_row = start[2]
local start_col = start[3]
local end_row = end_[2]
local end_col = end_[3]
-- A user can start visual selection at the end and move backwards
-- Normalize the range to start < end
if start_row == end_row and end_col < start_col then
end_col, start_col = start_col, end_col
elseif end_row < start_row then
start_row, end_row = end_row, start_row
start_col, end_col = end_col, start_col
end
if mode == 'V' then
start_col = 1
local lines = api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, true)
end_col = #lines[1]
end
return {
['start'] = { start_row, start_col - 1 },
['end'] = { end_row, end_col - 1 },
}
end
--- Formats a buffer using the attached (and optionally filtered) language
--- server clients.
---
--- @param options table|nil Optional table which holds the following optional fields:
--- - formatting_options (table|nil):
--- Can be used to specify FormattingOptions. Some unspecified options will be
--- automatically derived from the current Nvim options.
--- See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#formattingOptions
--- - timeout_ms (integer|nil, default 1000):
--- Time in milliseconds to block for formatting requests. No effect if async=true
--- - bufnr (number|nil):
--- Restrict formatting to the clients attached to the given buffer, defaults to the current
--- buffer (0).
---
--- - filter (function|nil):
--- Predicate used to filter clients. Receives a client as argument and must return a
--- boolean. Clients matching the predicate are included. Example: <pre>lua
--- -- Never request typescript-language-server for formatting
--- vim.lsp.buf.format {
--- filter = function(client) return client.name ~= "tsserver" end
--- }
--- </pre>
---
--- - async boolean|nil
--- If true the method won't block. Defaults to false.
--- Editing the buffer while formatting asynchronous can lead to unexpected
--- changes.
---
--- - id (number|nil):
--- Restrict formatting to the client with ID (client.id) matching this field.
--- - name (string|nil):
--- Restrict formatting to the client with name (client.name) matching this field.
---
--- - range (table|nil) Range to format.
--- Table must contain `start` and `end` keys with {row,col} tuples using
--- (1,0) indexing.
--- Defaults to current selection in visual mode
--- Defaults to `nil` in other modes, formatting the full buffer
function M.format(options)
options = options or {}
local bufnr = options.bufnr or api.nvim_get_current_buf()
local mode = api.nvim_get_mode().mode
local range = options.range
if not range and mode == 'v' or mode == 'V' then
range = range_from_selection(bufnr, mode)
end
local method = range and ms.textDocument_rangeFormatting or ms.textDocument_formatting
local clients = vim.lsp.get_clients({
id = options.id,
bufnr = bufnr,
name = options.name,
method = method,
})
if options.filter then
clients = vim.tbl_filter(options.filter, clients)
end
if #clients == 0 then
vim.notify('[LSP] Format request failed, no matching language servers.')
end
local function set_range(client, params)
if range then
local range_params =
util.make_given_range_params(range.start, range['end'], bufnr, client.offset_encoding)
params.range = range_params.range
end
return params
end
if options.async then
local do_format
do_format = function(idx, client)
if not client then
return
end
local params = set_range(client, util.make_formatting_params(options.formatting_options))
client.request(method, params, function(...)
local handler = client.handlers[method] or vim.lsp.handlers[method]
handler(...)
do_format(next(clients, idx))
end, bufnr)
end
do_format(next(clients))
else
local timeout_ms = options.timeout_ms or 1000
for _, client in pairs(clients) do
local params = set_range(client, util.make_formatting_params(options.formatting_options))
local result, err = client.request_sync(method, params, timeout_ms, bufnr)
if result and result.result then
util.apply_text_edits(result.result, bufnr, client.offset_encoding)
elseif err then
vim.notify(string.format('[LSP][%s] %s', client.name, err), vim.log.levels.WARN)
end
end
end
end
--- Renames all references to the symbol under the cursor.
---
---@param new_name string|nil If not provided, the user will be prompted for a new
--- name using |vim.ui.input()|.
---@param options table|nil additional options
--- - filter (function|nil):
--- Predicate used to filter clients. Receives a client as argument and
--- must return a boolean. Clients matching the predicate are included.
--- - name (string|nil):
--- Restrict clients used for rename to ones where client.name matches
--- this field.
function M.rename(new_name, options)
options = options or {}
local bufnr = options.bufnr or api.nvim_get_current_buf()
local clients = vim.lsp.get_clients({
bufnr = bufnr,
name = options.name,
-- Clients must at least support rename, prepareRename is optional
method = ms.textDocument_rename,
})
if options.filter then
clients = vim.tbl_filter(options.filter, clients)
end
if #clients == 0 then
vim.notify('[LSP] Rename, no matching language servers with rename capability.')
end
local win = api.nvim_get_current_win()
-- Compute early to account for cursor movements after going async
local cword = vim.fn.expand('<cword>')
local function get_text_at_range(range, offset_encoding)
return api.nvim_buf_get_text(
bufnr,
range.start.line,
util._get_line_byte_from_position(bufnr, range.start, offset_encoding),
range['end'].line,
util._get_line_byte_from_position(bufnr, range['end'], offset_encoding),
{}
)[1]
end
local try_use_client
try_use_client = function(idx, client)
if not client then
return
end
local function rename(name)
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]
client.request(ms.textDocument_rename, params, function(...)
handler(...)
try_use_client(next(clients, idx))
end, bufnr)
end
if client.supports_method(ms.textDocument_prepareRename) then
local params = util.make_position_params(win, client.offset_encoding)
client.request(ms.textDocument_prepareRename, params, function(err, result)
if err or result == nil then
if next(clients, idx) then
try_use_client(next(clients, idx))
else
local msg = err and ('Error on prepareRename: ' .. (err.message or ''))
or 'Nothing to rename'
vim.notify(msg, vim.log.levels.INFO)
end
return
end
if new_name then
rename(new_name)
return
end
local prompt_opts = {
prompt = 'New Name: ',
}
-- result: Range | { range: Range, placeholder: string }
if result.placeholder then
prompt_opts.default = result.placeholder
elseif result.start then
prompt_opts.default = get_text_at_range(result, client.offset_encoding)
elseif result.range then
prompt_opts.default = get_text_at_range(result.range, client.offset_encoding)
else
prompt_opts.default = cword
end
vim.ui.input(prompt_opts, function(input)
if not input or #input == 0 then
return
end
rename(input)
end)
end, bufnr)
else
assert(
client.supports_method(ms.textDocument_rename),
'Client must support textDocument/rename'
)
if new_name then
rename(new_name)
return
end
local prompt_opts = {
prompt = 'New Name: ',
default = cword,
}
vim.ui.input(prompt_opts, function(input)
if not input or #input == 0 then
return
end
rename(input)
end)
end
end
try_use_client(next(clients))
end
--- Lists all the references to the symbol under the cursor in the quickfix window.
---
---@param context (table|nil) Context for the request
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
---@param options table|nil additional options
--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.references(context, options)
validate({ context = { context, 't', true } })
local params = util.make_position_params()
params.context = context or {
includeDeclaration = true,
}
request_with_options(ms.textDocument_references, params, options)
end
--- Lists all symbols in the current buffer in the quickfix window.
---
---@param options table|nil additional options
--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.document_symbol(options)
local params = { textDocument = util.make_text_document_params() }
request_with_options(ms.textDocument_documentSymbol, params, options)
end
local function pick_call_hierarchy_item(call_hierarchy_items)
if not call_hierarchy_items then
return
end
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
return
end
return choice
end
local function call_hierarchy(method)
local params = util.make_position_params()
request(ms.textDocument_prepareCallHierarchy, params, function(err, result, ctx)
if err then
vim.notify(err.message, vim.log.levels.WARN)
return
end
local call_hierarchy_item = pick_call_hierarchy_item(result)
local client = vim.lsp.get_client_by_id(ctx.client_id)
if client then
client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr)
else
vim.notify(
string.format('Client with id=%d disappeared during call hierarchy request', ctx.client_id),
vim.log.levels.WARN
)
end
end)
end
--- Lists all the call sites of the symbol under the cursor in the
--- |quickfix| window. If the symbol can resolve to multiple
--- items, the user can pick one in the |inputlist()|.
function M.incoming_calls()
call_hierarchy(ms.callHierarchy_incomingCalls)
end
--- Lists all the items that are called by the symbol under the
--- cursor in the |quickfix| window. If the symbol can resolve to
--- multiple items, the user can pick one in the |inputlist()|.
function M.outgoing_calls()
call_hierarchy(ms.callHierarchy_outgoingCalls)
end
--- List workspace folders.
---
function M.list_workspace_folders()
local workspace_folders = {}
for _, client in pairs(vim.lsp.get_clients({ bufnr = 0 })) do
for _, folder in pairs(client.workspace_folders or {}) do
table.insert(workspace_folders, folder.name)
end
end
return workspace_folders
end
--- Add the folder at path to the workspace folders. If {path} is
--- not provided, the user will be prompted for a path using |input()|.
function M.add_workspace_folder(workspace_folder)
workspace_folder = workspace_folder
or npcall(vim.fn.input, 'Workspace Folder: ', vim.fn.expand('%:p:h'), 'dir')
api.nvim_command('redraw')
if not (workspace_folder and #workspace_folder > 0) then
return
end
if vim.fn.isdirectory(workspace_folder) == 0 then
print(workspace_folder, ' is not a valid directory')
return
end
local new_workspace = {
uri = vim.uri_from_fname(workspace_folder),
name = workspace_folder,
}
local params = { event = { added = { new_workspace }, removed = {} } }
local bufnr = vim.api.nvim_get_current_buf()
for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do
local found = false
for _, folder in pairs(client.workspace_folders or {}) do
if folder.name == workspace_folder then
found = true
print(workspace_folder, 'is already part of this workspace')
break
end
end
if not found then
client.notify(ms.workspace_didChangeWorkspaceFolders, params)
if not client.workspace_folders then
client.workspace_folders = {}
end
table.insert(client.workspace_folders, new_workspace)
end
end
end
--- Remove the folder at path from the workspace folders. If
--- {path} is not provided, the user will be prompted for
--- a path using |input()|.
function M.remove_workspace_folder(workspace_folder)
workspace_folder = workspace_folder
or npcall(vim.fn.input, 'Workspace Folder: ', vim.fn.expand('%:p:h'))
api.nvim_command('redraw')
if not (workspace_folder and #workspace_folder > 0) then
return
end
local workspace = {
uri = vim.uri_from_fname(workspace_folder),
name = workspace_folder,
}
local params = { event = { added = {}, removed = { workspace } } }
local bufnr = vim.api.nvim_get_current_buf()
for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do
for idx, folder in pairs(client.workspace_folders) do
if folder.name == workspace_folder then
client.notify(ms.workspace_didChangeWorkspaceFolders, params)
client.workspace_folders[idx] = nil
return
end
end
end
print(workspace_folder, 'is not currently part of the workspace')
end
--- Lists all symbols in the current workspace in the quickfix window.
---
--- The list is filtered against {query}; if the argument is omitted from the
--- call, the user is prompted to enter a string on the command line. An empty
--- string means no filtering is done.
---
---@param query string|nil optional
---@param options table|nil additional options
--- - on_list: (function) handler for list results. See |lsp-on-list-handler|
function M.workspace_symbol(query, options)
query = query or npcall(vim.fn.input, 'Query: ')
if query == nil then
return
end
local params = { query = query }
request_with_options(ms.workspace_symbol, params, options)
end
--- Send request to the server to resolve document highlights for the current
--- text document position. This request can be triggered by a key mapping or
--- by events such as `CursorHold`, e.g.:
---
--- ```vim
--- autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()
--- autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()
--- autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()
--- ```
---
--- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups
--- to be defined or you won't be able to see the actual highlights.
--- |hl-LspReferenceText|
--- |hl-LspReferenceRead|
--- |hl-LspReferenceWrite|
function M.document_highlight()
local params = util.make_position_params()
request(ms.textDocument_documentHighlight, params)
end
--- Removes document highlights from current buffer.
function M.clear_references()
util.buf_clear_references()
end
--- This is not public because the main extension point is
--- vim.ui.select which can be overridden independently.
---
--- Can't call/use vim.lsp.handlers['textDocument/codeAction'] because it expects
--- `(err, CodeAction[] | Command[], ctx)`, but we want to aggregate the results
--- from multiple clients to have 1 single UI prompt for the user, yet we still
--- need to be able to link a `CodeAction|Command` to the right client for
--- `codeAction/resolve`
local function on_code_action_results(results, ctx, options)
local action_tuples = {}
local function action_filter(a)
-- filter by specified action kind
if options and options.context and options.context.only then
if not a.kind then
return false
end
local found = false
for _, o in ipairs(options.context.only) do
-- action kinds are hierarchical with . as a separator: when requesting only 'type-annotate'
-- this filter allows both 'type-annotate' and 'type-annotate.foo', for example
if a.kind == o or vim.startswith(a.kind, o .. '.') then
found = true
break
end
end
if not found then
return false
end
end
-- filter by user function
if options and options.filter and not options.filter(a) then
return false
end
-- no filter removed this action
return true
end
for client_id, result in pairs(results) do
for _, action in pairs(result.result or {}) do
if action_filter(action) then
table.insert(action_tuples, { client_id, action })
end
end
end
if #action_tuples == 0 then
vim.notify('No code actions available', vim.log.levels.INFO)
return
end
local function apply_action(action, client)
if action.edit then
util.apply_workspace_edit(action.edit, client.offset_encoding)
end
if action.command then
local command = type(action.command) == 'table' and action.command or action
client._exec_cmd(command, ctx)
end
end
local function on_user_choice(action_tuple)
if not action_tuple then
return
end
-- textDocument/codeAction can return either Command[] or CodeAction[]
--
-- CodeAction
-- ...
-- edit?: WorkspaceEdit -- <- must be applied before command
-- command?: Command
--
-- Command:
-- title: string
-- command: string
-- arguments?: any[]
--
---@type lsp.Client
local client = vim.lsp.get_client_by_id(action_tuple[1])
local action = action_tuple[2]
local reg = client.dynamic_capabilities:get(ms.textDocument_codeAction, { bufnr = ctx.bufnr })
local supports_resolve = vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider')
or client.supports_method(ms.codeAction_resolve)
if not action.edit and client and supports_resolve then
client.request(ms.codeAction_resolve, action, function(err, resolved_action)
if err then
vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
return
end
apply_action(resolved_action, client)
end, ctx.bufnr)
else
apply_action(action, client)
end
end
-- If options.apply is given, and there are just one remaining code action,
-- apply it directly without querying the user.
if options and options.apply and #action_tuples == 1 then
on_user_choice(action_tuples[1])
return
end
vim.ui.select(action_tuples, {
prompt = 'Code actions:',
kind = 'codeaction',
format_item = function(action_tuple)
local title = action_tuple[2].title:gsub('\r\n', '\\r\\n')
return title:gsub('\n', '\\n')
end,
}, on_user_choice)
end
--- Requests code actions from all clients and calls the handler exactly once
--- with all aggregated results
local function code_action_request(params, options)
local bufnr = api.nvim_get_current_buf()
vim.lsp.buf_request_all(bufnr, ms.textDocument_codeAction, params, function(results)
local ctx = { bufnr = bufnr, method = ms.textDocument_codeAction, params = params }
on_code_action_results(results, ctx, options)
end)
end
--- Selects a code action available at the current
--- cursor position.
---
---@param options table|nil Optional table which holds the following optional fields:
--- - context: (table|nil)
--- Corresponds to `CodeActionContext` of the LSP specification:
--- - diagnostics (table|nil):
--- LSP `Diagnostic[]`. Inferred from the current
--- position if not provided.
--- - only (table|nil):
--- List of LSP `CodeActionKind`s used to filter the code actions.
--- Most language servers support values like `refactor`
--- or `quickfix`.
--- - triggerKind (number|nil): The reason why code actions were requested.
--- - filter: (function|nil)
--- Predicate taking an `CodeAction` and returning a boolean.
--- - apply: (boolean|nil)
--- When set to `true`, and there is just one remaining action
--- (after filtering), the action is applied without user query.
---
--- - range: (table|nil)
--- Range for which code actions should be requested.
--- If in visual mode this defaults to the active selection.
--- Table must contain `start` and `end` keys with {row,col} tuples
--- using mark-like indexing. See |api-indexing|
---
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
---@see vim.lsp.protocol.CodeActionTriggerKind
function M.code_action(options)
validate({ options = { options, 't', true } })
options = options or {}
-- Detect old API call code_action(context) which should now be
-- code_action({ context = context} )
if options.diagnostics or options.only then
options = { options = options }
end
local context = options.context or {}
if not context.triggerKind then
context.triggerKind = vim.lsp.protocol.CodeActionTriggerKind.Invoked
end
if not context.diagnostics then
local bufnr = api.nvim_get_current_buf()
context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics(bufnr)
end
local params
local mode = api.nvim_get_mode().mode
if options.range then
assert(type(options.range) == 'table', 'code_action range must be a table')
local start = assert(options.range.start, 'range must have a `start` property')
local end_ = assert(options.range['end'], 'range must have a `end` property')
params = util.make_given_range_params(start, end_)
elseif mode == 'v' or mode == 'V' then
local range = range_from_selection(0, mode)
params = util.make_given_range_params(range.start, range['end'])
else
params = util.make_range_params()
end
params.context = context
code_action_request(params, options)
end
--- Executes an LSP server command.
---
---@param command_params table A valid `ExecuteCommandParams` object
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
function M.execute_command(command_params)
validate({
command = { command_params.command, 's' },
arguments = { command_params.arguments, 't', true },
})
command_params = {
command = command_params.command,
arguments = command_params.arguments,
workDoneToken = command_params.workDoneToken,
}
request(ms.workspace_executeCommand, command_params)
end
return M