aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp.lua
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2024-03-09 14:57:57 -0700
committerJosh Rahm <joshuarahm@gmail.com>2024-03-09 14:57:57 -0700
commitc324271b99eee4c621463f368914d57cd729bd9c (patch)
tree5d979d333a2d5f9c080991d5482fd5916f8579c6 /runtime/lua/vim/lsp.lua
parent931bffbda3668ddc609fc1da8f9eb576b170aa52 (diff)
parentade1b12f49c3b3914c74847d791eb90ea90b56b7 (diff)
downloadrneovim-c324271b99eee4c621463f368914d57cd729bd9c.tar.gz
rneovim-c324271b99eee4c621463f368914d57cd729bd9c.tar.bz2
rneovim-c324271b99eee4c621463f368914d57cd729bd9c.zip
Merge remote-tracking branch 'upstream/master' into userreg
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r--runtime/lua/vim/lsp.lua1769
1 files changed, 257 insertions, 1512 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 261a3aa5de..d5c376ba44 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -1,39 +1,37 @@
----@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_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
-local lsp = {
- protocol = protocol,
-
- handlers = default_handlers,
-
- 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,
+local lsp = vim._defer_require('vim.lsp', {
+ _changetracking = ..., --- @module 'vim.lsp._changetracking'
+ _completion = ..., --- @module 'vim.lsp._completion'
+ _dynamic = ..., --- @module 'vim.lsp._dynamic'
+ _snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar'
+ _tagfunc = ..., --- @module 'vim.lsp._tagfunc'
+ _watchfiles = ..., --- @module 'vim.lsp._watchfiles'
+ buf = ..., --- @module 'vim.lsp.buf'
+ client = ..., --- @module 'vim.lsp.client'
+ codelens = ..., --- @module 'vim.lsp.codelens'
+ diagnostic = ..., --- @module 'vim.lsp.diagnostic'
+ handlers = ..., --- @module 'vim.lsp.handlers'
+ inlay_hint = ..., --- @module 'vim.lsp.inlay_hint'
+ log = ..., --- @module 'vim.lsp.log'
+ protocol = ..., --- @module 'vim.lsp.protocol'
+ rpc = ..., --- @module 'vim.lsp.rpc'
+ semantic_tokens = ..., --- @module 'vim.lsp.semantic_tokens'
+ util = ..., --- @module 'vim.lsp.util'
+})
- -- Allow raw RPC access.
- rpc = lsp_rpc,
+local log = lsp.log
+local protocol = lsp.protocol
+local ms = protocol.Methods
+local util = lsp.util
+local changetracking = lsp._changetracking
- -- Export these directly from rpc.
- rpc_response_error = lsp_rpc.rpc_response_error,
-}
+-- Export these directly from rpc.
+---@nodoc
+lsp.rpc_response_error = lsp.rpc.rpc_response_error
-- maps request name to the required server_capability in the client.
lsp._request_name_to_capability = {
@@ -69,14 +67,6 @@ lsp._request_name_to_capability = {
-- TODO improve handling of scratch buffers with LSP attached.
---- Concatenates and writes a list of strings to the Vim error 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
-
--- Returns the buffer number for the given {bufnr}.
---
---@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
@@ -102,57 +92,28 @@ function lsp._unsupported_method(method)
return msg
end
---- Checks whether a given path is a directory.
----
----@param filename (string) path to check
----@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)
- return stat and stat.type == 'directory' or false
-end
-
local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' }
-local valid_encodings = {
- ['utf-8'] = 'utf-8',
- ['utf-16'] = 'utf-16',
- ['utf-32'] = 'utf-32',
- ['utf8'] = 'utf-8',
- ['utf16'] = 'utf-16',
- ['utf32'] = 'utf-32',
- UTF8 = 'utf-8',
- UTF16 = 'utf-16',
- UTF32 = 'utf-32',
-}
-
local format_line_ending = {
['unix'] = '\n',
['dos'] = '\r\n',
['mac'] = '\r',
}
+---@private
---@param bufnr (number)
---@return string
-local function buf_get_line_ending(bufnr)
+function lsp._buf_get_line_ending(bufnr)
return format_line_ending[vim.bo[bufnr].fileformat] or '\n'
end
-local client_index = 0
---- Returns a new, unused 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 = {} --- @type table<integer,lsp.Client>
+local active_clients = {} --- @type table<integer,vim.lsp.Client>
local all_buffer_active_clients = {} --- @type table<integer,table<integer,true>>
-local uninitialized_clients = {} --- @type table<integer,lsp.Client>
+local uninitialized_clients = {} --- @type table<integer,vim.lsp.Client>
---@param bufnr? integer
----@param fn fun(client: lsp.Client, client_id: integer, bufnr: integer)
+---@param fn fun(client: vim.lsp.Client, client_id: integer, bufnr: integer)
local function for_each_buffer_client(bufnr, fn, restrict_client_ids)
validate({
fn = { fn, 'f' },
@@ -182,136 +143,36 @@ local function for_each_buffer_client(bufnr, fn, restrict_client_ids)
end
end
+local client_errors_base = table.maxn(lsp.rpc.client_errors)
+local client_errors_offset = 0
+
+local function client_error(name)
+ client_errors_offset = client_errors_offset + 1
+ local index = client_errors_base + client_errors_offset
+ return { [name] = index, [index] = name }
+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.
--- @nodoc
lsp.client_errors = tbl_extend(
'error',
- lsp_rpc.client_errors,
- vim.tbl_add_reverse_lookup({
- 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,
- })
+ lsp.rpc.client_errors,
+ client_error('BEFORE_INIT_CALLBACK_ERROR'),
+ client_error('ON_INIT_CALLBACK_ERROR'),
+ client_error('ON_ATTACH_ERROR'),
+ client_error('ON_EXIT_CALLBACK_ERROR')
)
---- Normalizes {encoding} to valid LSP encoding names.
----
----@param encoding (string) Encoding to normalize
----@return string # normalized encoding name
-local function validate_encoding(encoding)
- validate({
- encoding = { encoding, 's' },
- })
- return valid_encodings[encoding:lower()]
- or error(
- string.format(
- "Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
- encoding
- )
- )
-end
-
----@internal
---- 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 string[]
----@return string command, string[] args #the command and arguments
-function lsp._cmd_parts(input)
- validate({
- cmd = {
- input,
- function()
- return vim.tbl_islist(input)
- end,
- 'list',
- },
- })
-
- local cmd = input[1]
- local cmd_args = {}
- -- Don't mutate our input.
- for i, v in ipairs(input) do
- validate({ ['cmd argument'] = { v, 's' } })
- if i > 1 then
- table.insert(cmd_args, v)
- end
- end
- return cmd, cmd_args
-end
-
---- Augments a validator function with support for optional (nil) values.
----
----@param fn (fun(v): boolean) The original validator function; should return a
----bool.
----@return fun(v): boolean # The augmented function. Also returns true if {v} is
----`nil`.
-local function optional_validator(fn)
- return function(v)
- return v == nil or fn(v)
- end
-end
-
---- Validates a client configuration as given to |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' },
- })
- validate({
- handlers = { config.handlers, 't', true },
- capabilities = { config.capabilities, 't', true },
- cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' },
- cmd_env = { config.cmd_env, 't', true },
- detached = { config.detached, 'b', true },
- name = { config.name, 's', true },
- on_error = { config.on_error, 'f', true },
- on_exit = { config.on_exit, 'f', true },
- on_init = { config.on_init, 'f', true },
- settings = { config.settings, 't', true },
- commands = { config.commands, 't', true },
- before_init = { config.before_init, 'f', true },
- offset_encoding = { config.offset_encoding, 's', true },
- flags = { config.flags, 't', true },
- get_language_id = { config.get_language_id, 'f', true },
- })
- assert(
- (
- not config.flags
- or not config.flags.debounce_text_changes
- or type(config.flags.debounce_text_changes) == 'number'
- ),
- 'flags.debounce_text_changes must be a number with the debounce time in milliseconds'
- )
-
- 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)
- end
- local offset_encoding = valid_encodings.UTF16
- if config.offset_encoding then
- offset_encoding = validate_encoding(config.offset_encoding)
- end
-
- 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.
---@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)
+function lsp._buf_get_full_text(bufnr)
+ local line_ending = lsp._buf_get_line_ending(bufnr)
+ local text = table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, true), line_ending)
if vim.bo[bufnr].eol then
text = text .. line_ending
end
@@ -327,484 +188,26 @@ end
---@param fn (T) Function to run
---@return T
local function once(fn)
- local value --- @type any
+ local value --- @type function
local ran = false
return function(...)
if not ran then
- value = fn(...)
+ value = fn(...) --- @type function
ran = true
end
return value
end
end
-local changetracking = {}
-do
- ---@private
- ---
- --- LSP has 3 different sync modes:
- --- - None (Servers will read the files themselves when needed)
- --- - Full (Client sends the full buffer content on updates)
- --- - Incremental (Client sends only the changed parts)
- ---
- --- Changes are tracked per buffer.
- --- A buffer can have multiple clients attached and each client needs to send the changes
- --- To minimize the amount of changesets to compute, computation is grouped:
- ---
- --- None: One group for all clients
- --- Full: One group for all clients
- --- Incremental: One group per `offset_encoding`
- ---
- --- Sending changes can be debounced per buffer. To simplify the implementation the
- --- smallest debounce interval is used and we don't group clients by different intervals.
- ---
- --- @class CTGroup
- --- @field sync_kind integer TextDocumentSyncKind, considers config.flags.allow_incremental_sync
- --- @field offset_encoding "utf-8"|"utf-16"|"utf-32"
- ---
- --- @class CTBufferState
- --- @field name string name of the buffer
- --- @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|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 integer how many clients are using this group
- ---
- --- @class CTGroupState
- --- @field buffers table<integer, CTBufferState>
- --- @field debounce integer debounce duration in ms
- --- @field clients table<integer, table> clients using this state. {client_id, client}
-
- ---@param group CTGroup
- ---@return string
- local function group_key(group)
- if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
- return tostring(group.sync_kind) .. '\0' .. group.offset_encoding
- end
- return tostring(group.sync_kind)
- end
-
- ---@private
- ---@type table<CTGroup, CTGroupState>
- local state_by_group = setmetatable({}, {
- __index = function(tbl, k)
- return rawget(tbl, group_key(k))
- end,
- __newindex = function(tbl, k, v)
- rawset(tbl, group_key(k), v)
- end,
- })
-
- ---@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, '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
- end
- return {
- sync_kind = sync_kind,
- offset_encoding = client.offset_encoding,
- }
- end
-
- ---@param state CTBufferState
- local function incremental_changes(state, encoding, bufnr, firstline, lastline, new_lastline)
- local prev_lines = state.lines
- local curr_lines = state.lines_tmp
-
- local changed_lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
- for i = 1, firstline do
- curr_lines[i] = prev_lines[i]
- end
- for i = firstline + 1, new_lastline do
- curr_lines[i] = changed_lines[i - firstline]
- end
- for i = lastline + 1, #prev_lines do
- curr_lines[i - lastline + new_lastline] = prev_lines[i]
- end
- if tbl_isempty(curr_lines) then
- -- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259.
- curr_lines[1] = ''
- end
-
- local line_ending = buf_get_line_ending(bufnr)
- local incremental_change = sync.compute_diff(
- state.lines,
- curr_lines,
- firstline,
- lastline,
- new_lastline,
- encoding,
- line_ending
- )
-
- -- Double-buffering of lines tables is used to reduce the load on the garbage collector.
- -- At this point the prev_lines table is useless, but its internal storage has already been allocated,
- -- so let's keep it around for the next didChange event, in which it will become the next
- -- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the
- -- internal storage - it merely marks them as free, for the GC to deallocate them.
- for i in ipairs(prev_lines) do
- prev_lines[i] = nil
- end
- state.lines = curr_lines
- state.lines_tmp = prev_lines
-
- return incremental_change
- end
-
- ---@private
- function changetracking.init(client, bufnr)
- assert(client.offset_encoding, 'lsp client must have an offset_encoding')
- local group = get_group(client)
- local state = state_by_group[group]
- if state then
- state.debounce = math.min(state.debounce, client.config.flags.debounce_text_changes or 150)
- state.clients[client.id] = client
- else
- state = {
- buffers = {},
- debounce = client.config.flags.debounce_text_changes or 150,
- clients = {
- [client.id] = client,
- },
- }
- state_by_group[group] = state
- end
- local buf_state = state.buffers[bufnr]
- if buf_state then
- buf_state.refs = buf_state.refs + 1
- else
- buf_state = {
- name = api.nvim_buf_get_name(bufnr),
- lines = {},
- lines_tmp = {},
- pending_changes = {},
- needs_flush = false,
- refs = 1,
- }
- state.buffers[bufnr] = buf_state
- if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
- buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true)
- end
- end
- end
-
- ---@private
- function changetracking._get_and_set_name(client, bufnr, name)
- local state = state_by_group[get_group(client)] or {}
- local buf_state = (state.buffers or {})[bufnr]
- local old_name = buf_state.name
- buf_state.name = name
- return old_name
- end
-
- ---@private
- function changetracking.reset_buf(client, bufnr)
- changetracking.flush(client, bufnr)
- local state = state_by_group[get_group(client)]
- if not state then
- return
- end
- assert(state.buffers, 'CTGroupState must have buffers')
- local buf_state = state.buffers[bufnr]
- buf_state.refs = buf_state.refs - 1
- assert(buf_state.refs >= 0, 'refcount on buffer state must not get negative')
- if buf_state.refs == 0 then
- state.buffers[bufnr] = nil
- changetracking._reset_timer(buf_state)
- end
- end
-
- ---@private
- function changetracking.reset(client)
- local state = state_by_group[get_group(client)]
- if not state then
- return
- end
- state.clients[client.id] = nil
- if vim.tbl_count(state.clients) == 0 then
- for _, buf_state in pairs(state.buffers) do
- changetracking._reset_timer(buf_state)
- end
- state.buffers = {}
- end
- end
-
- -- 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 integer
- ---@param buf_state CTBufferState
- ---@return number
- local function next_debounce(debounce, buf_state)
- if debounce == 0 then
- return 0
- end
- local ns_to_ms = 0.000001
- if not buf_state.last_flush then
- return debounce
- end
- local now = uv.hrtime()
- local ms_since_last_flush = (now - buf_state.last_flush) * ns_to_ms
- return math.max(debounce - ms_since_last_flush, 0)
- end
-
- ---@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)
- if not buf_state.needs_flush then
- return
- end
- buf_state.last_flush = uv.hrtime()
- buf_state.needs_flush = false
-
- if not api.nvim_buf_is_valid(bufnr) then
- buf_state.pending_changes = {}
- return
- end
-
- local changes
- if sync_kind == protocol.TextDocumentSyncKind.None then
- return
- elseif sync_kind == protocol.TextDocumentSyncKind.Incremental then
- changes = buf_state.pending_changes
- buf_state.pending_changes = {}
- else
- changes = {
- { text = buf_get_full_text(bufnr) },
- }
- end
- 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(ms.textDocument_didChange, {
- textDocument = {
- uri = uri,
- version = util.buf_versions[bufnr],
- },
- contentChanges = changes,
- })
- end
- end
- end
-
- ---@private
- function changetracking.send_changes(bufnr, firstline, lastline, new_lastline)
- 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
- for _, group in pairs(groups) do
- local state = state_by_group[group]
- if not state then
- error(
- string.format(
- 'changetracking.init must have been called for all LSP clients. group=%s states=%s',
- vim.inspect(group),
- vim.inspect(vim.tbl_keys(state_by_group))
- )
- )
- end
- local buf_state = state.buffers[bufnr]
- buf_state.needs_flush = true
- changetracking._reset_timer(buf_state)
- local debounce = next_debounce(state.debounce, buf_state)
- if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then
- -- This must be done immediately and cannot be delayed
- -- The contents would further change and startline/endline may no longer fit
- local changes = incremental_changes(
- buf_state,
- group.offset_encoding,
- bufnr,
- firstline,
- lastline,
- new_lastline
- )
- table.insert(buf_state.pending_changes, changes)
- end
- if debounce == 0 then
- send_changes(bufnr, group.sync_kind, state, buf_state)
- else
- local timer = assert(uv.new_timer(), 'Must be able to create timer')
- buf_state.timer = timer
- timer:start(
- debounce,
- 0,
- vim.schedule_wrap(function()
- changetracking._reset_timer(buf_state)
- send_changes(bufnr, group.sync_kind, state, buf_state)
- end)
- )
- end
- end
- end
-
- ---@private
- function changetracking._reset_timer(buf_state)
- local timer = buf_state.timer
- if timer then
- buf_state.timer = nil
- if not timer:is_closing() then
- timer:stop()
- timer:close()
- end
- end
- end
-
- --- Flushes any outstanding change notification.
- ---@private
- function changetracking.flush(client, bufnr)
- local group = get_group(client)
- local state = state_by_group[group]
- if not state then
- return
- end
- if bufnr then
- local buf_state = state.buffers[bufnr] or {}
- changetracking._reset_timer(buf_state)
- send_changes(bufnr, group.sync_kind, state, buf_state)
- else
- for buf, buf_state in pairs(state.buffers) do
- changetracking._reset_timer(buf_state)
- send_changes(buf, group.sync_kind, state, buf_state)
- end
- end
- end
-end
-
---- Default handler for the 'textDocument/didOpen' LSP notification.
----
----@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)
- if not vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then
- return
- end
- if not api.nvim_buf_is_loaded(bufnr) then
- return
- end
- local filetype = vim.bo[bufnr].filetype
-
- local params = {
- textDocument = {
- version = 0,
- uri = vim.uri_from_bufnr(bufnr),
- languageId = client.config.get_language_id(bufnr, filetype),
- text = buf_get_full_text(bufnr),
- },
- }
- client.notify(ms.textDocument_didOpen, params)
- util.buf_versions[bufnr] = params.textDocument.version
-
- -- Next chance we get, we should re-do the diagnostics
- vim.schedule(function()
- -- Protect against a race where the buffer disappears
- -- between `did_open_handler` and the scheduled function firing.
- if api.nvim_buf_is_valid(bufnr) then
- local namespace = vim.lsp.diagnostic.get_namespace(client.id)
- vim.diagnostic.show(namespace, bufnr)
- end
- end)
-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_clients()|.
----
---- - Methods:
----
---- - request(method, params, [handler], bufnr)
---- Sends a request to the server.
---- This is a thin wrapper around {client.rpc.request} with some additional
---- checking.
---- If {handler} is not specified, If one is not found there, then an error will occur.
---- Returns: {status}, {[client_id]}. {status} is a boolean indicating if
---- the notification was successful. If it is `false`, then it will always
---- be `false` (the client has shutdown).
---- If {status} is `true`, the function returns {request_id} as the second
---- result. You can use this with `client.cancel_request(request_id)`
---- to cancel the request.
----
---- - request_sync(method, params, timeout_ms, bufnr)
---- Sends a request to the server and synchronously waits for the response.
---- This is a wrapper around {client.request}
---- Returns: { err=err, result=result }, a dictionary, where `err` and `result` come from
---- the |lsp-handler|. On timeout, cancel or error, returns `(nil, err)` where `err` is a
---- string describing the failure reason. If the request was unsuccessful returns `nil`.
----
---- - notify(method, params)
---- Sends a notification to an LSP server.
---- Returns: a boolean to indicate if the notification was successful. If
---- it is false, then it will always be false (the client has shutdown).
----
---- - cancel_request(id)
---- Cancels a request with a given request id.
---- Returns: same as `notify()`.
----
---- - stop([force])
---- Stops a client, optionally with force.
---- By default, it will just ask the server to shutdown without force.
---- If you request to stop a client which has previously been requested to
---- shutdown, it will automatically escalate and force shutdown.
----
---- - is_stopped()
---- Checks whether a client is stopped.
---- Returns: true if the client is fully stopped.
----
---- - on_attach(client, bufnr)
---- Runs the on_attach function from the client's config if it was defined.
---- Useful for buffer-local setup.
+--- @class vim.lsp.start.Opts
+--- @inlinedoc
---
---- - Members
---- - {id} (number): The id allocated to the client.
+--- Predicate used to decide if a client should be re-used. Used on all
+--- running clients. The default implementation re-uses a client if name and
+--- root_dir matches.
+--- @field reuse_client fun(client: vim.lsp.Client, config: table): boolean
---
---- - {name} (string): If a name is specified on creation, that will be
---- used. Otherwise it is just the client id. This is used for
---- logs and messages.
----
---- - {rpc} (table): RPC client object, for low level interaction with the
---- client. See |vim.lsp.rpc.start()|.
----
---- - {offset_encoding} (string): The encoding used for communicating
---- with the server. You can modify this in the `config`'s `on_init` method
---- before text is sent to the server.
----
---- - {handlers} (table): The handlers used by the client as described in |lsp-handler|.
----
---- - {requests} (table): The current pending requests in flight
---- 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. 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
+--- Buffer handle to attach to if starting or re-using a client (0 for current).
--- @field bufnr integer
--- Create a new LSP client and start a language server or reuses an already
@@ -824,8 +227,7 @@ end
--- See |vim.lsp.start_client()| for all available options. The most important are:
---
--- - `name` arbitrary name for the LSP client. Should be unique per language server.
---- - `cmd` command (in list form) used to start the language server. Must be absolute, or found on
---- `$PATH`. Shell constructs like `~` are not expanded.
+--- - `cmd` command string[] or function, described at |vim.lsp.start_client()|.
--- - `root_dir` path to the project root. By default this is used to decide if an existing client
--- should be re-used. The example above uses |vim.fs.find()| and |vim.fs.dirname()| to detect the
--- root by traversing the file system upwards starting from the current directory until either
@@ -844,30 +246,18 @@ end
--- Either use |:au|, |nvim_create_autocmd()| or put the call in a
--- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|)
---
----@param config table Same configuration as documented in |vim.lsp.start_client()|
----@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.
---- The default implementation re-uses a client if name
---- and root_dir matches.
---- - bufnr (number)
---- Buffer handle to attach to if starting or re-using a
---- client (0 for current).
----@return integer|nil client_id
+--- @param config vim.lsp.ClientConfig Configuration for the server.
+--- @param opts vim.lsp.start.Opts? Optional keyword arguments
+--- @return integer? 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
+ return client.root_dir == conf.root_dir and client.name == conf.name
end
- 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
- local bufnr = opts.bufnr
- if bufnr == nil or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+
+ local bufnr = resolve_bufnr(opts.bufnr)
+
for _, clients in ipairs({ uninitialized_clients, lsp.get_clients() }) do
for _, client in pairs(clients) do
if reuse_client(client, config) then
@@ -876,10 +266,13 @@ function lsp.start(config, opts)
end
end
end
+
local client_id = lsp.start_client(config)
- if client_id == nil then
- return nil -- lsp.start_client will have printed an error
+
+ if not client_id then
+ return -- lsp.start_client will have printed an error
end
+
lsp.buf_attach_client(bufnr, client_id)
return client_id
end
@@ -890,9 +283,11 @@ end
---@return string
function lsp.status()
local percentage = nil
- local messages = {}
+ local messages = {} --- @type string[]
for _, client in ipairs(vim.lsp.get_clients()) do
+ --- @diagnostic disable-next-line:no-unknown
for progress in client.progress do
+ --- @cast progress {token: lsp.ProgressToken, value: lsp.LSPAny}
local value = progress.value
if type(value) == 'table' and value.kind then
local message = value.message and (value.title .. ': ' .. value.message) or value.title
@@ -913,12 +308,15 @@ function lsp.status()
end
-- Determines whether the given option can be set by `set_defaults`.
+---@param bufnr integer
+---@param option string
+---@return boolean
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 info = 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())
@@ -931,7 +329,8 @@ local function is_empty_or_default(bufnr, option)
end
---@private
----@param client lsp.Client
+---@param client vim.lsp.Client
+---@param bufnr integer
function lsp._set_defaults(client, bufnr)
if
client.supports_method(ms.textDocument_definition) and is_empty_or_default(bufnr, 'tagfunc')
@@ -964,818 +363,151 @@ function lsp._set_defaults(client, 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
--- not handling method names correctly. If you change the documentation on
--- either, please make sure to update the other as well.
---
---- Starts and initializes a client with the given configuration.
----
---- Field `cmd` in {config} is required.
----
----@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`
---- See |vim.lsp.rpc.request()| and |vim.lsp.rpc.notify()|
---- For TCP there is a built-in rpc client factory: |vim.lsp.rpc.connect()|
----
---- - cmd_cwd: (string, default=|getcwd()|) Directory to launch
---- the `cmd` process. Not related to `root_dir`.
----
---- - cmd_env: (table) Environment flags to pass to the LSP on
---- spawn. Must be specified using a table.
---- Non-string values are coerced to string.
---- Example:
---- <pre>
---- { PORT = 8080; HOST = "0.0.0.0"; }
---- </pre>
----
---- - detached: (boolean, default true) Daemonize the server process so that it runs in a
---- separate process group from Nvim. Nvim will shutdown the process on exit, but if Nvim fails to
---- exit cleanly this could leave behind orphaned server processes.
----
---- - workspace_folders: (table) List of workspace folders passed to the
---- language server. For backwards compatibility rootUri and rootPath will be
---- derived from the first workspace folder in this list. See `workspaceFolders` in
---- the LSP spec.
----
---- - capabilities: Map overriding the default capabilities defined by
---- \|vim.lsp.protocol.make_client_capabilities()|, passed to the language
---- server on initializa