aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp.lua
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2025-02-05 23:09:29 +0000
committerJosh Rahm <joshuarahm@gmail.com>2025-02-05 23:09:29 +0000
commitd5f194ce780c95821a855aca3c19426576d28ae0 (patch)
treed45f461b19f9118ad2bb1f440a7a08973ad18832 /runtime/lua/vim/lsp.lua
parentc5d770d311841ea5230426cc4c868e8db27300a8 (diff)
parent44740e561fc93afe3ebecfd3618bda2d2abeafb0 (diff)
downloadrneovim-d5f194ce780c95821a855aca3c19426576d28ae0.tar.gz
rneovim-d5f194ce780c95821a855aca3c19426576d28ae0.tar.bz2
rneovim-d5f194ce780c95821a855aca3c19426576d28ae0.zip
Merge remote-tracking branch 'upstream/master' into mix_20240309HEADrahm
Diffstat (limited to 'runtime/lua/vim/lsp.lua')
-rw-r--r--runtime/lua/vim/lsp.lua692
1 files changed, 498 insertions, 194 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
index 0de3b4ee4d..a45f9adeb6 100644
--- a/runtime/lua/vim/lsp.lua
+++ b/runtime/lua/vim/lsp.lua
@@ -3,6 +3,7 @@ local validate = vim.validate
local lsp = vim._defer_require('vim.lsp', {
_changetracking = ..., --- @module 'vim.lsp._changetracking'
+ _folding_range = ..., --- @module 'vim.lsp._folding_range'
_snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar'
_tagfunc = ..., --- @module 'vim.lsp._tagfunc'
_watchfiles = ..., --- @module 'vim.lsp._watchfiles'
@@ -57,6 +58,7 @@ lsp._request_name_to_capability = {
[ms.textDocument_documentHighlight] = { 'documentHighlightProvider' },
[ms.textDocument_documentLink] = { 'documentLinkProvider' },
[ms.textDocument_documentSymbol] = { 'documentSymbolProvider' },
+ [ms.textDocument_foldingRange] = { 'foldingRangeProvider' },
[ms.textDocument_formatting] = { 'documentFormattingProvider' },
[ms.textDocument_hover] = { 'hoverProvider' },
[ms.textDocument_implementation] = { 'implementationProvider' },
@@ -87,18 +89,6 @@ lsp._request_name_to_capability = {
-- TODO improve handling of scratch buffers with LSP attached.
---- Returns the buffer number for the given {bufnr}.
----
----@param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer
----@return integer bufnr
-local function resolve_bufnr(bufnr)
- validate('bufnr', bufnr, 'number', true)
- if bufnr == nil or bufnr == 0 then
- return api.nvim_get_current_buf()
- end
- return bufnr
-end
-
---@private
--- Called by the client when trying to call a method that's not
--- supported in any of the servers registered for the current buffer.
@@ -112,6 +102,22 @@ function lsp._unsupported_method(method)
return msg
end
+---@private
+---@param workspace_folders string|lsp.WorkspaceFolder[]?
+---@return lsp.WorkspaceFolder[]?
+function lsp._get_workspace_folders(workspace_folders)
+ if type(workspace_folders) == 'table' then
+ return workspace_folders
+ elseif type(workspace_folders) == 'string' then
+ return {
+ {
+ uri = vim.uri_from_fname(workspace_folders),
+ name = workspace_folders,
+ },
+ }
+ end
+end
+
local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'error' }
local format_line_ending = {
@@ -194,34 +200,393 @@ local function reuse_client_default(client, config)
return false
end
- if config.root_dir then
- local root = vim.uri_from_fname(config.root_dir)
- for _, dir in ipairs(client.workspace_folders or {}) do
- -- note: do not need to check client.root_dir since that should be client.workspace_folders[1]
- if root == dir.uri then
- return true
+ local config_folders = lsp._get_workspace_folders(config.workspace_folders or config.root_dir)
+
+ if not config_folders or not next(config_folders) then
+ -- Reuse if the client was configured with no workspace folders
+ local client_config_folders =
+ lsp._get_workspace_folders(client.config.workspace_folders or client.config.root_dir)
+ return not client_config_folders or not next(client_config_folders)
+ end
+
+ for _, config_folder in ipairs(config_folders) do
+ local found = false
+ for _, client_folder in ipairs(client.workspace_folders or {}) do
+ if config_folder.uri == client_folder.uri then
+ found = true
+ break
end
end
+ if not found then
+ return false
+ end
end
- -- TODO(lewis6991): also check config.workspace_folders
+ return true
+end
- return false
+--- Reset defaults set by `set_defaults`.
+--- Must only be called if the last client attached to a buffer exits.
+local function reset_defaults(bufnr)
+ if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then
+ vim.bo[bufnr].tagfunc = nil
+ end
+ if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then
+ vim.bo[bufnr].omnifunc = nil
+ end
+ if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then
+ vim.bo[bufnr].formatexpr = nil
+ end
+ vim._with({ buf = bufnr }, function()
+ local keymap = vim.fn.maparg('K', 'n', false, true)
+ if keymap and keymap.callback == vim.lsp.buf.hover and keymap.buffer == 1 then
+ vim.keymap.del('n', 'K', { buffer = bufnr })
+ end
+ end)
end
---- @class vim.lsp.start.Opts
---- @inlinedoc
+--- @param code integer
+--- @param signal integer
+--- @param client_id integer
+local function on_client_exit(code, signal, client_id)
+ local client = all_clients[client_id]
+
+ vim.schedule(function()
+ for bufnr in pairs(client.attached_buffers) do
+ if client and client.attached_buffers[bufnr] and api.nvim_buf_is_valid(bufnr) then
+ api.nvim_exec_autocmds('LspDetach', {
+ buffer = bufnr,
+ modeline = false,
+ data = { client_id = client_id },
+ })
+ end
+
+ client.attached_buffers[bufnr] = nil
+
+ if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then
+ reset_defaults(bufnr)
+ end
+ end
+
+ local namespace = vim.lsp.diagnostic.get_namespace(client_id)
+ vim.diagnostic.reset(namespace)
+ end)
+
+ local name = client.name or 'unknown'
+
+ -- Schedule the deletion of the client object so that it exists in the execution of LspDetach
+ -- autocommands
+ vim.schedule(function()
+ all_clients[client_id] = nil
+
+ -- Client can be absent if executable starts, but initialize fails
+ -- init/attach won't have happened
+ if client then
+ 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. Check log for errors: %s',
+ name,
+ code,
+ signal,
+ lsp.get_log_path()
+ )
+ vim.notify(msg, vim.log.levels.WARN)
+ end
+ end)
+end
+
+--- Creates and initializes a client with the given configuration.
+--- @param config vim.lsp.ClientConfig Configuration for the server.
+--- @return integer? 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.
+--- @return string? # Error message, if any
+local function create_and_initialize_client(config)
+ local ok, res = pcall(require('vim.lsp.client').create, config)
+ if not ok then
+ return nil, res --[[@as string]]
+ end
+
+ local client = assert(res)
+
+ --- @diagnostic disable-next-line: invisible
+ table.insert(client._on_exit_cbs, on_client_exit)
+
+ all_clients[client.id] = client
+
+ client:initialize()
+
+ return client.id, nil
+end
+
+--- @class vim.lsp.Config : vim.lsp.ClientConfig
+---
+--- See `cmd` in [vim.lsp.ClientConfig].
+--- @field cmd? string[]|fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient
+---
+--- Filetypes the client will attach to, if activated by `vim.lsp.enable()`.
+--- If not provided, then the client will attach to all filetypes.
+--- @field filetypes? string[]
+---
+--- Directory markers (.e.g. '.git/') where the LSP server will base its workspaceFolders,
+--- rootUri, and rootPath on initialization. Unused if `root_dir` is provided.
+--- @field root_markers? string[]
+---
+--- Directory where the LSP server will base its workspaceFolders, rootUri, and rootPath on
+--- initialization. If a function, it accepts a single callback argument which must be called with
+--- the value of root_dir to use. The LSP server will not be started until the callback is called.
+--- @field root_dir? string|fun(cb:fun(string))
---
--- 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: vim.lsp.ClientConfig): boolean
+
+--- Update the configuration for an LSP client.
+---
+--- Use name '*' to set default configuration for all clients.
+---
+--- Can also be table-assigned to redefine the configuration for a client.
+---
+--- Examples:
+---
+--- - Add a root marker for all clients:
+--- ```lua
+--- vim.lsp.config('*', {
+--- root_markers = { '.git' },
+--- })
+--- ```
+--- - Add additional capabilities to all clients:
+--- ```lua
+--- vim.lsp.config('*', {
+--- capabilities = {
+--- textDocument = {
+--- semanticTokens = {
+--- multilineTokenSupport = true,
+--- }
+--- }
+--- }
+--- })
+--- ```
+--- - (Re-)define the configuration for clangd:
+--- ```lua
+--- vim.lsp.config.clangd = {
+--- cmd = {
+--- 'clangd',
+--- '--clang-tidy',
+--- '--background-index',
+--- '--offset-encoding=utf-8',
+--- },
+--- root_markers = { '.clangd', 'compile_commands.json' },
+--- filetypes = { 'c', 'cpp' },
+--- }
+--- ```
+--- - Get configuration for luals:
+--- ```lua
+--- local cfg = vim.lsp.config.luals
+--- ```
+---
+--- @param name string
+--- @param cfg vim.lsp.Config
+--- @diagnostic disable-next-line:assign-type-mismatch
+function lsp.config(name, cfg)
+ local _, _ = name, cfg -- ignore unused
+ -- dummy proto for docs
+end
+
+lsp._enabled_configs = {} --- @type table<string,{resolved_config:vim.lsp.Config?}>
+
+--- If a config in vim.lsp.config() is accessed then the resolved config becomes invalid.
+--- @param name string
+local function invalidate_enabled_config(name)
+ if name == '*' then
+ for _, v in pairs(lsp._enabled_configs) do
+ v.resolved_config = nil
+ end
+ elseif lsp._enabled_configs[name] then
+ lsp._enabled_configs[name].resolved_config = nil
+ end
+end
+
+--- @nodoc
+--- @class vim.lsp.config
+--- @field [string] vim.lsp.Config
+--- @field package _configs table<string,vim.lsp.Config>
+lsp.config = setmetatable({ _configs = {} }, {
+ --- @param self vim.lsp.config
+ --- @param name string
+ --- @return vim.lsp.Config
+ __index = function(self, name)
+ validate('name', name, 'string')
+
+ local rconfig = lsp._enabled_configs[name] or {}
+ self._configs[name] = self._configs[name] or {}
+
+ if not rconfig.resolved_config then
+ -- Resolve configs from lsp/*.lua
+ -- Calls to vim.lsp.config in lsp/* have a lower precedence than calls from other sites.
+ local rtp_config = {} ---@type vim.lsp.Config
+ for _, v in ipairs(api.nvim_get_runtime_file(('lsp/%s.lua'):format(name), true)) do
+ local config = assert(loadfile(v))() ---@type any?
+ if type(config) == 'table' then
+ rtp_config = vim.tbl_deep_extend('force', rtp_config, config)
+ else
+ log.warn(string.format('%s does not return a table, ignoring', v))
+ end
+ end
+
+ rconfig.resolved_config = vim.tbl_deep_extend(
+ 'force',
+ lsp.config._configs['*'] or {},
+ rtp_config,
+ lsp.config._configs[name] or {}
+ )
+ rconfig.resolved_config.name = name
+ end
+
+ return rconfig.resolved_config
+ end,
+
+ --- @param self vim.lsp.config
+ --- @param name string
+ --- @param cfg vim.lsp.Config
+ __newindex = function(self, name, cfg)
+ validate('name', name, 'string')
+ validate('cfg', cfg, 'table')
+ invalidate_enabled_config(name)
+ self._configs[name] = cfg
+ end,
+
+ --- @param self vim.lsp.config
+ --- @param name string
+ --- @param cfg vim.lsp.Config
+ __call = function(self, name, cfg)
+ validate('name', name, 'string')
+ validate('cfg', cfg, 'table')
+ invalidate_enabled_config(name)
+ self[name] = vim.tbl_deep_extend('force', self._configs[name] or {}, cfg)
+ end,
+})
+
+local lsp_enable_autocmd_id --- @type integer?
+
+--- @param bufnr integer
+local function lsp_enable_callback(bufnr)
+ -- Only ever attach to buffers that represent an actual file.
+ if vim.bo[bufnr].buftype ~= '' then
+ return
+ end
+
+ --- @param config vim.lsp.Config
+ local function can_start(config)
+ if config.filetypes and not vim.tbl_contains(config.filetypes, vim.bo[bufnr].filetype) then
+ return false
+ elseif type(config.cmd) == 'table' and vim.fn.executable(config.cmd[1]) == 0 then
+ return false
+ end
+
+ return true
+ end
+
+ --- @param config vim.lsp.Config
+ local function start(config)
+ return vim.lsp.start(config, {
+ bufnr = bufnr,
+ reuse_client = config.reuse_client,
+ _root_markers = config.root_markers,
+ })
+ end
+
+ for name in vim.spairs(lsp._enabled_configs) do
+ local config = lsp.config[name]
+ validate('cmd', config.cmd, { 'function', 'table' })
+ validate('cmd', config.reuse_client, 'function', true)
+
+ if can_start(config) then
+ -- Deepcopy config so changes done in the client
+ -- do not propagate back to the enabled configs.
+ config = vim.deepcopy(config)
+
+ if type(config.root_dir) == 'function' then
+ ---@param root_dir string
+ config.root_dir(function(root_dir)
+ config.root_dir = root_dir
+ vim.schedule(function()
+ start(config)
+ end)
+ end)
+ else
+ start(config)
+ end
+ end
+ end
+end
+
+--- Enable an LSP server to automatically start when opening a buffer.
+---
+--- Uses configuration defined with `vim.lsp.config`.
+---
+--- Examples:
+---
+--- ```lua
+--- vim.lsp.enable('clangd')
+---
+--- vim.lsp.enable({'luals', 'pyright'})
+--- ```
+---
+--- @param name string|string[] Name(s) of client(s) to enable.
+--- @param enable? boolean `true|nil` to enable, `false` to disable.
+function lsp.enable(name, enable)
+ validate('name', name, { 'string', 'table' })
+
+ local names = vim._ensure_list(name) --[[@as string[] ]]
+ for _, nm in ipairs(names) do
+ if nm == '*' then
+ error('Invalid name')
+ end
+ lsp._enabled_configs[nm] = enable ~= false and {} or nil
+ end
+
+ if not next(lsp._enabled_configs) then
+ if lsp_enable_autocmd_id then
+ api.nvim_del_autocmd(lsp_enable_autocmd_id)
+ lsp_enable_autocmd_id = nil
+ end
+ return
+ end
+
+ -- Only ever create autocmd once to reuse computation of config merging.
+ lsp_enable_autocmd_id = lsp_enable_autocmd_id
+ or api.nvim_create_autocmd('FileType', {
+ group = api.nvim_create_augroup('nvim.lsp.enable', {}),
+ callback = function(args)
+ lsp_enable_callback(args.buf)
+ end,
+ })
+end
+
+--- @class vim.lsp.start.Opts
+--- @inlinedoc
+---
+--- Predicate used to decide if a client should be re-used. Used on all
+--- running clients. The default implementation re-uses a client if it has the
+--- same name and if the given workspace folders (or root_dir) are all included
+--- in the client's workspace folders.
+--- @field reuse_client? fun(client: vim.lsp.Client, config: vim.lsp.ClientConfig): boolean
---
--- Buffer handle to attach to if starting or re-using a client (0 for current).
--- @field bufnr? integer
---
+--- Whether to attach the client to a buffer (default true).
+--- If set to `false`, `reuse_client` and `bufnr` will be ignored.
+--- @field attach? boolean
+---
--- Suppress error reporting if the LSP server fails to start (default false).
--- @field silent? boolean
+---
+--- @field package _root_markers? string[]
--- 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`.
@@ -237,10 +602,10 @@ end
--- })
--- ```
---
---- See |vim.lsp.start_client()| for all available options. The most important are:
+--- See |vim.lsp.ClientConfig| for all available options. The most important are:
---
--- - `name` arbitrary name for the LSP client. Should be unique per language server.
---- - `cmd` command string[] or function, described at |vim.lsp.start_client()|.
+--- - `cmd` command string[] or function.
--- - `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.root()| to detect the root by traversing
--- the file system upwards starting from the current directory until either a `pyproject.toml`
@@ -260,36 +625,46 @@ end
--- `ftplugin/<filetype_name>.lua` (See |ftplugin-name|)
---
--- @param config vim.lsp.ClientConfig Configuration for the server.
---- @param opts vim.lsp.start.Opts? Optional keyword arguments
+--- @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 reuse_client_default
- local bufnr = resolve_bufnr(opts.bufnr)
+ local bufnr = vim._resolve_bufnr(opts.bufnr)
+
+ if not config.root_dir and opts._root_markers then
+ config = vim.deepcopy(config)
+ config.root_dir = vim.fs.root(bufnr, opts._root_markers)
+ end
for _, client in pairs(all_clients) do
if reuse_client(client, config) then
+ if opts.attach == false then
+ return client.id
+ end
+
if lsp.buf_attach_client(bufnr, client.id) then
return client.id
- else
- return nil
end
+ return
end
end
- local client_id, err = lsp.start_client(config)
+ local client_id, err = create_and_initialize_client(config)
if err then
if not opts.silent then
vim.notify(err, vim.log.levels.WARN)
end
- return nil
+ return
end
- if client_id and lsp.buf_attach_client(bufnr, client_id) then
+ if opts.attach == false then
return client_id
end
- return nil
+ if client_id and lsp.buf_attach_client(bufnr, client_id) then
+ return client_id
+ end
end
--- Consumes the latest progress messages from all clients and formats them as a string.
@@ -349,17 +724,17 @@ end
---@param bufnr integer
function lsp._set_defaults(client, bufnr)
if
- client.supports_method(ms.textDocument_definition) and is_empty_or_default(bufnr, 'tagfunc')
+ 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')
+ 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)
+ client:supports_method(ms.textDocument_rangeFormatting)
and is_empty_or_default(bufnr, 'formatprg')
and is_empty_or_default(bufnr, 'formatexpr')
then
@@ -367,90 +742,21 @@ function lsp._set_defaults(client, bufnr)
end
vim._with({ buf = bufnr }, function()
if
- client.supports_method(ms.textDocument_hover)
+ 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, desc = 'vim.lsp.buf.hover()' })
+ vim.keymap.set('n', 'K', function()
+ vim.lsp.buf.hover()
+ end, { buffer = bufnr, desc = 'vim.lsp.buf.hover()' })
end
end)
- if client.supports_method(ms.textDocument_diagnostic) then
+ if client:supports_method(ms.textDocument_diagnostic) then
lsp.diagnostic._enable(bufnr)
end
end
---- Reset defaults set by `set_defaults`.
---- Must only be called if the last client attached to a buffer exits.
-local function reset_defaults(bufnr)
- if vim.bo[bufnr].tagfunc == 'v:lua.vim.lsp.tagfunc' then
- vim.bo[bufnr].tagfunc = nil
- end
- if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then
- vim.bo[bufnr].omnifunc = nil
- end
- if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then
- vim.bo[bufnr].formatexpr = nil
- end
- vim._with({ buf = bufnr }, function()
- local keymap = vim.fn.maparg('K', 'n', false, true)
- if keymap and keymap.callback == vim.lsp.buf.hover and keymap.buffer == 1 then
- vim.keymap.del('n', 'K', { buffer = bufnr })
- end
- end)
-end
-
---- @param code integer
---- @param signal integer
---- @param client_id integer
-local function on_client_exit(code, signal, client_id)
- local client = all_clients[client_id]
-
- vim.schedule(function()
- for bufnr in pairs(client.attached_buffers) do
- if client and client.attached_buffers[bufnr] and api.nvim_buf_is_valid(bufnr) then
- api.nvim_exec_autocmds('LspDetach', {
- buffer = bufnr,
- modeline = false,
- data = { client_id = client_id },
- })
- end
-
- client.attached_buffers[bufnr] = nil
-
- if #lsp.get_clients({ bufnr = bufnr, _uninitialized = true }) == 0 then
- reset_defaults(bufnr)
- end
- end
-
- local namespace = vim.lsp.diagnostic.get_namespace(client_id)
- vim.diagnostic.reset(namespace)
- end)
-
- local name = client.name or 'unknown'
-
- -- Schedule the deletion of the client object so that it exists in the execution of LspDetach
- -- autocommands
- vim.schedule(function()
- all_clients[client_id] = nil
-
- -- Client can be absent if executable starts, but initialize fails
- -- init/attach won't have happened
- if client then
- 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. Check log for errors: %s',
- name,
- code,
- signal,
- lsp.get_log_path()
- )
- vim.notify(msg, vim.log.levels.WARN)
- end
- end)
-end
-
+--- @deprecated
--- Starts and initializes a client with the given configuration.
--- @param config vim.lsp.ClientConfig Configuration for the server.
--- @return integer? client_id |vim.lsp.get_client_by_id()| Note: client may not be
@@ -458,39 +764,26 @@ end
--- the client has been initialized.
--- @return string? # Error message, if any
function lsp.start_client(config)
- local ok, res = pcall(require('vim.lsp.client').create, config)
- if not ok then
- return nil, res --[[@as string]]
- end
-
- local client = assert(res)
-
- --- @diagnostic disable-next-line: invisible
- table.insert(client._on_exit_cbs, on_client_exit)
-
- all_clients[client.id] = client
-
- client:initialize()
-
- return client.id, nil
+ vim.deprecate('vim.lsp.start_client()', 'vim.lsp.start()', '0.13')
+ return create_and_initialize_client(config)
end
---Buffer lifecycle handler for textDocument/didSave
--- @param bufnr integer
local function text_document_did_save_handler(bufnr)
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
local uri = vim.uri_from_bufnr(bufnr)
local text = once(lsp._buf_get_full_text)
for _, client in ipairs(lsp.get_clients({ bufnr = bufnr })) do
local name = api.nvim_buf_get_name(bufnr)
local old_name = changetracking._get_and_set_name(client, bufnr, name)
if old_name and name ~= old_name then
- client.notify(ms.textDocument_didClose, {
+ client:notify(ms.textDocument_didClose, {
textDocument = {
uri = vim.uri_from_fname(old_name),
},
})
- client.notify(ms.textDocument_didOpen, {
+ client:notify(ms.textDocument_didOpen, {
textDocument = {
version = 0,
uri = uri,
@@ -506,7 +799,7 @@ local function text_document_did_save_handler(bufnr)
if type(save_capability) == 'table' and save_capability.includeText then
included_text = text(bufnr)
end
- client.notify(ms.textDocument_didSave, {
+ client:notify(ms.textDocument_didSave, {
textDocument = {
uri = uri,
},
@@ -527,10 +820,10 @@ local function buf_detach_client(bufnr, client)
changetracking.reset_buf(client, bufnr)
- if client.supports_method(ms.textDocument_didClose) then
+ if client:supports_method(ms.textDocument_didClose) then
local uri = vim.uri_from_bufnr(bufnr)
local params = { textDocument = { uri = uri } }
- client.notify(ms.textDocument_didClose, params)
+ client:notify(ms.textDocument_didClose, params)
end
client.attached_buffers[bufnr] = nil
@@ -550,7 +843,7 @@ local function buf_attach(bufnr)
attached_buffers[bufnr] = true
local uri = vim.uri_from_bufnr(bufnr)
- local augroup = ('lsp_b_%d_save'):format(bufnr)
+ local augroup = ('nvim.lsp.b_%d_save'):format(bufnr)
local group = api.nvim_create_augroup(augroup, { clear = true })
api.nvim_create_autocmd('BufWritePre', {
group = group,
@@ -564,12 +857,12 @@ local function buf_attach(bufnr)
},
reason = protocol.TextDocumentSaveReason.Manual, ---@type integer
}
- if client.supports_method(ms.textDocument_willSave) then
- client.notify(ms.textDocument_willSave, params)
+ if client:supports_method(ms.textDocument_willSave) then
+ client:notify(ms.textDocument_willSave, params)
end
- if client.supports_method(ms.textDocument_willSaveWaitUntil) then
+ if client:supports_method(ms.textDocument_willSaveWaitUntil) then
local result, err =
- client.request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf)
+ client:request_sync(ms.textDocument_willSaveWaitUntil, params, 1000, ctx.buf)
if result and result.result then
util.apply_text_edits(result.result, ctx.buf, client.offset_encoding)
elseif err then
@@ -603,8 +896,8 @@ local function buf_attach(bufnr)
local params = { textDocument = { uri = uri } }
for _, client in ipairs(clients) do
changetracking.reset_buf(client, bufnr)
- if client.supports_method(ms.textDocument_didClose) then
- client.notify(ms.textDocument_didClose, params)
+ if client:supports_method(ms.textDocument_didClose) then
+ client:notify(ms.textDocument_didClose, params)
end
end
for _, client in ipairs(clients) do
@@ -639,7 +932,7 @@ end
function lsp.buf_attach_client(bufnr, client_id)
validate('bufnr', bufnr, 'number', true)
validate('client_id', client_id, 'number')
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
if not api.nvim_buf_is_loaded(bufnr) then
log.warn(string.format('buf_attach_client called on unloaded buffer (id: %d): ', bufnr))
return false
@@ -662,7 +955,7 @@ function lsp.buf_attach_client(bufnr, client_id)
-- Send didOpen for the client if it is initialized. If it isn't initialized
-- then it will send didOpen on initialize.
if client.initialized then
- client:_on_attach(bufnr)
+ client:on_attach(bufnr)
end
return true
end
@@ -676,7 +969,7 @@ end
function lsp.buf_detach_client(bufnr, client_id)
validate('bufnr', bufnr, 'number', true)
validate('client_id', client_id, 'number')
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
local client = all_clients[client_id]
if not client or not client.attached_buffers[bufnr] then
@@ -740,13 +1033,13 @@ function lsp.stop_client(client_id, force)
for _, id in ipairs(ids) do
if type(id) == 'table' then
if id.stop then
- id.stop(force)
+ id:stop(force)
end
else
--- @cast id -vim.lsp.Client
local client = all_clients[id]
if client then
- client.stop(force)
+ client:stop(force)
end
end
end
@@ -782,7 +1075,7 @@ function lsp.get_clients(filter)
local clients = {} --- @type vim.lsp.Client[]
- local bufnr = filter.bufnr and resolve_bufnr(filter.bufnr)
+ local bufnr = filter.bufnr and vim._resolve_bufnr(filter.bufnr)
for _, client in pairs(all_clients) do
if
@@ -790,7 +1083,7 @@ function lsp.get_clients(filter)
and (filter.id == nil or client.id == filter.id)
and (filter.bufnr == nil or client.attached_buffers[bufnr])
and (filter.name == nil or client.name == filter.name)
- and (filter.method == nil or client.supports_method(filter.method, { bufnr = filter.bufnr }))
+ and (filter.method == nil or client:supports_method(filter.method, filter.bufnr))
and (filter._uninitialized or client.initialized)
then
clients[#clients + 1] = client
@@ -812,7 +1105,7 @@ api.nvim_create_autocmd('VimLeavePre', {
local active_clients = lsp.get_clients()
log.info('exit_handler', active_clients)
for _, client in pairs(all_clients) do
- client.stop()
+ client:stop()
end
local timeouts = {} --- @type table<integer,integer>
@@ -847,7 +1140,7 @@ api.nvim_create_autocmd('VimLeavePre', {
if not vim.wait(max_timeout, check_clients_closed, poll_time) then
for client_id, client in pairs(active_clients) do
if timeouts[client_id] ~= nil then
- client.stop(true)
+ client:stop(true)
end
end
end
@@ -878,16 +1171,16 @@ function lsp.buf_request(bufnr, method, params, handler, on_unsupported)
validate('handler', handler, 'function', true)
validate('on_unsupported', on_unsupported, 'function', true)
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
local method_supported = false
local clients = lsp.get_clients({ bufnr = bufnr })
local client_request_ids = {} --- @type table<integer,integer>
for _, client in ipairs(clients) do
- if client.supports_method(method, { bufnr = bufnr }) then
+ if client:supports_method(method, bufnr) then
method_supported = true
local cparams = type(params) == 'function' and params(client, bufnr) or params --[[@as table?]]
- local request_success, request_id = client.request(method, cparams, handler, bufnr)
+ local request_success, request_id = client:request(method, cparams, handler, bufnr)
-- This could only fail if the client shut down in the time since we looked
-- it up and we did the request, which should be rare.
if request_success then
@@ -910,7 +1203,7 @@ function lsp.buf_request(bufnr, method, params, handler, on_unsupported)
local function _cancel_all_requests()
for client_id, request_id in pairs(client_request_ids) do
local client = all_clients[client_id]
- client.cancel_request(request_id)
+ client:cancel_request(request_id)
end
end
@@ -1049,7 +1342,7 @@ function lsp.formatexpr(opts)
end
local bufnr = api.nvim_get_current_buf()
for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do
- if client.supports_method(ms.textDocument_rangeFormatting) then
+ if client:supports_method(ms.textDocument_rangeFormatting) then
local params = util.make_formatting_params()
local end_line = vim.fn.getline(end_lnum) --[[@as string]]
local end_col = vim.str_utfindex(end_line, client.offset_encoding)
@@ -1065,7 +1358,7 @@ function lsp.formatexpr(opts)
},
}
local response =
- client.request_sync(ms.textDocument_rangeFormatting, params, timeout_ms, bufnr)
+ client:request_sync(ms.textDocument_rangeFormatting, params, timeout_ms, bufnr)
if response and response.result then
lsp.util.apply_text_edits(response.result, bufnr, client.offset_encoding)
return 0
@@ -1092,6 +1385,55 @@ function lsp.tagfunc(pattern, flags)
return vim.lsp._tagfunc(pattern, flags)
end
+--- Provides an interface between the built-in client and a `foldexpr` function.
+---
+--- To use, check for the "textDocument/foldingRange" capability in an
+--- |LspAttach| autocommand. Example:
+---
+--- ```lua
+--- vim.api.nvim_create_autocmd('LspAttach', {
+--- callback = function(args)
+--- local client = vim.lsp.get_client_by_id(args.data.client_id)
+--- if client:supports_method('textDocument/foldingRange') then
+--- local win = vim.api.nvim_get_current_win()
+--- vim.wo[win][0].foldmethod = 'expr'
+--- vim.wo[win][0].foldexpr = 'v:lua.vim.lsp.foldexpr()'
+--- end
+--- end,
+--- })
+--- ```
+---
+---@param lnum integer line number
+function lsp.foldexpr(lnum)
+ return vim.lsp._folding_range.foldexpr(lnum)
+end
+
+--- Close all {kind} of folds in the the window with {winid}.
+---
+--- To automatically fold imports when opening a file, you can use an autocmd:
+---
+--- ```lua
+--- vim.api.nvim_create_autocmd('LspNotify', {
+--- callback = function(args)
+--- if args.data.method == 'textDocument/didOpen' then
+--- vim.lsp.foldclose('imports', vim.fn.bufwinid(args.buf))
+--- end
+--- end,
+--- })
+--- ```
+---
+---@param kind lsp.FoldingRangeKind Kind to close, one of "comment", "imports" or "region".
+---@param winid? integer Defaults to the current window.
+function lsp.foldclose(kind, winid)
+ return vim.lsp._folding_range.foldclose(kind, winid)
+end
+
+--- Provides a `foldtext` function that shows the `collapsedText` retrieved,
+--- defaults to the first folded line if `collapsedText` is not provided.
+function lsp.foldtext()
+ return vim.lsp._folding_range.foldtext()
+end
+
---Checks whether a client is stopped.
---
---@param client_id (integer)
@@ -1110,7 +1452,7 @@ end
function lsp.buf_get_clients(bufnr)
vim.deprecate('vim.lsp.buf_get_clients()', 'vim.lsp.get_clients()', '0.12')
local result = {} --- @type table<integer,vim.lsp.Client>
- for _, client in ipairs(lsp.get_clients({ bufnr = resolve_bufnr(bufnr) })) do
+ for _, client in ipairs(lsp.get_clients({ bufnr = vim._resolve_bufnr(bufnr) })) do
result[client.id] = client
end
return result
@@ -1164,7 +1506,7 @@ function lsp.for_each_buffer_client(bufnr, fn)
'lsp.get_clients({ bufnr = bufnr }) with regular loop',
'0.12'
)
- bufnr = resolve_bufnr(bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do
fn(client, client.id, bufnr)
@@ -1181,44 +1523,6 @@ function lsp.with(handler, override_config)
end
end
---- Helper function to use when implementing a handler.
---- This will check that all of the keys in the user configuration
---- are valid keys and make sense to include for this handler.
----
---- Will error on invalid keys (i.e. keys that do not exist in the options)
---- @param name string
---- @param options table<string,any>
---- @param user_config table<string,any>
-function lsp._with_extend(name, options, user_config)
- user_config = user_config or {}
-
- local resulting_config = {} --- @type table<string,any>
- for k, v in pairs(user_config) do
- if options[k] == nil then
- error(
- debug.traceback(
- string.format(
- 'Invalid option for `%s`: %s. Valid options are:\n%s',
- name,
- k,
- vim.inspect(vim.tbl_keys(options))
- )
- )
- )
- end
-
- resulting_config[k] = v
- end
-
- for k, v in pairs(options) do
- if resulting_config[k] == nil then
- resulting_config[k] = v
- end
- end
-
- return resulting_config
-end
-
--- Registry for client side commands.
--- This is an extension point for plugins to handle custom commands which are
--- not part of the core language server protocol specification.
@@ -1227,7 +1531,7 @@ end
--- and the value is a function which is called if any LSP action
--- (code action, code lenses, ...) triggers the command.
---
---- If a LSP response contains a command for which no matching entry is
+--- If an LSP response contains a command for which no matching entry is
--- available in this registry, the command will be executed via the LSP server
--- using `workspace/executeCommand`.
---