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 | 2 | ||||
-rw-r--r-- | runtime/lua/vim/diagnostic.lua | 294 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 205 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 31 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/diagnostic.lua | 27 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 3 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/log.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 13 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/sync.lua | 79 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/tagfunc.lua | 76 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 258 | ||||
-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 | 6 | ||||
-rw-r--r-- | runtime/lua/vim/uri.lua | 28 |
18 files changed, 579 insertions, 474 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..a3d5f64630 100644 --- a/runtime/lua/vim/_meta.lua +++ b/runtime/lua/vim/_meta.lua @@ -398,7 +398,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, diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 191b9b9145..5c5631af98 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) @@ -106,8 +133,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,36 +238,6 @@ 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) @@ -287,11 +282,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}) @@ -377,6 +367,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) 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) @@ -386,7 +441,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 }) @@ -399,27 +456,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) @@ -431,13 +473,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 @@ -475,16 +518,15 @@ local function diagnostic_move_pos(opts, pos) 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, { + scope = "cursor", + focus = false, + }) ) end) end end --- }}} - --- Public API {{{ - --- Configure diagnostic options globally or for a specific diagnostic --- namespace. --- @@ -537,26 +579,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|. ---- * 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. ---- Defaults to "Diagnostics:". ---- * 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, 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. +--- - 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 @@ -564,6 +587,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) @@ -617,33 +641,26 @@ 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_command( + string.format("doautocmd <nomodeline> DiagnosticChanged %s", vim.api.nvim_buf_get_name(bufnr)) + ) 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' } } @@ -689,49 +706,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. @@ -807,7 +782,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( @@ -1115,7 +1092,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 @@ -1141,8 +1118,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) @@ -1156,7 +1131,7 @@ end ---@param opts table|nil Configuration table with the same keys as --- |vim.lsp.util.open_floating_preview()| in addition to the following: --- - 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"). --- - 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; @@ -1173,7 +1148,17 @@ end --- - 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, string, or table) 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) @@ -1184,7 +1169,7 @@ function M.open_float(bufnr, opts) opts = opts or {} bufnr = get_bufnr(bufnr) - local scope = opts.scope or "buffer" + local scope = opts.scope or "line" local lnum, col if scope == "line" or scope == "cursor" then if not opts.pos then @@ -1213,8 +1198,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) @@ -1334,16 +1318,18 @@ 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_command( + string.format("doautocmd <nomodeline> DiagnosticChanged %s", vim.api.nvim_buf_get_name(bufnr)) + ) end --- Add all diagnostics to the quickfix list. @@ -1545,7 +1531,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 @@ -1563,6 +1549,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..61a700bd15 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 @@ -566,12 +580,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 +599,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 +620,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 +674,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. @@ -779,6 +797,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 +826,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 +861,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 +877,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 +889,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 +1033,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 +1110,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 +1169,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 +1179,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 +1217,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 +1226,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 +1296,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 +1331,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 @@ -1587,6 +1615,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 +1683,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 +1752,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..f02ebfb9dc 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -165,7 +165,7 @@ 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 @@ -176,9 +176,10 @@ function M.formatting_sync(options, timeout_ms) 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()) + local bufnr = vim.api.nvim_get_current_buf() + local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, bufnr) if result and result.result then - util.apply_text_edits(result.result) + util.apply_text_edits(result.result, bufnr) elseif err then vim.notify("vim.lsp.buf.formatting_sync: " .. err, vim.log.levels.WARN) end @@ -202,6 +203,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 +222,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 @@ -261,6 +263,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 +372,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 +392,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 +401,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 +418,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 +447,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..075da41b23 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -220,12 +220,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 @@ -434,24 +431,6 @@ 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) if opts then if opts.severity then @@ -565,7 +544,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 @@ -717,5 +696,3 @@ end -- }}} return M - --- vim: fdm=marker diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index fa4f2b22a5..22089aaaa6 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 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/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 585c14a00f..5df2a4d144 100644 --- a/runtime/lua/vim/lsp/sync.lua +++ b/runtime/lua/vim/lsp/sync.lua @@ -75,42 +75,45 @@ 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) - 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 + 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 - error('`align` must be start or end.') + char = byte_to_utf(line, byte, offset_encoding) + byte = byte + offset end -- Extending line, find the nearest utf codepoint for the last valid character end @@ -131,7 +134,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 +157,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, start_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 +188,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,11 +233,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 + -- 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_position(prev_line, prev_end_byte_idx, 'start', offset_encoding) + 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 @@ -236,7 +251,7 @@ local function compute_end_range(prev_lines, curr_lines, start_range, firstline, if curr_end_byte_idx == 0 then curr_end_byte_idx = 1 end - local curr_byte_idx, curr_char_idx = align_position(curr_line, curr_end_byte_idx, 'start', offset_encoding) + 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 @@ -280,18 +295,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 ba5c20ef9f..fad2b962b5 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -148,6 +148,93 @@ 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 --- 1-indexed @@ -158,31 +245,25 @@ 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 = get_line(bufnr, position.line) + local ok, result - 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 offset_encoding == "utf-16" or not offset_encoding then + ok, result = pcall(vim.str_byteindex, line, col, true) + elseif offset_encoding == "utf-32" then + ok, result = pcall(vim.str_byteindex, line, col, false) + end - if ok then - return result - end - return math.min(#lines[1], col) + 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 = {} @@ -246,6 +327,10 @@ end ---@param bufnr number Buffer id ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit function M.apply_text_edits(text_edits, bufnr) + validate { + text_edits = { text_edits, 't', false }; + bufnr = { bufnr, 'number', false }; + } if not next(text_edits) then return end if not api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) @@ -474,7 +559,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 +659,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 +671,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) @@ -1054,7 +1139,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 +1172,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() @@ -1223,18 +1308,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 +1334,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.focus = opts.focus ~= false opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "BufHidden", "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,7 +1403,7 @@ 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}) + api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true, nowait = true}) M.close_preview_autocmd(opts.close_events, floating_winnr) -- save focus_id @@ -1331,7 +1420,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,9 +1428,9 @@ 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 offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 + ---@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 utf-16 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight function M.buf_highlight_references(bufnr, references, offset_encoding) validate { bufnr = {bufnr, 'n', true} } @@ -1372,88 +1461,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. --- @@ -1496,7 +1503,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 @@ -1541,7 +1548,7 @@ function M.set_qflist(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) @@ -1773,8 +1780,7 @@ end ---@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) + local line = get_line(bufnr, row) -- If the col is past the EOL, use the line length. if col > #line then return str_utfindex(line) 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..b8255e61ed 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}) @@ -446,7 +446,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 +466,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 { |