diff options
Diffstat (limited to 'runtime/lua/vim/lsp/util.lua')
| -rw-r--r-- | runtime/lua/vim/lsp/util.lua | 1101 |
1 files changed, 507 insertions, 594 deletions
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 882ec22ca6..6eab0f3da4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2,12 +2,8 @@ local protocol = require('vim.lsp.protocol') local validate = vim.validate local api = vim.api local list_extend = vim.list_extend -local highlight = vim.highlight local uv = vim.uv -local npcall = vim.F.npcall -local split = vim.split - local M = {} local default_border = { @@ -21,82 +17,73 @@ local default_border = { { ' ', 'NormalFloat' }, } +--- @param border string|(string|[string,string])[] +local function border_error(border) + error( + string.format( + 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', + vim.inspect(border) + ), + 2 + ) +end + +local border_size = { + none = { 0, 0 }, + single = { 2, 2 }, + double = { 2, 2 }, + rounded = { 2, 2 }, + solid = { 2, 2 }, + shadow = { 1, 1 }, +} + --- Check the border given by opts or the default border for the additional --- size it adds to a float. ----@param opts table optional options for the floating window ---- - border (string or table) the border ----@return table size of border in the form of { height = height, width = width } +--- @param opts? {border:string|(string|[string,string])[]} +--- @return integer height +--- @return integer width local function get_border_size(opts) local border = opts and opts.border or default_border - local height = 0 - local width = 0 if type(border) == 'string' then - local border_size = { - none = { 0, 0 }, - single = { 2, 2 }, - double = { 2, 2 }, - rounded = { 2, 2 }, - solid = { 2, 2 }, - shadow = { 1, 1 }, - } - if border_size[border] == nil then - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) + if not border_size[border] then + border_error(border) end - height, width = unpack(border_size[border]) - else - if 8 % #border ~= 0 then - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) - end - local function border_width(id) - id = (id - 1) % #border + 1 - if type(border[id]) == 'table' then - -- border specified as a table of <character, highlight group> - return vim.fn.strdisplaywidth(border[id][1]) - elseif type(border[id]) == 'string' then - -- border specified as a list of border characters - return vim.fn.strdisplaywidth(border[id]) - end - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) - end - local function border_height(id) - id = (id - 1) % #border + 1 - if type(border[id]) == 'table' then - -- border specified as a table of <character, highlight group> - return #border[id][1] > 0 and 1 or 0 - elseif type(border[id]) == 'string' then - -- border specified as a list of border characters - return #border[id] > 0 and 1 or 0 - end - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) + return unpack(border_size[border]) + end + + if 8 % #border ~= 0 then + border_error(border) + end + + --- @param id integer + --- @return string + local function elem(id) + id = (id - 1) % #border + 1 + local e = border[id] + if type(e) == 'table' then + -- border specified as a table of <character, highlight group> + return e[1] + elseif type(e) == 'string' then + -- border specified as a list of border characters + return e end - height = height + border_height(2) -- top - height = height + border_height(6) -- bottom - width = width + border_width(4) -- right - width = width + border_width(8) -- left + --- @diagnostic disable-next-line:missing-return + border_error(border) + end + + --- @param e string + local function border_height(e) + return #e > 0 and 1 or 0 end - return { height = height, width = width } + local top, bottom = elem(2), elem(6) + local height = border_height(top) + border_height(bottom) + + local right, left = elem(4), elem(8) + local width = vim.fn.strdisplaywidth(right) + vim.fn.strdisplaywidth(left) + + return height, width end --- Splits string at newlines, optionally removing unwanted blank lines. @@ -122,79 +109,13 @@ local function split_lines(s, no_blank) end local function create_window_without_focus() - local prev = vim.api.nvim_get_current_win() + local prev = api.nvim_get_current_win() vim.cmd.new() - local new = vim.api.nvim_get_current_win() - vim.api.nvim_set_current_win(prev) + local new = api.nvim_get_current_win() + api.nvim_set_current_win(prev) return new end ---- Convert byte index to `encoding` index. ---- Convenience wrapper around vim.str_utfindex ----@param line string line to be indexed ----@param index integer|nil byte index (utf-8), or `nil` for length ----@param encoding 'utf-8'|'utf-16'|'utf-32'|nil defaults to utf-16 ----@return integer `encoding` index of `index` in `line` -function M._str_utfindex_enc(line, index, encoding) - local len32, len16 = vim.str_utfindex(line) - if not encoding then - encoding = 'utf-16' - end - if encoding == 'utf-8' then - if index then - return index - else - return #line - end - elseif encoding == 'utf-16' then - if not index or index > len16 then - return len16 - end - local _, col16 = vim.str_utfindex(line, index) - return col16 - elseif encoding == 'utf-32' then - if not index or index > len32 then - return len32 - end - local col32, _ = vim.str_utfindex(line, index) - return col32 - else - error('Invalid encoding: ' .. vim.inspect(encoding)) - 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 integer UTF index ----@param encoding string utf-8|utf-16|utf-32| defaults to utf-16 ----@return integer byte (utf-8) index of `encoding` index `index` in `line` -function M._str_byteindex_enc(line, index, encoding) - local len = #line - if index > len then - -- LSP spec: if character > line length, default to the line length. - -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position - return len - end - if not encoding then - encoding = 'utf-16' - end - if encoding == 'utf-8' then - if index then - return index - else - return len - end - elseif encoding == 'utf-16' then - return vim.str_byteindex(line, index, true) - elseif encoding == 'utf-32' then - return vim.str_byteindex(line, index) - else - error('Invalid encoding: ' .. vim.inspect(encoding)) - end -end - --- Replaces text in a range with new text. --- --- CAUTION: Changes in-place! @@ -243,6 +164,8 @@ function M.set_lines(lines, A, B, new_lines) return lines end +--- @param fn fun(x:any):any[] +--- @return function local function sort_by_key(fn) return function(a, b) local ka, kb = fn(a), fn(b) @@ -354,7 +277,7 @@ end --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position ---@param position lsp.Position ----@param offset_encoding? string utf-8|utf-16|utf-32 +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@return integer local function get_line_byte_from_position(bufnr, position, offset_encoding) -- LSP's line and characters are 0-indexed @@ -364,7 +287,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding) -- character if col > 0 then local line = get_line(bufnr, position.line) or '' - return M._str_byteindex_enc(line, col, offset_encoding or 'utf-16') + return vim.str_byteindex(line, offset_encoding, col, false) end return col end @@ -372,14 +295,13 @@ end --- Applies a list of text edits to a buffer. ---@param text_edits lsp.TextEdit[] ---@param bufnr integer Buffer id ----@param offset_encoding string utf-8|utf-16|utf-32 +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit 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', false }, - }) + validate('text_edits', text_edits, 'table', false) + validate('bufnr', bufnr, 'number', false) + validate('offset_encoding', offset_encoding, 'string', false) + if not next(text_edits) then return end @@ -392,10 +314,8 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) vim.bo[bufnr].buflisted = true -- Fix reversed range and indexing each text_edits - local index = 0 - --- @param text_edit lsp.TextEdit - text_edits = vim.tbl_map(function(text_edit) - index = index + 1 + for index, text_edit in ipairs(text_edits) do + --- @cast text_edit lsp.TextEdit|{_index: integer} text_edit._index = index if @@ -407,8 +327,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) text_edit.range.start = text_edit.range['end'] text_edit.range['end'] = start end - return text_edit - end, text_edits) + end -- Sort text_edits ---@param a lsp.TextEdit | { _index: integer } @@ -439,47 +358,45 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n') -- Convert from LSP style ranges to Neovim style ranges. - local e = { - start_row = text_edit.range.start.line, - start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding), - end_row = text_edit.range['end'].line, - end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding), - text = split(text_edit.newText, '\n', { plain = true }), - } + local start_row = text_edit.range.start.line + local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding) + local end_row = text_edit.range['end'].line + local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding) + local text = vim.split(text_edit.newText, '\n', { plain = true }) local max = api.nvim_buf_line_count(bufnr) -- If the whole edit is after the lines in the buffer we can simply add the new text to the end -- of the buffer. - if max <= e.start_row then - api.nvim_buf_set_lines(bufnr, max, max, false, e.text) + if max <= start_row then + api.nvim_buf_set_lines(bufnr, max, max, false, text) else - local last_line_len = #(get_line(bufnr, math.min(e.end_row, max - 1)) or '') + local last_line_len = #(get_line(bufnr, math.min(end_row, max - 1)) or '') -- 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. - if max <= e.end_row then - e.end_row = max - 1 - e.end_col = last_line_len + if max <= end_row then + end_row = max - 1 + end_col = last_line_len has_eol_text_edit = true else - -- If the replacement is over the end of a line (i.e. e.end_col is equal to the line length and the + -- If the replacement is over the end of a line (i.e. end_col is equal to the line length and the -- replacement text ends with a newline We can likely assume that the replacement is assumed -- to be meant to replace the newline with another newline and we need to make sure this -- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r' -- in the file some servers (clangd on windows) will include that character in the line -- while nvim_buf_set_text doesn't count it as part of the line. if - e.end_col >= last_line_len - and text_edit.range['end'].character > e.end_col + end_col >= last_line_len + and text_edit.range['end'].character > end_col and #text_edit.newText > 0 and string.sub(text_edit.newText, -1) == '\n' then - table.remove(e.text, #e.text) + table.remove(text, #text) end end - -- Make sure we don't go out of bounds for e.end_col - e.end_col = math.min(last_line_len, e.end_col) + -- Make sure we don't go out of bounds for end_col + end_col = math.min(last_line_len, end_col) - api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text) + api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, text) end end @@ -495,7 +412,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- make sure we don't go out of bounds pos[1] = math.min(pos[1], max) pos[2] = math.min(pos[2], #(get_line(bufnr, pos[1] - 1) or '')) - vim.api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {}) + api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {}) end end @@ -513,7 +430,7 @@ end --- ---@param text_document_edit lsp.TextDocumentEdit ---@param index? integer: Optional index of the edit, if from a list of edits (or nil, if not from a list) ----@param offset_encoding? string +---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32' ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit function M.apply_text_document_edit(text_document_edit, index, offset_encoding) local text_document = text_document_edit.textDocument @@ -523,19 +440,15 @@ function M.apply_text_document_edit(text_document_edit, index, offset_encoding) 'apply_text_document_edit must be called with valid offset encoding', vim.log.levels.WARN ) - end - - -- For lists of text document edits, - -- do not check the version after the first edit. - local should_check_version = true - if index and index > 1 then - should_check_version = false + return end -- `VersionedTextDocumentIdentifier`s version may be null -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier if - should_check_version + -- For lists of text document edits, + -- do not check the version after the first edit. + not (index and index > 1) and ( text_document.version and text_document.version > 0 @@ -553,6 +466,9 @@ local function path_components(path) return vim.split(path, '/', { plain = true }) end +--- @param path string[] +--- @param prefix string[] +--- @return boolean local function path_under_prefix(path, prefix) for i, c in ipairs(prefix) do if c ~= path[i] then @@ -562,17 +478,24 @@ local function path_under_prefix(path, prefix) return true end ---- Get list of buffers whose filename matches the given path prefix (normalized full path) +--- Get list of loaded writable buffers whose filename matches the given path +--- prefix (normalized full path). ---@param prefix string ---@return integer[] -local function get_bufs_with_prefix(prefix) - prefix = path_components(prefix) - local buffers = {} - for _, v in ipairs(vim.api.nvim_list_bufs()) do - local bname = vim.api.nvim_buf_get_name(v) - local path = path_components(vim.fs.normalize(bname, { expand_env = false })) - if path_under_prefix(path, prefix) then - table.insert(buffers, v) +local function get_writable_bufs(prefix) + local prefix_parts = path_components(prefix) + local buffers = {} --- @type integer[] + for _, buf in ipairs(api.nvim_list_bufs()) do + -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them. + if + api.nvim_buf_is_loaded(buf) + and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[buf].buftype) + then + local bname = api.nvim_buf_get_name(buf) + local path = path_components(vim.fs.normalize(bname, { expand_env = false })) + if path_under_prefix(path, prefix_parts) then + buffers[#buffers + 1] = buf + end end end return buffers @@ -616,19 +539,13 @@ function M.rename(old_fname, new_fname, opts) local buf_rename = {} ---@type table<integer, {from: string, to: string}> local old_fname_pat = '^' .. vim.pesc(old_fname_full) - for b in - vim.iter(get_bufs_with_prefix(old_fname_full)):filter(function(b) - -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them. - return api.nvim_buf_is_loaded(b) - and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[b].buftype) - end) - do + for _, b in ipairs(get_writable_bufs(old_fname_full)) do -- Renaming a buffer may conflict with another buffer that happens to have the same name. In -- most cases, this would have been already detected by the file conflict check above, but the -- conflicting buffer may not be associated with a file. For example, 'buftype' can be "nofile" -- or "nowrite", or the buffer can be a normal buffer but has not been written to the file yet. -- Renaming should fail in such cases to avoid losing the contents of the conflicting buffer. - local old_bname = vim.api.nvim_buf_get_name(b) + local old_bname = api.nvim_buf_get_name(b) local new_bname = old_bname:gsub(old_fname_pat, escape_gsub_repl(new_fname)) if vim.fn.bufexists(new_bname) == 1 then local existing_buf = vim.fn.bufnr(new_bname) @@ -702,7 +619,7 @@ end --- Applies a `WorkspaceEdit`. --- ---@param workspace_edit lsp.WorkspaceEdit ----@param offset_encoding string utf-8|utf-16|utf-32 (required) +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' (required) ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit function M.apply_workspace_edit(workspace_edit, offset_encoding) if offset_encoding == nil then @@ -710,16 +627,18 @@ function M.apply_workspace_edit(workspace_edit, offset_encoding) 'apply_workspace_edit must be called with valid offset encoding', vim.log.levels.WARN ) + return end if workspace_edit.documentChanges then for idx, change in ipairs(workspace_edit.documentChanges) do if change.kind == 'rename' then - M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), change.options) + local options = change.options --[[@as vim.lsp.util.rename.Opts]] + M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), options) elseif change.kind == 'create' then create_file(change) elseif change.kind == 'delete' then delete_file(change) - elseif change.kind then + elseif change.kind then --- @diagnostic disable-line:undefined-field error(string.format('Unsupported change: %q', vim.inspect(change))) else M.apply_text_document_edit(change, idx, offset_encoding) @@ -748,7 +667,7 @@ end --- then the corresponding value is returned without further modifications. --- ---@param input lsp.MarkedString|lsp.MarkedString[]|lsp.MarkupContent ----@param contents string[]|nil List of strings to extend with converted lines. Defaults to {}. +---@param contents string[]? List of strings to extend with converted lines. Defaults to {}. ---@return string[] extended with lines of converted markdown. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover function M.convert_input_to_markdown_lines(input, contents) @@ -781,111 +700,117 @@ function M.convert_input_to_markdown_lines(input, contents) return contents end +--- Returns the line/column-based position in `contents` at the given offset. +--- +---@param offset integer +---@param contents string[] +---@return { [1]: integer, [2]: integer }? +local function get_pos_from_offset(offset, contents) + local i = 0 + for l, line in ipairs(contents) do + if offset >= i and offset < i + #line then + return { l - 1, offset - i + 1 } + else + i = i + #line + 1 + end + end +end + --- Converts `textDocument/signatureHelp` response to markdown lines. --- ---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp` ----@param ft string|nil filetype that will be use as the `lang` for the label markdown code block ----@param triggers table|nil list of trigger characters from the lsp server. used to better determine parameter offsets ----@return string[]|nil table list of lines of converted markdown. ----@return number[]|nil table of active hl +---@param ft string? filetype that will be use as the `lang` for the label markdown code block +---@param triggers string[]? list of trigger characters from the lsp server. used to better determine parameter offsets +---@return string[]? # lines of converted markdown. +---@return Range4? # highlight range for the active parameter ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers) - if not signature_help.signatures then - return - end --The active signature. If omitted or the value lies outside the range of --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`. --Whenever possible implementors should make an active decision about --the active signature and shouldn't rely on a default value. - local contents = {} - local active_hl + local contents = {} --- @type string[] + local active_offset ---@type [integer, integer]? local active_signature = signature_help.activeSignature or 0 -- If the activeSignature is not inside the valid range, then clip it. -- In 3.15 of the protocol, activeSignature was allowed to be negative if active_signature >= #signature_help.signatures or active_signature < 0 then active_signature = 0 end - local signature = signature_help.signatures[active_signature + 1] - if not signature then - return - end + local signature = vim.deepcopy(signature_help.signatures[active_signature + 1]) local label = signature.label if ft then -- wrap inside a code block for proper rendering label = ('```%s\n%s\n```'):format(ft, label) end - list_extend(contents, split(label, '\n', { plain = true, trimempty = true })) - if signature.documentation then + list_extend(contents, vim.split(label, '\n', { plain = true, trimempty = true })) + local doc = signature.documentation + if doc then -- if LSP returns plain string, we treat it as plaintext. This avoids -- special characters like underscore or similar from being interpreted -- as markdown font modifiers - if type(signature.documentation) == 'string' then - signature.documentation = { kind = 'plaintext', value = signature.documentation } + if type(doc) == 'string' then + signature.documentation = { kind = 'plaintext', value = doc } end M.convert_input_to_markdown_lines(signature.documentation, contents) end if signature.parameters and #signature.parameters > 0 then - local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0) - if active_parameter < 0 then - active_parameter = 0 - end + -- First check if the signature has an activeParameter. If it doesn't check if the response + -- had that property instead. Else just default to 0. + local active_parameter = + math.max(signature.activeParameter or signature_help.activeParameter or 0, 0) -- If the activeParameter is > #parameters, then set it to the last -- NOTE: this is not fully according to the spec, but a client-side interpretation - if active_parameter >= #signature.parameters then - active_parameter = #signature.parameters - 1 - end + active_parameter = math.min(active_parameter, #signature.parameters - 1) local parameter = signature.parameters[active_parameter + 1] - if parameter then - --[=[ - --Represents a parameter of a callable-signature. A parameter can - --have a label and a doc-comment. - interface ParameterInformation { - --The label of this parameter information. - -- - --Either a string or an inclusive start and exclusive end offsets within its containing - --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 - --string representation as `Position` and `Range` does. - -- - --*Note*: a label of type string should be a substring of its containing signature label. - --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. - label: string | [number, number]; - --The human-readable doc-comment of this parameter. Will be shown - --in the UI but can be omitted. - documentation?: string | MarkupContent; - } - --]=] - if parameter.label then - if type(parameter.label) == 'table' then - active_hl = parameter.label - else - local offset = 1 - -- try to set the initial offset to the first found trigger character - for _, t in ipairs(triggers or {}) do - local trigger_offset = signature.label:find(t, 1, true) - if trigger_offset and (offset == 1 or trigger_offset < offset) then - offset = trigger_offset - end - end - for p, param in pairs(signature.parameters) do - offset = signature.label:find(param.label, offset, true) - if not offset then - break - end - if p == active_parameter + 1 then - active_hl = { offset - 1, offset + #parameter.label - 1 } - break - end - offset = offset + #param.label + 1 - end + local parameter_label = parameter.label + if type(parameter_label) == 'table' then + active_offset = parameter_label + else + local offset = 1 ---@type integer? + -- try to set the initial offset to the first found trigger character + for _, t in ipairs(triggers or {}) do + local trigger_offset = signature.label:find(t, 1, true) + if trigger_offset and (offset == 1 or trigger_offset < offset) then + offset = trigger_offset end end - if parameter.documentation then - M.convert_input_to_markdown_lines(parameter.documentation, contents) + for p, param in pairs(signature.parameters) do + local plabel = param.label + assert(type(plabel) == 'string', 'Expected label to be a string') + offset = signature.label:find(plabel, offset, true) + if not offset then + break + end + if p == active_parameter + 1 then + active_offset = { offset - 1, offset + #parameter_label - 1 } + break + end + offset = offset + #param.label + 1 end end + if parameter.documentation then + M.convert_input_to_markdown_lines(parameter.documentation, contents) + end end + + local active_hl = nil + if active_offset then + -- Account for the start of the markdown block. + if ft then + active_offset[1] = active_offset[1] + #contents[1] + active_offset[2] = active_offset[2] + #contents[1] + end + + local a_start = get_pos_from_offset(active_offset[1], contents) + local a_end = get_pos_from_offset(active_offset[2], contents) + if a_start and a_end then + active_hl = { a_start[1], a_start[2], a_end[1], a_end[2] } + end + end + return contents, active_hl end @@ -894,32 +819,15 @@ end --- ---@param width integer window width (in character cells) ---@param height integer window height (in character cells) ----@param opts table optional ---- - offset_x (integer) offset to add to `col` ---- - offset_y (integer) offset to add to `row` ---- - border (string or table) override `border` ---- - focusable (string or table) override `focusable` ---- - zindex (string or table) override `zindex`, defaults to 50 ---- - relative ("mouse"|"cursor") defaults to "cursor" ---- - anchor_bias ("auto"|"above"|"below") defaults to "auto" ---- - "auto": place window based on which side of the cursor has more lines ---- - "above": place the window above the cursor unless there are not enough lines ---- to display the full window height. ---- - "below": place the window below the cursor unless there are not enough lines ---- to display the full window height. ----@return table Options +---@param opts? vim.lsp.util.open_floating_preview.Opts +---@return vim.api.keyset.win_config function M.make_floating_popup_options(width, height, opts) - validate({ - opts = { opts, 't', true }, - }) + validate('opts', opts, 'table', true) opts = opts or {} - validate({ - ['opts.offset_x'] = { opts.offset_x, 'n', true }, - ['opts.offset_y'] = { opts.offset_y, 'n', true }, - }) + validate('opts.offset_x', opts.offset_x, 'number', true) + validate('opts.offset_y', opts.offset_y, 'number', true) local anchor = '' - local row, col local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1 or vim.fn.winline() - 1 @@ -927,7 +835,7 @@ function M.make_floating_popup_options(width, height, opts) local anchor_bias = opts.anchor_bias or 'auto' - local anchor_below + local anchor_below --- @type boolean? if anchor_bias == 'below' then anchor_below = (lines_below > lines_above) or (height <= lines_below) @@ -938,7 +846,8 @@ function M.make_floating_popup_options(width, height, opts) anchor_below = lines_below > lines_above end - local border_height = get_border_size(opts).height + local border_height = get_border_size(opts) + local row, col --- @type integer?, integer? if anchor_below then anchor = anchor .. 'N' height = math.max(math.min(lines_below - border_height, height), 0) @@ -960,7 +869,7 @@ function M.make_floating_popup_options(width, height, opts) end local title = (opts.border and opts.title) and opts.title or nil - local title_pos + local title_pos --- @type 'left'|'center'|'right'? if title then title_pos = opts.title_pos or 'center' @@ -982,13 +891,21 @@ function M.make_floating_popup_options(width, height, opts) } end +--- @class vim.lsp.util.show_document.Opts +--- @inlinedoc +--- +--- Jump to existing window if buffer is already open. +--- @field reuse_win? boolean +--- +--- Whether to focus/jump to location if possible. +--- (defaults: true) +--- @field focus? boolean + --- Shows document and optionally jumps to the location. --- ---@param location lsp.Location|lsp.LocationLink ----@param offset_encoding string|nil utf-8|utf-16|utf-32 ----@param opts table|nil options ---- - reuse_win (boolean) Jump to existing window if buffer is already open. ---- - focus (boolean) Whether to focus/jump to location if possible. Defaults to true. +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? +---@param opts? vim.lsp.util.show_document.Opts ---@return boolean `true` if succeeded function M.show_document(location, offset_encoding, opts) -- location may be Location or LocationLink @@ -998,6 +915,7 @@ function M.show_document(location, offset_encoding, opts) end if offset_encoding == nil then vim.notify_once('show_document must be called with valid offset encoding', vim.log.levels.WARN) + return false end local bufnr = vim.uri_to_bufnr(uri) @@ -1041,18 +959,13 @@ end --- Jumps to a location. --- +---@deprecated use `vim.lsp.util.show_document` with `{focus=true}` instead ---@param location lsp.Location|lsp.LocationLink ----@param offset_encoding string|nil utf-8|utf-16|utf-32 ----@param reuse_win boolean|nil Jump to existing window if buffer is already open. +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? +---@param reuse_win boolean? Jump to existing window if buffer is already open. ---@return boolean `true` if the jump succeeded function M.jump_to_location(location, offset_encoding, reuse_win) - if offset_encoding == nil then - vim.notify_once( - 'jump_to_location must be called with valid offset encoding', - vim.log.levels.WARN - ) - end - + vim.deprecate('vim.lsp.util.jump_to_location', nil, '0.12') return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true }) end @@ -1063,9 +976,9 @@ end --- - for LocationLink, targetRange is shown (e.g., body of function definition) --- ---@param location lsp.Location|lsp.LocationLink ----@param opts table ----@return integer|nil buffer id of float window ----@return integer|nil window id of float window +---@param opts? vim.lsp.util.open_floating_preview.Opts +---@return integer? buffer id of float window +---@return integer? window id of float window function M.preview_location(location, opts) -- location may be LocationLink or Location (more useful for the former) local uri = location.targetUri or location.uri @@ -1092,7 +1005,7 @@ end local function find_window_by_var(name, value) for _, win in ipairs(api.nvim_list_wins()) do - if npcall(api.nvim_win_get_var, win, name) == value then + if vim.w[win][name] == value then return win end end @@ -1158,8 +1071,10 @@ local function collapse_blank_lines(contents) end local function get_markdown_fences() - local fences = {} - for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do + local fences = {} --- @type table<string,string> + for _, fence in + pairs(vim.g.markdown_fenced_languages or {} --[[@as string[] ]]) + do local lang, syntax = fence:match('^(.*)=(.*)$') if lang then fences[lang] = syntax @@ -1179,7 +1094,7 @@ end --- ---@param bufnr integer ---@param contents string[] of lines to show in window ----@param opts table with optional fields +---@param opts? table with optional fields --- - height of floating window --- - width of floating window --- - wrap_at character to wrap at for computing height @@ -1188,10 +1103,8 @@ end --- - separator insert separator after code block ---@return table stripped content function M.stylize_markdown(bufnr, contents, opts) - validate({ - contents = { contents, 't' }, - opts = { opts, 't', true }, - }) + validate('contents', contents, 'table') + validate('opts', opts, 'table', true) opts = opts or {} -- table of fence types to {ft, begin, end} @@ -1203,8 +1116,11 @@ function M.stylize_markdown(bufnr, contents, opts) text = { 'text', '<text>', '</text>' }, } - local match_begin = function(line) + --- @param line string + --- @return {type:string,ft:string}? + local function match_begin(line) for type, pattern in pairs(matchers) do + --- @type string? local ret = line:match(string.format('^%%s*%s%%s*$', pattern[2])) if ret then return { @@ -1215,7 +1131,10 @@ function M.stylize_markdown(bufnr, contents, opts) end end - local match_end = function(line, match) + --- @param line string + --- @param match {type:string,ft:string} + --- @return string + local function match_end(line, match) local pattern = matchers[match.type] return line:match(string.format('^%%s*%s%%s*$', pattern[3])) end @@ -1224,76 +1143,80 @@ function M.stylize_markdown(bufnr, contents, opts) contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) local stripped = {} - local highlights = {} + local highlights = {} --- @type {ft:string,start:integer,finish:integer}[] -- keep track of lnums that contain markdown - local markdown_lines = {} - do - local i = 1 - while i <= #contents do - local line = contents[i] - local match = match_begin(line) - if match then - local start = #stripped - i = i + 1 - while i <= #contents do - line = contents[i] - if match_end(line, match) then - i = i + 1 - break - end - table.insert(stripped, line) + local markdown_lines = {} --- @type table<integer,boolean> + + local i = 1 + while i <= #contents do + local line = contents[i] + local match = match_begin(line) + if match then + local start = #stripped + i = i + 1 + while i <= #contents do + line = contents[i] + if match_end(line, match) then i = i + 1 + break end - table.insert(highlights, { - ft = match.ft, - start = start + 1, - finish = #stripped, - }) - -- add a separator, but not on the last line - if opts.separator and i < #contents then - table.insert(stripped, '---') - markdown_lines[#stripped] = true - end - else - -- strip any empty lines or separators prior to this separator in actual markdown |