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 |