diff options
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r-- | runtime/lua/vim/lsp/_changetracking.lua | 18 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/_folding_range.lua | 373 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/_meta.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/_snippet_grammar.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/_tagfunc.lua | 18 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/_transport.lua | 182 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/_watchfiles.lua | 3 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 98 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/client.lua | 411 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/codelens.lua | 14 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/completion.lua | 43 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/diagnostic.lua | 29 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 20 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/health.lua | 77 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/inlay_hint.lua | 29 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 15 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 290 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/semantic_tokens.lua | 35 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/sync.lua | 52 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 222 |
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, |