diff options
| author | Lewis Russell <lewis6991@gmail.com> | 2024-10-17 13:35:02 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-17 13:35:02 +0100 |
| commit | fa6ab0d90958516d0bc1ed62839d85405ad08fa8 (patch) | |
| tree | 310a1c9bd3fcb6d176f823ebb81e7cfd595ff5ec /runtime/lua/vim | |
| parent | ce678043e3461d45f1251979cf047dd529b117ea (diff) | |
| parent | a18fa2f11c3a66be8addc303efb5c8033a47f41e (diff) | |
| download | rneovim-fa6ab0d90958516d0bc1ed62839d85405ad08fa8.tar.gz rneovim-fa6ab0d90958516d0bc1ed62839d85405ad08fa8.tar.bz2 rneovim-fa6ab0d90958516d0bc1ed62839d85405ad08fa8.zip | |
Merge pull request #30825 from lewis6991/refactor/lsputil
Diffstat (limited to 'runtime/lua/vim')
| -rw-r--r-- | runtime/lua/vim/_meta/builtin_types.lua | 91 | ||||
| -rw-r--r-- | runtime/lua/vim/_meta/vimfn.lua | 4 | ||||
| -rw-r--r-- | runtime/lua/vim/lsp/_tagfunc.lua | 7 | ||||
| -rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 2 | ||||
| -rw-r--r-- | runtime/lua/vim/lsp/util.lua | 813 |
5 files changed, 481 insertions, 436 deletions
diff --git a/runtime/lua/vim/_meta/builtin_types.lua b/runtime/lua/vim/_meta/builtin_types.lua index aca6649957..eae76d80d7 100644 --- a/runtime/lua/vim/_meta/builtin_types.lua +++ b/runtime/lua/vim/_meta/builtin_types.lua @@ -66,6 +66,97 @@ --- @field winnr integer --- @field winrow integer +--- @class vim.quickfix.entry +--- buffer number; must be the number of a valid buffer +--- @field bufnr? integer +--- +--- name of a file; only used when "bufnr" is not +--- present or it is invalid. +--- @field filename? string +--- +--- name of a module; if given it will be used in +--- quickfix error window instead of the filename. +--- @field module? string +--- +--- line number in the file +--- @field lnum? integer +--- +--- end of lines, if the item spans multiple lines +--- @field end_lnum? integer +--- +--- search pattern used to locate the error +--- @field pattern? string +--- +--- column number +--- @field col? integer +--- +--- when non-zero: "col" is visual column +--- when zero: "col" is byte index +--- @field vcol? integer +--- +--- end column, if the item spans multiple columns +--- @field end_col? integer +--- +--- error number +--- @field nr? integer +--- +--- description of the error +--- @field text? string +--- +--- single-character error type, 'E', 'W', etc. +--- @field type? string +--- +--- recognized error message +--- @field valid? boolean +--- +--- custom data associated with the item, can be +--- any type. +--- @field user_data? any + +--- @class vim.fn.setqflist.what +--- +--- quickfix list context. See |quickfix-context| +--- @field context? table +--- +--- errorformat to use when parsing text from +--- "lines". If this is not present, then the +--- 'errorformat' option value is used. +--- See |quickfix-parse| +--- @field efm? string +--- +--- quickfix list identifier |quickfix-ID| +--- @field id? integer +--- index of the current entry in the quickfix +--- list specified by "id" or "nr". If set to '$', +--- then the last entry in the list is set as the +--- current entry. See |quickfix-index| +--- @field idx? integer +--- +--- list of quickfix entries. Same as the {list} +--- argument. +--- @field items? vim.quickfix.entry[] +--- +--- use 'errorformat' to parse a list of lines and +--- add the resulting entries to the quickfix list +--- {nr} or {id}. Only a |List| value is supported. +--- See |quickfix-parse| +--- @field lines? string[] +--- +--- list number in the quickfix stack; zero +--- means the current quickfix list and "$" means +--- the last quickfix list. +--- @field nr? integer +--- +--- function to get the text to display in the +--- quickfix window. The value can be the name of +--- a function or a funcref or a lambda. Refer +--- to |quickfix-window-function| for an explanation +--- of how to write the function and an example. +--- @field quickfixtextfunc? function +--- +--- quickfix list title text. See |quickfix-title| +--- @field title? string + --- @class vim.fn.sign_define.dict --- @field text string --- @field icon? string diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index d00bbe4770..f40a94a442 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -8286,9 +8286,9 @@ function vim.fn.setpos(expr, list) end --- independent of the 'errorformat' setting. Use a command like --- `:cc 1` to jump to the first position. --- ---- @param list any[] +--- @param list vim.quickfix.entry[] --- @param action? string ---- @param what? table +--- @param what? vim.fn.setqflist.what --- @return any function vim.fn.setqflist(list, action, what) end diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua index 4ad50e4a58..70917de48d 100644 --- a/runtime/lua/vim/lsp/_tagfunc.lua +++ b/runtime/lua/vim/lsp/_tagfunc.lua @@ -27,15 +27,20 @@ local function query_definition(pattern) return {} end local results = {} + + --- @param range lsp.Range + --- @param uri string + --- @param offset_encoding string 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(assert(results_by_client)) do local client = lsp.get_client_by_id(client_id) local offset_encoding = client and client.offset_encoding or 'utf-16' local result = lsp_results.result or {} if result.range then -- Location - add(result.range, result.uri) + add(result.range, result.uri, offset_encoding) else result = result --[[@as (lsp.Location[]|lsp.LocationLink[])]] for _, item in pairs(result) do diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 984222efbe..ccef314d86 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -338,6 +338,8 @@ function M.rename(new_name, opts) -- Compute early to account for cursor movements after going async local cword = vim.fn.expand('<cword>') + --- @param range lsp.Range + --- @param offset_encoding string local function get_text_at_range(range, offset_encoding) return api.nvim_buf_get_text( bufnr, diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 2b9e734c18..e3eef8901b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -5,9 +5,6 @@ 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 +18,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) - ) - ) - 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) - ) - ) + if not border_size[border] then + border_error(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 - return { height = height, width = width } + --- @param e string + local function border_height(e) + return #e > 0 and 1 or 0 + end + + 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,18 +110,18 @@ 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 +---@param index integer? byte index (utf-8), or `nil` for length +---@param encoding 'utf-8'|'utf-16'|'utf-32'? 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) @@ -242,6 +230,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) @@ -353,7 +343,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 @@ -363,7 +353,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 M._str_byteindex_enc(line, col, offset_encoding) end return col end @@ -371,14 +361,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 @@ -391,10 +380,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 @@ -406,8 +393,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 } @@ -438,47 +424,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 @@ -494,7 +478,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 @@ -512,7 +496,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 @@ -522,19 +506,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 @@ -552,6 +532,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 @@ -561,17 +544,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 @@ -615,19 +605,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) @@ -701,7 +685,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 @@ -709,16 +693,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) @@ -747,7 +733,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) @@ -784,7 +770,7 @@ end --- ---@param offset integer ---@param contents string[] ----@return { [1]: integer, [2]: integer } +---@return { [1]: integer, [2]: integer }? local function get_pos_from_offset(offset, contents) local i = 0 for l, line in ipairs(contents) do @@ -799,18 +785,18 @@ 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 Range4|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) --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_offset ---@type [integer, integer]|nil + 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 @@ -823,54 +809,33 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers -- 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 -- 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 = (signature.activeParameter or signature_help.activeParameter or 0) - if active_parameter < 0 then - active_parameter = 0 - end + 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] local parameter_label = parameter.label - --[=[ - --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 type(parameter_label) == 'table' then active_offset = parameter_label else - local offset = 1 ---@type integer|nil + 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) @@ -879,7 +844,9 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers end end for p, param in pairs(signature.parameters) do - offset = signature.label:find(param.label, offset, true) + 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 @@ -897,15 +864,15 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers local active_hl = nil if active_offset then - active_hl = {} -- Account for the start of the markdown block. if ft then - active_offset[1], active_offset[2] = - active_offset[1] + #contents[1], active_offset[2] + #contents[1] + active_offset[1] = active_offset[1] + #contents[1] + active_offset[2] = active_offset[2] + #contents[1] end - list_extend(active_hl, get_pos_from_offset(active_offset[1], contents)) - list_extend(active_hl, get_pos_from_offset(active_offset[2], contents)) + active_hl = {} + list_extend(active_hl, get_pos_from_offset(active_offset[1], contents) or {}) + list_extend(active_hl, get_pos_from_offset(active_offset[2], contents) or {}) end return contents, active_hl @@ -916,32 +883,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. +---@param opts? vim.lsp.util.open_floating_preview.Opts ---@return table Options 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, 'n', true) + validate('opts.offset_y', opts.offset_y, 'n', true) local anchor = '' - local row, col local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1 or vim.fn.winline() - 1 @@ -949,7 +899,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) @@ -960,7 +910,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) @@ -982,7 +933,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' @@ -1004,13 +955,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 @@ -1020,6 +979,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) @@ -1064,8 +1024,8 @@ end --- Jumps to a location. --- ---@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 @@ -1085,9 +1045,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 @@ -1114,7 +1074,7 @@ end local function find_window_by_var(name, value) for _, win in ipairs(api.nvim_list_wins()) do |