From 2d5fce2cdb1254391481a1603be7bfb0872044a5 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Mon, 8 Aug 2022 12:34:37 +0200 Subject: feat(lsp): disable exit_timeout by default (#19672) The lsp client used to wait up to 500ms for a language server to shutdown before sending a TERM signal. The intention behind the 500ms grace period was to ensure the language server exits to prevent stale processes, but it has the side-effect that it can interrupt language-servers which are too slow to shutdown within 500ms. Language servers tend to write out index files or project files on shutdown, and being interrupted during this process can cause corruption of those files. This changes the default to not wait at all, at the risk of leaving stale processes around if the language server isn't well behaved. An alternative would be to wait indefinitely, but that can cause neovim to take several seconds to exit. --- runtime/lua/vim/lsp.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index bf2201d9c8..2f6323ee30 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -871,7 +871,7 @@ end --- - debounce_text_changes (number, default 150): Debounce didChange --- notifications to the server by the given number in milliseconds. No debounce --- occurs if nil ---- - exit_timeout (number, default 500): Milliseconds to wait for server to +--- - exit_timeout (number|boolean, default false): Milliseconds to wait for server to --- exit cleanly after sending the 'shutdown' request before sending kill -15. --- If set to false, nvim exits immediately after sending the 'shutdown' request to the server. --- @@ -1681,7 +1681,7 @@ api.nvim_create_autocmd('VimLeavePre', { local send_kill = false for client_id, client in pairs(active_clients) do - local timeout = if_nil(client.config.flags.exit_timeout, 500) + local timeout = if_nil(client.config.flags.exit_timeout, false) if timeout then send_kill = true timeouts[client_id] = timeout -- cgit From a46e6afb8b95229478c5c1fb75e3f1c55991def0 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Mon, 8 Aug 2022 13:02:15 +0200 Subject: fix(lsp): set end_col in formatexpr (#19676) The last line was excluded from formatting via formatexpr because the character in the params was set to 0 instead of the end of line. --- runtime/lua/vim/lsp.lua | 45 +++++++++++++++++++++++--------------------- runtime/lua/vim/lsp/util.lua | 2 +- 2 files changed, 25 insertions(+), 22 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 2f6323ee30..b26ed0c759 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1982,29 +1982,32 @@ function lsp.formatexpr(opts) return 1 end - local start_line = vim.v.lnum - local end_line = start_line + vim.v.count - 1 - - if start_line > 0 and end_line > 0 then - local params = { - textDocument = util.make_text_document_params(), - range = { - start = { line = start_line - 1, character = 0 }, - ['end'] = { line = end_line - 1, character = 0 }, - }, - } - params.options = util.make_formatting_params().options - local client_results = - vim.lsp.buf_request_sync(0, 'textDocument/rangeFormatting', params, timeout_ms) + local start_lnum = vim.v.lnum + local end_lnum = start_lnum + vim.v.count - 1 - -- Apply the text edits from one and only one of the clients. - for client_id, response in pairs(client_results) do + if start_lnum <= 0 or end_lnum <= 0 then + return 0 + end + local bufnr = api.nvim_get_current_buf() + for _, client in pairs(lsp.get_active_clients({ bufnr = bufnr })) do + if client.supports_method('textDocument/rangeFormatting') then + local params = util.make_formatting_params() + local end_line = vim.fn.getline(end_lnum) + local end_col = util._str_utfindex_enc(end_line, nil, client.offset_encoding) + params.range = { + start = { + line = start_lnum - 1, + character = 0, + }, + ['end'] = { + line = end_lnum - 1, + character = end_col, + }, + } + local response = + client.request_sync('textDocument/rangeFormatting', params, timeout_ms, bufnr) if response.result then - vim.lsp.util.apply_text_edits( - response.result, - 0, - vim.lsp.get_client_by_id(client_id).offset_encoding - ) + vim.lsp.util.apply_text_edits(response.result, 0, client.offset_encoding) return 0 end end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index eac21db386..283099bbcf 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -113,7 +113,7 @@ end --- Convert byte index to `encoding` index. --- Convenience wrapper around vim.str_utfindex ---@param line string line to be indexed ----@param index number byte index (utf-8), or `nil` for length +---@param index number|nil byte index (utf-8), or `nil` for length ---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 ---@return number `encoding` index of `index` in `line` function M._str_utfindex_enc(line, index, encoding) -- cgit From 68c674af0fbc4158690319aa6125a098a592412d Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Mon, 8 Aug 2022 18:30:17 +0200 Subject: feat(lsp): set formatexpr by default (#19677) Follow up to https://github.com/neovim/neovim/pull/19003 --- runtime/lua/vim/lsp.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index b26ed0c759..7f3237bdd0 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -968,12 +968,20 @@ function lsp.start_client(config) ---@private local function set_defaults(client, bufnr) - if client.server_capabilities.definitionProvider and vim.bo[bufnr].tagfunc == '' then + local capabilities = client.server_capabilities + if capabilities.definitionProvider and vim.bo[bufnr].tagfunc == '' then vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc' end - if client.server_capabilities.completionProvider and vim.bo[bufnr].omnifunc == '' then + if capabilities.completionProvider and vim.bo[bufnr].omnifunc == '' then vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' end + if + capabilities.documentRangeFormattingProvider + and vim.bo[bufnr].formatprg == '' + and vim.bo[bufnr].formatexpr == '' + then + vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()' + end end ---@private @@ -986,6 +994,9 @@ function lsp.start_client(config) if vim.bo[bufnr].omnifunc == 'v:lua.vim.lsp.omnifunc' then vim.bo[bufnr].omnifunc = nil end + if vim.bo[bufnr].formatexpr == 'v:lua.vim.lsp.formatexpr()' then + vim.bo[bufnr].formatexpr = nil + end end ---@private -- cgit From e6680ea7c3912d38f2ef967e053be741624633ad Mon Sep 17 00:00:00 2001 From: dundargoc <33953936+dundargoc@users.noreply.github.com> Date: Mon, 8 Aug 2022 18:58:32 +0200 Subject: docs(lua): add Lua 5.1 reference manual (#19663) based on http://www.vim.org/scripts/script.php?script_id=1291 reformatted to match Nvim documentation style; removed irrelevant sections Co-authored-by: dundargoc Co-authored-by: Christian Clason Co-authored-by: Lewis Russell --- runtime/lua/vim/shared.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index d6c3e25b3b..e1b4ed4ea9 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -526,7 +526,7 @@ function vim.trim(s) return s:match('^%s*(.*%S)') or '' end ---- Escapes magic chars in a Lua pattern. +--- Escapes magic chars in |lua-patterns|. --- ---@see https://github.com/rxi/lume ---@param s string String to escape -- cgit From a5e846b9969b1dfbd7e92f437f3ac7905039b84f Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Tue, 9 Aug 2022 10:43:28 +0200 Subject: vim-patch:partial: 48c3f4e0bff7 (#19684) vim-patch:partial:48c3f4e0bff7 Update runtime files https://github.com/vim/vim/commit/48c3f4e0bff7efd289a7001b68c777b6f89a7057 partially skip `options.txt` (needs 9.0.0138) --- runtime/lua/vim/filetype/detect.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua index 14f076717f..2be9dcff88 100644 --- a/runtime/lua/vim/filetype/detect.lua +++ b/runtime/lua/vim/filetype/detect.lua @@ -930,7 +930,7 @@ function M.progress_pascal(bufnr) return 'progress' end --- Distinguish between "default" and Cproto prototype file. +-- Distinguish between "default", Prolog and Cproto prototype file. function M.proto(bufnr, default) -- Cproto files have a comment in the first line and a function prototype in -- the second line, it always ends in ";". Indent files may also have @@ -940,7 +940,18 @@ function M.proto(bufnr, default) if getlines(bufnr, 2):find('.;$') then return 'cpp' else - return default + -- Recognize Prolog by specific text in the first non-empty line; + -- require a blank after the '%' because Perl uses "%list" and "%translate" + local line = nextnonblank(bufnr, 1) + if + line and line:find(':%-') + or matchregex(line, [[\c\]]) + or findany(line, { '^%s*%%+%s', '^%s*%%+$', '^%s*/%*' }) + then + return 'prolog' + else + return default + end end end -- cgit From bebfbfab3bca1808678bb196904b7570d6806458 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Tue, 9 Aug 2022 22:20:40 +0200 Subject: fix(lsp): handle multiple clients with incremental sync (#19658) The change tracking used a single lines/lines_tmp table to track changes to a buffer. If multiple clients using incremental sync are connected to a buffer, they both made changes to the same lines table. That resulted in an inconsistent state. This commit changes the didChange handling to group clients by synchronization scheme and offset encoding. This avoids computing the diff multiple times for clients using the same scheme and resolves the lines/lines_tmp conflicts. Fixes https://github.com/neovim/neovim/issues/19325 --- runtime/lua/vim/lsp.lua | 384 +++++++++++++++++++++++++++++++----------------- 1 file changed, 248 insertions(+), 136 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 7f3237bdd0..10a74ba257 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -338,54 +338,165 @@ end local changetracking = {} do - --@private - --- client_id → state + ---@private + --- + --- LSP has 3 different sync modes: + --- - None (Servers will read the files themselves when needed) + --- - Full (Client sends the full buffer content on updates) + --- - Incremental (Client sends only the changed parts) + --- + --- Changes are tracked per buffer. + --- A buffer can have multiple clients attached and each client needs to send the changes + --- To minimize the amount of changesets to compute, computation is grouped: --- - --- state - --- use_incremental_sync: bool - --- buffers: bufnr -> buffer_state + --- None: One group for all clients + --- Full: One group for all clients + --- Incremental: One group per `offset_encoding` --- - --- buffer_state - --- pending_change?: function that the timer starts to trigger didChange - --- pending_changes: table (uri -> list of pending changeset tables)); - --- Only set if incremental_sync is used + --- 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. --- - --- timer?: uv_timer - --- lines: table - local state_by_client = {} + --- @class CTGroup + --- @field sync_kind number TextDocumentSyncKind, considers config.flags.allow_incremental_sync + --- @field offset_encoding "utf-8"|"utf-16"|"utf-32" + --- + --- @class CTBufferState + --- @field name string name of the buffer + --- @field lines string[] snapshot of buffer lines from last didChange + --- @field lines_tmp string[] + --- @field pending_changes table[] List of debounced changes in incremental sync mode + --- @field timer nil|userdata uv_timer + --- @field last_flush nil|number uv.hrtime of the last flush/didChange-notification + --- @field needs_flush boolean true if buffer updates haven't been sent to clients/servers yet + --- @field refs number how many clients are using this group + --- + --- @class CTGroupState + --- @field buffers table + --- @field debounce number debounce duration in ms + --- @field clients table clients using this state. {client_id, client} ---@private - function changetracking.init(client, bufnr) - local use_incremental_sync = ( - if_nil(client.config.flags.allow_incremental_sync, true) - and vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change') - == protocol.TextDocumentSyncKind.Incremental + ---@param group CTGroup + ---@return string + local function group_key(group) + if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then + return tostring(group.sync_kind) .. '\0' .. group.offset_encoding + end + return tostring(group.sync_kind) + end + + ---@private + ---@type table + local state_by_group = setmetatable({}, { + __index = function(tbl, k) + return rawget(tbl, group_key(k)) + end, + __newindex = function(tbl, k, v) + rawset(tbl, group_key(k), v) + end, + }) + + ---@private + ---@return CTGroup + local function get_group(client) + local allow_inc_sync = if_nil(client.config.flags.allow_incremental_sync, true) + local change_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change') + local sync_kind = change_capability or protocol.TextDocumentSyncKind.None + if not allow_inc_sync and change_capability == protocol.TextDocumentSyncKind.Incremental then + sync_kind = protocol.TextDocumentSyncKind.Full + end + return { + sync_kind = sync_kind, + offset_encoding = client.offset_encoding, + } + end + + ---@private + ---@param state CTBufferState + local function incremental_changes(state, encoding, bufnr, firstline, lastline, new_lastline) + local prev_lines = state.lines + local curr_lines = state.lines_tmp + + local changed_lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true) + for i = 1, firstline do + curr_lines[i] = prev_lines[i] + end + for i = firstline + 1, new_lastline do + curr_lines[i] = changed_lines[i - firstline] + end + for i = lastline + 1, #prev_lines do + curr_lines[i - lastline + new_lastline] = prev_lines[i] + end + if tbl_isempty(curr_lines) then + -- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259. + curr_lines[1] = '' + end + + local line_ending = buf_get_line_ending(bufnr) + local incremental_change = sync.compute_diff( + state.lines, + curr_lines, + firstline, + lastline, + new_lastline, + encoding, + line_ending ) - local state = state_by_client[client.id] - if not state then + + -- Double-buffering of lines tables is used to reduce the load on the garbage collector. + -- At this point the prev_lines table is useless, but its internal storage has already been allocated, + -- so let's keep it around for the next didChange event, in which it will become the next + -- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the + -- internal storage - it merely marks them as free, for the GC to deallocate them. + for i in ipairs(prev_lines) do + prev_lines[i] = nil + end + state.lines = curr_lines + state.lines_tmp = prev_lines + + return incremental_change + end + + ---@private + function changetracking.init(client, bufnr) + assert(client.offset_encoding, 'lsp client must have an offset_encoding') + local group = get_group(client) + local state = state_by_group[group] + if state then + state.debounce = math.min(state.debounce, client.config.flags.debounce_text_changes or 150) + state.clients[client.id] = client + else state = { buffers = {}, debounce = client.config.flags.debounce_text_changes or 150, - use_incremental_sync = use_incremental_sync, + clients = { + [client.id] = client, + }, } - state_by_client[client.id] = state + state_by_group[group] = state end - if not state.buffers[bufnr] then - local buf_state = { + local buf_state = state.buffers[bufnr] + if buf_state then + buf_state.refs = buf_state.refs + 1 + else + buf_state = { name = api.nvim_buf_get_name(bufnr), + lines = {}, + lines_tmp = {}, + pending_changes = {}, + needs_flush = false, + refs = 1, } state.buffers[bufnr] = buf_state - if use_incremental_sync then + if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true) - buf_state.lines_tmp = {} - buf_state.pending_changes = {} end end end ---@private function changetracking._get_and_set_name(client, bufnr, name) - local state = state_by_client[client.id] or {} + local state = state_by_group[get_group(client)] or {} local buf_state = (state.buffers or {})[bufnr] local old_name = buf_state.name buf_state.name = name @@ -395,32 +506,33 @@ do ---@private function changetracking.reset_buf(client, bufnr) changetracking.flush(client, bufnr) - local state = state_by_client[client.id] - if state and state.buffers then - local buf_state = state.buffers[bufnr] + local state = state_by_group[get_group(client)] + if not state then + return + end + assert(state.buffers, 'CTGroupState must have buffers') + local buf_state = state.buffers[bufnr] + buf_state.refs = buf_state.refs - 1 + assert(buf_state.refs >= 0, 'refcount on buffer state must not get negative') + if buf_state.refs == 0 then state.buffers[bufnr] = nil - if buf_state and buf_state.timer then - buf_state.timer:stop() - buf_state.timer:close() - buf_state.timer = nil - end + changetracking._reset_timer(buf_state) end end ---@private - function changetracking.reset(client_id) - local state = state_by_client[client_id] + function changetracking.reset(client) + local state = state_by_group[get_group(client)] if not state then return end - for _, buf_state in pairs(state.buffers) do - if buf_state.timer then - buf_state.timer:stop() - buf_state.timer:close() - buf_state.timer = nil + state.clients[client.id] = nil + if vim.tbl_count(state.clients) == 0 then + for _, buf_state in pairs(state.buffers) do + changetracking._reset_timer(buf_state) end + state.buffers = {} end - state.buffers = {} end ---@private @@ -430,6 +542,10 @@ do -- debounce can be skipped and otherwise maybe reduced. -- -- This turns the debounce into a kind of client rate limiting + -- + ---@param debounce number + ---@param buf_state CTBufferState + ---@return number local function next_debounce(debounce, buf_state) if debounce == 0 then return 0 @@ -444,83 +560,36 @@ do end ---@private - function changetracking.prepare(bufnr, firstline, lastline, new_lastline) - local incremental_changes = function(client, buf_state) - local prev_lines = buf_state.lines - local curr_lines = buf_state.lines_tmp - - local changed_lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true) - for i = 1, firstline do - curr_lines[i] = prev_lines[i] - end - for i = firstline + 1, new_lastline do - curr_lines[i] = changed_lines[i - firstline] - end - for i = lastline + 1, #prev_lines do - curr_lines[i - lastline + new_lastline] = prev_lines[i] - end - if tbl_isempty(curr_lines) then - -- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259. - curr_lines[1] = '' - end - - local line_ending = buf_get_line_ending(bufnr) - local incremental_change = sync.compute_diff( - buf_state.lines, - curr_lines, - firstline, - lastline, - new_lastline, - client.offset_encoding or 'utf-16', - line_ending - ) - - -- Double-buffering of lines tables is used to reduce the load on the garbage collector. - -- At this point the prev_lines table is useless, but its internal storage has already been allocated, - -- so let's keep it around for the next didChange event, in which it will become the next - -- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the - -- internal storage - it merely marks them as free, for the GC to deallocate them. - for i in ipairs(prev_lines) do - prev_lines[i] = nil - end - buf_state.lines = curr_lines - buf_state.lines_tmp = prev_lines + ---@param bufnr number + ---@param sync_kind number protocol.TextDocumentSyncKind + ---@param state CTGroupState + ---@param buf_state CTBufferState + local function send_changes(bufnr, sync_kind, state, buf_state) + if not buf_state.needs_flush then + return + end + buf_state.last_flush = uv.hrtime() + buf_state.needs_flush = false - return incremental_change + if not api.nvim_buf_is_valid(bufnr) then + buf_state.pending_changes = {} + return end - local full_changes = once(function() - return { - text = buf_get_full_text(bufnr), + + local changes + if sync_kind == protocol.TextDocumentSyncKind.None then + return + elseif sync_kind == protocol.TextDocumentSyncKind.Incremental then + changes = buf_state.pending_changes + buf_state.pending_changes = {} + else + changes = { + { text = buf_get_full_text(bufnr) }, } - end) + end local uri = vim.uri_from_bufnr(bufnr) - return function(client) - if - vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change') - == protocol.TextDocumentSyncKind.None - then - return - end - local state = state_by_client[client.id] - local buf_state = state.buffers[bufnr] - changetracking._reset_timer(buf_state) - local debounce = next_debounce(state.debounce, buf_state) - if state.use_incremental_sync then - -- This must be done immediately and cannot be delayed - -- The contents would further change and startline/endline may no longer fit - table.insert(buf_state.pending_changes, incremental_changes(client, buf_state)) - end - buf_state.pending_change = function() - if buf_state.pending_change == nil then - return - end - buf_state.pending_change = nil - buf_state.last_flush = uv.hrtime() - if client.is_stopped() or not api.nvim_buf_is_valid(bufnr) then - return - end - local changes = state.use_incremental_sync and buf_state.pending_changes - or { full_changes() } + for _, client in pairs(state.clients) do + if not client.is_stopped() and lsp.buf_is_attached(bufnr, client.id) then client.notify('textDocument/didChange', { textDocument = { uri = uri, @@ -528,46 +597,90 @@ do }, contentChanges = changes, }) - buf_state.pending_changes = {} + end + end + end + + ---@private + function changetracking.send_changes(bufnr, firstline, lastline, new_lastline) + local groups = {} + for _, client in pairs(lsp.get_active_clients({ bufnr = bufnr })) do + local group = get_group(client) + groups[group_key(group)] = group + end + for _, group in pairs(groups) do + local state = state_by_group[group] + if not state then + error( + string.format( + 'changetracking.init must have been called for all LSP clients. group=%s states=%s', + vim.inspect(group), + vim.inspect(vim.tbl_keys(state_by_group)) + ) + ) + end + local buf_state = state.buffers[bufnr] + buf_state.needs_flush = true + changetracking._reset_timer(buf_state) + local debounce = next_debounce(state.debounce, buf_state) + if group.sync_kind == protocol.TextDocumentSyncKind.Incremental then + -- This must be done immediately and cannot be delayed + -- The contents would further change and startline/endline may no longer fit + local changes = incremental_changes( + buf_state, + group.offset_encoding, + bufnr, + firstline, + lastline, + new_lastline + ) + table.insert(buf_state.pending_changes, changes) end if debounce == 0 then - buf_state.pending_change() + send_changes(bufnr, group.sync_kind, state, buf_state) else local timer = uv.new_timer() buf_state.timer = timer - -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines - timer:start(debounce, 0, vim.schedule_wrap(buf_state.pending_change)) + timer:start( + debounce, + 0, + vim.schedule_wrap(function() + changetracking._reset_timer(buf_state) + send_changes(bufnr, group.sync_kind, state, buf_state) + end) + ) end end end + ---@private function changetracking._reset_timer(buf_state) - if buf_state.timer then - buf_state.timer:stop() - buf_state.timer:close() + local timer = buf_state.timer + if timer then buf_state.timer = nil + if not timer:is_closing() then + timer:stop() + timer:close() + end end end --- Flushes any outstanding change notification. ---@private function changetracking.flush(client, bufnr) - local state = state_by_client[client.id] + local group = get_group(client) + local state = state_by_group[group] if not state then return end if bufnr then local buf_state = state.buffers[bufnr] or {} changetracking._reset_timer(buf_state) - if buf_state.pending_change then - buf_state.pending_change() - end + send_changes(bufnr, group.sync_kind, state, buf_state) else - for _, buf_state in pairs(state.buffers) do + for buf, buf_state in pairs(state.buffers) do changetracking._reset_timer(buf_state) - if buf_state.pending_change then - buf_state.pending_change() - end + send_changes(buf, group.sync_kind, state, buf_state) end end end @@ -1030,11 +1143,12 @@ function lsp.start_client(config) end) end end - + local client = active_clients[client_id] and active_clients[client_id] + or uninitialized_clients[client_id] active_clients[client_id] = nil uninitialized_clients[client_id] = nil - changetracking.reset(client_id) + changetracking.reset(client) if code ~= 0 or (signal ~= 0 and signal ~= 15) then local msg = string.format('Client %s quit with exit code %s and signal %s', client_id, code, signal) @@ -1414,9 +1528,7 @@ do return true end util.buf_versions[bufnr] = changedtick - local compute_change_and_notify = - changetracking.prepare(bufnr, firstline, lastline, new_lastline) - for_each_buffer_client(bufnr, compute_change_and_notify) + changetracking.send_changes(bufnr, firstline, lastline, new_lastline) end end -- cgit From ff1266aaaaba80f86ab9624eea8939d644d7e730 Mon Sep 17 00:00:00 2001 From: Jonas Strittmatter <40792180+smjonas@users.noreply.github.com> Date: Wed, 10 Aug 2022 13:44:57 +0200 Subject: vim-patch:9.0.0182: quarto files are not recognized (#19702) Problem: Quarto files are not recognized. Solution: Recognize quarto files by the extension. (Jonas Strittmatter, closes vim/vim#10880) https://github.com/vim/vim/commit/3a9687fb2749cb3da6e3bbf60cb9eaa81f7889ae --- runtime/lua/vim/filetype.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 1b209e6a9d..e9e81caec0 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -776,6 +776,7 @@ local extension = { ptl = 'python', ql = 'ql', qll = 'ql', + qmd = 'quarto', R = function(path, bufnr) return require('vim.filetype.detect').r(bufnr) end, -- cgit From 8b67f37798d90da957801be791da9425fb6fe741 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 11 Aug 2022 15:17:05 +0200 Subject: fix(lsp): fix some type annotations in lsp.rpc (#19714) --- runtime/lua/vim/lsp/rpc.lua | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 913eee19a2..3d61dd5e33 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -58,12 +58,10 @@ end ---@private --- Parses an LSP Message's header --- ----@param header: The header to parse. ----@returns Parsed headers +---@param header string: The header to parse. +---@return table parsed headers local function parse_headers(header) - if type(header) ~= 'string' then - return nil - end + assert(type(header) == 'string', 'header must be a string') local headers = {} for line in vim.gsplit(header, '\r\n', true) do if line == '' then @@ -189,9 +187,9 @@ end --- Creates an RPC response object/table. --- ----@param code RPC error code defined in `vim.lsp.protocol.ErrorCodes` ----@param message (optional) arbitrary message to send to server ----@param data (optional) arbitrary data to send to server +---@param code number RPC error code defined in `vim.lsp.protocol.ErrorCodes` +---@param message string|nil arbitrary message to send to server +---@param data any|nil arbitrary data to send to server local function rpc_response_error(code, message, data) -- TODO should this error or just pick a sane error (like InternalError)? local code_name = assert(protocol.ErrorCodes[code], 'Invalid RPC error code') @@ -248,13 +246,13 @@ end --- ---@param cmd (string) Command to start the LSP server. ---@param cmd_args (table) List of additional string arguments to pass to {cmd}. ----@param dispatchers (table, optional) Dispatchers for LSP message types. Valid +---@param dispatchers table|nil Dispatchers for LSP message types. Valid ---dispatcher names are: --- - `"notification"` --- - `"server_request"` --- - `"on_error"` --- - `"on_exit"` ----@param extra_spawn_params (table, optional) Additional context for the LSP +---@param extra_spawn_params table|nil Additional context for the LSP --- server process. May contain: --- - {cwd} (string) Working directory for the LSP server process --- - {env} (table) Additional environment variables for LSP server process @@ -434,7 +432,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) end end - stderr:read_start(function(_err, chunk) + stderr:read_start(function(_, chunk) if chunk then local _ = log.error() and log.error('rpc', cmd, 'stderr', chunk) end @@ -520,7 +518,7 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) -- This works because we are expecting vim.NIL here elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then -- We sent a number, so we expect a number. - local result_id = tonumber(decoded.id) + 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_callback = notify_reply_callbacks and notify_reply_callbacks[result_id] -- cgit From 996fc2256bafabeb8f5806d70d531311a34d29f9 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 11 Aug 2022 17:04:55 +0200 Subject: fix(lsp): avoid pipe leaks if lsp cmd isn't executable (#19717) The `onexit` handler isn't called if `uv.spawn` doesn't return a handle. --- runtime/lua/vim/lsp/rpc.lua | 3 +++ 1 file changed, 3 insertions(+) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 3d61dd5e33..0926912066 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -340,6 +340,9 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) end handle, pid = uv.spawn(cmd, spawn_params, onexit) if handle == nil then + stdin:close() + stdout:close() + stderr:close() local msg = string.format('Spawning language server with cmd: `%s` failed', cmd) if string.match(pid, 'ENOENT') then msg = msg -- cgit From 33b77eb728a1f07926ba19c4f09fd20e806de363 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 11 Aug 2022 19:21:57 +0200 Subject: fix(lsp): handle nil client in onexit callback (#19722) Follow up to https://github.com/neovim/neovim/pull/19658 --- runtime/lua/vim/lsp.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 10a74ba257..2c62e87513 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -690,7 +690,7 @@ end --- Default handler for the 'textDocument/didOpen' LSP notification. --- ---@param bufnr number Number of the buffer, or 0 for current ----@param client Client object +---@param client table Client object local function text_document_did_open_handler(bufnr, client) changetracking.init(client, bufnr) if not vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'openClose') then @@ -1148,7 +1148,11 @@ function lsp.start_client(config) active_clients[client_id] = nil uninitialized_clients[client_id] = nil - changetracking.reset(client) + -- Client can be absent if executable starts, but initialize fails + -- init/attach won't have happened + if client then + changetracking.reset(client) + end if code ~= 0 or (signal ~= 0 and signal ~= 15) then local msg = string.format('Client %s quit with exit code %s and signal %s', client_id, code, signal) -- cgit From 02289ab898575368eeaa234bbd78725f8463044c Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Fri, 12 Aug 2022 10:10:03 +0200 Subject: fix(lsp): fix nil value error in get_group (#19735) `server_capabilities` can be nil until the server is initialized. Reproduced with: vim.lsp.stop_client(vim.lsp.start_client { cmd = { vim.v.progpath, '-es', '-u', 'NONE', '--headless' }; }) --- runtime/lua/vim/lsp.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 2c62e87513..8d4f388e23 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -400,7 +400,8 @@ do ---@return CTGroup local function get_group(client) local allow_inc_sync = if_nil(client.config.flags.allow_incremental_sync, true) - local change_capability = vim.tbl_get(client.server_capabilities, 'textDocumentSync', 'change') + local change_capability = + vim.tbl_get(client.server_capabilities or {}, 'textDocumentSync', 'change') local sync_kind = change_capability or protocol.TextDocumentSyncKind.None if not allow_inc_sync and change_capability == protocol.TextDocumentSyncKind.Incremental then sync_kind = protocol.TextDocumentSyncKind.Full -- cgit From a850b15e1968476e0f609a9d699cdf24fd13e3a2 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sat, 13 Aug 2022 10:26:12 +0200 Subject: vim-patch:9.0.0195: metafun files are not recogized (#19746) Problem: Metafun files are not recogized. Solution: Add filetype detection patterns. https://github.com/vim/vim/commit/9032b9ceb6073288d75386dbcbd9d1982fa24080 --- runtime/lua/vim/filetype.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index e9e81caec0..3cfc4e288a 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -668,6 +668,21 @@ local extension = { moo = 'moo', moon = 'moonscript', mp = 'mp', + mpiv = function(path, bufnr) + return 'mp', function(b) + vim.b[b].mp_metafun = 1 + end + end, + mpvi = function(path, bufnr) + return 'mp', function(b) + vim.b[b].mp_metafun = 1 + end + end, + mpxl = function(path, bufnr) + return 'mp', function(b) + vim.b[b].mp_metafun = 1 + end + end, mof = 'msidl', odl = 'msidl', msql = 'msql', -- cgit From 33b49d5f55423a1a3174ad0ce42c4454871aa9f8 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Sat, 13 Aug 2022 15:11:03 +0200 Subject: vim-patch:9.0.0197: astro files are not detected (#19755) Problem: Astro files are not detected. Solution: Add a pattern to match Astro files. (Emilia Zapata, closes vim/vim#10904) https://github.com/vim/vim/commit/6a76e84f555da6d9ee57db80143e1e5eb85535ff --- runtime/lua/vim/filetype.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 3cfc4e288a..872106b20d 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -142,6 +142,7 @@ local extension = { asp = function(path, bufnr) return require('vim.filetype.detect').asp(bufnr) end, + astro = 'astro', atl = 'atlas', as = 'atlas', ahk = 'autohotkey', -- cgit From 5854103dad5a9ae513c089a514364eff55582fbb Mon Sep 17 00:00:00 2001 From: Antoine Cotten Date: Sun, 14 Aug 2022 00:38:31 +0200 Subject: docs(lua): clarify vim.keymap.set() opts (#19761) --- runtime/lua/vim/keymap.lua | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua index 7265beb56b..219de16b5c 100644 --- a/runtime/lua/vim/keymap.lua +++ b/runtime/lua/vim/keymap.lua @@ -36,14 +36,17 @@ local keymap = {} ---@param lhs string Left-hand side |{lhs}| of the mapping. ---@param rhs string|function Right-hand side |{rhs}| of the mapping. Can also be a Lua function. -- ----@param opts table A table of |:map-arguments| such as "silent". In addition to the options ---- listed in |nvim_set_keymap()|, this table also accepts the following keys: ---- - buffer: (number or boolean) Add a mapping to the given buffer. When "true" ---- or 0, use the current buffer. ---- - remap: (boolean) Make the mapping recursive. This is the ---- inverse of the "noremap" option from |nvim_set_keymap()|. ---- Default `false`. ---- - replace_keycodes: (boolean) defaults to true if "expr" is true. +---@param opts table A table of |:map-arguments|. +--- + Accepts options accepted by the {opts} parameter in |nvim_set_keymap()|, +--- with the following notable differences: +--- - replace_keycodes: Defaults to `true` if "expr" is `true`. +--- - noremap: Always overridden with the inverse of "remap" (see below). +--- + In addition to those options, the table accepts the following keys: +--- - buffer: (number or boolean) Add a mapping to the given buffer. +--- When `0` or `true`, use the current buffer. +--- - remap: (boolean) Make the mapping recursive. +--- This is the inverse of the "noremap" option from |nvim_set_keymap()|. +--- Defaults to `false`. ---@see |nvim_set_keymap()| function keymap.set(mode, lhs, rhs, opts) vim.validate({ -- cgit From 7a076306e4e35371160d1a5d09c92744b8461b57 Mon Sep 17 00:00:00 2001 From: Jonas Strittmatter <40792180+smjonas@users.noreply.github.com> Date: Wed, 17 Aug 2022 12:39:38 +0200 Subject: docs(lsp): rename on-list-handler to lsp-on-list-handler (#19813) This makes it easier to find documentation about the on-list-handler when starting the search term with "lsp". --- runtime/lua/vim/lsp/buf.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 63f4688d94..6a070928d9 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -61,7 +61,7 @@ end --- ---@param options table|nil additional options --- - reuse_win: (boolean) Jump to existing window if buffer is already open. ---- - on_list: (function) handler for list results. See |on-list-handler| +--- - on_list: (function) handler for list results. See |lsp-on-list-handler| function M.declaration(options) local params = util.make_position_params() request_with_options('textDocument/declaration', params, options) @@ -71,7 +71,7 @@ end --- ---@param options table|nil additional options --- - reuse_win: (boolean) Jump to existing window if buffer is already open. ---- - on_list: (function) handler for list results. See |on-list-handler| +--- - on_list: (function) handler for list results. See |lsp-on-list-handler| function M.definition(options) local params = util.make_position_params() request_with_options('textDocument/definition', params, options) @@ -81,7 +81,7 @@ end --- ---@param options table|nil additional options --- - reuse_win: (boolean) Jump to existing window if buffer is already open. ---- - on_list: (function) handler for list results. See |on-list-handler| +--- - on_list: (function) handler for list results. See |lsp-on-list-handler| function M.type_definition(options) local params = util.make_position_params() request_with_options('textDocument/typeDefinition', params, options) @@ -91,7 +91,7 @@ end --- quickfix window. --- ---@param options table|nil additional options ---- - on_list: (function) handler for list results. See |on-list-handler| +--- - on_list: (function) handler for list results. See |lsp-on-list-handler| function M.implementation(options) local params = util.make_position_params() request_with_options('textDocument/implementation', params, options) @@ -503,7 +503,7 @@ end ---@param context (table) Context for the request ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references ---@param options table|nil additional options ---- - on_list: (function) handler for list results. See |on-list-handler| +--- - on_list: (function) handler for list results. See |lsp-on-list-handler| function M.references(context, options) validate({ context = { context, 't', true } }) local params = util.make_position_params() @@ -516,7 +516,7 @@ end --- Lists all symbols in the current buffer in the quickfix window. --- ---@param options table|nil additional options ---- - on_list: (function) handler for list results. See |on-list-handler| +--- - on_list: (function) handler for list results. See |lsp-on-list-handler| function M.document_symbol(options) local params = { textDocument = util.make_text_document_params() } request_with_options('textDocument/documentSymbol', params, options) @@ -659,7 +659,7 @@ end --- ---@param query (string, optional) ---@param options table|nil additional options ---- - on_list: (function) handler for list results. See |on-list-handler| +--- - on_list: (function) handler for list results. See |lsp-on-list-handler| function M.workspace_symbol(query, options) query = query or npcall(vim.fn.input, 'Query: ') if query == nil then -- cgit From 341ef46d008d765640e65404f8dc7760175d4c7d Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Thu, 18 Aug 2022 10:57:17 +0200 Subject: docs(lsp): remove lsp.buf_request from docs (#19738) This starts a soft phase-out of `buf_request`. `buf_request` is quite error prone: - Positional `params` depend on the client because of the `offset_encoding`. Currently if there is one client using UTF-8 offset encoding and another using UTF-16, the positions in the request are wrong for one of the clients. To solve this the params would need to be created per client instead of once for all of them. - `handler` is called *per* client but many users of it assume it is only called once. This can lead to a "select n + 1" kind of problem, where the handler makes another call to `buf_request`, multiplying the amount of requests. (There are in fact still some places where this happens in core) Or it leads to erratic behavior if called multiple times (E.g. the quicklist list flickering & being overwritten) (See hover or references implementation) `buf_request_all` returns an aggregate of the responses which is more sensible as it avoids this problem. For off-spec extensions it also has the problem that it sends requests to clients which cannot handle a given request. Given that `buf_request` is in use by a lot of plugins this starts a soft-phase out. Planned Steps: - Remove from docs - Provide an alternative, either `buf_request_all`, maybe with extensions (params being a function), or an entirely new method. - Mark as deprecated in 0.9 - Remove in 0.10 To note: - `buf_request_all` currently isn't ideal either because it suffers from the `params` problem as well. - This implies that the `vim.lsp.with` pattern will die, because the global handlers as they are don't fit a multi-client model, as most of the time an aggregate is needed. --- runtime/lua/vim/lsp.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 8d4f388e23..fd64c1a495 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1845,13 +1845,14 @@ api.nvim_create_autocmd('VimLeavePre', { end, }) +---@private --- Sends an async request for all active clients attached to the --- buffer. --- ---@param bufnr (number) Buffer handle, or 0 for current. ---@param method (string) LSP method name ----@param params (optional, table) Parameters to send to the server ----@param handler (optional, function) See |lsp-handler| +---@param params table|nil Parameters to send to the server +---@param handler function|nil See |lsp-handler| --- If nil, follows resolution strategy defined in |lsp-handler-configuration| --- ---@returns 2-tuple: -- cgit From b2f979b30beac67906b2dd717fcb6a34f46f5e54 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 19 Aug 2022 19:30:35 +0200 Subject: fix(filetype): only check first 100 and last line of buffer (#19819) fix(filetype): only pass first 100 and last lines to contents check sufficient for current content checks and avoids performance issues for buffers with a large number of lines fixes #19817 --- runtime/lua/vim/filetype.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'runtime/lua') diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 872106b20d..99c98764dd 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -2489,7 +2489,15 @@ function M.match(args) -- Finally, check file contents if contents or bufnr then - contents = contents or M.getlines(bufnr) + if contents == nil then + if api.nvim_buf_line_count(bufnr) > 101 then + -- only need first 100 and last line for current checks + contents = M.getlines(bufnr, 1, 100) + contents[#contents + 1] = M.getlines(bufnr, -1) + else + contents = M.getlines(bufnr) + end + end -- If name is nil, catch any errors from the contents filetype detection function. -- If the function tries to use the filename that is nil then it will fail, -- but this enables checks which do not need a filename to still work. -- cgit