diff options
Diffstat (limited to 'runtime/lua/vim/lsp/util.lua')
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 715 |
1 files changed, 361 insertions, 354 deletions
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 5a68138f1e..3deec6d74e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -5,46 +5,41 @@ local api = vim.api local list_extend = vim.list_extend local highlight = require 'vim.highlight' +local npcall = vim.F.npcall +local split = vim.split + +local _warned = {} +local warn_once = function(message) + if not _warned[message] then + vim.api.nvim_err_writeln(message) + _warned[message] = true + end +end + local M = {} ---- Diagnostics received from the server via `textDocument/publishDiagnostics` --- by buffer. --- --- {<bufnr>: {diagnostics}} --- --- This contains only entries for active buffers. Entries for detached buffers --- are discarded. --- --- If you override the `textDocument/publishDiagnostic` callback, --- this will be empty unless you call `buf_diagnostics_save_positions`. --- --- --- Diagnostic is: --- --- { --- range: Range --- message: string --- severity?: DiagnosticSeverity --- code?: number | string --- source?: string --- tags?: DiagnosticTag[] --- relatedInformation?: DiagnosticRelatedInformation[] --- } -M.diagnostics_by_buf = {} +-- TODO(remove-callbacks) +M.diagnostics_by_buf = setmetatable({}, { + __index = function(_, bufnr) + warn_once("diagnostics_by_buf is deprecated. Use 'vim.lsp.diagnostic.get'") + return vim.lsp.diagnostic.get(bufnr) + end +}) -local split = vim.split +--@private local function split_lines(value) return split(value, '\n', true) end -local function ok_or_nil(status, ...) - if not status then return end - return ... -end -local function npcall(fn, ...) - return ok_or_nil(pcall(fn, ...)) -end - +--- Replaces text in a range with new text. +--- +--- 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 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) -- 0-indexing to 1-indexing local i_0 = A[1] + 1 @@ -78,6 +73,7 @@ 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) @@ -91,13 +87,15 @@ local function sort_by_key(fn) return false end end +--@private local edit_sort_key = sort_by_key(function(e) - return {e.A[1], e.A[2], -e.i} + return {e.A[1], e.A[2], e.i} end) +--@private --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position --- Returns a zero-indexed column, since set_lines() does the conversion to --- 1-indexed +--- Returns a zero-indexed column, since set_lines() does the conversion to +--- 1-indexed local function get_line_byte_from_position(bufnr, position) -- LSP's line and characters are 0-indexed -- Vim's line and columns are 1-indexed @@ -105,15 +103,26 @@ local function get_line_byte_from_position(bufnr, position) -- When on the first character, we can ignore the difference between byte and -- character if col > 0 then + if not api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end + local line = position.line local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false) if #lines > 0 then - return vim.str_byteindex(lines[1], col) + local ok, result = pcall(vim.str_byteindex, lines[1], col) + + if ok then + return result + end end end return col end +--- Applies a list of text edits to a buffer. +--@param text_edits (table) list of `TextEdit` objects +--@param buf_nr (number) Buffer id function M.apply_text_edits(text_edits, bufnr) if not next(text_edits) then return end if not api.nvim_buf_is_loaded(bufnr) then @@ -168,39 +177,53 @@ end -- function M.glob_to_regex(glob) -- end --- textDocument/completion response returns one of CompletionItem[], CompletionList or null. --- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion +--- 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 +--@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 + -- result is a `CompletionList` return result.items elseif result ~= nil then + -- result is `CompletionItem[]` return result else + -- result is `null` return {} end end ---- Apply the TextDocumentEdit response. --- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification +--- Applies a `TextDocumentEdit`, which is a list of changes to a single +-- document. +--- +--@param text_document_edit (table) a `TextDocumentEdit` object +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit function M.apply_text_document_edit(text_document_edit) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) - if text_document.version then - -- `VersionedTextDocumentIdentifier`s version may be null https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier - if text_document.version ~= vim.NIL and M.buf_versions[bufnr] ~= nil and M.buf_versions[bufnr] > text_document.version then - print("Buffer ", text_document.uri, " newer than edits.") - return - end + + -- `VersionedTextDocumentIdentifier`s version may be null + -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier + if text_document.version + and M.buf_versions[bufnr] + and M.buf_versions[bufnr] > text_document.version then + print("Buffer ", text_document.uri, " newer than edits.") + return end - M.apply_text_edits(text_document_edit.edits, bufnr) -end -function M.get_current_line_to_cursor() - local pos = api.nvim_win_get_cursor(0) - local line = assert(api.nvim_buf_get_lines(0, pos[1]-1, pos[1], false)[1]) - return line:sub(pos[2]+1) + M.apply_text_edits(text_document_edit.edits, bufnr) end +--@private +--- Recursively parses snippets in a completion entry. +--- +--@param input (string) Snippet text to parse for snippets +--@param inner (bool) Whether this function is being called recursively +--@returns 2-tuple of strings: The first is the parsed result, the second is the +---unparsed rest of the input local function parse_snippet_rec(input, inner) local res = "" @@ -254,25 +277,30 @@ local function parse_snippet_rec(input, inner) return res, input end --- Parse completion entries, consuming snippet tokens +--- Parses snippets in a completion entry. +--- +--@param input (string) unparsed snippet +--@returns (string) parsed snippet function M.parse_snippet(input) local res, _ = parse_snippet_rec(input, false) return res end --- Sort by CompletionItem.sortText --- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +--@private +--- Sorts by CompletionItem.sortText. +--- +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion local function sort_completion_items(items) - if items[1] and items[1].sortText then - table.sort(items, function(a, b) return a.sortText < b.sortText - end) - end + 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 --- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +--@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 local function get_completion_word(item) if item.textEdit ~= nil and item.textEdit.newText ~= nil then if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then @@ -290,8 +318,10 @@ local function get_completion_word(item) 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. +--@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. local function remove_unmatch_completion_items(items, prefix) return vim.tbl_filter(function(item) local word = get_completion_word(item) @@ -299,16 +329,26 @@ local function remove_unmatch_completion_items(items, prefix) end, items) end --- Acording to LSP spec, if the client set "completionItemKind.valueSet", --- the client must handle it properly even if it receives a value outside the specification. --- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +--- Acording 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`) +--@returns (`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 ---- Getting vim complete-items with incomplete flag. --- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) --- @return { matches = complete-items table, incomplete = boolean } +--- 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 +---|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| function M.text_document_completion_list_to_complete_items(result, prefix) local items = M.extract_completion_items(result) if vim.tbl_isempty(items) then @@ -356,7 +396,10 @@ function M.text_document_completion_list_to_complete_items(result, prefix) return matches end --- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification +--- Applies a `WorkspaceEdit`. +--- +--@param workspace_edit (table) `WorkspaceEdit` +-- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit function M.apply_workspace_edit(workspace_edit) if workspace_edit.documentChanges then for _, change in ipairs(workspace_edit.documentChanges) do @@ -381,9 +424,15 @@ function M.apply_workspace_edit(workspace_edit) end end ---- Convert any of MarkedString | MarkedString[] | MarkupContent into markdown text lines --- see https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_hover --- Useful for textDocument/hover, textDocument/signatureHelp, and potentially others. +--- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into +--- a list of lines containing valid markdown. Useful to populate the hover +--- window for `textDocument/hover`, for parsing the result of +--- `textDocument/signatureHelp`, and potentially others. +--- +--@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`) +--@param contents (table, optional, default `{}`) List of strings to extend with converted lines +--@returns {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 {} -- MarkedString variation 1 @@ -422,8 +471,11 @@ function M.convert_input_to_markdown_lines(input, contents) return contents end ---- Convert SignatureHelp response to markdown lines. --- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp +--- Converts `textDocument/SignatureHelp` response to markdown lines. +--- +--@param signature_help Response of `textDocument/SignatureHelp` +--@returns list of lines of converted markdown. +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp function M.convert_signature_help_to_markdown_lines(signature_help) if not signature_help.signatures then return @@ -446,13 +498,13 @@ function M.convert_signature_help_to_markdown_lines(signature_help) if signature.documentation then M.convert_input_to_markdown_lines(signature.documentation, contents) end - if signature_help.parameters then + if signature.parameters and #signature.parameters > 0 then local active_parameter = signature_help.activeParameter or 0 -- If the activeParameter is not inside the valid range, then clip it. - if active_parameter >= #signature_help.parameters then + if active_parameter >= #signature.parameters then active_parameter = 0 end - local parameter = signature.parameters and signature.parameters[active_parameter] + local parameter = signature.parameters[active_parameter + 1] if parameter then --[=[ --Represents a parameter of a callable-signature. A parameter can @@ -474,13 +526,20 @@ function M.convert_signature_help_to_markdown_lines(signature_help) --]=] -- TODO highlight parameter if parameter.documentation then - M.convert_input_help_to_markdown_lines(parameter.documentation, contents) + M.convert_input_to_markdown_lines(parameter.documentation, contents) end end end return contents 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 opts (table, optional) +--@returns (table) Options function M.make_floating_popup_options(width, height, opts) validate { opts = { opts, 't', true }; @@ -526,6 +585,10 @@ function M.make_floating_popup_options(width, height, opts) } end +--- Jumps to a location. +--- +--@param location (`Location`|`LocationLink`) +--@returns `true` if the jump succeeded function M.jump_to_location(location) -- location may be Location or LocationLink local uri = location.uri or location.targetUri @@ -549,14 +612,14 @@ function M.jump_to_location(location) return true end ---- Preview a location in a floating windows +--- Previews a location in a floating window --- --- behavior depends on type of location: --- - 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 ---@return bufnr,winnr buffer and window number of floating window or nil +--@param location a single `Location` or `LocationLink` +--@returns (bufnr,winnr) buffer and window number of floating window or nil function M.preview_location(location) -- location may be LocationLink or Location (more useful for the former) local uri = location.targetUri or location.uri @@ -571,6 +634,7 @@ function M.preview_location(location) return M.open_floating_preview(contents, filetype) 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 @@ -579,19 +643,25 @@ local function find_window_by_var(name, value) end end --- Check if a window with `unique_name` tagged is associated with the current --- buffer. If not, make a new preview. --- --- fn()'s return bufnr, winnr --- case that a new floating window should be created. +--- Enters/leaves the focusable window associated with the current buffer via the +--window - variable `unique_name`. If no such window exists, run the function +--{fn}. +--- +--@param unique_name (string) Window variable +--@param fn (function) should return create a new window and return a tuple of +---({focusable_buffer_id}, {window_id}). if {focusable_buffer_id} is a valid +---buffer id, the newly created window will be the new focus associated with +---the current buffer via the tag `unique_name`. +--@returns (pbufnr, pwinnr) if `fn()` has created a new window; nil otherwise function M.focusable_float(unique_name, fn) + -- Go back to previous window if we are in a focusable one if npcall(api.nvim_win_get_var, 0, unique_name) then return api.nvim_command("wincmd p") end local bufnr = api.nvim_get_current_buf() do local win = find_window_by_var(unique_name, bufnr) - if win then + if win and api.nvim_win_is_valid(win) and not vim.fn.pumvisible() then api.nvim_set_current_win(win) api.nvim_command("stopinsert") return @@ -604,26 +674,29 @@ function M.focusable_float(unique_name, fn) end end --- Check if a window with `unique_name` tagged is associated with the current --- buffer. If not, make a new preview. --- --- fn()'s return values will be passed directly to open_floating_preview in the --- case that a new floating window should be created. +--- Focuses/unfocuses the floating preview window associated with the current +--- buffer via the window variable `unique_name`. If no such preview window +--- exists, makes a new one. +--- +--@param unique_name (string) Window variable +--@param fn (function) The return values of this function will be passed +---directly to |vim.lsp.util.open_floating_preview()|, in the case that a new +---floating window should be created function M.focusable_preview(unique_name, fn) return M.focusable_float(unique_name, function() return M.open_floating_preview(fn()) end) end ---- Trim empty lines from input and pad left and right with spaces +--- Trims empty lines from input and pad left and right with spaces --- ---@param contents table of lines to trim and pad ---@param opts dictionary with optional fields --- - pad_left number of columns to pad contents at left (default 1) --- - pad_right number of columns to pad contents at right (default 1) --- - 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 +---@param contents table of lines to trim and pad +---@param opts dictionary with optional fields +--- - pad_left number of columns to pad contents at left (default 1) +--- - pad_right number of columns to pad contents at right (default 1) +--- - 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 function M._trim_and_pad(contents, opts) validate { contents = { contents, 't' }; @@ -651,26 +724,27 @@ end ---- Convert markdown into syntax highlighted regions by stripping the code +-- TODO: refactor to separate stripping/converting and make use of open_floating_preview +-- +--- Converts markdown into syntax highlighted regions by stripping the code --- blocks and converting them into highlighted code. --- This will by default insert a blank line separator after those code block --- regions to improve readability. ---- The result is shown in a floating preview ---- TODO: refactor to separate stripping/converting and make use of open_floating_preview +--- The result is shown in a floating preview. --- ---@param contents table of lines to show in window ---@param opts dictionary 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 --- - pad_left number of columns to pad contents at left --- - pad_right number of columns to pad contents at right --- - 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 width,height size of float +---@param contents table of lines to show in window +---@param opts dictionary 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 +--- - pad_left number of columns to pad contents at left +--- - pad_right number of columns to pad contents at right +--- - 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 function M.fancy_floating_markdown(contents, opts) validate { contents = { contents, 't' }; @@ -719,13 +793,14 @@ function M.fancy_floating_markdown(contents, opts) local width, height = M._make_floating_popup_size(stripped, opts) -- Insert blank line separator after code block - local insert_separator = opts.separator or true + local insert_separator = opts.separator + if insert_separator == nil then insert_separator = true end if insert_separator then for i, h in ipairs(highlights) do h.start = h.start + i - 1 h.finish = h.finish + i - 1 if h.finish + 1 <= #stripped then - table.insert(stripped, h.finish + 1, string.rep("─", width)) + table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width))) height = height + 1 end end @@ -744,6 +819,7 @@ function M.fancy_floating_markdown(contents, opts) vim.cmd("ownsyntax markdown") local idx = 1 + --@private local function apply_syntax_to_region(ft, start, finish) if ft == '' then return end local name = ft..idx @@ -769,11 +845,17 @@ function M.fancy_floating_markdown(contents, opts) return bufnr, winnr 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 +--@see |autocmd-events| function M.close_preview_autocmd(events, winnr) api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)") end ---- Compute size of float needed to show contents (with optional wrapping) +--@internal +--- 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 @@ -782,7 +864,7 @@ end -- - wrap_at character to wrap at for computing height -- - max_width maximal width of floating window -- - max_height maximal height of floating window ---@return width,height size of float +--@returns width,height size of float function M._make_floating_popup_size(contents, opts) validate { contents = { contents, 't' }; @@ -833,7 +915,7 @@ function M._make_floating_popup_size(contents, opts) return width, height end ---- Show contents in a floating window +--- Shows contents in a floating window. --- --@param contents table of lines to show in window --@param filetype string of filetype to set for opened buffer @@ -847,7 +929,8 @@ end -- - pad_right number of columns to pad contents at right -- - pad_top number of lines to pad contents at top -- - pad_bottom number of lines to pad contents at bottom ---@return bufnr,winnr buffer and window number of floating window or nil +--@returns bufnr,winnr buffer and window number of the newly created floating +---preview window function M.open_floating_preview(contents, filetype, opts) validate { contents = { contents, 't' }; @@ -878,159 +961,93 @@ function M.open_floating_preview(contents, filetype, opts) return floating_bufnr, floating_winnr end +-- TODO(remove-callbacks) do - local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") - local reference_ns = api.nvim_create_namespace("vim_lsp_references") - local sign_ns = 'vim_lsp_signs' - local underline_highlight_name = "LspDiagnosticsUnderline" - vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name)) - for kind, _ in pairs(protocol.DiagnosticSeverity) do - if type(kind) == 'string' then - vim.cmd(string.format("highlight default link %s%s %s", underline_highlight_name, kind, underline_highlight_name)) - end + --@deprecated + function M.get_severity_highlight_name(severity) + warn_once("vim.lsp.util.get_severity_highlight_name is deprecated.") + return vim.lsp.diagnostic._get_severity_highlight_name(severity) end - local severity_highlights = {} + --@deprecated + function M.buf_clear_diagnostics(bufnr, client_id) + warn_once("buf_clear_diagnostics is deprecated. Use vim.lsp.diagnostic.clear") + return vim.lsp.diagnostic.clear(bufnr, client_id) + end - local severity_floating_highlights = {} + --@deprecated + function M.get_line_diagnostics() + warn_once("get_line_diagnostics is deprecated. Use vim.lsp.diagnostic.get_line_diagnostics") - local default_severity_highlight = { - [protocol.DiagnosticSeverity.Error] = { guifg = "Red" }; - [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" }; - [protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" }; - [protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" }; - } + local bufnr = api.nvim_get_current_buf() + local line_nr = api.nvim_win_get_cursor(0)[1] - 1 - -- Initialize default severity highlights - for severity, hi_info in pairs(default_severity_highlight) do - local severity_name = protocol.DiagnosticSeverity[severity] - local highlight_name = "LspDiagnostics"..severity_name - local floating_highlight_name = highlight_name.."Floating" - -- Try to fill in the foreground color with a sane default. - local cmd_parts = {"highlight", "default", highlight_name} - for k, v in pairs(hi_info) do - table.insert(cmd_parts, k.."="..v) - end - api.nvim_command(table.concat(cmd_parts, ' ')) - api.nvim_command('highlight link ' .. highlight_name .. 'Sign ' .. highlight_name) - api.nvim_command('highlight link ' .. highlight_name .. 'Floating ' .. highlight_name) - severity_highlights[severity] = highlight_name - severity_floating_highlights[severity] = floating_highlight_name + return vim.lsp.diagnostic.get_line_diagnostics(bufnr, line_nr) end - function M.buf_clear_diagnostics(bufnr) - validate { bufnr = {bufnr, 'n', true} } - bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr + --@deprecated + function M.show_line_diagnostics() + warn_once("show_line_diagnostics is deprecated. Use vim.lsp.diagnostic.show_line_diagnostics") - -- clear sign group - vim.fn.sign_unplace(sign_ns, {buffer=bufnr}) + local bufnr = api.nvim_get_current_buf() + local line_nr = api.nvim_win_get_cursor(0)[1] - 1 - -- clear virtual text namespace - api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) + return vim.lsp.diagnostic.show_line_diagnostics(bufnr, line_nr) end - function M.get_severity_highlight_name(severity) - return severity_highlights[severity] + --@deprecated + function M.buf_diagnostics_save_positions(bufnr, diagnostics, client_id) + warn_once("buf_diagnostics_save_positions is deprecated. Use vim.lsp.diagnostic.save") + return vim.lsp.diagnostic.save(diagnostics, bufnr, client_id) end - function M.get_line_diagnostics() - local bufnr = api.nvim_get_current_buf() - local linenr = api.nvim_win_get_cursor(0)[1] - 1 - - local buffer_diagnostics = M.diagnostics_by_buf[bufnr] - - if not buffer_diagnostics then - return {} - end + --@deprecated + function M.buf_diagnostics_get_positions(bufnr, client_id) + warn_once("buf_diagnostics_get_positions is deprecated. Use vim.lsp.diagnostic.get") + return vim.lsp.diagnostic.get(bufnr, client_id) + end - local diagnostics_by_line = M.diagnostics_group_by_line(buffer_diagnostics) - return diagnostics_by_line[linenr] or {} + --@deprecated + function M.buf_diagnostics_underline(bufnr, diagnostics, client_id) + warn_once("buf_diagnostics_underline is deprecated. Use 'vim.lsp.diagnostic.set_underline'") + return vim.lsp.diagnostic.set_underline(diagnostics, bufnr, client_id) end - function M.show_line_diagnostics() - -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {}) - -- if #marks == 0 then - -- return - -- end - local lines = {"Diagnostics:"} - local highlights = {{0, "Bold"}} - local line_diagnostics = M.get_line_diagnostics() - if vim.tbl_isempty(line_diagnostics) then return end - - for i, diagnostic in ipairs(line_diagnostics) do - -- for i, mark in ipairs(marks) do - -- local mark_id = mark[1] - -- local diagnostic = buffer_diagnostics[mark_id] - - -- TODO(ashkan) make format configurable? - local prefix = string.format("%d. ", i) - local hiname = severity_floating_highlights[diagnostic.severity] - assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity)) - local message_lines = split_lines(diagnostic.message) - table.insert(lines, prefix..message_lines[1]) - table.insert(highlights, {#prefix + 1, hiname}) - for j = 2, #message_lines do - table.insert(lines, message_lines[j]) - table.insert(highlights, {0, hiname}) - end - end - local popup_bufnr, winnr = M.open_floating_preview(lines, 'plaintext') - for i, hi in ipairs(highlights) do - local prefixlen, hiname = unpack(hi) - -- Start highlight after the prefix - api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1) - end - return popup_bufnr, winnr + --@deprecated + function M.buf_diagnostics_virtual_text(bufnr, diagnostics, client_id) + warn_once("buf_diagnostics_virtual_text is deprecated. Use 'vim.lsp.diagnostic.set_virtual_text'") + return vim.lsp.diagnostic.set_virtual_text(diagnostics, bufnr, client_id) end - --- Saves the diagnostics (Diagnostic[]) into diagnostics_by_buf - --- - --@param bufnr bufnr for which the diagnostics are for. - --@param diagnostics Diagnostics[] received from the language server. - function M.buf_diagnostics_save_positions(bufnr, diagnostics) - validate { - bufnr = {bufnr, 'n', true}; - diagnostics = {diagnostics, 't', true}; - } - if not diagnostics then return end - bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr - - if not M.diagnostics_by_buf[bufnr] then - -- Clean up our data when the buffer unloads. - api.nvim_buf_attach(bufnr, false, { - on_detach = function(b) - M.diagnostics_by_buf[b] = nil - end - }) - end - M.diagnostics_by_buf[bufnr] = diagnostics + --@deprecated + function M.buf_diagnostics_signs(bufnr, diagnostics, client_id) + warn_once("buf_diagnostics_signs is deprecated. Use 'vim.lsp.diagnostics.set_signs'") + return vim.lsp.diagnostic.set_signs(diagnostics, bufnr, client_id) end - function M.buf_diagnostics_underline(bufnr, diagnostics) - for _, diagnostic in ipairs(diagnostics) do - local start = diagnostic.range["start"] - local finish = diagnostic.range["end"] + --@deprecated + function M.buf_diagnostics_count(kind, client_id) + warn_once("buf_diagnostics_count is deprecated. Use 'vim.lsp.diagnostic.get_count'") + return vim.lsp.diagnostic.get_count(vim.api.nvim_get_current_buf(), client_id, kind) + end - local hlmap = { - [protocol.DiagnosticSeverity.Error]='Error', - [protocol.DiagnosticSeverity.Warning]='Warning', - [protocol.DiagnosticSeverity.Information]='Information', - [protocol.DiagnosticSeverity.Hint]='Hint', - } +end - highlight.range(bufnr, diagnostic_ns, - underline_highlight_name..hlmap[diagnostic.severity], - {start.line, start.character}, - {finish.line, finish.character} - ) - end - end +do --[[ References ]] + local reference_ns = api.nvim_create_namespace("vim_lsp_references") + --- Removes document highlights from a buffer. + --- + --@param bufnr buffer id function M.buf_clear_references(bufnr) validate { bufnr = {bufnr, 'n', true} } api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1) end + --- Shows a list of document highlights for a certain buffer. + --- + --@param bufnr buffer id + --@param references List of `DocumentHighlight` objects to highlight function M.buf_highlight_references(bufnr, references) validate { bufnr = {bufnr, 'n', true} } for _, reference in ipairs(references) do @@ -1045,105 +1062,17 @@ do highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos) end end - - function M.diagnostics_group_by_line(diagnostics) - if not diagnostics then return end - local diagnostics_by_line = {} - for _, diagnostic in ipairs(diagnostics) do - local start = diagnostic.range.start - local line_diagnostics = diagnostics_by_line[start.line] - if not line_diagnostics then - line_diagnostics = {} - diagnostics_by_line[start.line] = line_diagnostics - end - table.insert(line_diagnostics, diagnostic) - end - return diagnostics_by_line - end - - function M.buf_diagnostics_virtual_text(bufnr, diagnostics) - if not diagnostics then - return - end - local buffer_line_diagnostics = M.diagnostics_group_by_line(diagnostics) - for line, line_diags in pairs(buffer_line_diagnostics) do - local virt_texts = {} - for i = 1, #line_diags - 1 do - table.insert(virt_texts, {"■", severity_highlights[line_diags[i].severity]}) - end - local last = line_diags[#line_diags] - -- TODO(ashkan) use first line instead of subbing 2 spaces? - table.insert(virt_texts, {"■ "..last.message:gsub("\r", ""):gsub("\n", " "), severity_highlights[last.severity]}) - api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {}) - end - end - - --- Returns the number of diagnostics of given kind for current buffer. - --- - --- Useful for showing diagnostic counts in statusline. eg: - --- - --- <pre> - --- function! LspStatus() abort - --- let sl = '' - --- if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))') - --- let sl.='%#MyStatuslineLSP#E:' - --- let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Error]])")}' - --- let sl.='%#MyStatuslineLSP# W:' - --- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Warning]])")}' - --- else - --- let sl.='%#MyStatuslineLSPErrors#off' - --- endif - --- return sl - --- endfunction - --- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus() - --- </pre> - --- - --@param kind Diagnostic severity kind: See |vim.lsp.protocol.DiagnosticSeverity| - --- - --@return Count of diagnostics - function M.buf_diagnostics_count(kind) - local bufnr = vim.api.nvim_get_current_buf() - local diagnostics = M.diagnostics_by_buf[bufnr] - if not diagnostics then return end - local count = 0 - for _, diagnostic in pairs(diagnostics) do - if protocol.DiagnosticSeverity[kind] == diagnostic.severity then - count = count + 1 - end - end - return count - end - - local diagnostic_severity_map = { - [protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign"; - [protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign"; - [protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign"; - [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign"; - } - - --- Place signs for each diagnostic in the sign column. - --- - --- Sign characters can be customized with the following commands: - --- - --- <pre> - --- sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl= - --- sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl= - --- sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl= - --- sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl= - --- </pre> - function M.buf_diagnostics_signs(bufnr, diagnostics) - for _, diagnostic in ipairs(diagnostics) do - vim.fn.sign_place(0, sign_ns, diagnostic_severity_map[diagnostic.severity], bufnr, {lnum=(diagnostic.range.start.line+1)}) - end - end end local position_sort = sort_by_key(function(v) return {v.start.line, v.start.character} end) --- Returns the items with the byte position calculated correctly and in sorted --- order. +--- Returns the items with the byte position calculated correctly and in sorted +--- order, for display in quickfix and location lists. +--- +--@param locations (table) list of `Location`s or `LocationLink`s +--@returns (table) list of items function M.locations_to_items(locations) local items = {} local grouped = setmetatable({}, { @@ -1186,6 +1115,10 @@ function M.locations_to_items(locations) return items end +--- Fills current window's location list with given list of items. +--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. +--- +--@param items (table) list of items function M.set_loclist(items) vim.fn.setloclist(0, {}, ' ', { title = 'Language Server'; @@ -1193,6 +1126,10 @@ function M.set_loclist(items) }) end +--- Fills quickfix list with given list of items. +--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. +--- +--@param items (table) list of items function M.set_qflist(items) vim.fn.setqflist({}, ' ', { title = 'Language Server'; @@ -1207,10 +1144,11 @@ function M._get_symbol_kind_name(symbol_kind) return protocol.SymbolKind[symbol_kind] or "Unknown" end ---- Convert symbols to quickfix list items +--- Converts symbols to quickfix list items. --- --@param symbols 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 @@ -1245,7 +1183,9 @@ function M.symbols_to_items(symbols, bufnr) return _symbols_to_items(symbols, {}, bufnr) end --- Remove empty lines from the beginning and end. +--- Removes empty lines from the beginning and end. +--@param lines (table) list of lines to trim +--@returns (table) trimmed list of lines function M.trim_empty_lines(lines) local start = 1 for i = 1, #lines do @@ -1264,11 +1204,13 @@ function M.trim_empty_lines(lines) return vim.list_extend({}, lines, start, finish) end --- Accepts markdown lines and tries to reduce it to a filetype if it is --- just a single code block. --- Note: This modifies the input. --- --- Returns: filetype or 'markdown' if it was unchanged. +--- Accepts markdown lines and tries to reduce them to a filetype if they +--- comprise just a single code block. +--- +--- CAUTION: Modifies the input in-place! +--- +--@param lines (table) list of lines +--@returns (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 @@ -1291,14 +1233,22 @@ function M.try_trim_markdown_code_blocks(lines) end local str_utfindex = vim.str_utfindex +--@private local function make_position_param() local row, col = unpack(api.nvim_win_get_cursor(0)) row = row - 1 local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] + if not line then + return { line = 0; character = 0; } + end col = str_utfindex(line, col) return { line = row; character = col; } end +--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. +--- +--@returns `TextDocumentPositionParams` object +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams function M.make_position_params() return { textDocument = M.make_text_document_params(); @@ -1306,19 +1256,65 @@ function M.make_position_params() } end +--- Using the current position in the current buffer, creates an object that +--- can be used as a building block for several LSP requests, such as +--- `textDocument/codeAction`, `textDocument/colorPresentation`, +--- `textDocument/rangeFormatting`. +--- +--@returns { textDocument = { uri = `current_file_uri` }, range = { start = +---`current_position`, end = `current_position` } } function M.make_range_params() local position = make_position_param() return { - textDocument = { uri = vim.uri_from_bufnr(0) }, + textDocument = M.make_text_document_params(), range = { start = position; ["end"] = position; } } 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, number}, optional) mark-indexed position. +---Defaults to the start of the last visual selection. +--@param end_pos ({number, number}, optional) mark-indexed position. +---Defaults to the end of the last visual selection. +--@returns { textDocument = { uri = `current_file_uri` }, range = { start = +---`start_position`, end = `end_position` } } +function M.make_given_range_params(start_pos, end_pos) + validate { + start_pos = {start_pos, 't', true}; + end_pos = {end_pos, 't', true}; + } + local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<')) + local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>')) + -- convert to 0-index + A[1] = A[1] - 1 + B[1] = B[1] - 1 + -- account for encoding. + if A[2] > 0 then + A = {A[1], M.character_offset(0, A[1], A[2])} + end + if B[2] > 0 then + B = {B[1], M.character_offset(0, B[1], B[2])} + end + return { + textDocument = M.make_text_document_params(), + range = { + start = {line = A[1], character = A[2]}, + ['end'] = {line = B[1], character = B[2]} + } + } +end + +--- Creates a `TextDocumentIdentifier` object for the current buffer. +--- +--@returns `TextDocumentIdentifier` +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier function M.make_text_document_params() return { uri = vim.uri_from_bufnr(0) } end ---- Get visual width of tabstop. +--- Returns visual width of tabstop. --- --@see |softtabstop| --@param bufnr (optional, number): Buffer handle, defaults to current @@ -1330,6 +1326,11 @@ function M.get_effective_tabstop(bufnr) return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop end +--- Creates a `FormattingOptions` object for the current buffer and cursor position. +--- +--@param options Table with valid `FormattingOptions` entries +--@returns `FormattingOptions 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} } options = vim.tbl_extend('keep', options or {}, { @@ -1342,9 +1343,12 @@ function M.make_formatting_params(options) } end --- @param buf buffer handle or 0 for current. --- @param row 0-indexed line --- @param col 0-indexed byte offset in line +--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer. +--- +--@param buf buffer id (0 for current) +--@param row 0-indexed line +--@param col 0-indexed byte offset in line +--@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf} function M.character_offset(buf, row, col) local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] -- If the col is past the EOL, use the line length. @@ -1354,6 +1358,9 @@ function M.character_offset(buf, row, col) return str_utfindex(line, col) end +M._get_line_byte_from_position = get_line_byte_from_position +M._warn_once = warn_once + M.buf_versions = {} return M |