diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/lsp.lua | 236 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/diagnostic.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 47 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 68 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 3 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/health.lua | 34 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 14 |
7 files changed, 317 insertions, 89 deletions
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 563ffc479e..59b7180cf1 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -232,6 +232,12 @@ local function validate_client_config(config) flags = { config.flags, "t", true }; get_language_id = { config.get_language_id, "f", true }; } + assert( + (not config.flags + or not config.flags.debounce_text_changes + or type(config.flags.debounce_text_changes) == 'number'), + "flags.debounce_text_changes must be nil or a number with the debounce time in milliseconds" + ) local cmd, cmd_args = lsp._cmd_parts(config.cmd) local offset_encoding = valid_encodings.UTF16 @@ -260,21 +266,165 @@ local function buf_get_full_text(bufnr) end --@private +--- Memoizes a function. On first run, the function return value is saved and +--- immediately returned on subsequent runs. +--- +--@param fn (function) Function to run +--@returns (function) Memoized function +local function once(fn) + local value + return function(...) + if not value then value = fn(...) end + return value + end +end + + +local changetracking = {} +do + --- client_id → state + --- + --- state + --- pending_change?: function that the timer starts to trigger didChange + --- pending_changes: list of tables with the pending changesets; for incremental_sync only + --- use_incremental_sync: bool + --- buffers?: table (bufnr → lines); for incremental sync only + --- timer?: uv_timer + local state_by_client = {} + + function changetracking.init(client, bufnr) + local state = state_by_client[client.id] + if not state then + state = { + pending_changes = {}; + use_incremental_sync = ( + if_nil(client.config.flags.allow_incremental_sync, true) + and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental + ); + } + state_by_client[client.id] = state + end + if not state.use_incremental_sync then + return + end + if not state.buffers then + state.buffers = {} + end + state.buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true) + end + + function changetracking.reset_buf(client, bufnr) + local state = state_by_client[client.id] + if state then + changetracking._reset_timer(state) + if state.buffers then + state.buffers[bufnr] = nil + end + end + end + + function changetracking.reset(client_id) + local state = state_by_client[client_id] + if state then + state_by_client[client_id] = nil + changetracking._reset_timer(state) + end + end + + function changetracking.prepare(bufnr, firstline, new_lastline, changedtick) + local incremental_changes = function(client) + local cached_buffers = state_by_client[client.id].buffers + local lines = nvim_buf_get_lines(bufnr, 0, -1, true) + local startline = math.min(firstline + 1, math.min(#cached_buffers[bufnr], #lines)) + local endline = math.min(-(#lines - new_lastline), -1) + local incremental_change = vim.lsp.util.compute_diff( + cached_buffers[bufnr], lines, startline, endline, client.offset_encoding or 'utf-16') + cached_buffers[bufnr] = lines + return incremental_change + end + local full_changes = once(function() + return { + text = buf_get_full_text(bufnr); + }; + end) + local uri = vim.uri_from_bufnr(bufnr) + return function(client) + if client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.None then + return + end + local state = state_by_client[client.id] + local debounce = client.config.flags.debounce_text_changes + if not debounce then + local changes = state.use_incremental_sync and incremental_changes(client) or full_changes() + client.notify("textDocument/didChange", { + textDocument = { + uri = uri; + version = changedtick; + }; + contentChanges = { changes, } + }) + return + end + changetracking._reset_timer(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(state.pending_changes, incremental_changes(client)) + end + state.pending_change = function() + state.pending_change = nil + if client.is_stopped() then + return + end + local contentChanges + if state.use_incremental_sync then + contentChanges = state.pending_changes + state.pending_changes = {} + else + contentChanges = { full_changes(), } + end + client.notify("textDocument/didChange", { + textDocument = { + uri = uri; + version = changedtick; + }; + contentChanges = contentChanges + }) + end + state.timer = vim.loop.new_timer() + -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines + state.timer:start(debounce, 0, vim.schedule_wrap(state.pending_change)) + end + end + + function changetracking._reset_timer(state) + if state.timer then + state.timer:stop() + state.timer:close() + state.timer = nil + end + end + + --- Flushes any outstanding change notification. + function changetracking.flush(client) + local state = state_by_client[client.id] + if state then + changetracking._reset_timer(state) + if state.pending_change then + state.pending_change() + end + end + end +end + + +--@private --- Default handler for the 'textDocument/didOpen' LSP notification. --- --@param bufnr (Number) Number of the buffer, or 0 for current --@param client Client object local function text_document_did_open_handler(bufnr, client) - local use_incremental_sync = ( - if_nil(client.config.flags.allow_incremental_sync, true) - and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental - ) - if use_incremental_sync then - if not client._cached_buffers then - client._cached_buffers = {} - end - client._cached_buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true) - end + changetracking.init(client, bufnr) if not client.resolved_capabilities.text_document_open_close then return end @@ -469,6 +619,9 @@ end --- server in the initialize request. Invalid/empty values will default to "off" --@param flags: A table with flags for the client. The current (experimental) flags are: --- - allow_incremental_sync (bool, default true): Allow using incremental sync for buffer edits +--- - debounce_text_changes (number, default nil): Debounce didChange +--- notifications to the server by the given number in milliseconds. No debounce +--- occurs if nil --- --@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be --- fully initialized. Use `on_init` to do any actions once @@ -563,6 +716,7 @@ function lsp.start_client(config) uninitialized_clients[client_id] = nil lsp.diagnostic.reset(client_id, all_buffer_active_clients) + changetracking.reset(client_id) all_client_active_buffers[client_id] = nil for _, client_ids in pairs(all_buffer_active_clients) do client_ids[client_id] = nil @@ -721,6 +875,9 @@ function lsp.start_client(config) handler = resolve_handler(method) or error(string.format("not found: %q request handler for client %q.", method, client.name)) end + -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state + changetracking.flush(client) + local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr) return rpc.request(method, params, function(err, result) handler(err, method, result, client_id, bufnr) @@ -765,6 +922,7 @@ function lsp.start_client(config) function client.stop(force) lsp.diagnostic.reset(client_id, all_buffer_active_clients) + changetracking.reset(client_id) all_client_active_buffers[client_id] = nil for _, client_ids in pairs(all_buffer_active_clients) do client_ids[client_id] = nil @@ -816,20 +974,6 @@ function lsp.start_client(config) end --@private ---- Memoizes a function. On first run, the function return value is saved and ---- immediately returned on subsequent runs. ---- ---@param fn (function) Function to run ---@returns (function) Memoized function -local function once(fn) - local value - return function(...) - if not value then value = fn(...) end - return value - end -end - ---@private --@fn text_document_did_change_handler(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size) --- Notify all attached clients that a buffer has changed. local text_document_did_change_handler @@ -848,45 +992,9 @@ do if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then return end - util.buf_versions[bufnr] = changedtick - - local incremental_changes = function(client) - local lines = nvim_buf_get_lines(bufnr, 0, -1, true) - local startline = math.min(firstline + 1, math.min(#client._cached_buffers[bufnr], #lines)) - local endline = math.min(-(#lines - new_lastline), -1) - local incremental_change = vim.lsp.util.compute_diff( - client._cached_buffers[bufnr], lines, startline, endline, client.offset_encoding or "utf-16") - client._cached_buffers[bufnr] = lines - return incremental_change - end - - local full_changes = once(function() - return { - text = buf_get_full_text(bufnr); - }; - end) - - local uri = vim.uri_from_bufnr(bufnr) - for_each_buffer_client(bufnr, function(client) - local allow_incremental_sync = if_nil(client.config.flags.allow_incremental_sync, true) - local text_document_did_change = client.resolved_capabilities.text_document_did_change - local changes - if text_document_did_change == protocol.TextDocumentSyncKind.None then - return - elseif not allow_incremental_sync or text_document_did_change == protocol.TextDocumentSyncKind.Full then - changes = full_changes(client) - elseif text_document_did_change == protocol.TextDocumentSyncKind.Incremental then - changes = incremental_changes(client) - end - client.notify("textDocument/didChange", { - textDocument = { - uri = uri; - version = changedtick; - }; - contentChanges = { changes; } - }) - end) + local compute_change_and_notify = changetracking.prepare(bufnr, firstline, new_lastline, changedtick) + for_each_buffer_client(bufnr, compute_change_and_notify) end end @@ -956,9 +1064,7 @@ function lsp.buf_attach_client(bufnr, client_id) if client.resolved_capabilities.text_document_open_close then client.notify('textDocument/didClose', params) end - if client._cached_buffers then - client._cached_buffers[bufnr] = nil - end + changetracking.reset_buf(client, bufnr) end) util.buf_versions[bufnr] = nil all_buffer_active_clients[bufnr] = nil diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index 4e82c46fef..e6132e78bf 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -1140,14 +1140,14 @@ function M.show_line_diagnostics(opts, bufnr, line_nr, client_id) local message_lines = vim.split(diagnostic.message, '\n', true) table.insert(lines, prefix..message_lines[1]) - table.insert(highlights, {#prefix + 1, hiname}) + table.insert(highlights, {#prefix, hiname}) for j = 2, #message_lines do table.insert(lines, message_lines[j]) table.insert(highlights, {0, hiname}) end end - local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext') + local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts) for i, hi in ipairs(highlights) do local prefixlen, hiname = unpack(hi) -- Start highlight after the prefix diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index eacbd90077..525ec4ce5b 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -245,9 +245,22 @@ M['textDocument/completion'] = function(_, _, result) vim.fn.complete(textMatch+1, matches) end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover -M['textDocument/hover'] = function(_, method, result) - util.focusable_float(method, function() +--- |lsp-handler| for the method "textDocument/hover" +--- <pre> +--- vim.lsp.handlers["textDocument/hover"] = vim.lsp.with( +--- vim.lsp.handlers.hover, { +--- -- Use a sharp border with `FloatBorder` highlights +--- border = "single" +--- } +--- ) +--- </pre> +---@param config table Configuration table. +--- - border: (default=nil) +--- - Add borders to the floating window +--- - See |vim.api.nvim_open_win()| +function M.hover(_, method, result, _, _, config) + config = config or {} + local bufnr, winnr = util.focusable_float(method, function() if not (result and result.contents) then -- return { 'No information available' } return @@ -259,13 +272,17 @@ M['textDocument/hover'] = function(_, method, result) return end local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, { - pad_left = 1; pad_right = 1; + border = config.border }) util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr) return bufnr, winnr end) + return bufnr, winnr end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover +M['textDocument/hover'] = M.hover + --@private --- Jumps to a location. Used as a handler for multiple LSP methods. --@param _ (not used) @@ -303,8 +320,21 @@ M['textDocument/typeDefinition'] = location_handler --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation M['textDocument/implementation'] = location_handler ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp -M['textDocument/signatureHelp'] = function(_, method, result, _, bufnr) +--- |lsp-handler| for the method "textDocument/signatureHelp" +--- <pre> +--- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with( +--- vim.lsp.handlers.signature_help, { +--- -- Use a sharp border with `FloatBorder` highlights +--- border = "single" +--- } +--- ) +--- </pre> +---@param config table Configuration table. +--- - border: (default=nil) +--- - Add borders to the floating window +--- - See |vim.api.nvim_open_win()| +function M.signature_help(_, method, result, _, bufnr, config) + config = config or {} -- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore if not (result and result.signatures and result.signatures[1]) then @@ -319,11 +349,14 @@ M['textDocument/signatureHelp'] = function(_, method, result, _, bufnr) end local syntax = api.nvim_buf_get_option(bufnr, 'syntax') local p_bufnr, _ = util.focusable_preview(method, function() - return lines, util.try_trim_markdown_code_blocks(lines) + return lines, util.try_trim_markdown_code_blocks(lines), config end) api.nvim_buf_set_option(p_bufnr, 'syntax', syntax) end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp +M['textDocument/signatureHelp'] = M.signature_help + --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight M['textDocument/documentHighlight'] = function(_, _, result, _, bufnr, _) if not result then return end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index ec1131ae1f..325dc044be 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -18,6 +18,40 @@ end local M = {} +local default_border = { + {"", "NormalFloat"}, + {"", "NormalFloat"}, + {"", "NormalFloat"}, + {" ", "NormalFloat"}, + {"", "NormalFloat"}, + {"", "NormalFloat"}, + {"", "NormalFloat"}, + {" ", "NormalFloat"}, +} + +--@private +-- Check the border given by opts or the default border for the additional +-- size it adds to a float. +--@returns size of border in height and width +local function get_border_size(opts) + local border = opts and opts.border or default_border + local height = 0 + local width = 0 + + if type(border) == 'string' then + -- 'single', 'double', etc. + height = 2 + width = 2 + else + height = height + vim.fn.strdisplaywidth(border[2][1]) -- top + height = height + vim.fn.strdisplaywidth(border[6][1]) -- bottom + width = width + vim.fn.strdisplaywidth(border[4][1]) -- right + width = width + vim.fn.strdisplaywidth(border[8][1]) -- left + end + + return { height = height, width = width } +end + --@private local function split_lines(value) return split(value, '\n', true) @@ -856,7 +890,7 @@ function M.make_floating_popup_options(width, height, opts) else anchor = anchor..'S' height = math.min(lines_above, height) - row = 0 + row = -get_border_size(opts).height end if vim.fn.wincol() + width <= api.nvim_get_option('columns') then @@ -875,6 +909,7 @@ function M.make_floating_popup_options(width, height, opts) row = row + (opts.offset_y or 0), style = 'minimal', width = width, + border = opts.border or default_border, } end @@ -981,27 +1016,20 @@ function M.focusable_preview(unique_name, fn) end) end ---- Trims empty lines from input and pad left and right with spaces +--- Trims empty lines from input and pad top and bottom with empty lines --- ---@param contents table of lines to trim and pad ---@param opts dictionary with optional fields ---- - pad_left number of columns to pad contents at left (default 1) ---- - pad_right number of columns to pad contents at right (default 1) --- - pad_top number of lines to pad contents at top (default 0) --- - pad_bottom number of lines to pad contents at bottom (default 0) ---@return contents table of trimmed and padded lines -function M._trim_and_pad(contents, opts) +function M._trim(contents, opts) validate { contents = { contents, 't' }; opts = { opts, 't', true }; } opts = opts or {} - local left_padding = (" "):rep(opts.pad_left or 1) - local right_padding = (" "):rep(opts.pad_right or 1) contents = M.trim_empty_lines(contents) - for i, line in ipairs(contents) do - contents[i] = string.format('%s%s%s', left_padding, line:gsub("\r", ""), right_padding) - end if opts.pad_top then for _ = 1, opts.pad_top do table.insert(contents, 1, "") @@ -1078,8 +1106,8 @@ function M.fancy_floating_markdown(contents, opts) end end end - -- Clean up and add padding - stripped = M._trim_and_pad(stripped, opts) + -- Clean up + stripped = M._trim(stripped, opts) -- Compute size of float needed to show (wrapped) lines opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0)) @@ -1182,6 +1210,20 @@ function M._make_floating_popup_size(contents, opts) width = math.max(line_widths[i], width) end end + + local border_width = get_border_size(opts).width + local screen_width = api.nvim_win_get_width(0) + width = math.min(width, screen_width) + + -- make sure borders are always inside the screen + if width + border_width > screen_width then + width = width - (width + border_width - screen_width) + end + + if wrap_at > width then + wrap_at = width + end + if max_width then width = math.min(width, max_width) wrap_at = math.min(wrap_at or max_width, max_width) @@ -1235,7 +1277,7 @@ function M.open_floating_preview(contents, syntax, opts) opts = opts or {} -- Clean up input: trim empty lines from the end, pad - contents = M._trim_and_pad(contents, opts) + contents = M._trim(contents, opts) -- Compute size of float needed to show (wrapped) lines opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0)) diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index cac0ab864b..f223c7b8c8 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -17,6 +17,9 @@ setmetatable(M, { if k == "highlighter" then t[k] = require'vim.treesitter.highlighter' return t[k] + elseif k == "language" then + t[k] = require"vim.treesitter.language" + return t[k] end end }) diff --git a/runtime/lua/vim/treesitter/health.lua b/runtime/lua/vim/treesitter/health.lua new file mode 100644 index 0000000000..dd0b11a6c7 --- /dev/null +++ b/runtime/lua/vim/treesitter/health.lua @@ -0,0 +1,34 @@ +local M = {} +local ts = vim.treesitter + +function M.list_parsers() + return vim.api.nvim_get_runtime_file('parser/*', true) +end + +function M.check_health() + local report_info = vim.fn['health#report_info'] + local report_ok = vim.fn['health#report_ok'] + local report_error = vim.fn['health#report_error'] + local parsers = M.list_parsers() + + report_info(string.format("Runtime ABI version : %d", ts.language_version)) + + for _, parser in pairs(parsers) do + local parsername = vim.fn.fnamemodify(parser, ":t:r") + + local is_loadable, ret = pcall(ts.language.require_language, parsername) + + if not is_loadable then + report_error(string.format("Impossible to load parser for %s: %s", parsername, ret)) + elseif ret then + local lang = ts.language.inspect_language(parsername) + report_ok(string.format("Loaded parser for %s: ABI version %d", + parsername, lang._abi_version)) + else + report_error(string.format("Unable to load parser for %s", parsername)) + end + end +end + +return M + diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 79a88c5dbb..ed5146be44 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -22,6 +22,16 @@ local function dedupe_files(files) return result end +local function safe_read(filename, read_quantifier) + local file, err = io.open(filename, 'r') + if not file then + error(err) + end + local content = file:read(read_quantifier) + io.close(file) + return content +end + function M.get_query_files(lang, query_name, is_included) local query_path = string.format('queries/%s/%s.scm', lang, query_name) local lang_files = dedupe_files(a.nvim_get_runtime_file(query_path, true)) @@ -38,7 +48,7 @@ function M.get_query_files(lang, query_name, is_included) local MODELINE_FORMAT = "^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$" for _, file in ipairs(lang_files) do - local modeline = io.open(file, 'r'):read('*l') + local modeline = safe_read(file, '*l') if modeline then local langlist = modeline:match(MODELINE_FORMAT) @@ -73,7 +83,7 @@ local function read_query_files(filenames) local contents = {} for _,filename in ipairs(filenames) do - table.insert(contents, io.open(filename, 'r'):read('*a')) + table.insert(contents, safe_read(filename, '*a')) end return table.concat(contents, '') |