From f43fa301c1a2817239e046a242902af65b7cac71 Mon Sep 17 00:00:00 2001 From: Eduard Baturin Date: Sat, 18 Feb 2023 09:43:59 +0300 Subject: fix(lsp): check if the buffer is a directory before w! it (#22289) --- runtime/lua/vim/lsp/util.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 38051e6410..4beb4fc367 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -759,9 +759,11 @@ function M.rename(old_fname, new_fname, opts) vim.fn.bufload(oldbuf) -- The there may be pending changes in the buffer - api.nvim_buf_call(oldbuf, function() - vim.cmd('w!') - end) + if vim.fn.isdirectory(old_fname) == 0 then + api.nvim_buf_call(oldbuf, function() + vim.cmd('w!') + end) + end local ok, err = os.rename(old_fname, new_fname) assert(ok, err) -- cgit From 896d672736b32a8f4a4fa51844b44f266dcdcc6c Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Wed, 1 Mar 2023 15:33:13 +0100 Subject: fix(lsp): use buffer scheme for files not stored on disk (#22407) Sending `didOpen` with a `file` scheme causes problems with some language servers because they expect the file to exist on disk. See https://github.com/microsoft/language-server-protocol/pull/1679 --- runtime/lua/vim/lsp/util.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4beb4fc367..554e26022c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2032,7 +2032,12 @@ end ---@returns `TextDocumentIdentifier` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier function M.make_text_document_params(bufnr) - return { uri = vim.uri_from_bufnr(bufnr or 0) } + bufnr = bufnr or 0 + local uri = vim.uri_from_bufnr(bufnr) + if not uv.fs_stat(api.nvim_buf_get_name(bufnr)) then + uri = uri:gsub('^file://', 'buffer://') + end + return { uri = uri } end --- Create the workspace params @@ -2065,7 +2070,7 @@ function M.make_formatting_params(options) insertSpaces = vim.bo.expandtab, }) return { - textDocument = { uri = vim.uri_from_bufnr(0) }, + textDocument = M.make_text_document_params(0), options = options, } end -- cgit From 706bcab75eaad2c370d61bf828531054439d3a3e Mon Sep 17 00:00:00 2001 From: Jaehwang Jung Date: Tue, 7 Mar 2023 15:17:52 +0900 Subject: docs(lsp): change type annotations from number → integer (#22510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- runtime/lua/vim/lsp/util.lua | 76 ++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 38 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 554e26022c..c9613dc7a7 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -121,9 +121,9 @@ end --- Convert byte index to `encoding` index. --- Convenience wrapper around vim.str_utfindex ---@param line string line to be indexed ----@param index number|nil byte index (utf-8), or `nil` for length +---@param index integer|nil byte index (utf-8), or `nil` for length ---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 ----@return number `encoding` index of `index` in `line` +---@return integer `encoding` index of `index` in `line` function M._str_utfindex_enc(line, index, encoding) if not encoding then encoding = 'utf-16' @@ -149,9 +149,9 @@ end --- Convenience wrapper around vim.str_byteindex ---Alternative to vim.str_byteindex that takes an encoding. ---@param line string line to be indexed ----@param index number UTF index +---@param index integer UTF index ---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 ----@return number byte (utf-8) index of `encoding` index `index` in `line` +---@return integer byte (utf-8) index of `encoding` index `index` in `line` function M._str_byteindex_enc(line, index, encoding) if not encoding then encoding = 'utf-16' @@ -239,9 +239,9 @@ end --- 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 a table mapping rows to lines +---@param bufnr integer bufnr to get the lines from +---@param rows integer[] zero-indexed line numbers +---@return table a table mapping rows to lines local function get_lines(bufnr, rows) rows = type(rows) == 'table' and rows or { rows } @@ -321,8 +321,8 @@ end --- 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 +---@param bufnr integer +---@param row integer zero-indexed line number ---@return string the line at row in filename local function get_line(bufnr, row) return get_lines(bufnr, { row })[row] @@ -386,7 +386,7 @@ end --- Applies a list of text edits to a buffer. ---@param text_edits table list of `TextEdit` objects ----@param bufnr number Buffer id +---@param bufnr integer Buffer id ---@param offset_encoding string 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) @@ -571,7 +571,7 @@ end --- document. --- ---@param text_document_edit table: a `TextDocumentEdit` object ----@param index number: Optional index of the edit, if from a list of edits (or nil, if not from a list) +---@param index integer: Optional index of the edit, if from a list of edits (or nil, if not from a list) ---@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 @@ -1009,11 +1009,11 @@ end --- Creates a table with sensible default options for a floating window. The --- table can be passed to |nvim_open_win()|. --- ----@param width (number) window width (in character cells) ----@param height (number) window height (in character cells) +---@param width (integer) window width (in character cells) +---@param height (integer) window height (in character cells) ---@param opts (table, optional) ---- - offset_x (number) offset to add to `col` ---- - offset_y (number) offset to add to `row` +--- - 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 @@ -1429,7 +1429,7 @@ end ---@private --- Closes the preview window --- ----@param winnr number window id of preview window +---@param winnr integer window id of preview window ---@param bufnrs table|nil optional list of ignored buffers local function close_preview_window(winnr, bufnrs) vim.schedule(function() @@ -1448,7 +1448,7 @@ end --- Creates autocommands to close a preview window when events happen. --- ---@param events table list of events ----@param winnr number window id of preview window +---@param winnr integer window id of preview window ---@param bufnrs table list of buffers where the preview window will remain visible ---@see |autocmd-events| local function close_preview_autocmd(events, winnr, bufnrs) @@ -1556,14 +1556,14 @@ end ---@param contents table of lines to show in window ---@param syntax string of syntax to set for opened buffer ---@param opts table with optional fields (additional keys are passed on to |nvim_open_win()|) ---- - height: (number) height of floating window ---- - width: (number) width of floating window +--- - height: (integer) height of floating window +--- - width: (integer) width of floating window --- - wrap: (boolean, default true) wrap long lines ---- - wrap_at: (number) 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 +--- - wrap_at: (integer) character to wrap at for computing height when wrap is enabled +--- - max_width: (integer) maximal width of floating window +--- - max_height: (integer) maximal height of floating window +--- - pad_top: (integer) number of lines to pad contents at top +--- - pad_bottom: (integer) 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 @@ -1672,7 +1672,7 @@ do --[[ References ]] --- Removes document highlights from a buffer. --- - ---@param bufnr number Buffer id + ---@param bufnr integer Buffer id function M.buf_clear_references(bufnr) validate({ bufnr = { bufnr, 'n', true } }) api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1) @@ -1680,7 +1680,7 @@ do --[[ References ]] --- Shows a list of document highlights for a certain buffer. --- - ---@param bufnr number Buffer id + ---@param bufnr integer Buffer id ---@param references table List of `DocumentHighlight` objects to highlight ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32". ---@see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent @@ -1893,7 +1893,7 @@ function M.try_trim_markdown_code_blocks(lines) end ---@private ----@param window number|nil: window handle or 0 for current, defaults to current +---@param window integer|nil: window handle or 0 for current, defaults to current ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` local function make_position_param(window, offset_encoding) window = window or 0 @@ -1913,7 +1913,7 @@ end --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. --- ----@param window number|nil: window handle or 0 for current, defaults to current +---@param window integer|nil: window handle or 0 for current, defaults to current ---@param offset_encoding string|nil utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` ---@returns `TextDocumentPositionParams` object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams @@ -1928,7 +1928,7 @@ function M.make_position_params(window, offset_encoding) end --- Utility function for getting the encoding of the first LSP client on the given buffer. ----@param bufnr (number) buffer handle or 0 for current, defaults to current +---@param bufnr (integer) buffer handle or 0 for current, defaults to current ---@returns (string) encoding first client if there is one, nil otherwise function M._get_offset_encoding(bufnr) validate({ @@ -1966,7 +1966,7 @@ end --- `textDocument/codeAction`, `textDocument/colorPresentation`, --- `textDocument/rangeFormatting`. --- ----@param window number|nil: window handle or 0 for current, defaults to current +---@param window integer|nil: window handle or 0 for current, defaults to current ---@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of buffer of `window` ---@returns { textDocument = { uri = `current_file_uri` }, range = { start = ---`current_position`, end = `current_position` } } @@ -1983,11 +1983,11 @@ end --- Using the given range in the current buffer, creates an object that --- is similar to |vim.lsp.util.make_range_params()|. --- ----@param start_pos number[]|nil {row, col} mark-indexed position. +---@param start_pos integer[]|nil {row, col} mark-indexed position. --- Defaults to the start of the last visual selection. ----@param end_pos number[]|nil {row, col} mark-indexed position. +---@param end_pos integer[]|nil {row, col} mark-indexed position. --- Defaults to the end of the last visual selection. ----@param bufnr number|nil buffer handle or 0 for current, defaults to current +---@param bufnr integer|nil buffer handle or 0 for current, defaults to current ---@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of `bufnr` ---@returns { textDocument = { uri = `current_file_uri` }, range = { start = ---`start_position`, end = `end_position` } } @@ -2028,7 +2028,7 @@ end --- Creates a `TextDocumentIdentifier` object for the current buffer. --- ----@param bufnr number|nil: Buffer handle, defaults to current +---@param bufnr integer|nil: Buffer handle, defaults to current ---@returns `TextDocumentIdentifier` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier function M.make_text_document_params(bufnr) @@ -2049,8 +2049,8 @@ end --- Returns indentation size. --- ---@see 'shiftwidth' ----@param bufnr (number|nil): Buffer handle, defaults to current ----@returns (number) indentation size +---@param bufnr (integer|nil): Buffer handle, defaults to current +---@returns (integer) indentation size function M.get_effective_tabstop(bufnr) validate({ bufnr = { bufnr, 'n', true } }) local bo = bufnr and vim.bo[bufnr] or vim.bo @@ -2077,11 +2077,11 @@ end --- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer. --- ----@param buf number buffer number (0 for current) +---@param buf integer buffer number (0 for current) ---@param row 0-indexed line ---@param col 0-indexed byte offset in line ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `buf` ----@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf} +---@returns (integer, integer) `offset_encoding` index of the character in line {row} column {col} in buffer {buf} function M.character_offset(buf, row, col, offset_encoding) local line = get_line(buf, row) if offset_encoding == nil then -- cgit From 236c20795eb9f11e21e0719b735ea741711acc08 Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Sat, 11 Mar 2023 07:35:23 +0100 Subject: revert: "fix(lsp): use buffer scheme for files not stored on disk" (#22604) Although using `buffer://` for unsaved file buffers fixes issues with language servers like eclipse.jdt.ls or ansible-language-server, it breaks completion and signature help for clangd. A regression is worse than a fix for something else, so this reverts commit 896d672736b32a8f4a4fa51844b44f266dcdcc6c. The spec change is also still in dicussion, see https://github.com/microsoft/language-server-protocol/pull/1679#discussion_r1130704886 --- runtime/lua/vim/lsp/util.lua | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index c9613dc7a7..342fad33c2 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2032,12 +2032,7 @@ end ---@returns `TextDocumentIdentifier` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier function M.make_text_document_params(bufnr) - bufnr = bufnr or 0 - local uri = vim.uri_from_bufnr(bufnr) - if not uv.fs_stat(api.nvim_buf_get_name(bufnr)) then - uri = uri:gsub('^file://', 'buffer://') - end - return { uri = uri } + return { uri = vim.uri_from_bufnr(bufnr or 0) } end --- Create the workspace params @@ -2070,7 +2065,7 @@ function M.make_formatting_params(options) insertSpaces = vim.bo.expandtab, }) return { - textDocument = M.make_text_document_params(0), + textDocument = { uri = vim.uri_from_bufnr(0) }, options = options, } end -- cgit From 8dde7c907ca9ad365895bded2c2f59e08f65d3ed Mon Sep 17 00:00:00 2001 From: hrsh7th <629908+hrsh7th@users.noreply.github.com> Date: Tue, 14 Mar 2023 20:59:43 +0900 Subject: fix(lsp): vim.lsp.util.apply_text_edits cursor validation #22636 Problem Using wrong variable when checking the cursor position is valid or not in vim.lsp.util.apply_text_edits. Solution Use the correct variable. --- runtime/lua/vim/lsp/util.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 342fad33c2..82c9e3bc87 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -436,7 +436,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- Some LSP servers are depending on the VSCode behavior. -- The VSCode will re-locate the cursor position after applying TextEdit so we also do it. - local is_current_buf = api.nvim_get_current_buf() == bufnr + local is_current_buf = api.nvim_get_current_buf() == bufnr or bufnr == 0 local cursor = (function() if not is_current_buf then return { @@ -464,7 +464,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) 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', true), + text = split(text_edit.newText, '\n', { plain = true }), } local max = api.nvim_buf_line_count(bufnr) @@ -522,7 +522,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) if is_cursor_fixed then local is_valid_cursor = true is_valid_cursor = is_valid_cursor and cursor.row < max - is_valid_cursor = is_valid_cursor and cursor.col <= #(get_line(bufnr, max - 1) or '') + is_valid_cursor = is_valid_cursor and cursor.col <= #(get_line(bufnr, cursor.row) or '') if is_valid_cursor then api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col }) end -- cgit From 4f7879dff0f0dc22ddf4cb2a2095b88605a3bab0 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 14 Mar 2023 13:08:37 +0100 Subject: fix(lsp): kill buffers after renaming a directory #22618 Problem: When LSP client renames a directory, opened buffers in the edfitor are not renamed or closed. Then `:wall` shows errors. https://github.com/neovim/neovim/blob/master/runtime/lua/vim/lsp/util.lua#L776 works correctly if you try to rename a single file, but doesn't delete old buffers with `old_fname` is a dir. Solution: Update the logic in runtime/lua/vim/lsp/util.lua:rename() Fixes #22617 --- runtime/lua/vim/lsp/util.lua | 48 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 10 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 82c9e3bc87..48faddfce1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -743,6 +743,20 @@ local function bufwinid(bufnr) end end +--- Get list of buffers for a directory +---@private +local function get_dir_bufs(path) + path = path:gsub('([^%w])', '%%%1') + local buffers = {} + for _, v in ipairs(vim.api.nvim_list_bufs()) do + local bufname = vim.api.nvim_buf_get_name(v):gsub('buffer://', '') + if bufname:find(path) then + table.insert(buffers, v) + end + end + return buffers +end + --- Rename old_fname to new_fname --- ---@param opts (table) @@ -755,12 +769,22 @@ function M.rename(old_fname, new_fname, opts) vim.notify('Rename target already exists. Skipping rename.') return end - local oldbuf = vim.fn.bufadd(old_fname) - vim.fn.bufload(oldbuf) - -- The there may be pending changes in the buffer - if vim.fn.isdirectory(old_fname) == 0 then - api.nvim_buf_call(oldbuf, function() + local oldbufs = {} + local win = nil + + if vim.fn.isdirectory(old_fname) == 1 then + oldbufs = get_dir_bufs(old_fname) + else + local oldbuf = vim.fn.bufadd(old_fname) + table.insert(oldbufs, oldbuf) + win = bufwinid(oldbuf) + end + + for _, b in ipairs(oldbufs) do + vim.fn.bufload(b) + -- The there may be pending changes in the buffer + api.nvim_buf_call(b, function() vim.cmd('w!') end) end @@ -768,12 +792,16 @@ function M.rename(old_fname, new_fname, opts) local ok, err = os.rename(old_fname, new_fname) assert(ok, err) - local newbuf = vim.fn.bufadd(new_fname) - local win = bufwinid(oldbuf) - if win then - api.nvim_win_set_buf(win, newbuf) + if vim.fn.isdirectory(new_fname) == 0 then + local newbuf = vim.fn.bufadd(new_fname) + if win then + api.nvim_win_set_buf(win, newbuf) + end + end + + for _, b in ipairs(oldbufs) do + api.nvim_buf_delete(b, {}) end - api.nvim_buf_delete(oldbuf, { force = true }) end ---@private -- cgit From 257d894d75bc583bb16f4dbe441907eb273d20ad Mon Sep 17 00:00:00 2001 From: Roberto Pommella Alegro Date: Sat, 25 Mar 2023 13:46:07 -0300 Subject: feat(lsp): render markdown in docs hover #22766 Problem: LSP docs hover (textDocument/hover) doesn't handle HTML escape seqs in markdown. Solution: Convert common HTML escape seqs to a nicer form, to display in the float. closees #22757 Signed-off-by: Kasama --- runtime/lua/vim/lsp/util.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 48faddfce1..ebde7af16c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1378,6 +1378,20 @@ function M.stylize_markdown(bufnr, contents, opts) end end + -- Handle some common html escape sequences + stripped = vim.tbl_map(function(line) + local escapes = { + ['>'] = '>', + ['<'] = '<', + ['"'] = '"', + ['''] = "'", + [' '] = ' ', + [' '] = ' ', + ['&'] = '&', + } + return (string.gsub(line, '&[^ ;]+;', escapes)) + end, stripped) + -- Compute size of float needed to show (wrapped) lines opts.wrap_at = opts.wrap_at or (vim.wo['wrap'] and api.nvim_win_get_width(0)) local width = M._make_floating_popup_size(stripped, opts) -- cgit From bfb28b62dab756ec76a73506c2070ddf491a0cdd Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Thu, 13 Apr 2023 15:29:13 -0600 Subject: refactor: remove modelines from Lua files Now that we have builtin EditorConfig support and a formatting check in CI, these are not necessary. --- runtime/lua/vim/lsp/util.lua | 1 - 1 file changed, 1 deletion(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index ebde7af16c..dca258e4b9 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2154,4 +2154,3 @@ M._get_line_byte_from_position = get_line_byte_from_position M.buf_versions = {} return M --- vim:sw=2 ts=2 et -- cgit From 4d04feb6629cb049cb2a13ba35f0c8d3c6b67ff4 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Fri, 14 Apr 2023 10:39:57 +0200 Subject: feat(lua): vim.tbl_contains supports general tables and predicates (#23040) * feat(lua): vim.tbl_contains supports general tables and predicates Problem: `vim.tbl_contains` only works for list-like tables (integer keys without gaps) and primitive values (in particular, not for nested tables). Solution: Rename `vim.tbl_contains` to `vim.list_contains` and add new `vim.tbl_contains` that works for general tables and optionally allows `value` to be a predicate function that is checked for every key. --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index dca258e4b9..31af2afb0b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1476,7 +1476,7 @@ end local function close_preview_window(winnr, bufnrs) vim.schedule(function() -- exit if we are in one of ignored buffers - if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then + if bufnrs and vim.list_contains(bufnrs, api.nvim_get_current_buf()) then return end -- cgit From 648f777931d49b8013146f69d7e2776f69c52900 Mon Sep 17 00:00:00 2001 From: jdrouhard Date: Fri, 5 May 2023 00:41:36 -0500 Subject: perf(lsp): load buffer contents once when processing semantic tokens responses (#23484) perf(lsp): load buffer contents once when processing semantic token responses Using _get_line_byte_from_position() for each token's boundaries was a pretty huge bottleneck, since that function would load individual buffer lines via nvim_buf_get_lines() (plus a lot of extra overhead). So each token caused two calls to nvim_buf_get_lines() (once for the start position, and once for the end position). For semantic tokens, we only attach to buffers that have already been loaded, so we can safely just get all the lines for the entire buffer at once, and lift the rest of the _get_line_byte_from_position() implementation directly while bypassing the part that loads the buffer line. While I was looking at get_lines (used by _get_line_byte_from_position), I noticed that we were checking for non-file URIs before we even looked to see if we already had the buffer loaded. Moving the buffer-loaded check to be the first thing done in get_lines() more than halved the average time spent transforming the token list into highlight ranges vs when it was still using _get_line_byte_from_position. I ended up improving that loop more by not using get_lines, but figured the performance improvement it provided was worth leaving in. --- runtime/lua/vim/lsp/util.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 31af2afb0b..8274361f6d 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -253,12 +253,17 @@ local function get_lines(bufnr, rows) ---@private local function buf_lines() local lines = {} - for _, row in pairs(rows) do + for _, row in ipairs(rows) do lines[row] = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1] end return lines end + -- use loaded buffers if available + if vim.fn.bufloaded(bufnr) == 1 then + return buf_lines() + end + local uri = vim.uri_from_bufnr(bufnr) -- load the buffer if this is not a file uri @@ -268,11 +273,6 @@ local function get_lines(bufnr, rows) 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 -- cgit From 1fe1bb084d0099fc4f9bfdc11189485d0f74b75a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 19 Dec 2022 16:37:45 +0000 Subject: refactor(options): deprecate nvim[_buf|_win]_[gs]et_option Co-authored-by: zeertzjq Co-authored-by: famiu --- runtime/lua/vim/lsp/util.lua | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 8274361f6d..9fffc845b1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -401,7 +401,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) if not api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) end - api.nvim_buf_set_option(bufnr, 'buflisted', true) + vim.bo[bufnr].buflisted = true -- Fix reversed range and indexing each text_edits local index = 0 @@ -530,11 +530,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- Remove final line if needed local fix_eol = has_eol_text_edit - fix_eol = fix_eol - and ( - api.nvim_buf_get_option(bufnr, 'eol') - or (api.nvim_buf_get_option(bufnr, 'fixeol') and not api.nvim_buf_get_option(bufnr, 'binary')) - ) + fix_eol = fix_eol and (vim.bo[bufnr].eol or (vim.bo[bufnr].fixeol and not vim.bo[bufnr].binary)) fix_eol = fix_eol and get_line(bufnr, max - 1) == '' if fix_eol then api.nvim_buf_set_lines(bufnr, -2, -1, false, {}) @@ -1076,7 +1072,7 @@ function M.make_floating_popup_options(width, height, opts) local wincol = opts.relative == 'mouse' and vim.fn.getmousepos().column or vim.fn.wincol() - if wincol + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then + if wincol + width + (opts.offset_x or 0) <= vim.o.columns then anchor = anchor .. 'W' col = 0 else @@ -1142,7 +1138,7 @@ function M.show_document(location, offset_encoding, opts) or focus and api.nvim_get_current_win() or create_window_without_focus() - api.nvim_buf_set_option(bufnr, 'buflisted', true) + vim.bo[bufnr].buflisted = true api.nvim_win_set_buf(win, bufnr) if focus then api.nvim_set_current_win(win) @@ -1201,12 +1197,12 @@ function M.preview_location(location, opts) end local range = location.targetRange or location.range local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range['end'].line + 1, false) - local syntax = api.nvim_buf_get_option(bufnr, 'syntax') + local syntax = vim.bo[bufnr].syntax if syntax == '' then -- When no syntax is set, we use filetype as fallback. This might not result -- in a valid syntax definition. See also ft detection in stylize_markdown. -- An empty syntax is more common now with TreeSitter, since TS disables syntax. - syntax = api.nvim_buf_get_option(bufnr, 'filetype') + syntax = vim.bo[bufnr].filetype end opts = opts or {} opts.focus_id = 'location' @@ -1665,7 +1661,7 @@ function M.open_floating_preview(contents, syntax, opts) contents = M.stylize_markdown(floating_bufnr, contents, opts) else if syntax then - api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax) + vim.bo[floating_bufnr].syntax = syntax end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) end @@ -1681,16 +1677,16 @@ function M.open_floating_preview(contents, syntax, opts) local float_option = M.make_floating_popup_options(width, height, opts) local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) if do_stylize then - api.nvim_win_set_option(floating_winnr, 'conceallevel', 2) - api.nvim_win_set_option(floating_winnr, 'concealcursor', 'n') + vim.wo[floating_winnr].conceallevel = 2 + vim.wo[floating_winnr].concealcursor = 'n' end -- disable folding - api.nvim_win_set_option(floating_winnr, 'foldenable', false) + vim.wo[floating_winnr].foldenable = false -- soft wrapping - api.nvim_win_set_option(floating_winnr, 'wrap', opts.wrap) + vim.wo[floating_winnr].wrap = opts.wrap - api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) - api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe') + vim.bo[floating_bufnr].modifiable = false + vim.bo[floating_bufnr].bufhidden = 'wipe' api.nvim_buf_set_keymap( floating_bufnr, 'n', -- cgit From 2db719f6c2b677fcbc197b02fe52764a851523b2 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 3 Jun 2023 11:06:00 +0100 Subject: feat(lua): rename vim.loop -> vim.uv (#22846) --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 9fffc845b1..53f8dba814 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -4,7 +4,7 @@ local validate = vim.validate local api = vim.api local list_extend = vim.list_extend local highlight = require('vim.highlight') -local uv = vim.loop +local uv = vim.uv local npcall = vim.F.npcall local split = vim.split -- cgit From 5282d3299c9b1b07f3e02a9014bc2632cf3b4fed Mon Sep 17 00:00:00 2001 From: Folke Lemaitre Date: Mon, 5 Jun 2023 01:45:01 +0200 Subject: fix(lsp): restore marks after apply_text_edits() #14630 PROBLEM: Whenever any text edits are applied to the buffer, the `marks` part of those lines will be lost. This is mostly problematic for code formatters that format the whole buffer like `prettier`, `luafmt`, ... When doing atomic changes inside a vim doc, vim keeps track of those changes and can update the positions of marks accordingly, but in this case we have a whole doc that changed. There's no simple way to update the positions of all marks from the previous document state to the new document state. SOLUTION: * save marks right before `nvim_buf_set_lines` is called inside `apply_text_edits` * check if any marks were lost after doing `nvim_buf_set_lines` * restore those marks to the previous positions TEST CASE: * have a formatter enabled * open any file * create a couple of marks * indent the whole file to the right * save the file Before this change: all marks will be removed. After this change: they will be preserved. Fixes #14307 --- runtime/lua/vim/lsp/util.lua | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 53f8dba814..ba8c72128e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -451,6 +451,14 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) } end)() + -- save and restore local marks since they get deleted by nvim_buf_set_lines + local marks = {} + for _, m in pairs(vim.fn.getmarklist(bufnr or vim.api.nvim_get_current_buf())) do + if m.mark:match("^'[a-z]$") then + marks[m.mark:sub(2, 2)] = { m.pos[2], m.pos[3] - 1 } -- api-indexed + end + end + -- Apply text edits. local is_cursor_fixed = false local has_eol_text_edit = false @@ -518,6 +526,20 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) local max = api.nvim_buf_line_count(bufnr) + -- no need to restore marks that still exist + for _, m in pairs(vim.fn.getmarklist(bufnr or vim.api.nvim_get_current_buf())) do + marks[m.mark:sub(2, 2)] = nil + end + -- restore marks + for mark, pos in pairs(marks) do + if pos then + -- 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], {}) + end + end + -- Apply fixed cursor position. if is_cursor_fixed then local is_valid_cursor = true -- cgit From 3c6d971e5488dc75b7db07c14d01f87827f28a67 Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 5 Jun 2023 13:17:38 +0800 Subject: fix(lsp): set extra info only when it has a value (#23868) --- runtime/lua/vim/lsp/util.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index ba8c72128e..e36014d07d 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -716,15 +716,18 @@ function M.text_document_completion_list_to_complete_items(result, prefix) local matches = {} for _, completion_item in ipairs(items) do - local info = ' ' + local info = '' local documentation = completion_item.documentation if documentation then if type(documentation) == 'string' and documentation ~= '' then info = documentation elseif type(documentation) == 'table' and type(documentation.value) == 'string' then info = documentation.value - -- else - -- TODO(ashkan) Validation handling here? + else + vim.notify( + ('invalid documentation value %s'):format(vim.inspect(documentation)), + vim.log.levels.WARN + ) end end @@ -734,7 +737,7 @@ function M.text_document_completion_list_to_complete_items(result, prefix) abbr = completion_item.label, kind = M._get_completion_item_kind_name(completion_item.kind), menu = completion_item.detail or '', - info = info, + info = #info > 0 and info or nil, icase = 1, dup = 1, empty = 1, -- cgit From e5e0bda41b640d324350c5147b956e37e9f8b32c Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Fri, 9 Jun 2023 11:32:43 +0200 Subject: feat(lsp)!: add vim.lsp.status, client.progress and promote LspProgressUpdate (#23958) `client.messages` could grow unbounded because the default handler only added new messages, never removing them. A user either had to consume the messages by calling `vim.lsp.util.get_progress_messages` or by manually removing them from `client.messages.progress`. If they didn't do that, using LSP effectively leaked memory. To fix this, this deprecates the `messages` property and instead adds a `progress` ring buffer that only keeps at most 50 messages. In addition it deprecates `vim.lsp.util.get_progress_messages` in favour of a new `vim.lsp.status()` and also promotes the `LspProgressUpdate` user autocmd to a regular autocmd to allow users to pattern match on the progress kind. Also closes https://github.com/neovim/neovim/pull/20327 --- runtime/lua/vim/lsp/util.lua | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index e36014d07d..538e48c805 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -353,11 +353,40 @@ end --- Process and return progress reports from lsp server ---@private +---@deprecated Use vim.lsp.status() or access client.progress directly function M.get_progress_messages() + vim.deprecate('vim.lsp.util.get_progress_messages', 'vim.lsp.status', '0.11.0') local new_messages = {} local progress_remove = {} for _, client in ipairs(vim.lsp.get_active_clients()) do + local groups = {} + for progress in client.progress do + local value = progress.value + if type(value) == 'table' and value.kind then + local group = groups[progress.token] + if not group then + group = { + done = false, + progress = true, + title = 'empty title', + } + groups[progress.token] = group + end + group.title = value.title or group.title + group.cancellable = value.cancellable or group.cancellable + if value.kind == 'end' then + group.done = true + end + group.message = value.message or group.message + group.percentage = value.percentage or group.percentage + end + end + + for _, group in pairs(groups) do + table.insert(new_messages, group) + end + local messages = client.messages local data = messages for token, ctx in pairs(data.progress) do -- cgit From 036da0d07921e67090d1a62c9a4e382ca09d8584 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 24 Jun 2023 13:47:10 +0200 Subject: fix(docs): vimdoc syntax errors gen_help_html: truncate parse-error sample text --- runtime/lua/vim/lsp/util.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 538e48c805..4c319e7c41 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -178,8 +178,8 @@ local _str_byteindex_enc = M._str_byteindex_enc --- CAUTION: Changes in-place! --- ---@param lines (table) Original list of strings ----@param A (table) Start position; a 2-tuple of {line, col} numbers ----@param B (table) End position; a 2-tuple of {line, col} numbers +---@param A (table) Start position; a 2-tuple of {line,col} numbers +---@param B (table) End position; a 2-tuple of {line,col} numbers ---@param new_lines A list of strings to replace the original ---@returns (table) The modified {lines} object function M.set_lines(lines, A, B, new_lines) @@ -2075,9 +2075,9 @@ end --- Using the given range in the current buffer, creates an object that --- is similar to |vim.lsp.util.make_range_params()|. --- ----@param start_pos integer[]|nil {row, col} mark-indexed position. +---@param start_pos integer[]|nil {row,col} mark-indexed position. --- Defaults to the start of the last visual selection. ----@param end_pos integer[]|nil {row, col} mark-indexed position. +---@param end_pos integer[]|nil {row,col} mark-indexed position. --- Defaults to the end of the last visual selection. ---@param bufnr integer|nil buffer handle or 0 for current, defaults to current ---@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of `bufnr` -- cgit From ba8f19ebb67ca27d746f4b1cd902ab3d807eace3 Mon Sep 17 00:00:00 2001 From: Raphael Date: Sat, 1 Jul 2023 18:42:37 +0800 Subject: fix(lsp): lint warnings, default offset_encoding #24046 - fix lint / analysis warnings - locations_to_items(): get default offset_encoding from active client - character_offset(): get default offset_encoding from active client --- runtime/lua/vim/lsp/util.lua | 134 +++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 63 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4c319e7c41..6cb91417f2 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -25,9 +25,9 @@ local default_border = { ---@private --- 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 +---@param opts table optional options for the floating window --- - border (string or table) the border ----@returns (table) size of border in the form of { height = height, width = width } +---@return table size of border in the form of { height = height, width = width } local function get_border_size(opts) local border = opts and opts.border or default_border local height = 0 @@ -106,7 +106,7 @@ end ---@private local function split_lines(value) value = string.gsub(value, '\r\n?', '\n') - return split(value, '\n', true) + return split(value, '\n', { plain = true }) end ---@private @@ -122,7 +122,7 @@ end --- 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 string utf-8|utf-16|utf-32|nil defaults to utf-16 +---@param encoding string|nil 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) if not encoding then @@ -150,7 +150,7 @@ end ---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|nil defaults to utf-16 +---@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) if not encoding then @@ -180,8 +180,8 @@ local _str_byteindex_enc = M._str_byteindex_enc ---@param lines (table) Original list of strings ---@param A (table) Start position; a 2-tuple of {line,col} numbers ---@param B (table) End position; a 2-tuple of {line,col} numbers ----@param new_lines A list of strings to replace the original ----@returns (table) The modified {lines} object +---@param new_lines (table) list of strings to replace the original +---@return table The modified {lines} object function M.set_lines(lines, A, B, new_lines) -- 0-indexing to 1-indexing local i_0 = A[1] + 1 @@ -241,7 +241,7 @@ end --- ---@param bufnr integer bufnr to get the lines from ---@param rows integer[] zero-indexed line numbers ----@return table a table mapping rows to lines +---@return table|string a table mapping rows to lines local function get_lines(bufnr, rows) rows = type(rows) == 'table' and rows or { rows } @@ -331,8 +331,9 @@ end ---@private --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position --- Returns a zero-indexed column, since set_lines() does the conversion to ----@param offset_encoding string utf-8|utf-16|utf-32 +---@param offset_encoding string|nil utf-8|utf-16|utf-32 --- 1-indexed +---@return integer local function get_line_byte_from_position(bufnr, position, offset_encoding) -- LSP's line and characters are 0-indexed -- Vim's line and columns are 1-indexed @@ -598,8 +599,8 @@ end --- Can be used to extract the completion items from a --- `textDocument/completion` request, which may return one of --- `CompletionItem[]`, `CompletionList` or null. ----@param result (table) The result of a `textDocument/completion` request ----@returns (table) List of completion items +---@param result table The result of a `textDocument/completion` request +---@return table List of completion items ---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion function M.extract_completion_items(result) if type(result) == 'table' and result.items then @@ -658,7 +659,7 @@ end --- Parses snippets in a completion entry. --- ---@param input string unparsed snippet ----@returns string parsed snippet +---@return string parsed snippet function M.parse_snippet(input) local ok, parsed = pcall(function() return tostring(snippet.parse(input)) @@ -718,7 +719,7 @@ end --- specification. --- ---@param completion_item_kind (`vim.lsp.protocol.completionItemKind`) ----@returns (`vim.lsp.protocol.completionItemKind`) +---@return (`vim.lsp.protocol.completionItemKind`) ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion function M._get_completion_item_kind_name(completion_item_kind) return protocol.CompletionItemKind[completion_item_kind] or 'Unknown' @@ -727,12 +728,12 @@ end --- Turns the result of a `textDocument/completion` request into vim-compatible --- |complete-items|. --- ----@param result The result of a `textDocument/completion` call, e.g. from +---@param result table The result of a `textDocument/completion` call, e.g. from ---|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`, --- `CompletionList` or `null` ---@param prefix (string) the prefix to filter the completion items ----@returns { matches = complete-items table, incomplete = bool } ----@see |complete-items| +---@return table { matches = complete-items table, incomplete = bool } +---@see complete-items function M.text_document_completion_list_to_complete_items(result, prefix) local items = M.extract_completion_items(result) if vim.tbl_isempty(items) then @@ -937,7 +938,7 @@ end --- ---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`) ---@param contents (table|nil) List of strings to extend with converted lines. Defaults to {}. ----@returns {contents}, extended with lines of converted markdown. +---@return table {contents} 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) contents = contents or {} @@ -985,10 +986,11 @@ end --- Converts `textDocument/SignatureHelp` response to markdown lines. --- ----@param signature_help Response of `textDocument/SignatureHelp` ----@param ft optional filetype that will be use as the `lang` for the label markdown code block ----@param triggers optional list of trigger characters from the lsp server. used to better determine parameter offsets ----@returns list of lines of converted markdown. +---@param signature_help table 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 table|nil table list of lines of converted markdown. +---@return table|nil table of active hl ---@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 @@ -1015,7 +1017,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers -- wrap inside a code block so stylize_markdown can render it properly label = ('```%s\n%s\n```'):format(ft, label) end - list_extend(contents, split(label, '\n', true)) + list_extend(contents, split(label, '\n', { plain = true })) if signature.documentation then M.convert_input_to_markdown_lines(signature.documentation, contents) end @@ -1087,16 +1089,16 @@ end --- Creates a table with sensible default options for a floating window. The --- table can be passed to |nvim_open_win()|. --- ----@param width (integer) window width (in character cells) ----@param height (integer) window height (in character cells) ----@param opts (table, optional) +---@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" ----@returns (table) Options +---@return table Options function M.make_floating_popup_options(width, height, opts) validate({ opts = { opts, 't', true }, @@ -1160,7 +1162,7 @@ end --- Shows document and optionally jumps to the location. --- ---@param location table (`Location`|`LocationLink`) ----@param offset_encoding "utf-8" | "utf-16" | "utf-32" +---@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. @@ -1217,7 +1219,7 @@ end --- Jumps to a location. --- ---@param location table (`Location`|`LocationLink`) ----@param offset_encoding "utf-8" | "utf-16" | "utf-32" +---@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. ---@return boolean `true` if the jump succeeded function M.jump_to_location(location, offset_encoding, reuse_win) @@ -1237,8 +1239,9 @@ end --- - for Location, range is shown (e.g., function definition) --- - for LocationLink, targetRange is shown (e.g., body of function definition) --- ----@param location a single `Location` or `LocationLink` ----@returns (bufnr,winnr) buffer and window number of floating window or nil +---@param location table a single `Location` or `LocationLink` +---@return integer|nil buffer id of float window +---@return integer|nil 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 @@ -1275,10 +1278,10 @@ end --- Trims empty lines from input and pad top and bottom with empty lines --- ---@param contents table of lines to trim and pad ----@param opts dictionary with optional fields +---@param opts table with optional fields --- - pad_top number of lines to pad contents at top (default 0) --- - pad_bottom number of lines to pad contents at bottom (default 0) ----@return contents table of trimmed and padded lines +---@return table table of trimmed and padded lines function M._trim(contents, opts) validate({ contents = { contents, 't' }, @@ -1301,7 +1304,7 @@ end --- Generates a table mapping markdown code block lang to vim syntax, --- based on g:markdown_fenced_languages ----@return a table of lang -> syntax mappings +---@return table table of lang -> syntax mappings ---@private local function get_markdown_fences() local fences = {} @@ -1324,7 +1327,7 @@ end --- If you want to open a popup with fancy markdown, use `open_floating_preview` instead --- ---@param contents table of lines to show in window ----@param opts dictionary 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 @@ -1333,7 +1336,7 @@ end --- - pad_top number of lines to pad contents at top --- - pad_bottom number of lines to pad contents at bottom --- - separator insert separator after code block ----@returns width,height size of float +---@return table stripped content function M.stylize_markdown(bufnr, contents, opts) validate({ contents = { contents, 't' }, @@ -1480,10 +1483,10 @@ function M.stylize_markdown(bufnr, contents, opts) if not langs[lang] then -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set pcall(api.nvim_buf_del_var, bufnr, 'current_syntax') - -- TODO(ashkan): better validation before this. - if not pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft)) then + if #api.nvim_get_runtime_file(('syntax/%s.vim'):format(ft), true) == 0 then return end + vim.cmd(string.format('syntax include %s syntax/%s.vim', lang, ft)) langs[lang] = true end vim.cmd( @@ -1542,7 +1545,7 @@ end ---@param events table list of events ---@param winnr integer window id of preview window ---@param bufnrs table list of buffers where the preview window will remain visible ----@see |autocmd-events| +---@see autocmd-events local function close_preview_autocmd(events, winnr, bufnrs) local augroup = api.nvim_create_augroup('preview_window_' .. winnr, { clear = true, @@ -1572,13 +1575,14 @@ end --- Computes size of float needed to show contents (with optional wrapping) --- ---@param contents table of lines to show in window ----@param opts dictionary 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 --- - max_width maximal width of floating window --- - max_height maximal height of floating window ----@returns width,height size of float +---@return integer width size of float +---@return integer height size of float function M._make_floating_popup_size(contents, opts) validate({ contents = { contents, 't' }, @@ -1662,7 +1666,8 @@ end --- - 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 +---@return integer bufnr of newly created float window +---@return integer winid of newly created float window ---preview window function M.open_floating_preview(contents, syntax, opts) validate({ @@ -1766,7 +1771,7 @@ do --[[ References ]] --- ---@param bufnr integer Buffer id function M.buf_clear_references(bufnr) - validate({ bufnr = { bufnr, 'n', true } }) + validate({ bufnr = { bufnr, { 'n', 'nil' }, true } }) api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1) end @@ -1828,13 +1833,15 @@ end) --- ---@param locations table list of `Location`s or `LocationLink`s ---@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32 ----@returns (table) list of items +--- default to first client of buffer +---@return table list of items function M.locations_to_items(locations, offset_encoding) if offset_encoding == nil then vim.notify_once( 'locations_to_items must be called with valid offset encoding', vim.log.levels.WARN ) + offset_encoding = vim.lsp.get_active_clients({ bufnr = 0 })[1].offset_encoding end local items = {} @@ -1896,7 +1903,7 @@ end --- Converts symbols to quickfix list items. --- ----@param symbols DocumentSymbol[] or SymbolInformation[] +---@param symbols table DocumentSymbol[] or SymbolInformation[] function M.symbols_to_items(symbols, bufnr) ---@private local function _symbols_to_items(_symbols, _items, _bufnr) @@ -1936,8 +1943,8 @@ function M.symbols_to_items(symbols, bufnr) end --- Removes empty lines from the beginning and end. ----@param lines (table) list of lines to trim ----@returns (table) trimmed list of lines +---@param lines table list of lines to trim +---@return table trimmed list of lines function M.trim_empty_lines(lines) local start = 1 for i = 1, #lines do @@ -1961,8 +1968,8 @@ end --- --- CAUTION: Modifies the input in-place! --- ----@param lines (table) list of lines ----@returns (string) filetype or "markdown" if it was unchanged. +---@param lines table list of lines +---@return string filetype or "markdown" if it was unchanged. function M.try_trim_markdown_code_blocks(lines) local language_id = lines[1]:match('^```(.*)') if language_id then @@ -2007,7 +2014,7 @@ end --- ---@param window integer|nil: window handle or 0 for current, defaults to current ---@param offset_encoding string|nil utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` ----@returns `TextDocumentPositionParams` object +---@return table `TextDocumentPositionParams` object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams function M.make_position_params(window, offset_encoding) window = window or 0 @@ -2021,7 +2028,7 @@ end --- Utility function for getting the encoding of the first LSP client on the given buffer. ---@param bufnr (integer) buffer handle or 0 for current, defaults to current ----@returns (string) encoding first client if there is one, nil otherwise +---@return string encoding first client if there is one, nil otherwise function M._get_offset_encoding(bufnr) validate({ bufnr = { bufnr, 'n', true }, @@ -2060,7 +2067,7 @@ end --- ---@param window integer|nil: window handle or 0 for current, defaults to current ---@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of buffer of `window` ----@returns { textDocument = { uri = `current_file_uri` }, range = { start = +---@return table { textDocument = { uri = `current_file_uri` }, range = { start = ---`current_position`, end = `current_position` } } function M.make_range_params(window, offset_encoding) local buf = api.nvim_win_get_buf(window or 0) @@ -2081,7 +2088,7 @@ end --- Defaults to the end of the last visual selection. ---@param bufnr integer|nil buffer handle or 0 for current, defaults to current ---@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of `bufnr` ----@returns { textDocument = { uri = `current_file_uri` }, range = { start = +---@return table { textDocument = { uri = `current_file_uri` }, range = { start = ---`start_position`, end = `end_position` } } function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) validate({ @@ -2121,15 +2128,15 @@ end --- Creates a `TextDocumentIdentifier` object for the current buffer. --- ---@param bufnr integer|nil: Buffer handle, defaults to current ----@returns `TextDocumentIdentifier` +---@return table `TextDocumentIdentifier` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier function M.make_text_document_params(bufnr) return { uri = vim.uri_from_bufnr(bufnr or 0) } end --- Create the workspace params ----@param added ----@param removed +---@param added table +---@param removed table function M.make_workspace_params(added, removed) return { event = { added = added, removed = removed } } end @@ -2137,7 +2144,7 @@ end --- ---@see 'shiftwidth' ---@param bufnr (integer|nil): Buffer handle, defaults to current ----@returns (integer) indentation size +---@return (integer) indentation size function M.get_effective_tabstop(bufnr) validate({ bufnr = { bufnr, 'n', true } }) local bo = bufnr and vim.bo[bufnr] or vim.bo @@ -2148,7 +2155,7 @@ end --- Creates a `DocumentFormattingParams` object for the current buffer and cursor position. --- ---@param options table|nil with valid `FormattingOptions` entries ----@returns `DocumentFormattingParams` object +---@return `DocumentFormattingParams` object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting function M.make_formatting_params(options) validate({ options = { options, 't', true } }) @@ -2167,8 +2174,8 @@ end ---@param buf integer buffer number (0 for current) ---@param row 0-indexed line ---@param col 0-indexed byte offset in line ----@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `buf` ----@returns (integer, integer) `offset_encoding` index of the character in line {row} column {col} in buffer {buf} +---@param offset_encoding string utf-8|utf-16|utf-32 defaults to `offset_encoding` of first client of `buf` +---@return integer `offset_encoding` index of the character in line {row} column {col} in buffer {buf} function M.character_offset(buf, row, col, offset_encoding) local line = get_line(buf, row) if offset_encoding == nil then @@ -2176,6 +2183,7 @@ function M.character_offset(buf, row, col, offset_encoding) 'character_offset must be called with valid offset encoding', vim.log.levels.WARN ) + offset_encoding = vim.lsp.get_active_clients({ bufnr = buf })[1].offset_encoding end -- If the col is past the EOL, use the line length. if col > #line then @@ -2186,11 +2194,11 @@ end --- Helper function to return nested values in language server settings --- ----@param settings a table of language server settings ----@param section a string indicating the field of the settings table ----@returns (table or string) The value of settings accessed via section +---@param settings table language server settings +---@param section string indicating the field of the settings table +---@return table|string The value of settings accessed via section function M.lookup_section(settings, section) - for part in vim.gsplit(section, '.', true) do + for part in vim.gsplit(section, '.', { plain = true }) do settings = settings[part] if settings == nil then return vim.NIL -- cgit From cf5f1492d702f940934b0b40024d1741e4474542 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 4 Jul 2023 20:30:31 +0800 Subject: fix(lsp): revert change to buf.clear_references() #24238 Problem: in #24046 the signature of buf.clear_references() changed, which indirectly breaks callers that were passing "ignored" args. Solution: because util.buf_clear_references() already defaulted to "current buffer", the change to buf.clear_references() isn't actually needed, so just revert it. --- runtime/lua/vim/lsp/util.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 6cb91417f2..8d6f88bb2c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1769,9 +1769,9 @@ do --[[ References ]] --- Removes document highlights from a buffer. --- - ---@param bufnr integer Buffer id + ---@param bufnr integer|nil Buffer id function M.buf_clear_references(bufnr) - validate({ bufnr = { bufnr, { 'n', 'nil' }, true } }) + validate({ bufnr = { bufnr, { 'n' }, true } }) api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1) end -- cgit From 251ca45ac94851c896db0d27685622fb78a73b3e Mon Sep 17 00:00:00 2001 From: Mike <10135646+mikesmithgh@users.noreply.github.com> Date: Sun, 16 Jul 2023 06:11:45 -0400 Subject: fix(lsp): markdown code fence should allow space before info string #24364 Problem: Bash language server returns "hover" markdown content that starts with a code fence and info string of `man` preceded by whitespace, which Nvim does not render properly. See https://github.com/bash-lsp/bash-language-server/blob/0ee73c53cebdc18311d4a4ad9367185ea4d98a03/server/src/server.ts#L821C15-L821C15 ```typescript function getMarkdownContent(documentation: string, language?: string): LSP.MarkupContent { return { value: language ? // eslint-disable-next-line prefer-template ['``` ' + language, documentation, '```'].join('\n') : documentation, kind: LSP.MarkupKind.Markdown, } } ``` For example, ``` ``` man NAME git - the stupid content tracker ``` ``` If I remove the white space, then it is properly formatted. ``` ```man instead of ``` man ``` Per CommonMark Spec https://spec.commonmark.org/0.30/#info-string whitespace is allowed before and after the `info string` which identifies the language in a codeblock. > The line with the opening code fence may optionally contain some text > following the code fence; this is trimmed of leading and trailing > spaces or tabs and called the [info > string](https://spec.commonmark.org/0.30/#info-string). If the [info > string](https://spec.commonmark.org/0.30/#info-string) comes after > a backtick fence, it may not contain any backtick characters. (The > reason for this restriction is that otherwise some inline code would > be incorrectly interpreted as the beginning of a fenced code block.) Solution: Adjust stylize_markdown() to allow whitespace before codeblock info. --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 8d6f88bb2c..0da88f800e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1347,7 +1347,7 @@ function M.stylize_markdown(bufnr, contents, opts) -- table of fence types to {ft, begin, end} -- when ft is nil, we get the ft from the regex match local matchers = { - block = { nil, '```+([a-zA-Z0-9_]*)', '```+' }, + block = { nil, '```+%s*([a-zA-Z0-9_]*)', '```+' }, pre = { nil, '
([a-z0-9]*)', '
' }, code = { '', '', '' }, text = { 'text', '', '' }, -- cgit From 1b9ccd38a12f8fdbdff51ef0b3ff363540f745ec Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Mon, 17 Jul 2023 18:27:16 +0200 Subject: feat(lsp)!: rename vim.lsp.get_active_clients to get_clients (#24113) --- runtime/lua/vim/lsp/util.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 0da88f800e..59b9916f64 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -360,7 +360,7 @@ function M.get_progress_messages() local new_messages = {} local progress_remove = {} - for _, client in ipairs(vim.lsp.get_active_clients()) do + for _, client in ipairs(vim.lsp.get_clients()) do local groups = {} for progress in client.progress do local value = progress.value @@ -1841,7 +1841,7 @@ function M.locations_to_items(locations, offset_encoding) 'locations_to_items must be called with valid offset encoding', vim.log.levels.WARN ) - offset_encoding = vim.lsp.get_active_clients({ bufnr = 0 })[1].offset_encoding + offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding end local items = {} @@ -2036,7 +2036,7 @@ function M._get_offset_encoding(bufnr) local offset_encoding - for _, client in pairs(vim.lsp.get_active_clients({ bufnr = bufnr })) do + for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do if client.offset_encoding == nil then vim.notify_once( string.format( @@ -2183,7 +2183,7 @@ function M.character_offset(buf, row, col, offset_encoding) 'character_offset must be called with valid offset encoding', vim.log.levels.WARN ) - offset_encoding = vim.lsp.get_active_clients({ bufnr = buf })[1].offset_encoding + offset_encoding = vim.lsp.get_clients({ bufnr = buf })[1].offset_encoding end -- If the col is past the EOL, use the line length. if col > #line then -- cgit From be74807eef13ff8c90d55cf8b22b01d6d33b1641 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 18 Jul 2023 15:42:30 +0100 Subject: docs(lua): more improvements (#24387) * docs(lua): teach lua2dox how to table * docs(lua): teach gen_vimdoc.py about local functions No more need to mark local functions with @private * docs(lua): mention @nodoc and @meta in dev-lua-doc * fixup! Co-authored-by: Justin M. Keyes --------- Co-authored-by: Justin M. Keyes --- runtime/lua/vim/lsp/util.lua | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 59b9916f64..9a6114c35b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -22,7 +22,6 @@ local default_border = { { ' ', 'NormalFloat' }, } ----@private --- 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 @@ -60,7 +59,6 @@ local function get_border_size(opts) ) ) end - ---@private local function border_width(id) id = (id - 1) % #border + 1 if type(border[id]) == 'table' then @@ -77,7 +75,6 @@ local function get_border_size(opts) ) ) end - ---@private local function border_height(id) id = (id - 1) % #border + 1 if type(border[id]) == 'table' then @@ -103,13 +100,11 @@ local function get_border_size(opts) return { height = height, width = width } end ----@private local function split_lines(value) value = string.gsub(value, '\r\n?', '\n') return split(value, '\n', { plain = true }) end ----@private local function create_window_without_focus() local prev = vim.api.nvim_get_current_win() vim.cmd.new() @@ -219,7 +214,6 @@ function M.set_lines(lines, A, B, new_lines) return lines end ----@private local function sort_by_key(fn) return function(a, b) local ka, kb = fn(a), fn(b) @@ -234,7 +228,6 @@ local function sort_by_key(fn) end 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. @@ -250,7 +243,6 @@ local function get_lines(bufnr, rows) bufnr = api.nvim_get_current_buf() end - ---@private local function buf_lines() local lines = {} for _, row in ipairs(rows) do @@ -316,7 +308,6 @@ local function get_lines(bufnr, rows) 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. @@ -328,7 +319,6 @@ local function get_line(bufnr, row) return get_lines(bufnr, { row })[row] end ----@private --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position --- Returns a zero-indexed column, since set_lines() does the conversion to ---@param offset_encoding string|nil utf-8|utf-16|utf-32 @@ -670,7 +660,6 @@ function M.parse_snippet(input) return parsed end ----@private --- Sorts by CompletionItem.sortText. --- --see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion @@ -680,7 +669,6 @@ local function sort_completion_items(items) end) end ----@private --- Returns text that should be inserted when selecting completion item. The --- precedence is as follows: textEdit.newText > insertText > label --see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion @@ -703,7 +691,6 @@ local function get_completion_word(item) return item.label end ----@private --- Some language servers return complementary candidates whose prefixes do not --- match are also returned. So we exclude completion candidates whose prefix --- does not match. @@ -784,7 +771,6 @@ function M.text_document_completion_list_to_complete_items(result, prefix) return matches end ----@private --- Like vim.fn.bufwinid except it works across tabpages. local function bufwinid(bufnr) for _, win in ipairs(api.nvim_list_wins()) do @@ -795,7 +781,6 @@ local function bufwinid(bufnr) end --- Get list of buffers for a directory ----@private local function get_dir_bufs(path) path = path:gsub('([^%w])', '%%%1') local buffers = {} @@ -855,7 +840,6 @@ function M.rename(old_fname, new_fname, opts) end end ----@private local function create_file(change) local opts = change.options or {} -- from spec: Overwrite wins over `ignoreIfExists` @@ -870,7 +854,6 @@ 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) @@ -1266,7 +1249,6 @@ function M.preview_location(location, opts) return M.open_floating_preview(contents, syntax, opts) end ----@private 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 @@ -1305,7 +1287,6 @@ end --- Generates a table mapping markdown code block lang to vim syntax, --- based on g:markdown_fenced_languages ---@return table table of lang -> syntax mappings ----@private local function get_markdown_fences() local fences = {} for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do @@ -1460,7 +1441,6 @@ function M.stylize_markdown(bufnr, contents, opts) api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped) local idx = 1 - ---@private -- keep track of syntaxes we already included. -- no need to include the same syntax more than once local langs = {} @@ -1521,7 +1501,6 @@ function M.stylize_markdown(bufnr, contents, opts) return stripped end ----@private --- Closes the preview window --- ---@param winnr integer window id of preview window @@ -1539,7 +1518,6 @@ local function close_preview_window(winnr, bufnrs) end) end ----@private --- Creates autocommands to close a preview window when events happen. --- ---@param events table list of events @@ -1905,7 +1883,6 @@ end --- ---@param symbols table DocumentSymbol[] or SymbolInformation[] function M.symbols_to_items(symbols, bufnr) - ---@private local function _symbols_to_items(_symbols, _items, _bufnr) for _, symbol in ipairs(_symbols) do if symbol.location then -- SymbolInformation type @@ -1991,7 +1968,6 @@ function M.try_trim_markdown_code_blocks(lines) return 'markdown' end ----@private ---@param window integer|nil: window handle or 0 for current, defaults to current ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window` local function make_position_param(window, offset_encoding) @@ -2209,6 +2185,7 @@ end M._get_line_byte_from_position = get_line_byte_from_position +---@nodoc M.buf_versions = {} return M -- cgit From 63b3408551561127f7845470eb51404bcd6f547b Mon Sep 17 00:00:00 2001 From: Chris AtLee Date: Thu, 20 Jul 2023 03:03:48 -0400 Subject: feat(lsp): implement textDocument/diagnostic (#24128) --- runtime/lua/vim/lsp/util.lua | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 9a6114c35b..0b06d2bbb5 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2183,6 +2183,46 @@ function M.lookup_section(settings, section) return settings end +---@private +--- Request updated LSP information for a buffer. +--- +---@param method string LSP method to call +---@param opts (nil|table) Optional arguments +--- - bufnr (integer, default: 0): Buffer to refresh +--- - only_visible (boolean, default: false): Whether to only refresh for the visible regions of the buffer +function M._refresh(method, opts) + opts = opts or {} + local bufnr = opts.bufnr + if bufnr == nil or bufnr == 0 then + bufnr = api.nvim_get_current_buf() + end + local only_visible = opts.only_visible or false + for _, window in ipairs(api.nvim_list_wins()) do + if api.nvim_win_get_buf(window) == bufnr then + local first = vim.fn.line('w0', window) + local last = vim.fn.line('w$', window) + local params = { + textDocument = M.make_text_document_params(bufnr), + range = { + start = { line = first - 1, character = 0 }, + ['end'] = { line = last, character = 0 }, + }, + } + vim.lsp.buf_request(bufnr, method, params) + end + end + if not only_visible then + local params = { + textDocument = M.make_text_document_params(bufnr), + range = { + start = { line = 0, character = 0 }, + ['end'] = { line = api.nvim_buf_line_count(bufnr), character = 0 }, + }, + } + vim.lsp.buf_request(bufnr, method, params) + end +end + M._get_line_byte_from_position = get_line_byte_from_position ---@nodoc -- cgit From add7e106d59b8e3822310846a850b3ed3fb5db0e Mon Sep 17 00:00:00 2001 From: Keith Smiley Date: Mon, 24 Jul 2023 08:58:59 -0700 Subject: fix(lsp): noisy warning about offset_encodings #24441 In the case you hit this warning in a buffer (like with C++ and clangd), this message potentially fires over and over again making it difficult to use the editor at all. --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 0b06d2bbb5..4ff420cf48 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2026,7 +2026,7 @@ function M._get_offset_encoding(bufnr) if not offset_encoding then offset_encoding = this_offset_encoding elseif offset_encoding ~= this_offset_encoding then - vim.notify( + vim.notify_once( 'warning: multiple different client offset_encodings detected for buffer, this is not supported yet', vim.log.levels.WARN ) -- cgit From 4d0f4c3de9cbdcf85e606b5aaf9488820b95b679 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 25 Jul 2023 19:38:48 +0800 Subject: fix(lsp): E403 if doc contains multiple codeblocks #24458 Problem: Content that has codeblocks with different languages, results in multiple calls to: syntax include vim syntax/vim.vim which raises error: E403: syntax sync: line continuations pattern specified twice Before ba8f19ebb67ca27d746f4b1cd902ab3d807eace3, this was avoided by using pcall() to ignore the error. Solution: Restore the use of pcall() to ignore the error. We plan to replace this logic with a treesitter approach, so this is good enough for now. Fix #24431 --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4ff420cf48..4d3071fd68 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1466,7 +1466,7 @@ function M.stylize_markdown(bufnr, contents, opts) if #api.nvim_get_runtime_file(('syntax/%s.vim'):format(ft), true) == 0 then return end - vim.cmd(string.format('syntax include %s syntax/%s.vim', lang, ft)) + pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft)) langs[lang] = true end vim.cmd( -- cgit From 20c331915f4e317c615c7cfea469a9baedd2e4f7 Mon Sep 17 00:00:00 2001 From: Christoph Hasse Date: Tue, 25 Jul 2023 08:40:13 -0400 Subject: fix(lsp): SignatureHelp docstring is not escaped #16702 Problem: Nvim LSP client always treats signature.documentation as markdown, even if the server returns a plain string. Per https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#signatureInformation in a SignatureInformation response, the documentation field can be either "string" or "MarkupContent". Solution: If signature.documentation is a string, treat it as "plaintext". Closes #16563 --- runtime/lua/vim/lsp/util.lua | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4d3071fd68..738e23ff28 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1002,6 +1002,12 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers end list_extend(contents, split(label, '\n', { plain = true })) if signature.documentation 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 } + end M.convert_input_to_markdown_lines(signature.documentation, contents) end if signature.parameters and #signature.parameters > 0 then -- cgit From c43c745a14dced87a23227d7be4f1c33d4455193 Mon Sep 17 00:00:00 2001 From: Christian Clason Date: Wed, 9 Aug 2023 11:06:13 +0200 Subject: fix(lua): improve annotations for stricter luals diagnostics (#24609) Problem: luals returns stricter diagnostics with bundled luarc.json Solution: Improve some function and type annotations: * use recognized uv.* types * disable diagnostic for global `vim` in shared.lua * docs: don't start comment lines with taglink (otherwise LuaLS will interpret it as a type) * add type alias for lpeg pattern * fix return annotation for `vim.secure.trust` * rename local Range object in vim.version (shadows `Range` in vim.treesitter) * fix some "missing fields" warnings * add missing required fields for test functions in eval.lua * rename lsp meta files for consistency --- runtime/lua/vim/lsp/util.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 738e23ff28..633cddca2d 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -715,8 +715,8 @@ end --- Turns the result of a `textDocument/completion` request into vim-compatible --- |complete-items|. --- ----@param result table The result of a `textDocument/completion` call, e.g. from ----|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`, +---@param result table The result of a `textDocument/completion` call, e.g. +--- from |vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`, --- `CompletionList` or `null` ---@param prefix (string) the prefix to filter the completion items ---@return table { matches = complete-items table, incomplete = bool } -- cgit From c235959fd909d75248c066a781475e207606c5aa Mon Sep 17 00:00:00 2001 From: Chris AtLee Date: Thu, 31 Aug 2023 04:00:24 -0400 Subject: fix(lsp): only disable inlay hints / diagnostics if no other clients are connected (#24535) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes the issue where the LspNotify handlers for inlay_hint / diagnostics would end up refreshing all attached clients. The handler would call util._refresh, which called vim.lsp.buf_request, which calls the method on all attached clients. Now util._refresh takes an optional client_id parameter, which is used to specify a specific client to update. This commit also fixes util._refresh's handling of the `only_visible` flag. Previously if `only_visible` was false, two requests would be made to the server: one for the visible region, and one for the entire file. Co-authored-by: Stanislav Asunkin <1353637+stasjok@users.noreply.github.com> Co-authored-by: Mathias Fußenegger --- runtime/lua/vim/lsp/util.lua | 53 +++++++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 18 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 633cddca2d..2e376f9093 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2122,6 +2122,7 @@ end function M.make_workspace_params(added, removed) return { event = { added = added, removed = removed } } end + --- Returns indentation size. --- ---@see 'shiftwidth' @@ -2192,32 +2193,46 @@ end ---@private --- Request updated LSP information for a buffer. --- +---@class lsp.util.RefreshOptions +---@field bufnr integer? Buffer to refresh (default: 0) +---@field only_visible? boolean Whether to only refresh for the visible regions of the buffer (default: false) +---@field client_id? integer Client ID to refresh (default: all clients) +-- ---@param method string LSP method to call ----@param opts (nil|table) Optional arguments ---- - bufnr (integer, default: 0): Buffer to refresh ---- - only_visible (boolean, default: false): Whether to only refresh for the visible regions of the buffer +---@param opts? lsp.util.RefreshOptions Options table function M._refresh(method, opts) opts = opts or {} local bufnr = opts.bufnr if bufnr == nil or bufnr == 0 then bufnr = api.nvim_get_current_buf() end + + local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method, id = opts.client_id }) + + if #clients == 0 then + return + end + local only_visible = opts.only_visible or false - for _, window in ipairs(api.nvim_list_wins()) do - if api.nvim_win_get_buf(window) == bufnr then - local first = vim.fn.line('w0', window) - local last = vim.fn.line('w$', window) - local params = { - textDocument = M.make_text_document_params(bufnr), - range = { - start = { line = first - 1, character = 0 }, - ['end'] = { line = last, character = 0 }, - }, - } - vim.lsp.buf_request(bufnr, method, params) + + if only_visible then + for _, window in ipairs(api.nvim_list_wins()) do + if api.nvim_win_get_buf(window) == bufnr then + local first = vim.fn.line('w0', window) + local last = vim.fn.line('w$', window) + local params = { + textDocument = M.make_text_document_params(bufnr), + range = { + start = { line = first - 1, character = 0 }, + ['end'] = { line = last, character = 0 }, + }, + } + for _, client in ipairs(clients) do + client.request(method, params, nil, bufnr) + end + end end - end - if not only_visible then + else local params = { textDocument = M.make_text_document_params(bufnr), range = { @@ -2225,7 +2240,9 @@ function M._refresh(method, opts) ['end'] = { line = api.nvim_buf_line_count(bufnr), character = 0 }, }, } - vim.lsp.buf_request(bufnr, method, params) + for _, client in ipairs(clients) do + client.request(method, params, nil, bufnr) + end end end -- cgit From 131a1ee82d15ce9d1356a46117c9a1651947d4b8 Mon Sep 17 00:00:00 2001 From: Tom Praschan <13141438+tom-anders@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:12:02 +0200 Subject: feat(lsp): add original LSP Location as item's user_data in locations_to_items (#23743) --- runtime/lua/vim/lsp/util.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 2e376f9093..a6d17afa1b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1812,6 +1812,9 @@ end) --- Returns the items with the byte position calculated correctly and in sorted --- order, for display in quickfix and location lists. --- +--- The `user_data` field of each resulting item will contain the original +--- `Location` or `LocationLink` it was computed from. +--- --- The result can be passed to the {list} argument of |setqflist()| or --- |setloclist()|. --- @@ -1840,7 +1843,7 @@ function M.locations_to_items(locations, offset_encoding) -- locations may be Location or LocationLink local uri = d.uri or d.targetUri local range = d.range or d.targetSelectionRange - table.insert(grouped[uri], { start = range.start }) + table.insert(grouped[uri], { start = range.start, location = d }) end local keys = vim.tbl_keys(grouped) @@ -1872,6 +1875,7 @@ function M.locations_to_items(locations, offset_encoding) lnum = row + 1, col = col + 1, text = line, + user_data = temp.location, }) end end -- cgit From 5e3cf9fb4bc7f236c858f609ca83c57fb1779ab0 Mon Sep 17 00:00:00 2001 From: Grace Petryk Date: Sun, 10 Sep 2023 01:02:23 -0700 Subject: feat(lsp): improve control over placement of floating windows (#24494) --- runtime/lua/vim/lsp/util.lua | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index a6d17afa1b..e76fd15612 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1087,6 +1087,12 @@ end --- - 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 function M.make_floating_popup_options(width, height, opts) validate({ @@ -1105,7 +1111,20 @@ function M.make_floating_popup_options(width, height, opts) or vim.fn.winline() - 1 local lines_below = vim.fn.winheight(0) - lines_above - if lines_above < lines_below then + local anchor_bias = opts.anchor_bias or 'auto' + + local anchor_below + + if anchor_bias == 'below' then + anchor_below = (lines_below > lines_above) or (height <= lines_below) + elseif anchor_bias == 'above' then + local anchor_above = (lines_above > lines_below) or (height <= lines_above) + anchor_below = not anchor_above + else + anchor_below = lines_below > lines_above + end + + if anchor_below then anchor = anchor .. 'N' height = math.min(lines_below, height) row = 1 @@ -1635,7 +1654,8 @@ end --- ---@param contents table of lines to show in window ---@param syntax string of syntax to set for opened buffer ----@param opts table with optional fields (additional keys are passed on to |nvim_open_win()|) +---@param opts table with optional fields (additional keys are filtered with |vim.lsp.util.make_floating_popup_options()| +--- before they are passed on to |nvim_open_win()|) --- - height: (integer) height of floating window --- - width: (integer) width of floating window --- - wrap: (boolean, default true) wrap long lines -- cgit From d22172f36bbe147f3aa6b76a1c43ae445f481c2e Mon Sep 17 00:00:00 2001 From: Sergey Slipchenko Date: Mon, 11 Sep 2023 08:16:03 +0400 Subject: fix(api): more intuitive cursor updates in nvim_buf_set_text Fixes #22526 --- runtime/lua/vim/lsp/util.lua | 42 ------------------------------------------ 1 file changed, 42 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index e76fd15612..54721865b7 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -454,23 +454,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) end end) - -- Some LSP servers are depending on the VSCode behavior. - -- The VSCode will re-locate the cursor position after applying TextEdit so we also do it. - local is_current_buf = api.nvim_get_current_buf() == bufnr or bufnr == 0 - local cursor = (function() - if not is_current_buf then - return { - row = -1, - col = -1, - } - end - local cursor = api.nvim_win_get_cursor(0) - return { - row = cursor[1] - 1, - col = cursor[2], - } - end)() - -- save and restore local marks since they get deleted by nvim_buf_set_lines local marks = {} for _, m in pairs(vim.fn.getmarklist(bufnr or vim.api.nvim_get_current_buf())) do @@ -480,7 +463,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) end -- Apply text edits. - local is_cursor_fixed = false local has_eol_text_edit = false for _, text_edit in ipairs(text_edits) do -- Normalize line ending @@ -527,20 +509,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) e.end_col = math.min(last_line_len, e.end_col) api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text) - - -- Fix cursor position. - local row_count = (e.end_row - e.start_row) + 1 - if e.end_row < cursor.row then - cursor.row = cursor.row + (#e.text - row_count) - is_cursor_fixed = true - elseif e.end_row == cursor.row and e.end_col <= cursor.col then - cursor.row = cursor.row + (#e.text - row_count) - cursor.col = #e.text[#e.text] + (cursor.col - e.end_col) - if #e.text == 1 then - cursor.col = cursor.col + e.start_col - end - is_cursor_fixed = true - end end end @@ -560,16 +528,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) end end - -- Apply fixed cursor position. - if is_cursor_fixed then - local is_valid_cursor = true - is_valid_cursor = is_valid_cursor and cursor.row < max - is_valid_cursor = is_valid_cursor and cursor.col <= #(get_line(bufnr, cursor.row) or '') - if is_valid_cursor then - api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col }) - end - end - -- Remove final line if needed local fix_eol = has_eol_text_edit fix_eol = fix_eol and (vim.bo[bufnr].eol or (vim.bo[bufnr].fixeol and not vim.bo[bufnr].binary)) -- cgit From cfd4a9dfaf5fd900264a946ca33c4a4f26f66a49 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Tue, 12 Sep 2023 20:51:21 -0700 Subject: feat(lsp): use treesitter for stylize markdown --- runtime/lua/vim/lsp/util.lua | 177 +++++++++++++++++++++++++++++-------------- 1 file changed, 119 insertions(+), 58 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 54721865b7..32e85578d3 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -877,9 +877,12 @@ end --- window for `textDocument/hover`, for parsing the result of --- `textDocument/signatureHelp`, and potentially others. --- +--- Note that if the input is of type `MarkupContent` and its kind is `plaintext`, +--- then the corresponding value is returned without further modifications. +--- ---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`) ---@param contents (table|nil) List of strings to extend with converted lines. Defaults to {}. ----@return table {contents} extended with lines of converted markdown. +---@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) contents = contents or {} @@ -887,27 +890,13 @@ function M.convert_input_to_markdown_lines(input, contents) if type(input) == 'string' then list_extend(contents, split_lines(input)) else - assert(type(input) == 'table', 'Expected a table for Hover.contents') + assert(type(input) == 'table', 'Expected a table for LSP input') -- MarkupContent if input.kind then - -- The kind can be either plaintext or markdown. - -- If it's plaintext, then wrap it in a block - - -- Some servers send input.value as empty, so let's ignore this :( local value = input.value or '' - - if input.kind == 'plaintext' then - -- wrap this in a block so that stylize_markdown - -- can properly process it as plaintext - value = string.format('\n%s\n', value) - end - - -- assert(type(value) == 'string') list_extend(contents, split_lines(value)) -- MarkupString variation 2 elseif input.language then - -- Some servers send input.value as empty, so let's ignore this :( - -- assert(type(input.value) == 'string') table.insert(contents, '```' .. input.language) list_extend(contents, split_lines(input.value or '')) table.insert(contents, '```') @@ -925,7 +914,7 @@ function M.convert_input_to_markdown_lines(input, contents) return contents end ---- Converts `textDocument/SignatureHelp` response to markdown lines. +--- Converts `textDocument/signatureHelp` response to markdown lines. --- ---@param signature_help table Response of `textDocument/SignatureHelp` ---@param ft string|nil filetype that will be use as the `lang` for the label markdown code block @@ -955,7 +944,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers end local label = signature.label if ft then - -- wrap inside a code block so stylize_markdown can render it properly + -- 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 })) @@ -1223,7 +1212,7 @@ function M.preview_location(location, opts) local syntax = vim.bo[bufnr].syntax if syntax == '' then -- When no syntax is set, we use filetype as fallback. This might not result - -- in a valid syntax definition. See also ft detection in stylize_markdown. + -- in a valid syntax definition. -- An empty syntax is more common now with TreeSitter, since TS disables syntax. syntax = vim.bo[bufnr].filetype end @@ -1240,36 +1229,65 @@ local function find_window_by_var(name, value) end end ---- Trims empty lines from input and pad top and bottom with empty lines ---- ----@param contents table of lines to trim and pad ----@param opts table with optional fields ---- - pad_top number of lines to pad contents at top (default 0) ---- - pad_bottom number of lines to pad contents at bottom (default 0) ----@return table table of trimmed and padded lines -function M._trim(contents, opts) - validate({ - contents = { contents, 't' }, - opts = { opts, 't', true }, - }) - opts = opts or {} - contents = M.trim_empty_lines(contents) - if opts.pad_top then - for _ = 1, opts.pad_top do - table.insert(contents, 1, '') +---Returns true if the line is empty or only contains whitespace. +---@param line string +---@return boolean +local function is_blank_line(line) + return line and line:match('^%s*$') +end + +---Returns true if the line corresponds to a Markdown thematic break. +---@param line string +---@return boolean +local function is_separator_line(line) + return line and line:match('^ ? ? ?%-%-%-+%s*$') +end + +---Replaces separator lines by the given divider and removing surrounding blank lines. +---@param contents string[] +---@param divider string +---@return string[] +local function replace_separators(contents, divider) + local trimmed = {} + local l = 1 + while l <= #contents do + local line = contents[l] + if is_separator_line(line) then + if l > 1 and is_blank_line(contents[l - 1]) then + table.remove(trimmed) + end + table.insert(trimmed, divider) + if is_blank_line(contents[l + 1]) then + l = l + 1 + end + else + table.insert(trimmed, line) end + l = l + 1 end - if opts.pad_bottom then - for _ = 1, opts.pad_bottom do - table.insert(contents, '') + + return trimmed +end + +---Collapses successive blank lines in the input table into a single one. +---@param contents string[] +---@return string[] +local function collapse_blank_lines(contents) + local collapsed = {} + local l = 1 + while l <= #contents do + local line = contents[l] + if is_blank_line(line) then + while is_blank_line(contents[l + 1]) do + l = l + 1 + end end + table.insert(collapsed, line) + l = l + 1 end - return contents + return collapsed end ---- Generates a table mapping markdown code block lang to vim syntax, ---- based on g:markdown_fenced_languages ----@return table table of lang -> syntax mappings local function get_markdown_fences() local fences = {} for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do @@ -1297,8 +1315,6 @@ end --- - wrap_at character to wrap at for computing height --- - 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 --- - separator insert separator after code block ---@return table stripped content function M.stylize_markdown(bufnr, contents, opts) @@ -1335,7 +1351,7 @@ function M.stylize_markdown(bufnr, contents, opts) end -- Clean up - contents = M._trim(contents, opts) + contents = M.trim_empty_lines(contents) local stripped = {} local highlights = {} @@ -1484,6 +1500,49 @@ function M.stylize_markdown(bufnr, contents, opts) return stripped end +--- @class lsp.util.NormalizeMarkdownOptions +--- @field width integer Thematic breaks are expanded to this size. Defaults to 80. + +--- Normalizes Markdown input to a canonical form. +--- +--- The returned Markdown adheres to the GitHub Flavored Markdown (GFM) +--- specification. +--- +--- The following transformations are made: +--- +--- 1. Empty lines at the beginning or end of the content are removed +--- 2. Carriage returns ('\r') are removed +--- 3. Successive empty lines are collapsed into a single empty line +--- 4. Thematic breaks are expanded to the given width +--- +---@private +---@param contents string[] +---@param opts? lsp.util.NormalizeMarkdownOptions +---@return string[] table of lines containing normalized Markdown +---@see https://github.github.com/gfm +function M._normalize_markdown(contents, opts) + validate({ + contents = { contents, 't' }, + opts = { opts, 't', true }, + }) + opts = opts or {} + + -- 1. Empty lines at the beginning or end of the content are removed + contents = M.trim_empty_lines(contents) + + -- 2. Carriage returns are removed + contents = vim.split(table.concat(contents, '\n'):gsub('\r', ''), '\n') + + -- 3. Successive empty lines are collapsed into a single empty line + contents = collapse_blank_lines(contents) + + -- 4. Thematic breaks are expanded to the given width + local divider = string.rep('─', opts.width or 80) + contents = replace_separators(contents, divider) + + return contents +end + --- Closes the preview window --- ---@param winnr integer window id of preview window @@ -1620,8 +1679,6 @@ end --- - wrap_at: (integer) character to wrap at for computing height when wrap is enabled --- - max_width: (integer) maximal width of floating window --- - max_height: (integer) maximal height of floating window ---- - pad_top: (integer) number of lines to pad contents at top ---- - pad_bottom: (integer) 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 @@ -1629,8 +1686,7 @@ end --- is also `true`, focus an existing floating window with the same --- {focus_id} ---@return integer bufnr of newly created float window ----@return integer winid of newly created float window ----preview window +---@return integer winid of newly created float window preview window function M.open_floating_preview(contents, syntax, opts) validate({ contents = { contents, 't' }, @@ -1639,7 +1695,6 @@ 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 and vim.g.syntax_on ~= nil opts.focus = opts.focus ~= false opts.close_events = opts.close_events or { 'CursorMoved', 'CursorMovedI', 'InsertCharPre' } @@ -1671,16 +1726,21 @@ function M.open_floating_preview(contents, syntax, opts) api.nvim_win_close(existing_float, true) end + -- Create the buffer local floating_bufnr = api.nvim_create_buf(false, true) - local do_stylize = syntax == 'markdown' and opts.stylize_markdown - - -- Clean up input: trim empty lines from the end, pad - contents = M._trim(contents, opts) + -- Set up the contents, using treesitter for markdown + local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil if do_stylize then - -- applies the syntax and sets the lines to the buffer - contents = M.stylize_markdown(floating_bufnr, contents, opts) + local width = M._make_floating_popup_size(contents, opts) + contents = M._normalize_markdown(contents, { width = width }) + vim.bo[floating_bufnr].filetype = 'markdown' + vim.treesitter.start(floating_bufnr) + api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents) else + -- Clean up input: trim empty lines from the end, pad + contents = M.trim_empty_lines(contents) + if syntax then vim.bo[floating_bufnr].syntax = syntax end @@ -1697,9 +1757,9 @@ function M.open_floating_preview(contents, syntax, opts) local float_option = M.make_floating_popup_options(width, height, opts) local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) + if do_stylize then vim.wo[floating_winnr].conceallevel = 2 - vim.wo[floating_winnr].concealcursor = 'n' end -- disable folding vim.wo[floating_winnr].foldenable = false @@ -1708,6 +1768,7 @@ function M.open_floating_preview(contents, syntax, opts) vim.bo[floating_bufnr].modifiable = false vim.bo[floating_bufnr].bufhidden = 'wipe' + api.nvim_buf_set_keymap( floating_bufnr, 'n', -- cgit From 5a363ccac8ff5889332bafbf68e7e8d20bca316c Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Mon, 18 Sep 2023 11:04:01 -0700 Subject: fix(lsp)!: deprecate trim_empty_lines --- runtime/lua/vim/lsp/util.lua | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 32e85578d3..988057f5f9 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -102,7 +102,7 @@ end local function split_lines(value) value = string.gsub(value, '\r\n?', '\n') - return split(value, '\n', { plain = true }) + return split(value, '\n', { plain = true, trimempty = true }) end local function create_window_without_focus() @@ -947,7 +947,7 @@ 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 })) + list_extend(contents, split(label, '\n', { plain = true, trimempty = true })) if signature.documentation then -- if LSP returns plain string, we treat it as plaintext. This avoids -- special characters like underscore or similar from being interpreted @@ -1351,7 +1351,7 @@ function M.stylize_markdown(bufnr, contents, opts) end -- Clean up - contents = M.trim_empty_lines(contents) + contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) local stripped = {} local highlights = {} @@ -1510,10 +1510,9 @@ end --- --- The following transformations are made: --- ---- 1. Empty lines at the beginning or end of the content are removed ---- 2. Carriage returns ('\r') are removed ---- 3. Successive empty lines are collapsed into a single empty line ---- 4. Thematic breaks are expanded to the given width +--- 1. Carriage returns ('\r') and empty lines at the beginning and end are removed +--- 2. Successive empty lines are collapsed into a single empty line +--- 3. Thematic breaks are expanded to the given width --- ---@private ---@param contents string[] @@ -1527,16 +1526,13 @@ function M._normalize_markdown(contents, opts) }) opts = opts or {} - -- 1. Empty lines at the beginning or end of the content are removed - contents = M.trim_empty_lines(contents) + -- 1. Carriage returns are removed + contents = vim.split(table.concat(contents, '\n'):gsub('\r', ''), '\n', { trimempty = true }) - -- 2. Carriage returns are removed - contents = vim.split(table.concat(contents, '\n'):gsub('\r', ''), '\n') - - -- 3. Successive empty lines are collapsed into a single empty line + -- 2. Successive empty lines are collapsed into a single empty line contents = collapse_blank_lines(contents) - -- 4. Thematic breaks are expanded to the given width + -- 3. Thematic breaks are expanded to the given width local divider = string.rep('─', opts.width or 80) contents = replace_separators(contents, divider) @@ -1738,8 +1734,8 @@ function M.open_floating_preview(contents, syntax, opts) vim.treesitter.start(floating_bufnr) api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents) else - -- Clean up input: trim empty lines from the end, pad - contents = M.trim_empty_lines(contents) + -- Clean up input: trim empty lines + contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) if syntax then vim.bo[floating_bufnr].syntax = syntax @@ -1969,6 +1965,7 @@ function M.symbols_to_items(symbols, bufnr) end --- Removes empty lines from the beginning and end. +---@deprecated use `vim.split()` with `trimempty` instead ---@param lines table list of lines to trim ---@return table trimmed list of lines function M.trim_empty_lines(lines) -- cgit From 345bd91db28ecfc4deb308f4971253b534f82d49 Mon Sep 17 00:00:00 2001 From: Sergey Slipchenko Date: Thu, 21 Sep 2023 14:06:40 +0400 Subject: fix(lsp): handle absence of a trailing newline #25194 Fixes #24339 rust-analyzer sends "Invalid offset" error in such cases. Some other servers handle it specially. LSP spec mentions that "A range is comparable to a selection in an editor". Most editors don't handle trailing newlines the same way Neovim/Vim does, it's clearly visible if it's present or not. With that in mind it's understandable why sending end position as simply the start of the line after the last one is considered invalid in such cases. --- runtime/lua/vim/lsp/util.lua | 60 ++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 16 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 988057f5f9..0d1e3cc0d1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2230,6 +2230,35 @@ function M.lookup_section(settings, section) return settings end +--- Converts line range (0-based, end-inclusive) to lsp range, +--- handles absence of a trailing newline +--- +---@param bufnr integer +---@param start_line integer +---@param end_line integer +---@param offset_encoding lsp.PositionEncodingKind +---@return lsp.Range +local function make_line_range_params(bufnr, start_line, end_line, offset_encoding) + local last_line = api.nvim_buf_line_count(bufnr) - 1 + + ---@type lsp.Position + local end_pos + + if end_line == last_line and not vim.api.nvim_get_option_value('endofline', { buf = bufnr }) then + end_pos = { + line = end_line, + character = M.character_offset(bufnr, end_line, #get_line(bufnr, end_line), offset_encoding), + } + else + end_pos = { line = end_line + 1, character = 0 } + end + + return { + start = { line = start_line, character = 0 }, + ['end'] = end_pos, + } +end + ---@private --- Request updated LSP information for a buffer. --- @@ -2253,6 +2282,8 @@ function M._refresh(method, opts) return end + local textDocument = M.make_text_document_params(bufnr) + local only_visible = opts.only_visible or false if only_visible then @@ -2260,28 +2291,25 @@ function M._refresh(method, opts) if api.nvim_win_get_buf(window) == bufnr then local first = vim.fn.line('w0', window) local last = vim.fn.line('w$', window) - local params = { - textDocument = M.make_text_document_params(bufnr), - range = { - start = { line = first - 1, character = 0 }, - ['end'] = { line = last, character = 0 }, - }, - } for _, client in ipairs(clients) do - client.request(method, params, nil, bufnr) + client.request(method, { + textDocument = textDocument, + range = make_line_range_params(bufnr, first - 1, last - 1, client.offset_encoding), + }, nil, bufnr) end end end else - local params = { - textDocument = M.make_text_document_params(bufnr), - range = { - start = { line = 0, character = 0 }, - ['end'] = { line = api.nvim_buf_line_count(bufnr), character = 0 }, - }, - } for _, client in ipairs(clients) do - client.request(method, params, nil, bufnr) + client.request(method, { + textDocument = textDocument, + range = make_line_range_params( + bufnr, + 0, + api.nvim_buf_line_count(bufnr) - 1, + client.offset_encoding + ), + }, nil, bufnr) end end end -- cgit From 9ed830a3ca5847a9152b91fca5e1eaf712bed55b Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Fri, 29 Sep 2023 08:37:14 -0700 Subject: refactor(lsp): deprecate util methods (#25400) --- runtime/lua/vim/lsp/util.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 0d1e3cc0d1..51ed87219c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -168,10 +168,12 @@ end local _str_utfindex_enc = M._str_utfindex_enc local _str_byteindex_enc = M._str_byteindex_enc + --- Replaces text in a range with new text. --- --- CAUTION: Changes in-place! --- +---@deprecated ---@param lines (table) Original list of strings ---@param A (table) Start position; a 2-tuple of {line,col} numbers ---@param B (table) End position; a 2-tuple of {line,col} numbers @@ -320,9 +322,7 @@ local function get_line(bufnr, row) end --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position ---- Returns a zero-indexed column, since set_lines() does the conversion to ---@param offset_encoding string|nil utf-8|utf-16|utf-32 ---- 1-indexed ---@return integer local function get_line_byte_from_position(bufnr, position, offset_encoding) -- LSP's line and characters are 0-indexed @@ -1991,6 +1991,7 @@ end --- --- CAUTION: Modifies the input in-place! --- +---@deprecated ---@param lines table list of lines ---@return string filetype or "markdown" if it was unchanged. function M.try_trim_markdown_code_blocks(lines) -- cgit From eb1f0e8fcca756a00d287e23bf87554e0e7f6dfd Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Sun, 1 Oct 2023 09:54:04 -0700 Subject: feat(lsp)!: replace snippet parser by lpeg grammar --- runtime/lua/vim/lsp/util.lua | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 51ed87219c..a4c8959b99 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1,5 +1,5 @@ local protocol = require('vim.lsp.protocol') -local snippet = require('vim.lsp._snippet') +local snippet = require('vim.lsp._snippet_grammar') local validate = vim.validate local api = vim.api local list_extend = vim.list_extend @@ -610,12 +610,41 @@ end ---@return string parsed snippet function M.parse_snippet(input) local ok, parsed = pcall(function() - return tostring(snippet.parse(input)) + return snippet.parse(input) end) if not ok then return input end - return parsed + + --- @param node vim.snippet.Node + --- @return string + local function node_to_string(node) + local insert_text = {} + if node.type == snippet.NodeType.Snippet then + for _, child in + ipairs((node.data --[[@as vim.snippet.SnippetData]]).children) + do + table.insert(insert_text, node_to_string(child)) + end + elseif node.type == snippet.NodeType.Choice then + table.insert(insert_text, (node.data --[[@as vim.snippet.ChoiceData]]).values[1]) + elseif node.type == snippet.NodeType.Placeholder then + table.insert( + insert_text, + node_to_string((node.data --[[@as vim.snippet.PlaceholderData]]).value) + ) + elseif node.type == snippet.NodeType.Text then + table.insert( + insert_text, + node + .data --[[@as vim.snippet.TextData]] + .text + ) + end + return table.concat(insert_text) + end + + return node_to_string(parsed) end --- Sorts by CompletionItem.sortText. -- cgit From 9abced6ad95f6300ae80cd8b8aa124ebcf511b50 Mon Sep 17 00:00:00 2001 From: LW Date: Sun, 8 Oct 2023 01:09:25 -0700 Subject: fix(lsp): account for border height in max floating popup height (#25539) --- runtime/lua/vim/lsp/util.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index a4c8959b99..ec0a1b0ab0 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1100,13 +1100,14 @@ function M.make_floating_popup_options(width, height, opts) anchor_below = lines_below > lines_above end + local border_height = get_border_size(opts).height if anchor_below then anchor = anchor .. 'N' - height = math.min(lines_below, height) + height = math.max(math.min(lines_below - border_height, height), 0) row = 1 else anchor = anchor .. 'S' - height = math.min(lines_above, height) + height = math.max(math.min(lines_above - border_height, height), 0) row = 0 end -- cgit From 840e1864c2de2b4b192a4df1865b69093904b139 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Thu, 12 Oct 2023 15:39:39 +0800 Subject: fix(lsp): handle NUL bytes in popup text (#25612) Fix #25610 --- runtime/lua/vim/lsp/util.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index ec0a1b0ab0..7e6855528a 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1647,7 +1647,7 @@ function M._make_floating_popup_size(contents, opts) width = 0 for i, line in ipairs(contents) do -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. - line_widths[i] = vim.fn.strdisplaywidth(line) + line_widths[i] = vim.fn.strdisplaywidth(line:gsub('%z', '\n')) width = math.max(line_widths[i], width) end end @@ -1676,7 +1676,7 @@ function M._make_floating_popup_size(contents, opts) height = 0 if vim.tbl_isempty(line_widths) then for _, line in ipairs(contents) do - local line_width = vim.fn.strdisplaywidth(line) + local line_width = vim.fn.strdisplaywidth(line:gsub('%z', '\n')) height = height + math.ceil(line_width / wrap_at) end else -- cgit From c46a6c065e8d830adb8a2f410d3c92cf5bd4455b Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Mon, 16 Oct 2023 08:13:37 -0700 Subject: docs: do not hardcode LSP version in URL #25648 --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 7e6855528a..d525cae4c0 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1829,7 +1829,7 @@ do --[[ References ]] ---@param bufnr integer Buffer id ---@param references table List of `DocumentHighlight` objects to highlight ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32". - ---@see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentContentChangeEvent + ---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent function M.buf_highlight_references(bufnr, references, offset_encoding) validate({ bufnr = { bufnr, 'n', true }, -- cgit From f1775da07fe48da629468bcfcc2a8a6c4c3f40ed Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Fri, 20 Oct 2023 23:51:26 -0700 Subject: feat(lsp): add snippet API (#25301) --- runtime/lua/vim/lsp/util.lua | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index d525cae4c0..42c1508cbf 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -616,35 +616,7 @@ function M.parse_snippet(input) return input end - --- @param node vim.snippet.Node - --- @return string - local function node_to_string(node) - local insert_text = {} - if node.type == snippet.NodeType.Snippet then - for _, child in - ipairs((node.data --[[@as vim.snippet.SnippetData]]).children) - do - table.insert(insert_text, node_to_string(child)) - end - elseif node.type == snippet.NodeType.Choice then - table.insert(insert_text, (node.data --[[@as vim.snippet.ChoiceData]]).values[1]) - elseif node.type == snippet.NodeType.Placeholder then - table.insert( - insert_text, - node_to_string((node.data --[[@as vim.snippet.PlaceholderData]]).value) - ) - elseif node.type == snippet.NodeType.Text then - table.insert( - insert_text, - node - .data --[[@as vim.snippet.TextData]] - .text - ) - end - return table.concat(insert_text) - end - - return node_to_string(parsed) + return tostring(parsed) end --- Sorts by CompletionItem.sortText. -- cgit From 1e10310f4cc70cf95a68457c2be9e7459b5bbba6 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Sat, 21 Oct 2023 09:47:24 +0200 Subject: refactor(lsp): move completion logic into _completion module To reduce cross-chatter between modules and for https://github.com/neovim/neovim/issues/25272 Also preparing for https://github.com/neovim/neovim/issues/25714 --- runtime/lua/vim/lsp/util.lua | 93 ++------------------------------------------ 1 file changed, 3 insertions(+), 90 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 42c1508cbf..7ccb8a38b1 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -548,7 +548,7 @@ end --- `textDocument/completion` request, which may return one of --- `CompletionItem[]`, `CompletionList` or null. ---@param result table The result of a `textDocument/completion` request ----@return table List of completion items +---@return lsp.CompletionItem[] List of completion items ---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion function M.extract_completion_items(result) if type(result) == 'table' and result.items then @@ -619,47 +619,6 @@ function M.parse_snippet(input) return tostring(parsed) end ---- Sorts by CompletionItem.sortText. ---- ---see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion -local function sort_completion_items(items) - table.sort(items, function(a, b) - return (a.sortText or a.label) < (b.sortText or b.label) - end) -end - ---- Returns text that should be inserted when selecting completion item. The ---- precedence is as follows: textEdit.newText > insertText > label ---see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion -local function get_completion_word(item) - if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then - local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat] - if insert_text_format == 'PlainText' or insert_text_format == nil then - return item.textEdit.newText - else - return M.parse_snippet(item.textEdit.newText) - end - elseif item.insertText ~= nil and item.insertText ~= '' then - local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat] - if insert_text_format == 'PlainText' or insert_text_format == nil then - return item.insertText - else - return M.parse_snippet(item.insertText) - end - end - return item.label -end - ---- Some language servers return complementary candidates whose prefixes do not ---- match are also returned. So we exclude completion candidates whose prefix ---- does not match. -local function remove_unmatch_completion_items(items, prefix) - return vim.tbl_filter(function(item) - local word = get_completion_word(item) - return vim.startswith(word, prefix) - end, items) -end - --- 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. @@ -678,56 +637,10 @@ end --- from |vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`, --- `CompletionList` or `null` ---@param prefix (string) the prefix to filter the completion items ----@return table { matches = complete-items table, incomplete = bool } +---@return table[] items ---@see complete-items function M.text_document_completion_list_to_complete_items(result, prefix) - local items = M.extract_completion_items(result) - if vim.tbl_isempty(items) then - return {} - end - - items = remove_unmatch_completion_items(items, prefix) - sort_completion_items(items) - - local matches = {} - - for _, completion_item in ipairs(items) do - local info = '' - local documentation = completion_item.documentation - if documentation then - if type(documentation) == 'string' and documentation ~= '' then - info = documentation - elseif type(documentation) == 'table' and type(documentation.value) == 'string' then - info = documentation.value - else - vim.notify( - ('invalid documentation value %s'):format(vim.inspect(documentation)), - vim.log.levels.WARN - ) - end - end - - local word = get_completion_word(completion_item) - table.insert(matches, { - word = word, - abbr = completion_item.label, - kind = M._get_completion_item_kind_name(completion_item.kind), - menu = completion_item.detail or '', - info = #info > 0 and info or nil, - icase = 1, - dup = 1, - empty = 1, - user_data = { - nvim = { - lsp = { - completion_item = completion_item, - }, - }, - }, - }) - end - - return matches + return require('vim.lsp._completion')._lsp_to_complete_items(result, prefix) end --- Like vim.fn.bufwinid except it works across tabpages. -- cgit From 195301c60969c7ce97b1ef3a3caaf4965da1abd5 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Sat, 21 Oct 2023 09:57:50 +0200 Subject: refactor(lsp): deprecate completion util methods Relates to https://github.com/neovim/neovim/issues/25272 --- runtime/lua/vim/lsp/util.lua | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) (limited to 'runtime/lua/vim/lsp/util.lua') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 7ccb8a38b1..32b220746f 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -547,10 +547,12 @@ end --- Can be used to extract the completion items from a --- `textDocument/completion` request, which may return one of --- `CompletionItem[]`, `CompletionList` or null. +---@deprecated ---@param result table The result of a `textDocument/completion` request ---@return lsp.CompletionItem[] List of completion items ---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion function M.extract_completion_items(result) + vim.deprecate('vim.lsp.util.extract_completion_items', nil, '0.11') if type(result) == 'table' and result.items then -- result is a `CompletionList` return result.items @@ -606,9 +608,11 @@ end --- Parses snippets in a completion entry. --- +---@deprecated ---@param input string unparsed snippet ---@return string parsed snippet function M.parse_snippet(input) + vim.deprecate('vim.lsp.util.parse_snippet', nil, '0.11') local ok, parsed = pcall(function() return snippet.parse(input) end) @@ -619,20 +623,10 @@ function M.parse_snippet(input) return tostring(parsed) end ---- 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. ---- ----@param completion_item_kind (`vim.lsp.protocol.completionItemKind`) ----@return (`vim.lsp.protocol.completionItemKind`) ----@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion -function M._get_completion_item_kind_name(completion_item_kind) - return protocol.CompletionItemKind[completion_item_kind] or 'Unknown' -end - --- Turns the result of a `textDocument/completion` request into vim-compatible --- |complete-items|. --- +---@deprecated ---@param result table The result of a `textDocument/completion` call, e.g. --- from |vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`, --- `CompletionList` or `null` @@ -640,6 +634,7 @@ end ---@return table[] items ---@see complete-items function M.text_document_completion_list_to_complete_items(result, prefix) + vim.deprecate('vim.lsp.util.text_document_completion_list_to_complete_items', nil, '0.11') return require('vim.lsp._completion')._lsp_to_complete_items(result, prefix) end -- cgit