aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp.lua
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2023-11-30 20:35:25 +0000
committerJosh Rahm <joshuarahm@gmail.com>2023-11-30 20:35:25 +0000
commit1b7b916b7631ddf73c38e3a0070d64e4636cb2f3 (patch)
treecd08258054db80bb9a11b1061bb091c70b76926a /runtime/lua/vim/lsp.lua
parenteaa89c11d0f8aefbb512de769c6c82f61a8baca3 (diff)
parent4a8bf24ac690004aedf5540fa440e788459e5e34 (diff)
downloadrneovim-aucmd_textputpost.tar.gz
rneovim-aucmd_textputpost.tar.bz2
rneovim-aucmd_textputpost.zip
Merge remote-tracking branch 'upstream/master' into aucmd_textputpostaucmd_textputpost
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r--runtime/lua/vim/lsp.lua1063
1 files changed, 597 insertions, 466 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index c5392ac154..261a3aa5de 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -1,19 +1,17 @@
+---@diagnostic disable: invisible
local default_handlers = require('vim.lsp.handlers')
local log = require('vim.lsp.log')
local lsp_rpc = require('vim.lsp.rpc')
local protocol = require('vim.lsp.protocol')
+local ms = protocol.Methods
local util = require('vim.lsp.util')
local sync = require('vim.lsp.sync')
local semantic_tokens = require('vim.lsp.semantic_tokens')
local api = vim.api
-local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option, nvim_exec_autocmds =
- api.nvim_err_writeln,
- api.nvim_buf_get_lines,
- api.nvim_command,
- api.nvim_buf_get_option,
- api.nvim_exec_autocmds
-local uv = vim.loop
+local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_exec_autocmds =
+ api.nvim_err_writeln, api.nvim_buf_get_lines, api.nvim_command, api.nvim_exec_autocmds
+local uv = vim.uv
local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend
local validate = vim.validate
local if_nil = vim.F.if_nil
@@ -26,6 +24,7 @@ local lsp = {
buf = require('vim.lsp.buf'),
diagnostic = require('vim.lsp.diagnostic'),
codelens = require('vim.lsp.codelens'),
+ inlay_hint = require('vim.lsp.inlay_hint'),
semantic_tokens = semantic_tokens,
util = util,
@@ -38,47 +37,50 @@ local lsp = {
-- maps request name to the required server_capability in the client.
lsp._request_name_to_capability = {
- ['textDocument/hover'] = { 'hoverProvider' },
- ['textDocument/signatureHelp'] = { 'signatureHelpProvider' },
- ['textDocument/definition'] = { 'definitionProvider' },
- ['textDocument/implementation'] = { 'implementationProvider' },
- ['textDocument/declaration'] = { 'declarationProvider' },
- ['textDocument/typeDefinition'] = { 'typeDefinitionProvider' },
- ['textDocument/documentSymbol'] = { 'documentSymbolProvider' },
- ['textDocument/prepareCallHierarchy'] = { 'callHierarchyProvider' },
- ['textDocument/rename'] = { 'renameProvider' },
- ['textDocument/prepareRename'] = { 'renameProvider', 'prepareProvider' },
- ['textDocument/codeAction'] = { 'codeActionProvider' },
- ['textDocument/codeLens'] = { 'codeLensProvider' },
- ['codeLens/resolve'] = { 'codeLensProvider', 'resolveProvider' },
- ['workspace/executeCommand'] = { 'executeCommandProvider' },
- ['workspace/symbol'] = { 'workspaceSymbolProvider' },
- ['textDocument/references'] = { 'referencesProvider' },
- ['textDocument/rangeFormatting'] = { 'documentRangeFormattingProvider' },
- ['textDocument/formatting'] = { 'documentFormattingProvider' },
- ['textDocument/completion'] = { 'completionProvider' },
- ['textDocument/documentHighlight'] = { 'documentHighlightProvider' },
- ['textDocument/semanticTokens/full'] = { 'semanticTokensProvider' },
- ['textDocument/semanticTokens/full/delta'] = { 'semanticTokensProvider' },
+ [ms.textDocument_hover] = { 'hoverProvider' },
+ [ms.textDocument_signatureHelp] = { 'signatureHelpProvider' },
+ [ms.textDocument_definition] = { 'definitionProvider' },
+ [ms.textDocument_implementation] = { 'implementationProvider' },
+ [ms.textDocument_declaration] = { 'declarationProvider' },
+ [ms.textDocument_typeDefinition] = { 'typeDefinitionProvider' },
+ [ms.textDocument_documentSymbol] = { 'documentSymbolProvider' },
+ [ms.textDocument_prepareCallHierarchy] = { 'callHierarchyProvider' },
+ [ms.callHierarchy_incomingCalls] = { 'callHierarchyProvider' },
+ [ms.callHierarchy_outgoingCalls] = { 'callHierarchyProvider' },
+ [ms.textDocument_rename] = { 'renameProvider' },
+ [ms.textDocument_prepareRename] = { 'renameProvider', 'prepareProvider' },
+ [ms.textDocument_codeAction] = { 'codeActionProvider' },
+ [ms.textDocument_codeLens] = { 'codeLensProvider' },
+ [ms.codeLens_resolve] = { 'codeLensProvider', 'resolveProvider' },
+ [ms.codeAction_resolve] = { 'codeActionProvider', 'resolveProvider' },
+ [ms.workspace_executeCommand] = { 'executeCommandProvider' },
+ [ms.workspace_symbol] = { 'workspaceSymbolProvider' },
+ [ms.textDocument_references] = { 'referencesProvider' },
+ [ms.textDocument_rangeFormatting] = { 'documentRangeFormattingProvider' },
+ [ms.textDocument_formatting] = { 'documentFormattingProvider' },
+ [ms.textDocument_completion] = { 'completionProvider' },
+ [ms.textDocument_documentHighlight] = { 'documentHighlightProvider' },
+ [ms.textDocument_semanticTokens_full] = { 'semanticTokensProvider' },
+ [ms.textDocument_semanticTokens_full_delta] = { 'semanticTokensProvider' },
+ [ms.textDocument_inlayHint] = { 'inlayHintProvider' },
+ [ms.textDocument_diagnostic] = { 'diagnosticProvider' },
+ [ms.inlayHint_resolve] = { 'inlayHintProvider', 'resolveProvider' },
}
-- TODO improve handling of scratch buffers with LSP attached.
----@private
--- Concatenates and writes a list of strings to the Vim error buffer.
---
----@param {...} table[] List to write to the buffer
+---@param ... string List to write to the buffer
local function err_message(...)
nvim_err_writeln(table.concat(vim.tbl_flatten({ ... })))
nvim_command('redraw')
end
----@private
--- Returns the buffer number for the given {bufnr}.
---
----@param bufnr (number|nil) Buffer number to resolve. Defaults to the current
----buffer if not given.
----@returns bufnr (number) Number of requested buffer
+---@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
+---@return integer bufnr
local function resolve_bufnr(bufnr)
validate({ bufnr = { bufnr, 'n', true } })
if bufnr == nil or bufnr == 0 then
@@ -100,11 +102,10 @@ function lsp._unsupported_method(method)
return msg
end
----@private
--- Checks whether a given path is a directory.
---
---@param filename (string) path to check
----@returns true if {filename} exists and is a directory, false otherwise
+---@return boolean # true if {filename} exists and is a directory, false otherwise
local function is_dir(filename)
validate({ filename = { filename, 's' } })
local stat = uv.fs_stat(filename)
@@ -131,28 +132,27 @@ local format_line_ending = {
['mac'] = '\r',
}
----@private
---@param bufnr (number)
----@returns (string)
+---@return string
local function buf_get_line_ending(bufnr)
- return format_line_ending[nvim_buf_get_option(bufnr, 'fileformat')] or '\n'
+ return format_line_ending[vim.bo[bufnr].fileformat] or '\n'
end
local client_index = 0
----@private
--- Returns a new, unused client id.
---
----@returns (number) client id
+---@return integer client_id
local function next_client_id()
client_index = client_index + 1
return client_index
end
-- Tracks all clients created via lsp.start_client
-local active_clients = {}
-local all_buffer_active_clients = {}
-local uninitialized_clients = {}
+local active_clients = {} --- @type table<integer,lsp.Client>
+local all_buffer_active_clients = {} --- @type table<integer,table<integer,true>>
+local uninitialized_clients = {} --- @type table<integer,lsp.Client>
----@private
+---@param bufnr? integer
+---@param fn fun(client: lsp.Client, client_id: integer, bufnr: integer)
local function for_each_buffer_client(bufnr, fn, restrict_client_ids)
validate({
fn = { fn, 'f' },
@@ -165,9 +165,9 @@ local function for_each_buffer_client(bufnr, fn, restrict_client_ids)
end
if restrict_client_ids and #restrict_client_ids > 0 then
- local filtered_client_ids = {}
+ local filtered_client_ids = {} --- @type table<integer,true>
for client_id in pairs(client_ids) do
- if vim.tbl_contains(restrict_client_ids, client_id) then
+ if vim.list_contains(restrict_client_ids, client_id) then
filtered_client_ids[client_id] = true
end
end
@@ -182,22 +182,24 @@ local function for_each_buffer_client(bufnr, fn, restrict_client_ids)
end
end
--- Error codes to be used with `on_error` from |vim.lsp.start_client|.
--- Can be used to look up the string from a the number or the number
--- from the string.
+--- Error codes to be used with `on_error` from |vim.lsp.start_client|.
+--- Can be used to look up the string from a the number or the number
+--- from the string.
+--- @nodoc
lsp.client_errors = tbl_extend(
'error',
lsp_rpc.client_errors,
vim.tbl_add_reverse_lookup({
- ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1,
+ BEFORE_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1,
+ ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 2,
+ ON_ATTACH_ERROR = table.maxn(lsp_rpc.client_errors) + 3,
})
)
----@private
--- Normalizes {encoding} to valid LSP encoding names.
---
---@param encoding (string) Encoding to normalize
----@returns (string) normalized encoding name
+---@return string # normalized encoding name
local function validate_encoding(encoding)
validate({
encoding = { encoding, 's' },
@@ -215,9 +217,8 @@ end
--- Parses a command invocation into the command itself and its args. If there
--- are no arguments, an empty table is returned as the second argument.
---
----@param input (List)
----@returns (string) the command
----@returns (list of strings) its arguments
+---@param input string[]
+---@return string command, string[] args #the command and arguments
function lsp._cmd_parts(input)
validate({
cmd = {
@@ -241,12 +242,11 @@ function lsp._cmd_parts(input)
return cmd, cmd_args
end
----@private
--- Augments a validator function with support for optional (nil) values.
---
----@param fn (fun(v)) The original validator function; should return a
+---@param fn (fun(v): boolean) The original validator function; should return a
---bool.
----@returns (fun(v)) The augmented function. Also returns true if {v} is
+---@return fun(v): boolean # The augmented function. Also returns true if {v} is
---`nil`.
local function optional_validator(fn)
return function(v)
@@ -254,14 +254,12 @@ local function optional_validator(fn)
end
end
----@private
--- Validates a client configuration as given to |vim.lsp.start_client()|.
---
----@param config (table)
----@returns (table) "Cleaned" config, containing only the command, its
----arguments, and a valid encoding.
----
----@see |vim.lsp.start_client()|
+---@param config (lsp.ClientConfig)
+---@return (string|fun(dispatchers:table):table) Command
+---@return string[] Arguments
+---@return string Encoding.
local function validate_client_config(config)
validate({
config = { config, 't' },
@@ -292,48 +290,44 @@ local function validate_client_config(config)
'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
)
- local cmd, cmd_args
- if type(config.cmd) == 'function' then
- cmd = config.cmd
+ local cmd, cmd_args --- @type (string|fun(dispatchers:table):table), string[]
+ local config_cmd = config.cmd
+ if type(config_cmd) == 'function' then
+ cmd = config_cmd
else
- cmd, cmd_args = lsp._cmd_parts(config.cmd)
+ cmd, cmd_args = lsp._cmd_parts(config_cmd)
end
local offset_encoding = valid_encodings.UTF16
if config.offset_encoding then
offset_encoding = validate_encoding(config.offset_encoding)
end
- return {
- cmd = cmd,
- cmd_args = cmd_args,
- offset_encoding = offset_encoding,
- }
+ return cmd, cmd_args, offset_encoding
end
----@private
--- Returns full text of buffer {bufnr} as a string.
---
---@param bufnr (number) Buffer handle, or 0 for current.
----@returns Buffer text as string.
+---@return string # Buffer text as string.
local function buf_get_full_text(bufnr)
local line_ending = buf_get_line_ending(bufnr)
local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), line_ending)
- if nvim_buf_get_option(bufnr, 'eol') then
+ if vim.bo[bufnr].eol then
text = text .. line_ending
end
return text
end
----@private
--- Memoizes a function. On first run, the function return value is saved and
--- immediately returned on subsequent runs. If the function returns a multival,
--- only the first returned value will be memoized and returned. The function will only be run once,
--- even if it has side effects.
---
----@param fn (function) Function to run
----@returns (function) Memoized function
+---@generic T: function
+---@param fn (T) Function to run
+---@return T
local function once(fn)
- local value
+ local value --- @type any
local ran = false
return function(...)
if not ran then
@@ -365,7 +359,7 @@ do
--- smallest debounce interval is used and we don't group clients by different intervals.
---
--- @class CTGroup
- --- @field sync_kind number TextDocumentSyncKind, considers config.flags.allow_incremental_sync
+ --- @field sync_kind integer TextDocumentSyncKind, considers config.flags.allow_incremental_sync
--- @field offset_encoding "utf-8"|"utf-16"|"utf-32"
---
--- @class CTBufferState
@@ -373,17 +367,16 @@ do
--- @field lines string[] snapshot of buffer lines from last didChange
--- @field lines_tmp string[]
--- @field pending_changes table[] List of debounced changes in incremental sync mode
- --- @field timer nil|userdata uv_timer
+ --- @field timer nil|uv.uv_timer_t uv_timer
--- @field last_flush nil|number uv.hrtime of the last flush/didChange-notification
--- @field needs_flush boolean true if buffer updates haven't been sent to clients/servers yet
- --- @field refs number how many clients are using this group
+ --- @field refs integer how many clients are using this group
---
--- @class CTGroupState
- --- @field buffers table<number, CTBufferState>
- --- @field debounce number debounce duration in ms
- --- @field clients table<number, table> clients using this state. {client_id, client}
+ --- @field buffers table<integer, CTBufferState>
+ --- @field debounce integer debounce duration in ms
+ --- @field clients table<integer, table> clients using this state. {client_id, client}
- ---@private
---@param group CTGroup
---@return string
local function group_key(group)
@@ -404,12 +397,10 @@ do
end,
})
- ---@private
---@return CTGroup
local function get_group(client)
local allow_inc_sync = if_nil(client.config.flags.allow_incremental_sync, true)
- local change_capability =
- vim.tbl_get(client.server_capabilities or {}, 'textDocumentSync', 'change')
+ local change_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change')
local sync_kind = change_capability or protocol.TextDocumentSyncKind.None
if not allow_inc_sync and change_capability == protocol.TextDocumentSyncKind.Incremental then
sync_kind = protocol.TextDocumentSyncKind.Full
@@ -420,7 +411,6 @@ do
}
end
- ---@private
---@param state CTBufferState
local function incremental_changes(state, encoding, bufnr, firstline, lastline, new_lastline)
local prev_lines = state.lines
@@ -544,15 +534,13 @@ do
end
end
- ---@private
- --
-- Adjust debounce time by taking time of last didChange notification into
-- consideration. If the last didChange happened more than `debounce` time ago,
-- debounce can be skipped and otherwise maybe reduced.
--
-- This turns the debounce into a kind of client rate limiting
--
- ---@param debounce number
+ ---@param debounce integer
---@param buf_state CTBufferState
---@return number
local function next_debounce(debounce, buf_state)
@@ -568,9 +556,8 @@ do
return math.max(debounce - ms_since_last_flush, 0)
end
- ---@private
- ---@param bufnr number
- ---@param sync_kind number protocol.TextDocumentSyncKind
+ ---@param bufnr integer
+ ---@param sync_kind integer protocol.TextDocumentSyncKind
---@param state CTGroupState
---@param buf_state CTBufferState
local function send_changes(bufnr, sync_kind, state, buf_state)
@@ -599,7 +586,7 @@ do
local uri = vim.uri_from_bufnr(bufnr)
for _, client in pairs(state.clients) do
if not client.is_stopped() and lsp.buf_is_attached(bufnr, client.id) then
- client.notify('textDocument/didChange', {
+ client.notify(ms.textDocument_didChange, {
textDocument = {
uri = uri,
version = util.buf_versions[bufnr],
@@ -612,8 +599,8 @@ do
---@private
function changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
- local groups = {}
- for _, client in pairs(lsp.get_active_clients({ bufnr = bufnr })) do
+ local groups = {} ---@type table<string,CTGroup>
+ for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do
local group = get_group(client)
groups[group_key(group)] = group
end
@@ -648,7 +635,7 @@ do
if debounce == 0 then
send_changes(bufnr, group.sync_kind, state, buf_state)
else
- local timer = uv.new_timer()
+ local timer = assert(uv.new_timer(), 'Must be able to create timer')
buf_state.timer = timer
timer:start(
debounce,
@@ -695,10 +682,9 @@ do
end
end
----@private
--- Default handler for the 'textDocument/didOpen' LSP notification.
---
----@param bufnr number Number of the buffer, or 0 for current
+---@param bufnr integer Number of the buffer, or 0 for current
---@param client table Client object
local function text_document_did_open_handler(bufnr, client)
changetracking.init(client, bufnr)
@@ -708,7 +694,7 @@ local function text_document_did_open_handler(bufnr, client)
if not api.nvim_buf_is_loaded(bufnr) then
return
end
- local filetype = nvim_buf_get_option(bufnr, 'filetype')
+ local filetype = vim.bo[bufnr].filetype
local params = {
textDocument = {
@@ -718,7 +704,7 @@ local function text_document_did_open_handler(bufnr, client)
text = buf_get_full_text(bufnr),
},
}
- client.notify('textDocument/didOpen', params)
+ client.notify(ms.textDocument_didOpen, params)
util.buf_versions[bufnr] = params.textDocument.version
-- Next chance we get, we should re-do the diagnostics
@@ -735,7 +721,7 @@ end
-- FIXME: DOC: Shouldn't need to use a dummy function
--
--- LSP client object. You can get an active client object via
---- |vim.lsp.get_client_by_id()| or |vim.lsp.get_active_clients()|.
+--- |vim.lsp.get_client_by_id()| or |vim.lsp.get_clients()|.
---
--- - Methods:
---
@@ -801,29 +787,39 @@ end
--- to the server. Entries are key-value pairs with the key
--- being the request ID while the value is a table with `type`,
--- `bufnr`, and `method` key-value pairs. `type` is either "pending"
---- for an active request, or "cancel" for a cancel request.
+--- for an active request, or "cancel" for a cancel request. It will
+--- be "complete" ephemerally while executing |LspRequest| autocmds
+--- when replies are received from the server.
---
--- - {config} (table): copy of the table that was passed by the user
--- to |vim.lsp.start_client()|.
---
--- - {server_capabilities} (table): Response from the server sent on
--- `initialize` describing the server's capabilities.
+---
+--- - {progress} A ring buffer (|vim.ringbuf()|) containing progress messages
+--- sent by the server.
function lsp.client()
error()
end
+--- @class lsp.StartOpts
+--- @field reuse_client fun(client: lsp.Client, config: table): boolean
+--- @field bufnr integer
+
--- Create a new LSP client and start a language server or reuses an already
--- running client if one is found matching `name` and `root_dir`.
--- Attaches the current buffer to the client.
---
--- Example:
---- <pre>lua
+---
+--- ```lua
--- vim.lsp.start({
--- name = 'my-server-name',
--- cmd = {'name-of-language-server-executable'},
--- root_dir = vim.fs.dirname(vim.fs.find({'pyproject.toml', 'setup.py'}, { upward = true })[1]),
--- })
---- </pre>
+--- ```
---
--- See |vim.lsp.start_client()| for all available options. The most important are:
---
@@ -849,7 +845,7 @@ end
--- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|)
---
---@param config table Same configuration as documented in |vim.lsp.start_client()|
----@param opts nil|table Optional keyword arguments:
+---@param opts (nil|lsp.StartOpts) Optional keyword arguments:
--- - reuse_client (fun(client: client, config: table): boolean)
--- Predicate used to decide if a client should be re-used.
--- Used on all running clients.
@@ -858,14 +854,13 @@ end
--- - bufnr (number)
--- Buffer handle to attach to if starting or re-using a
--- client (0 for current).
----@return number|nil client_id
+---@return integer|nil client_id
function lsp.start(config, opts)
opts = opts or {}
local reuse_client = opts.reuse_client
or function(client, conf)
return client.config.root_dir == conf.root_dir and client.name == conf.name
end
- config.name = config.name
if not config.name and type(config.cmd) == 'table' then
config.name = config.cmd[1] and vim.fs.basename(config.cmd[1]) or nil
end
@@ -873,7 +868,7 @@ function lsp.start(config, opts)
if bufnr == nil or bufnr == 0 then
bufnr = api.nvim_get_current_buf()
end
- for _, clients in ipairs({ uninitialized_clients, lsp.get_active_clients() }) do
+ for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do
for _, client in pairs(clients) do
if reuse_client(client, config) then
lsp.buf_attach_client(bufnr, client.id)
@@ -889,6 +884,109 @@ function lsp.start(config, opts)
return client_id
end
+--- Consumes the latest progress messages from all clients and formats them as a string.
+--- Empty if there are no clients or if no new messages
+---
+---@return string
+function lsp.status()
+ local percentage = nil
+ local messages = {}
+ for _, client in ipairs(vim.lsp.get_clients()) do
+ for progress in client.progress do
+ local value = progress.value
+ if type(value) == 'table' and value.kind then
+ local message = value.message and (value.title .. ': ' .. value.message) or value.title
+ messages[#messages + 1] = message
+ if value.percentage then
+ percentage = math.max(percentage or 0, value.percentage)
+ end
+ end
+ -- else: Doesn't look like work done progress and can be in any format
+ -- Just ignore it as there is no sensible way to display it
+ end
+ end
+ local message = table.concat(messages, ', ')
+ if percentage then
+ return string.format('%3d%%: %s', percentage, message)
+ end
+ return message
+end
+
+-- Determines whether the given option can be set by `set_defaults`.
+local function is_empty_or_default(bufnr, option)
+ if vim.bo[bufnr][option] == '' then
+ return true
+ end
+
+ local info = vim.api.nvim_get_option_info2(option, { buf = bufnr })
+ local scriptinfo = vim.tbl_filter(function(e)
+ return e.sid == info.last_set_sid
+ end, vim.fn.getscriptinfo())
+
+ if #scriptinfo ~= 1 then
+ return false
+ end
+
+ return vim.startswith(scriptinfo[1].name, vim.fn.expand('$VIMRUNTIME'))
+end
+
+---@private
+---@param client lsp.Client
+function lsp._set_defaults(client, bufnr)
+ if
+ client.supports_method(ms.textDocument_definition) and is_empty_or_default(bufnr, 'tagfunc')
+ then
+ vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc'
+ end
+ if
+ client.supports_method(ms.textDocument_completion) and is_empty_or_default(bufnr, 'omnifunc')
+ then
+ vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc'
+ end
+ if
+ client.supports_method(ms.textDocument_rangeFormatting)
+ and is_empty_or_default(bufnr, 'formatprg')
+ and is_empty_or_default(bufnr, 'formatexpr')
+ then
+ vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()'
+ end
+ api.nvim_buf_call(bufnr, function()
+ if
+ client.supports_method(ms.textDocument_hover)
+ and is_empty_or_default(bufnr, 'keywordprg')
+ and vim.fn.maparg('K', 'n', false, false) == ''
+ then
+ vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buffer = bufnr })
+ end
+ end)
+ if client.supports_method(ms.textDocument_diagnostic) then
+ lsp.diagnostic._enable(bufnr)
+ end
+end
+
+--- @class lsp.ClientConfig
+--- @field cmd (string[]|fun(dispatchers: table):table)
+--- @field cmd_cwd string
+--- @field cmd_env (table)
+--- @field detached boolean
+--- @field workspace_folders (table)
+--- @field capabilities lsp.ClientCapabilities
+--- @field handlers table<string,function>
+--- @field settings table
+--- @field commands table
+--- @field init_options table
+--- @field name string
+--- @field get_language_id fun(bufnr: integer, filetype: string): string
+--- @field offset_encoding string
+--- @field on_error fun(code: integer)
+--- @field before_init function
+--- @field on_init function
+--- @field on_exit fun(code: integer, signal: integer, client_id: integer)
+--- @field on_attach fun(client: lsp.Client, bufnr: integer)
+--- @field trace 'off'|'messages'|'verbose'|nil
+--- @field flags table
+--- @field root_dir string
+
-- FIXME: DOC: Currently all methods on the `vim.lsp.client` object are
-- documented twice: Here, and on the methods themselves (e.g.
-- `client.request()`). This is a workaround for the vimdoc generator script
@@ -899,9 +997,9 @@ end
---
--- Field `cmd` in {config} is required.
---
----@param config (table) Configuration for the server:
---- - cmd: (table|string|fun(dispatchers: table):table) command string or
---- list treated like |jobstart()|. The command must launch the language server
+---@param config (lsp.ClientConfig) Configuration for the server:
+--- - cmd: (string[]|fun(dispatchers: table):table) command a list of
+--- strings treated like |jobstart()|. The command must launch the language server
--- process. `cmd` can also be a function that creates an RPC client.
--- The function receives a dispatchers table and must return a table with the
--- functions `request`, `notify`, `is_closing` and `terminate`
@@ -912,11 +1010,11 @@ end
--- the `cmd` process. Not related to `root_dir`.
---
--- - cmd_env: (table) Environment flags to pass to the LSP on
---- spawn. Can be specified using keys like a map or as a list with `k=v`
---- pairs or both. Non-string values are coerced to string.
+--- spawn. Must be specified using a table.
+--- Non-string values are coerced to string.
--- Example:
--- <pre>
---- { "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }
+--- { PORT = 8080; HOST = "0.0.0.0"; }
--- </pre>
---
--- - detached: (boolean, default true) Daemonize the server process so that it runs in a
@@ -929,11 +1027,10 @@ end
--- the LSP spec.
---
--- - capabilities: Map overriding the default capabilities defined by
---- |vim.lsp.protocol.make_client_capabilities()|, passed to the language
+--- \|vim.lsp.protocol.make_client_capabilities()|, passed to the language
--- server on initialization. Hint: use make_client_capabilities() and modify
--- its result.
---- - Note: To send an empty dictionary use
---- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an
+--- - Note: To send an empty dictionary use |vim.empty_dict()|, else it will be encoded as an
--- array.
---
--- - handlers: Map of language server method names to |lsp-handler|
@@ -977,7 +1074,7 @@ end
--- `initialize_result.offsetEncoding` if `capabilities.offsetEncoding` was
--- sent to it. You can only modify the `client.offset_encoding` here before
--- any notifications are sent. Most language servers expect to be sent client specified settings after
---- initialization. Neovim does not make this assumption. A
+--- initialization. Nvim does not make this assumption. A
--- `workspace/didChangeConfiguration` notification should be sent
--- to the server during on_init.
---
@@ -1006,13 +1103,11 @@ end
--- server will base its workspaceFolders, rootUri, and rootPath
--- on initialization.
---
----@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be
+---@return integer|nil client_id. |vim.lsp.get_client_by_id()| Note: client may not be
--- fully initialized. Use `on_init` to do any actions once
--- the client has been initialized.
function lsp.start_client(config)
- local cleaned_config = validate_client_config(config)
- local cmd, cmd_args, offset_encoding =
- cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding
+ local cmd, cmd_args, offset_encoding = validate_client_config(config)
config.flags = config.flags or {}
config.settings = config.settings or {}
@@ -1032,12 +1127,11 @@ function lsp.start_client(config)
local dispatch = {}
- ---@private
--- Returns the handler associated with an LSP method.
--- Returns the default handler if the user hasn't set a custom one.
---
---@param method (string) LSP method name
- ---@returns (fn) The handler for the given method, if defined, or the default from |vim.lsp.handlers|
+ ---@return lsp-handler|nil The handler for the given method, if defined, or the default from |vim.lsp.handlers|
local function resolve_handler(method)
return handlers[method] or default_handlers[method]
end
@@ -1049,7 +1143,9 @@ function lsp.start_client(config)
---@param method (string) LSP method name
---@param params (table) The parameters for that method.
function dispatch.notification(method, params)
- local _ = log.trace() and log.trace('notification', method, params)
+ if log.trace() then
+ log.trace('notification', method, params)
+ end
local handler = resolve_handler(method)
if handler then
-- Method name is provided here for convenience.
@@ -1063,27 +1159,41 @@ function lsp.start_client(config)
---@param method (string) LSP method name
---@param params (table) The parameters for that method
function dispatch.server_request(method, params)
- local _ = log.trace() and log.trace('server_request', method, params)
+ if log.trace() then
+ log.trace('server_request', method, params)
+ end
local handler = resolve_handler(method)
if handler then
- local _ = log.trace() and log.trace('server_request: found handler for', method)
+ if log.trace() then
+ log.trace('server_request: found handler for', method)
+ end
return handler(nil, params, { method = method, client_id = client_id })
end
- local _ = log.warn() and log.warn('server_request: no handler found for', method)
+ if log.warn() then
+ log.warn('server_request: no handler found for', method)
+ end
return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
end
+ --- Logs the given error to the LSP log and to the error buffer.
+ --- @param code integer Error code
+ --- @param err any Error arguments
+ local function write_error(code, err)
+ if log.error() then
+ log.error(log_prefix, 'on_error', { code = lsp.client_errors[code], err = err })
+ end
+ err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err))
+ end
+
---@private
--- Invoked when the client operation throws an error.
---
- ---@param code (number) Error code
+ ---@param code (integer) Error code
---@param err (...) Other arguments may be passed depending on the error kind
- ---@see `vim.lsp.rpc.client_errors` for possible errors. Use
+ ---@see vim.lsp.rpc.client_errors for possible errors. Use
---`vim.lsp.rpc.client_errors[code]` to get a human-friendly name.
function dispatch.on_error(code, err)
- local _ = log.error()
- and log.error(log_prefix, 'on_error', { code = lsp.client_errors[code], err = err })
- err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err))
+ write_error(code, err)
if config.on_error then
local status, usererr = pcall(config.on_error, code, err)
if not status then
@@ -1093,28 +1203,9 @@ function lsp.start_client(config)
end
end
- ---@private
- local function set_defaults(client, bufnr)
- local capabilities = client.server_capabilities
- if capabilities.definitionProvider and vim.bo[bufnr].tagfunc == '' then
- vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc'
- end
- if capabilities.completionProvider and vim.bo[bufnr].omnifunc == '' then
- vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc'
- end
- if
- capabilities.documentRangeFormattingProvider
- and vim.bo[bufnr].formatprg == ''
- and vim.bo[bufnr].formatexpr == ''
- then
- vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()'
- end
- end
-
- ---@private
--- Reset defaults set by `set_defaults`.
--- Must only be called if the last client attached to a buffer exits.
- local function unset_defaults(bufnr)
+ local function reset_defaults(bufnr)
if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then
vim.bo[bufnr].tagfunc = nil
end
@@ -1124,33 +1215,44 @@ function lsp.start_client(config)
if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then
vim.bo[bufnr].formatexpr = nil
end
+ api.nvim_buf_call(bufnr, function()
+ local keymap = vim.fn.maparg('K', 'n', false, true)
+ if keymap and keymap.callback == vim.lsp.buf.hover then
+ vim.keymap.del('n', 'K', { buffer = bufnr })
+ end
+ end)
end
---@private
--- Invoked on client exit.
---
- ---@param code (number) exit code of the process
- ---@param signal (number) the signal used to terminate (if any)
+ ---@param code (integer) exit code of the process
+ ---@param signal (integer) the signal used to terminate (if any)
function dispatch.on_exit(code, signal)
if config.on_exit then
pcall(config.on_exit, code, signal, client_id)
end
+ local client = active_clients[client_id] and active_clients[client_id]
+ or uninitialized_clients[client_id]
+
for bufnr, client_ids in pairs(all_buffer_active_clients) do
if client_ids[client_id] then
vim.schedule(function()
- nvim_exec_autocmds('LspDetach', {
- buffer = bufnr,
- modeline = false,
- data = { client_id = client_id },
- })
+ if client and client.attached_buffers[bufnr] then
+ nvim_exec_autocmds('LspDetach', {
+ buffer = bufnr,
+ modeline = false,
+ data = { client_id = client_id },
+ })
+ end
local namespace = vim.lsp.diagnostic.get_namespace(client_id)
vim.diagnostic.reset(namespace, bufnr)
client_ids[client_id] = nil
if vim.tbl_isempty(client_ids) then
- unset_defaults(bufnr)
+ reset_defaults(bufnr)
end
end)
end
@@ -1159,8 +1261,6 @@ function lsp.start_client(config)
-- Schedule the deletion of the client object so that it exists in the execution of LspDetach
-- autocommands
vim.schedule(function()
- local client = active_clients[client_id] and active_clients[client_id]
- or uninitialized_clients[client_id]
active_clients[client_id] = nil
uninitialized_clients[client_id] = nil
@@ -1170,8 +1270,13 @@ function lsp.start_client(config)
changetracking.reset(client)
end
if code ~= 0 or (signal ~= 0 and signal ~= 15) then
- local msg =
- string.format('Client %s quit with exit code %s and signal %s', client_id, code, signal)
+ local msg = string.format(
+ 'Client %s quit with exit code %s and signal %s. Check log for errors: %s',
+ name,
+ code,
+ signal,
+ lsp.get_log_path()
+ )
vim.notify(msg, vim.log.levels.WARN)
end
end)
@@ -1194,6 +1299,7 @@ function lsp.start_client(config)
return
end
+ ---@class lsp.Client
local client = {
id = client_id,
name = name,
@@ -1205,26 +1311,44 @@ function lsp.start_client(config)
handlers = handlers,
commands = config.commands or {},
+ --- @type table<integer,{ type: string, bufnr: integer, method: string}>
requests = {},
- -- for $/progress report
+
+ --- Contains $/progress report messages.
+ --- They have the format {token: integer|string, value: any}
+ --- For "work done progress", value will be one of:
+ --- - lsp.WorkDoneProgressBegin,
+ --- - lsp.WorkDoneProgressReport (extended with title from Begin)