aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
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
parentc5d770d311841ea5230426cc4c868e8db27300a8 (diff)
parent44740e561fc93afe3ebecfd3618bda2d2abeafb0 (diff)
downloadrneovim-rahm.tar.gz
rneovim-rahm.tar.bz2
rneovim-rahm.zip
Merge remote-tracking branch 'upstream/master' into mix_20240309HEADrahm
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/_changetracking.lua18
-rw-r--r--runtime/lua/vim/lsp/_folding_range.lua373
-rw-r--r--runtime/lua/vim/lsp/_meta.lua4
-rw-r--r--runtime/lua/vim/lsp/_snippet_grammar.lua1
-rw-r--r--runtime/lua/vim/lsp/_tagfunc.lua18
-rw-r--r--runtime/lua/vim/lsp/_transport.lua182
-rw-r--r--runtime/lua/vim/lsp/_watchfiles.lua3
-rw-r--r--runtime/lua/vim/lsp/buf.lua98
-rw-r--r--runtime/lua/vim/lsp/client.lua411
-rw-r--r--runtime/lua/vim/lsp/codelens.lua14
-rw-r--r--runtime/lua/vim/lsp/completion.lua43
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua29
-rw-r--r--runtime/lua/vim/lsp/handlers.lua20
-rw-r--r--runtime/lua/vim/lsp/health.lua77
-rw-r--r--runtime/lua/vim/lsp/inlay_hint.lua29
-rw-r--r--runtime/lua/vim/lsp/protocol.lua15
-rw-r--r--runtime/lua/vim/lsp/rpc.lua290
-rw-r--r--runtime/lua/vim/lsp/semantic_tokens.lua35
-rw-r--r--runtime/lua/vim/lsp/sync.lua52
-rw-r--r--runtime/lua/vim/lsp/util.lua222
20 files changed, 1219 insertions, 715 deletions
diff --git a/runtime/lua/vim/lsp/_changetracking.lua b/runtime/lua/vim/lsp/_changetracking.lua
index b2be53269f..265a74c8fa 100644
--- a/runtime/lua/vim/lsp/_changetracking.lua
+++ b/runtime/lua/vim/lsp/_changetracking.lua
@@ -18,14 +18,14 @@ local M = {}
---
--- None: One group for all clients
--- Full: One group for all clients
---- Incremental: One group per `offset_encoding`
+--- Incremental: One group per `position_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 vim.lsp.CTGroup
--- @field sync_kind integer TextDocumentSyncKind, considers config.flags.allow_incremental_sync
---- @field offset_encoding "utf-8"|"utf-16"|"utf-32"
+--- @field position_encoding "utf-8"|"utf-16"|"utf-32"
---
--- @class vim.lsp.CTBufferState
--- @field name string name of the buffer
@@ -40,13 +40,13 @@ local M = {}
--- @class vim.lsp.CTGroupState
--- @field buffers table<integer,vim.lsp.CTBufferState>
--- @field debounce integer debounce duration in ms
---- @field clients table<integer, table> clients using this state. {client_id, client}
+--- @field clients table<integer, vim.lsp.Client> clients using this state. {client_id, client}
---@param group vim.lsp.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
+ return tostring(group.sync_kind) .. '\0' .. group.position_encoding
end
return tostring(group.sync_kind)
end
@@ -64,7 +64,7 @@ local state_by_group = setmetatable({}, {
---@param client vim.lsp.Client
---@return vim.lsp.CTGroup
local function get_group(client)
- local allow_inc_sync = vim.F.if_nil(client.flags.allow_incremental_sync, true) --- @type boolean
+ local allow_inc_sync = vim.F.if_nil(client.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
@@ -72,7 +72,7 @@ local function get_group(client)
end
return {
sync_kind = sync_kind,
- offset_encoding = client.offset_encoding,
+ position_encoding = client.offset_encoding,
}
end
@@ -273,8 +273,8 @@ local function send_changes(bufnr, sync_kind, state, buf_state)
end
local uri = vim.uri_from_bufnr(bufnr)
for _, client in pairs(state.clients) do
- if not client.is_stopped() and vim.lsp.buf_is_attached(bufnr, client.id) then
- client.notify(protocol.Methods.textDocument_didChange, {
+ if not client:is_stopped() and vim.lsp.buf_is_attached(bufnr, client.id) then
+ client:notify(protocol.Methods.textDocument_didChange, {
textDocument = {
uri = uri,
version = util.buf_versions[bufnr],
@@ -310,7 +310,7 @@ local function send_changes_for_group(bufnr, firstline, lastline, new_lastline,
-- The contents would further change and startline/endline may no longer fit
local changes = incremental_changes(
buf_state,
- group.offset_encoding,
+ group.position_encoding,
bufnr,
firstline,
lastline,
diff --git a/runtime/lua/vim/lsp/_folding_range.lua b/runtime/lua/vim/lsp/_folding_range.lua
new file mode 100644
index 0000000000..66eb81db6e
--- /dev/null
+++ b/runtime/lua/vim/lsp/_folding_range.lua
@@ -0,0 +1,373 @@
+local util = require('vim.lsp.util')
+local log = require('vim.lsp.log')
+local ms = require('vim.lsp.protocol').Methods
+local api = vim.api
+
+local M = {}
+
+---@class (private) vim.lsp.folding_range.BufState
+---
+---@field version? integer
+---
+--- Never use this directly, `renew()` the cached foldinfo
+--- then use on demand via `row_*` fields.
+---
+--- Index In the form of client_id -> ranges
+---@field client_ranges table<integer, lsp.FoldingRange[]?>
+---
+--- Index in the form of row -> [foldlevel, mark]
+---@field row_level table<integer, [integer, ">" | "<"?]?>
+---
+--- Index in the form of start_row -> kinds
+---@field row_kinds table<integer, table<lsp.FoldingRangeKind, true?>?>>
+---
+--- Index in the form of start_row -> collapsed_text
+---@field row_text table<integer, string?>
+
+---@type table<integer, vim.lsp.folding_range.BufState?>
+local bufstates = {}
+
+--- Renew the cached foldinfo in the buffer.
+---@param bufnr integer
+local function renew(bufnr)
+ local bufstate = assert(bufstates[bufnr])
+
+ ---@type table<integer, [integer, ">" | "<"?]?>
+ local row_level = {}
+ ---@type table<integer, table<lsp.FoldingRangeKind, true?>?>>
+ local row_kinds = {}
+ ---@type table<integer, string?>
+ local row_text = {}
+
+ for _, ranges in pairs(bufstate.client_ranges) do
+ for _, range in ipairs(ranges) do
+ local start_row = range.startLine
+ local end_row = range.endLine
+ -- Adding folds within a single line is not supported by Nvim.
+ if start_row ~= end_row then
+ row_text[start_row] = range.collapsedText
+
+ local kind = range.kind
+ if kind then
+ local kinds = row_kinds[start_row] or {}
+ kinds[kind] = true
+ row_kinds[start_row] = kinds
+ end
+
+ for row = start_row, end_row do
+ local level = row_level[row] or { 0 }
+ level[1] = level[1] + 1
+ row_level[row] = level
+ end
+ row_level[start_row][2] = '>'
+ row_level[end_row][2] = '<'
+ end
+ end
+ end
+
+ bufstate.row_level = row_level
+ bufstate.row_kinds = row_kinds
+ bufstate.row_text = row_text
+end
+
+--- Renew the cached foldinfo then force `foldexpr()` to be re-evaluated,
+--- without opening folds.
+---@param bufnr integer
+local function foldupdate(bufnr)
+ renew(bufnr)
+ for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do
+ local wininfo = vim.fn.getwininfo(winid)[1]
+ if wininfo and wininfo.tabnr == vim.fn.tabpagenr() then
+ if vim.wo[winid].foldmethod == 'expr' then
+ vim._foldupdate(winid, 0, api.nvim_buf_line_count(bufnr))
+ end
+ end
+ end
+end
+
+--- Whether `foldupdate()` is scheduled for the buffer with `bufnr`.
+---
+--- Index in the form of bufnr -> true?
+---@type table<integer, true?>
+local scheduled_foldupdate = {}
+
+--- Schedule `foldupdate()` after leaving insert mode.
+---@param bufnr integer
+local function schedule_foldupdate(bufnr)
+ if not scheduled_foldupdate[bufnr] then
+ scheduled_foldupdate[bufnr] = true
+ api.nvim_create_autocmd('InsertLeave', {
+ buffer = bufnr,
+ once = true,
+ callback = function()
+ foldupdate(bufnr)
+ scheduled_foldupdate[bufnr] = nil
+ end,
+ })
+ end
+end
+
+---@param results table<integer,{err: lsp.ResponseError?, result: lsp.FoldingRange[]?}>
+---@type lsp.MultiHandler
+local function multi_handler(results, ctx)
+ local bufnr = assert(ctx.bufnr)
+ -- Handling responses from outdated buffer only causes performance overhead.
+ if util.buf_versions[bufnr] ~= ctx.version then
+ return
+ end
+
+ local bufstate = assert(bufstates[bufnr])
+ for client_id, result in pairs(results) do
+ if result.err then
+ log.error(result.err)
+ else
+ bufstate.client_ranges[client_id] = result.result
+ end
+ end
+ bufstate.version = ctx.version
+
+ if api.nvim_get_mode().mode:match('^i') then
+ -- `foldUpdate()` is guarded in insert mode.
+ schedule_foldupdate(bufnr)
+ else
+ foldupdate(bufnr)
+ end
+end
+
+---@param result lsp.FoldingRange[]?
+---@type lsp.Handler
+local function handler(err, result, ctx)
+ multi_handler({ [ctx.client_id] = { err = err, result = result } }, ctx)
+end
+
+--- Request `textDocument/foldingRange` from the server.
+--- `foldupdate()` is scheduled once after the request is completed.
+---@param bufnr integer
+---@param client? vim.lsp.Client The client whose server supports `foldingRange`.
+local function request(bufnr, client)
+ ---@type lsp.FoldingRangeParams
+ local params = { textDocument = util.make_text_document_params(bufnr) }
+
+ if client then
+ client:request(ms.textDocument_foldingRange, params, handler, bufnr)
+ return
+ end
+
+ if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then
+ return
+ end
+
+ vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, multi_handler)
+end
+
+-- NOTE:
+-- `bufstate` and event hooks are interdependent:
+-- * `bufstate` needs event hooks for correctness.
+-- * event hooks require the previous `bufstate` for updates.
+-- Since they are manually created and destroyed,
+-- we ensure their lifecycles are always synchronized.
+--
+-- TODO(ofseed):
+-- 1. Implement clearing `bufstate` and event hooks
+-- when no clients in the buffer support the corresponding method.
+-- 2. Then generalize this state management to other LSP modules.
+local augroup_setup = api.nvim_create_augroup('nvim.lsp.folding_range.setup', {})
+
+--- Initialize `bufstate` and event hooks, then request folding ranges.
+--- Manage their lifecycle within this function.
+---@param bufnr integer
+---@return vim.lsp.folding_range.BufState?
+local function setup(bufnr)
+ if not api.nvim_buf_is_loaded(bufnr) then
+ return
+ end
+
+ -- Register the new `bufstate`.
+ bufstates[bufnr] = {
+ client_ranges = {},
+ row_level = {},
+ row_kinds = {},
+ row_text = {},
+ }
+
+ -- Event hooks from `buf_attach` can't be removed externally.
+ -- Hooks and `bufstate` share the same lifecycle;
+ -- they should self-destroy if `bufstate == nil`.
+ api.nvim_buf_attach(bufnr, false, {
+ -- `on_detach` also runs on buffer reload (`:e`).
+ -- Ensure `bufstate` and hooks are cleared to avoid duplication or leftover states.
+ on_detach = function()
+ bufstates[bufnr] = nil
+ api.nvim_clear_autocmds({ buffer = bufnr, group = augroup_setup })
+ end,
+ -- Reset `bufstate` and request folding ranges.
+ on_reload = function()
+ bufstates[bufnr] = {
+ client_ranges = {},
+ row_level = {},
+ row_kinds = {},
+ row_text = {},
+ }
+ request(bufnr)
+ end,
+ --- Sync changed rows with their previous foldlevels before applying new ones.
+ on_bytes = function(_, _, _, start_row, _, _, old_row, _, _, new_row, _, _)
+ if bufstates[bufnr] == nil then
+ return true
+ end
+ local row_level = bufstates[bufnr].row_level
+ if next(row_level) == nil then
+ return
+ end
+ local row = new_row - old_row
+ if row > 0 then
+ vim._list_insert(row_level, start_row, start_row + math.abs(row) - 1, { -1 })
+ -- If the previous row ends a fold,
+ -- Nvim treats the first row after consecutive `-1`s as a new fold start,
+ -- which is not the desired behavior.
+ local prev_level = row_level[start_row - 1]
+ if prev_level and prev_level[2] == '<' then
+ row_level[start_row] = { prev_level[1] - 1 }
+ end
+ elseif row < 0 then
+ vim._list_remove(row_level, start_row, start_row + math.abs(row) - 1)
+ end
+ end,
+ })
+ api.nvim_create_autocmd('LspDetach', {
+ group = augroup_setup,
+ buffer = bufnr,
+ callback = function(args)
+ if not api.nvim_buf_is_loaded(bufnr) then
+ return
+ end
+
+ ---@type integer
+ local client_id = args.data.client_id
+ bufstates[bufnr].client_ranges[client_id] = nil
+
+ ---@type vim.lsp.Client[]
+ local clients = vim
+ .iter(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange }))
+ ---@param client vim.lsp.Client
+ :filter(function(client)
+ return client.id ~= client_id
+ end)
+ :totable()
+ if #clients == 0 then
+ bufstates[bufnr] = {
+ client_ranges = {},
+ row_level = {},
+ row_kinds = {},
+ row_text = {},
+ }
+ end
+
+ foldupdate(bufnr)
+ end,
+ })
+ api.nvim_create_autocmd('LspAttach', {
+ group = augroup_setup,
+ buffer = bufnr,
+ callback = function(args)
+ local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
+ if client:supports_method(vim.lsp.protocol.Methods.textDocument_foldingRange, bufnr) then
+ request(bufnr, client)
+ end
+ end,
+ })
+ api.nvim_create_autocmd('LspNotify', {
+ group = augroup_setup,
+ buffer = bufnr,
+ callback = function(args)
+ local client = assert(vim.lsp.get_client_by_id(args.data.client_id))
+ if
+ client:supports_method(ms.textDocument_foldingRange, bufnr)
+ and (
+ args.data.method == ms.textDocument_didChange
+ or args.data.method == ms.textDocument_didOpen
+ )
+ then
+ request(bufnr, client)
+ end
+ end,
+ })
+
+ request(bufnr)
+
+ return bufstates[bufnr]
+end
+
+---@param kind lsp.FoldingRangeKind
+---@param winid integer
+local function foldclose(kind, winid)
+ vim._with({ win = winid }, function()
+ local bufnr = api.nvim_win_get_buf(winid)
+ local row_kinds = bufstates[bufnr].row_kinds
+ -- Reverse traverse to ensure that the smallest ranges are closed first.
+ for row = api.nvim_buf_line_count(bufnr) - 1, 0, -1 do
+ local kinds = row_kinds[row]
+ if kinds and kinds[kind] then
+ vim.cmd(row + 1 .. 'foldclose')
+ end
+ end
+ end)
+end
+
+---@param kind lsp.FoldingRangeKind
+---@param winid? integer
+function M.foldclose(kind, winid)
+ vim.validate('kind', kind, 'string')
+ vim.validate('winid', winid, 'number', true)
+
+ winid = winid or api.nvim_get_current_win()
+ local bufnr = api.nvim_win_get_buf(winid)
+ local bufstate = bufstates[bufnr]
+ if not bufstate then
+ return
+ end
+
+ if bufstate.version == util.buf_versions[bufnr] then
+ foldclose(kind, winid)
+ return
+ end
+ -- Schedule `foldclose()` if the buffer is not up-to-date.
+
+ if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then
+ return
+ end
+ ---@type lsp.FoldingRangeParams
+ local params = { textDocument = util.make_text_document_params(bufnr) }
+ vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, function(...)
+ multi_handler(...)
+ foldclose(kind, winid)
+ end)
+end
+
+---@return string
+function M.foldtext()
+ local bufnr = api.nvim_get_current_buf()
+ local lnum = vim.v.foldstart
+ local row = lnum - 1
+ local bufstate = bufstates[bufnr]
+ if bufstate and bufstate.row_text[row] then
+ return bufstate.row_text[row]
+ end
+ return vim.fn.getline(lnum)
+end
+
+---@param lnum? integer
+---@return string level
+function M.foldexpr(lnum)
+ local bufnr = api.nvim_get_current_buf()
+ local bufstate = bufstates[bufnr] or setup(bufnr)
+ if not bufstate then
+ return '0'
+ end
+
+ local row = (lnum or vim.v.lnum) - 1
+ local level = bufstate.row_level[row]
+ return level and (level[2] or '') .. (level[1] or '0') or '0'
+end
+
+return M
diff --git a/runtime/lua/vim/lsp/_meta.lua b/runtime/lua/vim/lsp/_meta.lua
index bf693ccc57..589a49c003 100644
--- a/runtime/lua/vim/lsp/_meta.lua
+++ b/runtime/lua/vim/lsp/_meta.lua
@@ -1,8 +1,8 @@
---@meta
error('Cannot require a meta file')
----@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext): ...any
----@alias lsp.MultiHandler fun(results: table<integer,{err: lsp.ResponseError?, result: any}>, context: lsp.HandlerContext): ...any
+---@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext, config?: table): ...any
+---@alias lsp.MultiHandler fun(results: table<integer,{err: lsp.ResponseError?, result: any}>, context: lsp.HandlerContext, config?: table): ...any
---@class lsp.HandlerContext
---@field method string
diff --git a/runtime/lua/vim/lsp/_snippet_grammar.lua b/runtime/lua/vim/lsp/_snippet_grammar.lua
index 9318fefcbc..f06d6e9afd 100644
--- a/runtime/lua/vim/lsp/_snippet_grammar.lua
+++ b/runtime/lua/vim/lsp/_snippet_grammar.lua
@@ -127,6 +127,7 @@ local function node(type)
end
-- stylua: ignore
+--- @diagnostic disable-next-line: missing-fields
local G = P({
'snippet';
snippet = Ct(Cg(
diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua
index f75d43f373..554f0cb991 100644
--- a/runtime/lua/vim/lsp/_tagfunc.lua
+++ b/runtime/lua/vim/lsp/_tagfunc.lua
@@ -6,12 +6,12 @@ local ms = lsp.protocol.Methods
---@param name string
---@param range lsp.Range
---@param uri string
----@param offset_encoding string
+---@param position_encoding string
---@return {name: string, filename: string, cmd: string, kind?: string}
-local function mk_tag_item(name, range, uri, offset_encoding)
+local function mk_tag_item(name, range, uri, position_encoding)
local bufnr = vim.uri_to_bufnr(uri)
-- This is get_line_byte_from_position is 0-indexed, call cursor expects a 1-indexed position
- local byte = util._get_line_byte_from_position(bufnr, range.start, offset_encoding) + 1
+ local byte = util._get_line_byte_from_position(bufnr, range.start, position_encoding) + 1
return {
name = name,
filename = vim.uri_to_fname(uri),
@@ -32,9 +32,9 @@ local function query_definition(pattern)
--- @param range lsp.Range
--- @param uri string
- --- @param offset_encoding string
- local add = function(range, uri, offset_encoding)
- table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding))
+ --- @param position_encoding string
+ local add = function(range, uri, position_encoding)
+ table.insert(results, mk_tag_item(pattern, range, uri, position_encoding))
end
local remaining = #clients
@@ -59,7 +59,7 @@ local function query_definition(pattern)
remaining = remaining - 1
end
local params = util.make_position_params(win, client.offset_encoding)
- client.request(ms.textDocument_definition, params, on_response, bufnr)
+ client:request(ms.textDocument_definition, params, on_response, bufnr)
end
vim.wait(1000, function()
return remaining == 0
@@ -78,11 +78,11 @@ local function query_workspace_symbols(pattern)
local results = {}
for client_id, responses in pairs(assert(results_by_client)) do
local client = lsp.get_client_by_id(client_id)
- local offset_encoding = client and client.offset_encoding or 'utf-16'
+ local position_encoding = client and client.offset_encoding or 'utf-16'
local symbols = responses.result --[[@as lsp.SymbolInformation[]|nil]]
for _, symbol in pairs(symbols or {}) do
local loc = symbol.location
- local item = mk_tag_item(symbol.name, loc.range, loc.uri, offset_encoding)
+ local item = mk_tag_item(symbol.name, loc.range, loc.uri, position_encoding)
item.kind = lsp.protocol.SymbolKind[symbol.kind] or 'Unknown'
table.insert(results, item)
end
diff --git a/runtime/lua/vim/lsp/_transport.lua b/runtime/lua/vim/lsp/_transport.lua
new file mode 100644
index 0000000000..19ff2a8ab0
--- /dev/null
+++ b/runtime/lua/vim/lsp/_transport.lua
@@ -0,0 +1,182 @@
+local uv = vim.uv
+local log = require('vim.lsp.log')
+
+local is_win = vim.fn.has('win32') == 1
+
+--- Checks whether a given path exists and is a directory.
+---@param filename string path to check
+---@return boolean
+local function is_dir(filename)
+ local stat = uv.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+end
+
+--- @class (private) vim.lsp.rpc.Transport
+--- @field write fun(self: vim.lsp.rpc.Transport, msg: string)
+--- @field is_closing fun(self: vim.lsp.rpc.Transport): boolean
+--- @field terminate fun(self: vim.lsp.rpc.Transport)
+
+--- @class (private,exact) vim.lsp.rpc.Transport.Run : vim.lsp.rpc.Transport
+--- @field new fun(): vim.lsp.rpc.Transport.Run
+--- @field sysobj? vim.SystemObj
+local TransportRun = {}
+
+--- @return vim.lsp.rpc.Transport.Run
+function TransportRun.new()
+ return setmetatable({}, { __index = TransportRun })
+end
+
+--- @param cmd string[] Command to start the LSP server.
+--- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams
+--- @param on_read fun(err: any, data: string)
+--- @param on_exit fun(code: integer, signal: integer)
+function TransportRun:run(cmd, extra_spawn_params, on_read, on_exit)
+ local function on_stderr(_, chunk)
+ if chunk then
+ log.error('rpc', cmd[1], 'stderr', chunk)
+ end
+ end
+
+ extra_spawn_params = extra_spawn_params or {}
+
+ if extra_spawn_params.cwd then
+ assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
+ end
+
+ local detached = not is_win
+ if extra_spawn_params.detached ~= nil then
+ detached = extra_spawn_params.detached
+ end
+
+ local ok, sysobj_or_err = pcall(vim.system, cmd, {
+ stdin = true,
+ stdout = on_read,
+ stderr = on_stderr,
+ cwd = extra_spawn_params.cwd,
+ env = extra_spawn_params.env,
+ detach = detached,
+ }, function(obj)
+ on_exit(obj.code, obj.signal)
+ end)
+
+ if not ok then
+ local err = sysobj_or_err --[[@as string]]
+ local sfx = err:match('ENOENT')
+ and '. The language server is either not installed, missing from PATH, or not executable.'
+ or string.format(' with error message: %s', err)
+
+ error(('Spawning language server with cmd: `%s` failed%s'):format(vim.inspect(cmd), sfx))
+ end
+
+ self.sysobj = sysobj_or_err --[[@as vim.SystemObj]]
+end
+
+function TransportRun:write(msg)
+ assert(self.sysobj):write(msg)
+end
+
+function TransportRun:is_closing()
+ return self.sysobj == nil or self.sysobj:is_closing()
+end
+
+function TransportRun:terminate()
+ assert(self.sysobj):kill(15)
+end
+
+--- @class (private,exact) vim.lsp.rpc.Transport.Connect : vim.lsp.rpc.Transport
+--- @field new fun(): vim.lsp.rpc.Transport.Connect
+--- @field handle? uv.uv_pipe_t|uv.uv_tcp_t
+--- Connect returns a PublicClient synchronously so the caller
+--- can immediately send messages before the connection is established
+--- -> Need to buffer them until that happens
+--- @field connected boolean
+--- @field closing boolean
+--- @field msgbuf vim.Ringbuf
+--- @field on_exit? fun(code: integer, signal: integer)
+local TransportConnect = {}
+
+--- @return vim.lsp.rpc.Transport.Connect
+function TransportConnect.new()
+ return setmetatable({
+ connected = false,
+ -- size should be enough because the client can't really do anything until initialization is done
+ -- which required a response from the server - implying the connection got established
+ msgbuf = vim.ringbuf(10),
+ closing = false,
+ }, { __index = TransportConnect })
+end
+
+--- @param host_or_path string
+--- @param port? integer
+--- @param on_read fun(err: any, data: string)
+--- @param on_exit? fun(code: integer, signal: integer)
+function TransportConnect:connect(host_or_path, port, on_read, on_exit)
+ self.on_exit = on_exit
+ self.handle = (
+ port and assert(uv.new_tcp(), 'Could not create new TCP socket')
+ or assert(uv.new_pipe(false), 'Pipe could not be opened.')
+ )
+
+ local function on_connect(err)
+ if err then
+ local address = not port and host_or_path or (host_or_path .. ':' .. port)
+ vim.schedule(function()
+ vim.notify(
+ string.format('Could not connect to %s, reason: %s', address, vim.inspect(err)),
+ vim.log.levels.WARN
+ )
+ end)
+ return
+ end
+ self.handle:read_start(on_read)
+ self.connected = true
+ for msg in self.msgbuf do
+ self.handle:write(msg)
+ end
+ end
+
+ if not port then
+ self.handle:connect(host_or_path, on_connect)
+ return
+ end
+
+ --- @diagnostic disable-next-line:param-type-mismatch bad UV typing
+ local info = uv.getaddrinfo(host_or_path, nil)
+ local resolved_host = info and info[1] and info[1].addr or host_or_path
+ self.handle:connect(resolved_host, port, on_connect)
+end
+
+function TransportConnect:write(msg)
+ if self.connected then
+ local _, err = self.handle:write(msg)
+ if err and not self.closing then
+ log.error('Error on handle:write: %q', err)
+ end
+ return
+ end
+
+ self.msgbuf:push(msg)
+end
+
+function TransportConnect:is_closing()
+ return self.closing
+end
+
+function TransportConnect:terminate()
+ if self.closing then
+ return
+ end
+ self.closing = true
+ if self.handle then
+ self.handle:shutdown()
+ self.handle:close()
+ end
+ if self.on_exit then
+ self.on_exit(0, 0)
+ end
+end
+
+return {
+ TransportRun = TransportRun,
+ TransportConnect = TransportConnect,
+}
diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua
index c4cdb5aea8..4711b3cc9b 100644
--- a/runtime/lua/vim/lsp/_watchfiles.lua
+++ b/runtime/lua/vim/lsp/_watchfiles.lua
@@ -116,7 +116,7 @@ function M.register(reg, client_id)
local params = {
changes = change_queues[client_id],
}
- client.notify(ms.workspace_didChangeWatchedFiles, params)
+ client:notify(ms.workspace_didChangeWatchedFiles, params)
queue_timers[client_id] = nil
change_queues[client_id] = nil
change_cache[client_id] = nil
@@ -174,6 +174,7 @@ function M.cancel(client_id)
cancel()
end
end
+ cancels[client_id] = nil
end
return M
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
index 6383855a30..48aa809ebd 100644
--- a/runtime/lua/vim/lsp/buf.lua
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -20,7 +20,7 @@ local function client_positional_params(params)
end
end
-local hover_ns = api.nvim_create_namespace('vim_lsp_hover_range')
+local hover_ns = api.nvim_create_namespace('nvim.lsp.hover_range')
--- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts
--- @field silent? boolean
@@ -232,7 +232,7 @@ local function get_locations(method, opts)
end
for _, client in ipairs(clients) do
local params = util.make_position_params(win, client.offset_encoding)
- client.request(method, params, function(_, result)
+ client:request(method, params, function(_, result)
on_response(_, result, client)
end)
end
@@ -252,13 +252,13 @@ end
--- vim.lsp.buf.definition({ on_list = on_list })
--- vim.lsp.buf.references(nil, { on_list = on_list })
--- ```
+--- @field on_list? fun(t: vim.lsp.LocationOpts.OnList)
---
---- If you prefer loclist instead of qflist:
+--- Whether to use the |location-list| or the |quickfix| list in the default handler.
--- ```lua
--- vim.lsp.buf.definition({ loclist = true })
---- vim.lsp.buf.references(nil, { loclist = true })
+--- vim.lsp.buf.references(nil, { loclist = false })
--- ```
---- @field on_list? fun(t: vim.lsp.LocationOpts.OnList)
--- @field loclist? boolean
--- @class vim.lsp.LocationOpts.OnList
@@ -324,12 +324,11 @@ local function process_signature_help_results(results)
return signatures
end
-local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help')
+local sig_help_ns = api.nvim_create_namespace('nvim.lsp.signature_help')
--- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts
--- @field silent? boolean
--- TODO(lewis6991): support multiple clients
--- Displays signature information about the symbol under the cursor in a
--- floating window.
--- @param config? vim.lsp.buf.signature_help.Opts
@@ -356,6 +355,7 @@ function M.signature_help(config)
local ft = vim.bo[ctx.bufnr].filetype
local total = #signatures
+ local can_cycle = total > 1 and config.focusable
local idx = 0
--- @param update_win? integer
@@ -371,7 +371,7 @@ function M.signature_help(config)
return
end
- local sfx = total > 1 and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or ''
+ local sfx = can_cycle and string.format(' (%d/%d) (<C-s> to cycle)', idx, total) or ''
local title = string.format('Signature Help: %s%s', client.name, sfx)
if config.border then
config.title = title
@@ -402,7 +402,7 @@ function M.signature_help(config)
local fbuf, fwin = show_signature()
- if total > 1 then
+ if can_cycle then
vim.keymap.set('n', '<C-s>', function()
show_signature(fwin)
end, {
@@ -423,7 +423,7 @@ end
---
---@see vim.lsp.protocol.CompletionTriggerKind
function M.completion(context)
- vim.depends('vim.lsp.buf.completion', 'vim.lsp.commpletion.trigger', '0.12')
+ vim.depends('vim.lsp.buf.completion', 'vim.lsp.completion.trigger', '0.12')
return lsp.buf_request(
0,
ms.textDocument_completion,
@@ -450,10 +450,10 @@ local function range_from_selection(bufnr, mode)
-- A user can start visual selection at the end and move backwards
-- Normalize the range to start < end
if start_row == end_row and end_col < start_col then
- end_col, start_col = start_col, end_col
+ end_col, start_col = start_col, end_col --- @type integer, integer
elseif end_row < start_row then
- start_row, end_row = end_row, start_row
- start_col, end_col = end_col, start_col
+ start_row, end_row = end_row, start_row --- @type integer, integer
+ start_col, end_col = end_col, start_col --- @type integer, integer
end
if mode == 'V' then
start_col = 1
@@ -487,7 +487,7 @@ end
--- ```lua
--- -- Never request typescript-language-server for formatting
--- vim.lsp.buf.format {
---- filter = function(client) return client.name ~= "tsserver" end
+--- filter = function(client) return client.name ~= "ts_ls" end
--- }
--- ```
--- @field filter? fun(client: vim.lsp.Client): boolean?
@@ -519,7 +519,7 @@ end
--- @param opts? vim.lsp.buf.format.Opts
function M.format(opts)
opts = opts or {}
- local bufnr = opts.bufnr or api.nvim_get_current_buf()
+ local bufnr = vim._resolve_bufnr(opts.bufnr)
local mode = api.nvim_get_mode().mode
local range = opts.range
-- Try to use visual selection if no range is given
@@ -553,27 +553,34 @@ function M.format(opts)
--- @param client vim.lsp.Client
--- @param params lsp.DocumentFormattingParams
- --- @return lsp.DocumentFormattingParams
+ --- @return lsp.DocumentFormattingParams|lsp.DocumentRangeFormattingParams|lsp.DocumentRangesFormattingParams
local function set_range(client, params)
- local to_lsp_range = function(r) ---@return lsp.DocumentRangeFormattingParams|lsp.DocumentRangesFormattingParams
+ --- @param r {start:[integer,integer],end:[integer, integer]}
+ local function to_lsp_range(r)
return util.make_given_range_params(r.start, r['end'], bufnr, client.offset_encoding).range
end
+ local ret = params --[[@as lsp.DocumentFormattingParams|lsp.DocumentRangeFormattingParams|lsp.DocumentRangesFormattingParams]]
if passed_multiple_ranges then
- params.ranges = vim.tbl_map(to_lsp_range, range)
+ ret = params --[[@as lsp.DocumentRangesFormattingParams]]
+ --- @cast range {start:[integer,integer],end:[integer, integer]}
+ ret.ranges = vim.tbl_map(to_lsp_range, range)
elseif range then
- params.range = to_lsp_range(range)
+ ret = params --[[@as lsp.DocumentRangeFormattingParams]]
+ ret.range = to_lsp_range(range)
end
- return params
+ return ret
end
if opts.async then
+ --- @param idx? integer
+ --- @param client? vim.lsp.Client
local function do_format(idx, client)
- if not client then
+ if not idx or not client then
return
end
local params = set_range(client, util.make_formatting_params(opts.formatting_options))
- client.request(method, params, function(...)
+ client:request(method, params, function(...)
local handler = client.handlers[method] or lsp.handlers[method]
handler(...)
do_format(next(clients, idx))
@@ -584,7 +591,7 @@ function M.format(opts)
local timeout_ms = opts.timeout_ms or 1000
for _, client in pairs(clients) do
local params = set_range(client, util.make_formatting_params(opts.formatting_options))
- local result, err = client.request_sync(method, params, timeout_ms, bufnr)
+ local result, err = client:request_sync(method, params, timeout_ms, bufnr)
if result and result.result then
util.apply_text_edits(result.result, bufnr, client.offset_encoding)
elseif err then
@@ -615,7 +622,7 @@ end
---@param opts? vim.lsp.buf.rename.Opts Additional options:
function M.rename(new_name, opts)
opts = opts or {}
- local bufnr = opts.bufnr or api.nvim_get_current_buf()
+ local bufnr = vim._resolve_bufnr(opts.bufnr)
local clients = lsp.get_clients({
bufnr = bufnr,
name = opts.name,
@@ -636,38 +643,40 @@ function M.rename(new_name, opts)
local cword = vim.fn.expand('<cword>')
--- @param range lsp.Range
- --- @param offset_encoding string
- local function get_text_at_range(range, offset_encoding)
+ --- @param position_encoding string
+ local function get_text_at_range(range, position_encoding)
return api.nvim_buf_get_text(
bufnr,
range.start.line,
- util._get_line_byte_from_position(bufnr, range.start, offset_encoding),
+ util._get_line_byte_from_position(bufnr, range.start, position_encoding),
range['end'].line,
- util._get_line_byte_from_position(bufnr, range['end'], offset_encoding),
+ util._get_line_byte_from_position(bufnr, range['end'], position_encoding),
{}
)[1]
end
+ --- @param idx? integer
+ --- @param client? vim.lsp.Client
local function try_use_client(idx, client)
- if not client then
+ if not idx or not client then
return
end
--- @param name string
local function rename(name)
- local params = util.make_position_params(win, client.offset_encoding)
+ local params = util.make_position_params(win, client.offset_encoding) --[[@as lsp.RenameParams]]
params.newName = name
local handler = client.handlers[ms.textDocument_rename]
or lsp.handlers[ms.textDocument_rename]
- client.request(ms.textDocument_rename, params, function(...)
+ client:request(ms.textDocument_rename, params, function(...)
handler(...)
try_use_client(next(clients, idx))
end, bufnr)
end
- if client.supports_method(ms.textDocument_prepareRename) then
+ if client:supports_method(ms.textDocument_prepareRename) then
local params = util.make_position_params(win, client.offset_encoding)
- client.request(ms.textDocument_prepareRename, params, function(err, result)
+ client:request(ms.textDocument_prepareRename, params, function(err, result)
if err or result == nil then
if next(clients, idx) then
try_use_client(next(clients, idx))
@@ -706,7 +715,7 @@ function M.rename(new_name, opts)
end, bufnr)
else
assert(
- client.supports_method(ms.textDocument_rename),
+ client:supports_method(ms.textDocument_rename),
'Client must support textDocument/rename'
)
if new_name then
@@ -732,7 +741,7 @@ end
--- Lists all the references to the symbol under the cursor in the quickfix window.
---
----@param context (table|nil) Context for the request
+---@param context lsp.ReferenceContext? Context for the request
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
---@param opts? vim.lsp.ListOpts
function M.references(context, opts)
@@ -781,7 +790,7 @@ function M.references(context, opts)
params.context = context or {
includeDeclaration = true,
}
- client.request(ms.textDocument_references, params, function(_, result)
+ client:request(ms.textDocument_references, params, function(_, result)
local items = util.locations_to_items(result or {}, client.offset_encoding)
vim.list_extend(all_items, items)
remaining = remaining - 1
@@ -792,9 +801,10 @@ function M.references(context, opts)
end
end
---- Lists all symbols in the current buffer in the quickfix window.
+--- Lists all symbols in the current buffer in the |location-list|.
--- @param opts? vim.lsp.ListOpts
function M.document_symbol(opts)
+ opts = vim.tbl_deep_extend('keep', opts or {}, { loclist = true })
local params = { textDocument = util.make_text_document_params() }
request_with_opts(ms.textDocument_documentSymbol, params, opts)
end
@@ -813,7 +823,7 @@ local function request_with_id(client_id, method, params, handler, bufnr)
)
return
end
- client.request(method, params, handler, bufnr)
+ client:request(method, params, handler, bufnr)
end
--- @param item lsp.TypeHierarchyItem|lsp.CallHierarchyItem
@@ -880,7 +890,7 @@ local function hierarchy(method)
for _, client in ipairs(clients) do
local params = util.make_position_params(win, client.offset_encoding)
--- @param result lsp.CallHierarchyItem[]|lsp.TypeHierarchyItem[]?
- client.request(prepare_method, params, function(err, result, ctx)
+ client:request(prepare_method, params, function(err, result, ctx)
if err then
vim.notify(err.message, vim.log.levels.WARN)
elseif result then
@@ -1131,8 +1141,8 @@ local function on_code_action_results(results, opts)
local action = choice.action
local bufnr = assert(choice.ctx.bufnr, 'Must have buffer number')
- if not action.edit and client.supports_method(ms.codeAction_resolve) then
- client.request(ms.codeAction_resolve, action, function(err, resolved_action)
+ if not action.edit and client:supports_method(ms.codeAction_resolve) then
+ client:request(ms.codeAction_resolve, action, function(err, resolved_action)
if err then
if action.command then
apply_action(action, client, choice.ctx)
@@ -1224,6 +1234,7 @@ function M.code_action(opts)
for _, client in ipairs(clients) do
---@type lsp.CodeActionParams
local params
+
if opts.range then
assert(type(opts.range) == 'table', 'code_action range must be a table')
local start = assert(opts.range.start, 'range must have a `start` property')
@@ -1236,6 +1247,9 @@ function M.code_action(opts)
else
params = util.make_range_params(win, client.offset_encoding)
end
+
+ --- @cast params lsp.CodeActionParams
+
if context.diagnostics then
params.context = context
else
@@ -1253,7 +1267,7 @@ function M.code_action(opts)
})
end
- client.request(ms.textDocument_codeAction, params, on_result, bufnr)
+ client:request(ms.textDocument_codeAction, params, on_result, bufnr)
end
end
diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua
index 11ecb87507..253ccc48f4 100644
--- a/runtime/lua/vim/lsp/client.lua
+++ b/runtime/lua/vim/lsp/client.lua
@@ -75,17 +75,17 @@ local validate = vim.validate
---
--- Map with language server specific settings.
--- See the {settings} in |vim.lsp.Client|.
---- @field settings? table
+--- @field settings? lsp.LSPObject
---
--- Table that maps string of clientside commands to user-defined functions.
---- Commands passed to start_client take precedence over the global command registry. Each key
+--- Commands passed to `start()` take precedence over the global command registry. Each key
--- must be a unique command name, and the value is a function which is called if any LSP action
--- (code action, code lenses, ...) triggers the command.
--- @field commands? table<string,fun(command: lsp.Command, ctx: table)>
---
--- Values to pass in the initialization request as `initializationOptions`. See `initialize` in
--- the LSP spec.
---- @field init_options? table
+--- @field init_options? lsp.LSPObject
---
--- Name in log messages.
--- (default: client-id)
@@ -94,7 +94,8 @@ local validate = vim.validate
--- Language ID as string. Defaults to the buffer filetype.
--- @field get_language_id? fun(bufnr: integer, filetype: string): string
---
---- The encoding that the LSP server expects. Client does not verify this is correct.
+--- Called "position encoding" in LSP spec, the encoding that the LSP server expects.
+--- Client does not verify this is correct.
--- @field offset_encoding? 'utf-8'|'utf-16'|'utf-32'
---
--- Callback invoked when the client operation throws an error. `code` is a number describing the error.
@@ -103,7 +104,7 @@ local validate = vim.validate
--- @field on_error? fun(code: integer, err: string)
---
--- Callback invoked before the LSP "initialize" phase, where `params` contains the parameters
---- being sent to the server and `config` is the config that was passed to |vim.lsp.start_client()|.
+--- being sent to the server and `config` is the config that was passed to |vim.lsp.start()|.
--- You can use this to modify parameters before they are sent.
--- @field before_init? fun(params: lsp.InitializeParams, config: vim.lsp.ClientConfig)
---
@@ -148,8 +149,10 @@ local validate = vim.validate
--- See |vim.lsp.rpc.start()|.
--- @field rpc vim.lsp.rpc.PublicClient
---
---- 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.
+--- Called "position encoding" in LSP spec,
+--- 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.
--- @field offset_encoding string
---
--- The handlers used by the client as described in |lsp-handler|.
@@ -161,16 +164,20 @@ local validate = vim.validate
--- 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.
---- @field requests table<integer,{ type: string, bufnr: integer, method: string}>
+--- @field requests table<integer,{ type: string, bufnr: integer, method: string}?>
---
--- copy of the table that was passed by the user
---- to |vim.lsp.start_client()|.
+--- to |vim.lsp.start()|.
--- @field config vim.lsp.ClientConfig
---
--- Response from the server sent on `initialize` describing the server's
--- capabilities.
--- @field server_capabilities lsp.ServerCapabilities?
---
+--- Response from the server sent on `initialize` describing information about
+--- the server.
+--- @field server_info lsp.ServerInfo?
+---
--- A ring buffer (|vim.ringbuf()|) containing progress messages
--- sent by the server.
--- @field progress vim.lsp.Client.Progress
@@ -186,9 +193,6 @@ local validate = vim.validate
---
--- @field attached_buffers table<integer,true>
---
---- Buffers that should be attached to upon initialize()
---- @field package _buffers_to_attach table<integer,true>
----
--- @field private _log_prefix string
---
--- Track this so that we can escalate automatically if we've already tried a
@@ -207,7 +211,7 @@ local validate = vim.validate
--- Map with language server specific settings. These are returned to the
--- language server if requested via `workspace/configuration`. Keys are
--- case-sensitive.
---- @field settings table
+--- @field settings lsp.LSPObject
---
--- A table with flags for the client. The current (experimental) flags are:
--- @field flags vim.lsp.Client.Flags
@@ -219,70 +223,28 @@ local validate = vim.validate
--- @field private registrations table<string,lsp.Registration[]>
--- @field dynamic_capabilities lsp.DynamicCapabilities
---
---- Sends a request to the server.
---- This is a thin wrapper around {client.rpc.request} with some additional
---- checking.
---- If {handler} is not specified and if there's no respective global
---- handler, 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.
---- @field request fun(method: string, params: table?, handler: lsp.Handler?, bufnr: integer?): boolean, integer?
----
---- 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 dict, 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`.
---- @field request_sync fun(method: string, params: table?, timeout_ms: integer?, bufnr: integer): {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dict
----
---- 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).
---- @field notify fun(method: string, params: table?): boolean
----
---- Cancels a request with a given request id.
---- Returns: same as `notify()`.
---- @field cancel_request fun(id: integer): boolean
----
---- 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.
---- @field stop fun(force?: boolean)
----
---- Runs the on_attach function from the client's config if it was defined.
---- Useful for buffer-local setup.
---- @field on_attach fun(bufnr: integer)
----
--- @field private _before_init_cb? vim.lsp.client.before_init_cb
--- @field private _on_attach_cbs vim.lsp.client.on_attach_cb[]
--- @field private _on_init_cbs vim.lsp.client.on_init_cb[]
--- @field private _on_exit_cbs vim.lsp.client.on_exit_cb[]
--- @field private _on_error_cb? fun(code: integer, err: string)
----
---- Checks if a client supports a given method.
---- Always returns true for unknown off-spec methods.
---- {opts} is a optional `{bufnr?: integer}` table.
---- Some language server capabilities can be file specific.
---- @field supports_method fun(method: string, opts?: {bufnr: integer?}): boolean
----
---- Checks whether a client is stopped.
---- Returns: true if the client is fully stopped.
---- @field is_stopped fun(): boolean
local Client = {}
Client.__index = Client
---- @param cls table
---- @param meth any
---- @return function
-local function method_wrapper(cls, meth)
- return function(...)
- return meth(cls, ...)
+--- @param obj table<string,any>
+--- @param cls table<string,function>
+--- @param name string
+local function method_wrapper(obj, cls, name)
+ local meth = assert(cls[name])
+ obj[name] = function(...)
+ local arg = select(1, ...)
+ if arg and getmetatable(arg) == cls then
+ -- First argument is self, call meth directly
+ return meth(...)
+ end
+ vim.deprecate('client.' .. name, 'client:' .. name, '0.13')
+ -- First argument is not self, insert it
+ return meth(obj, ...)
end
end
@@ -304,9 +266,6 @@ local valid_encodings = {
['utf8'] = 'utf-8',
['utf16'] = 'utf-16',
['utf32'] = 'utf-32',
- UTF8 = 'utf-8',
- UTF16 = 'utf-16',
- UTF32 = 'utf-32',
}
--- Normalizes {encoding} to valid LSP encoding names.
@@ -315,12 +274,12 @@ local valid_encodings = {
local function validate_encoding(encoding)
validate('encoding', encoding, 'string', true)
if not encoding then
- return valid_encodings.UTF16
+ return valid_encodings.utf16
end
return valid_encodings[encoding:lower()]
or error(
string.format(
- "Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
+ "Invalid position encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'",
encoding
)
)
@@ -346,7 +305,7 @@ local function default_get_language_id(_bufnr, filetype)
return filetype
end
---- Validates a client configuration as given to |vim.lsp.start_client()|.
+--- Validates a client configuration as given to |vim.lsp.start()|.
--- @param config vim.lsp.ClientConfig
local function validate_config(config)
validate('config', config, 'table')
@@ -404,31 +363,6 @@ local function get_name(id, config)
return tostring(id)
end
---- @param workspace_folders string|lsp.WorkspaceFolder[]?
---- @return lsp.WorkspaceFolder[]?
-local function 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
-
---- @generic T
---- @param x elem_or_list<T>?
---- @return T[]
-local function ensure_list(x)
- if type(x) == 'table' then
- return x
- end
- return { x }
-end
-
--- @nodoc
--- @param config vim.lsp.ClientConfig
--- @return vim.lsp.Client?
@@ -455,13 +389,13 @@ function Client.create(config)
settings = config.settings or {},
flags = config.flags or {},
get_language_id = config.get_language_id or default_get_language_id,
- capabilities = config.capabilities or lsp.protocol.make_client_capabilities(),
- workspace_folders = get_workspace_folders(config.workspace_folders or config.root_dir),
+ capabilities = config.capabilities,
+ workspace_folders = lsp._get_workspace_folders(config.workspace_folders or config.root_dir),
root_dir = config.root_dir,
_before_init_cb = config.before_init,
- _on_init_cbs = ensure_list(config.on_init),
- _on_exit_cbs = ensure_list(config.on_exit),
- _on_attach_cbs = ensure_list(config.on_attach),
+ _on_init_cbs = vim._ensure_list(config.on_init),
+ _on_exit_cbs = vim._ensure_list(config.on_exit),
+ _on_attach_cbs = vim._ensure_list(config.on_attach),
_on_error_cb = config.on_error,
_trace = get_trace(config.trace),
@@ -477,6 +411,9 @@ function Client.create(config)
messages = { name = name, messages = {}, progress = {}, status = {} },
}
+ self.capabilities =
+ vim.tbl_deep_extend('force', lsp.protocol.make_client_capabilities(), self.capabilities or {})
+
--- @class lsp.DynamicCapabilities
--- @nodoc
self.dynamic_capabilities = {
@@ -499,24 +436,23 @@ function Client.create(config)
end,
}
- self.request = method_wrapper(self, Client._request)
- self.request_sync = method_wrapper(self, Client._request_sync)
- self.notify = method_wrapper(self, Client._notify)
- self.cancel_request = method_wrapper(self, Client._cancel_request)
- self.stop = method_wrapper(self, Client._stop)
- self.is_stopped = method_wrapper(self, Client._is_stopped)
- self.on_attach = method_wrapper(self, Client._on_attach)
- self.supports_method = method_wrapper(self, Client._supports_method)
-
--- @type table<string|integer, string> title of unfinished progress sequences by token
self.progress.pending = {}
--- @type vim.lsp.rpc.Dispatchers
local dispatchers = {
- notification = method_wrapper(self, Client._notification),
- server_request = method_wrapper(self, Client._server_request),
- on_error = method_wrapper(self, Client._on_error),
- on_exit = method_wrapper(self, Client._on_exit),
+ notification = function(...)
+ return self:_notification(...)
+ end,
+ server_request = function(...)
+ return self:_server_request(...)
+ end,
+ on_error = function(...)
+ return self:_on_error(...)
+ end,
+ on_exit = function(...)
+ return self:_on_exit(...)
+ end,
}
-- Start the RPC client.
@@ -533,6 +469,15 @@ function Client.create(config)
setmetatable(self, Client)
+ method_wrapper(self, Client, 'request')
+ method_wrapper(self, Client, 'request_sync')
+ method_wrapper(self, Client, 'notify')
+ method_wrapper(self, Client, 'cancel_request')
+ method_wrapper(self, Client, 'stop')
+ method_wrapper(self, Client, 'is_stopped')
+ method_wrapper(self, Client, 'on_attach')
+ method_wrapper(self, Client, 'supports_method')
+
return self
end
@@ -615,8 +560,10 @@ function Client:initialize()
self.offset_encoding = self.server_capabilities.positionEncoding
end
+ self.server_info = result.serverInfo
+
if next(self.settings) then
- self:_notify(ms.workspace_didChangeConfiguration, { settings = self.settings })
+ self:notify(ms.workspace_didChangeConfiguration, { settings = self.settings })
end
-- If server is being restarted, make sure to re-attach to any previously attached buffers.
@@ -628,7 +575,7 @@ function Client:initialize()
for buf in pairs(reattach_bufs) do
-- The buffer may have been detached in the on_init callback.
if self.attached_buffers[buf] then
- self:_on_attach(buf)
+ self:on_attach(buf)
end
end
@@ -645,24 +592,62 @@ end
--- Returns the default handler if the user hasn't set a custom one.
---
--- @param method (string) LSP method name
---- @return lsp.Handler|nil handler for the given method, if defined, or the default from |vim.lsp.handlers|
+--- @return lsp.Handler? handler for the given method, if defined, or the default from |vim.lsp.handlers|
function Client:_resolve_handler(method)
return self.handlers[method] or lsp.handlers[method]
end
---- 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()
+--- @private
+--- @param id integer
+--- @param req_type 'pending'|'complete'|'cancel'|
+--- @param bufnr? integer (only required for req_type='pending')
+--- @param method? string (only required for req_type='pending')
+function Client:_process_request(id, req_type, bufnr, method)
+ local pending = req_type == 'pending'
+
+ validate('id', id, 'number')
+ if pending then
+ validate('bufnr', bufnr, 'number')
+ validate('method', method, 'string')
+ end
+
+ local cur_request = self.requests[id]
+
+ if pending and cur_request then
+ log.error(
+ self._log_prefix,
+ ('Cannot create request with id %d as one already exists'):format(id)
+ )
+ return
+ elseif not pending and not cur_request then
+ log.error(
+ self._log_prefix,
+ ('Cannot find request with id %d whilst attempting to %s'):format(id, req_type)
+ )
+ return
+ end
+
+ if cur_request then
+ bufnr = cur_request.bufnr
+ method = cur_request.method
end
- return bufnr
+
+ assert(bufnr and method)
+
+ local request = { type = req_type, bufnr = bufnr, method = method }
+
+ -- Clear 'complete' requests
+ -- Note 'pending' and 'cancelled' requests are cleared when the server sends a response
+ -- which is processed via the notify_reply_callback argument to rpc.request.
+ self.requests[id] = req_type ~= 'complete' and request or nil
+
+ api.nvim_exec_autocmds('LspRequest', {
+ buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
+ modeline = false,
+ data = { client_id = self.id, request_id = id, request = request },
+ })
end
---- @private
--- Sends a request to the server.
---
--- This is a thin wrapper around {client.rpc.request} with some additional
@@ -671,15 +656,14 @@ end
--- @param method string LSP method name.
--- @param params? table LSP request params.
--- @param handler? lsp.Handler Response |lsp-handler| for this method.
---- @param bufnr integer Buffer handle (0 for current).
---- @return boolean status, integer? request_id {status} is a bool indicating
---- whether the request was successful. If it is `false`, then it will
---- always be `false` (the client has shutdown). If it was
---- successful, then it will return {request_id} as the
---- second result. You can use this with `client.cancel_request(request_id)`
+--- @param bufnr? integer (default: 0) Buffer handle, or 0 for current.
+--- @return boolean status indicates whether the request was successful.
+--- If it is `false`, then it will always be `false` (the client has shutdown).
+--- @return integer? request_id Can be used with |Client:cancel_request()|.
+--- `nil` is request failed.
--- to cancel the-request.
--- @see |vim.lsp.buf_request_all()|
-function Client:_request(method, params, handler, bufnr)
+function Client:request(method, params, handler, bufnr)
if not handler then
handler = assert(
self:_resolve_handler(method),
@@ -688,37 +672,24 @@ function Client:_request(method, params, handler, bufnr)
end
-- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state
changetracking.flush(self, bufnr)
+ bufnr = vim._resolve_bufnr(bufnr)
local version = lsp.util.buf_versions[bufnr]
- bufnr = resolve_bufnr(bufnr)
log.debug(self._log_prefix, 'client.request', self.id, method, params, handler, bufnr)
local success, request_id = self.rpc.request(method, params, function(err, result)
- local context = {
+ handler(err, result, {
method = method,
client_id = self.id,
bufnr = bufnr,
params = params,
version = version,
- }
- handler(err, result, context)
- end, function(request_id)
- local request = self.requests[request_id]
- request.type = 'complete'
- api.nvim_exec_autocmds('LspRequest', {
- buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
- modeline = false,
- data = { client_id = self.id, request_id = request_id, request = request },
})
- self.requests[request_id] = nil
+ end, function(request_id)
+ -- Called when the server sends a response to the request (including cancelled acknowledgment).
+ self:_process_request(request_id, 'complete')
end)
if success and request_id then
- local request = { type = 'pending', bufnr = bufnr, method = method }
- self.requests[request_id] = request
- api.nvim_exec_autocmds('LspRequest', {
- buffer = api.nvim_buf_is_valid(bufnr) and bufnr or nil,
- modeline = false,
- data = { client_id = self.id, request_id = request_id, request = request },
- })
+ self:_process_request(request_id, 'pending', bufnr, method)
end
return success, request_id
@@ -731,41 +702,39 @@ local wait_result_reason = { [-1] = 'timeout', [-2] = 'interrupted', [-3] = 'err
---
--- @param ... string List to write to the buffer
local function err_message(...)
- local message = table.concat(vim.iter({ ... }):flatten():totable())
+ local chunks = { { table.concat(vim.iter({ ... }):flatten():totable()) } }
if vim.in_fast_event() then
vim.schedule(function()
- api.nvim_err_writeln(message)
+ api.nvim_echo(chunks, true, { err = true })
api.nvim_command('redraw')
end)
else
- api.nvim_err_writeln(message)
+ api.nvim_echo(chunks, true, { err = true })
api.nvim_command('redraw')
end
end
---- @private
--- Sends a request to the server and synchronously waits for the response.
---
---- This is a wrapper around {client.request}
+--- This is a wrapper around |Client:request()|
---
---- @param method (string) LSP method name.
---- @param params (table) LSP request params.
---- @param timeout_ms (integer|nil) Maximum time in milliseconds to wait for
+--- @param method string LSP method name.
+--- @param params table LSP request params.
+--- @param timeout_ms integer? Maximum time in milliseconds to wait for
--- a result. Defaults to 1000
---- @param bufnr (integer) Buffer handle (0 for current).
---- @return {err: lsp.ResponseError|nil, result:any}|nil, string|nil err # a dict, 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`.
+--- @param bufnr? integer (default: 0) Buffer handle, or 0 for current.
+--- @return {err: lsp.ResponseError?, result:any}? `result` and `err` from the |lsp-handler|.
+--- `nil` is the request was unsuccessful
+--- @return string? err On timeout, cancel or error, where `err` is a
+--- string describing the failure reason.
--- @see |vim.lsp.buf_request_sync()|
-function Client:_request_sync(method, params, timeout_ms, bufnr)
+function Client:request_sync(method, params, timeout_ms, bufnr)
local request_result = nil
local function _sync_handler(err, result)
request_result = { err = err, result = result }
end
- local success, request_id = self:_request(method, params, _sync_handler, bufnr)
+ local success, request_id = self:request(method, params, _sync_handler, bufnr)
if not success then
return nil
end
@@ -776,22 +745,20 @@ function Client:_request_sync(method, params, timeout_ms, bufnr)
if not wait_result then
if request_id then
- self:_cancel_request(request_id)
+ self:cancel_request(request_id)
end
return nil, wait_result_reason[reason]
end
return request_result
end
---- @package
--- Sends a notification to an LSP server.
---
--- @param method string LSP method name.
---- @param params table|nil LSP request params.
---- @return boolean status true if the notification was successful.
---- If it is false, then it will always be false
---- (the client has shutdown).
-function Client:_notify(method, params)
+--- @param params table? LSP request params.
+--- @return boolean status indicating if the notification was successful.
+--- If it is false, then the client has shutdown.
+function Client:notify(method, params)
if method ~= ms.textDocument_didChange then
changetracking.flush(self)
end
@@ -814,41 +781,32 @@ function Client:_notify(method, params)
return client_active
end
---- @private
--- Cancels a request with a given request id.
---
---- @param id (integer) id of request to cancel
---- @return boolean status true if notification was successful. false otherwise
---- @see |vim.lsp.client.notify()|
-function Client:_cancel_request(id)
- validate('id', id, 'number')
- local request = self.requests[id]
- if request and request.type == 'pending' then
- request.type = 'cancel'
- api.nvim_exec_autocmds('LspRequest', {
- buffer = api.nvim_buf_is_valid(request.bufnr) and request.bufnr or nil,
- modeline = false,
- data = { client_id = self.id, request_id = id, request = request },
- })
- end
+--- @param id integer id of request to cancel
+--- @return boolean status indicating if the notification was successful.
+--- @see |Client:notify()|
+function Client:cancel_request(id)
+ self:_process_request(id, 'cancel')
return self.rpc.notify(ms.dollar_cancelRequest, { id = id })
end
---- @private
--- Stops a client, optionally with force.
---
---- By default, it will just ask the - server to shutdown without force. If
+--- By default, it will just request 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.
---
---- @param force boolean|nil
-function Client:_stop(force)
+--- @param force? boolean
+function Client:stop(force)
local rpc = self.rpc
if rpc.is_closing() then
return
end
+ vim.lsp._watchfiles.cancel(self.id)
+
if force or not self.initialized or self._graceful_shutdown_failed then
rpc.terminate()
return
@@ -863,7 +821,6 @@ function Client:_stop(force)
rpc.terminate()
self._graceful_shutdown_failed = true
end
- vim.lsp._watchfiles.cancel(self.id)
end)
end
@@ -945,20 +902,22 @@ end
--- @param bufnr? integer
--- @return lsp.Registration?
function Client:_get_registration(method, bufnr)
- bufnr = bufnr or vim.api.nvim_get_current_buf()
+ bufnr = vim._resolve_bufnr(bufnr)
for _, reg in ipairs(self.registrations[method] or {}) do
- if not reg.registerOptions or not reg.registerOptions.documentSelector then
+ local regoptions = reg.registerOptions --[[@as {documentSelector:lsp.TextDocumentFilter[]}]]
+ if not regoptions or not regoptions.documentSelector then
return reg
end
- local documentSelector = reg.registerOptions.documentSelector
+ local documentSelector = regoptions.documentSelector
local language = self:_get_language_id(bufnr)
local uri = vim.uri_from_bufnr(bufnr)
local fname = vim.uri_to_fname(uri)
for _, filter in ipairs(documentSelector) do
+ local flang, fscheme, fpat = filter.language, filter.scheme, filter.pattern
if
- not (filter.language and language ~= filter.language)
- and not (filter.scheme and not vim.startswith(uri, filter.scheme .. ':'))
- and not (filter.pattern and not vim.glob.to_lpeg(filter.pattern):match(fname))
+ not (flang and language ~= flang)
+ and not (fscheme and not vim.startswith(uri, fscheme .. ':'))
+ and not (type(fpat) == 'string' and not vim.glob.to_lpeg(fpat):match(fname))
then
return reg
end
@@ -966,12 +925,11 @@ function Client:_get_registration(method, bufnr)
end
end
---- @private
--- Checks whether a client is stopped.
---
--- @return boolean # true if client is stopped or in the process of being
--- stopped; false otherwise
-function Client:_is_stopped()
+function Client:is_stopped()
return self.rpc.is_closing()
end
@@ -983,7 +941,7 @@ end
--- @param handler? lsp.Handler only called if a server command
function Client:exec_cmd(command, context, handler)
context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]]
- context.bufnr = context.bufnr or api.nvim_get_current_buf()
+ context.bufnr = vim._resolve_bufnr(context.bufnr)
context.client_id = self.id
local cmdname = command.command
local fn = self.commands[cmdname] or lsp.commands[cmdname]
@@ -1013,7 +971,7 @@ function Client:exec_cmd(command, context, handler)
command = cmdname,
arguments = command.arguments,
}
- self.request(ms.workspace_executeCommand, params, handler, context.bufnr)
+ self:request(ms.workspace_executeCommand, params, handler, context.bufnr)
end
--- Default handler for the 'textDocument/didOpen' LSP notification.
@@ -1021,14 +979,14 @@ end
--- @param bufnr integer Number of the buffer, or 0 for current
function Client:_text_document_did_open_handler(bufnr)
changetracking.init(self, bufnr)
- if not self.supports_method(ms.textDocument_didOpen) then
+ if not self:supports_method(ms.textDocument_didOpen) then
return
end
if not api.nvim_buf_is_loaded(bufnr) then
return
end
- self.notify(ms.textDocument_didOpen, {
+ self:notify(ms.textDocument_didOpen, {
textDocument = {
version = lsp.util.buf_versions[bufnr],
uri = vim.uri_from_bufnr(bufnr),
@@ -1049,8 +1007,9 @@ function Client:_text_document_did_open_handler(bufnr)
end
--- Runs the on_attach function from the client's config if it was defined.
+--- Useful for buffer-local setup.
--- @param bufnr integer Buffer number
-function Client:_on_attach(bufnr)
+function Client:on_attach(bufnr)
self:_text_document_did_open_handler(bufnr)
lsp._set_defaults(self, bufnr)
@@ -1085,10 +1044,18 @@ function Client:write_error(code, err)
err_message(self._log_prefix, ': Error ', client_error, ': ', vim.inspect(err))
end
---- @private
+--- Checks if a client supports a given method.
+--- Always returns true for unknown off-spec methods.
+---
+--- Note: Some language server capabilities can be file specific.
--- @param method string
---- @param opts? {bufnr: integer?}
-function Client:_supports_method(method, opts)
+--- @param bufnr? integer
+function Client:supports_method(method, bufnr)
+ -- Deprecated form
+ if type(bufnr) == 'table' then
+ --- @diagnostic disable-next-line:no-unknown
+ bufnr = bufnr.bufnr
+ end
local required_capability = lsp._request_name_to_capability[method]
-- if we don't know about the method, assume that the client supports it.
if not required_capability then
@@ -1101,12 +1068,12 @@ function Client:_supports_method(method, opts)
local rmethod = lsp._resolve_to_request[method]
if rmethod then
if self:_supports_registration(rmethod) then
- local reg = self:_get_registration(rmethod, opts and opts.bufnr)
+ local reg = self:_get_registration(rmethod, bufnr)
return vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') or false
end
else
if self:_supports_registration(method) then
- return self:_get_registration(method, opts and opts.bufnr) ~= nil
+ return self:_get_registration(method, bufnr) ~= nil
end
end
return false
@@ -1205,9 +1172,9 @@ function Client:_add_workspace_folder(dir)
end
end
- local wf = assert(get_workspace_folders(dir))
+ local wf = assert(lsp._get_workspace_folders(dir))
- self:_notify(ms.workspace_didChangeWorkspaceFolders, {
+ self:notify(ms.workspace_didChangeWorkspaceFolders, {
event = { added = wf, removed = {} },
})
@@ -1220,9 +1187,9 @@ end
--- Remove a directory to the workspace folders.
--- @param dir string?
function Client:_remove_workspace_folder(dir)
- local wf = assert(get_workspace_folders(dir))
+ local wf = assert(lsp._get_workspace_folders(dir))
- self:_notify(ms.workspace_didChangeWorkspaceFolders, {
+ self:notify(ms.workspace_didChangeWorkspaceFolders, {
event = { added = {}, removed = wf },
})
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index fdbdda695a..e36d8fee27 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -21,7 +21,7 @@ local lens_cache_by_buf = setmetatable({}, {
---client_id -> namespace
local namespaces = setmetatable({}, {
__index = function(t, key)
- local value = api.nvim_create_namespace('vim_lsp_codelens:' .. key)
+ local value = api.nvim_create_namespace('nvim.lsp.codelens:' .. key)
rawset(t, key, value)
return value
end,
@@ -30,7 +30,7 @@ local namespaces = setmetatable({}, {
---@private
M.__namespaces = namespaces
-local augroup = api.nvim_create_augroup('vim_lsp_codelens', {})
+local augroup = api.nvim_create_augroup('nvim.lsp.codelens', {})
api.nvim_create_autocmd('LspDetach', {
group = augroup,
@@ -104,16 +104,12 @@ function M.run()
end
end
-local function resolve_bufnr(bufnr)
- return bufnr == 0 and api.nvim_get_current_buf() or bufnr
-end
-
--- Clear the lenses
---
---@param client_id integer|nil filter by client_id. All clients if nil
---@param bufnr integer|nil filter by buffer. All buffers if nil, 0 for current buffer
function M.clear(client_id, bufnr)
- bufnr = bufnr and resolve_bufnr(bufnr)
+ bufnr = bufnr and vim._resolve_bufnr(bufnr)
local buffers = bufnr and { bufnr }
or vim.tbl_filter(api.nvim_buf_is_loaded, api.nvim_list_bufs())
for _, iter_bufnr in pairs(buffers) do
@@ -231,7 +227,7 @@ local function resolve_lenses(lenses, bufnr, client_id, callback)
countdown()
else
assert(client)
- client.request(ms.codeLens_resolve, lens, function(_, result)
+ client:request(ms.codeLens_resolve, lens, function(_, result)
if api.nvim_buf_is_loaded(bufnr) and result and result.command then
lens.command = result.command
-- Eager display to have some sort of incremental feedback
@@ -296,7 +292,7 @@ end
--- @param opts? vim.lsp.codelens.refresh.Opts Optional fields
function M.refresh(opts)
opts = opts or {}
- local bufnr = opts.bufnr and resolve_bufnr(opts.bufnr)
+ local bufnr = opts.bufnr and vim._resolve_bufnr(opts.bufnr)
local buffers = bufnr and { bufnr }
or vim.tbl_filter(api.nvim_buf_is_loaded, api.nvim_list_bufs())
diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua
index 92bc110a97..cf6d07745f 100644
--- a/runtime/lua/vim/lsp/completion.lua
+++ b/runtime/lua/vim/lsp/completion.lua
@@ -127,8 +127,10 @@ end
--- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
---
--- @param item lsp.CompletionItem
+--- @param prefix string
+--- @param match fun(text: string, prefix: string):boolean
--- @return string
-local function get_completion_word(item)
+local function get_completion_word(item, prefix, match)
if item.insertTextFormat == protocol.InsertTextFormat.Snippet then
if item.textEdit then
-- Use label instead of text if text has different starting characters.
@@ -146,7 +148,12 @@ local function get_completion_word(item)
--
-- Typing `i` would remove the candidate because newText starts with `t`.
local text = parse_snippet(item.insertText or item.textEdit.newText)
- return #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
+ local word = #text < #item.label and vim.fn.matchstr(text, '\\k*') or item.label
+ if item.filterText and not match(word, prefix) then
+ return item.filterText
+ else
+ return word
+ end
elseif item.insertText and item.insertText ~= '' then
return parse_snippet(item.insertText)
else
@@ -224,6 +231,9 @@ end
---@param prefix string
---@return boolean
local function match_item_by_value(value, prefix)
+ if prefix == '' then
+ return true
+ end
if vim.o.completeopt:find('fuzzy') ~= nil then
return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil
end
@@ -276,7 +286,7 @@ function M._lsp_to_complete_items(result, prefix, client_id)
local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert')
for _, item in ipairs(items) do
if matches(item) then
- local word = get_completion_word(item)
+ local word = get_completion_word(item, prefix, match_item_by_value)
local hl_group = ''
if
item.deprecated
@@ -404,7 +414,7 @@ local function request(clients, bufnr, win, callback)
for _, client in pairs(clients) do
local client_id = client.id
local params = lsp.util.make_position_params(win, client.offset_encoding)
- local ok, request_id = client.request(ms.textDocument_completion, params, function(err, result)
+ local ok, request_id = client:request(ms.textDocument_completion, params, function(err, result)
responses[client_id] = { err = err, result = result }
remaining_requests = remaining_requests - 1
if remaining_requests == 0 then
@@ -421,7 +431,7 @@ local function request(clients, bufnr, win, callback)
for client_id, request_id in pairs(request_ids) do
local client = lsp.get_client_by_id(client_id)
if client then
- client.cancel_request(request_id)
+ client:cancel_request(request_id)
end
end
end
@@ -460,7 +470,7 @@ local function trigger(bufnr, clients)
local server_start_boundary --- @type integer?
for client_id, response in pairs(responses) do
if response.err then
- vim.notify_once(response.err.message, vim.log.levels.warn)
+ vim.notify_once(response.err.message, vim.log.levels.WARN)
end
local result = response.result
@@ -550,7 +560,7 @@ local function on_complete_done()
return
end
- local offset_encoding = client.offset_encoding or 'utf-16'
+ local position_encoding = client.offset_encoding or 'utf-16'
local resolve_provider = (client.server_capabilities.completionProvider or {}).resolveProvider
local function clear_word()
@@ -576,13 +586,13 @@ local function on_complete_done()
if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then
clear_word()
- lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, offset_encoding)
+ lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, position_encoding)
apply_snippet_and_command()
elseif resolve_provider and type(completion_item) == 'table' then
local changedtick = vim.b[bufnr].changedtick
--- @param result lsp.CompletionItem
- client.request(ms.completionItem_resolve, completion_item, function(err, result)
+ client:request(ms.completionItem_resolve, completion_item, function(err, result)
if changedtick ~= vim.b[bufnr].changedtick then
return
end
@@ -591,7 +601,7 @@ local function on_complete_done()
if err then
vim.notify_once(err.message, vim.log.levels.WARN)
elseif result and result.additionalTextEdits then
- lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, offset_encoding)
+ lsp.util.apply_text_edits(result.additionalTextEdits, bufnr, position_encoding)
if result.command then
completion_item.command = result.command
end
@@ -605,6 +615,12 @@ local function on_complete_done()
end
end
+---@param bufnr integer
+---@return string
+local function get_augroup(bufnr)
+ return string.format('nvim.lsp.completion_%d', bufnr)
+end
+
--- @class vim.lsp.completion.BufferOpts
--- @field autotrigger? boolean Default: false When true, completion triggers automatically based on the server's `triggerCharacters`.
--- @field convert? fun(item: lsp.CompletionItem): table Transforms an LSP CompletionItem to |complete-items|.
@@ -629,8 +645,7 @@ local function enable_completions(client_id, bufnr, opts)
})
-- Set up autocommands.
- local group =
- api.nvim_create_augroup(string.format('vim/lsp/completion-%d', bufnr), { clear = true })
+ local group = api.nvim_create_augroup(get_augroup(bufnr), { clear = true })
api.nvim_create_autocmd('CompleteDone', {
group = group,
buffer = bufnr,
@@ -698,7 +713,7 @@ local function disable_completions(client_id, bufnr)
handle.clients[client_id] = nil
if not next(handle.clients) then
buf_handles[bufnr] = nil
- api.nvim_del_augroup_by_name(string.format('vim/lsp/completion-%d', bufnr))
+ api.nvim_del_augroup_by_name(get_augroup(bufnr))
else
for char, clients in pairs(handle.triggers) do
--- @param c vim.lsp.Client
@@ -716,7 +731,7 @@ end
--- @param bufnr integer Buffer handle, or 0 for the current buffer
--- @param opts? vim.lsp.completion.BufferOpts
function M.enable(enable, client_id, bufnr, opts)
- bufnr = (bufnr == 0 and api.nvim_get_current_buf()) or bufnr
+ bufnr = vim._resolve_bufnr(bufnr)
if enable then
enable_completions(client_id, bufnr, opts or {})
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index 8fd30c7668..fe24928a69 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -5,7 +5,7 @@ local api = vim.api
local M = {}
-local augroup = api.nvim_create_augroup('vim_lsp_diagnostic', {})
+local augroup = api.nvim_create_augroup('nvim.lsp.diagnostic', {})
local DEFAULT_CLIENT_ID = -1
@@ -20,7 +20,7 @@ end
---@return lsp.DiagnosticSeverity
local function severity_vim_to_lsp(severity)
if type(severity) == 'string' then
- severity = vim.diagnostic.severity[severity]
+ severity = vim.diagnostic.severity[severity] --- @type integer
end
return severity
end
@@ -77,7 +77,7 @@ end
local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
local buf_lines = get_buf_lines(bufnr)
local client = vim.lsp.get_client_by_id(client_id)
- local offset_encoding = client and client.offset_encoding or 'utf-16'
+ local position_encoding = client and client.offset_encoding or 'utf-16'
--- @param diagnostic lsp.Diagnostic
--- @return vim.Diagnostic
return vim.tbl_map(function(diagnostic)
@@ -89,15 +89,16 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
string.format('Unsupported Markup message from LSP client %d', client_id),
vim.lsp.log_levels.ERROR
)
+ --- @diagnostic disable-next-line: undefined-field,no-unknown
message = diagnostic.message.value
end
local line = buf_lines and buf_lines[start.line + 1] or ''
--- @type vim.Diagnostic
return {
lnum = start.line,
- col = vim.str_byteindex(line, offset_encoding, start.character, false),
+ col = vim.str_byteindex(line, position_encoding, start.character, false),
end_lnum = _end.line,
- end_col = vim.str_byteindex(line, offset_encoding, _end.character, false),
+ end_col = vim.str_byteindex(line, position_encoding, _end.character, false),
severity = severity_lsp_to_vim(diagnostic.severity),
message = message,
source = diagnostic.source,
@@ -208,7 +209,7 @@ end
--- @param uri string
--- @param client_id? integer
---- @param diagnostics vim.Diagnostic[]
+--- @param diagnostics lsp.Diagnostic[]
--- @param is_pull boolean
local function handle_diagnostics(uri, client_id, diagnostics, is_pull)
local fname = vim.uri_to_fname(uri)
@@ -246,10 +247,18 @@ end
---
--- See |vim.diagnostic.config()| for configuration options.
---
----@param _ lsp.ResponseError?
+---@param error lsp.ResponseError?
---@param result lsp.DocumentDiagnosticReport
---@param ctx lsp.HandlerContext
-function M.on_diagnostic(_, result, ctx)
+function M.on_diagnostic(error, result, ctx)
+ if error ~= nil and error.code == protocol.ErrorCodes.ServerCancelled then
+ if error.data == nil or error.data.retriggerRequest ~= false then
+ local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
+ client:request(ctx.method, ctx.params)
+ end
+ return
+ end
+
if result == nil or result.kind == 'unchanged' then
return
end
@@ -348,9 +357,7 @@ end
---@param bufnr (integer) Buffer handle, or 0 for current
---@private
function M._enable(bufnr)
- if bufnr == nil or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
if not bufstates[bufnr] then
bufstates[bufnr] = { enabled = true }
diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua
index 5c28d88b38..b35140dfad 100644
--- a/runtime/lua/vim/lsp/handlers.lua
+++ b/runtime/lua/vim/lsp/handlers.lua
@@ -47,7 +47,7 @@ RSC[ms.dollar_progress] = function(_, params, ctx)
local value = params.value
if type(value) == 'table' then
- kind = value.kind
+ kind = value.kind --- @type string
-- Carry over title of `begin` messages to `report` and `end` messages
-- So that consumers always have it available, even if they consume a
-- subset of the full sequence
@@ -247,12 +247,12 @@ local function response_to_list(map_result, entity, title_fn)
local items = map_result(result, ctx.bufnr)
local list = { title = title, items = items, context = ctx }
- if config.loclist then
- vim.fn.setloclist(0, {}, ' ', list)
- vim.cmd.lopen()
- elseif config.on_list then
+ if config.on_list then
assert(vim.is_callable(config.on_list), 'on_list is not a function')
config.on_list(list)
+ elseif config.loclist then
+ vim.fn.setloclist(0, {}, ' ', list)
+ vim.cmd.lopen()
else
vim.fn.setqflist({}, ' ', list)
vim.cmd('botright copen')
@@ -382,7 +382,7 @@ end
--- @diagnostic disable-next-line: deprecated
RCS[ms.textDocument_hover] = M.hover
-local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help')
+local sig_help_ns = api.nvim_create_namespace('nvim.lsp.signature_help')
--- @deprecated remove in 0.13
--- |lsp-handler| for the method "textDocument/signatureHelp".
@@ -582,9 +582,8 @@ NSC['window/showMessage'] = function(_, params, ctx)
if message_type == protocol.MessageType.Error then
err_message('LSP[', client_name, '] ', message)
else
- --- @type string
- local message_type_name = protocol.MessageType[message_type]
- api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message))
+ message = ('LSP[%s][%s] %s\n'):format(client_name, protocol.MessageType[message_type], message)
+ api.nvim_echo({ { message } }, true, {})
end
return params
end
@@ -659,7 +658,8 @@ for k, fn in pairs(M) do
})
end
- if err then
+ -- ServerCancelled errors should be propagated to the request handler
+ if err and err.code ~= protocol.ErrorCodes.ServerCancelled then
-- LSP spec:
-- interface ResponseError:
-- code: integer;
diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua
index 0d314108fe..8af9f2f791 100644
--- a/runtime/lua/vim/lsp/health.lua
+++ b/runtime/lua/vim/lsp/health.lua
@@ -28,42 +28,48 @@ local function check_log()
report_fn(string.format('Log size: %d KB', log_size / 1000))
end
+--- @param f function
+--- @return string
+local function func_tostring(f)
+ local info = debug.getinfo(f, 'S')
+ return ('<function %s:%s>'):format(info.source, info.linedefined)
+end
+
local function check_active_clients()
vim.health.start('vim.lsp: Active Clients')
local clients = vim.lsp.get_clients()
if next(clients) then
for _, client in pairs(clients) do
+ local server_version = vim.tbl_get(client, 'server_info', 'version')
+ or '? (no serverInfo.version response)'
local cmd ---@type string
- if type(client.config.cmd) == 'table' then
- cmd = table.concat(client.config.cmd --[[@as table]], ' ')
- elseif type(client.config.cmd) == 'function' then
- cmd = tostring(client.config.cmd)
+ local ccmd = client.config.cmd
+ if type(ccmd) == 'table' then
+ cmd = vim.inspect(ccmd)
+ elseif type(ccmd) == 'function' then
+ cmd = func_tostring(ccmd)
end
local dirs_info ---@type string
if client.workspace_folders and #client.workspace_folders > 1 then
- dirs_info = string.format(
- ' Workspace folders:\n %s',
- vim
- .iter(client.workspace_folders)
- ---@param folder lsp.WorkspaceFolder
- :map(function(folder)
- return folder.name
- end)
- :join('\n ')
- )
+ local wfolders = {} --- @type string[]
+ for _, dir in ipairs(client.workspace_folders) do
+ wfolders[#wfolders + 1] = dir.name
+ end
+ dirs_info = ('- Workspace folders:\n %s'):format(table.concat(wfolders, '\n '))
else
dirs_info = string.format(
- ' Root directory: %s',
+ '- Root directory: %s',
client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~')
) or nil
end
report_info(table.concat({
string.format('%s (id: %d)', client.name, client.id),
+ string.format('- Version: %s', server_version),
dirs_info,
- string.format(' Command: %s', cmd),
- string.format(' Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),
+ string.format('- Command: %s', cmd),
+ string.format('- Settings: %s', vim.inspect(client.settings, { newline = '\n ' })),
string.format(
- ' Attached buffers: %s',
+ '- Attached buffers: %s',
vim.iter(pairs(client.attached_buffers)):map(tostring):join(', ')
),
}, '\n'))
@@ -174,10 +180,45 @@ local function check_position_encodings()
end
end
+local function check_enabled_configs()
+ vim.health.start('vim.lsp: Enabled Configurations')
+
+ for name in vim.spairs(vim.lsp._enabled_configs) do
+ local config = vim.lsp.config[name]
+ local text = {} --- @type string[]
+ text[#text + 1] = ('%s:'):format(name)
+ for k, v in
+ vim.spairs(config --[[@as table<string,any>]])
+ do
+ local v_str --- @type string?
+ if k == 'name' then
+ v_str = nil
+ elseif k == 'filetypes' or k == 'root_markers' then
+ v_str = table.concat(v, ', ')
+ elseif type(v) == 'function' then
+ v_str = func_tostring(v)
+ else
+ v_str = vim.inspect(v, { newline = '\n ' })
+ end
+
+ if k == 'cmd' and type(v) == 'table' and vim.fn.executable(v[1]) == 0 then
+ report_warn(("'%s' is not executable. Configuration will not be used."):format(v[1]))
+ end
+
+ if v_str then
+ text[#text + 1] = ('- %s: %s'):format(k, v_str)
+ end
+ end
+ text[#text + 1] = ''
+ report_info(table.concat(text, '\n'))
+ end
+end
+
--- Performs a healthcheck for LSP
function M.check()
check_log()
check_active_clients()
+ check_enabled_configs()
check_watcher()
check_position_encodings()
end
diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua
index f1ae9a8e9e..37e1202d1d 100644
--- a/runtime/lua/vim/lsp/inlay_hint.lua
+++ b/runtime/lua/vim/lsp/inlay_hint.lua
@@ -29,8 +29,8 @@ local bufstates = vim.defaulttable(function(_)
})
end)
-local namespace = api.nvim_create_namespace('vim_lsp_inlayhint')
-local augroup = api.nvim_create_augroup('vim_lsp_inlayhint', {})
+local namespace = api.nvim_create_namespace('nvim.lsp.inlayhint')
+local augroup = api.nvim_create_augroup('nvim.lsp.inlayhint', {})
--- |lsp-handler| for the method `textDocument/inlayHint`
--- Store hints for a specific buffer and client
@@ -122,12 +122,12 @@ end
--- local hint = vim.lsp.inlay_hint.get({ bufnr = 0 })[1] -- 0 for current buffer
---
--- local client = vim.lsp.get_client_by_id(hint.client_id)
---- local resp = client.request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0)
+--- local resp = client:request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0)
--- local resolved_hint = assert(resp and resp.result, resp.err)
--- vim.lsp.util.apply_text_edits(resolved_hint.textEdits, 0, client.encoding)
---
--- location = resolved_hint.label[1].location
---- client.request('textDocument/hover', {
+--- client:request('textDocument/hover', {
--- textDocument = { uri = location.uri },
--- position = location.range.start,
--- })
@@ -149,8 +149,8 @@ function M.get(filter)
vim.list_extend(hints, M.get(vim.tbl_extend('keep', { bufnr = buf }, filter)))
end, vim.api.nvim_list_bufs())
return hints
- elseif bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
+ else
+ bufnr = vim._resolve_bufnr(bufnr)
end
local bufstate = bufstates[bufnr]
@@ -203,9 +203,7 @@ end
--- Clear inlay hints
---@param bufnr (integer) Buffer handle, or 0 for current
local function clear(bufnr)
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local bufstate = bufstates[bufnr]
local client_lens = (bufstate or {}).client_hints or {}
local client_ids = vim.tbl_keys(client_lens) --- @type integer[]
@@ -221,9 +219,7 @@ end
--- Disable inlay hints for a buffer
---@param bufnr (integer) Buffer handle, or 0 for current
local function _disable(bufnr)
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
clear(bufnr)
bufstates[bufnr] = nil
bufstates[bufnr].enabled = false
@@ -242,9 +238,7 @@ end
--- Enable inlay hints for a buffer
---@param bufnr (integer) Buffer handle, or 0 for current
local function _enable(bufnr)
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
bufstates[bufnr] = nil
bufstates[bufnr].enabled = true
_refresh(bufnr)
@@ -371,13 +365,10 @@ function M.is_enabled(filter)
filter = filter or {}
local bufnr = filter.bufnr
- vim.validate('bufnr', bufnr, 'number', true)
if bufnr == nil then
return globalstate.enabled
- elseif bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
end
- return bufstates[bufnr].enabled
+ return bufstates[vim._resolve_bufnr(bufnr)].enabled
end
--- Optional filters |kwargs|, or `nil` for all.
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
index 7db48b0c06..fbfd0cd6b0 100644
--- a/runtime/lua/vim/lsp/protocol.lua
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -15,7 +15,6 @@ local sysname = vim.uv.os_uname().sysname
--- @class vim.lsp.protocol.constants
--- @nodoc
local constants = {
- --- @enum lsp.DiagnosticSeverity
DiagnosticSeverity = {
-- Reports an error.
Error = 1,
@@ -27,7 +26,6 @@ local constants = {
Hint = 4,
},
- --- @enum lsp.DiagnosticTag
DiagnosticTag = {
-- Unused or unnecessary code
Unnecessary = 1,
@@ -35,7 +33,6 @@ local constants = {
Deprecated = 2,
},
- ---@enum lsp.MessageType
MessageType = {
-- An error message.
Error = 1,
@@ -50,7 +47,6 @@ local constants = {
},
-- The file event type.
- ---@enum lsp.FileChangeType
FileChangeType = {
-- The file got created.
Created = 1,
@@ -149,7 +145,6 @@ local constants = {
},
-- Represents reasons why a text document is saved.
- ---@enum lsp.TextDocumentSaveReason
TextDocumentSaveReason = {
-- Manually triggered, e.g. by the user pressing save, by starting debugging,
-- or by an API call.
@@ -174,6 +169,7 @@ local constants = {
-- Defined by the protocol.
RequestCancelled = -32800,
ContentModified = -32801,
+ ServerCancelled = -32802,
},
-- Describes the content type that a client supports in various
@@ -245,7 +241,6 @@ local constants = {
-- Defines whether the insert text in a completion item should be interpreted as
-- plain text or a snippet.
- --- @enum lsp.InsertTextFormat
InsertTextFormat = {
-- The primary text to be inserted is treated as a plain string.
PlainText = 1,
@@ -304,7 +299,6 @@ local constants = {
SourceOrganizeImports = 'source.organizeImports',
},
-- The reason why code actions were requested.
- ---@enum lsp.CodeActionTriggerKind
CodeActionTriggerKind = {
-- Code actions were explicitly requested by the user or by an extension.
Invoked = 1,
@@ -439,6 +433,13 @@ function protocol.make_client_capabilities()
properties = { 'command' },
},
},
+ foldingRange = {
+ dynamicRegistration = false,
+ lineFoldingOnly = true,
+ foldingRange = {
+ collapsedText = true,
+ },
+ },
formatting = {
dynamicRegistration = true,
},
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
index 6c8564845f..a0d1fe776b 100644
--- a/runtime/lua/vim/lsp/rpc.lua
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -1,18 +1,8 @@
-local uv = vim.uv
local log = require('vim.lsp.log')
local protocol = require('vim.lsp.protocol')
+local lsp_transport = require('vim.lsp._transport')
local validate, schedule_wrap = vim.validate, vim.schedule_wrap
-local is_win = vim.fn.has('win32') == 1
-
---- Checks whether a given path exists and is a directory.
----@param filename string path to check
----@return boolean
-local function is_dir(filename)
- local stat = uv.fs_stat(filename)
- return stat and stat.type == 'directory' or false
-end
-
--- Embeds the given string into a table and correctly computes `Content-Length`.
---
---@param message string
@@ -242,8 +232,11 @@ local default_dispatchers = {
end,
}
----@private
-function M.create_read_loop(handle_body, on_no_chunk, on_error)
+--- @private
+--- @param handle_body fun(body: string)
+--- @param on_exit? fun()
+--- @param on_error fun(err: any)
+function M.create_read_loop(handle_body, on_exit, on_error)
local parse_chunk = coroutine.wrap(request_parser_loop) --[[@as fun(chunk: string?): vim.lsp.rpc.Headers?, string?]]
parse_chunk()
return function(err, chunk)
@@ -253,8 +246,8 @@ function M.create_read_loop(handle_body, on_no_chunk, on_error)
end
if not chunk then
- if on_no_chunk then
- on_no_chunk()
+ if on_exit then
+ on_exit()
end
return
end
@@ -262,7 +255,7 @@ function M.create_read_loop(handle_body, on_no_chunk, on_error)
while true do
local headers, body = parse_chunk(chunk)
if headers then
- handle_body(body)
+ handle_body(assert(body))
chunk = ''
else
break
@@ -282,14 +275,14 @@ local Client = {}
---@private
function Client:encode_and_send(payload)
log.debug('rpc.send', payload)
- if self.transport.is_closing() then
+ if self.transport:is_closing() then
return false
end
local jsonstr = assert(
vim.json.encode(payload),
string.format("Couldn't encode payload '%s'", vim.inspect(payload))
)
- self.transport.write(format_message_with_content_length(jsonstr))
+ self.transport:write(format_message_with_content_length(jsonstr))
return true
end
@@ -323,7 +316,7 @@ end
---@param method string The invoked LSP method
---@param params table? Parameters for the invoked LSP method
---@param callback fun(err?: lsp.ResponseError, result: any) Callback to invoke
----@param notify_reply_callback fun(message_id: integer)|nil Callback to invoke as soon as a request is no longer pending
+---@param notify_reply_callback? fun(message_id: integer) Callback to invoke as soon as a request is no longer pending
---@return boolean success `true` if request could be sent, `false` if not
---@return integer? message_id if request could be sent, `nil` if not
function Client:request(method, params, callback, notify_reply_callback)
@@ -337,21 +330,16 @@ function Client:request(method, params, callback, notify_reply_callback)
method = method,
params = params,
})
- local message_callbacks = self.message_callbacks
- local notify_reply_callbacks = self.notify_reply_callbacks
- if result then
- if message_callbacks then
- message_callbacks[message_id] = schedule_wrap(callback)
- else
- return false, nil
- end
- if notify_reply_callback and notify_reply_callbacks then
- notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
- end
- return result, message_id
- else
- return false, nil
+
+ if not result then
+ return false
+ end
+
+ self.message_callbacks[message_id] = schedule_wrap(callback)
+ if notify_reply_callback then
+ self.notify_reply_callbacks[message_id] = schedule_wrap(notify_reply_callback)
end
+ return result, message_id
end
---@package
@@ -370,7 +358,7 @@ end
---@param ... any
---@return boolean status
---@return any head
----@return any|nil ...
+---@return any? ...
function Client:pcall_handler(errkind, status, head, ...)
if not status then
self:on_error(errkind, head, ...)
@@ -385,7 +373,7 @@ end
---@param ... any
---@return boolean status
---@return any head
----@return any|nil ...
+---@return any? ...
function Client:try_call(errkind, fn, ...)
return self:pcall_handler(errkind, pcall(fn, ...))
end
@@ -394,7 +382,8 @@ end
-- time and log them. This would require storing the timestamp. I could call
-- them with an error then, perhaps.
----@package
+--- @package
+--- @param body string
function Client:handle_body(body)
local ok, decoded = pcall(vim.json.decode, body, { luanil = { object = true } })
if not ok then
@@ -406,7 +395,7 @@ function Client:handle_body(body)
if type(decoded) ~= 'table' then
self:on_error(M.client_errors.INVALID_SERVER_MESSAGE, decoded)
elseif type(decoded.method) == 'string' and decoded.id then
- local err --- @type lsp.ResponseError|nil
+ local err --- @type lsp.ResponseError?
-- Schedule here so that the users functions don't trigger an error and
-- we can still use the result.
vim.schedule(coroutine.wrap(function()
@@ -453,45 +442,36 @@ function Client:handle_body(body)
local result_id = assert(tonumber(decoded.id), 'response id must be a number')
-- Notify the user that a response was received for the request
- local notify_reply_callbacks = self.notify_reply_callbacks
- local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id]
+ local notify_reply_callback = self.notify_reply_callbacks[result_id]
if notify_reply_callback then
validate('notify_reply_callback', notify_reply_callback, 'function')
notify_reply_callback(result_id)
- notify_reply_callbacks[result_id] = nil
+ self.notify_reply_callbacks[result_id] = nil
end
- local message_callbacks = self.message_callbacks
-
-- Do not surface RequestCancelled to users, it is RPC-internal.
if decoded.error then
- local mute_error = false
+ assert(type(decoded.error) == 'table')
if decoded.error.code == protocol.ErrorCodes.RequestCancelled then
log.debug('Received cancellation ack', decoded)
- mute_error = true
- end
-
- if mute_error then
-- Clear any callback since this is cancelled now.
-- This is safe to do assuming that these conditions hold:
-- - The server will not send a result callback after this cancellation.
-- - If the server sent this cancellation ACK after sending the result, the user of this RPC
-- client will ignore the result themselves.
- if result_id and message_callbacks then
- message_callbacks[result_id] = nil
+ if result_id then
+ self.message_callbacks[result_id] = nil
end
return
end
end
- local callback = message_callbacks and message_callbacks[result_id]
+ local callback = self.message_callbacks[result_id]
if callback then
- message_callbacks[result_id] = nil
+ self.message_callbacks[result_id] = nil
validate('callback', callback, 'function')
if decoded.error then
- decoded.error = setmetatable(decoded.error, {
- __tostring = M.format_rpc_error,
- })
+ setmetatable(decoded.error, { __tostring = M.format_rpc_error })
end
self:try_call(
M.client_errors.SERVER_RESULT_CALLBACK_ERROR,
@@ -517,11 +497,6 @@ function Client:handle_body(body)
end
end
----@class (private) vim.lsp.rpc.Transport
----@field write fun(msg: string)
----@field is_closing fun(): boolean
----@field terminate fun()
-
---@param dispatchers vim.lsp.rpc.Dispatchers
---@param transport vim.lsp.rpc.Transport
---@return vim.lsp.rpc.Client
@@ -536,11 +511,20 @@ local function new_client(dispatchers, transport)
return setmetatable(state, { __index = Client })
end
----@class vim.lsp.rpc.PublicClient
----@field request fun(method: string, params: table?, callback: fun(err: lsp.ResponseError|nil, result: any), notify_reply_callback: fun(message_id: integer)|nil):boolean,integer? see |vim.lsp.rpc.request()|
----@field notify fun(method: string, params: any):boolean see |vim.lsp.rpc.notify()|
----@field is_closing fun(): boolean
----@field terminate fun()
+--- Client RPC object
+--- @class vim.lsp.rpc.PublicClient
+---
+--- See [vim.lsp.rpc.request()]
+--- @field request fun(method: string, params: table?, callback: fun(err?: lsp.ResponseError, result: any), notify_reply_callback?: fun(message_id: integer)):boolean,integer?
+---
+--- See [vim.lsp.rpc.notify()]
+--- @field notify fun(method: string, params: any): boolean
+---
+--- Indicates if the RPC is closing.
+--- @field is_closing fun(): boolean
+---
+--- Terminates the RPC client.
+--- @field terminate fun()
---@param client vim.lsp.rpc.Client
---@return vim.lsp.rpc.PublicClient
@@ -551,20 +535,20 @@ local function public_client(client)
---@private
function result.is_closing()
- return client.transport.is_closing()
+ return client.transport:is_closing()
end
---@private
function result.terminate()
- client.transport.terminate()
+ client.transport:terminate()
end
--- Sends a request to the LSP server and runs {callback} upon response.
---
---@param method (string) The invoked LSP method
---@param params (table?) Parameters for the invoked LSP method
- ---@param callback fun(err: lsp.ResponseError|nil, result: any) Callback to invoke
- ---@param notify_reply_callback fun(message_id: integer)|nil Callback to invoke as soon as a request is no longer pending
+ ---@param callback fun(err: lsp.ResponseError?, result: any) Callback to invoke
+ ---@param notify_reply_callback? fun(message_id: integer) Callback to invoke as soon as a request is no longer pending
---@return boolean success `true` if request could be sent, `false` if not
---@return integer? message_id if request could be sent, `nil` if not
function result.request(method, params, callback, notify_reply_callback)
@@ -610,6 +594,21 @@ local function merge_dispatchers(dispatchers)
return merged
end
+--- @param client vim.lsp.rpc.Client
+--- @param on_exit? fun()
+local function create_client_read_loop(client, on_exit)
+ --- @param body string
+ local function handle_body(body)
+ client:handle_body(body)
+ end
+
+ local function on_error(err)
+ client:on_error(M.client_errors.READ_ERROR, err)
+ end
+
+ return M.create_read_loop(handle_body, on_exit, on_error)
+end
+
--- Create a LSP RPC client factory that connects to either:
---
--- - a named pipe (windows)
@@ -617,83 +616,26 @@ end
--- - a host and port via TCP
---
--- Return a function that can be passed to the `cmd` field for
---- |vim.lsp.start_client()| or |vim.lsp.start()|.
+--- |vim.lsp.start()|.
---
---@param host_or_path string host to connect to or path to a pipe/domain socket
---@param port integer? TCP port to connect to. If absent the first argument must be a pipe
---@return fun(dispatchers: vim.lsp.rpc.Dispatchers): vim.lsp.rpc.PublicClient
function M.connect(host_or_path, port)
+ validate('host_or_path', host_or_path, 'string')
+ validate('port', port, 'number', true)
+
return function(dispatchers)
+ validate('dispatchers', dispatchers, 'table', true)
+
dispatchers = merge_dispatchers(dispatchers)
- local handle = (
- port == nil
- and assert(
- uv.new_pipe(false),
- string.format('Pipe with name %s could not be opened.', host_or_path)
- )
- or assert(uv.new_tcp(), 'Could not create new TCP socket')
- )
- local closing = false
- -- Connect returns a PublicClient synchronously so the caller
- -- can immediately send messages before the connection is established
- -- -> Need to buffer them until that happens
- local connected = false
- -- size should be enough because the client can't really do anything until initialization is done
- -- which required a response from the server - implying the connection got established
- local msgbuf = vim.ringbuf(10)
- local transport = {
- write = function(msg)
- if connected then
- local _, err = handle:write(msg)
- if err and not closing then
- log.error('Error on handle:write: %q', err)
- end
- else
- msgbuf:push(msg)
- end
- end,
- is_closing = function()
- return closing
- end,
- terminate = function()
- if not closing then
- closing = true
- handle:shutdown()
- handle:close()
- dispatchers.on_exit(0, 0)
- end
- end,
- }
+
+ local transport = lsp_transport.TransportConnect.new()
local client = new_client(dispatchers, transport)
- local function on_connect(err)
- if err then
- local address = port == nil and host_or_path or (host_or_path .. ':' .. port)
- vim.schedule(function()
- vim.notify(
- string.format('Could not connect to %s, reason: %s', address, vim.inspect(err)),
- vim.log.levels.WARN
- )
- end)
- return
- end
- local handle_body = function(body)
- client:handle_body(body)
- end
- handle:read_start(M.create_read_loop(handle_body, transport.terminate, function(read_err)
- client:on_error(M.client_errors.READ_ERROR, read_err)
- end))
- connected = true
- for msg in msgbuf do
- handle:write(msg)
- end
- end
- if port == nil then
- handle:connect(host_or_path, on_connect)
- else
- local info = uv.getaddrinfo(host_or_path, nil)
- local resolved_host = info and info[1] and info[1].addr or host_or_path
- handle:connect(resolved_host, port, on_connect)
- end
+ local on_read = create_client_read_loop(client, function()
+ transport:terminate()
+ end)
+ transport:connect(host_or_path, port, on_read, dispatchers.on_exit)
return public_client(client)
end
@@ -713,83 +655,19 @@ end
--- @param cmd string[] Command to start the LSP server.
--- @param dispatchers? vim.lsp.rpc.Dispatchers
--- @param extra_spawn_params? vim.lsp.rpc.ExtraSpawnParams
---- @return vim.lsp.rpc.PublicClient : Client RPC object, with these methods:
---- - `notify()` |vim.lsp.rpc.notify()|
---- - `request()` |vim.lsp.rpc.request()|
---- - `is_closing()` returns a boolean indicating if the RPC is closing.
---- - `terminate()` terminates the RPC client.
+--- @return vim.lsp.rpc.PublicClient
function M.start(cmd, dispatchers, extra_spawn_params)
log.info('Starting RPC client', { cmd = cmd, extra = extra_spawn_params })
validate('cmd', cmd, 'table')
validate('dispatchers', dispatchers, 'table', true)
- extra_spawn_params = extra_spawn_params or {}
-
- if extra_spawn_params.cwd then
- assert(is_dir(extra_spawn_params.cwd), 'cwd must be a directory')
- end
-
dispatchers = merge_dispatchers(dispatchers)
- local sysobj ---@type vim.SystemObj
-
- local client = new_client(dispatchers, {
- write = function(msg)
- sysobj:write(msg)
- end,
- is_closing = function()
- return sysobj == nil or sysobj:is_closing()
- end,
- terminate = function()
- sysobj:kill(15)
- end,
- })
-
- local handle_body = function(body)
- client:handle_body(body)
- end
-
- local stdout_handler = M.create_read_loop(handle_body, nil, function(err)
- client:on_error(M.client_errors.READ_ERROR, err)
- end)
-
- local stderr_handler = function(_, chunk)
- if chunk then
- log.error('rpc', cmd[1], 'stderr', chunk)
- end
- end
-
- local detached = not is_win
- if extra_spawn_params.detached ~= nil then
- detached = extra_spawn_params.detached
- end
-
- local ok, sysobj_or_err = pcall(vim.system, cmd, {
- stdin = true,
- stdout = stdout_handler,
- stderr = stderr_handler,
- cwd = extra_spawn_params.cwd,
- env = extra_spawn_params.env,
- detach = detached,
- }, function(obj)
- dispatchers.on_exit(obj.code, obj.signal)
- end)
-
- if not ok then
- local err = sysobj_or_err --[[@as string]]
- local sfx --- @type string
- if string.match(err, 'ENOENT') then
- sfx = '. The language server is either not installed, missing from PATH, or not executable.'
- else
- sfx = string.format(' with error message: %s', err)
- end
- local msg =
- string.format('Spawning language server with cmd: `%s` failed%s', vim.inspect(cmd), sfx)
- error(msg)
- end
-
- sysobj = sysobj_or_err --[[@as vim.SystemObj]]
+ local transport = lsp_transport.TransportRun.new()
+ local client = new_client(dispatchers, transport)
+ local on_read = create_client_read_loop(client)
+ transport:run(cmd, extra_spawn_params, on_read, dispatchers.on_exit)
return public_client(client)
end
diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua
index 215e5f41aa..dd8b654856 100644
--- a/runtime/lua/vim/lsp/semantic_tokens.lua
+++ b/runtime/lua/vim/lsp/semantic_tokens.lua
@@ -139,7 +139,7 @@ local function tokens_to_ranges(data, bufnr, client, request)
if token_type then
local modifiers = modifiers_from_number(data[i + 4], token_modifiers)
- local end_char = start_char + data[i + 2]
+ local end_char = start_char + data[i + 2] --- @type integer LuaLS bug
local buf_line = lines and lines[line + 1] or ''
local start_col = vim.str_byteindex(buf_line, encoding, start_char, false)
local end_col = vim.str_byteindex(buf_line, encoding, end_char, false)
@@ -166,7 +166,7 @@ function STHighlighter.new(bufnr)
local self = setmetatable({}, { __index = STHighlighter })
self.bufnr = bufnr
- self.augroup = api.nvim_create_augroup('vim_lsp_semantic_tokens:' .. bufnr, { clear = true })
+ self.augroup = api.nvim_create_augroup('nvim.lsp.semantic_tokens:' .. bufnr, { clear = true })
self.client_state = {}
STHighlighter.active[bufnr] = self
@@ -225,7 +225,7 @@ function STHighlighter:attach(client_id)
local state = self.client_state[client_id]
if not state then
state = {
- namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens:' .. client_id),
+ namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens:' .. client_id),
active_request = {},
current_result = {},
}
@@ -273,7 +273,7 @@ function STHighlighter:send_request()
if client and current_result.version ~= version and active_request.version ~= version then
-- cancel stale in-flight request
if active_request.request_id then
- client.cancel_request(active_request.request_id)
+ client:cancel_request(active_request.request_id)
active_request = {}
state.active_request = active_request
end
@@ -288,7 +288,7 @@ function STHighlighter:send_request()
method = method .. '/delta'
params.previousResultId = current_result.result_id
end
- local success, request_id = client.request(method, params, function(err, response, ctx)
+ local success, request_id = client:request(method, params, function(err, response, ctx)
-- look client up again using ctx.client_id instead of using a captured
-- client object
local c = vim.lsp.get_client_by_id(ctx.client_id)
@@ -519,7 +519,7 @@ function STHighlighter:reset()
if state.active_request.request_id then
local client = vim.lsp.get_client_by_id(client_id)
assert(client)
- client.cancel_request(state.active_request.request_id)
+ client:cancel_request(state.active_request.request_id)
state.active_request = {}
end
end
@@ -547,7 +547,7 @@ function STHighlighter:mark_dirty(client_id)
if state.active_request.request_id then
local client = vim.lsp.get_client_by_id(client_id)
assert(client)
- client.cancel_request(state.active_request.request_id)
+ client:cancel_request(state.active_request.request_id)
state.active_request = {}
end
end
@@ -600,9 +600,7 @@ function M.start(bufnr, client_id, opts)
vim.validate('bufnr', bufnr, 'number')
vim.validate('client_id', client_id, 'number')
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
opts = opts or {}
assert(
@@ -655,9 +653,7 @@ function M.stop(bufnr, client_id)
vim.validate('bufnr', bufnr, 'number')
vim.validate('client_id', client_id, 'number')
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local highlighter = STHighlighter.active[bufnr]
if not highlighter then
@@ -691,9 +687,7 @@ end
--- - modifiers (table) token modifiers as a set. E.g., { static = true, readonly = true }
--- - client_id (integer)
function M.get_at_pos(bufnr, row, col)
- if bufnr == nil or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local highlighter = STHighlighter.active[bufnr]
if not highlighter then
@@ -739,8 +733,7 @@ function M.force_refresh(bufnr)
vim.validate('bufnr', bufnr, 'number', true)
local buffers = bufnr == nil and vim.tbl_keys(STHighlighter.active)
- or bufnr == 0 and { api.nvim_get_current_buf() }
- or { bufnr }
+ or { vim._resolve_bufnr(bufnr) }
for _, buffer in ipairs(buffers) do
local highlighter = STHighlighter.active[buffer]
@@ -770,9 +763,7 @@ end
---@param hl_group (string) Highlight group name
---@param opts? vim.lsp.semantic_tokens.highlight_token.Opts Optional parameters:
function M.highlight_token(token, bufnr, client_id, hl_group, opts)
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local highlighter = STHighlighter.active[bufnr]
if not highlighter then
return
@@ -814,7 +805,7 @@ function M._refresh(err, _, ctx)
return vim.NIL
end
-local namespace = api.nvim_create_namespace('vim_lsp_semantic_tokens')
+local namespace = api.nvim_create_namespace('nvim.lsp.semantic_tokens')
api.nvim_set_decoration_provider(namespace, {
on_win = function(_, _, bufnr, topline, botline)
local highlighter = STHighlighter.active[bufnr]
diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua
index 3df45ebff0..621f63b25f 100644
--- a/runtime/lua/vim/lsp/sync.lua
+++ b/runtime/lua/vim/lsp/sync.lua
@@ -48,21 +48,21 @@ local str_utfindex = vim.str_utfindex
local str_utf_start = vim.str_utf_start
local str_utf_end = vim.str_utf_end
--- Given a line, byte idx, alignment, and offset_encoding convert to the aligned
+-- Given a line, byte idx, alignment, and position_encoding convert to the aligned
-- utf-8 index and either the utf-16, or utf-32 index.
---@param line string the line to index into
---@param byte integer the byte idx
----@param offset_encoding string utf-8|utf-16|utf-32|nil (default: utf-8)
+---@param position_encoding string utf-8|utf-16|utf-32|nil (default: utf-8)
---@return integer byte_idx of first change position
---@return integer char_idx of first change position
-local function align_end_position(line, byte, offset_encoding)
+local function align_end_position(line, byte, position_encoding)
local char --- @type integer
-- If on the first byte, or an empty string: the trivial case
if byte == 1 or #line == 0 then
char = byte
-- Called in the case of extending an empty line "" -> "a"
elseif byte == #line + 1 then
- char = str_utfindex(line, offset_encoding) + 1
+ char = str_utfindex(line, position_encoding) + 1
else
-- Modifying line, find the nearest utf codepoint
local offset = str_utf_start(line, byte)
@@ -73,9 +73,9 @@ local function align_end_position(line, byte, offset_encoding)
end
if byte <= #line then
--- Convert to 0 based for input, and from 0 based for output
- char = str_utfindex(line, offset_encoding, byte - 1) + 1
+ char = str_utfindex(line, position_encoding, byte - 1) + 1
else
- char = str_utfindex(line, offset_encoding) + 1
+ char = str_utfindex(line, position_encoding) + 1
end
-- Extending line, find the nearest utf codepoint for the last valid character
end
@@ -93,7 +93,7 @@ end
---@param firstline integer firstline from on_lines, adjusted to 1-index
---@param lastline integer lastline from on_lines, adjusted to 1-index
---@param new_lastline integer new_lastline from on_lines, adjusted to 1-index
----@param offset_encoding string utf-8|utf-16|utf-32|nil (fallback to utf-8)
+---@param position_encoding string utf-8|utf-16|utf-32|nil (fallback to utf-8)
---@return vim.lsp.sync.Range result table include line_idx, byte_idx, and char_idx of first change position
local function compute_start_range(
prev_lines,
@@ -101,7 +101,7 @@ local function compute_start_range(
firstline,
lastline,
new_lastline,
- offset_encoding
+ position_encoding
)
local char_idx --- @type integer?
local byte_idx --- @type integer?
@@ -115,7 +115,7 @@ local function compute_start_range(
if line then
line_idx = firstline - 1
byte_idx = #line + 1
- char_idx = str_utfindex(line, offset_encoding) + 1
+ char_idx = str_utfindex(line, position_encoding) + 1
else
line_idx = firstline
byte_idx = 1
@@ -152,11 +152,11 @@ local function compute_start_range(
char_idx = 1
elseif start_byte_idx == #prev_line + 1 then
byte_idx = start_byte_idx
- char_idx = str_utfindex(prev_line, offset_encoding) + 1
+ char_idx = str_utfindex(prev_line, position_encoding) + 1
else
byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx)
--- Convert to 0 based for input, and from 0 based for output
- char_idx = vim.str_utfindex(prev_line, offset_encoding, byte_idx - 1) + 1
+ char_idx = vim.str_utfindex(prev_line, position_encoding, byte_idx - 1) + 1
end
-- Return the start difference (shared for new and prev lines)
@@ -174,7 +174,7 @@ end
---@param firstline integer
---@param lastline integer
---@param new_lastline integer
----@param offset_encoding string
+---@param position_encoding string
---@return vim.lsp.sync.Range prev_end_range
---@return vim.lsp.sync.Range curr_end_range
local function compute_end_range(
@@ -184,7 +184,7 @@ local function compute_end_range(
firstline,
lastline,
new_lastline,
- offset_encoding
+ position_encoding
)
-- A special case for the following `firstline == new_lastline` case where lines are deleted.
-- Even if the buffer has become empty, nvim behaves as if it has an empty line with eol.
@@ -193,7 +193,7 @@ local function compute_end_range(
return {
line_idx = lastline - 1,
byte_idx = #prev_line + 1,
- char_idx = str_utfindex(prev_line, offset_encoding) + 1,
+ char_idx = str_utfindex(prev_line, position_encoding) + 1,
}, { line_idx = 1, byte_idx = 1, char_idx = 1 }
end
-- If firstline == new_lastline, the first change occurred on a line that was deleted.
@@ -259,7 +259,7 @@ local function compute_end_range(
prev_end_byte_idx = 1
end
local prev_byte_idx, prev_char_idx =
- align_end_position(prev_line, prev_end_byte_idx, offset_encoding)
+ align_end_position(prev_line, prev_end_byte_idx, position_encoding)
local prev_end_range =
{ line_idx = prev_line_idx, byte_idx = prev_byte_idx, char_idx = prev_char_idx }
@@ -274,7 +274,7 @@ local function compute_end_range(
curr_end_byte_idx = 1
end
local curr_byte_idx, curr_char_idx =
- align_end_position(curr_line, curr_end_byte_idx, offset_encoding)
+ align_end_position(curr_line, curr_end_byte_idx, position_encoding)
curr_end_range =
{ line_idx = curr_line_idx, byte_idx = curr_byte_idx, char_idx = curr_char_idx }
end
@@ -317,7 +317,7 @@ local function extract_text(lines, start_range, end_range, line_ending)
end
end
--- rangelength depends on the offset encoding
+-- rangelength depends on the position encoding
-- bytes for utf-8 (clangd with extension)
-- codepoints for utf-16
-- codeunits for utf-32
@@ -326,10 +326,10 @@ end
---@param lines string[]
---@param start_range vim.lsp.sync.Range
---@param end_range vim.lsp.sync.Range
----@param offset_encoding string
+---@param position_encoding string
---@param line_ending string
---@return integer
-local function compute_range_length(lines, start_range, end_range, offset_encoding, line_ending)
+local function compute_range_length(lines, start_range, end_range, position_encoding, line_ending)
local line_ending_length = #line_ending
-- Single line case
if start_range.line_idx == end_range.line_idx then
@@ -339,7 +339,7 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi
local start_line = lines[start_range.line_idx]
local range_length --- @type integer
if start_line and #start_line > 0 then
- range_length = str_utfindex(start_line, offset_encoding)
+ range_length = str_utfindex(start_line, position_encoding)
- start_range.char_idx
+ 1
+ line_ending_length
@@ -352,7 +352,7 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi
for idx = start_range.line_idx + 1, end_range.line_idx - 1 do
-- Length full line plus newline character
if #lines[idx] > 0 then
- range_length = range_length + str_utfindex(lines[idx], offset_encoding) + #line_ending
+ range_length = range_length + str_utfindex(lines[idx], position_encoding) + #line_ending
else
range_length = range_length + line_ending_length
end
@@ -372,7 +372,7 @@ end
---@param firstline integer line to begin search for first difference
---@param lastline integer line to begin search in old_lines for last difference
---@param new_lastline integer line to begin search in new_lines for last difference
----@param offset_encoding string encoding requested by language server
+---@param position_encoding string encoding requested by language server
---@param line_ending string
---@return lsp.TextDocumentContentChangeEvent : see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
function M.compute_diff(
@@ -381,7 +381,7 @@ function M.compute_diff(
firstline,
lastline,
new_lastline,
- offset_encoding,
+ position_encoding,
line_ending
)
-- Find the start of changes between the previous and current buffer. Common between both.
@@ -393,7 +393,7 @@ function M.compute_diff(
firstline + 1,
lastline + 1,
new_lastline + 1,
- offset_encoding
+ position_encoding
)
-- Find the last position changed in the previous and current buffer.
-- prev_end_range is sent to the server as as the end of the changed range.
@@ -405,7 +405,7 @@ function M.compute_diff(
firstline + 1,
lastline + 1,
new_lastline + 1,
- offset_encoding
+ position_encoding
)
-- Grab the changed text of from start_range to curr_end_range in the current buffer.
@@ -414,7 +414,7 @@ function M.compute_diff(
-- Compute the range of the replaced text. Deprecated but still required for certain language servers
local range_length =
- compute_range_length(prev_lines, start_range, prev_end_range, offset_encoding, line_ending)
+ compute_range_length(prev_lines, start_range, prev_end_range, position_encoding, line_ending)
-- convert to 0 based indexing
local result = {
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 6eab0f3da4..e16a905c44 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -49,7 +49,8 @@ local function get_border_size(opts)
if not border_size[border] then
border_error(border)
end
- return unpack(border_size[border])
+ local r = border_size[border]
+ return r[1], r[2]
end
if 8 % #border ~= 0 then
@@ -192,9 +193,7 @@ local function get_lines(bufnr, rows)
rows = type(rows) == 'table' and rows or { rows }
-- This is needed for bufload and bufloaded
- if bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ bufnr = vim._resolve_bufnr(bufnr)
local function buf_lines()
local lines = {} --- @type table<integer,string>
@@ -277,9 +276,9 @@ end
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
---@param position lsp.Position
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@return integer
-local function get_line_byte_from_position(bufnr, position, offset_encoding)
+local function get_line_byte_from_position(bufnr, position, position_encoding)
-- LSP's line and characters are 0-indexed
-- Vim's line and columns are 1-indexed
local col = position.character
@@ -287,7 +286,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding)
-- character
if col > 0 then
local line = get_line(bufnr, position.line) or ''
- return vim.str_byteindex(line, offset_encoding, col, false)
+ return vim.str_byteindex(line, position_encoding, col, false)
end
return col
end
@@ -295,12 +294,12 @@ end
--- Applies a list of text edits to a buffer.
---@param text_edits lsp.TextEdit[]
---@param bufnr integer Buffer id
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
-function M.apply_text_edits(text_edits, bufnr, offset_encoding)
+function M.apply_text_edits(text_edits, bufnr, position_encoding)
validate('text_edits', text_edits, 'table', false)
validate('bufnr', bufnr, 'number', false)
- validate('offset_encoding', offset_encoding, 'string', false)
+ validate('position_encoding', position_encoding, 'string', false)
if not next(text_edits) then
return
@@ -359,9 +358,9 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding)
-- Convert from LSP style ranges to Neovim style ranges.
local start_row = text_edit.range.start.line
- local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding)
+ local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, position_encoding)
local end_row = text_edit.range['end'].line
- local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding)
+ local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], position_encoding)
local text = vim.split(text_edit.newText, '\n', { plain = true })
local max = api.nvim_buf_line_count(bufnr)
@@ -430,14 +429,14 @@ end
---
---@param text_document_edit lsp.TextDocumentEdit
---@param index? integer: Optional index of the edit, if from a list of edits (or nil, if not from a list)
----@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
-function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
+function M.apply_text_document_edit(text_document_edit, index, position_encoding)
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr(text_document.uri)
- if offset_encoding == nil then
+ if position_encoding == nil then
vim.notify_once(
- 'apply_text_document_edit must be called with valid offset encoding',
+ 'apply_text_document_edit must be called with valid position encoding',
vim.log.levels.WARN
)
return
@@ -459,7 +458,7 @@ function M.apply_text_document_edit(text_document_edit, index, offset_encoding)
return
end
- M.apply_text_edits(text_document_edit.edits, bufnr, offset_encoding)
+ M.apply_text_edits(text_document_edit.edits, bufnr, position_encoding)
end
local function path_components(path)
@@ -619,12 +618,12 @@ end
--- Applies a `WorkspaceEdit`.
---
---@param workspace_edit lsp.WorkspaceEdit
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32' (required)
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32' (required)
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
-function M.apply_workspace_edit(workspace_edit, offset_encoding)
- if offset_encoding == nil then
+function M.apply_workspace_edit(workspace_edit, position_encoding)
+ if position_encoding == nil then
vim.notify_once(
- 'apply_workspace_edit must be called with valid offset encoding',
+ 'apply_workspace_edit must be called with valid position encoding',
vim.log.levels.WARN
)
return
@@ -641,7 +640,7 @@ function M.apply_workspace_edit(workspace_edit, offset_encoding)
elseif change.kind then --- @diagnostic disable-line:undefined-field
error(string.format('Unsupported change: %q', vim.inspect(change)))
else
- M.apply_text_document_edit(change, idx, offset_encoding)
+ M.apply_text_document_edit(change, idx, position_encoding)
end
end
return
@@ -654,7 +653,7 @@ function M.apply_workspace_edit(workspace_edit, offset_encoding)
for uri, changes in pairs(all_changes) do
local bufnr = vim.uri_to_bufnr(uri)
- M.apply_text_edits(changes, bufnr, offset_encoding)
+ M.apply_text_edits(changes, bufnr, position_encoding)
end
end
@@ -877,15 +876,16 @@ function M.make_floating_popup_options(width, height, opts)
return {
anchor = anchor,
+ row = row + (opts.offset_y or 0),
col = col + (opts.offset_x or 0),
height = height,
focusable = opts.focusable,
- relative = opts.relative == 'mouse' and 'mouse' or 'cursor',
- row = row + (opts.offset_y or 0),
+ relative = (opts.relative == 'mouse' or opts.relative == 'editor') and opts.relative
+ or 'cursor',
style = 'minimal',
width = width,
border = opts.border or default_border,
- zindex = opts.zindex or 50,
+ zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1,
title = title,
title_pos = title_pos,
}
@@ -904,17 +904,20 @@ end
--- Shows document and optionally jumps to the location.
---
---@param location lsp.Location|lsp.LocationLink
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'?
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'?
---@param opts? vim.lsp.util.show_document.Opts
---@return boolean `true` if succeeded
-function M.show_document(location, offset_encoding, opts)
+function M.show_document(location, position_encoding, opts)
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then
return false
end
- if offset_encoding == nil then
- vim.notify_once('show_document must be called with valid offset encoding', vim.log.levels.WARN)
+ if position_encoding == nil then
+ vim.notify_once(
+ 'show_document must be called with valid position encoding',
+ vim.log.levels.WARN
+ )
return false
end
local bufnr = vim.uri_to_bufnr(uri)
@@ -946,7 +949,7 @@ function M.show_document(location, offset_encoding, opts)
if range then
-- Jump to new location (adjusting for encoding of characters)
local row = range.start.line
- local col = get_line_byte_from_position(bufnr, range.start, offset_encoding)
+ local col = get_line_byte_from_position(bufnr, range.start, position_encoding)
api.nvim_win_set_cursor(win, { row + 1, col })
vim._with({ win = win }, function()
-- Open folds under the cursor
@@ -961,12 +964,12 @@ end
---
---@deprecated use `vim.lsp.util.show_document` with `{focus=true}` instead
---@param location lsp.Location|lsp.LocationLink
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'?
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'?
---@param reuse_win boolean? Jump to existing window if buffer is already open.
---@return boolean `true` if the jump succeeded
-function M.jump_to_location(location, offset_encoding, reuse_win)
+function M.jump_to_location(location, position_encoding, reuse_win)
vim.deprecate('vim.lsp.util.jump_to_location', nil, '0.12')
- return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true })
+ return M.show_document(location, position_encoding, { reuse_win = reuse_win, focus = true })
end
--- Previews a location in a floating window
@@ -1355,7 +1358,7 @@ end
---@param bufnrs table list of buffers where the preview window will remain visible
---@see autocmd-events
local function close_preview_autocmd(events, winnr, bufnrs)
- local augroup = api.nvim_create_augroup('preview_window_' .. winnr, {
+ local augroup = api.nvim_create_augroup('nvim.preview_window_' .. winnr, {
clear = true,
})
@@ -1430,7 +1433,7 @@ function M._make_floating_popup_size(contents, opts)
if vim.tbl_isempty(line_widths) then
for _, line in ipairs(contents) do
local line_width = vim.fn.strdisplaywidth(line:gsub('%z', '\n'))
- height = height + math.ceil(line_width / wrap_at)
+ height = height + math.max(1, math.ceil(line_width / wrap_at))
end
else
for i = 1, #contents do
@@ -1493,7 +1496,7 @@ end
--- @field title_pos? 'left'|'center'|'right'
---
--- (default: `'cursor'`)
---- @field relative? 'mouse'|'cursor'
+--- @field relative? 'mouse'|'cursor'|'editor'
---
--- - "auto": place window based on which side of the cursor has more lines
--- - "above": place the window above the cursor unless there are not enough lines
@@ -1566,8 +1569,6 @@ function M.open_floating_preview(contents, syntax, opts)
if do_stylize then
local width = M._make_floating_popup_size(contents, opts)
contents = M._normalize_markdown(contents, { width = width })
- vim.bo[floating_bufnr].filetype = 'markdown'
- vim.treesitter.start(floating_bufnr)
else
-- Clean up input: trim empty lines
contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true })
@@ -1617,9 +1618,22 @@ function M.open_floating_preview(contents, syntax, opts)
api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr)
end
- if do_stylize then
- vim.wo[floating_winnr].conceallevel = 2
+ local augroup_name = ('nvim.closing_floating_preview_%d'):format(floating_winnr)
+ local ok =
+ pcall(api.nvim_get_autocmds, { group = augroup_name, pattern = tostring(floating_winnr) })
+ if not ok then
+ api.nvim_create_autocmd('WinClosed', {
+ group = api.nvim_create_augroup(augroup_name, {}),
+ pattern = tostring(floating_winnr),
+ callback = function()
+ if api.nvim_buf_is_valid(bufnr) then
+ vim.b[bufnr].lsp_floating_preview = nil
+ end
+ api.nvim_del_augroup_by_name(augroup_name)
+ end,
+ })
end
+
vim.wo[floating_winnr].foldenable = false -- Disable folding.
vim.wo[floating_winnr].wrap = opts.wrap -- Soft wrapping.
vim.wo[floating_winnr].breakindent = true -- Slightly better list presentation.
@@ -1628,11 +1642,17 @@ function M.open_floating_preview(contents, syntax, opts)
vim.bo[floating_bufnr].modifiable = false
vim.bo[floating_bufnr].bufhidden = 'wipe'
+ if do_stylize then
+ vim.wo[floating_winnr].conceallevel = 2
+ vim.bo[floating_bufnr].filetype = 'markdown'
+ vim.treesitter.start(floating_bufnr)
+ end
+
return floating_bufnr, floating_winnr
end
do --[[ References ]]
- local reference_ns = api.nvim_create_namespace('vim_lsp_references')
+ local reference_ns = api.nvim_create_namespace('nvim.lsp.references')
--- Removes document highlights from a buffer.
---
@@ -1645,18 +1665,18 @@ do --[[ References ]]
---
---@param bufnr integer Buffer id
---@param references lsp.DocumentHighlight[] objects to highlight
- ---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
+ ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
- function M.buf_highlight_references(bufnr, references, offset_encoding)
+ function M.buf_highlight_references(bufnr, references, position_encoding)
validate('bufnr', bufnr, 'number', true)
- validate('offset_encoding', offset_encoding, 'string', false)
+ validate('position_encoding', position_encoding, 'string', false)
for _, reference in ipairs(references) do
local range = reference.range
local start_line = range.start.line
local end_line = range['end'].line
- local start_idx = get_line_byte_from_position(bufnr, range.start, offset_encoding)
- local end_idx = get_line_byte_from_position(bufnr, range['end'], offset_encoding)
+ local start_idx = get_line_byte_from_position(bufnr, range.start, position_encoding)
+ local end_idx = get_line_byte_from_position(bufnr, range['end'], position_encoding)
local document_highlight_kind = {
[protocol.DocumentHighlightKind.Text] = 'LspReferenceText',
@@ -1690,16 +1710,16 @@ end)
--- |setloclist()|.
---
---@param locations lsp.Location[]|lsp.LocationLink[]
----@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
--- default to first client of buffer
---@return vim.quickfix.entry[] # See |setqflist()| for the format
-function M.locations_to_items(locations, offset_encoding)
- if offset_encoding == nil then
+function M.locations_to_items(locations, position_encoding)
+ if position_encoding == nil then
vim.notify_once(
- 'locations_to_items must be called with valid offset encoding',
+ 'locations_to_items must be called with valid position encoding',
vim.log.levels.WARN
)
- offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding
+ position_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding
end
local items = {} --- @type vim.quickfix.entry[]
@@ -1736,8 +1756,8 @@ function M.locations_to_items(locations, offset_encoding)
local end_row = end_pos.line
local line = lines[row] or ''
local end_line = lines[end_row] or ''
- local col = vim.str_byteindex(line, offset_encoding, pos.character, false)
- local end_col = vim.str_byteindex(end_line, offset_encoding, end_pos.character, false)
+ local col = vim.str_byteindex(line, position_encoding, pos.character, false)
+ local end_col = vim.str_byteindex(end_line, position_encoding, end_pos.character, false)
items[#items + 1] = {
filename = filename,
@@ -1848,19 +1868,18 @@ function M.try_trim_markdown_code_blocks(lines)
end
---@param window integer?: window handle or 0 for current, defaults to current
----@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window`
-local function make_position_param(window, offset_encoding)
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
+local function make_position_param(window, position_encoding)
window = window or 0
local buf = api.nvim_win_get_buf(window)
local row, col = unpack(api.nvim_win_get_cursor(window))
- offset_encoding = offset_encoding or M._get_offset_encoding(buf)
row = row - 1
local line = api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
if not line then
return { line = 0, character = 0 }
end
- col = vim.str_utfindex(line, offset_encoding, col, false)
+ col = vim.str_utfindex(line, position_encoding, col, false)
return { line = row, character = col }
end
@@ -1868,20 +1887,28 @@ end
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
---@param window integer?: window handle or 0 for current, defaults to current
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window`
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@return lsp.TextDocumentPositionParams
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
-function M.make_position_params(window, offset_encoding)
+function M.make_position_params(window, position_encoding)
window = window or 0
local buf = api.nvim_win_get_buf(window)
- offset_encoding = offset_encoding or M._get_offset_encoding(buf)
+ if position_encoding == nil then
+ vim.notify_once(
+ 'position_encoding param is required in vim.lsp.util.make_position_params. Defaulting to position encoding of the first client.',
+ vim.log.levels.WARN
+ )
+ --- @diagnostic disable-next-line: deprecated
+ position_encoding = M._get_offset_encoding(buf)
+ end
return {
textDocument = M.make_text_document_params(buf),
- position = make_position_param(window, offset_encoding),
+ position = make_position_param(window, position_encoding),
}
end
--- Utility function for getting the encoding of the first LSP client on the given buffer.
+---@deprecated
---@param bufnr integer buffer handle or 0 for current, defaults to current
---@return string encoding first client if there is one, nil otherwise
function M._get_offset_encoding(bufnr)
@@ -1904,7 +1931,7 @@ function M._get_offset_encoding(bufnr)
offset_encoding = this_offset_encoding
elseif offset_encoding ~= this_offset_encoding then
vim.notify_once(
- 'warning: multiple different client offset_encodings detected for buffer, this is not supported yet',
+ 'warning: multiple different client offset_encodings detected for buffer, vim.lsp.util._get_offset_encoding() uses the offset_encoding from the first client',
vim.log.levels.WARN
)
end
@@ -1919,13 +1946,19 @@ end
--- `textDocument/rangeFormatting`.
---
---@param window integer? window handle or 0 for current, defaults to current
----@param offset_encoding "utf-8"|"utf-16"|"utf-32"? defaults to `offset_encoding` of first client of buffer of `window`
----@return table { textDocument = { uri = `current_file_uri` }, range = { start =
----`current_position`, end = `current_position` } }
-function M.make_range_params(window, offset_encoding)
+---@param position_encoding "utf-8"|"utf-16"|"utf-32"
+---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range }
+function M.make_range_params(window, position_encoding)
local buf = api.nvim_win_get_buf(window or 0)
- offset_encoding = offset_encoding or M._get_offset_encoding(buf)
- local position = make_position_param(window, offset_encoding)
+ if position_encoding == nil then
+ vim.notify_once(
+ 'position_encoding param is required in vim.lsp.util.make_range_params. Defaulting to position encoding of the first client.',
+ vim.log.levels.WARN
+ )
+ --- @diagnostic disable-next-line: deprecated
+ position_encoding = M._get_offset_encoding(buf)
+ end
+ local position = make_position_param(window, position_encoding)
return {
textDocument = M.make_text_document_params(buf),
range = { start = position, ['end'] = position },
@@ -1940,15 +1973,21 @@ end
---@param end_pos [integer,integer]? {row,col} mark-indexed position.
--- Defaults to the end of the last visual selection.
---@param bufnr integer? buffer handle or 0 for current, defaults to current
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of `bufnr`
----@return table { textDocument = { uri = `current_file_uri` }, range = { start =
----`start_position`, end = `end_position` } }
-function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
+---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range }
+function M.make_given_range_params(start_pos, end_pos, bufnr, position_encoding)
validate('start_pos', start_pos, 'table', true)
validate('end_pos', end_pos, 'table', true)
- validate('offset_encoding', offset_encoding, 'string', true)
- bufnr = bufnr or api.nvim_get_current_buf()
- offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
+ validate('position_encoding', position_encoding, 'string', true)
+ bufnr = vim._resolve_bufnr(bufnr)
+ if position_encoding == nil then
+ vim.notify_once(
+ 'position_encoding param is required in vim.lsp.util.make_given_range_params. Defaulting to position encoding of the first client.',
+ vim.log.levels.WARN
+ )
+ --- @diagnostic disable-next-line: deprecated
+ position_encoding = M._get_offset_encoding(bufnr)
+ end
--- @type [integer, integer]
local A = { unpack(start_pos or api.nvim_buf_get_mark(bufnr, '<')) }
--- @type [integer, integer]
@@ -1956,12 +1995,12 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
-- convert to 0-index
A[1] = A[1] - 1
B[1] = B[1] - 1
- -- account for offset_encoding.
+ -- account for position_encoding.
if A[2] > 0 then
- A[2] = M.character_offset(bufnr, A[1], A[2], offset_encoding)
+ A[2] = M.character_offset(bufnr, A[1], A[2], position_encoding)
end
if B[2] > 0 then
- B[2] = M.character_offset(bufnr, B[1], B[2], offset_encoding)
+ B[2] = M.character_offset(bufnr, B[1], B[2], position_encoding)
end
-- we need to offset the end character position otherwise we loose the last
-- character of the selection, as LSP end position is exclusive
@@ -2068,9 +2107,9 @@ end
---@param bufnr integer
---@param start_line integer
---@param end_line integer
----@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
+---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
---@return lsp.Range
-local function make_line_range_params(bufnr, start_line, end_line, offset_encoding)
+local function make_line_range_params(bufnr, start_line, end_line, position_encoding)
local last_line = api.nvim_buf_line_count(bufnr) - 1
---@type lsp.Position
@@ -2079,7 +2118,12 @@ local function make_line_range_params(bufnr, start_line, end_line, offset_encodi
if end_line == last_line and not vim.bo[bufnr].endofline then
end_pos = {
line = end_line,
- character = M.character_offset(bufnr, end_line, #get_line(bufnr, end_line), offset_encoding),
+ character = M.character_offset(
+ bufnr,
+ end_line,
+ #get_line(bufnr, end_line),
+ position_encoding
+ ),
}
else
end_pos = { line = end_line + 1, character = 0 }
@@ -2103,10 +2147,7 @@ end
---@param opts? vim.lsp.util._refresh.Opts Options table
function M._refresh(method, opts)
opts = opts or {}
- local bufnr = opts.bufnr
- if bufnr == nil or bufnr == 0 then
- bufnr = api.nvim_get_current_buf()
- end
+ local bufnr = vim._resolve_bufnr(opts.bufnr)
local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method, id = opts.client_id })
@@ -2122,7 +2163,12 @@ function M._refresh(method, opts)
local first = vim.fn.line('w0', window)
local last = vim.fn.line('w$', window)
for _, client in ipairs(clients) do
- client.request(method, {
+ for rid, req in pairs(client.requests) do
+ if req.method == method and req.type == 'pending' and req.bufnr == bufnr then
+ client:cancel_request(rid)
+ end
+ end
+ client:request(method, {
textDocument = textDocument,
range = make_line_range_params(bufnr, first - 1, last - 1, client.offset_encoding),
}, nil, bufnr)
@@ -2131,7 +2177,7 @@ function M._refresh(method, opts)
end
else
for _, client in ipairs(clients) do
- client.request(method, {
+ client:request(method, {
textDocument = textDocument,
range = make_line_range_params(
bufnr,