diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/F.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/_meta.lua | 123 | ||||
-rw-r--r-- | runtime/lua/vim/diagnostic.lua | 545 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 228 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 106 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/diagnostic.lua | 79 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 12 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/log.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 144 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 13 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/sync.lua | 86 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/tagfunc.lua | 76 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 465 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 20 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 44 | ||||
-rw-r--r-- | runtime/lua/vim/uri.lua | 28 |
19 files changed, 1062 insertions, 918 deletions
diff --git a/runtime/lua/vim/F.lua b/runtime/lua/vim/F.lua index 1a258546a5..9327c652db 100644 --- a/runtime/lua/vim/F.lua +++ b/runtime/lua/vim/F.lua @@ -27,7 +27,7 @@ function F.nil_wrap(fn) end end ---- like {...} except preserve the lenght explicitly +--- like {...} except preserve the length explicitly function F.pack_len(...) return {n=select('#', ...), ...} end diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua index f7d47c1030..522e26caa7 100644 --- a/runtime/lua/vim/_meta.lua +++ b/runtime/lua/vim/_meta.lua @@ -16,10 +16,6 @@ for _, v in pairs(a.nvim_get_all_options_info()) do if v.shortname ~= "" then options_info[v.shortname] = v end end -local is_global_option = function(info) return info.scope == "global" end -local is_buffer_option = function(info) return info.scope == "buf" end -local is_window_option = function(info) return info.scope == "win" end - local get_scoped_options = function(scope) local result = {} for name, option_info in pairs(options_info) do @@ -133,107 +129,18 @@ do -- window option accessor vim.wo = new_win_opt_accessor(nil) end ---[[ -Local window setter - -buffer options: does not get copied when split - nvim_set_option(buf_opt, value) -> sets the default for NEW buffers - this sets the hidden global default for buffer options - - nvim_buf_set_option(...) -> sets the local value for the buffer - - set opt=value, does BOTH global default AND buffer local value - setlocal opt=value, does ONLY buffer local value - -window options: gets copied - does not need to call nvim_set_option because nobody knows what the heck this does⸮ - We call it anyway for more readable code. - - - Command global value local value - :set option=value set set - :setlocal option=value - set -:setglobal option=value set - ---]] -local function set_scoped_option(k, v, set_type) - local info = options_info[k] - - -- Don't let people do setlocal with global options. - -- That is a feature that doesn't make sense. - if set_type == SET_TYPES.LOCAL and is_global_option(info) then - error(string.format("Unable to setlocal option: '%s', which is a global option.", k)) - end - - -- Only `setlocal` skips setting the default/global value - -- This will more-or-less noop for window options, but that's OK - if set_type ~= SET_TYPES.LOCAL then - a.nvim_set_option(k, v) - end - - if is_window_option(info) then - if set_type ~= SET_TYPES.GLOBAL then - a.nvim_win_set_option(0, k, v) - end - elseif is_buffer_option(info) then - if set_type == SET_TYPES.LOCAL - or (set_type == SET_TYPES.SET and not info.global_local) then - a.nvim_buf_set_option(0, k, v) - end - end -end - ---[[ -Local window getter - - Command global value local value - :set option? - display - :setlocal option? - display -:setglobal option? display - ---]] -local function get_scoped_option(k, set_type) - local info = assert(options_info[k], "Must be a valid option: " .. tostring(k)) - - if set_type == SET_TYPES.GLOBAL or is_global_option(info) then - return a.nvim_get_option(k) - end - - if is_buffer_option(info) then - local was_set, value = pcall(a.nvim_buf_get_option, 0, k) - if was_set then return value end - - if info.global_local then - return a.nvim_get_option(k) - end - - error("buf_get: This should not be able to happen, given my understanding of options // " .. k) - end - - if is_window_option(info) then - local ok, value = pcall(a.nvim_win_get_option, 0, k) - if ok then - return value - end - - local global_ok, global_val = pcall(a.nvim_get_option, k) - if global_ok then - return global_val - end - - error("win_get: This should never happen. File an issue and tag @tjdevries") - end - - error("This fallback case should not be possible. " .. k) -end - -- vim global option -- this ONLY sets the global option. like `setglobal` -vim.go = make_meta_accessor(a.nvim_get_option, a.nvim_set_option) +vim.go = make_meta_accessor( + function(k) return a.nvim_get_option_value(k, {scope = "global"}) end, + function(k, v) return a.nvim_set_option_value(k, v, {scope = "global"}) end +) -- vim `set` style options. -- it has no additional metamethod magic. vim.o = make_meta_accessor( - function(k) return get_scoped_option(k, SET_TYPES.SET) end, - function(k, v) return set_scoped_option(k, v, SET_TYPES.SET) end + function(k) return a.nvim_get_option_value(k, {}) end, + function(k, v) return a.nvim_set_option_value(k, v, {}) end ) ---@brief [[ @@ -389,6 +296,10 @@ local convert_value_to_vim = (function() } return function(name, info, value) + if value == nil then + return vim.NIL + end + local option_type = get_option_type(name, info) assert_valid_value(name, value, valid_types[option_type]) @@ -398,7 +309,7 @@ end)() --- Converts a vimoption_T style value to a Lua value local convert_value_to_lua = (function() - -- Map of OptionType to functions that take vimoption_T values and conver to lua values. + -- Map of OptionType to functions that take vimoption_T values and convert to lua values. -- Each function takes (info, vim_value) -> lua_value local to_lua_value = { [OptionTypes.BOOLEAN] = function(_, value) return value end, @@ -671,15 +582,19 @@ local create_option_metatable = function(set_type) }, option_mt) end - -- TODO(tjdevries): consider supporting `nil` for set to remove the local option. - -- vim.cmd [[set option<]] + local scope + if set_type == SET_TYPES.GLOBAL then + scope = "global" + elseif set_type == SET_TYPES.LOCAL then + scope = "local" + end option_mt = { -- To set a value, instead use: -- opt[my_option] = value _set = function(self) local value = convert_value_to_vim(self._name, self._info, self._value) - set_scoped_option(self._name, value, set_type) + a.nvim_set_option_value(self._name, value, {scope = scope}) return self end, @@ -716,7 +631,7 @@ local create_option_metatable = function(set_type) set_mt = { __index = function(_, k) - return make_option(k, get_scoped_option(k, set_type)) + return make_option(k, a.nvim_get_option_value(k, {scope = scope})) end, __newindex = function(_, k, v) diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index b30a678eeb..742ebf69b2 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -36,7 +36,34 @@ M.handlers = setmetatable({}, { end, }) --- Local functions {{{ +-- Metatable that automatically creates an empty table when assigning to a missing key +local bufnr_and_namespace_cacher_mt = { + __index = function(t, bufnr) + assert(bufnr > 0, "Invalid buffer number") + t[bufnr] = {} + return t[bufnr] + end, +} + +local diagnostic_cache = setmetatable({}, { + __index = function(t, bufnr) + assert(bufnr > 0, "Invalid buffer number") + vim.api.nvim_buf_attach(bufnr, false, { + on_detach = function() + rawset(t, bufnr, nil) -- clear cache + end + }) + t[bufnr] = {} + return t[bufnr] + end, +}) + +local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt) +local diagnostic_attached_buffers = {} +local diagnostic_disabled = {} +local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt) + +local all_namespaces = {} ---@private local function to_severity(severity) @@ -64,23 +91,22 @@ local function filter_by_severity(severity, diagnostics) end ---@private -local function prefix_source(source, diagnostics) - vim.validate { source = {source, function(v) - return v == "always" or v == "if_many" - end, "'always' or 'if_many'" } } - - if source == "if_many" then - local sources = {} - for _, d in pairs(diagnostics) do - if d.source then - sources[d.source] = true +local function count_sources(bufnr) + local seen = {} + local count = 0 + for _, namespace_diagnostics in pairs(diagnostic_cache[bufnr]) do + for _, diagnostic in ipairs(namespace_diagnostics) do + if diagnostic.source and not seen[diagnostic.source] then + seen[diagnostic.source] = true + count = count + 1 end end - if #vim.tbl_keys(sources) <= 1 then - return diagnostics - end end + return count +end +---@private +local function prefix_source(diagnostics) return vim.tbl_map(function(d) if not d.source then return d @@ -106,8 +132,6 @@ local function reformat_diagnostics(format, diagnostics) return formatted end -local all_namespaces = {} - ---@private local function enabled_value(option, namespace) local ns = namespace and M.get_namespace(namespace) or {} @@ -213,38 +237,13 @@ local function get_bufnr(bufnr) return bufnr end --- Metatable that automatically creates an empty table when assigning to a missing key -local bufnr_and_namespace_cacher_mt = { - __index = function(t, bufnr) - if not bufnr or bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - - if rawget(t, bufnr) == nil then - rawset(t, bufnr, {}) - end - - return rawget(t, bufnr) - end, - - __newindex = function(t, bufnr, v) - if not bufnr or bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - - rawset(t, bufnr, v) - end, -} - -local diagnostic_cleanup = setmetatable({}, bufnr_and_namespace_cacher_mt) -local diagnostic_cache = setmetatable({}, bufnr_and_namespace_cacher_mt) -local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt) -local diagnostic_attached_buffers = {} -local diagnostic_disabled = {} -local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt) - ---@private local function is_disabled(namespace, bufnr) + local ns = M.get_namespace(namespace) + if ns.disabled then + return true + end + if type(diagnostic_disabled[bufnr]) == "table" then return diagnostic_disabled[bufnr][namespace] end @@ -272,6 +271,8 @@ end ---@private local function set_diagnostic_cache(namespace, bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do + assert(diagnostic.lnum, "Diagnostic line number is required") + assert(diagnostic.col, "Diagnostic column is required") diagnostic.severity = diagnostic.severity and to_severity(diagnostic.severity) or M.severity.ERROR diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum diagnostic.end_col = diagnostic.end_col or diagnostic.col @@ -282,11 +283,6 @@ local function set_diagnostic_cache(namespace, bufnr, diagnostics) end ---@private -local function clear_diagnostic_cache(namespace, bufnr) - diagnostic_cache[bufnr][namespace] = nil -end - ----@private local function restore_extmarks(bufnr, last) for ns, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do local extmarks_current = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true}) @@ -302,11 +298,6 @@ local function restore_extmarks(bufnr, last) if not found[extmark[1]] then local opts = extmark[4] opts.id = extmark[1] - -- HACK: end_row should be end_line - if opts.end_row then - opts.end_line = opts.end_row - opts.end_row = nil - end pcall(vim.api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts) end end @@ -372,6 +363,71 @@ local function clear_scheduled_display(namespace, bufnr) end ---@private +local function get_diagnostics(bufnr, opts, clamp) + opts = opts or {} + + local namespace = opts.namespace + local diagnostics = {} + + -- Memoized results of buf_line_count per bufnr + local buf_line_count = setmetatable({}, { + __index = function(t, k) + t[k] = vim.api.nvim_buf_line_count(k) + return rawget(t, k) + end, + }) + + ---@private + local function add(b, d) + if not opts.lnum or d.lnum == opts.lnum then + if clamp and vim.api.nvim_buf_is_loaded(b) then + local line_count = buf_line_count[b] - 1 + if (d.lnum > line_count or d.end_lnum > line_count or d.lnum < 0 or d.end_lnum < 0) then + d = vim.deepcopy(d) + d.lnum = math.max(math.min(d.lnum, line_count), 0) + d.end_lnum = math.max(math.min(d.end_lnum, line_count), 0) + end + end + table.insert(diagnostics, d) + end + end + + if namespace == nil and bufnr == nil then + for b, t in pairs(diagnostic_cache) do + for _, v in pairs(t) do + for _, diagnostic in pairs(v) do + add(b, diagnostic) + end + end + end + elseif namespace == nil then + bufnr = get_bufnr(bufnr) + for iter_namespace in pairs(diagnostic_cache[bufnr]) do + for _, diagnostic in pairs(diagnostic_cache[bufnr][iter_namespace]) do + add(bufnr, diagnostic) + end + end + elseif bufnr == nil then + for b, t in pairs(diagnostic_cache) do + for _, diagnostic in pairs(t[namespace] or {}) do + add(b, diagnostic) + end + end + else + bufnr = get_bufnr(bufnr) + for _, diagnostic in pairs(diagnostic_cache[bufnr][namespace] or {}) do + add(bufnr, diagnostic) + end + end + + if opts.severity then + diagnostics = filter_by_severity(opts.severity, diagnostics) + end + + return diagnostics +end + +---@private local function set_list(loclist, opts) opts = opts or {} local open = vim.F.if_nil(opts.open, true) @@ -381,7 +437,9 @@ local function set_list(loclist, opts) if loclist then bufnr = vim.api.nvim_win_get_buf(winnr) end - local diagnostics = M.get(bufnr, opts) + -- Don't clamp line numbers since the quickfix list can already handle line + -- numbers beyond the end of the buffer + local diagnostics = get_diagnostics(bufnr, opts, false) local items = M.toqflist(diagnostics) if loclist then vim.fn.setloclist(winnr, {}, ' ', { title = title, items = items }) @@ -394,27 +452,12 @@ local function set_list(loclist, opts) end ---@private ---- To (slightly) improve performance, modifies diagnostics in place. -local function clamp_line_numbers(bufnr, diagnostics) - local buf_line_count = vim.api.nvim_buf_line_count(bufnr) - if buf_line_count == 0 then - return - end - - for _, diagnostic in ipairs(diagnostics) do - diagnostic.lnum = math.max(math.min(diagnostic.lnum, buf_line_count - 1), 0) - diagnostic.end_lnum = math.max(math.min(diagnostic.end_lnum, buf_line_count - 1), 0) - end -end - ----@private local function next_diagnostic(position, search_forward, bufnr, opts, namespace) position[1] = position[1] - 1 bufnr = get_bufnr(bufnr) local wrap = vim.F.if_nil(opts.wrap, true) local line_count = vim.api.nvim_buf_line_count(bufnr) - local diagnostics = M.get(bufnr, vim.tbl_extend("keep", opts, {namespace = namespace})) - clamp_line_numbers(bufnr, diagnostics) + local diagnostics = get_diagnostics(bufnr, vim.tbl_extend("keep", opts, {namespace = namespace}), true) local line_diagnostics = diagnostic_lines(diagnostics) for i = 0, line_count do local offset = i * (search_forward and 1 or -1) @@ -426,13 +469,14 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace) lnum = (lnum + line_count) % line_count end if line_diagnostics[lnum] and not vim.tbl_isempty(line_diagnostics[lnum]) then + local line_length = #vim.api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1] local sort_diagnostics, is_next if search_forward then sort_diagnostics = function(a, b) return a.col < b.col end - is_next = function(diagnostic) return diagnostic.col > position[2] end + is_next = function(d) return math.min(d.col, line_length - 1) > position[2] end else sort_diagnostics = function(a, b) return a.col > b.col end - is_next = function(diagnostic) return diagnostic.col < position[2] end + is_next = function(d) return math.min(d.col, line_length - 1) < position[2] end end table.sort(line_diagnostics[lnum], sort_diagnostics) if i == 0 then @@ -460,26 +504,28 @@ local function diagnostic_move_pos(opts, pos) return end - -- Save position in the window's jumplist - vim.api.nvim_win_call(win_id, function() vim.cmd("normal! m'") end) - - vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]}) + vim.api.nvim_win_call(win_id, function() + -- Save position in the window's jumplist + vim.cmd("normal! m'") + vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]}) + -- Open folds under the cursor + vim.cmd("normal! zv") + end) if float then local float_opts = type(float) == "table" and float or {} vim.schedule(function() M.open_float( - vim.api.nvim_win_get_buf(win_id), - vim.tbl_extend("keep", float_opts, {scope="cursor"}) + vim.tbl_extend("keep", float_opts, { + bufnr = vim.api.nvim_win_get_buf(win_id), + scope = "cursor", + focus = false, + }) ) end) end end --- }}} - --- Public API {{{ - --- Configure diagnostic options globally or for a specific diagnostic --- namespace. --- @@ -513,8 +559,10 @@ end --- - virtual_text: (default true) Use virtual text for diagnostics. Options: --- * severity: Only show virtual text for diagnostics matching the given --- severity |diagnostic-severity| ---- * source: (string) Include the diagnostic source in virtual ---- text. One of "always" or "if_many". +--- * source: (boolean or string) Include the diagnostic source in virtual +--- text. Use "if_many" to only show sources if there is more than +--- one diagnostic source in the buffer. Otherwise, any truthy value +--- means to always show the diagnostic source. --- * format: (function) A function that takes a diagnostic as input and --- returns a string. The return value is the text used to display --- the diagnostic. Example: @@ -532,19 +580,7 @@ end --- * priority: (number, default 10) Base priority to use for signs. When --- {severity_sort} is used, the priority of a sign is adjusted based on --- its severity. Otherwise, all signs use the same priority. ---- - float: Options for floating windows: ---- * severity: See |diagnostic-severity|. ---- * show_header: (boolean, default true) Show "Diagnostics:" header ---- * source: (string) Include the diagnostic source in ---- the message. One of "always" or "if_many". ---- * format: (function) A function that takes a diagnostic as input and returns a ---- string. The return value is the text used to display the diagnostic. ---- * prefix: (function or string) Prefix each diagnostic in the floating window. If ---- a function, it must have the signature (diagnostic, i, total) -> string, ---- where {i} is the index of the diagnostic being evaluated and {total} is ---- the total number of diagnostics displayed in the window. The returned ---- string is prepended to each diagnostic in the window. Otherwise, ---- if {prefix} is a string, it is prepended to each diagnostic. +--- - float: Options for floating windows. See |vim.diagnostic.open_float()|. --- - update_in_insert: (default false) Update diagnostics in Insert mode (if false, --- diagnostics are updated on InsertLeave) --- - severity_sort: (default false) Sort diagnostics by severity. This affects the order in @@ -552,6 +588,7 @@ end --- are displayed before lower severities (e.g. ERROR is displayed before WARN). --- Options: --- * reverse: (boolean) Reverse sort order +--- ---@param namespace number|nil Update the options for the given namespace. When omitted, update the --- global diagnostic options. function M.config(opts, namespace) @@ -605,33 +642,31 @@ function M.set(namespace, bufnr, diagnostics, opts) opts = {opts, 't', true}, } + bufnr = get_bufnr(bufnr) + if vim.tbl_isempty(diagnostics) then - clear_diagnostic_cache(namespace, bufnr) + diagnostic_cache[bufnr][namespace] = nil else - if not diagnostic_cleanup[bufnr][namespace] then - diagnostic_cleanup[bufnr][namespace] = true - - -- Clean up our data when the buffer unloads. - vim.api.nvim_buf_attach(bufnr, false, { - on_detach = function(_, b) - clear_diagnostic_cache(namespace, b) - diagnostic_cleanup[b][namespace] = nil - end - }) - end set_diagnostic_cache(namespace, bufnr, diagnostics) end if vim.api.nvim_buf_is_loaded(bufnr) then - M.show(namespace, bufnr, diagnostics, opts) + M.show(namespace, bufnr, nil, opts) end - vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged") + vim.api.nvim_buf_call(bufnr, function() + vim.api.nvim_command( + string.format( + "doautocmd <nomodeline> DiagnosticChanged %s", + vim.fn.fnameescape(vim.api.nvim_buf_get_name(bufnr)) + ) + ) + end) end --- Get namespace metadata. --- ----@param ns number Diagnostic namespace +---@param namespace number Diagnostic namespace ---@return table Namespace metadata function M.get_namespace(namespace) vim.validate { namespace = { namespace, 'n' } } @@ -677,49 +712,7 @@ function M.get(bufnr, opts) opts = { opts, 't', true }, } - opts = opts or {} - - local namespace = opts.namespace - local diagnostics = {} - - ---@private - local function add(d) - if not opts.lnum or d.lnum == opts.lnum then - table.insert(diagnostics, d) - end - end - - if namespace == nil and bufnr == nil then - for _, t in pairs(diagnostic_cache) do - for _, v in pairs(t) do - for _, diagnostic in pairs(v) do - add(diagnostic) - end - end - end - elseif namespace == nil then - for iter_namespace in pairs(diagnostic_cache[bufnr]) do - for _, diagnostic in pairs(diagnostic_cache[bufnr][iter_namespace]) do - add(diagnostic) - end - end - elseif bufnr == nil then - for _, t in pairs(diagnostic_cache) do - for _, diagnostic in pairs(t[namespace] or {}) do - add(diagnostic) - end - end - else - for _, diagnostic in pairs(diagnostic_cache[bufnr][namespace] or {}) do - add(diagnostic) - end - end - - if opts.severity then - diagnostics = filter_by_severity(opts.severity, diagnostics) - end - - return diagnostics + return get_diagnostics(bufnr, opts, false) end --- Get the previous diagnostic closest to the cursor position. @@ -795,7 +788,9 @@ end --- - severity: See |diagnostic-severity|. --- - float: (boolean or table, default true) If "true", call |vim.diagnostic.open_float()| --- after moving. If a table, pass the table as the {opts} parameter to ---- |vim.diagnostic.open_float()|. +--- |vim.diagnostic.open_float()|. Unless overridden, the float will show +--- diagnostics at the new cursor position (as if "cursor" were passed to +--- the "scope" option). --- - win_id: (number, default 0) Window ID function M.goto_next(opts) return diagnostic_move_pos( @@ -931,8 +926,11 @@ M.handlers.virtual_text = { if opts.virtual_text.format then diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics) end - if opts.virtual_text.source then - diagnostics = prefix_source(opts.virtual_text.source, diagnostics) + if + opts.virtual_text.source + and (opts.virtual_text.source ~= "if_many" or count_sources(bufnr) > 1) + then + diagnostics = prefix_source(diagnostics) end if opts.virtual_text.severity then severity = opts.virtual_text.severity @@ -1039,19 +1037,22 @@ end --- ---@param namespace number|nil Diagnostic namespace. When omitted, hide --- diagnostics from all namespaces. ----@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param bufnr number|nil Buffer number, or 0 for current buffer. When +--- omitted, hide diagnostics in all buffers. function M.hide(namespace, bufnr) vim.validate { namespace = { namespace, 'n', true }, bufnr = { bufnr, 'n', true }, } - bufnr = get_bufnr(bufnr) - local namespaces = namespace and {namespace} or vim.tbl_keys(diagnostic_cache[bufnr]) - for _, iter_namespace in ipairs(namespaces) do - for _, handler in pairs(M.handlers) do - if handler.hide then - handler.hide(iter_namespace, bufnr) + local buffers = bufnr and {get_bufnr(bufnr)} or vim.tbl_keys(diagnostic_cache) + for _, iter_bufnr in ipairs(buffers) do + local namespaces = namespace and {namespace} or vim.tbl_keys(diagnostic_cache[iter_bufnr]) + for _, iter_namespace in ipairs(namespaces) do + for _, handler in pairs(M.handlers) do + if handler.hide then + handler.hide(iter_namespace, iter_bufnr) + end end end end @@ -1061,12 +1062,14 @@ end --- ---@param namespace number|nil Diagnostic namespace. When omitted, show --- diagnostics from all namespaces. ----@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param bufnr number|nil Buffer number, or 0 for current buffer. When omitted, show +--- diagnostics in all buffers. ---@param diagnostics table|nil The diagnostics to display. When omitted, use the --- saved diagnostics for the given namespace and --- buffer. This can be used to display a list of diagnostics --- without saving them or to display only a subset of ---- diagnostics. May not be used when {namespace} is nil. +--- diagnostics. May not be used when {namespace} +--- or {bufnr} is nil. ---@param opts table|nil Display options. See |vim.diagnostic.config()|. function M.show(namespace, bufnr, diagnostics, opts) vim.validate { @@ -1076,11 +1079,18 @@ function M.show(namespace, bufnr, diagnostics, opts) opts = { opts, 't', true }, } - bufnr = get_bufnr(bufnr) - if not namespace then - assert(not diagnostics, "Cannot show diagnostics without a namespace") - for iter_namespace in pairs(diagnostic_cache[bufnr]) do - M.show(iter_namespace, bufnr, nil, opts) + if not bufnr or not namespace then + assert(not diagnostics, "Cannot show diagnostics without a buffer and namespace") + if not bufnr then + for iter_bufnr in pairs(diagnostic_cache) do + M.show(namespace, iter_bufnr, nil, opts) + end + else + -- namespace is nil + bufnr = get_bufnr(bufnr) + for iter_namespace in pairs(diagnostic_cache[bufnr]) do + M.show(iter_namespace, bufnr, nil, opts) + end end return end @@ -1091,7 +1101,7 @@ function M.show(namespace, bufnr, diagnostics, opts) M.hide(namespace, bufnr) - diagnostics = diagnostics or M.get(bufnr, {namespace=namespace}) + diagnostics = diagnostics or get_diagnostics(bufnr, {namespace=namespace}, true) if not diagnostics or vim.tbl_isempty(diagnostics) then return @@ -1117,8 +1127,6 @@ function M.show(namespace, bufnr, diagnostics, opts) end end - clamp_line_numbers(bufnr, diagnostics) - for handler_name, handler in pairs(M.handlers) do if handler.show and opts[handler_name] then handler.show(namespace, bufnr, diagnostics, opts) @@ -1128,12 +1136,15 @@ end --- Show diagnostics in a floating window. --- ----@param bufnr number|nil Buffer number. Defaults to the current buffer. ---@param opts table|nil Configuration table with the same keys as --- |vim.lsp.util.open_floating_preview()| in addition to the following: +--- - bufnr: (number) Buffer number to show diagnostics from. +--- Defaults to the current buffer. --- - namespace: (number) Limit diagnostics to the given namespace ---- - scope: (string, default "buffer") Show diagnostics from the whole buffer ("buffer"), +--- - scope: (string, default "line") Show diagnostics from the whole buffer ("buffer"), --- the current cursor line ("line"), or the current cursor position ("cursor"). +--- Shorthand versions are also accepted ("c" for "cursor", "l" for "line", "b" +--- for "buffer"). --- - pos: (number or table) If {scope} is "line" or "cursor", use this position rather --- than the cursor position. If a number, interpreted as a line number; --- otherwise, a (row, col) tuple. @@ -1141,25 +1152,45 @@ end --- from |vim.diagnostic.config()|. --- - severity: See |diagnostic-severity|. Overrides the setting from --- |vim.diagnostic.config()|. ---- - show_header: (boolean, default true) Show "Diagnostics:" header. Overrides the ---- setting from |vim.diagnostic.config()|. ---- - source: (string) Include the diagnostic source in the message. One of "always" or ---- "if_many". Overrides the setting from |vim.diagnostic.config()|. +--- - header: (string or table) String to use as the header for the floating window. If a +--- table, it is interpreted as a [text, hl_group] tuple. Overrides the setting +--- from |vim.diagnostic.config()|. +--- - source: (boolean or string) Include the diagnostic source in the message. +--- Use "if_many" to only show sources if there is more than one source of +--- diagnostics in the buffer. Otherwise, any truthy value means to always show +--- the diagnostic source. Overrides the setting from +--- |vim.diagnostic.config()|. --- - format: (function) A function that takes a diagnostic as input and returns a --- string. The return value is the text used to display the diagnostic. --- Overrides the setting from |vim.diagnostic.config()|. ---- - prefix: (function or string) Prefix each diagnostic in the floating window. +--- - prefix: (function, string, or table) Prefix each diagnostic in the floating +--- window. If a function, it must have the signature (diagnostic, i, +--- total) -> (string, string), where {i} is the index of the diagnostic +--- being evaluated and {total} is the total number of diagnostics +--- displayed in the window. The function should return a string which +--- is prepended to each diagnostic in the window as well as an +--- (optional) highlight group which will be used to highlight the +--- prefix. If {prefix} is a table, it is interpreted as a [text, +--- hl_group] tuple as in |nvim_echo()|; otherwise, if {prefix} is a +--- string, it is prepended to each diagnostic in the window with no +--- highlight. --- Overrides the setting from |vim.diagnostic.config()|. ---@return tuple ({float_bufnr}, {win_id}) -function M.open_float(bufnr, opts) - vim.validate { - bufnr = { bufnr, 'n', true }, - opts = { opts, 't', true }, - } +function M.open_float(opts, ...) + -- Support old (bufnr, opts) signature + local bufnr + if opts == nil or type(opts) == "number" then + bufnr = opts + opts = ... + else + vim.validate { + opts = { opts, 't', true }, + } + end opts = opts or {} - bufnr = get_bufnr(bufnr) - local scope = opts.scope or "buffer" + bufnr = get_bufnr(bufnr or opts.bufnr) + local scope = ({l = "line", c = "cursor", b = "buffer"})[opts.scope] or opts.scope or "line" local lnum, col if scope == "line" or scope == "cursor" then if not opts.pos then @@ -1188,8 +1219,7 @@ function M.open_float(bufnr, opts) opts = get_resolved_options({ float = float_opts }, nil, bufnr).float end - local diagnostics = M.get(bufnr, opts) - clamp_line_numbers(bufnr, diagnostics) + local diagnostics = get_diagnostics(bufnr, opts, true) if scope == "line" then diagnostics = vim.tbl_filter(function(d) @@ -1220,35 +1250,56 @@ function M.open_float(bufnr, opts) local lines = {} local highlights = {} - local show_header = vim.F.if_nil(opts.show_header, true) - if show_header then - table.insert(lines, "Diagnostics:") - table.insert(highlights, {0, "Bold"}) + local header = if_nil(opts.header, "Diagnostics:") + if header then + vim.validate { header = { header, function(v) + return type(v) == "string" or type(v) == "table" + end, "'string' or 'table'" } } + if type(header) == "table" then + -- Don't insert any lines for an empty string + if string.len(if_nil(header[1], "")) > 0 then + table.insert(lines, header[1]) + table.insert(highlights, {0, header[2] or "Bold"}) + end + elseif #header > 0 then + table.insert(lines, header) + table.insert(highlights, {0, "Bold"}) + end end if opts.format then diagnostics = reformat_diagnostics(opts.format, diagnostics) end - if opts.source then - diagnostics = prefix_source(opts.source, diagnostics) + if opts.source and (opts.source ~= "if_many" or count_sources(bufnr) > 1) then + diagnostics = prefix_source(diagnostics) end local prefix_opt = if_nil(opts.prefix, (scope == "cursor" and #diagnostics <= 1) and "" or function(_, i) return string.format("%d. ", i) end) + + local prefix, prefix_hl_group if prefix_opt then vim.validate { prefix = { prefix_opt, function(v) - return type(v) == "string" or type(v) == "function" - end, "'string' or 'function'" } } + return type(v) == "string" or type(v) == "table" or type(v) == "function" + end, "'string' or 'table' or 'function'" } } + if type(prefix_opt) == "string" then + prefix, prefix_hl_group = prefix_opt, "NormalFloat" + elseif type(prefix_opt) == "table" then + prefix, prefix_hl_group = prefix_opt[1] or "", prefix_opt[2] or "NormalFloat" + end end for i, diagnostic in ipairs(diagnostics) do - local prefix = type(prefix_opt) == "string" and prefix_opt or prefix_opt(diagnostic, i, #diagnostics) + if prefix_opt and type(prefix_opt) == "function" then + prefix, prefix_hl_group = prefix_opt(diagnostic, i, #diagnostics) + prefix, prefix_hl_group = prefix or "", prefix_hl_group or "NormalFloat" + end local hiname = floating_highlight_map[diagnostic.severity] local message_lines = vim.split(diagnostic.message, '\n') table.insert(lines, prefix..message_lines[1]) - table.insert(highlights, {#prefix, hiname}) + table.insert(highlights, {#prefix, hiname, prefix_hl_group}) for j = 2, #message_lines do table.insert(lines, string.rep(' ', #prefix) .. message_lines[j]) table.insert(highlights, {0, hiname}) @@ -1261,8 +1312,10 @@ function M.open_float(bufnr, opts) end local float_bufnr, winnr = require('vim.lsp.util').open_floating_preview(lines, 'plaintext', opts) for i, hi in ipairs(highlights) do - local prefixlen, hiname = unpack(hi) - -- Start highlight after the prefix + local prefixlen, hiname, prefix_hiname = unpack(hi) + if prefix_hiname then + vim.api.nvim_buf_add_highlight(float_bufnr, -1, prefix_hiname, i-1, 0, prefixlen) + end vim.api.nvim_buf_add_highlight(float_bufnr, -1, hiname, i-1, prefixlen, -1) end @@ -1286,16 +1339,23 @@ function M.reset(namespace, bufnr) bufnr = {bufnr, 'n', true}, } - local buffers = bufnr and {bufnr} or vim.tbl_keys(diagnostic_cache) + local buffers = bufnr and {get_bufnr(bufnr)} or vim.tbl_keys(diagnostic_cache) for _, iter_bufnr in ipairs(buffers) do local namespaces = namespace and {namespace} or vim.tbl_keys(diagnostic_cache[iter_bufnr]) for _, iter_namespace in ipairs(namespaces) do - clear_diagnostic_cache(iter_namespace, iter_bufnr) + diagnostic_cache[iter_bufnr][iter_namespace] = nil M.hide(iter_namespace, iter_bufnr) end end - vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged") + vim.api.nvim_buf_call(bufnr, function() + vim.api.nvim_command( + string.format( + "doautocmd <nomodeline> DiagnosticChanged %s", + vim.fn.fnameescape(vim.api.nvim_buf_get_name(bufnr)) + ) + ) + end) end --- Add all diagnostics to the quickfix list. @@ -1323,44 +1383,67 @@ end --- Disable diagnostics in the given buffer. --- ----@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param bufnr number|nil Buffer number, or 0 for current buffer. When +--- omitted, disable diagnostics in all buffers. ---@param namespace number|nil Only disable diagnostics for the given namespace. function M.disable(bufnr, namespace) vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} } - bufnr = get_bufnr(bufnr) - if namespace == nil then - diagnostic_disabled[bufnr] = true - for ns in pairs(diagnostic_cache[bufnr]) do - M.hide(ns, bufnr) + if bufnr == nil then + if namespace == nil then + -- Disable everything (including as yet non-existing buffers and + -- namespaces) by setting diagnostic_disabled to an empty table and set + -- its metatable to always return true. This metatable is removed + -- in enable() + diagnostic_disabled = setmetatable({}, { + __index = function() return true end, + }) + else + local ns = M.get_namespace(namespace) + ns.disabled = true end else - if type(diagnostic_disabled[bufnr]) ~= "table" then - diagnostic_disabled[bufnr] = {} + bufnr = get_bufnr(bufnr) + if namespace == nil then + diagnostic_disabled[bufnr] = true + else + if type(diagnostic_disabled[bufnr]) ~= "table" then + diagnostic_disabled[bufnr] = {} + end + diagnostic_disabled[bufnr][namespace] = true end - diagnostic_disabled[bufnr][namespace] = true - M.hide(namespace, bufnr) end + + M.hide(namespace, bufnr) end --- Enable diagnostics in the given buffer. --- ----@param bufnr number|nil Buffer number. Defaults to the current buffer. +---@param bufnr number|nil Buffer number, or 0 for current buffer. When +--- omitted, enable diagnostics in all buffers. ---@param namespace number|nil Only enable diagnostics for the given namespace. function M.enable(bufnr, namespace) vim.validate { bufnr = {bufnr, 'n', true}, namespace = {namespace, 'n', true} } - bufnr = get_bufnr(bufnr) - if namespace == nil then - diagnostic_disabled[bufnr] = nil - for ns in pairs(diagnostic_cache[bufnr]) do - M.show(ns, bufnr) + if bufnr == nil then + if namespace == nil then + -- Enable everything by setting diagnostic_disabled to an empty table + diagnostic_disabled = {} + else + local ns = M.get_namespace(namespace) + ns.disabled = false end else - if type(diagnostic_disabled[bufnr]) ~= "table" then - return + bufnr = get_bufnr(bufnr) + if namespace == nil then + diagnostic_disabled[bufnr] = nil + else + if type(diagnostic_disabled[bufnr]) ~= "table" then + return + end + diagnostic_disabled[bufnr][namespace] = nil end - diagnostic_disabled[bufnr][namespace] = nil - M.show(namespace, bufnr) end + + M.show(namespace, bufnr) end --- Parse a diagnostic from a string. @@ -1474,7 +1557,7 @@ function M.fromqflist(list) for _, item in ipairs(list) do if item.valid == 1 then local lnum = math.max(0, item.lnum - 1) - local col = item.col > 0 and (item.col - 1) or nil + local col = math.max(0, item.col - 1) local end_lnum = item.end_lnum > 0 and (item.end_lnum - 1) or lnum local end_col = item.end_col > 0 and (item.end_col - 1) or col local severity = item.type ~= "" and M.severity[item.type] or M.severity.ERROR @@ -1492,6 +1575,4 @@ function M.fromqflist(list) return diagnostics end --- }}} - return M diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 0fc0a7a7aa..2e530ec17a 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -115,6 +115,13 @@ local format_line_ending = { ["mac"] = '\r', } +---@private +---@param bufnr (number) +---@returns (string) +local function buf_get_line_ending(bufnr) + return format_line_ending[nvim_buf_get_option(bufnr, 'fileformat')] or '\n' +end + local client_index = 0 ---@private --- Returns a new, unused client id. @@ -130,12 +137,6 @@ local all_buffer_active_clients = {} local uninitialized_clients = {} ---@private ---- Invokes a function for each LSP client attached to the buffer {bufnr}. ---- ----@param bufnr (Number) of buffer ----@param fn (function({client}, {client_id}, {bufnr}) Function to run on ----each client attached to that buffer. ----@param restrict_client_ids table list of client ids on which to restrict function application. local function for_each_buffer_client(bufnr, fn, restrict_client_ids) validate { fn = { fn, 'f' }; @@ -236,7 +237,6 @@ local function validate_client_config(config) config = { config, 't' }; } validate { - root_dir = { config.root_dir, optional_validator(is_dir), "directory" }; handlers = { config.handlers, "t", true }; capabilities = { config.capabilities, "t", true }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; @@ -278,9 +278,10 @@ end ---@param bufnr (number) Buffer handle, or 0 for current. ---@returns Buffer text as string. local function buf_get_full_text(bufnr) - local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), '\n') + local line_ending = buf_get_line_ending(bufnr) + local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), line_ending) if nvim_buf_get_option(bufnr, 'eol') then - text = text .. '\n' + text = text .. line_ending end return text end @@ -313,12 +314,14 @@ do --- --- state --- pending_change?: function that the timer starts to trigger didChange - --- pending_changes: list of tables with the pending changesets; for incremental_sync only + --- pending_changes: table (uri -> list of pending changeset tables)); + -- Only set if incremental_sync is used --- use_incremental_sync: bool --- buffers?: table (bufnr → lines); for incremental sync only --- timer?: uv_timer local state_by_client = {} + ---@private function changetracking.init(client, bufnr) local state = state_by_client[client.id] if not state then @@ -340,16 +343,16 @@ do state.buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true) end + ---@private function changetracking.reset_buf(client, bufnr) + changetracking.flush(client) local state = state_by_client[client.id] - if state then - changetracking._reset_timer(state) - if state.buffers then - state.buffers[bufnr] = nil - end + if state and state.buffers then + state.buffers[bufnr] = nil end end + ---@private function changetracking.reset(client_id) local state = state_by_client[client_id] if state then @@ -358,13 +361,14 @@ do end end - function changetracking.prepare(bufnr, firstline, lastline, new_lastline, changedtick) + ---@private + function changetracking.prepare(bufnr, firstline, lastline, new_lastline) local incremental_changes = function(client) local cached_buffers = state_by_client[client.id].buffers local curr_lines = nvim_buf_get_lines(bufnr, 0, -1, true) - local line_ending = format_line_ending[vim.api.nvim_buf_get_option(0, 'fileformat')] + local line_ending = buf_get_line_ending(bufnr) local incremental_change = sync.compute_diff( - cached_buffers[bufnr], curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending or '\n') + cached_buffers[bufnr], curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending) cached_buffers[bufnr] = curr_lines return incremental_change end @@ -385,7 +389,7 @@ do client.notify("textDocument/didChange", { textDocument = { uri = uri; - version = changedtick; + version = util.buf_versions[bufnr]; }; contentChanges = { changes, } }) @@ -395,27 +399,36 @@ do 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)) + if not state.pending_changes[uri] then + state.pending_changes[uri] = {} + end + table.insert(state.pending_changes[uri], incremental_changes(client)) end state.pending_change = function() state.pending_change = nil if client.is_stopped() or not vim.api.nvim_buf_is_valid(bufnr) then return end - local contentChanges if state.use_incremental_sync then - contentChanges = state.pending_changes + for change_uri, content_changes in pairs(state.pending_changes) do + client.notify("textDocument/didChange", { + textDocument = { + uri = change_uri; + version = util.buf_versions[vim.uri_to_bufnr(change_uri)]; + }; + contentChanges = content_changes, + }) + end state.pending_changes = {} else - contentChanges = { full_changes(), } + client.notify("textDocument/didChange", { + textDocument = { + uri = uri; + version = util.buf_versions[bufnr]; + }; + 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 @@ -432,6 +445,7 @@ do end --- Flushes any outstanding change notification. + ---@private function changetracking.flush(client) local state = state_by_client[client.id] if state then @@ -475,7 +489,8 @@ local function text_document_did_open_handler(bufnr, client) -- Protect against a race where the buffer disappears -- between `did_open_handler` and the scheduled function firing. if vim.api.nvim_buf_is_valid(bufnr) then - vim.lsp.diagnostic.redraw(bufnr, client.id) + local namespace = vim.lsp.diagnostic.get_namespace(client.id) + vim.diagnostic.show(namespace, bufnr) end end) end @@ -545,6 +560,12 @@ end --- --- - {handlers} (table): The handlers used by the client as described in |lsp-handler|. --- +--- - {requests} (table): The current pending requests in flight +--- to the server. Entries are key-value pairs with the key +--- being the request ID while the value is a table with `type`, +--- `bufnr`, and `method` key-value pairs. `type` is either "pending" +--- for an active request, or "cancel" for a cancel request. +--- --- - {config} (table): copy of the table that was passed by the user --- to |vim.lsp.start_client()|. --- @@ -566,12 +587,10 @@ end -- --- Starts and initializes a client with the given configuration. --- ---- Parameters `cmd` and `root_dir` are required. +--- Parameter `cmd` is required. --- --- The following parameters describe fields in the {config} table. --- ----@param root_dir: (string) Directory where the LSP server will base ---- its rootUri on initialization. --- ---@param cmd: (required, string or list treated like |jobstart()|) Base command --- that initiates the LSP client. @@ -587,6 +606,11 @@ end --- { "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; } --- </pre> --- +---@param workspace_folders (table) List of workspace folders passed to the +--- language server. For backwards compatibility rootUri and rootPath will be +--- derived from the first workspace folder in this list. See `workspaceFolders` in +--- the LSP spec. +--- ---@param capabilities Map overriding the default capabilities defined by --- |vim.lsp.protocol.make_client_capabilities()|, passed to the language --- server on initialization. Hint: use make_client_capabilities() and modify @@ -603,17 +627,13 @@ end --- ---@param commands table Table that maps string of clientside commands to user-defined functions. --- Commands passed to start_client take precedence over the global command registry. Each key ---- must be a unique comand name, and the value is a function which is called if any LSP action +--- must be a unique command name, and the value is a function which is called if any LSP action --- (code action, code lenses, ...) triggers the command. --- ---@param init_options Values to pass in the initialization request --- as `initializationOptions`. See `initialize` in the LSP spec. --- ---@param name (string, default=client-id) Name in log messages. --- ----@param workspace_folders (table) List of workspace folders passed to the ---- language server. Defaults to root_dir if not set. See `workspaceFolders` in ---- the LSP spec --- ---@param get_language_id function(bufnr, filetype) -> language ID as string. --- Defaults to the filetype. @@ -661,8 +681,13 @@ end --- 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 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. +--- 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. +--- +---@param root_dir string Directory where the LSP +--- server will base its workspaceFolders, rootUri, and rootPath +--- on initialization. +--- ---@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be --- fully initialized. Use `on_init` to do any actions once --- the client has been initialized. @@ -752,6 +777,10 @@ function lsp.start_client(config) ---@param code (number) exit code of the process ---@param signal (number) the signal used to terminate (if any) function dispatch.on_exit(code, signal) + if config.on_exit then + pcall(config.on_exit, code, signal, client_id) + end + active_clients[client_id] = nil uninitialized_clients[client_id] = nil @@ -767,10 +796,6 @@ function lsp.start_client(config) vim.notify(msg, vim.log.levels.WARN) end) end - - if config.on_exit then - pcall(config.on_exit, code, signal, client_id) - end end -- Start the RPC client. @@ -779,6 +804,9 @@ function lsp.start_client(config) env = config.cmd_env; }) + -- Return nil if client fails to start + if not rpc then return end + local client = { id = client_id; name = name; @@ -805,11 +833,24 @@ function lsp.start_client(config) } local version = vim.version() - if config.root_dir and not config.workspace_folders then - config.workspace_folders = {{ - uri = vim.uri_from_fname(config.root_dir); - name = string.format("%s", config.root_dir); - }}; + local workspace_folders + local root_uri + local root_path + if config.workspace_folders or config.root_dir then + if config.root_dir and not config.workspace_folders then + workspace_folders = {{ + uri = vim.uri_from_fname(config.root_dir); + name = string.format("%s", config.root_dir); + }}; + else + workspace_folders = config.workspace_folders + end + root_uri = workspace_folders[1].uri + root_path = vim.uri_to_fname(root_uri) + else + workspace_folders = nil + root_uri = nil + root_path = nil end local initialize_params = { @@ -827,10 +868,15 @@ function lsp.start_client(config) -- The rootPath of the workspace. Is null if no folder is open. -- -- @deprecated in favour of rootUri. - rootPath = config.root_dir; + rootPath = root_path or vim.NIL; -- The rootUri of the workspace. Is null if no folder is open. If both -- `rootPath` and `rootUri` are set `rootUri` wins. - rootUri = config.root_dir and vim.uri_from_fname(config.root_dir); + rootUri = root_uri or vim.NIL; + -- The workspace folders configured in the client when the server starts. + -- This property is only available if the client supports workspace folders. + -- It can be `null` if the client supports workspace folders but none are + -- configured. + workspaceFolders = workspace_folders or vim.NIL; -- User provided initialization options. initializationOptions = config.init_options; -- The capabilities provided by the client (editor or tool) @@ -838,21 +884,6 @@ function lsp.start_client(config) -- The initial trace setting. If omitted trace is disabled ("off"). -- trace = "off" | "messages" | "verbose"; trace = valid_traces[config.trace] or 'off'; - -- The workspace folders configured in the client when the server starts. - -- This property is only available if the client supports workspace folders. - -- It can be `null` if the client supports workspace folders but none are - -- configured. - -- - -- Since 3.6.0 - -- workspaceFolders?: WorkspaceFolder[] | null; - -- export interface WorkspaceFolder { - -- -- The associated URI for this workspace folder. - -- uri - -- -- The name of the workspace folder. Used to refer to this - -- -- workspace folder in the user interface. - -- name - -- } - workspaceFolders = config.workspace_folders, } if config.before_init then -- TODO(ashkan) handle errors here. @@ -865,7 +896,9 @@ function lsp.start_client(config) rpc.notify('initialized', vim.empty_dict()) client.initialized = true uninitialized_clients[client_id] = nil - client.workspaceFolders = initialize_params.workspaceFolders + client.workspace_folders = workspace_folders + -- TODO(mjlbach): Backwards compatbility, to be removed in 0.7 + client.workspaceFolders = client.workspace_folders client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities") -- These are the cleaned up capabilities we use for dynamically deciding -- when to send certain events to clients. @@ -1007,7 +1040,7 @@ function lsp.start_client(config) return rpc.notify("$/cancelRequest", { id = id }) end - -- Track this so that we can escalate automatically if we've alredy tried a + -- Track this so that we can escalate automatically if we've already tried a -- graceful shutdown local graceful_shutdown_failed = false ---@private @@ -1084,7 +1117,7 @@ do return end util.buf_versions[bufnr] = changedtick - local compute_change_and_notify = changetracking.prepare(bufnr, firstline, lastline, new_lastline, changedtick) + local compute_change_and_notify = changetracking.prepare(bufnr, firstline, lastline, new_lastline) for_each_buffer_client(bufnr, compute_change_and_notify) end end @@ -1143,6 +1176,7 @@ function lsp.buf_attach_client(bufnr, client_id) on_reload = function() local params = { textDocument = { uri = uri; } } for_each_buffer_client(bufnr, function(client, _) + changetracking.reset_buf(client, bufnr) if client.resolved_capabilities.text_document_open_close then client.notify('textDocument/didClose', params) end @@ -1152,10 +1186,10 @@ function lsp.buf_attach_client(bufnr, client_id) on_detach = function() local params = { textDocument = { uri = uri; } } for_each_buffer_client(bufnr, function(client, _) + changetracking.reset_buf(client, bufnr) if client.resolved_capabilities.text_document_open_close then client.notify('textDocument/didClose', params) end - changetracking.reset_buf(client, bufnr) end) util.buf_versions[bufnr] = nil all_buffer_active_clients[bufnr] = nil @@ -1190,7 +1224,7 @@ end --- Gets a client by id, or nil if the id is invalid. --- The returned client may not yet be fully initialized. --- +--- ---@param client_id number client id --- ---@returns |vim.lsp.client| object, or nil @@ -1199,7 +1233,7 @@ function lsp.get_client_by_id(client_id) end --- Returns list of buffers attached to client_id. --- +--- ---@param client_id number client id ---@returns list of buffer ids function lsp.get_buffers_by_client_id(client_id) @@ -1269,6 +1303,7 @@ function lsp._vim_exit_handler() local poll_time = 50 + ---@private local function check_clients_closed() for client_id, timeout in pairs(timeouts) do timeouts[client_id] = timeout - poll_time @@ -1303,8 +1338,8 @@ nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()") ---@param method (string) LSP method name ---@param params (optional, table) Parameters to send to the server ---@param handler (optional, function) See |lsp-handler| --- If nil, follows resolution strategy defined in |lsp-handler-configuration| --- +--- If nil, follows resolution strategy defined in |lsp-handler-configuration| +--- ---@returns 2-tuple: --- - Map of client-id:request-id pairs for all successful requests. --- - Function which can be used to cancel all the requests. You could instead @@ -1458,11 +1493,7 @@ local function adjust_start_col(lnum, line, items, encoding) end end if min_start_char then - if encoding == 'utf-8' then - return min_start_char - else - return vim.str_byteindex(line, min_start_char, encoding == 'utf-16') - end + return util._str_byteindex_enc(line, min_start_char, encoding) else return nil end @@ -1587,6 +1618,21 @@ function lsp.formatexpr(opts) return 0 end +--- Provides an interface between the built-in client and 'tagfunc'. +--- +--- When used with normal mode commands (e.g. |CTRL-]|) this will invoke +--- the "textDocument/definition" LSP method to find the tag under the cursor. +--- Otherwise, uses "workspace/symbol". If no results are returned from +--- any LSP servers, falls back to using built-in tags. +--- +---@param pattern Pattern used to find a workspace symbol +---@param flags See |tag-function| +--- +---@returns A list of matching tags +function lsp.tagfunc(...) + return require('vim.lsp.tagfunc')(...) +end + ---Checks whether a client is stopped. --- ---@param client_id (Number) @@ -1640,7 +1686,17 @@ function lsp.get_log_path() return log.get_filename() end ---- Call {fn} for every client attached to {bufnr} +--- Invokes a function for each LSP client attached to a buffer. +--- +---@param bufnr number Buffer number +---@param fn function Function to run on each client attached to buffer +--- {bufnr}. The function takes the client, client ID, and +--- buffer number as arguments. Example: +--- <pre> +--- vim.lsp.for_each_buffer_client(0, function(client, client_id, bufnr) +--- print(vim.inspect(client)) +--- end) +--- </pre> function lsp.for_each_buffer_client(bufnr, fn) return for_each_buffer_client(bufnr, fn) end @@ -1699,11 +1755,11 @@ end --- using `workspace/executeCommand`. --- --- The first argument to the function will be the `Command`: --- Command --- title: String --- command: String --- arguments?: any[] --- +--- Command +--- title: String +--- command: String +--- arguments?: any[] +--- --- The second argument is the `ctx` of |lsp-handler| lsp.commands = setmetatable({}, { __newindex = function(tbl, key, value) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 747d761730..8e3ed9b002 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -116,31 +116,30 @@ end --- asks the user to select one. -- ---@returns The client that the user selected or nil -local function select_client(method) - local clients = vim.tbl_values(vim.lsp.buf_get_clients()); - clients = vim.tbl_filter(function (client) +local function select_client(method, on_choice) + validate { + on_choice = { on_choice, 'function', false }, + } + local clients = vim.tbl_values(vim.lsp.buf_get_clients()) + clients = vim.tbl_filter(function(client) return client.supports_method(method) end, clients) -- better UX when choices are always in the same order (between restarts) - table.sort(clients, function (a, b) return a.name < b.name end) + table.sort(clients, function(a, b) + return a.name < b.name + end) if #clients > 1 then - local choices = {} - for k,v in pairs(clients) do - table.insert(choices, string.format("%d %s", k, v.name)) - end - local user_choice = vim.fn.confirm( - "Select a language server:", - table.concat(choices, "\n"), - 0, - "Question" - ) - if user_choice == 0 then return nil end - return clients[user_choice] + vim.ui.select(clients, { + prompt = 'Select a language server:', + format_item = function(client) + return client.name + end, + }, on_choice) elseif #clients < 1 then - return nil + on_choice(nil) else - return clients[1] + on_choice(clients[1]) end end @@ -152,11 +151,15 @@ end -- ---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting function M.formatting(options) - local client = select_client("textDocument/formatting") - if client == nil then return end - local params = util.make_formatting_params(options) - return client.request("textDocument/formatting", params, nil, vim.api.nvim_get_current_buf()) + local bufnr = vim.api.nvim_get_current_buf() + select_client('textDocument/formatting', function(client) + if client == nil then + return + end + + return client.request('textDocument/formatting', params, nil, bufnr) + end) end --- Performs |vim.lsp.buf.formatting()| synchronously. @@ -165,23 +168,27 @@ end --- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example: --- --- <pre> ---- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()]] +--- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync() --- </pre> --- ---@param options Table with valid `FormattingOptions` entries ---@param timeout_ms (number) Request timeout ---@see |vim.lsp.buf.formatting_seq_sync| function M.formatting_sync(options, timeout_ms) - local client = select_client("textDocument/formatting") - if client == nil then return end - local params = util.make_formatting_params(options) - local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf()) - if result and result.result then - util.apply_text_edits(result.result) - elseif err then - vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN) - end + local bufnr = vim.api.nvim_get_current_buf() + select_client('textDocument/formatting', function(client) + if client == nil then + return + end + + local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr) + if result and result.result then + util.apply_text_edits(result.result, bufnr) + elseif err then + vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN) + end + end) end --- Formats the current buffer by sequentially requesting formatting from attached clients. @@ -202,6 +209,7 @@ end ---the remaining clients in the order as they occur in the `order` list. function M.formatting_seq_sync(options, timeout_ms, order) local clients = vim.tbl_values(vim.lsp.buf_get_clients()); + local bufnr = vim.api.nvim_get_current_buf() -- sort the clients according to `order` for _, client_name in pairs(order or {}) do @@ -220,7 +228,7 @@ function M.formatting_seq_sync(options, timeout_ms, order) local params = util.make_formatting_params(options) local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf()) if result and result.result then - util.apply_text_edits(result.result) + util.apply_text_edits(result.result, bufnr) elseif err then vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN) end @@ -236,12 +244,15 @@ end ---@param end_pos ({number, number}, optional) mark-indexed position. ---Defaults to the end of the last visual selection. function M.range_formatting(options, start_pos, end_pos) - local client = select_client("textDocument/rangeFormatting") - if client == nil then return end - local params = util.make_given_range_params(start_pos, end_pos) params.options = util.make_formatting_params(options).options - return client.request("textDocument/rangeFormatting", params) + select_client('textDocument/rangeFormatting', function(client) + if client == nil then + return + end + + return client.request('textDocument/rangeFormatting', params) + end) end --- Renames all references to the symbol under the cursor. @@ -261,6 +272,7 @@ function M.rename(new_name) request('textDocument/rename', params) end + ---@private local function prepare_rename(err, result) if err == nil and result == nil then vim.notify('nothing to rename', vim.log.levels.INFO) @@ -369,7 +381,7 @@ end function M.list_workspace_folders() local workspace_folders = {} for _, client in pairs(vim.lsp.buf_get_clients()) do - for _, folder in pairs(client.workspaceFolders or {}) do + for _, folder in pairs(client.workspace_folders or {}) do table.insert(workspace_folders, folder.name) end end @@ -389,7 +401,7 @@ function M.add_workspace_folder(workspace_folder) local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}}) for _, client in pairs(vim.lsp.buf_get_clients()) do local found = false - for _, folder in pairs(client.workspaceFolders or {}) do + for _, folder in pairs(client.workspace_folders or {}) do if folder.name == workspace_folder then found = true print(workspace_folder, "is already part of this workspace") @@ -398,10 +410,10 @@ function M.add_workspace_folder(workspace_folder) end if not found then vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params) - if not client.workspaceFolders then - client.workspaceFolders = {} + if not client.workspace_folders then + client.workspace_folders = {} end - table.insert(client.workspaceFolders, params.event.added[1]) + table.insert(client.workspace_folders, params.event.added[1]) end end end @@ -415,10 +427,10 @@ function M.remove_workspace_folder(workspace_folder) if not (workspace_folder and #workspace_folder > 0) then return end local params = util.make_workspace_params({{}}, {{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}) for _, client in pairs(vim.lsp.buf_get_clients()) do - for idx, folder in pairs(client.workspaceFolders) do + for idx, folder in pairs(client.workspace_folders) do if folder.name == workspace_folder then vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params) - client.workspaceFolders[idx] = nil + client.workspace_folders[idx] = nil return end end @@ -444,9 +456,9 @@ end --- by events such as `CursorHold`, eg: --- --- <pre> ---- vim.api.nvim_command [[autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()]] ---- vim.api.nvim_command [[autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()]] ---- vim.api.nvim_command [[autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()]] +--- autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight() +--- autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight() +--- autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references() --- </pre> --- --- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index 1e6f83c1ba..8850d25233 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -153,19 +153,6 @@ function M.get_namespace(client_id) return _client_namespaces[client_id] end ---- Save diagnostics to the current buffer. ---- ---- Handles saving diagnostics from multiple clients in the same buffer. ----@param diagnostics Diagnostic[] ----@param bufnr number ----@param client_id number ----@private -function M.save(diagnostics, bufnr, client_id) - local namespace = M.get_namespace(client_id) - vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) -end --- }}} - --- |lsp-handler| for the method "textDocument/publishDiagnostics" --- --- See |vim.diagnostic.config()| for configuration options. Handler-specific @@ -220,12 +207,9 @@ function M.on_publish_diagnostics(_, result, ctx, config) end vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) - - -- Keep old autocmd for back compat. This should eventually be removed. - vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged") end ---- Clear diagnotics and diagnostic cache. +--- Clear diagnostics and diagnostic cache. --- --- Diagnostic producers should prefer |vim.diagnostic.reset()|. However, --- this method signature is still used internally in some parts of the LSP @@ -248,6 +232,23 @@ end -- Deprecated Functions {{{ + +--- Save diagnostics to the current buffer. +--- +---@deprecated Prefer |vim.diagnostic.set()| +--- +--- Handles saving diagnostics from multiple clients in the same buffer. +---@param diagnostics Diagnostic[] +---@param bufnr number +---@param client_id number +---@private +function M.save(diagnostics, bufnr, client_id) + vim.api.nvim_echo({{'vim.lsp.diagnostic.save is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + local namespace = M.get_namespace(client_id) + vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) +end +-- }}} + --- Get all diagnostics for clients --- ---@deprecated Prefer |vim.diagnostic.get()| @@ -256,6 +257,7 @@ end --- If nil, diagnostics of all clients are included. ---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[]) function M.get_all(client_id) + vim.api.nvim_echo({{'vim.lsp.diagnostic.get_all is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) local result = {} local namespace if client_id then @@ -277,6 +279,7 @@ end --- Else, return just the diagnostics associated with the client_id. ---@param predicate function|nil Optional function for filtering diagnostics function M.get(bufnr, client_id, predicate) + vim.api.nvim_echo({{'vim.lsp.diagnostic.get is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) predicate = predicate or function() return true end if client_id == nil then local all_diagnostics = {} @@ -338,6 +341,7 @@ end ---@param severity DiagnosticSeverity ---@param client_id number the client id function M.get_count(bufnr, severity, client_id) + vim.api.nvim_echo({{'vim.lsp.diagnostic.get_count is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) severity = severity_lsp_to_vim(severity) local opts = { severity = severity } if client_id ~= nil then @@ -354,6 +358,7 @@ end ---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@return table Previous diagnostic function M.get_prev(opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.get_prev is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -371,6 +376,7 @@ end ---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@return table Previous diagnostic position function M.get_prev_pos(opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.get_prev_pos is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -387,6 +393,7 @@ end --- ---@param opts table See |vim.lsp.diagnostic.goto_next()| function M.goto_prev(opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.goto_prev is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -404,6 +411,7 @@ end ---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@return table Next diagnostic function M.get_next(opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.get_next is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -421,6 +429,7 @@ end ---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@return table Next diagnostic position function M.get_next_pos(opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.get_next_pos is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -434,25 +443,8 @@ end --- Move to the next diagnostic --- ---@deprecated Prefer |vim.diagnostic.goto_next()| ---- ----@param opts table|nil Configuration table. Keys: ---- - {client_id}: (number) ---- - If nil, will consider all clients attached to buffer. ---- - {cursor_position}: (Position, default current position) ---- - See |nvim_win_get_cursor()| ---- - {wrap}: (boolean, default true) ---- - Whether to loop around file or not. Similar to 'wrapscan' ---- - {severity}: (DiagnosticSeverity) ---- - Exclusive severity to consider. Overrides {severity_limit} ---- - {severity_limit}: (DiagnosticSeverity) ---- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. ---- - {enable_popup}: (boolean, default true) ---- - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump ---- - {popup_opts}: (table) ---- - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()| ---- - {win_id}: (number, default 0) ---- - Window ID function M.goto_next(opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.goto_next is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -476,6 +468,7 @@ end --- - severity_limit (DiagnosticSeverity): --- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. function M.set_signs(diagnostics, bufnr, client_id, _, opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.set_signs is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) local namespace = M.get_namespace(client_id) if opts and not opts.severity and opts.severity_limit then opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} @@ -496,6 +489,7 @@ end --- - severity_limit (DiagnosticSeverity): --- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. function M.set_underline(diagnostics, bufnr, client_id, _, opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.set_underline is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) local namespace = M.get_namespace(client_id) if opts and not opts.severity and opts.severity_limit then opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} @@ -517,6 +511,7 @@ end --- - severity_limit (DiagnosticSeverity): --- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. function M.set_virtual_text(diagnostics, bufnr, client_id, _, opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.set_virtual_text is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) local namespace = M.get_namespace(client_id) if opts and not opts.severity and opts.severity_limit then opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} @@ -535,6 +530,7 @@ end ---@return an array of [text, hl_group] arrays. This can be passed directly to --- the {virt_text} option of |nvim_buf_set_extmark()|. function M.get_virtual_text_chunks_for_line(bufnr, _, line_diags, opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.get_virtual_text_chunks_for_line is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) return vim.diagnostic._get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts) end @@ -552,6 +548,7 @@ end ---@param position table|nil The (0,0)-indexed position ---@return table {popup_bufnr, win_id} function M.show_position_diagnostics(opts, buf_nr, position) + vim.api.nvim_echo({{'vim.lsp.diagnostic.show_position_diagnostics is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) opts = opts or {} opts.scope = "cursor" opts.pos = position @@ -565,7 +562,7 @@ end --- Open a floating window with the diagnostics from {line_nr} --- ----@deprecated Prefer |vim.diagnostic.show_line_diagnostics()| +---@deprecated Prefer |vim.diagnostic.open_float()| --- ---@param opts table Configuration table --- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and @@ -575,6 +572,7 @@ end ---@param client_id number|nil the client id ---@return table {popup_bufnr, win_id} function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id) + vim.api.nvim_echo({{'vim.lsp.diagnostic.show_line_diagnostics is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) opts = opts or {} opts.scope = "line" opts.pos = line_nr @@ -586,7 +584,7 @@ end --- Redraw diagnostics for the given buffer and client --- ----@deprecated Prefer |vim.diagnostic.redraw()| +---@deprecated Prefer |vim.diagnostic.show()| --- --- This calls the "textDocument/publishDiagnostics" handler manually using --- the cached diagnostics already received from the server. This can be useful @@ -598,6 +596,7 @@ end --- client. The default is to redraw diagnostics for all attached --- clients. function M.redraw(bufnr, client_id) + vim.api.nvim_echo({{'vim.lsp.diagnostic.redraw is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) bufnr = get_bufnr(bufnr) if not client_id then return vim.lsp.for_each_buffer_client(bufnr, function(client) @@ -625,6 +624,7 @@ end --- - {workspace}: (boolean, default true) --- - Set the list with workspace diagnostics function M.set_qflist(opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.set_qflist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) opts = opts or {} if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -656,6 +656,7 @@ end --- - {workspace}: (boolean, default false) --- - Set the list with workspace diagnostics function M.set_loclist(opts) + vim.api.nvim_echo({{'vim.lsp.diagnostic.set_loclist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) opts = opts or {} if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -683,6 +684,7 @@ end -- send diagnostic information and the client will still process it. The -- diagnostics are simply not displayed to the user. function M.disable(bufnr, client_id) + vim.api.nvim_echo({{'vim.lsp.diagnostic.disable is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) if not client_id then return vim.lsp.for_each_buffer_client(bufnr, function(client) M.disable(bufnr, client.id) @@ -703,6 +705,7 @@ end --- client. The default is to enable diagnostics for all attached --- clients. function M.enable(bufnr, client_id) + vim.api.nvim_echo({{'vim.lsp.diagnostic.enable is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) if not client_id then return vim.lsp.for_each_buffer_client(bufnr, function(client) M.enable(bufnr, client.id) @@ -717,5 +720,3 @@ end -- }}} return M - --- vim: fdm=marker diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index a561630c2b..c974d1a6b4 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -28,7 +28,7 @@ local function progress_handler(_, result, ctx, _) local client_name = client and client.name or string.format("id=%d", client_id) if not client then err_message("LSP[", client_name, "] client has shut down after sending the message") - return + return vim.NIL end local val = result.value -- unspecified yet local token = result.token -- string or number @@ -70,6 +70,7 @@ M['window/workDoneProgress/create'] = function(_, result, ctx) local client_name = client and client.name or string.format("id=%d", client_id) if not client then err_message("LSP[", client_name, "] client has shut down after sending the message") + return vim.NIL end client.messages.progress[token] = {} return vim.NIL @@ -284,7 +285,7 @@ local function location_handler(_, result, ctx, _) util.jump_to_location(result[1]) if #result > 1 then - util.set_qflist(util.locations_to_items(result)) + vim.fn.setqflist({}, ' ', {title = 'LSP locations', items = util.locations_to_items(result)}) api.nvim_command("copen") end else @@ -350,7 +351,10 @@ M['textDocument/signatureHelp'] = M.signature_help --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight M['textDocument/documentHighlight'] = function(_, result, ctx, _) if not result then return end - util.buf_highlight_references(ctx.bufnr, result, ctx.client_id) + local client_id = ctx.client_id + local client = vim.lsp.get_client_by_id(client_id) + if not client then return end + util.buf_highlight_references(ctx.bufnr, result, client.offset_encoding) end ---@private @@ -375,7 +379,7 @@ local make_call_hierarchy_handler = function(direction) }) end end - util.set_qflist(items) + vim.fn.setqflist({}, ' ', {title = 'LSP call hierarchy', items = items}) api.nvim_command("copen") end end diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 4597f1919a..dbc473b52c 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -101,6 +101,7 @@ function log.set_level(level) end --- Gets the current log level. +---@return string current log level function log.get_level() return current_log_level end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index b3aa8b934f..86c9e2fd58 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -776,149 +776,9 @@ function protocol.make_client_capabilities() } end ---[=[ -export interface DocumentFilter { - --A language id, like `typescript`. - language?: string; - --A Uri [scheme](#Uri.scheme), like `file` or `untitled`. - scheme?: string; - --A glob pattern, like `*.{ts,js}`. - -- - --Glob patterns can have the following syntax: - --- `*` to match one or more characters in a path segment - --- `?` to match on one character in a path segment - --- `**` to match any number of path segments, including none - --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) - --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) - pattern?: string; -} ---]=] - ---[[ ---Static registration options to be returned in the initialize request. -interface StaticRegistrationOptions { - --The id used to register the request. The id can be used to deregister - --the request again. See also Registration#id. - id?: string; -} - -export interface DocumentFilter { - --A language id, like `typescript`. - language?: string; - --A Uri [scheme](#Uri.scheme), like `file` or `untitled`. - scheme?: string; - --A glob pattern, like `*.{ts,js}`. - -- - --Glob patterns can have the following syntax: - --- `*` to match one or more characters in a path segment - --- `?` to match on one character in a path segment - --- `**` to match any number of path segments, including none - --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) - --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) - --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) - pattern?: string; -} -export type DocumentSelector = DocumentFilter[]; -export interface TextDocumentRegistrationOptions { - --A document selector to identify the scope of the registration. If set to null - --the document selector provided on the client side will be used. - documentSelector: DocumentSelector | null; -} - ---Code Action options. -export interface CodeActionOptions { - --CodeActionKinds that this server may return. - -- - --The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server - --may list out every specific kind they provide. - codeActionKinds?: CodeActionKind[]; -} - -interface ServerCapabilities { - --Defines how text documents are synced. Is either a detailed structure defining each notification or - --for backwards compatibility the TextDocumentSyncKind number. If omitted it defaults to `TextDocumentSyncKind.None`. - textDocumentSync?: TextDocumentSyncOptions | number; - --The server provides hover support. - hoverProvider?: boolean; - --The server provides completion support. - completionProvider?: CompletionOptions; - --The server provides signature help support. - signatureHelpProvider?: SignatureHelpOptions; - --The server provides goto definition support. - definitionProvider?: boolean; - --The server provides Goto Type Definition support. - -- - --Since 3.6.0 - typeDefinitionProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions); - --The server provides Goto Implementation support. - -- - --Since 3.6.0 - implementationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions); - --The server provides find references support. - referencesProvider?: boolean; - --The server provides document highlight support. - documentHighlightProvider?: boolean; - --The server provides document symbol support. - documentSymbolProvider?: boolean; - --The server provides workspace symbol support. - workspaceSymbolProvider?: boolean; - --The server provides code actions. The `CodeActionOptions` return type is only - --valid if the client signals code action literal support via the property - --`textDocument.codeAction.codeActionLiteralSupport`. - codeActionProvider?: boolean | CodeActionOptions; - --The server provides code lens. - codeLensProvider?: CodeLensOptions; - --The server provides document formatting. - documentFormattingProvider?: boolean; - --The server provides document range formatting. - documentRangeFormattingProvider?: boolean; - --The server provides document formatting on typing. - documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions; - --The server provides rename support. RenameOptions may only be - --specified if the client states that it supports - --`prepareSupport` in its initial `initialize` request. - renameProvider?: boolean | RenameOptions; - --The server provides document link support. - documentLinkProvider?: DocumentLinkOptions; - --The server provides color provider support. - -- - --Since 3.6.0 - colorProvider?: boolean | ColorProviderOptions | (ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions); - --The server provides folding provider support. - -- - --Since 3.10.0 - foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions); - --The server provides go to declaration support. - -- - --Since 3.14.0 - declarationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions); - --The server provides execute command support. - executeCommandProvider?: ExecuteCommandOptions; - --Workspace specific server capabilities - workspace?: { - --The server supports workspace folder. - -- - --Since 3.6.0 - workspaceFolders?: { - * The server has support for workspace folders - supported?: boolean; - * Whether the server wants to receive workspace folder - * change notifications. - * - * If a strings is provided the string is treated as a ID - * under which the notification is registered on the client - * side. The ID can be used to unregister for these events - * using the `client/unregisterCapability` request. - changeNotifications?: string | boolean; - } - } - --Experimental server capabilities. - experimental?: any; -} ---]] - --- Creates a normalized object describing LSP server capabilities. +---@param server_capabilities table Table of capabilities supported by the server +---@return table Normalized table of capabilities function protocol.resolve_capabilities(server_capabilities) local general_properties = {} local text_document_sync_properties diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index bce1e9f35d..1fb75ddeb7 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -230,7 +230,7 @@ function default_dispatchers.on_error(code, err) end --- Starts an LSP server process and create an LSP RPC client object to ---- interact with it. +--- interact with it. Communication with the server is currently limited to stdio. --- ---@param cmd (string) Command to start the LSP server. ---@param cmd_args (table) List of additional string arguments to pass to {cmd}. @@ -264,8 +264,6 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) if extra_spawn_params and extra_spawn_params.cwd then assert(is_dir(extra_spawn_params.cwd), "cwd must be a directory") - elseif not (vim.fn.executable(cmd) == 1) then - error(string.format("The given command %q is not executable.", cmd)) end if dispatchers then local user_dispatchers = dispatchers @@ -325,7 +323,14 @@ local function start(cmd, cmd_args, dispatchers, extra_spawn_params) end handle, pid = uv.spawn(cmd, spawn_params, onexit) if handle == nil then - error(string.format("start `%s` failed: %s", cmd, pid)) + local msg = string.format("Spawning language server with cmd: `%s` failed", cmd) + if string.match(pid, "ENOENT") then + msg = msg .. ". The language server is either not installed, missing from PATH, or not executable." + else + msg = msg .. string.format(" with error message: %s", pid) + end + vim.notify(msg, vim.log.levels.WARN) + return end end diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua index 37247c61b9..d01f45ad8f 100644 --- a/runtime/lua/vim/lsp/sync.lua +++ b/runtime/lua/vim/lsp/sync.lua @@ -75,42 +75,46 @@ local function byte_to_utf(line, byte, offset_encoding) end ---@private +local function compute_line_length(line, offset_encoding) + local length + local _ + if offset_encoding == 'utf-16' then + _, length = str_utfindex(line) + elseif offset_encoding == 'utf-32' then + length, _ = str_utfindex(line) + else + length = #line + end + return length +end + +---@private -- Given a line, byte idx, alignment, and offset_encoding convert to the aligned -- utf-8 index and either the utf-16, or utf-32 index. ---@param line string the line to index into ---@param byte integer the byte idx ----@param align string when dealing with multibyte characters, --- to choose the start of the current character or the beginning of the next. --- Used for incremental sync for start/end range respectively ---@param offset_encoding string utf-8|utf-16|utf-32|nil (default: utf-8) ---@returns table<string, int> byte_idx and char_idx of first change position -local function align_position(line, byte, align, offset_encoding) +local function align_end_position(line, byte, offset_encoding) local char -- If on the first byte, or an empty string: the trivial case if byte == 1 or #line == 0 then char = byte -- Called in the case of extending an empty line "" -> "a" elseif byte == #line + 1 then - byte = byte + str_utf_end(line, #line) - char = byte_to_utf(line, byte, offset_encoding) + char = compute_line_length(line, offset_encoding) + 1 else -- Modifying line, find the nearest utf codepoint - if align == 'start' then - byte = byte + str_utf_start(line, byte) + local offset = str_utf_start(line, byte) + -- If the byte does not fall on the start of the character, then + -- align to the start of the next character. + if offset < 0 then + byte = byte + str_utf_end(line, byte) + 1 + end + if byte <= #line then char = byte_to_utf(line, byte, offset_encoding) - elseif align == 'end' then - local offset = str_utf_end(line, byte) - -- If the byte does not fall on the start of the character, then - -- align to the start of the next character. - if offset > 0 then - char = byte_to_utf(line, byte, offset_encoding) + 1 - byte = byte + offset - else - char = byte_to_utf(line, byte, offset_encoding) - byte = byte + offset - end else - error('`align` must be start or end.') + char = compute_line_length(line, offset_encoding) + 1 end -- Extending line, find the nearest utf codepoint for the last valid character end @@ -131,7 +135,7 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline, -- occur on a new line pointed to by lastline. This occurs during insertion of -- new lines(O), the new newline is inserted at the line indicated by -- new_lastline. - -- If firstline == new_lastline, the first change occured on a line that was deleted. + -- If firstline == new_lastline, the first change occurred on a line that was deleted. -- In this case, the first byte change is also at the first byte of firstline if firstline == new_lastline or firstline == lastline then return { line_idx = firstline, byte_idx = 1, char_idx = 1 } @@ -154,7 +158,18 @@ local function compute_start_range(prev_lines, curr_lines, firstline, lastline, end -- Convert byte to codepoint if applicable - local byte_idx, char_idx = align_position(prev_line, start_byte_idx, 'start', offset_encoding) + local char_idx + local byte_idx + if start_byte_idx == 1 or (#prev_line == 0 and start_byte_idx == 1)then + byte_idx = start_byte_idx + char_idx = 1 + elseif start_byte_idx == #prev_line + 1 then + byte_idx = start_byte_idx + char_idx = compute_line_length(prev_line, offset_encoding) + 1 + else + byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx) + char_idx = byte_to_utf(prev_line, byte_idx, offset_encoding) + end -- Return the start difference (shared for new and prev lines) return { line_idx = firstline, byte_idx = byte_idx, char_idx = char_idx } @@ -174,7 +189,7 @@ end ---@param offset_encoding string ---@returns (int, int) end_line_idx and end_col_idx of range local function compute_end_range(prev_lines, curr_lines, start_range, firstline, lastline, new_lastline, offset_encoding) - -- If firstline == new_lastline, the first change occured on a line that was deleted. + -- If firstline == new_lastline, the first change occurred on a line that was deleted. -- In this case, the last_byte... if firstline == new_lastline then return { line_idx = (lastline - new_lastline + firstline), byte_idx = 1, char_idx = 1 }, { line_idx = firstline, byte_idx = 1, char_idx = 1 } @@ -219,7 +234,12 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline, -- Iterate from end to beginning of shortest line local prev_end_byte_idx = prev_line_length - byte_offset + 1 - local prev_byte_idx, prev_char_idx = align_position(prev_line, prev_end_byte_idx, 'start', offset_encoding) + + -- Handle case where lines match + if prev_end_byte_idx == 0 then + prev_end_byte_idx = 1 + end + local prev_byte_idx, prev_char_idx = align_end_position(prev_line, prev_end_byte_idx, offset_encoding) local prev_end_range = { line_idx = prev_line_idx, byte_idx = prev_byte_idx, char_idx = prev_char_idx } local curr_end_range @@ -228,7 +248,11 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline, curr_end_range = { line_idx = start_line_idx, byte_idx = 1, char_idx = 1 } else local curr_end_byte_idx = curr_line_length - byte_offset + 1 - local curr_byte_idx, curr_char_idx = align_position(curr_line, curr_end_byte_idx, 'start', offset_encoding) + -- Handle case where lines match + if curr_end_byte_idx == 0 then + curr_end_byte_idx = 1 + end + local curr_byte_idx, curr_char_idx = align_end_position(curr_line, curr_end_byte_idx, offset_encoding) curr_end_range = { line_idx = curr_line_idx, byte_idx = curr_byte_idx, char_idx = curr_char_idx } end @@ -272,18 +296,6 @@ local function extract_text(lines, start_range, end_range, line_ending) end end -local function compute_line_length(line, offset_encoding) - local length - local _ - if offset_encoding == 'utf-16' then - _, length = str_utfindex(line) - elseif offset_encoding == 'utf-32' then - length, _ = str_utfindex(line) - else - length = #line - end - return length -end ---@private -- rangelength depends on the offset encoding -- bytes for utf-8 (clangd with extenion) diff --git a/runtime/lua/vim/lsp/tagfunc.lua b/runtime/lua/vim/lsp/tagfunc.lua new file mode 100644 index 0000000000..5c55e8559f --- /dev/null +++ b/runtime/lua/vim/lsp/tagfunc.lua @@ -0,0 +1,76 @@ +local lsp = vim.lsp +local util = vim.lsp.util + +---@private +local function mk_tag_item(name, range, uri, offset_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 + return { + name = name, + filename = vim.uri_to_fname(uri), + cmd = string.format('call cursor(%d, %d)|', range.start.line + 1, byte), + } +end + +---@private +local function query_definition(pattern) + local params = lsp.util.make_position_params() + local results_by_client, err = lsp.buf_request_sync(0, 'textDocument/definition', params, 1000) + if err then + return {} + end + local results = {} + local add = function(range, uri, offset_encoding) + table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding)) + end + for client_id, lsp_results in pairs(results_by_client) do + local client = lsp.get_client_by_id(client_id) + local result = lsp_results.result or {} + if result.range then -- Location + add(result.range, result.uri) + else -- Location[] or LocationLink[] + for _, item in pairs(result) do + if item.range then -- Location + add(item.range, item.uri, client.offset_encoding) + else -- LocationLink + add(item.targetSelectionRange, item.targetUri, client.offset_encoding) + end + end + end + end + return results +end + +---@private +local function query_workspace_symbols(pattern) + local results_by_client, err = lsp.buf_request_sync(0, 'workspace/symbol', { query = pattern }, 1000) + if err then + return {} + end + local results = {} + for client_id, symbols in pairs(results_by_client) do + local client = lsp.get_client_by_id(client_id) + for _, symbol in pairs(symbols.result or {}) do + local loc = symbol.location + local item = mk_tag_item(symbol.name, loc.range, loc.uri, client.offset_encoding) + item.kind = lsp.protocol.SymbolKind[symbol.kind] or 'Unknown' + table.insert(results, item) + end + end + return results +end + +---@private +local function tagfunc(pattern, flags) + local matches + if string.match(flags, 'c') then + matches = query_definition(pattern) + else + matches = query_workspace_symbols(pattern) + end + -- fall back to tags if no matches + return #matches > 0 and matches or vim.NIL +end + +return tagfunc diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index a4b7b9922b..68a030d50b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -90,6 +90,42 @@ local function split_lines(value) return split(value, '\n', true) 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 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) + if encoding ~= 'utf-8' then + local col32, col16 = vim.str_utfindex(line, index) + if encoding == 'utf-32' then + return col32 + else + return col16 + end + else + return index + end +end + +--- Convert UTF index to `encoding` index. +--- Convenience wrapper around vim.str_byteindex +---Alternative to vim.str_byteindex that takes an encoding. +---@param line string line to be indexed +---@param index number UTF index +---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 +---@return number byte (utf-8) index of `encoding` index `index` in `line` +function M._str_byteindex_enc(line, index, encoding) + if encoding ~= 'utf-8' then + return vim.str_byteindex(line, index, not encoding or encoding ~= 'utf-32') + else + return index + end +end + +local _str_utfindex_enc = M._str_utfindex_enc +local _str_byteindex_enc = M._str_byteindex_enc --- Replaces text in a range with new text. --- --- CAUTION: Changes in-place! @@ -148,8 +184,96 @@ local function sort_by_key(fn) end ---@private +--- Gets the zero-indexed lines from the given buffer. +--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events. +--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI. +--- +---@param bufnr number bufnr to get the lines from +---@param rows number[] zero-indexed line numbers +---@return table<number string> a table mapping rows to lines +local function get_lines(bufnr, rows) + rows = type(rows) == "table" and rows or { rows } + + ---@private + local function buf_lines() + local lines = {} + for _, row in pairs(rows) do + lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1] + end + return lines + end + + local uri = vim.uri_from_bufnr(bufnr) + + -- load the buffer if this is not a file uri + -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds. + if uri:sub(1, 4) ~= "file" then + vim.fn.bufload(bufnr) + return buf_lines() + end + + -- use loaded buffers if available + if vim.fn.bufloaded(bufnr) == 1 then + return buf_lines() + end + + local filename = api.nvim_buf_get_name(bufnr) + + -- get the data from the file + local fd = uv.fs_open(filename, "r", 438) + if not fd then return "" end + local stat = uv.fs_fstat(fd) + local data = uv.fs_read(fd, stat.size, 0) + uv.fs_close(fd) + + local lines = {} -- rows we need to retrieve + local need = 0 -- keep track of how many unique rows we need + for _, row in pairs(rows) do + if not lines[row] then + need = need + 1 + end + lines[row] = true + end + + local found = 0 + local lnum = 0 + + for line in string.gmatch(data, "([^\n]*)\n?") do + if lines[lnum] == true then + lines[lnum] = line + found = found + 1 + if found == need then break end + end + lnum = lnum + 1 + end + + -- change any lines we didn't find to the empty string + for i, line in pairs(lines) do + if line == true then + lines[i] = "" + end + end + return lines +end + + +---@private +--- Gets the zero-indexed line from the given buffer. +--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events. +--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI. +--- +---@param bufnr number +---@param row number zero-indexed line number +---@return string the line at row in filename +local function get_line(bufnr, row) + return get_lines(bufnr, { row })[row] +end + + +---@private --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position --- Returns a zero-indexed column, since set_lines() does the conversion to +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 --- 1-indexed local function get_line_byte_from_position(bufnr, position, offset_encoding) -- LSP's line and characters are 0-indexed @@ -158,31 +282,19 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding) -- When on the first character, we can ignore the difference between byte and -- character if col > 0 then - if not api.nvim_buf_is_loaded(bufnr) then - vim.fn.bufload(bufnr) - end - - local line = position.line - local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false) - if #lines > 0 then - local ok, result - - if offset_encoding == "utf-16" or not offset_encoding then - ok, result = pcall(vim.str_byteindex, lines[1], col, true) - elseif offset_encoding == "utf-32" then - ok, result = pcall(vim.str_byteindex, lines[1], col, false) - end - - if ok then - return result - end - return math.min(#lines[1], col) + local line = get_line(bufnr, position.line) + local ok, result + ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding) + if ok then + return result end + return math.min(#line, col) end return col end --- Process and return progress reports from lsp server +---@private function M.get_progress_messages() local new_messages = {} @@ -244,8 +356,15 @@ end --- Applies a list of text edits to a buffer. ---@param text_edits table list of `TextEdit` objects ---@param bufnr number Buffer id +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to encoding of first client of `bufnr` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit -function M.apply_text_edits(text_edits, bufnr) +function M.apply_text_edits(text_edits, bufnr, offset_encoding) + validate { + text_edits = { text_edits, 't', false }; + bufnr = { bufnr, 'number', false }; + offset_encoding = { offset_encoding, 'string', true }; + } + offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) if not next(text_edits) then return end if not api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) @@ -282,8 +401,7 @@ function M.apply_text_edits(text_edits, bufnr) -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here. local has_eol_text_edit = false local max = vim.api.nvim_buf_line_count(bufnr) - -- TODO handle offset_encoding - local _, len = vim.str_utfindex(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') + local len = _str_utfindex_enc(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '', nil, offset_encoding) text_edits = vim.tbl_map(function(text_edit) if max <= text_edit.range.start.line then text_edit.range.start.line = max - 1 @@ -474,7 +592,7 @@ local function remove_unmatch_completion_items(items, prefix) end, items) end ---- Acording to LSP spec, if the client set `completionItemKind.valueSet`, +--- According to LSP spec, if the client set `completionItemKind.valueSet`, --- the client must handle it properly even if it receives a value outside the --- specification. --- @@ -574,7 +692,7 @@ function M.rename(old_fname, new_fname, opts) api.nvim_buf_delete(oldbuf, { force = true }) end - +---@private local function create_file(change) local opts = change.options or {} -- from spec: Overwrite wins over `ignoreIfExists` @@ -586,7 +704,7 @@ local function create_file(change) vim.fn.bufadd(fname) end - +---@private local function delete_file(change) local opts = change.options or {} local fname = vim.uri_to_fname(change.uri) @@ -880,6 +998,8 @@ function M.jump_to_location(location) local row = range.start.line local col = get_line_byte_from_position(0, range.start) api.nvim_win_set_cursor(0, {row + 1, col}) + -- Open folds under the cursor + vim.cmd("normal! zv") return true end @@ -1054,7 +1174,7 @@ function M.stylize_markdown(bufnr, contents, opts) markdown_lines[#stripped] = true end else - -- strip any emty lines or separators prior to this separator in actual markdown + -- strip any empty lines or separators prior to this separator in actual markdown if line:match("^---+$") then while markdown_lines[#stripped] and (stripped[#stripped]:match("^%s*$") or stripped[#stripped]:match("^---+$")) do markdown_lines[#stripped] = false @@ -1087,7 +1207,7 @@ function M.stylize_markdown(bufnr, contents, opts) local idx = 1 ---@private - -- keep track of syntaxes we already inlcuded. + -- keep track of syntaxes we already included. -- no need to include the same syntax more than once local langs = {} local fences = get_markdown_fences() @@ -1133,17 +1253,57 @@ function M.stylize_markdown(bufnr, contents, opts) return stripped end +---@private --- Creates autocommands to close a preview window when events happen. --- ----@param events (table) list of events ----@param winnr (number) window id of preview window +---@param events table list of events +---@param winnr number window id of preview window +---@param bufnrs table list of buffers where the preview window will remain visible ---@see |autocmd-events| -function M.close_preview_autocmd(events, winnr) +local function close_preview_autocmd(events, winnr, bufnrs) + local augroup = 'preview_window_'..winnr + + -- close the preview window when entered a buffer that is not + -- the floating window buffer or the buffer that spawned it + vim.cmd(string.format([[ + augroup %s + autocmd! + autocmd BufEnter * lua vim.lsp.util._close_preview_window(%d, {%s}) + augroup end + ]], augroup, winnr, table.concat(bufnrs, ','))) + if #events > 0 then - api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)") + vim.cmd(string.format([[ + augroup %s + autocmd %s <buffer> lua vim.lsp.util._close_preview_window(%d) + augroup end + ]], augroup, table.concat(events, ','), winnr)) end end +---@private +--- Closes the preview window +--- +---@param winnr number window id of preview window +---@param bufnrs table|nil optional list of ignored buffers +function M._close_preview_window(winnr, bufnrs) + vim.schedule(function() + -- exit if we are in one of ignored buffers + if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then + return + end + + local augroup = 'preview_window_'..winnr + vim.cmd(string.format([[ + augroup %s + autocmd! + augroup end + augroup! %s + ]], augroup, augroup)) + pcall(vim.api.nvim_win_close, winnr, true) + end) +end + ---@internal --- Computes size of float needed to show contents (with optional wrapping) --- @@ -1223,18 +1383,21 @@ end --- ---@param contents table of lines to show in window ---@param syntax string of syntax to set for opened buffer ----@param opts dictionary with optional fields ---- - height of floating window ---- - width of floating window ---- - wrap boolean enable wrapping of long lines (defaults to true) ---- - wrap_at character to wrap at for computing height when wrap is enabled ---- - max_width maximal width of floating window ---- - max_height maximal height of floating window ---- - pad_top number of lines to pad contents at top ---- - pad_bottom number of lines to pad contents at bottom ---- - focus_id if a popup with this id is opened, then focus it ---- - close_events list of events that closes the floating window ---- - focusable (boolean, default true): Make float focusable +---@param opts table with optional fields (additional keys are passed on to |vim.api.nvim_open_win()|) +--- - height: (number) height of floating window +--- - width: (number) width of floating window +--- - wrap: (boolean, default true) wrap long lines +--- - wrap_at: (string) character to wrap at for computing height when wrap is enabled +--- - max_width: (number) maximal width of floating window +--- - max_height: (number) maximal height of floating window +--- - pad_top: (number) number of lines to pad contents at top +--- - pad_bottom: (number) number of lines to pad contents at bottom +--- - focus_id: (string) if a popup with this id is opened, then focus it +--- - close_events: (table) list of events that closes the floating window +--- - focusable: (boolean, default true) Make float focusable +--- - focus: (boolean, default true) If `true`, and if {focusable} +--- is also `true`, focus an existing floating window with the same +--- {focus_id} ---@returns bufnr,winnr buffer and window number of the newly created floating ---preview window function M.open_floating_preview(contents, syntax, opts) @@ -1246,12 +1409,13 @@ function M.open_floating_preview(contents, syntax, opts) opts = opts or {} opts.wrap = opts.wrap ~= false -- wrapping by default opts.stylize_markdown = opts.stylize_markdown ~= false - opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre"} + opts.focus = opts.focus ~= false + opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "InsertCharPre"} local bufnr = api.nvim_get_current_buf() -- check if this popup is focusable and we need to focus - if opts.focus_id and opts.focusable ~= false then + if opts.focus_id and opts.focusable ~= false and opts.focus then -- Go back to previous window if we are in a focusable one local current_winnr = api.nvim_get_current_win() if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then @@ -1314,8 +1478,8 @@ function M.open_floating_preview(contents, syntax, opts) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe') - api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true}) - M.close_preview_autocmd(opts.close_events, floating_winnr) + api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true, nowait = true}) + close_preview_autocmd(opts.close_events, floating_winnr, {floating_bufnr, bufnr}) -- save focus_id if opts.focus_id then @@ -1331,7 +1495,7 @@ do --[[ References ]] --- Removes document highlights from a buffer. --- - ---@param bufnr buffer id + ---@param bufnr number Buffer id function M.buf_clear_references(bufnr) validate { bufnr = {bufnr, 'n', true} } api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1) @@ -1339,21 +1503,19 @@ do --[[ References ]] --- Shows a list of document highlights for a certain buffer. --- - ---@param bufnr buffer id - ---@param references List of `DocumentHighlight` objects to highlight + ---@param bufnr number Buffer id + ---@param references table List of `DocumentHighlight` objects to highlight + ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32", or nil. Defaults to `offset_encoding` of first client of `bufnr` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight - function M.buf_highlight_references(bufnr, references, client_id) + function M.buf_highlight_references(bufnr, references, offset_encoding) validate { bufnr = {bufnr, 'n', true} } - local client = vim.lsp.get_client_by_id(client_id) - if not client then - return - end + offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) for _, reference in ipairs(references) do local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"] local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"] - local start_idx = get_line_byte_from_position(bufnr, { line = start_line, character = start_char }, client.offset_encoding) - local end_idx = get_line_byte_from_position(bufnr, { line = start_line, character = end_char }, client.offset_encoding) + local start_idx = get_line_byte_from_position(bufnr, { line = start_line, character = start_char }, offset_encoding) + local end_idx = get_line_byte_from_position(bufnr, { line = start_line, character = end_char }, offset_encoding) local document_highlight_kind = { [protocol.DocumentHighlightKind.Text] = "LspReferenceText"; @@ -1374,88 +1536,6 @@ local position_sort = sort_by_key(function(v) return {v.start.line, v.start.character} end) ---- Gets the zero-indexed line from the given uri. ----@param uri string uri of the resource to get the line from ----@param row number zero-indexed line number ----@return string the line at row in filename --- For non-file uris, we load the buffer and get the line. --- If a loaded buffer exists, then that is used. --- Otherwise we get the line using libuv which is a lot faster than loading the buffer. -function M.get_line(uri, row) - return M.get_lines(uri, { row })[row] -end - ---- Gets the zero-indexed lines from the given uri. ----@param uri string uri of the resource to get the lines from ----@param rows number[] zero-indexed line numbers ----@return table<number string> a table mapping rows to lines --- For non-file uris, we load the buffer and get the lines. --- If a loaded buffer exists, then that is used. --- Otherwise we get the lines using libuv which is a lot faster than loading the buffer. -function M.get_lines(uri, rows) - rows = type(rows) == "table" and rows or { rows } - - local function buf_lines(bufnr) - local lines = {} - for _, row in pairs(rows) do - lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1] - end - return lines - end - - -- load the buffer if this is not a file uri - -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds. - if uri:sub(1, 4) ~= "file" then - local bufnr = vim.uri_to_bufnr(uri) - vim.fn.bufload(bufnr) - return buf_lines(bufnr) - end - - local filename = vim.uri_to_fname(uri) - - -- use loaded buffers if available - if vim.fn.bufloaded(filename) == 1 then - local bufnr = vim.fn.bufnr(filename, false) - return buf_lines(bufnr) - end - - -- get the data from the file - local fd = uv.fs_open(filename, "r", 438) - if not fd then return "" end - local stat = uv.fs_fstat(fd) - local data = uv.fs_read(fd, stat.size, 0) - uv.fs_close(fd) - - local lines = {} -- rows we need to retrieve - local need = 0 -- keep track of how many unique rows we need - for _, row in pairs(rows) do - if not lines[row] then - need = need + 1 - end - lines[row] = true - end - - local found = 0 - local lnum = 0 - - for line in string.gmatch(data, "([^\n]*)\n?") do - if lines[lnum] == true then - lines[lnum] = line - found = found + 1 - if found == need then break end - end - lnum = lnum + 1 - end - - -- change any lines we didn't find to the empty string - for i, line in pairs(lines) do - if line == true then - lines[i] = "" - end - end - return lines -end - --- Returns the items with the byte position calculated correctly and in sorted --- order, for display in quickfix and location lists. --- @@ -1498,7 +1578,7 @@ function M.locations_to_items(locations) end -- get all the lines for this uri - local lines = M.get_lines(uri, uri_rows) + local lines = get_lines(vim.uri_to_bufnr(uri), uri_rows) for _, temp in ipairs(rows) do local pos = temp.start @@ -1524,6 +1604,7 @@ end --- ---@param items (table) list of items function M.set_loclist(items, win_id) + vim.api.nvim_echo({{'vim.lsp.util.set_loclist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) vim.fn.setloclist(win_id or 0, {}, ' ', { title = 'Language Server'; items = items; @@ -1537,13 +1618,14 @@ end --- ---@param items (table) list of items function M.set_qflist(items) + vim.api.nvim_echo({{'vim.lsp.util.set_qflist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) vim.fn.setqflist({}, ' ', { title = 'Language Server'; items = items; }) end --- Acording to LSP spec, if the client set "symbolKind.valueSet", +-- According to LSP spec, if the client set "symbolKind.valueSet", -- the client must handle it properly even if it receives a value outside the specification. -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol function M._get_symbol_kind_name(symbol_kind) @@ -1640,43 +1722,78 @@ function M.try_trim_markdown_code_blocks(lines) return 'markdown' end -local str_utfindex = vim.str_utfindex ---@private -local function make_position_param() - local row, col = unpack(api.nvim_win_get_cursor(0)) +---@param window (optional, number): window handle or 0 for current, defaults to current +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` +local function make_position_param(window, offset_encoding) + window = window or 0 + local buf = vim.api.nvim_win_get_buf(window) + local row, col = unpack(api.nvim_win_get_cursor(window)) + offset_encoding = offset_encoding or M._get_offset_encoding(buf) row = row - 1 - local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] + local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] if not line then return { line = 0; character = 0; } end - -- TODO handle offset_encoding - local _ - _, col = str_utfindex(line, col) + + col = _str_utfindex_enc(line, col, offset_encoding) + return { line = row; character = col; } end --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. --- +---@param window (optional, number): window handle or 0 for current, defaults to current +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` ---@returns `TextDocumentPositionParams` object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams -function M.make_position_params() +function M.make_position_params(window, offset_encoding) + window = window or 0 + local buf = vim.api.nvim_win_get_buf(window) + offset_encoding = offset_encoding or M._get_offset_encoding(buf) return { - textDocument = M.make_text_document_params(); - position = make_position_param() + textDocument = M.make_text_document_params(buf); + position = make_position_param(window, offset_encoding) } end +--- Utility function for getting the encoding of the first LSP client on the given buffer. +---@param bufnr (number) buffer handle or 0 for current, defaults to current +---@returns (string) encoding first client if there is one, nil otherwise +function M._get_offset_encoding(bufnr) + validate { + bufnr = {bufnr, 'n', true}; + } + + local offset_encoding + + for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do + local this_offset_encoding = client.offset_encoding or "utf-16" + if not offset_encoding then + offset_encoding = this_offset_encoding + elseif offset_encoding ~= this_offset_encoding then + vim.notify("warning: multiple different client offset_encodings detected for buffer, this is not supported yet", vim.log.levels.WARN) + end + end + + return offset_encoding +end + --- Using the current position in the current buffer, creates an object that --- can be used as a building block for several LSP requests, such as --- `textDocument/codeAction`, `textDocument/colorPresentation`, --- `textDocument/rangeFormatting`. --- +---@param window (optional, number): window handle or 0 for current, defaults to current +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` ---@returns { textDocument = { uri = `current_file_uri` }, range = { start = ---`current_position`, end = `current_position` } } -function M.make_range_params() - local position = make_position_param() +function M.make_range_params(window, offset_encoding) + local buf = vim.api.nvim_win_get_buf(window) + offset_encoding = offset_encoding or M._get_offset_encoding(buf) + local position = make_position_param(window, offset_encoding) return { - textDocument = M.make_text_document_params(), + textDocument = M.make_text_document_params(buf), range = { start = position; ["end"] = position; } } end @@ -1688,27 +1805,29 @@ end ---Defaults to the start of the last visual selection. ---@param end_pos ({number, number}, optional) mark-indexed position. ---Defaults to the end of the last visual selection. +---@param bufnr (optional, number): buffer handle or 0 for current, defaults to current +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `bufnr` ---@returns { textDocument = { uri = `current_file_uri` }, range = { start = ---`start_position`, end = `end_position` } } -function M.make_given_range_params(start_pos, end_pos) +function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) validate { start_pos = {start_pos, 't', true}; end_pos = {end_pos, 't', true}; + offset_encoding = {offset_encoding, 's', true}; } - local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<')) - local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>')) + bufnr = bufnr or 0 + offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) + local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<')) + local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>')) -- convert to 0-index A[1] = A[1] - 1 B[1] = B[1] - 1 - -- account for encoding. - -- TODO handle offset_encoding + -- account for offset_encoding. if A[2] > 0 then - local _, char = M.character_offset(0, A[1], A[2]) - A = {A[1], char} + A = {A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding)} end if B[2] > 0 then - local _, char = M.character_offset(0, B[1], B[2]) - B = {B[1], char} + B = {B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding)} end -- we need to offset the end character position otherwise we loose the last -- character of the selection, as LSP end position is exclusive @@ -1717,7 +1836,7 @@ function M.make_given_range_params(start_pos, end_pos) B[2] = B[2] + 1 end return { - textDocument = M.make_text_document_params(), + textDocument = M.make_text_document_params(bufnr), range = { start = {line = A[1], character = A[2]}, ['end'] = {line = B[1], character = B[2]} @@ -1727,10 +1846,11 @@ end --- Creates a `TextDocumentIdentifier` object for the current buffer. --- +---@param bufnr (optional, number): Buffer handle, defaults to current ---@returns `TextDocumentIdentifier` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier -function M.make_text_document_params() - return { uri = vim.uri_from_bufnr(0) } +function M.make_text_document_params(bufnr) + return { uri = vim.uri_from_bufnr(bufnr or 0) } end --- Create the workspace params @@ -1773,15 +1893,16 @@ end ---@param buf buffer id (0 for current) ---@param row 0-indexed line ---@param col 0-indexed byte offset in line ----@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf} -function M.character_offset(bufnr, row, col) - local uri = vim.uri_from_bufnr(bufnr) - local line = M.get_line(uri, row) +---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `buf` +---@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf} +function M.character_offset(buf, row, col, offset_encoding) + local line = get_line(buf, row) + offset_encoding = offset_encoding or M._get_offset_encoding(buf) -- If the col is past the EOL, use the line length. if col > #line then - return str_utfindex(line) + return _str_utfindex_enc(line, nil, offset_encoding) end - return str_utfindex(line, col) + return _str_utfindex_enc(line, col, offset_encoding) end --- Helper function to return nested values in language server settings diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 6e40b6ca52..1cf618725d 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -12,7 +12,7 @@ local vim = vim or {} --- same functions as those in the input table. Userdata and threads are not --- copied and will throw an error. --- ----@param orig Table to copy +---@param orig table Table to copy ---@returns New table of copied keys and (nested) values. function vim.deepcopy(orig) end -- luacheck: no unused vim.deepcopy = (function() @@ -21,17 +21,16 @@ vim.deepcopy = (function() end local deepcopy_funcs = { - table = function(orig) + table = function(orig, cache) + if cache[orig] then return cache[orig] end local copy = {} - if vim._empty_dict_mt ~= nil and getmetatable(orig) == vim._empty_dict_mt then - copy = vim.empty_dict() - end - + cache[orig] = copy + local mt = getmetatable(orig) for k, v in pairs(orig) do - copy[vim.deepcopy(k)] = vim.deepcopy(v) + copy[vim.deepcopy(k, cache)] = vim.deepcopy(v, cache) end - return copy + return setmetatable(copy, mt) end, number = _id, string = _id, @@ -40,10 +39,10 @@ vim.deepcopy = (function() ['function'] = _id, } - return function(orig) + return function(orig, cache) local f = deepcopy_funcs[type(orig)] if f then - return f(orig) + return f(orig, cache or {}) else error("Cannot deepcopy object of type "..type(orig)) end @@ -560,6 +559,7 @@ do return type(val) == t or (t == 'callable' and vim.is_callable(val)) end + ---@private local function is_valid(opt) if type(opt) ~= 'table' then return false, string.format('opt: expected table, got %s', type(opt)) diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 66999c5f7f..07f6418c0c 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -72,7 +72,7 @@ end --- Gets the parser for this bufnr / ft combination. --- --- If needed this will create the parser. ---- Unconditionnally attach the provided callback +--- Unconditionally attach the provided callback --- ---@param bufnr The buffer the parser should be tied to ---@param lang The filetype of this parser diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 89ddd6cd5a..6f347ff25f 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -38,7 +38,7 @@ end --- Inspects the provided language. --- ---- Inspecting provides some useful informations on the language like node names, ... +--- Inspecting provides some useful information on the language like node names, ... --- ---@param lang The language. function M.inspect_language(lang) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 7e392f72a4..594765761d 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -493,9 +493,9 @@ local function tree_contains(tree, range) return false end ---- Determines wether @param range is contained in this language tree +--- Determines whether @param range is contained in this language tree --- ---- This goes down the tree to recursively check childs. +--- This goes down the tree to recursively check children. --- ---@param range A range, that is a `{ start_line, start_col, end_line, end_col }` table. function LanguageTree:contains(range) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 66da179ea3..ebed502c92 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -48,7 +48,7 @@ function M.get_query_files(lang, query_name, is_included) local base_langs = {} -- Now get the base languages by looking at the first line of every file - -- The syntax is the folowing : + -- The syntax is the following : -- ;+ inherits: ({language},)*{language} -- -- {language} ::= {lang} | ({lang}) @@ -164,22 +164,36 @@ function M.parse_query(lang, query) return self end --- TODO(vigoux): support multiline nodes too - --- Gets the text corresponding to a given node --- ---@param node the node ----@param bsource The buffer or string from which the node is extracted +---@param source The buffer or string from which the node is extracted function M.get_node_text(node, source) local start_row, start_col, start_byte = node:start() local end_row, end_col, end_byte = node:end_() if type(source) == "number" then - if start_row ~= end_row then + local lines + local eof_row = a.nvim_buf_line_count(source) + if start_row >= eof_row then return nil end - local line = a.nvim_buf_get_lines(source, start_row, start_row+1, true)[1] - return string.sub(line, start_col+1, end_col) + + if end_col == 0 then + lines = a.nvim_buf_get_lines(source, start_row, end_row, true) + end_col = -1 + else + lines = a.nvim_buf_get_lines(source, start_row, end_row + 1, true) + end + + if #lines == 1 then + lines[1] = string.sub(lines[1], start_col+1, end_col) + else + lines[1] = string.sub(lines[1], start_col+1) + lines[#lines] = string.sub(lines[#lines], 1, end_col) + end + + return table.concat(lines, "\n") elseif type(source) == "string" then return source:sub(start_byte+1, end_byte) end @@ -211,11 +225,6 @@ local predicate_handlers = { ["lua-match?"] = function(match, _, source, predicate) local node = match[predicate[2]] local regex = predicate[3] - local start_row, _, end_row, _ = node:range() - if start_row ~= end_row then - return false - end - return string.find(M.get_node_text(node, source), regex) end, @@ -239,13 +248,8 @@ local predicate_handlers = { return function(match, _, source, pred) local node = match[pred[2]] - local start_row, start_col, end_row, end_col = node:range() - if start_row ~= end_row then - return false - end - local regex = compiled_vim_regexes[pred[3]] - return regex:match_line(source, start_row, start_col, end_col) + return regex:match_str(M.get_node_text(node, source)) end end)(), @@ -446,7 +450,7 @@ end --- --- {source} is needed if the query contains predicates, then the caller --- must ensure to use a freshly parsed tree consistent with the current ---- text of the buffer (if relevent). {start_row} and {end_row} can be used to limit +--- text of the buffer (if relevant). {start_row} and {end_row} can be used to limit --- matches inside a row range (this is typically used with root node --- as the node, i e to get syntax highlight matches in the current --- viewport). When omitted the start and end row values are used from the given node. @@ -466,7 +470,7 @@ end --- </pre> --- ---@param node The node under which the search will occur ----@param source The source buffer or string to exctract text from +---@param source The source buffer or string to extract text from ---@param start The starting line of the search ---@param stop The stopping line of the search (end-exclusive) --- diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index 5d8d4fa169..d08d2a3ee3 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -56,8 +56,8 @@ local function is_windows_file_uri(uri) end --- Get a URI from a file path. ----@param path (string): Path to file ----@return URI +---@param path string Path to file +---@return string URI local function uri_from_fname(path) local volume_path, fname = path:match("^([a-zA-Z]:)(.*)") local is_windows = volume_path ~= nil @@ -78,8 +78,8 @@ local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9+-.]*):.*' local WINDOWS_URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9+-.]*):[a-zA-Z]:.*' --- Get a URI from a bufnr ----@param bufnr (number): Buffer number ----@return URI +---@param bufnr number +---@return string URI local function uri_from_bufnr(bufnr) local fname = vim.api.nvim_buf_get_name(bufnr) local volume_path = fname:match("^([a-zA-Z]:).*") @@ -99,8 +99,8 @@ local function uri_from_bufnr(bufnr) end --- Get a filename from a URI ----@param uri (string): The URI ----@return Filename +---@param uri string +---@return string filename or unchanged URI for non-file URIs local function uri_to_fname(uri) local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) if scheme ~= 'file' then @@ -117,17 +117,13 @@ local function uri_to_fname(uri) return uri end ---- Return or create a buffer for a uri. ----@param uri (string): The URI ----@return bufnr. ----@note Creates buffer but does not load it +--- Get the buffer for a uri. +--- Creates a new unloaded buffer if no buffer for the uri already exists. +-- +---@param uri string +---@return number bufnr local function uri_to_bufnr(uri) - local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) - if scheme == 'file' then - return vim.fn.bufadd(uri_to_fname(uri)) - else - return vim.fn.bufadd(uri) - end + return vim.fn.bufadd(uri_to_fname(uri)) end return { |