From 8450752f46e1482bf34b7f05e484cca740f61075 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 9 Oct 2024 08:15:14 +0800 Subject: vim-patch:9.1.0771: completion attribute hl_group is confusing Problem: Currently completion attribute hl_group is combined with all items, which is redundant and confusing with kind_hlgroup Solution: Renamed to abbr_hlgroup and combine it only with the abbr item (glepnir). closes: vim/vim#15818 https://github.com/vim/vim/commit/0fe17f8ffbd2588ecd2bf42dced556897bc64f89 Co-authored-by: glepnir --- runtime/lua/vim/lsp/completion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 71ea2df100..e74f993356 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -272,7 +272,7 @@ function M._lsp_to_complete_items(result, prefix, client_id) icase = 1, dup = 1, empty = 1, - hl_group = hl_group, + abbr_hlgroup = hl_group, user_data = { nvim = { lsp = { -- cgit From b3109084c2c3675aa886bf16ff15f50025f30096 Mon Sep 17 00:00:00 2001 From: Tomasz N Date: Thu, 10 Oct 2024 11:40:03 +0200 Subject: fix(lsp): fix cursor position after snippet expansion (#30659) Problem: on `CompleteDone` cursor can jump to the end of line instead of the end of the completed word. Solution: remove only inserted word for snippet expansion instead of everything until eol. Fixes #30656 Co-authored-by: Mathias Fussenegger Co-authored-by: Justin M. Keyes --- runtime/lua/vim/lsp/completion.lua | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index e74f993356..14fad987e0 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -113,12 +113,11 @@ local function parse_snippet(input) end --- @param item lsp.CompletionItem ---- @param suffix? string -local function apply_snippet(item, suffix) +local function apply_snippet(item) if item.textEdit then - vim.snippet.expand(item.textEdit.newText .. suffix) + vim.snippet.expand(item.textEdit.newText) elseif item.insertText then - vim.snippet.expand(item.insertText .. suffix) + vim.snippet.expand(item.insertText) end end @@ -539,15 +538,12 @@ local function on_complete_done() -- Remove the already inserted word. local start_char = cursor_col - #completed_item.word - local line = api.nvim_buf_get_lines(bufnr, cursor_row, cursor_row + 1, true)[1] - api.nvim_buf_set_text(bufnr, cursor_row, start_char, cursor_row, #line, { '' }) - return line:sub(cursor_col + 1) + api.nvim_buf_set_text(bufnr, cursor_row, start_char, cursor_row, cursor_col, { '' }) end - --- @param suffix? string - local function apply_snippet_and_command(suffix) + local function apply_snippet_and_command() if expand_snippet then - apply_snippet(completion_item, suffix) + apply_snippet(completion_item) end local command = completion_item.command @@ -565,9 +561,9 @@ local function on_complete_done() end if completion_item.additionalTextEdits and next(completion_item.additionalTextEdits) then - local suffix = clear_word() + clear_word() lsp.util.apply_text_edits(completion_item.additionalTextEdits, bufnr, offset_encoding) - apply_snippet_and_command(suffix) + apply_snippet_and_command() elseif resolve_provider and type(completion_item) == 'table' then local changedtick = vim.b[bufnr].changedtick @@ -577,7 +573,7 @@ local function on_complete_done() return end - local suffix = clear_word() + clear_word() if err then vim.notify_once(err.message, vim.log.levels.WARN) elseif result and result.additionalTextEdits then @@ -587,11 +583,11 @@ local function on_complete_done() end end - apply_snippet_and_command(suffix) + apply_snippet_and_command() end, bufnr) else - local suffix = clear_word() - apply_snippet_and_command(suffix) + clear_word() + apply_snippet_and_command() end end -- cgit From c3cb702ac7a5d6a1515129c80ce143e3bdf5d739 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Fri, 11 Oct 2024 00:50:09 -0700 Subject: fix(lsp): set 'smoothscroll' in docs hover #30748 --- runtime/lua/vim/lsp/util.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 882ec22ca6..ebc3f7c55e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1656,6 +1656,7 @@ function M.open_floating_preview(contents, syntax, opts) vim.wo[floating_winnr].foldenable = false -- Disable folding. vim.wo[floating_winnr].wrap = opts.wrap -- Soft wrapping. vim.wo[floating_winnr].breakindent = true -- Slightly better list presentation. + vim.wo[floating_winnr].smoothscroll = true -- Scroll by screen-line instead of buffer-line. vim.bo[floating_bufnr].modifiable = false vim.bo[floating_bufnr].bufhidden = 'wipe' -- cgit From 2d24558c285198d2e725c83a8c4ad4d90797c950 Mon Sep 17 00:00:00 2001 From: glepnir Date: Sun, 13 Oct 2024 18:44:05 +0800 Subject: docs: update autotrigger description of vim.lsp.compleiton.BufferOpts (#30796) Currently the behavior of autotrigger is not well documented. --- runtime/lua/vim/lsp/completion.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 14fad987e0..3df506d23a 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -592,7 +592,7 @@ local function on_complete_done() end --- @class vim.lsp.completion.BufferOpts ---- @field autotrigger? boolean Whether to trigger completion automatically. Default: false +--- @field autotrigger? boolean Default: false When true, completion triggers automatically based on the server's `triggerCharacters`. --- @field convert? fun(item: lsp.CompletionItem): table Transforms an LSP CompletionItem to |complete-items|. ---@param client_id integer -- cgit From e0a5c3bb581752569df4490b48cb54e7c1ab0613 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Tue, 15 Oct 2024 02:36:04 -0700 Subject: fix(lsp): handle multiline signature help labels #30460 --- runtime/lua/vim/lsp/handlers.lua | 15 +++++-- runtime/lua/vim/lsp/util.lua | 93 +++++++++++++++++++++++++--------------- 2 files changed, 69 insertions(+), 39 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 44548fec92..8e538242d9 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -462,6 +462,8 @@ M[ms.textDocument_typeDefinition] = location_handler --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation M[ms.textDocument_implementation] = location_handler +local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') + --- |lsp-handler| for the method "textDocument/signatureHelp". --- --- The active parameter is highlighted with |hl-LspSignatureActiveParameter|. @@ -476,7 +478,7 @@ M[ms.textDocument_implementation] = location_handler --- ``` --- ---@param _ lsp.ResponseError? ----@param result lsp.SignatureHelp Response from the language server +---@param result lsp.SignatureHelp? Response from the language server ---@param ctx lsp.HandlerContext Client context ---@param config table Configuration table. --- - border: (default=nil) @@ -509,10 +511,15 @@ function M.signature_help(_, result, ctx, config) return end local fbuf, fwin = util.open_floating_preview(lines, 'markdown', config) + -- Highlight the active parameter. if hl then - -- Highlight the second line if the signature is wrapped in a Markdown code block. - local line = vim.startswith(lines[1], '```') and 1 or 0 - api.nvim_buf_add_highlight(fbuf, -1, 'LspSignatureActiveParameter', line, unpack(hl)) + vim.highlight.range( + fbuf, + sig_help_ns, + 'LspSignatureActiveParameter', + { hl[1], hl[2] }, + { hl[3], hl[4] } + ) end return fbuf, fwin end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index ebc3f7c55e..fc822f1403 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -781,24 +781,37 @@ function M.convert_input_to_markdown_lines(input, contents) return contents end +--- Returns the line/column-based position in `contents` at the given offset. +--- +---@param offset integer +---@param contents string[] +---@return { [1]: integer, [2]: integer } +local function get_pos_from_offset(offset, contents) + local i = 0 + for l, line in ipairs(contents) do + if offset >= i and offset < i + #line then + return { l - 1, offset - i + 1 } + else + i = i + #line + 1 + end + end +end + --- Converts `textDocument/signatureHelp` response to markdown lines. --- ---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp` ---@param ft string|nil filetype that will be use as the `lang` for the label markdown code block ---@param triggers table|nil list of trigger characters from the lsp server. used to better determine parameter offsets ---@return string[]|nil table list of lines of converted markdown. ----@return number[]|nil table of active hl +---@return Range4|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 - return - end --The active signature. If omitted or the value lies outside the range of --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`. --Whenever possible implementors should make an active decision about --the active signature and shouldn't rely on a default value. local contents = {} - local active_hl + local active_offset ---@type [integer, integer]|nil local active_signature = signature_help.activeSignature or 0 -- If the activeSignature is not inside the valid range, then clip it. -- In 3.15 of the protocol, activeSignature was allowed to be negative @@ -806,9 +819,6 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers active_signature = 0 end local signature = signature_help.signatures[active_signature + 1] - if not signature then - return - end local label = signature.label if ft then -- wrap inside a code block for proper rendering @@ -825,6 +835,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers M.convert_input_to_markdown_lines(signature.documentation, contents) end if signature.parameters and #signature.parameters > 0 then + -- First check if the signature has an activeParameter. If it doesn't check if the response + -- had that property instead. Else just default to 0. local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0) if active_parameter < 0 then active_parameter = 0 @@ -837,8 +849,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers end local parameter = signature.parameters[active_parameter + 1] - if parameter then - --[=[ + local parameter_label = parameter.label + --[=[ --Represents a parameter of a callable-signature. A parameter can --have a label and a doc-comment. interface ParameterInformation { @@ -856,36 +868,47 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers documentation?: string | MarkupContent; } --]=] - if parameter.label then - if type(parameter.label) == 'table' then - active_hl = parameter.label - else - local offset = 1 - -- try to set the initial offset to the first found trigger character - for _, t in ipairs(triggers or {}) do - local trigger_offset = signature.label:find(t, 1, true) - if trigger_offset and (offset == 1 or trigger_offset < offset) then - offset = trigger_offset - end - end - for p, param in pairs(signature.parameters) do - offset = signature.label:find(param.label, offset, true) - if not offset then - break - end - if p == active_parameter + 1 then - active_hl = { offset - 1, offset + #parameter.label - 1 } - break - end - offset = offset + #param.label + 1 - end + if type(parameter_label) == 'table' then + active_offset = parameter_label + else + local offset = 1 ---@type integer|nil + -- try to set the initial offset to the first found trigger character + for _, t in ipairs(triggers or {}) do + local trigger_offset = signature.label:find(t, 1, true) + if trigger_offset and (offset == 1 or trigger_offset < offset) then + offset = trigger_offset end end - if parameter.documentation then - M.convert_input_to_markdown_lines(parameter.documentation, contents) + for p, param in pairs(signature.parameters) do + offset = signature.label:find(param.label, offset, true) + if not offset then + break + end + if p == active_parameter + 1 then + active_offset = { offset - 1, offset + #parameter_label - 1 } + break + end + offset = offset + #param.label + 1 end end + if parameter.documentation then + M.convert_input_to_markdown_lines(parameter.documentation, contents) + end + end + + local active_hl = nil + if active_offset then + active_hl = {} + -- Account for the start of the markdown block. + if ft then + active_offset[1], active_offset[2] = + active_offset[1] + #contents[1], active_offset[2] + #contents[1] + end + + list_extend(active_hl, get_pos_from_offset(active_offset[1], contents)) + list_extend(active_hl, get_pos_from_offset(active_offset[2], contents)) end + return contents, active_hl end -- cgit From 0066dd0f653e9321ea77fd54a017e96d457307d3 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 15 Oct 2024 16:56:35 +0100 Subject: feat(lsp.util): use faster version of vim.validate --- runtime/lua/vim/lsp/util.lua | 68 ++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 44 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index fc822f1403..15f5b86fec 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -375,11 +375,10 @@ end ---@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) - validate({ - text_edits = { text_edits, 't', false }, - bufnr = { bufnr, 'number', false }, - offset_encoding = { offset_encoding, 'string', false }, - }) + validate('text_edits', text_edits, 'table', false) + validate('bufnr', bufnr, 'number', false) + validate('offset_encoding', offset_encoding, 'string', false) + if not next(text_edits) then return end @@ -932,14 +931,10 @@ end --- to display the full window height. ---@return table Options function M.make_floating_popup_options(width, height, opts) - validate({ - opts = { opts, 't', true }, - }) + validate('opts', opts, 'table', true) opts = opts or {} - validate({ - ['opts.offset_x'] = { opts.offset_x, 'n', true }, - ['opts.offset_y'] = { opts.offset_y, 'n', true }, - }) + validate('opts.offset_x', opts.offset_x, 'n', true) + validate('opts.offset_y', opts.offset_y, 'n', true) local anchor = '' local row, col @@ -1211,10 +1206,8 @@ end --- - separator insert separator after code block ---@return table stripped content function M.stylize_markdown(bufnr, contents, opts) - validate({ - contents = { contents, 't' }, - opts = { opts, 't', true }, - }) + validate('contents', contents, 'table') + validate('opts', opts, 'table', true) opts = opts or {} -- table of fence types to {ft, begin, end} @@ -1413,10 +1406,8 @@ end ---@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 }, - }) + validate('contents', contents, 'table') + validate('opts', opts, 'table', true) opts = opts or {} -- 1. Carriage returns are removed @@ -1493,10 +1484,8 @@ end ---@return integer width size of float ---@return integer height size of float function M._make_floating_popup_size(contents, opts) - validate({ - contents = { contents, 't' }, - opts = { opts, 't', true }, - }) + validate('contents', contents, 'table') + validate('opts', opts, 'table', true) opts = opts or {} local width = opts.width @@ -1603,11 +1592,9 @@ end ---@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({ - contents = { contents, 't' }, - syntax = { syntax, 's', true }, - opts = { opts, 't', true }, - }) + validate('contents', contents, 'table') + validate('syntax', syntax, 'string', true) + validate('opts', opts, 'table', true) opts = opts or {} opts.wrap = opts.wrap ~= false -- wrapping by default opts.focus = opts.focus ~= false @@ -1709,7 +1696,6 @@ do --[[ References ]] --- ---@param bufnr integer|nil 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) end @@ -1720,10 +1706,8 @@ do --[[ References ]] ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32". ---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent function M.buf_highlight_references(bufnr, references, offset_encoding) - validate({ - bufnr = { bufnr, 'n', true }, - offset_encoding = { offset_encoding, 'string', false }, - }) + validate('bufnr', bufnr, 'number', true) + validate('offset_encoding', offset_encoding, 'string', false) for _, reference in ipairs(references) do local start_line, start_char = reference['range']['start']['line'], reference['range']['start']['character'] @@ -1994,9 +1978,7 @@ end ---@param bufnr integer buffer handle or 0 for current, defaults to current ---@return string encoding first client if there is one, nil otherwise function M._get_offset_encoding(bufnr) - validate({ - bufnr = { bufnr, 'n', true }, - }) + validate('bufnr', bufnr, 'number', true) local offset_encoding @@ -2055,11 +2037,9 @@ end ---@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({ - start_pos = { start_pos, 't', true }, - end_pos = { end_pos, 't', true }, - offset_encoding = { offset_encoding, 's', true }, - }) + validate('start_pos', start_pos, 'table', true) + validate('end_pos', end_pos, 'table', true) + validate('offset_encoding', offset_encoding, 'string', true) bufnr = bufnr or api.nvim_get_current_buf() offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<')) @@ -2112,7 +2092,7 @@ end ---@param bufnr integer|nil: Buffer handle, defaults to current ---@return integer indentation size function M.get_effective_tabstop(bufnr) - validate({ bufnr = { bufnr, 'n', true } }) + validate('bufnr', bufnr, 'number', true) local bo = bufnr and vim.bo[bufnr] or vim.bo local sw = bo.shiftwidth return (sw == 0 and bo.tabstop) or sw @@ -2124,7 +2104,7 @@ end ---@return lsp.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 } }) + validate('options', options, 'table', true) options = vim.tbl_extend('keep', options or {}, { tabSize = M.get_effective_tabstop(), insertSpaces = vim.bo.expandtab, -- cgit From 0e8568d72c8825292af1b74018d8c3def0e3f16b Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 15 Oct 2024 17:02:14 +0100 Subject: feat(lsp.util): remove lsp spec extract --- runtime/lua/vim/lsp/util.lua | 18 ------------------ 1 file changed, 18 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 15f5b86fec..092d0a5576 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -849,24 +849,6 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers local parameter = signature.parameters[active_parameter + 1] local parameter_label = parameter.label - --[=[ - --Represents a parameter of a callable-signature. A parameter can - --have a label and a doc-comment. - interface ParameterInformation { - --The label of this parameter information. - -- - --Either a string or an inclusive start and exclusive end offsets within its containing - --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 - --string representation as `Position` and `Range` does. - -- - --*Note*: a label of type string should be a substring of its containing signature label. - --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. - label: string | [number, number]; - --The human-readable doc-comment of this parameter. Will be shown - --in the UI but can be omitted. - documentation?: string | MarkupContent; - } - --]=] if type(parameter_label) == 'table' then active_offset = parameter_label else -- cgit From 80e37aa533573ef1ad96bcccc006b8d45dc963b9 Mon Sep 17 00:00:00 2001 From: Tristan Knight Date: Wed, 16 Oct 2024 17:12:19 +0100 Subject: fix(lsp): str_byteindex_enc bounds checking #30747 Problem: Previously the index was only checked against the UTF8 length. This could cause unexpected behaviours for strings containing multibyte chars Solution: Check indicies correctly against their max value before returning the fallback length --- runtime/lua/vim/lsp/util.lua | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index fc822f1403..2b9e734c18 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -171,25 +171,24 @@ end ---@param encoding string utf-8|utf-16|utf-32| defaults to utf-16 ---@return integer byte (utf-8) index of `encoding` index `index` in `line` function M._str_byteindex_enc(line, index, encoding) - local len = #line - if index > len then - -- LSP spec: if character > line length, default to the line length. - -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position - return len - end + -- LSP spec: if character > line length, default to the line length. + -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position + local len8 = #line if not encoding then encoding = 'utf-16' end if encoding == 'utf-8' then - if index then + if index and index <= len8 then return index else - return len + return len8 end - elseif encoding == 'utf-16' then - return vim.str_byteindex(line, index, true) + end + local len32, len16 = vim.str_utfindex(line) + if encoding == 'utf-16' then + return index <= len16 and vim.str_byteindex(line, index, true) or len8 elseif encoding == 'utf-32' then - return vim.str_byteindex(line, index) + return index <= len32 and vim.str_byteindex(line, index) or len8 else error('Invalid encoding: ' .. vim.inspect(encoding)) end -- cgit From ce678043e3461d45f1251979cf047dd529b117ea Mon Sep 17 00:00:00 2001 From: Jordan <46637683+JordanllHarper@users.noreply.github.com> Date: Thu, 17 Oct 2024 09:35:19 +0100 Subject: feat(lsp): show server name in code actions #30830 Problem: If there are multiple LSP clients, it's not clear which actions are provided by which servers. #30710 Solution: Show the server name next to each action (only if there are multiple clients). --- runtime/lua/vim/lsp/buf.lua | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 301c1f0cb6..984222efbe 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -827,11 +827,19 @@ local function on_code_action_results(results, opts) return end - ---@param item {action: lsp.Command|lsp.CodeAction} + ---@param item {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext} local function format_item(item) - local title = item.action.title:gsub('\r\n', '\\r\\n') - return title:gsub('\n', '\\n') + local clients = vim.lsp.get_clients({ bufnr = item.ctx.bufnr }) + local title = item.action.title:gsub('\r\n', '\\r\\n'):gsub('\n', '\\n') + + if #clients == 1 then + return title + end + + local source = vim.lsp.get_client_by_id(item.ctx.client_id).name + return ('%s [%s]'):format(title, source) end + local select_opts = { prompt = 'Code actions:', kind = 'codeaction', -- cgit From 92e4e3fb76c250fa82a704823a5731fde5f220e1 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 15 Oct 2024 17:27:27 +0100 Subject: feat(lsp.util): fix type errors --- runtime/lua/vim/lsp/util.lua | 310 +++++++++++++++++++++++-------------------- 1 file changed, 168 insertions(+), 142 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 092d0a5576..26692e0bcd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -23,9 +23,8 @@ local default_border = { --- Check the border given by opts or the default border for the additional --- size it adds to a float. ----@param opts table optional options for the floating window ---- - border (string or table) the border ----@return table size of border in the form of { height = height, width = width } +---@param opts? {border:string|(string|[string,string])[]} +---@return {height:integer,width:integer} # 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 @@ -58,14 +57,16 @@ local function get_border_size(opts) ) ) end + --- @param id integer local function border_width(id) id = (id - 1) % #border + 1 - if type(border[id]) == 'table' then + local e = border[id] + if type(e) == 'table' then -- border specified as a table of - return vim.fn.strdisplaywidth(border[id][1]) - elseif type(border[id]) == 'string' then + return vim.fn.strdisplaywidth(e[1]) + elseif type(e) == 'string' then -- border specified as a list of border characters - return vim.fn.strdisplaywidth(border[id]) + return vim.fn.strdisplaywidth(e) end error( string.format( @@ -74,14 +75,16 @@ local function get_border_size(opts) ) ) end + --- @param id integer local function border_height(id) id = (id - 1) % #border + 1 - if type(border[id]) == 'table' then + local e = border[id] + if type(e) == 'table' then -- border specified as a table of - return #border[id][1] > 0 and 1 or 0 - elseif type(border[id]) == 'string' then + return #e[1] > 0 and 1 or 0 + elseif type(e) == 'string' then -- border specified as a list of border characters - return #border[id] > 0 and 1 or 0 + return #e > 0 and 1 or 0 end error( string.format( @@ -132,8 +135,8 @@ end --- Convert byte index to `encoding` index. --- Convenience wrapper around vim.str_utfindex ---@param line string line to be indexed ----@param index integer|nil byte index (utf-8), or `nil` for length ----@param encoding 'utf-8'|'utf-16'|'utf-32'|nil defaults to utf-16 +---@param index integer? byte index (utf-8), or `nil` for length +---@param encoding 'utf-8'|'utf-16'|'utf-32'? defaults to utf-16 ---@return integer `encoding` index of `index` in `line` function M._str_utfindex_enc(line, index, encoding) local len32, len16 = vim.str_utfindex(line) @@ -243,6 +246,8 @@ function M.set_lines(lines, A, B, new_lines) return lines end +--- @param fn fun(x:any):any[] +--- @return function local function sort_by_key(fn) return function(a, b) local ka, kb = fn(a), fn(b) @@ -391,10 +396,8 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) vim.bo[bufnr].buflisted = true -- Fix reversed range and indexing each text_edits - local index = 0 - --- @param text_edit lsp.TextEdit - text_edits = vim.tbl_map(function(text_edit) - index = index + 1 + for index, text_edit in ipairs(text_edits) do + --- @cast text_edit lsp.TextEdit|{_index: integer} text_edit._index = index if @@ -406,8 +409,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) text_edit.range.start = text_edit.range['end'] text_edit.range['end'] = start end - return text_edit - end, text_edits) + end -- Sort text_edits ---@param a lsp.TextEdit | { _index: integer } @@ -552,6 +554,9 @@ local function path_components(path) return vim.split(path, '/', { plain = true }) end +--- @param path string[] +--- @param prefix string[] +--- @return boolean local function path_under_prefix(path, prefix) for i, c in ipairs(prefix) do if c ~= path[i] then @@ -565,12 +570,12 @@ end ---@param prefix string ---@return integer[] local function get_bufs_with_prefix(prefix) - prefix = path_components(prefix) + local prefix_parts = path_components(prefix) local buffers = {} for _, v in ipairs(vim.api.nvim_list_bufs()) do local bname = vim.api.nvim_buf_get_name(v) local path = path_components(vim.fs.normalize(bname, { expand_env = false })) - if path_under_prefix(path, prefix) then + if path_under_prefix(path, prefix_parts) then table.insert(buffers, v) end end @@ -701,7 +706,7 @@ end --- Applies a `WorkspaceEdit`. --- ---@param workspace_edit lsp.WorkspaceEdit ----@param offset_encoding string utf-8|utf-16|utf-32 (required) +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' (required) ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit function M.apply_workspace_edit(workspace_edit, offset_encoding) if offset_encoding == nil then @@ -713,12 +718,13 @@ function M.apply_workspace_edit(workspace_edit, offset_encoding) if workspace_edit.documentChanges then for idx, change in ipairs(workspace_edit.documentChanges) do if change.kind == 'rename' then - M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), change.options) + local options = change.options --[[@as vim.lsp.util.rename.Opts]] + M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), options) elseif change.kind == 'create' then create_file(change) elseif change.kind == 'delete' then delete_file(change) - elseif change.kind then + elseif change.kind then --- @diagnostic disable-line:undefined-field error(string.format('Unsupported change: %q', vim.inspect(change))) else M.apply_text_document_edit(change, idx, offset_encoding) @@ -747,7 +753,7 @@ end --- then the corresponding value is returned without further modifications. --- ---@param input lsp.MarkedString|lsp.MarkedString[]|lsp.MarkupContent ----@param contents string[]|nil List of strings to extend with converted lines. Defaults to {}. +---@param contents string[]? List of strings to extend with converted lines. Defaults to {}. ---@return string[] extended with lines of converted markdown. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover function M.convert_input_to_markdown_lines(input, contents) @@ -784,7 +790,7 @@ end --- ---@param offset integer ---@param contents string[] ----@return { [1]: integer, [2]: integer } +---@return { [1]: integer, [2]: integer }? local function get_pos_from_offset(offset, contents) local i = 0 for l, line in ipairs(contents) do @@ -799,18 +805,18 @@ end --- Converts `textDocument/signatureHelp` response to markdown lines. --- ---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp` ----@param ft string|nil filetype that will be use as the `lang` for the label markdown code block ----@param triggers table|nil list of trigger characters from the lsp server. used to better determine parameter offsets ----@return string[]|nil table list of lines of converted markdown. ----@return Range4|nil table of active hl +---@param ft string? filetype that will be use as the `lang` for the label markdown code block +---@param triggers string[]? list of trigger characters from the lsp server. used to better determine parameter offsets +---@return string[]? # lines of converted markdown. +---@return Range4? # highlight range for the active parameter ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers) --The active signature. If omitted or the value lies outside the range of --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`. --Whenever possible implementors should make an active decision about --the active signature and shouldn't rely on a default value. - local contents = {} - local active_offset ---@type [integer, integer]|nil + local contents = {} --- @type string[] + local active_offset ---@type [integer, integer]? local active_signature = signature_help.activeSignature or 0 -- If the activeSignature is not inside the valid range, then clip it. -- In 3.15 of the protocol, activeSignature was allowed to be negative @@ -824,12 +830,13 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers label = ('```%s\n%s\n```'):format(ft, label) end list_extend(contents, split(label, '\n', { plain = true, trimempty = true })) - if signature.documentation then + local doc = signature.documentation + if doc then -- if LSP returns plain string, we treat it as plaintext. This avoids -- special characters like underscore or similar from being interpreted -- as markdown font modifiers - if type(signature.documentation) == 'string' then - signature.documentation = { kind = 'plaintext', value = signature.documentation } + if type(doc) == 'string' then + signature.documentation = { kind = 'plaintext', value = doc } end M.convert_input_to_markdown_lines(signature.documentation, contents) end @@ -852,7 +859,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers if type(parameter_label) == 'table' then active_offset = parameter_label else - local offset = 1 ---@type integer|nil + local offset = 1 ---@type integer? -- try to set the initial offset to the first found trigger character for _, t in ipairs(triggers or {}) do local trigger_offset = signature.label:find(t, 1, true) @@ -861,7 +868,9 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers end end for p, param in pairs(signature.parameters) do - offset = signature.label:find(param.label, offset, true) + local plabel = param.label + assert(type(plabel) == 'string', 'Expected label to be a string') + offset = signature.label:find(plabel, offset, true) if not offset then break end @@ -879,15 +888,15 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers local active_hl = nil if active_offset then - active_hl = {} -- Account for the start of the markdown block. if ft then - active_offset[1], active_offset[2] = - active_offset[1] + #contents[1], active_offset[2] + #contents[1] + active_offset[1] = active_offset[1] + #contents[1] + active_offset[2] = active_offset[2] + #contents[1] end - list_extend(active_hl, get_pos_from_offset(active_offset[1], contents)) - list_extend(active_hl, get_pos_from_offset(active_offset[2], contents)) + active_hl = {} + list_extend(active_hl, get_pos_from_offset(active_offset[1], contents) or {}) + list_extend(active_hl, get_pos_from_offset(active_offset[2], contents) or {}) end return contents, active_hl @@ -898,19 +907,7 @@ end --- ---@param width integer window width (in character cells) ---@param height integer window height (in character cells) ----@param opts table optional ---- - offset_x (integer) offset to add to `col` ---- - offset_y (integer) offset to add to `row` ---- - border (string or table) override `border` ---- - focusable (string or table) override `focusable` ---- - zindex (string or table) override `zindex`, defaults to 50 ---- - relative ("mouse"|"cursor") defaults to "cursor" ---- - anchor_bias ("auto"|"above"|"below") defaults to "auto" ---- - "auto": place window based on which side of the cursor has more lines ---- - "above": place the window above the cursor unless there are not enough lines ---- to display the full window height. ---- - "below": place the window below the cursor unless there are not enough lines ---- to display the full window height. +---@param opts? vim.lsp.util.open_floating_preview.Opts ---@return table Options function M.make_floating_popup_options(width, height, opts) validate('opts', opts, 'table', true) @@ -919,7 +916,6 @@ function M.make_floating_popup_options(width, height, opts) validate('opts.offset_y', opts.offset_y, 'n', true) local anchor = '' - local row, col local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1 or vim.fn.winline() - 1 @@ -927,7 +923,7 @@ function M.make_floating_popup_options(width, height, opts) local anchor_bias = opts.anchor_bias or 'auto' - local anchor_below + local anchor_below --- @type boolean? if anchor_bias == 'below' then anchor_below = (lines_below > lines_above) or (height <= lines_below) @@ -939,6 +935,7 @@ function M.make_floating_popup_options(width, height, opts) end local border_height = get_border_size(opts).height + local row, col --- @type integer?, integer? if anchor_below then anchor = anchor .. 'N' height = math.max(math.min(lines_below - border_height, height), 0) @@ -960,7 +957,7 @@ function M.make_floating_popup_options(width, height, opts) end local title = (opts.border and opts.title) and opts.title or nil - local title_pos + local title_pos --- @type 'left'|'center'|'right'? if title then title_pos = opts.title_pos or 'center' @@ -982,13 +979,21 @@ function M.make_floating_popup_options(width, height, opts) } end +--- @class vim.lsp.util.show_document.Opts +--- @inlinedoc +--- +--- Jump to existing window if buffer is already open. +--- @field reuse_win? boolean +--- +--- Whether to focus/jump to location if possible. +--- (defaults: true) +--- @field focus? boolean + --- Shows document and optionally jumps to the location. --- ---@param location lsp.Location|lsp.LocationLink ----@param offset_encoding string|nil utf-8|utf-16|utf-32 ----@param opts table|nil options ---- - reuse_win (boolean) Jump to existing window if buffer is already open. ---- - focus (boolean) Whether to focus/jump to location if possible. Defaults to true. +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? +---@param opts? vim.lsp.util.show_document.Opts ---@return boolean `true` if succeeded function M.show_document(location, offset_encoding, opts) -- location may be Location or LocationLink @@ -1042,8 +1047,8 @@ end --- Jumps to a location. --- ---@param location lsp.Location|lsp.LocationLink ----@param offset_encoding string|nil utf-8|utf-16|utf-32 ----@param reuse_win boolean|nil Jump to existing window if buffer is already open. +---@param offset_encoding string? utf-8|utf-16|utf-32 +---@param reuse_win boolean? Jump to existing window if buffer is already open. ---@return boolean `true` if the jump succeeded function M.jump_to_location(location, offset_encoding, reuse_win) if offset_encoding == nil then @@ -1063,9 +1068,9 @@ end --- - for LocationLink, targetRange is shown (e.g., body of function definition) --- ---@param location lsp.Location|lsp.LocationLink ----@param opts table ----@return integer|nil buffer id of float window ----@return integer|nil window id of float window +---@param opts? vim.lsp.util.open_floating_preview.Opts +---@return integer? buffer id of float window +---@return integer? window id of float window function M.preview_location(location, opts) -- location may be LocationLink or Location (more useful for the former) local uri = location.targetUri or location.uri @@ -1158,8 +1163,10 @@ local function collapse_blank_lines(contents) end local function get_markdown_fences() - local fences = {} - for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do + local fences = {} --- @type table + for _, fence in + pairs(vim.g.markdown_fenced_languages or {} --[[@as string[] ]]) + do local lang, syntax = fence:match('^(.*)=(.*)$') if lang then fences[lang] = syntax @@ -1179,7 +1186,7 @@ end --- ---@param bufnr integer ---@param contents string[] of lines to show in window ----@param opts table with optional fields +---@param opts? table with optional fields --- - height of floating window --- - width of floating window --- - wrap_at character to wrap at for computing height @@ -1201,8 +1208,11 @@ function M.stylize_markdown(bufnr, contents, opts) text = { 'text', '', '' }, } - local match_begin = function(line) + --- @param line string + --- @return {type:string,ft:string}? + local function match_begin(line) for type, pattern in pairs(matchers) do + --- @type string? local ret = line:match(string.format('^%%s*%s%%s*$', pattern[2])) if ret then return { @@ -1213,7 +1223,10 @@ function M.stylize_markdown(bufnr, contents, opts) end end - local match_end = function(line, match) + --- @param line string + --- @param match {type:string,ft:string} + --- @return string + local function match_end(line, match) local pattern = matchers[match.type] return line:match(string.format('^%%s*%s%%s*$', pattern[3])) end @@ -1222,9 +1235,9 @@ function M.stylize_markdown(bufnr, contents, opts) contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) local stripped = {} - local highlights = {} + local highlights = {} --- @type {ft:string,start:integer,finish:integer}[] -- keep track of lnums that contain markdown - local markdown_lines = {} + local markdown_lines = {} --- @type table do local i = 1 while i <= #contents do @@ -1280,18 +1293,23 @@ function M.stylize_markdown(bufnr, contents, opts) end -- Handle some common html escape sequences - stripped = vim.tbl_map(function(line) - local escapes = { - ['>'] = '>', - ['<'] = '<', - ['"'] = '"', - ['''] = "'", - [' '] = ' ', - [' '] = ' ', - ['&'] = '&', - } - return (string.gsub(line, '&[^ ;]+;', escapes)) - end, stripped) + --- @type string[] + stripped = vim.tbl_map( + --- @param line string + function(line) + local escapes = { + ['>'] = '>', + ['<'] = '<', + ['"'] = '"', + ['''] = "'", + [' '] = ' ', + [' '] = ' ', + ['&'] = '&', + } + return (line:gsub('&[^ ;]+;', 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)) @@ -1310,7 +1328,7 @@ function M.stylize_markdown(bufnr, contents, opts) local idx = 1 -- keep track of syntaxes we already included. -- no need to include the same syntax more than once - local langs = {} + local langs = {} --- @type table local fences = get_markdown_fences() local function apply_syntax_to_region(ft, start, finish) if ft == '' then @@ -1333,6 +1351,7 @@ function M.stylize_markdown(bufnr, contents, opts) if #api.nvim_get_runtime_file(('syntax/%s.vim'):format(ft), true) == 0 then return end + --- @diagnostic disable-next-line:param-type-mismatch pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft)) langs[lang] = true end @@ -1408,7 +1427,7 @@ end --- Closes the preview window --- ---@param winnr integer window id of preview window ----@param bufnrs table|nil optional list of ignored buffers +---@param bufnrs table? optional list of ignored buffers local function close_preview_window(winnr, bufnrs) vim.schedule(function() -- exit if we are in one of ignored buffers @@ -1456,13 +1475,8 @@ end ---@private --- Computes size of float needed to show contents (with optional wrapping) --- ----@param contents table of lines to show in window ----@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 +---@param contents string[] of lines to show in window +---@param opts? vim.lsp.util.open_floating_preview.Opts ---@return integer width size of float ---@return integer height size of float function M._make_floating_popup_size(contents, opts) @@ -1475,7 +1489,7 @@ function M._make_floating_popup_size(contents, opts) local wrap_at = opts.wrap_at local max_width = opts.max_width local max_height = opts.max_height - local line_widths = {} + local line_widths = {} --- @type table if not width then width = 0 @@ -1491,12 +1505,10 @@ function M._make_floating_popup_size(contents, opts) width = math.min(width, screen_width) -- make sure borders are always inside the screen - if width + border_width > screen_width then - width = width - (width + border_width - screen_width) - end + width = math.min(width, screen_width - border_width) - if wrap_at and wrap_at > width then - wrap_at = width + if wrap_at then + wrap_at = math.min(wrap_at, width) end if max_width then @@ -1528,7 +1540,6 @@ function M._make_floating_popup_size(contents, opts) end --- @class vim.lsp.util.open_floating_preview.Opts ---- @inlinedoc --- --- Height of floating window --- @field height? integer @@ -1563,6 +1574,27 @@ end --- window with the same {focus_id} --- (default: `true`) --- @field focus? boolean +--- +--- offset to add to `col` +--- @field offset_x? integer +--- +--- offset to add to `row` +--- @field offset_y? integer +--- @field border? (string|[string,string])[] override `border` +--- @field zindex? integer override `zindex`, defaults to 50 +--- @field title? string +--- @field title_pos? 'left'|'center'|'right' +--- +--- (default: `'cursor'`) +--- @field relative? 'mouse'|'cursor' +--- +--- - "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. +--- (default: `'auto'`) +--- @field anchor_bias? 'auto'|'above'|'below' --- Shows contents in a floating window. --- @@ -1676,7 +1708,7 @@ do --[[ References ]] --- Removes document highlights from a buffer. --- - ---@param bufnr integer|nil Buffer id + ---@param bufnr integer? Buffer id function M.buf_clear_references(bufnr) api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1) end @@ -1729,16 +1761,6 @@ local position_sort = sort_by_key(function(v) return { v.start.line, v.start.character } end) ----@class vim.lsp.util.locations_to_items.ret ----@inlinedoc ----@field filename string ----@field lnum integer 1-indexed line number ----@field end_lnum integer 1-indexed end line number ----@field col integer 1-indexed column ----@field end_col integer 1-indexed end column ----@field text string ----@field user_data lsp.Location|lsp.LocationLink - --- Returns the items with the byte position calculated correctly and in sorted --- order, for display in quickfix and location lists. --- @@ -1751,7 +1773,7 @@ end) ---@param locations lsp.Location[]|lsp.LocationLink[] ---@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32 --- default to first client of buffer ----@return vim.lsp.util.locations_to_items.ret[] +---@return vim.quickfix.entry[] # See |setqflist()| for the format function M.locations_to_items(locations, offset_encoding) if offset_encoding == nil then vim.notify_once( @@ -1761,7 +1783,7 @@ function M.locations_to_items(locations, offset_encoding) offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding end - local items = {} + local items = {} --- @type vim.quickfix.entry[] ---@type table local grouped = setmetatable({}, { __index = function(t, k) @@ -1777,11 +1799,7 @@ function M.locations_to_items(locations, offset_encoding) table.insert(grouped[uri], { start = range.start, ['end'] = range['end'], location = d }) end - ---@type string[] - local keys = vim.tbl_keys(grouped) - table.sort(keys) - -- TODO(ashkan) I wish we could do this lazily. - for _, uri in ipairs(keys) do + for uri in vim.spairs(grouped) do local rows = grouped[uri] table.sort(rows, position_sort) local filename = vim.uri_to_fname(uri) @@ -1807,7 +1825,7 @@ function M.locations_to_items(locations, offset_encoding) local col = M._str_byteindex_enc(line, pos.character, offset_encoding) local end_col = M._str_byteindex_enc(end_line, end_pos.character, offset_encoding) - table.insert(items, { + items[#items + 1] = { filename = filename, lnum = row + 1, end_lnum = end_row + 1, @@ -1815,7 +1833,7 @@ function M.locations_to_items(locations, offset_encoding) end_col = end_col + 1, text = line, user_data = temp.location, - }) + } end end return items @@ -1830,31 +1848,36 @@ end --- Converts symbols to quickfix list items. --- ----@param symbols table DocumentSymbol[] or SymbolInformation[] +---@param symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[] ---@param bufnr? integer +---@return vim.quickfix.entry[] # See |setqflist()| for the format function M.symbols_to_items(symbols, bufnr) + ---@param _symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[] + ---@param _items vim.quickfix.entry[] + ---@param _bufnr integer + ---@return vim.quickfix.entry[] local function _symbols_to_items(_symbols, _items, _bufnr) for _, symbol in ipairs(_symbols) do if symbol.location then -- SymbolInformation type local range = symbol.location.range local kind = M._get_symbol_kind_name(symbol.kind) - table.insert(_items, { + _items[#_items + 1] = { filename = vim.uri_to_fname(symbol.location.uri), lnum = range.start.line + 1, col = range.start.character + 1, kind = kind, text = '[' .. kind .. '] ' .. symbol.name, - }) + } elseif symbol.selectionRange then -- DocumentSymbole type local kind = M._get_symbol_kind_name(symbol.kind) - table.insert(_items, { + _items[#_items + 1] = { -- bufnr = _bufnr, filename = api.nvim_buf_get_name(_bufnr), lnum = symbol.selectionRange.start.line + 1, col = symbol.selectionRange.start.character + 1, kind = kind, text = '[' .. kind .. '] ' .. symbol.name, - }) + } if symbol.children then for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do for _, s in ipairs(v) do @@ -1889,7 +1912,7 @@ function M.trim_empty_lines(lines) break end end - return list_extend({}, lines, start, finish) + return vim.list_slice(lines, start, finish) end --- Accepts markdown lines and tries to reduce them to a filetype if they @@ -1922,8 +1945,8 @@ function M.try_trim_markdown_code_blocks(lines) return 'markdown' end ----@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` +---@param window integer?: window handle or 0 for current, defaults to current +---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window` local function make_position_param(window, offset_encoding) window = window or 0 local buf = api.nvim_win_get_buf(window) @@ -1942,8 +1965,8 @@ end --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. --- ----@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` +---@param window integer?: window handle or 0 for current, defaults to current +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window` ---@return lsp.TextDocumentPositionParams ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams function M.make_position_params(window, offset_encoding) @@ -1962,7 +1985,7 @@ end function M._get_offset_encoding(bufnr) validate('bufnr', bufnr, 'number', true) - local offset_encoding + local offset_encoding --- @type 'utf-8'|'utf-16'|'utf-32'? for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do if client.offset_encoding == nil then @@ -1993,8 +2016,8 @@ end --- `textDocument/codeAction`, `textDocument/colorPresentation`, --- `textDocument/rangeFormatting`. --- ----@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` +---@param window integer? window handle or 0 for current, defaults to current +---@param offset_encoding "utf-8"|"utf-16"|"utf-32"? defaults to `offset_encoding` of first client of buffer of `window` ---@return table { textDocument = { uri = `current_file_uri` }, range = { start = ---`current_position`, end = `current_position` } } function M.make_range_params(window, offset_encoding) @@ -2010,12 +2033,12 @@ 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,integer]? {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,integer]? {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` +---@param bufnr integer? buffer handle or 0 for current, defaults to current +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of `bufnr` ---@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) @@ -2024,7 +2047,9 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) validate('offset_encoding', offset_encoding, 'string', true) bufnr = bufnr or api.nvim_get_current_buf() offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) + --- @type integer[] local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<')) + --- @type integer[] local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>')) -- convert to 0-index A[1] = A[1] - 1 @@ -2053,7 +2078,7 @@ end --- Creates a `TextDocumentIdentifier` object for the current buffer. --- ----@param bufnr integer|nil: Buffer handle, defaults to current +---@param bufnr integer?: Buffer handle, defaults to current ---@return lsp.TextDocumentIdentifier ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier function M.make_text_document_params(bufnr) @@ -2071,7 +2096,7 @@ end --- Returns indentation size. --- ---@see 'shiftwidth' ----@param bufnr integer|nil: Buffer handle, defaults to current +---@param bufnr integer?: Buffer handle, defaults to current ---@return integer indentation size function M.get_effective_tabstop(bufnr) validate('bufnr', bufnr, 'number', true) @@ -2082,7 +2107,7 @@ end --- Creates a `DocumentFormattingParams` object for the current buffer and cursor position. --- ----@param options lsp.FormattingOptions|nil with valid `FormattingOptions` entries +---@param options lsp.FormattingOptions? with valid `FormattingOptions` entries ---@return lsp.DocumentFormattingParams object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting function M.make_formatting_params(options) @@ -2125,6 +2150,7 @@ end function M.lookup_section(settings, section) vim.deprecate('vim.lsp.util.lookup_section()', 'vim.tbl_get() with `vim.split`', '0.12') for part in vim.gsplit(section, '.', { plain = true }) do + --- @diagnostic disable-next-line:no-unknown settings = settings[part] if settings == nil then return vim.NIL -- cgit From 1edfe5c09ebcb9e81464f805f99d276de4bcef9b Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 09:45:12 +0100 Subject: feat(lsp.util): use vim.api alias --- runtime/lua/vim/lsp/util.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 26692e0bcd..e1027685ee 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -125,10 +125,10 @@ local function split_lines(s, no_blank) end local function create_window_without_focus() - local prev = vim.api.nvim_get_current_win() + local prev = api.nvim_get_current_win() vim.cmd.new() - local new = vim.api.nvim_get_current_win() - vim.api.nvim_set_current_win(prev) + local new = api.nvim_get_current_win() + api.nvim_set_current_win(prev) return new end @@ -496,7 +496,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- make sure we don't go out of bounds pos[1] = math.min(pos[1], max) pos[2] = math.min(pos[2], #(get_line(bufnr, pos[1] - 1) or '')) - vim.api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {}) + api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {}) end end @@ -572,8 +572,8 @@ end local function get_bufs_with_prefix(prefix) local prefix_parts = path_components(prefix) local buffers = {} - for _, v in ipairs(vim.api.nvim_list_bufs()) do - local bname = vim.api.nvim_buf_get_name(v) + for _, v in ipairs(api.nvim_list_bufs()) do + local bname = api.nvim_buf_get_name(v) local path = path_components(vim.fs.normalize(bname, { expand_env = false })) if path_under_prefix(path, prefix_parts) then table.insert(buffers, v) @@ -632,7 +632,7 @@ function M.rename(old_fname, new_fname, opts) -- conflicting buffer may not be associated with a file. For example, 'buftype' can be "nofile" -- or "nowrite", or the buffer can be a normal buffer but has not been written to the file yet. -- Renaming should fail in such cases to avoid losing the contents of the conflicting buffer. - local old_bname = vim.api.nvim_buf_get_name(b) + local old_bname = api.nvim_buf_get_name(b) local new_bname = old_bname:gsub(old_fname_pat, escape_gsub_repl(new_fname)) if vim.fn.bufexists(new_bname) == 1 then local existing_buf = vim.fn.bufnr(new_bname) @@ -2173,7 +2173,7 @@ local function make_line_range_params(bufnr, start_line, end_line, offset_encodi ---@type lsp.Position local end_pos - if end_line == last_line and not vim.api.nvim_get_option_value('endofline', { buf = bufnr }) then + if end_line == last_line and not vim.bo[bufnr].endofline then end_pos = { line = end_line, character = M.character_offset(bufnr, end_line, #get_line(bufnr, end_line), offset_encoding), -- cgit From 5bec7288a5b56f1ac38673e59e32f6be774558f3 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 09:48:27 +0100 Subject: feat(lsp.util): remove uneeded do-end --- runtime/lua/vim/lsp/util.lua | 93 ++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 47 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index e1027685ee..4922c9225e 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1238,57 +1238,56 @@ function M.stylize_markdown(bufnr, contents, opts) local highlights = {} --- @type {ft:string,start:integer,finish:integer}[] -- keep track of lnums that contain markdown local markdown_lines = {} --- @type table - do - local i = 1 - while i <= #contents do - local line = contents[i] - local match = match_begin(line) - if match then - local start = #stripped - i = i + 1 - while i <= #contents do - line = contents[i] - if match_end(line, match) then - i = i + 1 - break - end - table.insert(stripped, line) + + local i = 1 + while i <= #contents do + local line = contents[i] + local match = match_begin(line) + if match then + local start = #stripped + i = i + 1 + while i <= #contents do + line = contents[i] + if match_end(line, match) then i = i + 1 + break end - table.insert(highlights, { - ft = match.ft, - start = start + 1, - finish = #stripped, - }) - -- add a separator, but not on the last line - if opts.separator and i < #contents then - table.insert(stripped, '---') - markdown_lines[#stripped] = true - end - else - -- strip any empty lines or separators prior to this separator in actual markdown - if line:match('^---+$') then - while - markdown_lines[#stripped] - and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$')) - do - markdown_lines[#stripped] = false - table.remove(stripped, #stripped) - end - end - -- add the line if its not an empty line following a separator - if - not ( - line:match('^%s*$') - and markdown_lines[#stripped] - and stripped[#stripped]:match('^---+$') - ) - then - table.insert(stripped, line) - markdown_lines[#stripped] = true - end + table.insert(stripped, line) i = i + 1 end + table.insert(highlights, { + ft = match.ft, + start = start + 1, + finish = #stripped, + }) + -- add a separator, but not on the last line + if opts.separator and i < #contents then + table.insert(stripped, '---') + markdown_lines[#stripped] = true + end + else + -- strip any empty lines or separators prior to this separator in actual markdown + if line:match('^---+$') then + while + markdown_lines[#stripped] + and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$')) + do + markdown_lines[#stripped] = false + table.remove(stripped, #stripped) + end + end + -- add the line if its not an empty line following a separator + if + not ( + line:match('^%s*$') + and markdown_lines[#stripped] + and stripped[#stripped]:match('^---+$') + ) + then + table.insert(stripped, line) + markdown_lines[#stripped] = true + end + i = i + 1 end end -- cgit From f0973d42272e9474c758c87697e3803e1796d17c Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 09:58:10 +0100 Subject: feat(lsp.util): refactor symbols_to_items() - Remove the trivial function vim.lsp.util._get_symbol_kind_name() and its tests. --- runtime/lua/vim/lsp/util.lua | 74 +++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 43 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4922c9225e..2101e940f0 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1838,57 +1838,45 @@ function M.locations_to_items(locations, offset_encoding) return items end --- According to LSP spec, if the client set "symbolKind.valueSet", --- the client must handle it properly even if it receives a value outside the specification. --- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol -function M._get_symbol_kind_name(symbol_kind) - return protocol.SymbolKind[symbol_kind] or 'Unknown' -end - --- Converts symbols to quickfix list items. --- ---@param symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[] ---@param bufnr? integer ---@return vim.quickfix.entry[] # See |setqflist()| for the format function M.symbols_to_items(symbols, bufnr) - ---@param _symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[] - ---@param _items vim.quickfix.entry[] - ---@param _bufnr integer - ---@return vim.quickfix.entry[] - local function _symbols_to_items(_symbols, _items, _bufnr) - for _, symbol in ipairs(_symbols) do - if symbol.location then -- SymbolInformation type - local range = symbol.location.range - local kind = M._get_symbol_kind_name(symbol.kind) - _items[#_items + 1] = { - filename = vim.uri_to_fname(symbol.location.uri), - lnum = range.start.line + 1, - col = range.start.character + 1, - kind = kind, - text = '[' .. kind .. '] ' .. symbol.name, - } - elseif symbol.selectionRange then -- DocumentSymbole type - local kind = M._get_symbol_kind_name(symbol.kind) - _items[#_items + 1] = { - -- bufnr = _bufnr, - filename = api.nvim_buf_get_name(_bufnr), - lnum = symbol.selectionRange.start.line + 1, - col = symbol.selectionRange.start.character + 1, - kind = kind, - text = '[' .. kind .. '] ' .. symbol.name, - } - if symbol.children then - for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do - for _, s in ipairs(v) do - table.insert(_items, s) - end - end - end - end + bufnr = bufnr or 0 + local items = {} --- @type vim.quickfix.entry[] + for _, symbol in ipairs(symbols) do + --- @type string?, lsp.Position? + local filename, pos + + if symbol.location then + --- @cast symbol lsp.SymbolInformation + filename = vim.uri_to_fname(symbol.location.uri) + pos = symbol.location.range.start + elseif symbol.selectionRange then + --- @cast symbol lsp.DocumentSymbol + filename = api.nvim_buf_get_name(bufnr) + pos = symbol.selectionRange.start + end + + if filename and pos then + local kind = protocol.SymbolKind[symbol.kind] or 'Unknown' + items[#items + 1] = { + filename = filename, + lnum = pos.line + 1, + col = pos.character + 1, + kind = kind, + text = '[' .. kind .. '] ' .. symbol.name, + } + end + + if symbol.children then + list_extend(items, M.symbols_to_items(symbol.children, bufnr)) end - return _items end - return _symbols_to_items(symbols, {}, bufnr or 0) + + return items end --- Removes empty lines from the beginning and end. -- cgit From 0621718e3b5f522ad2df9ef7d4332fa324bc8db5 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 10:06:59 +0100 Subject: feat(lsp.util): remove metatable in locations_to_items --- runtime/lua/vim/lsp/util.lua | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 2101e940f0..d42b33f7a0 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1783,18 +1783,14 @@ function M.locations_to_items(locations, offset_encoding) end local items = {} --- @type vim.quickfix.entry[] + ---@type table - local grouped = setmetatable({}, { - __index = function(t, k) - local v = {} - rawset(t, k, v) - return v - end, - }) + local grouped = {} for _, d in ipairs(locations) do -- locations may be Location or LocationLink local uri = d.uri or d.targetUri local range = d.range or d.targetSelectionRange + grouped[uri] = grouped[uri] or {} table.insert(grouped[uri], { start = range.start, ['end'] = range['end'], location = d }) end -- cgit From d44d36b8ff6cbd966daefc4ea9b775236175c887 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 10:34:37 +0100 Subject: feat(lsp.util): simplify some bounds checking --- runtime/lua/vim/lsp/util.lua | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index d42b33f7a0..f77597e40d 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -843,16 +843,12 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers if signature.parameters and #signature.parameters > 0 then -- First check if the signature has an activeParameter. If it doesn't check if the response -- had that property instead. Else just default to 0. - local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0) - if active_parameter < 0 then - active_parameter = 0 - end + local active_parameter = + math.max(signature.activeParameter or signature_help.activeParameter or 0, 0) -- If the activeParameter is > #parameters, then set it to the last -- NOTE: this is not fully according to the spec, but a client-side interpretation - if active_parameter >= #signature.parameters then - active_parameter = #signature.parameters - 1 - end + active_parameter = math.min(active_parameter, #signature.parameters - 1) local parameter = signature.parameters[active_parameter + 1] local parameter_label = parameter.label -- cgit From 1944c0d610ce1616f0e4c93fca22b614361224e7 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 12:49:14 +0100 Subject: feat(lsp.util): refactor get_border_size() --- runtime/lua/vim/lsp/util.lua | 130 ++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 71 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index f77597e40d..3ae9e6669f 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -21,85 +21,73 @@ local default_border = { { ' ', 'NormalFloat' }, } +--- @param border string|(string|[string,string])[] +local function border_error(border) + error( + string.format( + 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', + vim.inspect(border) + ), + 2 + ) +end + +local border_size = { + none = { 0, 0 }, + single = { 2, 2 }, + double = { 2, 2 }, + rounded = { 2, 2 }, + solid = { 2, 2 }, + shadow = { 1, 1 }, +} + --- Check the border given by opts or the default border for the additional --- size it adds to a float. ----@param opts? {border:string|(string|[string,string])[]} ----@return {height:integer,width:integer} # size of border in the form of { height = height, width = width } +--- @param opts? {border:string|(string|[string,string])[]} +--- @return integer height +--- @return integer width local function get_border_size(opts) local border = opts and opts.border or default_border - local height = 0 - local width = 0 if type(border) == 'string' then - local border_size = { - none = { 0, 0 }, - single = { 2, 2 }, - double = { 2, 2 }, - rounded = { 2, 2 }, - solid = { 2, 2 }, - shadow = { 1, 1 }, - } - if border_size[border] == nil then - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) - end - height, width = unpack(border_size[border]) - else - if 8 % #border ~= 0 then - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) - end - --- @param id integer - local function border_width(id) - id = (id - 1) % #border + 1 - local e = border[id] - if type(e) == 'table' then - -- border specified as a table of - return vim.fn.strdisplaywidth(e[1]) - elseif type(e) == 'string' then - -- border specified as a list of border characters - return vim.fn.strdisplaywidth(e) - end - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) + if not border_size[border] then + border_error(border) end - --- @param id integer - local function border_height(id) - id = (id - 1) % #border + 1 - local e = border[id] - if type(e) == 'table' then - -- border specified as a table of - return #e[1] > 0 and 1 or 0 - elseif type(e) == 'string' then - -- border specified as a list of border characters - return #e > 0 and 1 or 0 - end - error( - string.format( - 'invalid floating preview border: %s. :help vim.api.nvim_open_win()', - vim.inspect(border) - ) - ) + return unpack(border_size[border]) + end + + if 8 % #border ~= 0 then + border_error(border) + end + + --- @param id integer + --- @return string + local function elem(id) + id = (id - 1) % #border + 1 + local e = border[id] + if type(e) == 'table' then + -- border specified as a table of + return e[1] + elseif type(e) == 'string' then + -- border specified as a list of border characters + return e end - height = height + border_height(2) -- top - height = height + border_height(6) -- bottom - width = width + border_width(4) -- right - width = width + border_width(8) -- left + --- @diagnostic disable-next-line:missing-return + border_error(border) end - return { height = height, width = width } + --- @param e string + local function border_height(e) + return #e > 0 and 1 or 0 + end + + local top, bottom = elem(2), elem(6) + local height = border_height(top) + border_height(bottom) + + local right, left = elem(4), elem(8) + local width = vim.fn.strdisplaywidth(right) + vim.fn.strdisplaywidth(left) + + return height, width end --- Splits string at newlines, optionally removing unwanted blank lines. @@ -930,7 +918,7 @@ function M.make_floating_popup_options(width, height, opts) anchor_below = lines_below > lines_above end - local border_height = get_border_size(opts).height + local border_height = get_border_size(opts) local row, col --- @type integer?, integer? if anchor_below then anchor = anchor .. 'N' @@ -1495,7 +1483,7 @@ function M._make_floating_popup_size(contents, opts) end end - local border_width = get_border_size(opts).width + local _, border_width = get_border_size(opts) local screen_width = api.nvim_win_get_width(0) width = math.min(width, screen_width) -- cgit From ff1d7d42995931d17395223cec6fb2031a870d15 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 13:10:21 +0100 Subject: feat(lsp.util): get_bufs_with_prefix -> get_writeable_bufs --- runtime/lua/vim/lsp/util.lua | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 3ae9e6669f..31a9fdb2fc 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -554,17 +554,24 @@ local function path_under_prefix(path, prefix) return true end ---- Get list of buffers whose filename matches the given path prefix (normalized full path) +--- Get list of loaded writable buffers whose filename matches the given path +--- prefix (normalized full path). ---@param prefix string ---@return integer[] -local function get_bufs_with_prefix(prefix) +local function get_writable_bufs(prefix) local prefix_parts = path_components(prefix) - local buffers = {} - for _, v in ipairs(api.nvim_list_bufs()) do - local bname = api.nvim_buf_get_name(v) - local path = path_components(vim.fs.normalize(bname, { expand_env = false })) - if path_under_prefix(path, prefix_parts) then - table.insert(buffers, v) + local buffers = {} --- @type integer[] + for _, buf in ipairs(api.nvim_list_bufs()) do + -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them. + if + api.nvim_buf_is_loaded(buf) + and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[buf].buftype) + then + local bname = api.nvim_buf_get_name(buf) + local path = path_components(vim.fs.normalize(bname, { expand_env = false })) + if path_under_prefix(path, prefix_parts) then + buffers[#buffers + 1] = buf + end end end return buffers @@ -608,13 +615,7 @@ function M.rename(old_fname, new_fname, opts) local buf_rename = {} ---@type table local old_fname_pat = '^' .. vim.pesc(old_fname_full) - for b in - vim.iter(get_bufs_with_prefix(old_fname_full)):filter(function(b) - -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them. - return api.nvim_buf_is_loaded(b) - and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[b].buftype) - end) - do + for _, b in ipairs(get_writable_bufs(old_fname_full)) do -- Renaming a buffer may conflict with another buffer that happens to have the same name. In -- most cases, this would have been already detected by the file conflict check above, but the -- conflicting buffer may not be associated with a file. For example, 'buftype' can be "nofile" -- cgit From acbc6a7f91d15fe5f59df08227d196156fafedb4 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 13:17:10 +0100 Subject: fix(lsp.util): inconsistent handling of offset_encoding --- runtime/lua/vim/lsp/_tagfunc.lua | 7 ++++++- runtime/lua/vim/lsp/buf.lua | 2 ++ runtime/lua/vim/lsp/util.lua | 7 +++++-- 3 files changed, 13 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua index 4ad50e4a58..70917de48d 100644 --- a/runtime/lua/vim/lsp/_tagfunc.lua +++ b/runtime/lua/vim/lsp/_tagfunc.lua @@ -27,15 +27,20 @@ local function query_definition(pattern) return {} end local results = {} + + --- @param range lsp.Range + --- @param uri string + --- @param offset_encoding string local add = function(range, uri, offset_encoding) table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding)) end + for client_id, lsp_results in pairs(assert(results_by_client)) do local client = lsp.get_client_by_id(client_id) local offset_encoding = client and client.offset_encoding or 'utf-16' local result = lsp_results.result or {} if result.range then -- Location - add(result.range, result.uri) + add(result.range, result.uri, offset_encoding) else result = result --[[@as (lsp.Location[]|lsp.LocationLink[])]] for _, item in pairs(result) do diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 301c1f0cb6..e3f4beeeb1 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -338,6 +338,8 @@ function M.rename(new_name, opts) -- Compute early to account for cursor movements after going async local cword = vim.fn.expand('') + --- @param range lsp.Range + --- @param offset_encoding string local function get_text_at_range(range, offset_encoding) return api.nvim_buf_get_text( bufnr, diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 31a9fdb2fc..aed742f001 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -347,7 +347,7 @@ end --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position ---@param position lsp.Position ----@param offset_encoding? string utf-8|utf-16|utf-32 +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@return integer local function get_line_byte_from_position(bufnr, position, offset_encoding) -- LSP's line and characters are 0-indexed @@ -357,7 +357,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding) -- character if col > 0 then local line = get_line(bufnr, position.line) or '' - return M._str_byteindex_enc(line, col, offset_encoding or 'utf-16') + return M._str_byteindex_enc(line, col, offset_encoding) end return col end @@ -512,6 +512,7 @@ function M.apply_text_document_edit(text_document_edit, index, offset_encoding) 'apply_text_document_edit must be called with valid offset encoding', vim.log.levels.WARN ) + return end -- For lists of text document edits, @@ -703,6 +704,7 @@ function M.apply_workspace_edit(workspace_edit, offset_encoding) 'apply_workspace_edit must be called with valid offset encoding', vim.log.levels.WARN ) + return end if workspace_edit.documentChanges then for idx, change in ipairs(workspace_edit.documentChanges) do @@ -988,6 +990,7 @@ function M.show_document(location, offset_encoding, opts) end if offset_encoding == nil then vim.notify_once('show_document must be called with valid offset encoding', vim.log.levels.WARN) + return false end local bufnr = vim.uri_to_bufnr(uri) -- cgit From 8ad000ef7cf0409e2575d39c8061e17297b0cfec Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 13:32:12 +0100 Subject: feat(lsp.util): remove unneeded table --- runtime/lua/vim/lsp/util.lua | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index aed742f001..be43087a2c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -428,47 +428,45 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n') -- Convert from LSP style ranges to Neovim style ranges. - local e = { - start_row = text_edit.range.start.line, - start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding), - end_row = text_edit.range['end'].line, - end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding), - text = split(text_edit.newText, '\n', { plain = true }), - } + local start_row = text_edit.range.start.line + local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding) + local end_row = text_edit.range['end'].line + local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding) + local text = split(text_edit.newText, '\n', { plain = true }) local max = api.nvim_buf_line_count(bufnr) -- If the whole edit is after the lines in the buffer we can simply add the new text to the end -- of the buffer. - if max <= e.start_row then - api.nvim_buf_set_lines(bufnr, max, max, false, e.text) + if max <= start_row then + api.nvim_buf_set_lines(bufnr, max, max, false, text) else - local last_line_len = #(get_line(bufnr, math.min(e.end_row, max - 1)) or '') + local last_line_len = #(get_line(bufnr, math.min(end_row, max - 1)) or '') -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't -- accept it so we should fix it here. - if max <= e.end_row then - e.end_row = max - 1 - e.end_col = last_line_len + if max <= end_row then + end_row = max - 1 + end_col = last_line_len has_eol_text_edit = true else - -- If the replacement is over the end of a line (i.e. e.end_col is equal to the line length and the + -- If the replacement is over the end of a line (i.e. end_col is equal to the line length and the -- replacement text ends with a newline We can likely assume that the replacement is assumed -- to be meant to replace the newline with another newline and we need to make sure this -- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r' -- in the file some servers (clangd on windows) will include that character in the line -- while nvim_buf_set_text doesn't count it as part of the line. if - e.end_col >= last_line_len - and text_edit.range['end'].character > e.end_col + end_col >= last_line_len + and text_edit.range['end'].character > end_col and #text_edit.newText > 0 and string.sub(text_edit.newText, -1) == '\n' then - table.remove(e.text, #e.text) + table.remove(text, #text) end end - -- Make sure we don't go out of bounds for e.end_col - e.end_col = math.min(last_line_len, e.end_col) + -- Make sure we don't go out of bounds for end_col + end_col = math.min(last_line_len, end_col) - api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text) + api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, text) end end -- cgit From 3f87e222f79bcbd4f6c863128064954d62da5b75 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 13:33:07 +0100 Subject: feat(lsp.util): remove some aliases --- runtime/lua/vim/lsp/util.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index be43087a2c..790a7001ad 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -6,7 +6,6 @@ local highlight = vim.highlight local uv = vim.uv local npcall = vim.F.npcall -local split = vim.split local M = {} @@ -432,7 +431,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding) local end_row = text_edit.range['end'].line local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding) - local text = split(text_edit.newText, '\n', { plain = true }) + local text = vim.split(text_edit.newText, '\n', { plain = true }) local max = api.nvim_buf_line_count(bufnr) -- If the whole edit is after the lines in the buffer we can simply add the new text to the end @@ -818,7 +817,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, trimempty = true })) + list_extend(contents, vim.split(label, '\n', { plain = true, trimempty = true })) local doc = signature.documentation if doc then -- if LSP returns plain string, we treat it as plaintext. This avoids -- cgit From 97119a236937f47dab2d16321ce062b653bf3f2c Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 13:35:03 +0100 Subject: feat(lsp.util): use vim.w/b --- runtime/lua/vim/lsp/util.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 790a7001ad..4953cb0fbc 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -5,8 +5,6 @@ local list_extend = vim.list_extend local highlight = vim.highlight local uv = vim.uv -local npcall = vim.F.npcall - local M = {} local default_border = { @@ -1082,7 +1080,7 @@ end local function find_window_by_var(name, value) for _, win in ipairs(api.nvim_list_wins()) do - if npcall(api.nvim_win_get_var, win, name) == value then + if vim.w[win][name] == value then return win end end @@ -1604,7 +1602,7 @@ function M.open_floating_preview(contents, syntax, opts) if opts.focus_id and opts.focusable ~= false and opts.focus then -- Go back to previous window if we are in a focusable one local current_winnr = api.nvim_get_current_win() - if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then + if vim.w[current_winnr][opts.focus_id] then api.nvim_command('wincmd p') return bufnr, current_winnr end @@ -1621,7 +1619,7 @@ function M.open_floating_preview(contents, syntax, opts) -- check if another floating preview already exists for this buffer -- and close it if needed - local existing_float = npcall(api.nvim_buf_get_var, bufnr, 'lsp_floating_preview') + local existing_float = vim.b[bufnr].lsp_floating_preview if existing_float and api.nvim_win_is_valid(existing_float) then api.nvim_win_close(existing_float, true) end -- cgit From e954a16063e61c2c16f2a17885ab8377e07d4e2f Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 13:38:29 +0100 Subject: feat(lsp.util): remove some variables --- runtime/lua/vim/lsp/util.lua | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4953cb0fbc..e0e93678e2 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -510,17 +510,12 @@ function M.apply_text_document_edit(text_document_edit, index, offset_encoding) return end - -- For lists of text document edits, - -- do not check the version after the first edit. - local should_check_version = true - if index and index > 1 then - should_check_version = false - end - -- `VersionedTextDocumentIdentifier`s version may be null -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier if - should_check_version + -- For lists of text document edits, + -- do not check the version after the first edit. + not (index and index > 1) and ( text_document.version and text_document.version > 0 @@ -2179,9 +2174,7 @@ function M._refresh(method, opts) local textDocument = M.make_text_document_params(bufnr) - local only_visible = opts.only_visible or false - - if only_visible then + if opts.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) -- cgit From cbc82011ce19e308f249e1e3ba16f7b1219c9984 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 15:48:30 +0100 Subject: feat(lsp.util): improve offset_encoding type annotations --- runtime/lua/vim/lsp/util.lua | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index e0e93678e2..7c89be1d88 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -362,7 +362,7 @@ end --- Applies a list of text edits to a buffer. ---@param text_edits lsp.TextEdit[] ---@param bufnr integer Buffer id ----@param offset_encoding string utf-8|utf-16|utf-32 +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit function M.apply_text_edits(text_edits, bufnr, offset_encoding) validate('text_edits', text_edits, 'table', false) @@ -497,7 +497,7 @@ end --- ---@param text_document_edit lsp.TextDocumentEdit ---@param index? integer: Optional index of the edit, if from a list of edits (or nil, if not from a list) ----@param offset_encoding? string +---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32' ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit function M.apply_text_document_edit(text_document_edit, index, offset_encoding) local text_document = text_document_edit.textDocument @@ -1025,7 +1025,7 @@ end --- Jumps to a location. --- ---@param location lsp.Location|lsp.LocationLink ----@param offset_encoding string? utf-8|utf-16|utf-32 +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? ---@param reuse_win boolean? Jump to existing window if buffer is already open. ---@return boolean `true` if the jump succeeded function M.jump_to_location(location, offset_encoding, reuse_win) @@ -1694,7 +1694,7 @@ do --[[ References ]] --- ---@param bufnr integer Buffer id ---@param references lsp.DocumentHighlight[] objects to highlight - ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32". + ---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent function M.buf_highlight_references(bufnr, references, offset_encoding) validate('bufnr', bufnr, 'number', true) @@ -1748,8 +1748,8 @@ end) --- |setloclist()|. --- ---@param locations lsp.Location[]|lsp.LocationLink[] ----@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32 ---- default to first client of buffer +---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32' +--- default to first client of buffer ---@return vim.quickfix.entry[] # See |setqflist()| for the format function M.locations_to_items(locations, offset_encoding) if offset_encoding == nil then @@ -2088,7 +2088,8 @@ end ---@param buf integer buffer number (0 for current) ---@param row integer 0-indexed line ---@param col integer 0-indexed byte offset in line ----@param offset_encoding string utf-8|utf-16|utf-32 defaults to `offset_encoding` of first client of `buf` +---@param offset_encoding? '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) @@ -2126,7 +2127,7 @@ end ---@param bufnr integer ---@param start_line integer ---@param end_line integer ----@param offset_encoding lsp.PositionEncodingKind +---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' ---@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 -- cgit From a18fa2f11c3a66be8addc303efb5c8033a47f41e Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 16:11:48 +0100 Subject: feat(lsp.util): minor codestyle --- runtime/lua/vim/lsp/util.lua | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 7c89be1d88..76a18a409c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1700,10 +1700,10 @@ do --[[ References ]] validate('bufnr', bufnr, 'number', true) validate('offset_encoding', offset_encoding, 'string', false) for _, reference in ipairs(references) do - local start_line, start_char = - reference['range']['start']['line'], reference['range']['start']['character'] - local end_line, end_char = - reference['range']['end']['line'], reference['range']['end']['character'] + local start_line = reference.range.start.line + local start_char = reference.range.start.character + local end_line = reference.range['end'].line + local end_char = reference.range['end'].character local start_idx = get_line_byte_from_position( bufnr, @@ -1772,8 +1772,7 @@ function M.locations_to_items(locations, offset_encoding) table.insert(grouped[uri], { start = range.start, ['end'] = range['end'], location = d }) end - for uri in vim.spairs(grouped) do - local rows = grouped[uri] + for uri, rows in vim.spairs(grouped) do table.sort(rows, position_sort) local filename = vim.uri_to_fname(uri) @@ -2008,19 +2007,19 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) validate('offset_encoding', offset_encoding, 'string', true) bufnr = bufnr or api.nvim_get_current_buf() offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) - --- @type integer[] - local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<')) - --- @type integer[] - local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>')) + --- @type [integer, integer] + local A = { unpack(start_pos or api.nvim_buf_get_mark(bufnr, '<')) } + --- @type [integer, integer] + local B = { unpack(end_pos or api.nvim_buf_get_mark(bufnr, '>')) } -- convert to 0-index A[1] = A[1] - 1 B[1] = B[1] - 1 -- account for offset_encoding. if A[2] > 0 then - A = { A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding) } + A[2] = M.character_offset(bufnr, A[1], A[2], offset_encoding) end if B[2] > 0 then - B = { B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding) } + B[2] = M.character_offset(bufnr, B[1], B[2], offset_encoding) end -- we need to offset the end character position otherwise we loose the last -- character of the selection, as LSP end position is exclusive -- cgit From 3f3e4837d5f7d2d9cb1c89bd3a5b2ee8a730772a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 16 Oct 2024 17:03:48 +0100 Subject: perf(validate): use lighter version - Also fix `vim.validate()` for PUC Lua when showing errors for values that aren't string or number. --- runtime/lua/vim/lsp/buf.lua | 16 ++++++---------- runtime/lua/vim/lsp/client.lua | 14 +++++--------- runtime/lua/vim/lsp/diagnostic.lua | 2 +- runtime/lua/vim/lsp/inlay_hint.lua | 9 +++++---- runtime/lua/vim/lsp/rpc.lua | 24 +++++++----------------- runtime/lua/vim/lsp/semantic_tokens.lua | 16 +++++----------- 6 files changed, 29 insertions(+), 52 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index ccef314d86..f6837a627f 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -21,10 +21,8 @@ local M = {} --- ---@see |vim.lsp.buf_request()| local function request(method, params, handler) - validate({ - method = { method, 's' }, - handler = { handler, 'f', true }, - }) + validate('method', method, 'string') + validate('handler', handler, 'function', true) return vim.lsp.buf_request(0, method, params, handler) end @@ -439,7 +437,7 @@ end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references ---@param opts? vim.lsp.ListOpts function M.references(context, opts) - validate({ context = { context, 't', true } }) + validate('context', context, 'table', true) local params = util.make_position_params() params.context = context or { includeDeclaration = true, @@ -857,7 +855,7 @@ end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction ---@see vim.lsp.protocol.CodeActionTriggerKind function M.code_action(opts) - validate({ options = { opts, 't', true } }) + validate('options', opts, 'table', true) opts = opts or {} -- Detect old API call code_action(context) which should now be -- code_action({ context = context} ) @@ -935,10 +933,8 @@ end --- @param command_params lsp.ExecuteCommandParams --- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand function M.execute_command(command_params) - validate({ - command = { command_params.command, 's' }, - arguments = { command_params.arguments, 't', true }, - }) + validate('command', command_params.command, 'string') + validate('arguments', command_params.arguments, 'table', true) command_params = { command = command_params.command, arguments = command_params.arguments, diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index e3c82f4169..d5fc5b8908 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -291,7 +291,7 @@ local client_index = 0 --- @param filename (string) path to check --- @return boolean # true if {filename} exists and is a directory, false otherwise local function is_dir(filename) - validate({ filename = { filename, 's' } }) + validate('filename', filename, 'string') local stat = uv.fs_stat(filename) return stat and stat.type == 'directory' or false end @@ -312,9 +312,7 @@ local valid_encodings = { --- @param encoding string? Encoding to normalize --- @return string # normalized encoding name local function validate_encoding(encoding) - validate({ - encoding = { encoding, 's', true }, - }) + validate('encoding', encoding, 'string', true) if not encoding then return valid_encodings.UTF16 end @@ -350,9 +348,7 @@ end --- Validates a client configuration as given to |vim.lsp.start_client()|. --- @param config vim.lsp.ClientConfig local function validate_config(config) - validate({ - config = { config, 't' }, - }) + validate('config', config, 'table') validate({ handlers = { config.handlers, 't', true }, capabilities = { config.capabilities, 't', true }, @@ -640,7 +636,7 @@ end --- @param bufnr (integer|nil) Buffer number to resolve. Defaults to current buffer --- @return integer bufnr local function resolve_bufnr(bufnr) - validate({ bufnr = { bufnr, 'n', true } }) + validate('bufnr', bufnr, 'number', true) if bufnr == nil or bufnr == 0 then return api.nvim_get_current_buf() end @@ -806,7 +802,7 @@ end --- @return boolean status true if notification was successful. false otherwise --- @see |vim.lsp.client.notify()| function Client:_cancel_request(id) - validate({ id = { id, 'n' } }) + validate('id', id, 'number') local request = self.requests[id] if request and request.type == 'pending' then request.type = 'cancel' diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index c10312484b..bf72222536 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -195,7 +195,7 @@ local _client_pull_namespaces = {} ---@param client_id integer The id of the LSP client ---@param is_pull boolean? Whether the namespace is for a pull or push client. Defaults to push function M.get_namespace(client_id, is_pull) - vim.validate({ client_id = { client_id, 'n' } }) + vim.validate('client_id', client_id, 'number') local client = vim.lsp.get_client_by_id(client_id) if is_pull then diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua index 61059180fe..61d119e653 100644 --- a/runtime/lua/vim/lsp/inlay_hint.lua +++ b/runtime/lua/vim/lsp/inlay_hint.lua @@ -145,7 +145,7 @@ end --- @return vim.lsp.inlay_hint.get.ret[] --- @since 12 function M.get(filter) - vim.validate({ filter = { filter, 'table', true } }) + vim.validate('filter', filter, 'table', true) filter = filter or {} local bufnr = filter.bufnr @@ -375,11 +375,11 @@ api.nvim_set_decoration_provider(namespace, { --- @return boolean --- @since 12 function M.is_enabled(filter) - vim.validate({ filter = { filter, 'table', true } }) + vim.validate('filter', filter, 'table', true) filter = filter or {} local bufnr = filter.bufnr - vim.validate({ bufnr = { bufnr, 'number', true } }) + vim.validate('bufnr', bufnr, 'number', true) if bufnr == nil then return globalstate.enabled elseif bufnr == 0 then @@ -406,7 +406,8 @@ end --- @param filter vim.lsp.inlay_hint.enable.Filter? --- @since 12 function M.enable(enable, filter) - vim.validate({ enable = { enable, 'boolean', true }, filter = { filter, 'table', true } }) + vim.validate('enable', enable, 'boolean', true) + vim.validate('filter', filter, 'table', true) enable = enable == nil or enable filter = filter or {} diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index e79dbd2db3..ac43801c1c 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -152,9 +152,7 @@ end ---@param err table The error object ---@return string error_message The formatted error message function M.format_rpc_error(err) - validate({ - err = { err, 't' }, - }) + validate('err', err, 'table') -- There is ErrorCodes in the LSP specification, -- but in ResponseError.code it is not used and the actual type is number. @@ -329,10 +327,8 @@ end ---@return boolean success `true` if request could be sent, `false` if not ---@return integer? message_id if request could be sent, `nil` if not function Client:request(method, params, callback, notify_reply_callback) - validate({ - callback = { callback, 'f' }, - notify_reply_callback = { notify_reply_callback, 'f', true }, - }) + validate('callback', callback, 'function') + validate('notify_reply_callback', notify_reply_callback, 'function', true) self.message_index = self.message_index + 1 local message_id = self.message_index local result = self:encode_and_send({ @@ -465,9 +461,7 @@ function Client:handle_body(body) local notify_reply_callbacks = self.notify_reply_callbacks local notify_reply_callback = notify_reply_callbacks and notify_reply_callbacks[result_id] if notify_reply_callback then - validate({ - notify_reply_callback = { notify_reply_callback, 'f' }, - }) + validate('notify_reply_callback', notify_reply_callback, 'function') notify_reply_callback(result_id) notify_reply_callbacks[result_id] = nil end @@ -498,9 +492,7 @@ function Client:handle_body(body) local callback = message_callbacks and message_callbacks[result_id] if callback then message_callbacks[result_id] = nil - validate({ - callback = { callback, 'f' }, - }) + validate('callback', callback, 'function') if decoded.error then decoded.error = setmetatable(decoded.error, { __tostring = M.format_rpc_error, @@ -734,10 +726,8 @@ end function M.start(cmd, dispatchers, extra_spawn_params) log.info('Starting RPC client', { cmd = cmd, extra = extra_spawn_params }) - validate({ - cmd = { cmd, 't' }, - dispatchers = { dispatchers, 't', true }, - }) + validate('cmd', cmd, 'table') + validate('dispatchers', dispatchers, 'table', true) extra_spawn_params = extra_spawn_params or {} diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 8182457dd0..2c7958b0d5 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -565,10 +565,8 @@ local M = {} --- - debounce (integer, default: 200): Debounce token requests --- to the server by the given number in milliseconds function M.start(bufnr, client_id, opts) - vim.validate({ - bufnr = { bufnr, 'n', false }, - client_id = { client_id, 'n', false }, - }) + vim.validate('bufnr', bufnr, 'number') + vim.validate('client_id', client_id, 'number') if bufnr == 0 then bufnr = api.nvim_get_current_buf() @@ -622,10 +620,8 @@ end ---@param bufnr (integer) Buffer number, or `0` for current buffer ---@param client_id (integer) The ID of the |vim.lsp.Client| function M.stop(bufnr, client_id) - vim.validate({ - bufnr = { bufnr, 'n', false }, - client_id = { client_id, 'n', false }, - }) + vim.validate('bufnr', bufnr, 'number') + vim.validate('client_id', client_id, 'number') if bufnr == 0 then bufnr = api.nvim_get_current_buf() @@ -708,9 +704,7 @@ end ---@param bufnr (integer|nil) filter by buffer. All buffers if nil, current --- buffer if 0 function M.force_refresh(bufnr) - vim.validate({ - bufnr = { bufnr, 'n', true }, - }) + vim.validate('bufnr', bufnr, 'number', true) local buffers = bufnr == nil and vim.tbl_keys(STHighlighter.active) or bufnr == 0 and { api.nvim_get_current_buf() } -- cgit From 564173e5568ccf11bf6554680922d3716fb6862b Mon Sep 17 00:00:00 2001 From: temhelk Date: Fri, 18 Oct 2024 03:20:58 +0300 Subject: fix(lsp.util): wrong arguments to 'validate' function --- runtime/lua/vim/lsp/util.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index e3eef8901b..83c8226326 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -888,8 +888,8 @@ end function M.make_floating_popup_options(width, height, opts) validate('opts', opts, 'table', true) opts = opts or {} - validate('opts.offset_x', opts.offset_x, 'n', true) - validate('opts.offset_y', opts.offset_y, 'n', true) + validate('opts.offset_x', opts.offset_x, 'number', true) + validate('opts.offset_y', opts.offset_y, 'number', true) local anchor = '' -- cgit From dff684fdb3d2e787ac6d6fd49ec52ede604fd0ce Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Tue, 8 Oct 2024 19:04:28 +0200 Subject: feat(lsp)!: support multiple clients in lsp.buf.references Relates to: - https://github.com/neovim/neovim/issues/17712 - https://github.com/neovim/neovim/issues/30034 --- runtime/lua/vim/lsp/buf.lua | 58 ++++++++++++++++++++++++++++++++++++---- runtime/lua/vim/lsp/handlers.lua | 25 ----------------- 2 files changed, 53 insertions(+), 30 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index f6837a627f..8803c0495a 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -438,11 +438,59 @@ end ---@param opts? vim.lsp.ListOpts function M.references(context, opts) validate('context', context, 'table', true) - local params = util.make_position_params() - params.context = context or { - includeDeclaration = true, - } - request_with_opts(ms.textDocument_references, params, opts) + local clients = vim.lsp.get_clients({ method = ms.textDocument_references }) + if not next(clients) then + return + end + local win = api.nvim_get_current_win() + local bufnr = api.nvim_get_current_buf() + opts = opts or {} + + local all_items = {} + local title = 'References' + + local function on_done() + if not next(all_items) then + vim.notify('No references found') + else + local list = { + title = title, + items = all_items, + context = { + method = ms.textDocument_references, + bufnr = bufnr, + }, + } + if opts.loclist then + vim.fn.setloclist(0, {}, ' ', list) + vim.cmd.lopen() + elseif opts.on_list then + assert(vim.is_callable(opts.on_list), 'on_list is not a function') + opts.on_list(list) + else + vim.fn.setqflist({}, ' ', list) + vim.cmd('botright copen') + end + end + end + + local remaining = #clients + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + + ---@diagnostic disable-next-line: inject-field + params.context = context or { + includeDeclaration = true, + } + client.request(ms.textDocument_references, params, function(_, result) + local items = util.locations_to_items(result or {}, client.offset_encoding) + vim.list_extend(all_items, items) + remaining = remaining - 1 + if remaining == 0 then + on_done() + end + end) + end end --- Lists all symbols in the current buffer in the quickfix window. diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 8e538242d9..3306e480dd 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -242,31 +242,6 @@ M[ms.textDocument_inlayHint] = function(...) return vim.lsp.inlay_hint.on_inlayhint(...) end ---- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references -M[ms.textDocument_references] = function(_, result, ctx, config) - if not result or vim.tbl_isempty(result) then - vim.notify('No references found') - return - end - - local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) - config = config or {} - local title = 'References' - local items = util.locations_to_items(result, client.offset_encoding) - - local list = { title = title, items = items, context = ctx } - if config.loclist then - vim.fn.setloclist(0, {}, ' ', list) - vim.cmd.lopen() - elseif config.on_list then - assert(vim.is_callable(config.on_list), 'on_list is not a function') - config.on_list(list) - else - vim.fn.setqflist({}, ' ', list) - vim.cmd('botright copen') - end -end - --- Return a function that converts LSP responses to list items and opens the list --- --- The returned function has an optional {config} parameter that accepts |vim.lsp.ListOpts| -- cgit From 0083e03d6fa7586d0d6360b40b52b0cab0d2e7ba Mon Sep 17 00:00:00 2001 From: Mathias Fußenegger Date: Sun, 20 Oct 2024 23:40:44 +0200 Subject: feat(lsp)!: support multiple clients in goto methods (#30877) Relates to: - https://github.com/neovim/neovim/issues/30034 - https://github.com/neovim/neovim/issues/17712 - https://github.com/neovim/neovim/issues/16363 Closes: - https://github.com/neovim/neovim/issues/26936 (but only provides bufnr and method) - https://github.com/neovim/neovim/issues/22318 Might fix: https://github.com/neovim/neovim/issues/30737 --- runtime/lua/vim/lsp/buf.lua | 89 +++++++++++++++++++++++++++++++++++----- runtime/lua/vim/lsp/handlers.lua | 51 ----------------------- runtime/lua/vim/lsp/util.lua | 9 +--- 3 files changed, 81 insertions(+), 68 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 8803c0495a..7bf3565499 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -50,6 +50,79 @@ local function request_with_opts(name, params, opts) request(name, params, req_handler) end +---@param method string +---@param opts? vim.lsp.LocationOpts +local function get_locations(method, opts) + opts = opts or {} + local bufnr = api.nvim_get_current_buf() + local clients = vim.lsp.get_clients({ method = method, bufnr = bufnr }) + if not next(clients) then + vim.notify(vim.lsp._unsupported_method(method), vim.log.levels.WARN) + return + end + local win = api.nvim_get_current_win() + local remaining = #clients + + ---@type vim.quickfix.entry[] + local all_items = {} + + ---@param result nil|lsp.Location|lsp.Location[] + ---@param client vim.lsp.Client + local function on_response(_, result, client) + local locations = {} + if result then + locations = vim.islist(result) and result or { result } + end + local items = util.locations_to_items(locations, client.offset_encoding) + vim.list_extend(all_items, items) + remaining = remaining - 1 + if remaining == 0 then + if vim.tbl_isempty(all_items) then + vim.notify('No locations found', vim.log.levels.INFO) + return + end + + local title = 'LSP locations' + if opts.on_list then + assert(vim.is_callable(opts.on_list), 'on_list is not a function') + opts.on_list({ + title = title, + items = all_items, + context = { bufnr = bufnr, method = method }, + }) + return + end + + if #all_items == 1 then + local item = all_items[1] + local b = item.bufnr or vim.fn.bufadd(item.filename) + vim.bo[b].buflisted = true + local w = opts.reuse_win and vim.fn.win_findbuf(b)[1] or win + api.nvim_win_set_buf(w, b) + api.nvim_win_set_cursor(w, { item.lnum, item.col - 1 }) + vim._with({ win = w }, function() + -- Open folds under the cursor + vim.cmd('normal! zv') + end) + return + end + if opts.loclist then + vim.fn.setloclist(0, {}, ' ', { title = title, items = all_items }) + vim.cmd.lopen() + else + vim.fn.setqflist({}, ' ', { title = title, items = all_items }) + vim.cmd('botright copen') + end + end + end + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + client.request(method, params, function(_, result) + on_response(_, result, client) + end) + end +end + --- @class vim.lsp.ListOpts --- --- list-handler replacing the default handler. @@ -87,30 +160,26 @@ end --- @note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead. --- @param opts? vim.lsp.LocationOpts function M.declaration(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_declaration, params, opts) + get_locations(ms.textDocument_declaration, opts) end --- Jumps to the definition of the symbol under the cursor. --- @param opts? vim.lsp.LocationOpts function M.definition(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_definition, params, opts) + get_locations(ms.textDocument_definition, opts) end --- Jumps to the definition of the type of the symbol under the cursor. --- @param opts? vim.lsp.LocationOpts function M.type_definition(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_typeDefinition, params, opts) + get_locations(ms.textDocument_typeDefinition, opts) end --- Lists all the implementations for the symbol under the cursor in the --- quickfix window. --- @param opts? vim.lsp.LocationOpts function M.implementation(opts) - local params = util.make_position_params() - request_with_opts(ms.textDocument_implementation, params, opts) + get_locations(ms.textDocument_implementation, opts) end --- Displays signature information about the symbol under the cursor in a @@ -438,12 +507,12 @@ end ---@param opts? vim.lsp.ListOpts function M.references(context, opts) validate('context', context, 'table', true) - local clients = vim.lsp.get_clients({ method = ms.textDocument_references }) + local bufnr = api.nvim_get_current_buf() + local clients = vim.lsp.get_clients({ method = ms.textDocument_references, bufnr = bufnr }) if not next(clients) then return end local win = api.nvim_get_current_win() - local bufnr = api.nvim_get_current_buf() opts = opts or {} local all_items = {} diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 3306e480dd..c87cf94b98 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -386,57 +386,6 @@ end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover M[ms.textDocument_hover] = M.hover ---- Jumps to a location. Used as a handler for multiple LSP methods. ----@param _ nil not used ----@param result (table) result of LSP method; a location or a list of locations. ----@param ctx (lsp.HandlerContext) table containing the context of the request, including the method ----@param config? vim.lsp.LocationOpts ----(`textDocument/definition` can return `Location` or `Location[]` -local function location_handler(_, result, ctx, config) - if result == nil or vim.tbl_isempty(result) then - log.info(ctx.method, 'No location found') - return nil - end - local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) - - config = config or {} - - -- textDocument/definition can return Location or Location[] - -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition - if not vim.islist(result) then - result = { result } - end - - local title = 'LSP locations' - local items = util.locations_to_items(result, client.offset_encoding) - - if config.on_list then - assert(vim.is_callable(config.on_list), 'on_list is not a function') - config.on_list({ title = title, items = items }) - return - end - if #result == 1 then - util.jump_to_location(result[1], client.offset_encoding, config.reuse_win) - return - end - if config.loclist then - vim.fn.setloclist(0, {}, ' ', { title = title, items = items }) - vim.cmd.lopen() - else - vim.fn.setqflist({}, ' ', { title = title, items = items }) - vim.cmd('botright copen') - end -end - ---- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration -M[ms.textDocument_declaration] = location_handler ---- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition -M[ms.textDocument_definition] = location_handler ---- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition -M[ms.textDocument_typeDefinition] = location_handler ---- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation -M[ms.textDocument_implementation] = location_handler - local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') --- |lsp-handler| for the method "textDocument/signatureHelp". diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 83c8226326..2c28633e3c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1023,18 +1023,13 @@ end --- Jumps to a location. --- +---@deprecated ---@param location lsp.Location|lsp.LocationLink ---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? ---@param reuse_win boolean? Jump to existing window if buffer is already open. ---@return boolean `true` if the jump succeeded function M.jump_to_location(location, offset_encoding, reuse_win) - if offset_encoding == nil then - vim.notify_once( - 'jump_to_location must be called with valid offset encoding', - vim.log.levels.WARN - ) - end - + vim.deprecate('vim.lsp.util.jump_to_location', nil, '0.12') return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true }) end -- cgit From 18b43c331d8a0ed87d7cbefe2a18543b8e4ad360 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 17 Oct 2024 11:16:16 +0200 Subject: refactor: rename vim.highlight => vim.hl Problem: - `vim.highlight` module does not follow `:help dev-name-common`, which documents the name for "highlight" as "hl". - Shorter names are usually preferred. Solution: Rename `vim.highlight` to `vim.hl`. This is not a breaking change until 2.0 (or maybe never). --- runtime/lua/vim/lsp/handlers.lua | 2 +- runtime/lua/vim/lsp/semantic_tokens.lua | 6 +++--- runtime/lua/vim/lsp/util.lua | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index c87cf94b98..37cb07025c 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -437,7 +437,7 @@ function M.signature_help(_, result, ctx, config) local fbuf, fwin = util.open_floating_preview(lines, 'markdown', config) -- Highlight the active parameter. if hl then - vim.highlight.range( + vim.hl.range( fbuf, sig_help_ns, 'LspSignatureActiveParameter', diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 2c7958b0d5..0f6e45c330 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -443,7 +443,7 @@ function STHighlighter:on_win(topline, botline) vim.api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, { hl_group = hl_group, end_col = token.end_col, - priority = vim.highlight.priorities.semantic_tokens + delta, + priority = vim.hl.priorities.semantic_tokens + delta, strict = false, }) end @@ -723,7 +723,7 @@ end --- @inlinedoc --- --- Priority for the applied extmark. ---- (Default: `vim.highlight.priorities.semantic_tokens + 3`) +--- (Default: `vim.hl.priorities.semantic_tokens + 3`) --- @field priority? integer --- Highlight a semantic token. @@ -752,7 +752,7 @@ function M.highlight_token(token, bufnr, client_id, hl_group, opts) end opts = opts or {} - local priority = opts.priority or vim.highlight.priorities.semantic_tokens + 3 + local priority = opts.priority or vim.hl.priorities.semantic_tokens + 3 vim.api.nvim_buf_set_extmark(bufnr, state.namespace, token.line, token.start_col, { hl_group = hl_group, diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 2c28633e3c..2e9c71cf38 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -2,7 +2,6 @@ local protocol = require('vim.lsp.protocol') local validate = vim.validate local api = vim.api local list_extend = vim.list_extend -local highlight = vim.highlight local uv = vim.uv local M = {} @@ -1716,13 +1715,13 @@ do --[[ References ]] [protocol.DocumentHighlightKind.Write] = 'LspReferenceWrite', } local kind = reference['kind'] or protocol.DocumentHighlightKind.Text - highlight.range( + vim.hl.range( bufnr, reference_ns, document_highlight_kind[kind], { start_line, start_idx }, { end_line, end_idx }, - { priority = vim.highlight.priorities.user } + { priority = vim.hl.priorities.user } ) end end -- cgit From 3572319b4cb1a4163624a5fe328886f1928dbc4a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 18 Oct 2024 11:33:12 +0100 Subject: feat(vim.validate): improve fast form and deprecate spec form Problem: `vim.validate()` takes two forms when it only needs one. Solution: - Teach the fast form all the features of the spec form. - Deprecate the spec form. - General optimizations for both forms. - Add a `message` argument which can be used alongside or in place of the `optional` argument. --- runtime/lua/vim/lsp/client.lua | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index d5fc5b8908..d9d6b851d0 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -349,24 +349,22 @@ end --- @param config vim.lsp.ClientConfig local function validate_config(config) validate('config', config, 'table') - validate({ - handlers = { config.handlers, 't', true }, - capabilities = { config.capabilities, 't', true }, - cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), 'directory' }, - cmd_env = { config.cmd_env, 't', true }, - detached = { config.detached, 'b', true }, - name = { config.name, 's', true }, - on_error = { config.on_error, 'f', true }, - on_exit = { config.on_exit, { 'f', 't' }, true }, - on_init = { config.on_init, { 'f', 't' }, true }, - on_attach = { config.on_attach, { 'f', 't' }, true }, - settings = { config.settings, 't', true }, - commands = { config.commands, 't', true }, - before_init = { config.before_init, { 'f', 't' }, true }, - offset_encoding = { config.offset_encoding, 's', true }, - flags = { config.flags, 't', true }, - get_language_id = { config.get_language_id, 'f', true }, - }) + validate('handlers', config.handlers, 'table', true) + validate('capabilities', config.capabilities, 'table', true) + validate('cmd_cwd', config.cmd_cwd, optional_validator(is_dir), 'directory') + validate('cmd_env', config.cmd_env, 'table', true) + validate('detached', config.detached, 'boolean', true) + validate('name', config.name, 'string', true) + validate('on_error', config.on_error, 'function', true) + validate('on_exit', config.on_exit, { 'function', 'table' }, true) + validate('on_init', config.on_init, { 'function', 'table' }, true) + validate('on_attach', config.on_attach, { 'function', 'table' }, true) + validate('settings', config.settings, 'table', true) + validate('commands', config.commands, 'table', true) + validate('before_init', config.before_init, { 'function', 'table' }, true) + validate('offset_encoding', config.offset_encoding, 'string', true) + validate('flags', config.flags, 'table', true) + validate('get_language_id', config.get_language_id, 'function', true) assert( ( -- cgit From 629a5b71b55e439392e2a5791940f6dc8d0dd7c0 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 22 Oct 2024 12:16:24 +0100 Subject: fix(lsp): support multiple clients in typehierarchy --- runtime/lua/vim/lsp/buf.lua | 139 +++++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 67 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 7bf3565499..fc41246588 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -569,6 +569,23 @@ function M.document_symbol(opts) request_with_opts(ms.textDocument_documentSymbol, params, opts) end +--- @param client_id integer +--- @param method string +--- @param params table +--- @param handler? lsp.Handler +--- @param bufnr? integer +local function request_with_id(client_id, method, params, handler, bufnr) + local client = vim.lsp.get_client_by_id(client_id) + if not client then + vim.notify( + string.format('Client with id=%d disappeared during call hierarchy request', client_id), + vim.log.levels.WARN + ) + return + end + client.request(method, params, handler, bufnr) +end + --- @param call_hierarchy_items lsp.CallHierarchyItem[] --- @return lsp.CallHierarchyItem? local function pick_call_hierarchy_item(call_hierarchy_items) @@ -600,19 +617,11 @@ local function call_hierarchy(method) vim.notify('No item resolved', vim.log.levels.WARN) return end - local call_hierarchy_item = pick_call_hierarchy_item(result) - if not call_hierarchy_item then + local item = pick_call_hierarchy_item(result) + if not item then return end - local client = vim.lsp.get_client_by_id(ctx.client_id) - if client then - client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr) - else - vim.notify( - string.format('Client with id=%d disappeared during call hierarchy request', ctx.client_id), - vim.log.levels.WARN - ) - end + request_with_id(ctx.client_id, method, { item = item }, nil, ctx.bufnr) end) end @@ -630,78 +639,74 @@ function M.outgoing_calls() call_hierarchy(ms.callHierarchy_outgoingCalls) end +--- @param item lsp.TypeHierarchyItem +local function format_type_hierarchy_item(item) + if not item.detail or #item.detail == 0 then + return item.name + end + return string.format('%s %s', item.name, item.detail) +end + --- Lists all the subtypes or supertypes of the symbol under the --- cursor in the |quickfix| window. If the symbol can resolve to --- multiple items, the user can pick one using |vim.ui.select()|. ---@param kind "subtypes"|"supertypes" function M.typehierarchy(kind) local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes - - --- Merge results from multiple clients into a single table. Client-ID is preserved. - --- - --- @param results table - --- @return [integer, lsp.TypeHierarchyItem][] - local function merge_results(results) - local merged_results = {} - for client_id, client_result in pairs(results) do - if client_result.error then - vim.notify(client_result.error.message, vim.log.levels.WARN) - elseif client_result.result then - for _, item in pairs(client_result.result) do - table.insert(merged_results, { client_id, item }) - end - end - end - return merged_results + local bufnr = api.nvim_get_current_buf() + local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method }) + if not next(clients) then + vim.notify(vim.lsp._unsupported_method(method), vim.log.levels.WARN) + return end - local bufnr = api.nvim_get_current_buf() - local params = util.make_position_params() - --- @param results table - vim.lsp.buf_request_all(bufnr, ms.textDocument_prepareTypeHierarchy, params, function(results) - local merged_results = merge_results(results) - if #merged_results == 0 then - vim.notify('No items resolved', vim.log.levels.INFO) - return - end + local win = api.nvim_get_current_win() - if #merged_results == 1 then - local item = merged_results[1] - local client = vim.lsp.get_client_by_id(item[1]) - if client then - client.request(method, { item = item[2] }, nil, bufnr) - else - vim.notify( - string.format('Client with id=%d disappeared during call hierarchy request', item[1]), - vim.log.levels.WARN - ) - end + --- @param results [integer, lsp.TypeHierarchyItem][] + local function on_response(results) + if #results == 0 then + vim.notify('No items resolved', vim.log.levels.INFO) + elseif #results == 1 then + local client_id, item = results[1][1], results[1][2] + request_with_id(client_id, method, { item = item }, nil, bufnr) else - local select_opts = { + vim.ui.select(results, { prompt = 'Select a type hierarchy item:', kind = 'typehierarchy', - format_item = function(item) - if not item[2].detail or #item[2].detail == 0 then - return item[2].name - end - return string.format('%s %s', item[2].name, item[2].detail) + format_item = function(x) + return format_type_hierarchy_item(x[2]) end, - } - - vim.ui.select(merged_results, select_opts, function(item) - local client = vim.lsp.get_client_by_id(item[1]) - if client then - --- @type lsp.TypeHierarchyItem - client.request(method, { item = item[2] }, nil, bufnr) - else - vim.notify( - string.format('Client with id=%d disappeared during call hierarchy request', item[1]), - vim.log.levels.WARN - ) + }, function(x) + if x then + local client_id, item = x[1], x[2] + request_with_id(client_id, method, { item = item }, nil, bufnr) end end) end - end) + end + + local results = {} --- @type [integer, lsp.TypeHierarchyItem][] + + local remaining = #clients + + for _, client in ipairs(clients) do + local params = util.make_position_params(win, client.offset_encoding) + client.request(method, params, function(err, result, ctx) + --- @cast result lsp.TypeHierarchyItem[]? + if err then + vim.notify(err.message, vim.log.levels.WARN) + elseif result then + for _, item in pairs(result) do + results[#results + 1] = { ctx.client_id, item } + end + end + + remaining = remaining - 1 + if remaining == 0 then + on_response(results) + end + end, bufnr) + end end --- List workspace folders. -- cgit From 008782208d826a03ab046bb2cb1bdda27c6554d0 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Tue, 22 Oct 2024 20:10:39 +0200 Subject: fix(lsp): handle mixed encoding in tagfunc params Relates to https://github.com/neovim/neovim/issues/30034 --- runtime/lua/vim/lsp/_tagfunc.lua | 44 +++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 16 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua index 70917de48d..f75d43f373 100644 --- a/runtime/lua/vim/lsp/_tagfunc.lua +++ b/runtime/lua/vim/lsp/_tagfunc.lua @@ -1,4 +1,5 @@ local lsp = vim.lsp +local api = vim.api local util = lsp.util local ms = lsp.protocol.Methods @@ -21,11 +22,12 @@ end ---@param pattern string ---@return table[] local function query_definition(pattern) - local params = util.make_position_params() - local results_by_client, err = lsp.buf_request_sync(0, ms.textDocument_definition, params, 1000) - if err then + local bufnr = api.nvim_get_current_buf() + local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_definition }) + if not next(clients) then return {} end + local win = api.nvim_get_current_win() local results = {} --- @param range lsp.Range @@ -35,23 +37,33 @@ local function query_definition(pattern) table.insert(results, mk_tag_item(pattern, range, uri, offset_encoding)) end - for client_id, lsp_results in pairs(assert(results_by_client)) do - local client = lsp.get_client_by_id(client_id) - local offset_encoding = client and client.offset_encoding or 'utf-16' - local result = lsp_results.result or {} - if result.range then -- Location - add(result.range, result.uri, offset_encoding) - else - result = result --[[@as (lsp.Location[]|lsp.LocationLink[])]] - for _, item in pairs(result) do - if item.range then -- Location - add(item.range, item.uri, offset_encoding) - else -- LocationLink - add(item.targetSelectionRange, item.targetUri, offset_encoding) + local remaining = #clients + for _, client in ipairs(clients) do + ---@param result nil|lsp.Location|lsp.Location[]|lsp.LocationLink[] + local function on_response(_, result) + if result then + local encoding = client.offset_encoding + -- single Location + if result.range then + add(result.range, result.uri, encoding) + else + for _, location in ipairs(result) do + if location.range then -- Location + add(location.range, location.uri, encoding) + else -- LocationLink + add(location.targetSelectionRange, location.targetUri, encoding) + end + end end end + remaining = remaining - 1 end + local params = util.make_position_params(win, client.offset_encoding) + client.request(ms.textDocument_definition, params, on_response, bufnr) end + vim.wait(1000, function() + return remaining == 0 + end) return results end -- cgit From ad4e14c201c2f400e721362d8581f524fdad9038 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 09:55:23 +0100 Subject: refactor(lsp.buf): use alias for vim.lsp --- runtime/lua/vim/lsp/buf.lua | 53 +++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 26 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index fc41246588..0bb7904ca1 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1,4 +1,5 @@ local api = vim.api +local lsp = vim.lsp local validate = vim.validate local util = require('vim.lsp.util') local npcall = vim.F.npcall @@ -23,7 +24,7 @@ local M = {} local function request(method, params, handler) validate('method', method, 'string') validate('handler', handler, 'function', true) - return vim.lsp.buf_request(0, method, params, handler) + return lsp.buf_request(0, method, params, handler) end --- Displays hover information about the symbol under the cursor in a floating @@ -35,15 +36,15 @@ end --- You can scroll the contents the same as you would any other buffer. function M.hover() local params = util.make_position_params() - request(ms.textDocument_hover, params) + lsp.buf_request(0, ms.textDocument_hover, params) end local function request_with_opts(name, params, opts) local req_handler --- @type function? if opts then req_handler = function(err, result, ctx, config) - local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) - local handler = client.handlers[name] or vim.lsp.handlers[name] + local client = assert(lsp.get_client_by_id(ctx.client_id)) + local handler = client.handlers[name] or lsp.handlers[name] handler(err, result, ctx, vim.tbl_extend('force', config or {}, opts)) end end @@ -55,9 +56,9 @@ end local function get_locations(method, opts) opts = opts or {} local bufnr = api.nvim_get_current_buf() - local clients = vim.lsp.get_clients({ method = method, bufnr = bufnr }) + local clients = lsp.get_clients({ method = method, bufnr = bufnr }) if not next(clients) then - vim.notify(vim.lsp._unsupported_method(method), vim.log.levels.WARN) + vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) return end local win = api.nvim_get_current_win() @@ -307,7 +308,7 @@ function M.format(opts) method = ms.textDocument_formatting end - local clients = vim.lsp.get_clients({ + local clients = lsp.get_clients({ id = opts.id, bufnr = bufnr, name = opts.name, @@ -344,7 +345,7 @@ function M.format(opts) end local params = set_range(client, util.make_formatting_params(opts.formatting_options)) client.request(method, params, function(...) - local handler = client.handlers[method] or vim.lsp.handlers[method] + local handler = client.handlers[method] or lsp.handlers[method] handler(...) do_format(next(clients, idx)) end, bufnr) @@ -386,7 +387,7 @@ end function M.rename(new_name, opts) opts = opts or {} local bufnr = opts.bufnr or api.nvim_get_current_buf() - local clients = vim.lsp.get_clients({ + local clients = lsp.get_clients({ bufnr = bufnr, name = opts.name, -- Clients must at least support rename, prepareRename is optional @@ -428,7 +429,7 @@ function M.rename(new_name, opts) local params = util.make_position_params(win, client.offset_encoding) params.newName = name local handler = client.handlers[ms.textDocument_rename] - or vim.lsp.handlers[ms.textDocument_rename] + or lsp.handlers[ms.textDocument_rename] client.request(ms.textDocument_rename, params, function(...) handler(...) try_use_client(next(clients, idx)) @@ -508,7 +509,7 @@ end function M.references(context, opts) validate('context', context, 'table', true) local bufnr = api.nvim_get_current_buf() - local clients = vim.lsp.get_clients({ method = ms.textDocument_references, bufnr = bufnr }) + local clients = lsp.get_clients({ method = ms.textDocument_references, bufnr = bufnr }) if not next(clients) then return end @@ -575,7 +576,7 @@ end --- @param handler? lsp.Handler --- @param bufnr? integer local function request_with_id(client_id, method, params, handler, bufnr) - local client = vim.lsp.get_client_by_id(client_id) + local client = lsp.get_client_by_id(client_id) if not client then vim.notify( string.format('Client with id=%d disappeared during call hierarchy request', client_id), @@ -654,9 +655,9 @@ end function M.typehierarchy(kind) local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes local bufnr = api.nvim_get_current_buf() - local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method }) + local clients = lsp.get_clients({ bufnr = bufnr, method = method }) if not next(clients) then - vim.notify(vim.lsp._unsupported_method(method), vim.log.levels.WARN) + vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) return end @@ -713,7 +714,7 @@ end --- function M.list_workspace_folders() local workspace_folders = {} - for _, client in pairs(vim.lsp.get_clients({ bufnr = 0 })) do + for _, client in pairs(lsp.get_clients({ bufnr = 0 })) do for _, folder in pairs(client.workspace_folders or {}) do table.insert(workspace_folders, folder.name) end @@ -736,7 +737,7 @@ function M.add_workspace_folder(workspace_folder) return end local bufnr = api.nvim_get_current_buf() - for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do + for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do client:_add_workspace_folder(workspace_folder) end end @@ -753,7 +754,7 @@ function M.remove_workspace_folder(workspace_folder) return end local bufnr = api.nvim_get_current_buf() - for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do + for _, client in pairs(lsp.get_clients({ bufnr = bufnr })) do client:_remove_workspace_folder(workspace_folder) end print(workspace_folder, 'is not currently part of the workspace') @@ -916,7 +917,7 @@ local function on_code_action_results(results, opts) -- command: string -- arguments?: any[] -- - local client = assert(vim.lsp.get_client_by_id(choice.ctx.client_id)) + local client = assert(lsp.get_client_by_id(choice.ctx.client_id)) local action = choice.action local bufnr = assert(choice.ctx.bufnr, 'Must have buffer number') @@ -951,14 +952,14 @@ local function on_code_action_results(results, opts) ---@param item {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext} local function format_item(item) - local clients = vim.lsp.get_clients({ bufnr = item.ctx.bufnr }) + local clients = lsp.get_clients({ bufnr = item.ctx.bufnr }) local title = item.action.title:gsub('\r\n', '\\r\\n'):gsub('\n', '\\n') if #clients == 1 then return title end - local source = vim.lsp.get_client_by_id(item.ctx.client_id).name + local source = lsp.get_client_by_id(item.ctx.client_id).name return ('%s [%s]'):format(title, source) end @@ -987,16 +988,16 @@ function M.code_action(opts) end local context = opts.context and vim.deepcopy(opts.context) or {} if not context.triggerKind then - context.triggerKind = vim.lsp.protocol.CodeActionTriggerKind.Invoked + context.triggerKind = lsp.protocol.CodeActionTriggerKind.Invoked end local mode = api.nvim_get_mode().mode local bufnr = api.nvim_get_current_buf() local win = api.nvim_get_current_win() - local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_codeAction }) + local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_codeAction }) local remaining = #clients if remaining == 0 then - if next(vim.lsp.get_clients({ bufnr = bufnr })) then - vim.notify(vim.lsp._unsupported_method(ms.textDocument_codeAction), vim.log.levels.WARN) + if next(lsp.get_clients({ bufnr = bufnr })) then + vim.notify(lsp._unsupported_method(ms.textDocument_codeAction), vim.log.levels.WARN) end return end @@ -1033,8 +1034,8 @@ function M.code_action(opts) if context.diagnostics then params.context = context else - local ns_push = vim.lsp.diagnostic.get_namespace(client.id, false) - local ns_pull = vim.lsp.diagnostic.get_namespace(client.id, true) + local ns_push = lsp.diagnostic.get_namespace(client.id, false) + local ns_pull = lsp.diagnostic.get_namespace(client.id, true) local diagnostics = {} local lnum = api.nvim_win_get_cursor(0)[1] - 1 vim.list_extend(diagnostics, vim.diagnostic.get(bufnr, { namespace = ns_pull, lnum = lnum })) -- cgit From 2ee39b7eb46f091bf22dd1ba3066afff51139bdd Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 09:58:22 +0100 Subject: refactor(lsp.buf): remove buf_request wrapper --- runtime/lua/vim/lsp/buf.lua | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 0bb7904ca1..4a48172ab1 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -7,26 +7,6 @@ local ms = require('vim.lsp.protocol').Methods local M = {} ---- Sends an async request to all active clients attached to the current ---- buffer. ---- ----@param method (string) LSP method name ----@param params (table|nil) Parameters to send to the server ----@param handler lsp.Handler? See |lsp-handler|. Follows |lsp-handler-resolution| ---- ----@return table client_request_ids Map of client-id:request-id pairs ----for all successful requests. ----@return function _cancel_all_requests Function which can be used to ----cancel all the requests. You could instead ----iterate all clients and call their `cancel_request()` methods. ---- ----@see |vim.lsp.buf_request()| -local function request(method, params, handler) - validate('method', method, 'string') - validate('handler', handler, 'function', true) - return lsp.buf_request(0, method, params, handler) -end - --- Displays hover information about the symbol under the cursor in a floating --- window. The window will be dismissed on cursor move. --- Calling the function twice will jump into the floating window @@ -48,7 +28,7 @@ local function request_with_opts(name, params, opts) handler(err, result, ctx, vim.tbl_extend('force', config or {}, opts)) end end - request(name, params, req_handler) + lsp.buf_request(0, name, params, req_handler) end ---@param method string @@ -187,7 +167,7 @@ end --- floating window. function M.signature_help() local params = util.make_position_params() - request(ms.textDocument_signatureHelp, params) + lsp.buf_request(0, ms.textDocument_signatureHelp, params) end --- Retrieves the completion items at the current cursor position. Can only be @@ -201,7 +181,7 @@ end function M.completion(context) local params = util.make_position_params() params.context = context - return request(ms.textDocument_completion, params) + return lsp.buf_request(0, ms.textDocument_completion, params) end ---@param bufnr integer @@ -609,7 +589,7 @@ end local function call_hierarchy(method) local params = util.make_position_params() --- @param result lsp.CallHierarchyItem[]? - request(ms.textDocument_prepareCallHierarchy, params, function(err, result, ctx) + lsp.buf_request(0, ms.textDocument_prepareCallHierarchy, params, function(err, result, ctx) if err then vim.notify(err.message, vim.log.levels.WARN) return @@ -794,7 +774,7 @@ end --- |hl-LspReferenceWrite| function M.document_highlight() local params = util.make_position_params() - request(ms.textDocument_documentHighlight, params) + lsp.buf_request(0, ms.textDocument_documentHighlight, params) end --- Removes document highlights from current buffer. @@ -1063,7 +1043,7 @@ function M.execute_command(command_params) arguments = command_params.arguments, workDoneToken = command_params.workDoneToken, } - request(ms.workspace_executeCommand, command_params) + lsp.buf_request(0, ms.workspace_executeCommand, command_params) end return M -- cgit From 3275ae830da97ef907ff3cfb0170706b6a430f57 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 10:31:34 +0100 Subject: fix(lsp.protocal): improve typing of constants --- runtime/lua/vim/lsp/protocol.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 1699fff0c1..b299a8438f 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -12,6 +12,8 @@ end local sysname = vim.uv.os_uname().sysname +--- @class vim.lsp.protocol.constants +--- @nodoc local constants = { --- @enum lsp.DiagnosticSeverity DiagnosticSeverity = { @@ -314,7 +316,9 @@ local constants = { }, } --- Protocol for the Microsoft Language Server Protocol (mslsp) +--- Protocol for the Microsoft Language Server Protocol (mslsp) +--- @class vim.lsp.protocol : vim.lsp.protocol.constants +--- @nodoc local protocol = {} --- @diagnostic disable:no-unknown -- cgit From 2dcbfe78fcec5f73ce061bb24b718187b9c6b134 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 10:18:36 +0100 Subject: fix(lsp.buf): use correct offset_encoding for all requests Problem: `lsp.buf_request` send the same params to all servers and many calls to this pass PositionalParams which depends on the clients offset_encoding. This can result with incorrect params being sent to a server. Solution: `lsp.buf_request` `params` argument can now be passed as a function which takes the client as the first argument. This is used in lsp/buf.lua to construct correct params for each client request. --- runtime/lua/vim/lsp/buf.lua | 68 ++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 25 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 4a48172ab1..80ba3c648f 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -7,6 +7,19 @@ local ms = require('vim.lsp.protocol').Methods local M = {} +--- @param params? table +--- @return fun(client: vim.lsp.Client): lsp.TextDocumentPositionParams +local function client_positional_params(params) + local win = api.nvim_get_current_win() + return function(client) + local ret = util.make_position_params(win, client.offset_encoding) + if params then + ret = vim.tbl_extend('force', ret, params) + end + return ret + end +end + --- Displays hover information about the symbol under the cursor in a floating --- window. The window will be dismissed on cursor move. --- Calling the function twice will jump into the floating window @@ -15,8 +28,7 @@ local M = {} --- except that "q" dismisses the window. --- You can scroll the contents the same as you would any other buffer. function M.hover() - local params = util.make_position_params() - lsp.buf_request(0, ms.textDocument_hover, params) + lsp.buf_request(0, ms.textDocument_hover, client_positional_params()) end local function request_with_opts(name, params, opts) @@ -166,8 +178,7 @@ end --- Displays signature information about the symbol under the cursor in a --- floating window. function M.signature_help() - local params = util.make_position_params() - lsp.buf_request(0, ms.textDocument_signatureHelp, params) + lsp.buf_request(0, ms.textDocument_signatureHelp, client_positional_params()) end --- Retrieves the completion items at the current cursor position. Can only be @@ -179,9 +190,13 @@ end --- ---@see vim.lsp.protocol.CompletionTriggerKind function M.completion(context) - local params = util.make_position_params() - params.context = context - return lsp.buf_request(0, ms.textDocument_completion, params) + return lsp.buf_request( + 0, + ms.textDocument_completion, + client_positional_params({ + context = context, + }) + ) end ---@param bufnr integer @@ -587,23 +602,27 @@ end --- @param method string local function call_hierarchy(method) - local params = util.make_position_params() - --- @param result lsp.CallHierarchyItem[]? - lsp.buf_request(0, ms.textDocument_prepareCallHierarchy, params, function(err, result, ctx) - if err then - vim.notify(err.message, vim.log.levels.WARN) - return - end - if not result or vim.tbl_isempty(result) then - vim.notify('No item resolved', vim.log.levels.WARN) - return - end - local item = pick_call_hierarchy_item(result) - if not item then - return + lsp.buf_request( + 0, + ms.textDocument_prepareCallHierarchy, + client_positional_params(), + --- @param result lsp.CallHierarchyItem[]? + function(err, result, ctx) + if err then + vim.notify(err.message, vim.log.levels.WARN) + return + end + if not result or vim.tbl_isempty(result) then + vim.notify('No item resolved', vim.log.levels.WARN) + return + end + local item = pick_call_hierarchy_item(result) + if not item then + return + end + request_with_id(ctx.client_id, method, { item = item }, nil, ctx.bufnr) end - request_with_id(ctx.client_id, method, { item = item }, nil, ctx.bufnr) - end) + ) end --- Lists all the call sites of the symbol under the cursor in the @@ -773,8 +792,7 @@ end --- |hl-LspReferenceRead| --- |hl-LspReferenceWrite| function M.document_highlight() - local params = util.make_position_params() - lsp.buf_request(0, ms.textDocument_documentHighlight, params) + lsp.buf_request(0, ms.textDocument_documentHighlight, client_positional_params()) end --- Removes document highlights from current buffer. -- cgit From 3c51058d7611ef235a23ae77f994403480237700 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Thu, 24 Oct 2024 10:22:38 +0200 Subject: fix(lsp): set tagstack on jump via goto methods Follow up to https://github.com/neovim/neovim/pull/30877 Fixes https://github.com/neovim/neovim/issues/30926 --- runtime/lua/vim/lsp/buf.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 80ba3c648f..1858095918 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -54,6 +54,9 @@ local function get_locations(method, opts) return end local win = api.nvim_get_current_win() + local from = vim.fn.getpos('.') + from[1] = bufnr + local tagname = vim.fn.expand('') local remaining = #clients ---@type vim.quickfix.entry[] @@ -89,6 +92,13 @@ local function get_locations(method, opts) if #all_items == 1 then local item = all_items[1] local b = item.bufnr or vim.fn.bufadd(item.filename) + + -- Save position in jumplist + vim.cmd("normal! m'") + -- Push a new item into tagstack + local tagstack = { { tagname = tagname, from = from } } + vim.fn.settagstack(vim.fn.win_getid(win), { items = tagstack }, 't') + vim.bo[b].buflisted = true local w = opts.reuse_win and vim.fn.win_findbuf(b)[1] or win api.nvim_win_set_buf(w, b) -- cgit From c8d7d65679f832b406eb551411721cf21bae52bb Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 11:34:17 +0100 Subject: fix(lsp): use correct method for prepareTypehierarchy Regression from #30902 --- runtime/lua/vim/lsp/buf.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 1858095918..e0c03edf1e 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -663,8 +663,9 @@ end ---@param kind "subtypes"|"supertypes" function M.typehierarchy(kind) local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes + local pmethod = ms.textDocument_prepareTypeHierarchy local bufnr = api.nvim_get_current_buf() - local clients = lsp.get_clients({ bufnr = bufnr, method = method }) + local clients = lsp.get_clients({ bufnr = bufnr, method = pmethod }) if not next(clients) then vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) return @@ -701,7 +702,7 @@ function M.typehierarchy(kind) for _, client in ipairs(clients) do local params = util.make_position_params(win, client.offset_encoding) - client.request(method, params, function(err, result, ctx) + client.request(pmethod, params, function(err, result, ctx) --- @cast result lsp.TypeHierarchyItem[]? if err then vim.notify(err.message, vim.log.levels.WARN) -- cgit From 39d79efa1e1e1e5c3476dee54cc2bc4abc725a8f Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 11:20:27 +0100 Subject: fix(lsp): better multi-client support for callHierarchy Only ever display a dialogue box once. Switch from vim.fn.inpulist to vim.ui.select refactor(lsp): merge call and type hierarchy functions --- runtime/lua/vim/lsp/buf.lua | 130 ++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 77 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index e0c03edf1e..f2d5d204f4 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -584,7 +584,7 @@ local function request_with_id(client_id, method, params, handler, bufnr) local client = lsp.get_client_by_id(client_id) if not client then vim.notify( - string.format('Client with id=%d disappeared during call hierarchy request', client_id), + string.format('Client with id=%d disappeared during hierarchy request', client_id), vim.log.levels.WARN ) return @@ -592,80 +592,33 @@ local function request_with_id(client_id, method, params, handler, bufnr) client.request(method, params, handler, bufnr) end ---- @param call_hierarchy_items lsp.CallHierarchyItem[] ---- @return lsp.CallHierarchyItem? -local function pick_call_hierarchy_item(call_hierarchy_items) - if #call_hierarchy_items == 1 then - return call_hierarchy_items[1] - end - local items = {} - for i, item in pairs(call_hierarchy_items) do - local entry = item.detail or item.name - table.insert(items, string.format('%d. %s', i, entry)) - end - local choice = vim.fn.inputlist(items) - if choice < 1 or choice > #items then - return - end - return call_hierarchy_items[choice] -end - ---- @param method string -local function call_hierarchy(method) - lsp.buf_request( - 0, - ms.textDocument_prepareCallHierarchy, - client_positional_params(), - --- @param result lsp.CallHierarchyItem[]? - function(err, result, ctx) - if err then - vim.notify(err.message, vim.log.levels.WARN) - return - end - if not result or vim.tbl_isempty(result) then - vim.notify('No item resolved', vim.log.levels.WARN) - return - end - local item = pick_call_hierarchy_item(result) - if not item then - return - end - request_with_id(ctx.client_id, method, { item = item }, nil, ctx.bufnr) - end - ) -end - ---- Lists all the call sites of the symbol under the cursor in the ---- |quickfix| window. If the symbol can resolve to multiple ---- items, the user can pick one in the |inputlist()|. -function M.incoming_calls() - call_hierarchy(ms.callHierarchy_incomingCalls) -end - ---- Lists all the items that are called by the symbol under the ---- cursor in the |quickfix| window. If the symbol can resolve to ---- multiple items, the user can pick one in the |inputlist()|. -function M.outgoing_calls() - call_hierarchy(ms.callHierarchy_outgoingCalls) -end - ---- @param item lsp.TypeHierarchyItem -local function format_type_hierarchy_item(item) +--- @param item lsp.TypeHierarchyItem|lsp.CallHierarchyItem +local function format_hierarchy_item(item) if not item.detail or #item.detail == 0 then return item.name end return string.format('%s %s', item.name, item.detail) end ---- Lists all the subtypes or supertypes of the symbol under the ---- cursor in the |quickfix| window. If the symbol can resolve to ---- multiple items, the user can pick one using |vim.ui.select()|. ----@param kind "subtypes"|"supertypes" -function M.typehierarchy(kind) - local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes - local pmethod = ms.textDocument_prepareTypeHierarchy +local hierarchy_methods = { + [ms.typeHierarchy_subtypes] = 'type', + [ms.typeHierarchy_supertypes] = 'type', + [ms.callHierarchy_incomingCalls] = 'call', + [ms.callHierarchy_outgoingCalls] = 'call', +} + +--- @param method string +local function hierarchy(method) + local kind = hierarchy_methods[method] + if not kind then + error('unsupported method ' .. method) + end + + local prepare_method = kind == 'type' and ms.textDocument_prepareTypeHierarchy + or ms.textDocument_prepareCallHierarchy + local bufnr = api.nvim_get_current_buf() - local clients = lsp.get_clients({ bufnr = bufnr, method = pmethod }) + local clients = lsp.get_clients({ bufnr = bufnr, method = prepare_method }) if not next(clients) then vim.notify(lsp._unsupported_method(method), vim.log.levels.WARN) return @@ -673,19 +626,19 @@ function M.typehierarchy(kind) local win = api.nvim_get_current_win() - --- @param results [integer, lsp.TypeHierarchyItem][] + --- @param results [integer, lsp.TypeHierarchyItem|lsp.CallHierarchyItem][] local function on_response(results) if #results == 0 then - vim.notify('No items resolved', vim.log.levels.INFO) + vim.notify('No item resolved', vim.log.levels.WARN) elseif #results == 1 then local client_id, item = results[1][1], results[1][2] request_with_id(client_id, method, { item = item }, nil, bufnr) else vim.ui.select(results, { - prompt = 'Select a type hierarchy item:', - kind = 'typehierarchy', + prompt = string.format('Select a %s hierarchy item:', kind), + kind = kind .. 'hierarchy', format_item = function(x) - return format_type_hierarchy_item(x[2]) + return format_hierarchy_item(x[2]) end, }, function(x) if x then @@ -696,18 +649,18 @@ function M.typehierarchy(kind) end end - local results = {} --- @type [integer, lsp.TypeHierarchyItem][] + local results = {} --- @type [integer, lsp.TypeHierarchyItem|lsp.CallHierarchyItem][] local remaining = #clients for _, client in ipairs(clients) do local params = util.make_position_params(win, client.offset_encoding) - client.request(pmethod, params, function(err, result, ctx) - --- @cast result lsp.TypeHierarchyItem[]? + --- @param result lsp.CallHierarchyItem[]|lsp.TypeHierarchyItem[]? + client.request(prepare_method, params, function(err, result, ctx) if err then vim.notify(err.message, vim.log.levels.WARN) elseif result then - for _, item in pairs(result) do + for _, item in ipairs(result) do results[#results + 1] = { ctx.client_id, item } end end @@ -720,6 +673,29 @@ function M.typehierarchy(kind) end end +--- Lists all the call sites of the symbol under the cursor in the +--- |quickfix| window. If the symbol can resolve to multiple +--- items, the user can pick one in the |inputlist()|. +function M.incoming_calls() + hierarchy(ms.callHierarchy_incomingCalls) +end + +--- Lists all the items that are called by the symbol under the +--- cursor in the |quickfix| window. If the symbol can resolve to +--- multiple items, the user can pick one in the |inputlist()|. +function M.outgoing_calls() + hierarchy(ms.callHierarchy_outgoingCalls) +end + +--- Lists all the subtypes or supertypes of the symbol under the +--- cursor in the |quickfix| window. If the symbol can resolve to +--- multiple items, the user can pick one using |vim.ui.select()|. +---@param kind "subtypes"|"supertypes" +function M.typehierarchy(kind) + local method = kind == 'subtypes' and ms.typeHierarchy_subtypes or ms.typeHierarchy_supertypes + hierarchy(method) +end + --- List workspace folders. --- function M.list_workspace_folders() -- cgit From 7a7747f1e4d96aab53ff9c52d0c3492308c22c58 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 12:11:27 +0100 Subject: feat(lsp): deprecate execute_command with client:exec_cmd --- runtime/lua/vim/lsp/buf.lua | 5 ++++- runtime/lua/vim/lsp/client.lua | 29 +++++++++++++---------------- runtime/lua/vim/lsp/codelens.lua | 2 +- runtime/lua/vim/lsp/completion.lua | 10 +--------- 4 files changed, 19 insertions(+), 27 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index f2d5d204f4..4cd4008105 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -881,7 +881,8 @@ local function on_code_action_results(results, opts) local a_cmd = action.command if a_cmd then local command = type(a_cmd) == 'table' and a_cmd or action - client:_exec_cmd(command, ctx) + --- @cast command lsp.Command + client:exec_cmd(command, ctx) end end @@ -1037,12 +1038,14 @@ function M.code_action(opts) end end +--- @deprecated --- Executes an LSP server command. --- @param command_params lsp.ExecuteCommandParams --- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand function M.execute_command(command_params) validate('command', command_params.command, 'string') validate('arguments', command_params.arguments, 'table', true) + vim.deprecate('execute_command', 'client:exec_cmd', '0.12') command_params = { command = command_params.command, arguments = command_params.arguments, diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index d9d6b851d0..2718f40c96 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -859,10 +859,9 @@ end --- or via workspace/executeCommand (if supported by the server) --- --- @param command lsp.Command ---- @param context? {bufnr: integer} +--- @param context? {bufnr?: integer} --- @param handler? lsp.Handler only called if a server command ---- @param on_unsupported? function handler invoked when the command is not supported by the client. -function Client:_exec_cmd(command, context, handler, on_unsupported) +function Client:exec_cmd(command, context, handler) context = vim.deepcopy(context or {}, true) --[[@as lsp.HandlerContext]] context.bufnr = context.bufnr or api.nvim_get_current_buf() context.client_id = self.id @@ -875,25 +874,23 @@ function Client:_exec_cmd(command, context, handler, on_unsupported) local command_provider = self.server_capabilities.executeCommandProvider local commands = type(command_provider) == 'table' and command_provider.commands or {} + if not vim.list_contains(commands, cmdname) then - if on_unsupported then - on_unsupported() - else - vim.notify_once( - string.format( - 'Language server `%s` does not support command `%s`. This command may require a client extension.', - self.name, - cmdname - ), - vim.log.levels.WARN - ) - end + vim.notify_once( + string.format( + 'Language server `%s` does not support command `%s`. This command may require a client extension.', + self.name, + cmdname + ), + vim.log.levels.WARN + ) return end -- Not using command directly to exclude extra properties, -- see https://github.com/python-lsp/python-lsp-server/issues/146 + --- @type lsp.ExecuteCommandParams local params = { - command = command.command, + command = cmdname, arguments = command.arguments, } self.request(ms.workspace_executeCommand, params, handler, context.bufnr) diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index c1b6bfb28c..d8c6c27a98 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -48,7 +48,7 @@ local function execute_lens(lens, bufnr, client_id) local client = vim.lsp.get_client_by_id(client_id) assert(client, 'Client is required to execute lens, client_id=' .. client_id) - client:_exec_cmd(lens.command, { bufnr = bufnr }, function(...) + client:exec_cmd(lens.command, { bufnr = bufnr }, function(...) vim.lsp.handlers[ms.workspace_executeCommand](...) M.refresh() end) diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 3df506d23a..e36d329dc5 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -548,15 +548,7 @@ local function on_complete_done() local command = completion_item.command if command then - client:_exec_cmd(command, { bufnr = bufnr }, nil, function() - vim.lsp.log.warn( - string.format( - 'Language server `%s` does not support command `%s`. This command may require a client extension.', - client.name, - command.command - ) - ) - end) + client:exec_cmd(command, { bufnr = bufnr }) end end -- cgit From 54249d051c3e0b97321939e6a3ae3d2e83971ce7 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 15:43:38 +0100 Subject: feat(lsp): deprecate vim.lsp.buf.completion Use `vim.lsp.completion.trigger()` instead' --- runtime/lua/vim/lsp/buf.lua | 2 ++ runtime/lua/vim/lsp/handlers.lua | 1 + 2 files changed, 3 insertions(+) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 4cd4008105..5f50e361c4 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -191,6 +191,7 @@ function M.signature_help() lsp.buf_request(0, ms.textDocument_signatureHelp, client_positional_params()) end +--- @deprecated --- Retrieves the completion items at the current cursor position. Can only be --- called in Insert mode. --- @@ -200,6 +201,7 @@ end --- ---@see vim.lsp.protocol.CompletionTriggerKind function M.completion(context) + vim.depends('vim.lsp.buf.completion', 'vim.lsp.commpletion.trigger', '0.12') return lsp.buf_request( 0, ms.textDocument_completion, diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 37cb07025c..a905f76fda 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -317,6 +317,7 @@ M[ms.textDocument_formatting] = function(_, result, ctx, _) util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) end +--- @deprecated --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion M[ms.textDocument_completion] = function(_, result, _, _) if vim.tbl_isempty(result or {}) then -- cgit From 25b53b593ef6f229fbec5b3dc205a7539579d13a Mon Sep 17 00:00:00 2001 From: Tristan Knight Date: Sat, 26 Oct 2024 15:38:25 +0100 Subject: refactor(lsp): drop str_byteindex/str_utfindex wrappers #30915 * deprecate old signatures * move to new str_byteindex/str_utfindex signature * use single-underscore name (double-underscore is reserved for Lua itself) --- runtime/lua/vim/lsp/completion.lua | 2 +- runtime/lua/vim/lsp/diagnostic.lua | 24 ++--------- runtime/lua/vim/lsp/inlay_hint.lua | 14 ++---- runtime/lua/vim/lsp/semantic_tokens.lua | 14 ++---- runtime/lua/vim/lsp/sync.lua | 59 +++++--------------------- runtime/lua/vim/lsp/util.lua | 75 +++------------------------------ 6 files changed, 27 insertions(+), 161 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index e36d329dc5..10086fa49e 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -315,7 +315,7 @@ local function adjust_start_col(lnum, line, items, encoding) end end if min_start_char then - return lsp.util._str_byteindex_enc(line, min_start_char, encoding) + return vim.str_byteindex(line, encoding, min_start_char, false) else return nil end diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index bf72222536..c59e2db901 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -33,25 +33,6 @@ local function severity_vim_to_lsp(severity) return severity end ----@param lines string[]? ----@param lnum integer ----@param col integer ----@param offset_encoding string ----@return integer -local function line_byte_from_position(lines, lnum, col, offset_encoding) - if not lines or offset_encoding == 'utf-8' then - return col - end - - local line = lines[lnum + 1] - local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == 'utf-16') - if ok then - return result --- @type integer - end - - return col -end - ---@param bufnr integer ---@return string[]? local function get_buf_lines(bufnr) @@ -118,12 +99,13 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id) ) message = diagnostic.message.value end + local line = buf_lines and buf_lines[start.line + 1] or '' --- @type vim.Diagnostic return { lnum = start.line, - col = line_byte_from_position(buf_lines, start.line, start.character, offset_encoding), + col = vim.str_byteindex(line, offset_encoding, start.character, false), end_lnum = _end.line, - end_col = line_byte_from_position(buf_lines, _end.line, _end.character, offset_encoding), + end_col = vim.str_byteindex(line, offset_encoding, _end.character, false), severity = severity_lsp_to_vim(diagnostic.severity), message = message, source = diagnostic.source, diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua index 61d119e653..e5892928cf 100644 --- a/runtime/lua/vim/lsp/inlay_hint.lua +++ b/runtime/lua/vim/lsp/inlay_hint.lua @@ -70,20 +70,12 @@ function M.on_inlayhint(err, result, ctx, _) end local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) - ---@param position lsp.Position - ---@return integer - local function pos_to_byte(position) - local col = position.character - if col > 0 then - local line = lines[position.line + 1] or '' - return util._str_byteindex_enc(line, col, client.offset_encoding) - end - return col - end for _, hint in ipairs(result) do local lnum = hint.position.line - hint.position.character = pos_to_byte(hint.position) + local line = lines and lines[lnum + 1] or '' + hint.position.character = + vim.str_byteindex(line, client.offset_encoding, hint.position.character, false) table.insert(new_lnum_hints[lnum], hint) end diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 0f6e45c330..d680522592 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -137,16 +137,10 @@ local function tokens_to_ranges(data, bufnr, client, request) local token_type = token_types[data[i + 3] + 1] local modifiers = modifiers_from_number(data[i + 4], token_modifiers) - local function _get_byte_pos(col) - if col > 0 then - local buf_line = lines[line + 1] or '' - return util._str_byteindex_enc(buf_line, col, client.offset_encoding) - end - return col - end - - local start_col = _get_byte_pos(start_char) - local end_col = _get_byte_pos(start_char + data[i + 2]) + local end_char = start_char + data[i + 2] + local buf_line = lines and lines[line + 1] or '' + local start_col = vim.str_byteindex(buf_line, client.offset_encoding, start_char, false) + local end_col = vim.str_byteindex(buf_line, client.offset_encoding, end_char, false) if token_type then ranges[#ranges + 1] = { diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua index bdfe8d51b8..3df45ebff0 100644 --- a/runtime/lua/vim/lsp/sync.lua +++ b/runtime/lua/vim/lsp/sync.lua @@ -48,45 +48,6 @@ local str_utfindex = vim.str_utfindex local str_utf_start = vim.str_utf_start local str_utf_end = vim.str_utf_end --- Given a line, byte idx, and offset_encoding convert to the --- utf-8, utf-16, or utf-32 index. ----@param line string the line to index into ----@param byte integer the byte idx ----@param offset_encoding string utf-8|utf-16|utf-32|nil (default: utf-8) ----@return integer utf_idx for the given encoding -local function byte_to_utf(line, byte, offset_encoding) - -- convert to 0 based indexing for str_utfindex - byte = byte - 1 - - local utf_idx, _ --- @type integer, integer - -- Convert the byte range to utf-{8,16,32} and convert 1-based (lua) indexing to 0-based - if offset_encoding == 'utf-16' then - _, utf_idx = str_utfindex(line, byte) - elseif offset_encoding == 'utf-32' then - utf_idx, _ = str_utfindex(line, byte) - else - utf_idx = byte - end - - -- convert to 1 based indexing - return utf_idx + 1 -end - ----@param line string ----@param offset_encoding string ----@return integer -local function compute_line_length(line, offset_encoding) - local length, _ --- @type integer, integer - if offset_encoding == 'utf-16' then - _, length = str_utfindex(line) - elseif offset_encoding == 'utf-32' then - length, _ = str_utfindex(line) - else - length = #line - end - return length -end - -- Given a line, byte idx, alignment, and offset_encoding convert to the aligned -- utf-8 index and either the utf-16, or utf-32 index. ---@param line string the line to index into @@ -101,7 +62,7 @@ local function align_end_position(line, byte, offset_encoding) char = byte -- Called in the case of extending an empty line "" -> "a" elseif byte == #line + 1 then - char = compute_line_length(line, offset_encoding) + 1 + char = str_utfindex(line, offset_encoding) + 1 else -- Modifying line, find the nearest utf codepoint local offset = str_utf_start(line, byte) @@ -111,9 +72,10 @@ local function align_end_position(line, byte, offset_encoding) byte = byte + str_utf_end(line, byte) + 1 end if byte <= #line then - char = byte_to_utf(line, byte, offset_encoding) + --- Convert to 0 based for input, and from 0 based for output + char = str_utfindex(line, offset_encoding, byte - 1) + 1 else - char = compute_line_length(line, offset_encoding) + 1 + char = str_utfindex(line, offset_encoding) + 1 end -- Extending line, find the nearest utf codepoint for the last valid character end @@ -153,7 +115,7 @@ local function compute_start_range( if line then line_idx = firstline - 1 byte_idx = #line + 1 - char_idx = compute_line_length(line, offset_encoding) + 1 + char_idx = str_utfindex(line, offset_encoding) + 1 else line_idx = firstline byte_idx = 1 @@ -190,10 +152,11 @@ local function compute_start_range( char_idx = 1 elseif start_byte_idx == #prev_line + 1 then byte_idx = start_byte_idx - char_idx = compute_line_length(prev_line, offset_encoding) + 1 + char_idx = str_utfindex(prev_line, offset_encoding) + 1 else byte_idx = start_byte_idx + str_utf_start(prev_line, start_byte_idx) - char_idx = byte_to_utf(prev_line, byte_idx, offset_encoding) + --- Convert to 0 based for input, and from 0 based for output + char_idx = vim.str_utfindex(prev_line, offset_encoding, byte_idx - 1) + 1 end -- Return the start difference (shared for new and prev lines) @@ -230,7 +193,7 @@ local function compute_end_range( return { line_idx = lastline - 1, byte_idx = #prev_line + 1, - char_idx = compute_line_length(prev_line, offset_encoding) + 1, + char_idx = str_utfindex(prev_line, offset_encoding) + 1, }, { line_idx = 1, byte_idx = 1, char_idx = 1 } end -- If firstline == new_lastline, the first change occurred on a line that was deleted. @@ -376,7 +339,7 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi local start_line = lines[start_range.line_idx] local range_length --- @type integer if start_line and #start_line > 0 then - range_length = compute_line_length(start_line, offset_encoding) + range_length = str_utfindex(start_line, offset_encoding) - start_range.char_idx + 1 + line_ending_length @@ -389,7 +352,7 @@ local function compute_range_length(lines, start_range, end_range, offset_encodi for idx = start_range.line_idx + 1, end_range.line_idx - 1 do -- Length full line plus newline character if #lines[idx] > 0 then - range_length = range_length + compute_line_length(lines[idx], offset_encoding) + #line_ending + range_length = range_length + str_utfindex(lines[idx], offset_encoding) + #line_ending else range_length = range_length + line_ending_length end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 2e9c71cf38..9646f4d571 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -116,71 +116,6 @@ local function create_window_without_focus() return new end ---- Convert byte index to `encoding` index. ---- Convenience wrapper around vim.str_utfindex ----@param line string line to be indexed ----@param index integer? byte index (utf-8), or `nil` for length ----@param encoding 'utf-8'|'utf-16'|'utf-32'? defaults to utf-16 ----@return integer `encoding` index of `index` in `line` -function M._str_utfindex_enc(line, index, encoding) - local len32, len16 = vim.str_utfindex(line) - if not encoding then - encoding = 'utf-16' - end - if encoding == 'utf-8' then - if index then - return index - else - return #line - end - elseif encoding == 'utf-16' then - if not index or index > len16 then - return len16 - end - local _, col16 = vim.str_utfindex(line, index) - return col16 - elseif encoding == 'utf-32' then - if not index or index > len32 then - return len32 - end - local col32, _ = vim.str_utfindex(line, index) - return col32 - else - error('Invalid encoding: ' .. vim.inspect(encoding)) - end -end - ---- Convert UTF index to `encoding` index. ---- Convenience wrapper around vim.str_byteindex ----Alternative to vim.str_byteindex that takes an encoding. ----@param line string line to be indexed ----@param index integer UTF index ----@param encoding string utf-8|utf-16|utf-32| defaults to utf-16 ----@return integer byte (utf-8) index of `encoding` index `index` in `line` -function M._str_byteindex_enc(line, index, encoding) - -- LSP spec: if character > line length, default to the line length. - -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position - local len8 = #line - if not encoding then - encoding = 'utf-16' - end - if encoding == 'utf-8' then - if index and index <= len8 then - return index - else - return len8 - end - end - local len32, len16 = vim.str_utfindex(line) - if encoding == 'utf-16' then - return index <= len16 and vim.str_byteindex(line, index, true) or len8 - elseif encoding == 'utf-32' then - return index <= len32 and vim.str_byteindex(line, index) or len8 - else - error('Invalid encoding: ' .. vim.inspect(encoding)) - end -end - --- Replaces text in a range with new text. --- --- CAUTION: Changes in-place! @@ -352,7 +287,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding) -- character if col > 0 then local line = get_line(bufnr, position.line) or '' - return M._str_byteindex_enc(line, col, offset_encoding) + return vim.str_byteindex(line, offset_encoding, col, false) end return col end @@ -1787,8 +1722,8 @@ function M.locations_to_items(locations, offset_encoding) local end_row = end_pos.line local line = lines[row] or '' local end_line = lines[end_row] or '' - local col = M._str_byteindex_enc(line, pos.character, offset_encoding) - local end_col = M._str_byteindex_enc(end_line, end_pos.character, offset_encoding) + local col = vim.str_byteindex(line, offset_encoding, pos.character, false) + local end_col = vim.str_byteindex(end_line, offset_encoding, end_pos.character, false) items[#items + 1] = { filename = filename, @@ -1911,7 +1846,7 @@ local function make_position_param(window, offset_encoding) return { line = 0, character = 0 } end - col = M._str_utfindex_enc(line, col, offset_encoding) + col = vim.str_utfindex(line, offset_encoding, col, false) return { line = row, character = col } end @@ -2092,7 +2027,7 @@ function M.character_offset(buf, row, col, offset_encoding) ) offset_encoding = vim.lsp.get_clients({ bufnr = buf })[1].offset_encoding end - return M._str_utfindex_enc(line, col, offset_encoding) + return vim.str_utfindex(line, offset_encoding, col, false) end --- Helper function to return nested values in language server settings -- cgit From 123c0b6b4e2d3f0665774b70b6bec79afbef4509 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Sat, 26 Oct 2024 14:06:15 -0700 Subject: docs(lsp): document alternative for vim.lsp.util.jump_to_location --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 9646f4d571..41f93e5374 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -957,7 +957,7 @@ end --- Jumps to a location. --- ----@deprecated +---@deprecated use `vim.lsp.util.show_document` with `{focus=true}` instead ---@param location lsp.Location|lsp.LocationLink ---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? ---@param reuse_win boolean? Jump to existing window if buffer is already open. -- cgit From 0086ee90dd2f058f48bbaceef842bb0d8923cc34 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Mon, 28 Oct 2024 06:14:56 -0700 Subject: fix(lsp): list all workspace folders in healthcheck #30966 --- runtime/lua/vim/lsp/health.lua | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/health.lua b/runtime/lua/vim/lsp/health.lua index 18066a84db..0d314108fe 100644 --- a/runtime/lua/vim/lsp/health.lua +++ b/runtime/lua/vim/lsp/health.lua @@ -39,12 +39,27 @@ local function check_active_clients() elseif type(client.config.cmd) == 'function' then cmd = tostring(client.config.cmd) end + local dirs_info ---@type string + if client.workspace_folders and #client.workspace_folders > 1 then + dirs_info = string.format( + ' Workspace folders:\n %s', + vim + .iter(client.workspace_folders) + ---@param folder lsp.WorkspaceFolder + :map(function(folder) + return folder.name + end) + :join('\n ') + ) + else + dirs_info = string.format( + ' Root directory: %s', + client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~') + ) or nil + end report_info(table.concat({ string.format('%s (id: %d)', client.name, client.id), - string.format( - ' Root directory: %s', - client.root_dir and vim.fn.fnamemodify(client.root_dir, ':~') or nil - ), + dirs_info, string.format(' Command: %s', cmd), string.format(' Settings: %s', vim.inspect(client.settings, { newline = '\n ' })), string.format( -- cgit From 8260e4860b27a54a061bd8e2a9da23069993953a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 24 Oct 2024 16:47:41 +0100 Subject: feat(lsp)!: multiple client support for vim.lsp.buf.hover() Deprecate `vim.lsp.handlers.hover` and `vim.lsp.handlers['textDocument/hover']` --- runtime/lua/vim/lsp/_meta.lua | 1 + runtime/lua/vim/lsp/buf.lua | 77 ++++++++++++++++++++++++++++++++++++++-- runtime/lua/vim/lsp/handlers.lua | 5 ++- 3 files changed, 80 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/_meta.lua b/runtime/lua/vim/lsp/_meta.lua index be3222828d..589a49c003 100644 --- a/runtime/lua/vim/lsp/_meta.lua +++ b/runtime/lua/vim/lsp/_meta.lua @@ -2,6 +2,7 @@ error('Cannot require a meta file') ---@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext, config?: table): ...any +---@alias lsp.MultiHandler fun(results: table, context: lsp.HandlerContext, config?: table): ...any ---@class lsp.HandlerContext ---@field method string diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 5f50e361c4..dd06b1b6c0 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -20,6 +20,9 @@ local function client_positional_params(params) end end +--- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts +--- @field silent? boolean + --- Displays hover information about the symbol under the cursor in a floating --- window. The window will be dismissed on cursor move. --- Calling the function twice will jump into the floating window @@ -27,8 +30,78 @@ end --- In the floating window, all commands and mappings are available as usual, --- except that "q" dismisses the window. --- You can scroll the contents the same as you would any other buffer. -function M.hover() - lsp.buf_request(0, ms.textDocument_hover, client_positional_params()) +--- @param config? vim.lsp.buf.hover.Opts +function M.hover(config) + config = config or {} + config.focus_id = ms.textDocument_hover + + lsp.buf_request_all(0, ms.textDocument_hover, client_positional_params(), function(results, ctx) + if api.nvim_get_current_buf() ~= ctx.bufnr then + -- Ignore result since buffer changed. This happens for slow language servers. + return + end + + -- Filter errors from results + local results1 = {} --- @type table + + for client_id, resp in pairs(results) do + local err, result = resp.err, resp.result + if err then + lsp.log.error(err.code, err.message) + elseif result then + results1[client_id] = result + end + end + + if #results1 == 0 then + if config.silent ~= true then + vim.notify('No information available') + end + return + end + + local contents = {} --- @type string[] + + local nresults = #vim.tbl_keys(results1) + + local format = 'markdown' + + for client_id, result in pairs(results1) do + if nresults > 1 then + -- Show client name if there are multiple clients + contents[#contents + 1] = string.format('# %s', lsp.get_client_by_id(client_id).name) + end + if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then + if #results1 == 1 then + format = 'plaintext' + contents = vim.split(result.contents.value or '', '\n', { trimempty = true }) + else + -- Surround plaintext with ``` to get correct formatting + contents[#contents + 1] = '```' + vim.list_extend( + contents, + vim.split(result.contents.value or '', '\n', { trimempty = true }) + ) + contents[#contents + 1] = '```' + end + else + vim.list_extend(contents, util.convert_input_to_markdown_lines(result.contents)) + end + contents[#contents + 1] = '---' + end + + -- Remove last linebreak ('---') + contents[#contents] = nil + + if vim.tbl_isempty(contents) then + if config.silent ~= true then + vim.notify('No information available') + end + return + end + + lsp.util.open_floating_preview(contents, format, config) + end) end local function request_with_opts(name, params, opts) diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index a905f76fda..ad827cfd34 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -317,7 +317,7 @@ M[ms.textDocument_formatting] = function(_, result, ctx, _) util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) end ---- @deprecated +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion M[ms.textDocument_completion] = function(_, result, _, _) if vim.tbl_isempty(result or {}) then @@ -334,6 +334,7 @@ M[ms.textDocument_completion] = function(_, result, _, _) vim.fn.complete(textMatch + 1, matches) end +--- @deprecated --- |lsp-handler| for the method "textDocument/hover" --- --- ```lua @@ -384,7 +385,9 @@ function M.hover(_, result, ctx, config) return util.open_floating_preview(contents, format, config) end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover +--- @diagnostic disable-next-line: deprecated M[ms.textDocument_hover] = M.hover local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') -- cgit From b4599acbf8e30aa1234bc4041546f960dcc3a238 Mon Sep 17 00:00:00 2001 From: notomo <18519692+notomo@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:57:33 +0900 Subject: fix(lsp): correct hover result handling (#30995) Problem: vim.lsp.buf.hover() displays "No information available" when client_id is not 1. Solution: use vim.tbl_isempty(tbl) instead of #tbl==0 --- runtime/lua/vim/lsp/buf.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index dd06b1b6c0..4818359fb3 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -53,7 +53,7 @@ function M.hover(config) end end - if #results1 == 0 then + if vim.tbl_isempty(results1) then if config.silent ~= true then vim.notify('No information available') end -- cgit From f54266dbed6f1a4cb4fad3486a722a25070d7feb Mon Sep 17 00:00:00 2001 From: nikolightsaber <103886134+nikolightsaber@users.noreply.github.com> Date: Fri, 1 Nov 2024 10:03:09 +0100 Subject: fix(lsp): hover border type can be string (#31013) Border type can also be a string as defined in `api-win_config` Co-authored-by: Nikolai Devolder --- runtime/lua/vim/lsp/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 41f93e5374..763cd940c3 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1485,7 +1485,7 @@ end --- --- offset to add to `row` --- @field offset_y? integer ---- @field border? (string|[string,string])[] override `border` +--- @field border? string|(string|[string,string])[] override `border` --- @field zindex? integer override `zindex`, defaults to 50 --- @field title? string --- @field title_pos? 'left'|'center'|'right' -- cgit From 9b357e30fdd0a575480182872331fdb87e9cc331 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 29 Oct 2024 09:36:02 +0000 Subject: feat(lsp)!: remove client-server handlers from vim.lsp.handlers - Partition the handlers in vim.lsp.handlers as: - client to server response handlers (RCS) - server to client request handlers (RSC) - server to client notification handlers (NSC) Note use string indexes instead of protocol.methods for improved typing in LuaLS (tip: use hover on RCS, RSC or NSC). --- runtime/lua/vim/lsp/_meta.lua | 4 +- runtime/lua/vim/lsp/buf.lua | 66 +++++++++++++- runtime/lua/vim/lsp/codelens.lua | 2 +- runtime/lua/vim/lsp/diagnostic.lua | 83 +++--------------- runtime/lua/vim/lsp/handlers.lua | 173 ++++++++++++++++++++++--------------- runtime/lua/vim/lsp/inlay_hint.lua | 4 +- runtime/lua/vim/lsp/protocol.lua | 104 +++++++++++++++++++++- 7 files changed, 287 insertions(+), 149 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/_meta.lua b/runtime/lua/vim/lsp/_meta.lua index 589a49c003..bf693ccc57 100644 --- a/runtime/lua/vim/lsp/_meta.lua +++ b/runtime/lua/vim/lsp/_meta.lua @@ -1,8 +1,8 @@ ---@meta error('Cannot require a meta file') ----@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext, config?: table): ...any ----@alias lsp.MultiHandler fun(results: table, context: lsp.HandlerContext, config?: table): ...any +---@alias lsp.Handler fun(err: lsp.ResponseError?, result: any, context: lsp.HandlerContext): ...any +---@alias lsp.MultiHandler fun(results: table, context: lsp.HandlerContext): ...any ---@class lsp.HandlerContext ---@field method string diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 4818359fb3..152226a757 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -258,10 +258,72 @@ function M.implementation(opts) get_locations(ms.textDocument_implementation, opts) end +local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') + +--- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts +--- @field silent? boolean + +-- TODO(lewis6991): support multiple clients --- Displays signature information about the symbol under the cursor in a --- floating window. -function M.signature_help() - lsp.buf_request(0, ms.textDocument_signatureHelp, client_positional_params()) +--- @param config? vim.lsp.buf.signature_help.Opts +function M.signature_help(config) + local method = ms.textDocument_signatureHelp + + config = config or {} + config.focus_id = method + + lsp.buf_request(0, method, client_positional_params(), function(err, result, ctx) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + + if err then + vim.notify( + client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, + vim.log.levels.ERROR + ) + api.nvim_command('redraw') + return + end + + if api.nvim_get_current_buf() ~= ctx.bufnr then + -- Ignore result since buffer changed. This happens for slow language servers. + return + end + + -- When use `autocmd CompleteDone lua vim.lsp.buf.signature_help()` to call signatureHelp handler + -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `` to ignore + if not result or not result.signatures or not result.signatures[1] then + if config.silent ~= true then + print('No signature help available') + end + return + end + + local triggers = + vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') + + local ft = vim.bo[ctx.bufnr].filetype + local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers) + if not lines or vim.tbl_isempty(lines) then + if config.silent ~= true then + print('No signature help available') + end + return + end + + local fbuf = util.open_floating_preview(lines, 'markdown', config) + + -- Highlight the active parameter. + if hl then + vim.hl.range( + fbuf, + sig_help_ns, + 'LspSignatureActiveParameter', + { hl[1], hl[2] }, + { hl[3], hl[4] } + ) + end + end) end --- @deprecated diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index d8c6c27a98..fdbdda695a 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -261,7 +261,7 @@ end ---@param err lsp.ResponseError? ---@param result lsp.CodeLens[] ---@param ctx lsp.HandlerContext -function M.on_codelens(err, result, ctx, _) +function M.on_codelens(err, result, ctx) if err then active_refreshes[assert(ctx.bufnr)] = nil log.error('codelens', err) diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index c59e2db901..8fd30c7668 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -9,14 +9,6 @@ local augroup = api.nvim_create_augroup('vim_lsp_diagnostic', {}) local DEFAULT_CLIENT_ID = -1 -local function get_client_id(client_id) - if client_id == nil then - client_id = DEFAULT_CLIENT_ID - end - - return client_id -end - ---@param severity lsp.DiagnosticSeverity local function severity_lsp_to_vim(severity) if type(severity) == 'string' then @@ -218,8 +210,7 @@ end --- @param client_id? integer --- @param diagnostics vim.Diagnostic[] --- @param is_pull boolean ---- @param config? vim.diagnostic.Opts -local function handle_diagnostics(uri, client_id, diagnostics, is_pull, config) +local function handle_diagnostics(uri, client_id, diagnostics, is_pull) local fname = vim.uri_to_fname(uri) if #diagnostics == 0 and vim.fn.bufexists(fname) == 0 then @@ -231,91 +222,39 @@ local function handle_diagnostics(uri, client_id, diagnostics, is_pull, config) return end - client_id = get_client_id(client_id) - local namespace = M.get_namespace(client_id, is_pull) - - if config then - --- @cast config table - for _, opt in pairs(config) do - convert_severity(opt) - end - -- Persist configuration to ensure buffer reloads use the same - -- configuration. To make lsp.with configuration work (See :help - -- lsp-handler-configuration) - vim.diagnostic.config(config, namespace) + if client_id == nil then + client_id = DEFAULT_CLIENT_ID end + local namespace = M.get_namespace(client_id, is_pull) + vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) end --- |lsp-handler| for the method "textDocument/publishDiagnostics" --- ---- See |vim.diagnostic.config()| for configuration options. Handler-specific ---- configuration can be set using |vim.lsp.with()|: ---- ---- ```lua ---- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with( ---- vim.lsp.diagnostic.on_publish_diagnostics, { ---- -- Enable underline, use default values ---- underline = true, ---- -- Enable virtual text, override spacing to 4 ---- virtual_text = { ---- spacing = 4, ---- }, ---- -- Use a function to dynamically turn signs off ---- -- and on, using buffer local variables ---- signs = function(namespace, bufnr) ---- return vim.b[bufnr].show_signs == true ---- end, ---- -- Disable a feature ---- update_in_insert = false, ---- } ---- ) ---- ``` +--- See |vim.diagnostic.config()| for configuration options. --- ---@param _ lsp.ResponseError? ---@param result lsp.PublishDiagnosticsParams ---@param ctx lsp.HandlerContext ----@param config? vim.diagnostic.Opts Configuration table (see |vim.diagnostic.config()|). -function M.on_publish_diagnostics(_, result, ctx, config) - handle_diagnostics(result.uri, ctx.client_id, result.diagnostics, false, config) +function M.on_publish_diagnostics(_, result, ctx) + handle_diagnostics(result.uri, ctx.client_id, result.diagnostics, false) end --- |lsp-handler| for the method "textDocument/diagnostic" --- ---- See |vim.diagnostic.config()| for configuration options. Handler-specific ---- configuration can be set using |vim.lsp.with()|: ---- ---- ```lua ---- vim.lsp.handlers["textDocument/diagnostic"] = vim.lsp.with( ---- vim.lsp.diagnostic.on_diagnostic, { ---- -- Enable underline, use default values ---- underline = true, ---- -- Enable virtual text, override spacing to 4 ---- virtual_text = { ---- spacing = 4, ---- }, ---- -- Use a function to dynamically turn signs off ---- -- and on, using buffer local variables ---- signs = function(namespace, bufnr) ---- return vim.b[bufnr].show_signs == true ---- end, ---- -- Disable a feature ---- update_in_insert = false, ---- } ---- ) ---- ``` +--- See |vim.diagnostic.config()| for configuration options. --- ---@param _ lsp.ResponseError? ---@param result lsp.DocumentDiagnosticReport ---@param ctx lsp.HandlerContext ----@param config vim.diagnostic.Opts Configuration table (see |vim.diagnostic.config()|). -function M.on_diagnostic(_, result, ctx, config) +function M.on_diagnostic(_, result, ctx) if result == nil or result.kind == 'unchanged' then return end - handle_diagnostics(ctx.params.textDocument.uri, ctx.client_id, result.items, true, config) + handle_diagnostics(ctx.params.textDocument.uri, ctx.client_id, result.items, true) end --- Clear push diagnostics and diagnostic cache. diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index ad827cfd34..2b7aefe0e1 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -5,10 +5,21 @@ local util = require('vim.lsp.util') local api = vim.api local completion = require('vim.lsp.completion') ---- @type table +--- @type table local M = {} --- FIXME: DOC: Expose in vimdocs +--- @deprecated +--- Client to server response handlers. +--- @type table +local RCS = {} + +--- Server to client request handlers. +--- @type table +local RSC = {} + +--- Server to client notification handlers. +--- @type table +local NSC = {} --- Writes to error buffer. ---@param ... string Will be concatenated before being written @@ -18,14 +29,15 @@ local function err_message(...) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand -M[ms.workspace_executeCommand] = function(_, _, _, _) +RCS[ms.workspace_executeCommand] = function(_, _, _) -- Error handling is done implicitly by wrapping all handlers; see end of this file end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress ---@param params lsp.ProgressParams ---@param ctx lsp.HandlerContext -M[ms.dollar_progress] = function(_, params, ctx) +---@diagnostic disable-next-line:no-unknown +RSC[ms.dollar_progress] = function(_, params, ctx) local client = vim.lsp.get_client_by_id(ctx.client_id) if not client then err_message('LSP[id=', tostring(ctx.client_id), '] client has shut down during progress update') @@ -59,26 +71,26 @@ M[ms.dollar_progress] = function(_, params, ctx) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create ----@param result lsp.WorkDoneProgressCreateParams +---@param params lsp.WorkDoneProgressCreateParams ---@param ctx lsp.HandlerContext -M[ms.window_workDoneProgress_create] = function(_, result, ctx) +RSC[ms.window_workDoneProgress_create] = function(_, params, ctx) local client = vim.lsp.get_client_by_id(ctx.client_id) if not client then err_message('LSP[id=', tostring(ctx.client_id), '] client has shut down during progress update') return vim.NIL end - client.progress:push(result) + client.progress:push(params) return vim.NIL end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest ----@param result lsp.ShowMessageRequestParams -M[ms.window_showMessageRequest] = function(_, result) - local actions = result.actions or {} +---@param params lsp.ShowMessageRequestParams +RSC[ms.window_showMessageRequest] = function(_, params) + local actions = params.actions or {} local co, is_main = coroutine.running() if co and not is_main then local opts = { - prompt = result.message .. ': ', + prompt = params.message .. ': ', format_item = function(action) return (action.title:gsub('\r\n', '\\r\\n')):gsub('\n', '\\n') end, @@ -92,7 +104,7 @@ M[ms.window_showMessageRequest] = function(_, result) end) return coroutine.yield() else - local option_strings = { result.message, '\nRequest Actions:' } + local option_strings = { params.message, '\nRequest Actions:' } for i, action in ipairs(actions) do local title = action.title:gsub('\r\n', '\\r\\n') title = title:gsub('\n', '\\n') @@ -108,19 +120,19 @@ M[ms.window_showMessageRequest] = function(_, result) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability ---- @param result lsp.RegistrationParams -M[ms.client_registerCapability] = function(_, result, ctx) +--- @param params lsp.RegistrationParams +RSC[ms.client_registerCapability] = function(_, params, ctx) local client_id = ctx.client_id local client = assert(vim.lsp.get_client_by_id(client_id)) - client.dynamic_capabilities:register(result.registrations) + client.dynamic_capabilities:register(params.registrations) for bufnr, _ in pairs(client.attached_buffers) do vim.lsp._set_defaults(client, bufnr) end ---@type string[] local unsupported = {} - for _, reg in ipairs(result.registrations) do + for _, reg in ipairs(params.registrations) do if reg.method == ms.workspace_didChangeWatchedFiles then vim.lsp._watchfiles.register(reg, ctx) elseif not client.dynamic_capabilities:supports_registration(reg.method) then @@ -139,13 +151,13 @@ M[ms.client_registerCapability] = function(_, result, ctx) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability ---- @param result lsp.UnregistrationParams -M[ms.client_unregisterCapability] = function(_, result, ctx) +--- @param params lsp.UnregistrationParams +RSC[ms.client_unregisterCapability] = function(_, params, ctx) local client_id = ctx.client_id local client = assert(vim.lsp.get_client_by_id(client_id)) - client.dynamic_capabilities:unregister(result.unregisterations) + client.dynamic_capabilities:unregister(params.unregisterations) - for _, unreg in ipairs(result.unregisterations) do + for _, unreg in ipairs(params.unregisterations) do if unreg.method == ms.workspace_didChangeWatchedFiles then vim.lsp._watchfiles.unregister(unreg, ctx) end @@ -153,20 +165,20 @@ M[ms.client_unregisterCapability] = function(_, result, ctx) return vim.NIL end +-- TODO(lewis6991): Do we need to notify other servers? --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit -M[ms.workspace_applyEdit] = function(_, workspace_edit, ctx) +RSC[ms.workspace_applyEdit] = function(_, params, ctx) assert( - workspace_edit, + params, 'workspace/applyEdit must be called with `ApplyWorkspaceEditParams`. Server is violating the specification' ) -- TODO(ashkan) Do something more with label? local client_id = ctx.client_id local client = assert(vim.lsp.get_client_by_id(client_id)) - if workspace_edit.label then - print('Workspace edit', workspace_edit.label) + if params.label then + print('Workspace edit', params.label) end - local status, result = - pcall(util.apply_workspace_edit, workspace_edit.edit, client.offset_encoding) + local status, result = pcall(util.apply_workspace_edit, params.edit, client.offset_encoding) return { applied = status, failureReason = result, @@ -182,8 +194,8 @@ local function lookup_section(table, section) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration ---- @param result lsp.ConfigurationParams -M[ms.workspace_configuration] = function(_, result, ctx) +--- @param params lsp.ConfigurationParams +RSC[ms.workspace_configuration] = function(_, params, ctx) local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) if not client then @@ -194,12 +206,12 @@ M[ms.workspace_configuration] = function(_, result, ctx) ) return end - if not result.items then + if not params.items then return {} end local response = {} - for _, item in ipairs(result.items) do + for _, item in ipairs(params.items) do if item.section then local value = lookup_section(client.settings, item.section) -- For empty sections with no explicit '' key, return settings as is @@ -216,7 +228,7 @@ M[ms.workspace_configuration] = function(_, result, ctx) end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders -M[ms.workspace_workspaceFolders] = function(_, _, ctx) +RSC[ms.workspace_workspaceFolders] = function(_, _, ctx) local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) if not client then @@ -226,19 +238,22 @@ M[ms.workspace_workspaceFolders] = function(_, _, ctx) return client.workspace_folders or vim.NIL end -M[ms.textDocument_publishDiagnostics] = function(...) +NSC[ms.textDocument_publishDiagnostics] = function(...) return vim.lsp.diagnostic.on_publish_diagnostics(...) end -M[ms.textDocument_diagnostic] = function(...) +--- @private +RCS[ms.textDocument_diagnostic] = function(...) return vim.lsp.diagnostic.on_diagnostic(...) end -M[ms.textDocument_codeLens] = function(...) +--- @private +RCS[ms.textDocument_codeLens] = function(...) return vim.lsp.codelens.on_codelens(...) end -M[ms.textDocument_inlayHint] = function(...) +--- @private +RCS[ms.textDocument_inlayHint] = function(...) return vim.lsp.inlay_hint.on_inlayhint(...) end @@ -251,6 +266,7 @@ end ---@param title_fn fun(ctx: lsp.HandlerContext): string Function to call to generate list title ---@return lsp.Handler local function response_to_list(map_result, entity, title_fn) + --- @diagnostic disable-next-line:redundant-parameter return function(_, result, ctx, config) if not result or vim.tbl_isempty(result) then vim.notify('No ' .. entity .. ' found') @@ -274,8 +290,9 @@ local function response_to_list(map_result, entity, title_fn) end end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol -M[ms.textDocument_documentSymbol] = response_to_list( +RCS[ms.textDocument_documentSymbol] = response_to_list( util.symbols_to_items, 'document symbols', function(ctx) @@ -284,13 +301,15 @@ M[ms.textDocument_documentSymbol] = response_to_list( end ) +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol -M[ms.workspace_symbol] = response_to_list(util.symbols_to_items, 'symbols', function(ctx) +RCS[ms.workspace_symbol] = response_to_list(util.symbols_to_items, 'symbols', function(ctx) return string.format("Symbols matching '%s'", ctx.params.query) end) +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename -M[ms.textDocument_rename] = function(_, result, ctx, _) +RCS[ms.textDocument_rename] = function(_, result, ctx) if not result then vim.notify("Language server couldn't provide rename result", vim.log.levels.INFO) return @@ -299,8 +318,9 @@ M[ms.textDocument_rename] = function(_, result, ctx, _) util.apply_workspace_edit(result, client.offset_encoding) end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting -M[ms.textDocument_rangeFormatting] = function(_, result, ctx, _) +RCS[ms.textDocument_rangeFormatting] = function(_, result, ctx) if not result then return end @@ -308,8 +328,9 @@ M[ms.textDocument_rangeFormatting] = function(_, result, ctx, _) util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting -M[ms.textDocument_formatting] = function(_, result, ctx, _) +RCS[ms.textDocument_formatting] = function(_, result, ctx) if not result then return end @@ -319,7 +340,7 @@ end --- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion -M[ms.textDocument_completion] = function(_, result, _, _) +RCS[ms.textDocument_completion] = function(_, result, _) if vim.tbl_isempty(result or {}) then return end @@ -355,6 +376,7 @@ end --- - border: (default=nil) --- - Add borders to the floating window --- - See |vim.lsp.util.open_floating_preview()| for more options. +--- @diagnostic disable-next-line:redundant-parameter function M.hover(_, result, ctx, config) config = config or {} config.focus_id = ctx.method @@ -388,10 +410,11 @@ end --- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover --- @diagnostic disable-next-line: deprecated -M[ms.textDocument_hover] = M.hover +RCS[ms.textDocument_hover] = M.hover local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') +--- @deprecated remove in 0.13 --- |lsp-handler| for the method "textDocument/signatureHelp". --- --- The active parameter is highlighted with |hl-LspSignatureActiveParameter|. @@ -412,6 +435,7 @@ local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') --- - border: (default=nil) --- - Add borders to the floating window --- - See |vim.lsp.util.open_floating_preview()| for more options +--- @diagnostic disable-next-line:redundant-parameter function M.signature_help(_, result, ctx, config) config = config or {} config.focus_id = ctx.method @@ -452,11 +476,14 @@ function M.signature_help(_, result, ctx, config) return fbuf, fwin end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp -M[ms.textDocument_signatureHelp] = M.signature_help +--- @diagnostic disable-next-line:deprecated +RCS[ms.textDocument_signatureHelp] = M.signature_help +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight -M[ms.textDocument_documentHighlight] = function(_, result, ctx, _) +RCS[ms.textDocument_documentHighlight] = function(_, result, ctx) if not result then return end @@ -499,11 +526,13 @@ local function make_call_hierarchy_handler(direction) end end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_incomingCalls -M[ms.callHierarchy_incomingCalls] = make_call_hierarchy_handler('from') +RCS[ms.callHierarchy_incomingCalls] = make_call_hierarchy_handler('from') +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_outgoingCalls -M[ms.callHierarchy_outgoingCalls] = make_call_hierarchy_handler('to') +RCS[ms.callHierarchy_outgoingCalls] = make_call_hierarchy_handler('to') --- Displays type hierarchy in the quickfix window. local function make_type_hierarchy_handler() @@ -538,17 +567,19 @@ local function make_type_hierarchy_handler() end end +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#typeHierarchy_incomingCalls -M[ms.typeHierarchy_subtypes] = make_type_hierarchy_handler() +RCS[ms.typeHierarchy_subtypes] = make_type_hierarchy_handler() +--- @deprecated remove in 0.13 --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#typeHierarchy_outgoingCalls -M[ms.typeHierarchy_supertypes] = make_type_hierarchy_handler() +RCS[ms.typeHierarchy_supertypes] = make_type_hierarchy_handler() --- @see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage ---- @param result lsp.LogMessageParams -M[ms.window_logMessage] = function(_, result, ctx, _) - local message_type = result.type - local message = result.message +--- @param params lsp.LogMessageParams +NSC['window/logMessage'] = function(_, params, ctx) + local message_type = params.type + local message = params.message local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) local client_name = client and client.name or string.format('id=%d', client_id) @@ -564,14 +595,14 @@ M[ms.window_logMessage] = function(_, result, ctx, _) else log.debug(message) end - return result + return params end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessage ---- @param result lsp.ShowMessageParams -M[ms.window_showMessage] = function(_, result, ctx, _) - local message_type = result.type - local message = result.message +--- @param params lsp.ShowMessageParams +NSC['window/showMessage'] = function(_, params, ctx) + local message_type = params.type + local message = params.message local client_id = ctx.client_id local client = vim.lsp.get_client_by_id(client_id) local client_name = client and client.name or string.format('id=%d', client_id) @@ -585,15 +616,16 @@ M[ms.window_showMessage] = function(_, result, ctx, _) local message_type_name = protocol.MessageType[message_type] api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message)) end - return result + return params end +--- @private --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showDocument ---- @param result lsp.ShowDocumentParams -M[ms.window_showDocument] = function(_, result, ctx, _) - local uri = result.uri +--- @param params lsp.ShowDocumentParams +RSC[ms.window_showDocument] = function(_, params, ctx) + local uri = params.uri - if result.external then + if params.external then -- TODO(lvimuser): ask the user for confirmation local cmd, err = vim.ui.open(uri) local ret = cmd and cmd:wait(2000) or nil @@ -621,35 +653,39 @@ M[ms.window_showDocument] = function(_, result, ctx, _) local location = { uri = uri, - range = result.selection, + range = params.selection, } local success = util.show_document(location, client.offset_encoding, { reuse_win = true, - focus = result.takeFocus, + focus = params.takeFocus, }) return { success = success or false } end ---@see https://microsoft.github.io/language-server-protocol/specification/#workspace_inlayHint_refresh -M[ms.workspace_inlayHint_refresh] = function(err, result, ctx, config) - return vim.lsp.inlay_hint.on_refresh(err, result, ctx, config) +RSC[ms.workspace_inlayHint_refresh] = function(err, result, ctx) + return vim.lsp.inlay_hint.on_refresh(err, result, ctx) end ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#semanticTokens_refreshRequest -M[ms.workspace_semanticTokens_refresh] = function(err, result, ctx, _config) +RSC[ms.workspace_semanticTokens_refresh] = function(err, result, ctx) return vim.lsp.semantic_tokens._refresh(err, result, ctx) end +--- @nodoc +--- @type table +M = vim.tbl_extend('force', M, RSC, NSC, RCS) + -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do + --- @diagnostic disable-next-line:redundant-parameter M[k] = function(err, result, ctx, config) if log.trace() then log.trace('default_handler', ctx.method, { err = err, result = result, ctx = vim.inspect(ctx), - config = config, }) end @@ -670,6 +706,7 @@ for k, fn in pairs(M) do return end + --- @diagnostic disable-next-line:redundant-parameter return fn(err, result, ctx, config) end end diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua index e5892928cf..ca7bc3b022 100644 --- a/runtime/lua/vim/lsp/inlay_hint.lua +++ b/runtime/lua/vim/lsp/inlay_hint.lua @@ -37,7 +37,7 @@ local augroup = api.nvim_create_augroup('vim_lsp_inlayhint', {}) ---@param result lsp.InlayHint[]? ---@param ctx lsp.HandlerContext ---@private -function M.on_inlayhint(err, result, ctx, _) +function M.on_inlayhint(err, result, ctx) if err then log.error('inlayhint', err) return @@ -87,7 +87,7 @@ end --- |lsp-handler| for the method `workspace/inlayHint/refresh` ---@param ctx lsp.HandlerContext ---@private -function M.on_refresh(err, _, ctx, _) +function M.on_refresh(err, _, ctx) if err then return vim.NIL end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index b299a8438f..98965d0e20 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -618,10 +618,110 @@ function protocol.resolve_capabilities(server_capabilities) return server_capabilities end +-- Generated by gen_lsp.lua, keep at end of file. +--- @alias vim.lsp.protocol.Method.ClientToServer +--- | 'callHierarchy/incomingCalls', +--- | 'callHierarchy/outgoingCalls', +--- | 'codeAction/resolve', +--- | 'codeLens/resolve', +--- | 'completionItem/resolve', +--- | 'documentLink/resolve', +--- | '$/setTrace', +--- | 'exit', +--- | 'initialize', +--- | 'initialized', +--- | 'inlayHint/resolve', +--- | 'notebookDocument/didChange', +--- | 'notebookDocument/didClose', +--- | 'notebookDocument/didOpen', +--- | 'notebookDocument/didSave', +--- | 'shutdown', +--- | 'textDocument/codeAction', +--- | 'textDocument/codeLens', +--- | 'textDocument/colorPresentation', +--- | 'textDocument/completion', +--- | 'textDocument/declaration', +--- | 'textDocument/definition', +--- | 'textDocument/diagnostic', +--- | 'textDocument/didChange', +--- | 'textDocument/didClose', +--- | 'textDocument/didOpen', +--- | 'textDocument/didSave', +--- | 'textDocument/documentColor', +--- | 'textDocument/documentHighlight', +--- | 'textDocument/documentLink', +--- | 'textDocument/documentSymbol', +--- | 'textDocument/foldingRange', +--- | 'textDocument/formatting', +--- | 'textDocument/hover', +--- | 'textDocument/implementation', +--- | 'textDocument/inlayHint', +--- | 'textDocument/inlineCompletion', +--- | 'textDocument/inlineValue', +--- | 'textDocument/linkedEditingRange', +--- | 'textDocument/moniker', +--- | 'textDocument/onTypeFormatting', +--- | 'textDocument/prepareCallHierarchy', +--- | 'textDocument/prepareRename', +--- | 'textDocument/prepareTypeHierarchy', +--- | 'textDocument/rangeFormatting', +--- | 'textDocument/rangesFormatting', +--- | 'textDocument/references', +--- | 'textDocument/rename', +--- | 'textDocument/selectionRange', +--- | 'textDocument/semanticTokens/full', +--- | 'textDocument/semanticTokens/full/delta', +--- | 'textDocument/semanticTokens/range', +--- | 'textDocument/signatureHelp', +--- | 'textDocument/typeDefinition', +--- | 'textDocument/willSave', +--- | 'textDocument/willSaveWaitUntil', +--- | 'typeHierarchy/subtypes', +--- | 'typeHierarchy/supertypes', +--- | 'window/workDoneProgress/cancel', +--- | 'workspaceSymbol/resolve', +--- | 'workspace/diagnostic', +--- | 'workspace/didChangeConfiguration', +--- | 'workspace/didChangeWatchedFiles', +--- | 'workspace/didChangeWorkspaceFolders', +--- | 'workspace/didCreateFiles', +--- | 'workspace/didDeleteFiles', +--- | 'workspace/didRenameFiles', +--- | 'workspace/executeCommand', +--- | 'workspace/symbol', +--- | 'workspace/willCreateFiles', +--- | 'workspace/willDeleteFiles', +--- | 'workspace/willRenameFiles', + +--- @alias vim.lsp.protocol.Method.ServerToClient +--- | 'client/registerCapability', +--- | 'client/unregisterCapability', +--- | '$/logTrace', +--- | 'telemetry/event', +--- | 'textDocument/publishDiagnostics', +--- | 'window/logMessage', +--- | 'window/showDocument', +--- | 'window/showMessage', +--- | 'window/showMessageRequest', +--- | 'window/workDoneProgress/create', +--- | 'workspace/applyEdit', +--- | 'workspace/codeLens/refresh', +--- | 'workspace/configuration', +--- | 'workspace/diagnostic/refresh', +--- | 'workspace/foldingRange/refresh', +--- | 'workspace/inlayHint/refresh', +--- | 'workspace/inlineValue/refresh', +--- | 'workspace/semanticTokens/refresh', +--- | 'workspace/workspaceFolders', + +--- @alias vim.lsp.protocol.Method +--- | vim.lsp.protocol.Method.ClientToServer +--- | vim.lsp.protocol.Method.ServerToClient + -- Generated by gen_lsp.lua, keep at end of file. --- ----@enum vim.lsp.protocol.Methods ----@see https://microsoft.github.io/language-server-protocol/specification/#metaModel +--- @enum vim.lsp.protocol.Methods +--- @see https://microsoft.github.io/language-server-protocol/specification/#metaModel --- LSP method names. protocol.Methods = { --- A request to resolve the incoming calls for a given `CallHierarchyItem`. -- cgit From 6e68fed37441096bf9fd2aa27b9bf6e7d7eae550 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 1 Nov 2024 12:30:36 +0000 Subject: feat(lsp): multi-client support for signature_help Signatures can be cycled using `` when the user enters the floating window. --- runtime/lua/vim/lsp/buf.lua | 116 +++++++++++++++++++++++++++----------- runtime/lua/vim/lsp/util.lua | 131 +++++++++++++++++++++++++------------------ 2 files changed, 159 insertions(+), 88 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 152226a757..6d7597c5ff 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -258,6 +258,33 @@ function M.implementation(opts) get_locations(ms.textDocument_implementation, opts) end +--- @param results table +local function process_signature_help_results(results) + local signatures = {} --- @type [vim.lsp.Client,lsp.SignatureInformation][] + + -- Pre-process results + for client_id, r in pairs(results) do + local err = r.err + local client = assert(lsp.get_client_by_id(client_id)) + if err then + vim.notify( + client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, + vim.log.levels.ERROR + ) + api.nvim_command('redraw') + else + local result = r.result --- @type lsp.SignatureHelp + if result and result.signatures and result.signatures[1] then + for _, sig in ipairs(result.signatures) do + signatures[#signatures + 1] = { client, sig } + end + end + end + end + + return signatures +end + local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') --- @class vim.lsp.buf.signature_help.Opts : vim.lsp.util.open_floating_preview.Opts @@ -270,58 +297,79 @@ local sig_help_ns = api.nvim_create_namespace('vim_lsp_signature_help') function M.signature_help(config) local method = ms.textDocument_signatureHelp - config = config or {} + config = config and vim.deepcopy(config) or {} config.focus_id = method - lsp.buf_request(0, method, client_positional_params(), function(err, result, ctx) - local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) - - if err then - vim.notify( - client.name .. ': ' .. tostring(err.code) .. ': ' .. err.message, - vim.log.levels.ERROR - ) - api.nvim_command('redraw') - return - end - + lsp.buf_request_all(0, method, client_positional_params(), function(results, ctx) if api.nvim_get_current_buf() ~= ctx.bufnr then -- Ignore result since buffer changed. This happens for slow language servers. return end - -- When use `autocmd CompleteDone lua vim.lsp.buf.signature_help()` to call signatureHelp handler - -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `` to ignore - if not result or not result.signatures or not result.signatures[1] then + local signatures = process_signature_help_results(results) + + if not next(signatures) then if config.silent ~= true then print('No signature help available') end return end - local triggers = - vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') - local ft = vim.bo[ctx.bufnr].filetype - local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers) - if not lines or vim.tbl_isempty(lines) then - if config.silent ~= true then - print('No signature help available') + local total = #signatures + local idx = 0 + + --- @param update_win? integer + local function show_signature(update_win) + idx = (idx % total) + 1 + local client, result = signatures[idx][1], signatures[idx][2] + --- @type string[]? + local triggers = + vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') + local lines, hl = + util.convert_signature_help_to_markdown_lines({ signatures = { result } }, ft, triggers) + if not lines then + return end - return + + local sfx = total > 1 and string.format(' (%d/%d) ( to cycle)', idx, total) or '' + local title = string.format('Signature Help: %s%s', client.name, sfx) + if config.border then + config.title = title + else + table.insert(lines, 1, '# ' .. title) + if hl then + hl[1] = hl[1] + 1 + hl[3] = hl[3] + 1 + end + end + + config._update_win = update_win + + local buf, win = util.open_floating_preview(lines, 'markdown', config) + + if hl then + vim.api.nvim_buf_clear_namespace(buf, sig_help_ns, 0, -1) + vim.hl.range( + buf, + sig_help_ns, + 'LspSignatureActiveParameter', + { hl[1], hl[2] }, + { hl[3], hl[4] } + ) + end + return buf, win end - local fbuf = util.open_floating_preview(lines, 'markdown', config) + local fbuf, fwin = show_signature() - -- Highlight the active parameter. - if hl then - vim.hl.range( - fbuf, - sig_help_ns, - 'LspSignatureActiveParameter', - { hl[1], hl[2] }, - { hl[3], hl[4] } - ) + if total > 1 then + vim.keymap.set('n', '', function() + show_signature(fwin) + end, { + buffer = fbuf, + desc = 'Cycle next signature', + }) end end) end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 763cd940c3..4ca4239127 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -737,7 +737,7 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers if active_signature >= #signature_help.signatures or active_signature < 0 then active_signature = 0 end - local signature = signature_help.signatures[active_signature + 1] + local signature = vim.deepcopy(signature_help.signatures[active_signature + 1]) local label = signature.label if ft then -- wrap inside a code block for proper rendering @@ -804,9 +804,11 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers active_offset[2] = active_offset[2] + #contents[1] end - active_hl = {} - list_extend(active_hl, get_pos_from_offset(active_offset[1], contents) or {}) - list_extend(active_hl, get_pos_from_offset(active_offset[2], contents) or {}) + local a_start = get_pos_from_offset(active_offset[1], contents) + local a_end = get_pos_from_offset(active_offset[2], contents) + if a_start and a_end then + active_hl = { a_start[1], a_start[2], a_end[1], a_end[2] } + end end return contents, active_hl @@ -818,7 +820,7 @@ end ---@param width integer window width (in character cells) ---@param height integer window height (in character cells) ---@param opts? vim.lsp.util.open_floating_preview.Opts ----@return table Options +---@return vim.api.keyset.win_config function M.make_floating_popup_options(width, height, opts) validate('opts', opts, 'table', true) opts = opts or {} @@ -1500,6 +1502,8 @@ end --- to display the full window height. --- (default: `'auto'`) --- @field anchor_bias? 'auto'|'above'|'below' +--- +--- @field _update_win? integer --- Shows contents in a floating window. --- @@ -1521,43 +1525,49 @@ function M.open_floating_preview(contents, syntax, opts) local bufnr = api.nvim_get_current_buf() - -- check if this popup is focusable and we need to focus - if opts.focus_id and opts.focusable ~= false and opts.focus then - -- Go back to previous window if we are in a focusable one - local current_winnr = api.nvim_get_current_win() - if vim.w[current_winnr][opts.focus_id] then - api.nvim_command('wincmd p') - return bufnr, current_winnr - end - do - local win = find_window_by_var(opts.focus_id, bufnr) - if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then - -- focus and return the existing buf, win - api.nvim_set_current_win(win) - api.nvim_command('stopinsert') - return api.nvim_win_get_buf(win), win + local floating_winnr = opts._update_win + + -- Create/get the buffer + local floating_bufnr --- @type integer + if floating_winnr then + floating_bufnr = api.nvim_win_get_buf(floating_winnr) + else + -- check if this popup is focusable and we need to focus + if opts.focus_id and opts.focusable ~= false and opts.focus then + -- Go back to previous window if we are in a focusable one + local current_winnr = api.nvim_get_current_win() + if vim.w[current_winnr][opts.focus_id] then + api.nvim_command('wincmd p') + return bufnr, current_winnr + end + do + local win = find_window_by_var(opts.focus_id, bufnr) + if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then + -- focus and return the existing buf, win + api.nvim_set_current_win(win) + api.nvim_command('stopinsert') + return api.nvim_win_get_buf(win), win + end end end - end - -- check if another floating preview already exists for this buffer - -- and close it if needed - local existing_float = vim.b[bufnr].lsp_floating_preview - if existing_float and api.nvim_win_is_valid(existing_float) then - api.nvim_win_close(existing_float, true) + -- check if another floating preview already exists for this buffer + -- and close it if needed + local existing_float = vim.b[bufnr].lsp_floating_preview + if existing_float and api.nvim_win_is_valid(existing_float) then + api.nvim_win_close(existing_float, true) + end + floating_bufnr = api.nvim_create_buf(false, true) end - -- Create the buffer - local floating_bufnr = api.nvim_create_buf(false, true) - -- Set up the contents, using treesitter for markdown local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil + if do_stylize then 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 contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) @@ -1565,19 +1575,47 @@ function M.open_floating_preview(contents, syntax, opts) if syntax then vim.bo[floating_bufnr].syntax = syntax end - api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) end - -- Compute size of float needed to show (wrapped) lines - if opts.wrap then - opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0) + vim.bo[floating_bufnr].modifiable = true + api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents) + + if floating_winnr then + api.nvim_win_set_config(floating_winnr, { + border = opts.border, + title = opts.title, + }) else - opts.wrap_at = nil - end - local width, height = M._make_floating_popup_size(contents, opts) + -- Compute size of float needed to show (wrapped) lines + if opts.wrap then + opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0) + else + opts.wrap_at = nil + end + + -- TODO(lewis6991): These function assume the current window to determine options, + -- therefore it won't work for opts._update_win and the current window if the floating + -- window + local width, height = M._make_floating_popup_size(contents, opts) + local float_option = M.make_floating_popup_options(width, height, opts) - local float_option = M.make_floating_popup_options(width, height, opts) - local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) + floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) + + api.nvim_buf_set_keymap( + floating_bufnr, + 'n', + 'q', + 'bdelete', + { silent = true, noremap = true, nowait = true } + ) + close_preview_autocmd(opts.close_events, floating_winnr, { floating_bufnr, bufnr }) + + -- save focus_id + if opts.focus_id then + api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr) + end + api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr) + end if do_stylize then vim.wo[floating_winnr].conceallevel = 2 @@ -1590,21 +1628,6 @@ 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', - 'q', - 'bdelete', - { silent = true, noremap = true, nowait = true } - ) - close_preview_autocmd(opts.close_events, floating_winnr, { floating_bufnr, bufnr }) - - -- save focus_id - if opts.focus_id then - api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr) - end - api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr) - return floating_bufnr, floating_winnr end -- cgit From 7342e6b00d5e9f67fd5ad4d3fadaf7e501598486 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Fri, 8 Nov 2024 11:46:53 +0000 Subject: perf(lsp): do not apply semantic tokens to folded lines Fixes #31106 --- runtime/lua/vim/lsp/semantic_tokens.lua | 63 ++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 21 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index d680522592..839ec8bea3 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -380,6 +380,20 @@ function STHighlighter:process_response(response, client, version) api.nvim__redraw({ buf = self.bufnr, valid = true }) end +--- @param bufnr integer +--- @param ns integer +--- @param token STTokenRange +--- @param hl_group string +--- @param priority integer +local function set_mark(bufnr, ns, token, hl_group, priority) + vim.api.nvim_buf_set_extmark(bufnr, ns, token.line, token.start_col, { + hl_group = hl_group, + end_col = token.end_col, + priority = priority, + strict = false, + }) +end + --- on_win handler for the decoration provider (see |nvim_set_decoration_provider|) --- --- If there is a current result for the buffer and the version matches the @@ -433,13 +447,14 @@ function STHighlighter:on_win(topline, botline) -- finishes, clangd sends a refresh request which lets the client -- re-synchronize the tokens. - local set_mark = function(token, hl_group, delta) - vim.api.nvim_buf_set_extmark(self.bufnr, state.namespace, token.line, token.start_col, { - hl_group = hl_group, - end_col = token.end_col, - priority = vim.hl.priorities.semantic_tokens + delta, - strict = false, - }) + local function set_mark0(token, hl_group, delta) + set_mark( + self.bufnr, + state.namespace, + token, + hl_group, + vim.hl.priorities.semantic_tokens + delta + ) end local ft = vim.bo[self.bufnr].filetype @@ -447,13 +462,19 @@ function STHighlighter:on_win(topline, botline) local first = lower_bound(highlights, topline, 1, #highlights + 1) local last = upper_bound(highlights, botline, first, #highlights + 1) - 1 - for i = first, last do + local i = first + while i <= last do local token = highlights[i] - if not token.marked then - set_mark(token, string.format('@lsp.type.%s.%s', token.type, ft), 0) - for modifier, _ in pairs(token.modifiers) do - set_mark(token, string.format('@lsp.mod.%s.%s', modifier, ft), 1) - set_mark(token, string.format('@lsp.typemod.%s.%s.%s', token.type, modifier, ft), 2) + local folded = vim.fn.foldclosed(token.line + 1) + + -- Do not highlight folded lines + local can_mark = folded == -1 or folded == token.line + 1 + + if can_mark and not token.marked then + set_mark0(token, string.format('@lsp.type.%s.%s', token.type, ft), 0) + for modifier in pairs(token.modifiers) do + set_mark0(token, string.format('@lsp.mod.%s.%s', modifier, ft), 1) + set_mark0(token, string.format('@lsp.typemod.%s.%s.%s', token.type, modifier, ft), 2) end token.marked = true @@ -466,6 +487,12 @@ function STHighlighter:on_win(topline, botline) }, }) end + + if folded ~= -1 then + i = math.min(last, vim.fn.foldclosedend(token.line + 1) - 1) + else + i = i + 1 + end end end end @@ -745,15 +772,9 @@ function M.highlight_token(token, bufnr, client_id, hl_group, opts) return end - opts = opts or {} - local priority = opts.priority or vim.hl.priorities.semantic_tokens + 3 + local priority = opts and opts.priority or vim.hl.priorities.semantic_tokens + 3 - vim.api.nvim_buf_set_extmark(bufnr, state.namespace, token.line, token.start_col, { - hl_group = hl_group, - end_col = token.end_col, - priority = priority, - strict = false, - }) + set_mark(bufnr, state.namespace, token, hl_group, priority) end --- |lsp-handler| for the method `workspace/semanticTokens/refresh` -- cgit From 4b001f297ae29654b86eccd4dde72b2cc314ca01 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 9 Nov 2024 08:40:27 +0000 Subject: fix(lsp): fix infinite loop Fixes #31129 --- runtime/lua/vim/lsp/semantic_tokens.lua | 35 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 839ec8bea3..f23c694c4b 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -394,6 +394,23 @@ local function set_mark(bufnr, ns, token, hl_group, priority) }) end +--- @param lnum integer +--- @param foldend integer? +--- @return boolean, integer? +local function check_fold(lnum, foldend) + if foldend and lnum <= foldend then + return true, foldend + end + + local folded = vim.fn.foldclosed(lnum) + + if folded == -1 then + return false, nil + end + + return folded ~= lnum, vim.fn.foldclosedend(lnum) +end + --- on_win handler for the decoration provider (see |nvim_set_decoration_provider|) --- --- If there is a current result for the buffer and the version matches the @@ -462,15 +479,15 @@ function STHighlighter:on_win(topline, botline) local first = lower_bound(highlights, topline, 1, #highlights + 1) local last = upper_bound(highlights, botline, first, #highlights + 1) - 1 - local i = first - while i <= last do + --- @type boolean?, integer? + local is_folded, foldend + + for i = first, last do local token = highlights[i] - local folded = vim.fn.foldclosed(token.line + 1) - -- Do not highlight folded lines - local can_mark = folded == -1 or folded == token.line + 1 + is_folded, foldend = check_fold(token.line + 1, foldend) - if can_mark and not token.marked then + if not is_folded and not token.marked then set_mark0(token, string.format('@lsp.type.%s.%s', token.type, ft), 0) for modifier in pairs(token.modifiers) do set_mark0(token, string.format('@lsp.mod.%s.%s', modifier, ft), 1) @@ -487,12 +504,6 @@ function STHighlighter:on_win(topline, botline) }, }) end - - if folded ~= -1 then - i = math.min(last, vim.fn.foldclosedend(token.line + 1) - 1) - else - i = i + 1 - end end end end -- cgit From c5f217db01eb9b07368604f86cbdba0cff8f21a3 Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sat, 9 Nov 2024 13:00:37 -0800 Subject: refactor(lsp): simplify reference range logic --- runtime/lua/vim/lsp/util.lua | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4ca4239127..6eab0f3da4 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1651,21 +1651,12 @@ do --[[ References ]] validate('bufnr', bufnr, 'number', true) validate('offset_encoding', offset_encoding, 'string', false) for _, reference in ipairs(references) do - local start_line = reference.range.start.line - local start_char = reference.range.start.character - local end_line = reference.range['end'].line - local end_char = reference.range['end'].character + local range = reference.range + local start_line = range.start.line + local end_line = range['end'].line - local start_idx = get_line_byte_from_position( - bufnr, - { line = start_line, character = start_char }, - offset_encoding - ) - local end_idx = get_line_byte_from_position( - bufnr, - { line = start_line, character = end_char }, - offset_encoding - ) + local start_idx = get_line_byte_from_position(bufnr, range.start, offset_encoding) + local end_idx = get_line_byte_from_position(bufnr, range['end'], offset_encoding) local document_highlight_kind = { [protocol.DocumentHighlightKind.Text] = 'LspReferenceText', -- cgit From 3e855d533f4477ffddfe94ccea48255979c8b7fb Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 11 Nov 2024 14:23:38 +0000 Subject: perf(lsp): use faster version of str_byteindex --- runtime/lua/vim/lsp/semantic_tokens.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index f23c694c4b..215e5f41aa 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -99,11 +99,12 @@ local function tokens_to_ranges(data, bufnr, client, request) local legend = client.server_capabilities.semanticTokensProvider.legend local token_types = legend.tokenTypes local token_modifiers = legend.tokenModifiers + local encoding = client.offset_encoding local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) local ranges = {} ---@type STTokenRange[] local start = uv.hrtime() - local ms_to_ns = 1000 * 1000 + local ms_to_ns = 1e6 local yield_interval_ns = 5 * ms_to_ns local co, is_main = coroutine.running() @@ -135,14 +136,13 @@ local function tokens_to_ranges(data, bufnr, client, request) -- data[i+3] +1 because Lua tables are 1-indexed local token_type = token_types[data[i + 3] + 1] - local modifiers = modifiers_from_number(data[i + 4], token_modifiers) - - local end_char = start_char + data[i + 2] - local buf_line = lines and lines[line + 1] or '' - local start_col = vim.str_byteindex(buf_line, client.offset_encoding, start_char, false) - local end_col = vim.str_byteindex(buf_line, client.offset_encoding, end_char, false) if token_type then + local modifiers = modifiers_from_number(data[i + 4], token_modifiers) + local end_char = start_char + data[i + 2] + local buf_line = lines and lines[line + 1] or '' + local start_col = vim.str_byteindex(buf_line, encoding, start_char, false) + local end_col = vim.str_byteindex(buf_line, encoding, end_char, false) ranges[#ranges + 1] = { line = line, start_col = start_col, -- cgit From 9aab98a275cb6140540eecbe643a7943d700b58d Mon Sep 17 00:00:00 2001 From: Nicolas Hillegeer Date: Tue, 12 Nov 2024 10:51:42 +0100 Subject: refactor(lsp): avoid redundant function wrapping Leftover from #21026. --- runtime/lua/vim/lsp/rpc.lua | 79 +++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 42 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index ac43801c1c..6c8564845f 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -1,7 +1,7 @@ local uv = vim.uv local log = require('vim.lsp.log') local protocol = require('vim.lsp.protocol') -local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap +local validate, schedule_wrap = vim.validate, vim.schedule_wrap local is_win = vim.fn.has('win32') == 1 @@ -409,49 +409,44 @@ function Client:handle_body(body) local err --- @type lsp.ResponseError|nil -- Schedule here so that the users functions don't trigger an error and -- we can still use the result. - schedule(function() - coroutine.wrap(function() - local status, result - status, result, err = self:try_call( - M.client_errors.SERVER_REQUEST_HANDLER_ERROR, - self.dispatchers.server_request, - decoded.method, - decoded.params - ) - log.debug( - 'server_request: callback result', - { status = status, result = result, err = err } - ) - if status then - if result == nil and err == nil then - error( - string.format( - 'method %q: either a result or an error must be sent to the server in response', - decoded.method - ) - ) - end - if err then - ---@cast err lsp.ResponseError - assert( - type(err) == 'table', - 'err must be a table. Use rpc_response_error to help format errors.' - ) - ---@type string - local code_name = assert( - protocol.ErrorCodes[err.code], - 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.' + vim.schedule(coroutine.wrap(function() + local status, result + status, result, err = self:try_call( + M.client_errors.SERVER_REQUEST_HANDLER_ERROR, + self.dispatchers.server_request, + decoded.method, + decoded.params + ) + log.debug('server_request: callback result', { status = status, result = result, err = err }) + if status then + if result == nil and err == nil then + error( + string.format( + 'method %q: either a result or an error must be sent to the server in response', + decoded.method ) - err.message = err.message or code_name - end - else - -- On an exception, result will contain the error message. - err = M.rpc_response_error(protocol.ErrorCodes.InternalError, result) - result = nil + ) end - self:send_response(decoded.id, err, result) - end)() - end) + if err then + ---@cast err lsp.ResponseError + assert( + type(err) == 'table', + 'err must be a table. Use rpc_response_error to help format errors.' + ) + ---@type string + local code_name = assert( + protocol.ErrorCodes[err.code], + 'Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.' + ) + err.message = err.message or code_name + end + else + -- On an exception, result will contain the error message. + err = M.rpc_response_error(protocol.ErrorCodes.InternalError, result) + result = nil + end + self:send_response(decoded.id, err, result) + end)) -- This works because we are expecting vim.NIL here elseif decoded.id and (decoded.result ~= vim.NIL or decoded.error ~= vim.NIL) then -- We sent a number, so we expect a number. -- cgit From 33d10db5b7a7cd03b22e3ba401cb3bc74640f522 Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Wed, 13 Nov 2024 16:18:29 +0100 Subject: fix(lsp): filter completion candidates based on completeopt (#30945) --- runtime/lua/vim/lsp/completion.lua | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 10086fa49e..92bc110a97 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -220,6 +220,20 @@ local function get_doc(item) return '' end +---@param value string +---@param prefix string +---@return boolean +local function match_item_by_value(value, prefix) + if vim.o.completeopt:find('fuzzy') ~= nil then + return next(vim.fn.matchfuzzy({ value }, prefix)) ~= nil + end + + if vim.o.ignorecase and (not vim.o.smartcase or not prefix:find('%u')) then + return vim.startswith(value:lower(), prefix:lower()) + end + return vim.startswith(value, prefix) +end + --- Turns the result of a `textDocument/completion` request into vim-compatible --- |complete-items|. --- @@ -244,8 +258,16 @@ function M._lsp_to_complete_items(result, prefix, client_id) else ---@param item lsp.CompletionItem matches = function(item) - local text = item.filterText or item.label - return next(vim.fn.matchfuzzy({ text }, prefix)) ~= nil + if item.filterText then + return match_item_by_value(item.filterText, prefix) + end + + if item.textEdit then + -- server took care of filtering + return true + end + + return match_item_by_value(item.label, prefix) end end -- cgit From d3e4ffafff425cf488c2bd076678a63440875cb7 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 14 Nov 2024 14:17:33 +0000 Subject: feat(lsp): support utf-8 and utf-32 position encodings Resolves #30034 --- runtime/lua/vim/lsp/protocol.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 98965d0e20..7db48b0c06 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -338,7 +338,9 @@ function protocol.make_client_capabilities() return { general = { positionEncodings = { + 'utf-8', 'utf-16', + 'utf-32', }, }, textDocument = { -- cgit From 38838fb00ab3d2ebaefc820cebcc5990ea98ea03 Mon Sep 17 00:00:00 2001 From: Maria José Solano Date: Sat, 16 Nov 2024 18:32:09 -0800 Subject: fix(lsp): type-errors, other nits in vim.lsp.log #31235 --- runtime/lua/vim/lsp/log.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 4f177b47fd..ec78dd3dc5 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -32,12 +32,12 @@ local function notify(msg, level) end end -local logfilename = vim.fs.joinpath(vim.fn.stdpath('log'), 'lsp.log') +local logfilename = vim.fs.joinpath(vim.fn.stdpath('log') --[[@as string]], 'lsp.log') -- TODO: Ideally the directory should be created in open_logfile(), right -- before opening the log file, but open_logfile() can be called from libuv -- callbacks, where using fn.mkdir() is not allowed. -vim.fn.mkdir(vim.fn.stdpath('log'), 'p') +vim.fn.mkdir(vim.fn.stdpath('log') --[[@as string]], 'p') --- Returns the log filename. ---@return string log filename @@ -82,6 +82,7 @@ end for level, levelnr in pairs(log_levels) do -- Also export the log level on the root object. + ---@diagnostic disable-next-line: no-unknown log[level] = levelnr -- Add a reverse lookup. @@ -93,7 +94,7 @@ end --- @return fun(...:any): boolean? local function create_logger(level, levelnr) return function(...) - if levelnr < current_log_level then + if not log.should_log(levelnr) then return false end local argc = select('#', ...) @@ -169,7 +170,7 @@ end --- Checks whether the level is sufficient for logging. ---@param level integer log level ----@return bool : true if would log, false if not +---@return boolean : true if would log, false if not function log.should_log(level) return level >= current_log_level end -- cgit From 44229bb85b6cff00193164967126d85a7a785a7b Mon Sep 17 00:00:00 2001 From: Riley Bruins Date: Sun, 17 Nov 2024 12:31:32 -0800 Subject: feat(lsp): highlight hover target/range #31110 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem:** Despite the LSP providing the option for language servers to specify a range with a hover response (for highlighting), Neovim does not give the option to highlight this range. **Solution:** Add an option to `buf.hover()` which causes this range to be highlighted. Co-authored-by: Mathias Fußenegger --- runtime/lua/vim/lsp/buf.lua | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 6d7597c5ff..a75e322e90 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -20,6 +20,8 @@ local function client_positional_params(params) end end +local hover_ns = api.nvim_create_namespace('vim_lsp_hover_range') + --- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts --- @field silent? boolean @@ -30,13 +32,24 @@ end --- In the floating window, all commands and mappings are available as usual, --- except that "q" dismisses the window. --- You can scroll the contents the same as you would any other buffer. +--- +--- Note: to disable hover highlights, add the following to your config: +--- +--- ```lua +--- vim.api.nvim_create_autocmd('ColorScheme', { +--- callback = function() +--- vim.api.nvim_set_hl(0, 'LspReferenceTarget', {}) +--- end, +--- }) +--- ``` --- @param config? vim.lsp.buf.hover.Opts function M.hover(config) config = config or {} config.focus_id = ms.textDocument_hover lsp.buf_request_all(0, ms.textDocument_hover, client_positional_params(), function(results, ctx) - if api.nvim_get_current_buf() ~= ctx.bufnr then + local bufnr = assert(ctx.bufnr) + if api.nvim_get_current_buf() ~= bufnr then -- Ignore result since buffer changed. This happens for slow language servers. return end @@ -67,9 +80,10 @@ function M.hover(config) local format = 'markdown' for client_id, result in pairs(results1) do + local client = assert(lsp.get_client_by_id(client_id)) if nresults > 1 then -- Show client name if there are multiple clients - contents[#contents + 1] = string.format('# %s', lsp.get_client_by_id(client_id).name) + contents[#contents + 1] = string.format('# %s', client.name) end if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then if #results1 == 1 then @@ -87,6 +101,22 @@ function M.hover(config) else vim.list_extend(contents, util.convert_input_to_markdown_lines(result.contents)) end + local range = result.range + if range then + local start = range.start + local end_ = range['end'] + local start_idx = util._get_line_byte_from_position(bufnr, start, client.offset_encoding) + local end_idx = util._get_line_byte_from_position(bufnr, end_, client.offset_encoding) + + vim.hl.range( + bufnr, + hover_ns, + 'LspReferenceTarget', + { start.line, start_idx }, + { end_.line, end_idx }, + { priority = vim.hl.priorities.user } + ) + end contents[#contents + 1] = '---' end @@ -100,7 +130,16 @@ function M.hover(config) return end - lsp.util.open_floating_preview(contents, format, config) + local _, winid = lsp.util.open_floating_preview(contents, format, config) + + api.nvim_create_autocmd('WinClosed', { + pattern = tostring(winid), + once = true, + callback = function() + api.nvim_buf_clear_namespace(bufnr, hover_ns, 0, -1) + return true + end, + }) end) end -- cgit From 40347f6e27f1594797ecb0b268076a94630e4c1b Mon Sep 17 00:00:00 2001 From: luukvbaal Date: Mon, 18 Nov 2024 15:35:21 +0100 Subject: fix(api): only flush nvim__redraw when necessary #31250 Problem: Not possible to only set a "redraw later" type with nvim__redraw, which seems to be desired for the treesitter highlighter. Solution: Do not update the screen when "flush" is explicitly set to false and only redraw later types are present. In that case, do not call ui_flush() either. --- runtime/lua/vim/lsp/inlay_hint.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua index ca7bc3b022..f1ae9a8e9e 100644 --- a/runtime/lua/vim/lsp/inlay_hint.lua +++ b/runtime/lua/vim/lsp/inlay_hint.lua @@ -65,7 +65,7 @@ function M.on_inlayhint(err, result, ctx) if num_unprocessed == 0 then client_hints[client_id] = {} bufstate.version = ctx.version - api.nvim__redraw({ buf = bufnr, valid = true }) + api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) return end @@ -81,7 +81,7 @@ function M.on_inlayhint(err, result, ctx) client_hints[client_id] = new_lnum_hints bufstate.version = ctx.version - api.nvim__redraw({ buf = bufnr, valid = true }) + api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) end --- |lsp-handler| for the method `workspace/inlayHint/refresh` @@ -215,7 +215,7 @@ local function clear(bufnr) end end api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) - api.nvim__redraw({ buf = bufnr, valid = true }) + api.nvim__redraw({ buf = bufnr, valid = true, flush = false }) end --- Disable inlay hints for a buffer -- cgit From 989a37a594649528f28432388c0e7e28e8be2753 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 5 Nov 2024 17:34:21 +0000 Subject: refactor(lsp): fold in dynamic_registration code into the client Problem: Capability register logic is spread across 3 files. Solution: - Consolidate (and simplify) logic into the client. - Teach client.supports_method about resolve methods --- runtime/lua/vim/lsp/_dynamic.lua | 110 --------------------- runtime/lua/vim/lsp/_watchfiles.lua | 10 +- runtime/lua/vim/lsp/buf.lua | 7 +- runtime/lua/vim/lsp/client.lua | 185 +++++++++++++++++++++++++++++++----- runtime/lua/vim/lsp/handlers.lua | 50 ++-------- 5 files changed, 177 insertions(+), 185 deletions(-) delete mode 100644 runtime/lua/vim/lsp/_dynamic.lua (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/_dynamic.lua b/runtime/lua/vim/lsp/_dynamic.lua deleted file mode 100644 index 27113c0e74..0000000000 --- a/runtime/lua/vim/lsp/_dynamic.lua +++ /dev/null @@ -1,110 +0,0 @@ -local glob = vim.glob - ---- @class lsp.DynamicCapabilities ---- @field capabilities table ---- @field client_id number -local M = {} - ---- @param client_id number ---- @return lsp.DynamicCapabilities -function M.new(client_id) - return setmetatable({ - capabilities = {}, - client_id = client_id, - }, { __index = M }) -end - -function M:supports_registration(method) - local client = vim.lsp.get_client_by_id(self.client_id) - if not client then - return false - end - local capability = vim.tbl_get(client.capabilities, unpack(vim.split(method, '/'))) - return type(capability) == 'table' and capability.dynamicRegistration -end - ---- @param registrations lsp.Registration[] -function M:register(registrations) - -- remove duplicates - self:unregister(registrations) - for _, reg in ipairs(registrations) do - local method = reg.method - if not self.capabilities[method] then - self.capabilities[method] = {} - end - table.insert(self.capabilities[method], reg) - end -end - ---- @param unregisterations lsp.Unregistration[] -function M:unregister(unregisterations) - for _, unreg in ipairs(unregisterations) do - local method = unreg.method - if not self.capabilities[method] then - return - end - local id = unreg.id - for i, reg in ipairs(self.capabilities[method]) do - if reg.id == id then - table.remove(self.capabilities[method], i) - break - end - end - end -end - ---- @param method string ---- @param opts? {bufnr: integer?} ---- @return lsp.Registration? (table|nil) the registration if found -function M:get(method, opts) - opts = opts or {} - opts.bufnr = opts.bufnr or vim.api.nvim_get_current_buf() - for _, reg in ipairs(self.capabilities[method] or {}) do - if not reg.registerOptions then - return reg - end - local documentSelector = reg.registerOptions.documentSelector - if not documentSelector then - return reg - end - if self:match(opts.bufnr, documentSelector) then - return reg - end - end -end - ---- @param method string ---- @param opts? {bufnr: integer?} -function M:supports(method, opts) - return self:get(method, opts) ~= nil -end - ---- @param bufnr number ---- @param documentSelector lsp.DocumentSelector ---- @private -function M:match(bufnr, documentSelector) - local client = vim.lsp.get_client_by_id(self.client_id) - if not client then - return false - end - local language = client.get_language_id(bufnr, vim.bo[bufnr].filetype) - local uri = vim.uri_from_bufnr(bufnr) - local fname = vim.uri_to_fname(uri) - for _, filter in ipairs(documentSelector) do - local matches = true - if filter.language and language ~= filter.language then - matches = false - end - if matches and filter.scheme and not vim.startswith(uri, filter.scheme .. ':') then - matches = false - end - if matches and filter.pattern and not glob.to_lpeg(filter.pattern):match(fname) then - matches = false - end - if matches then - return true - end - end -end - -return M diff --git a/runtime/lua/vim/lsp/_watchfiles.lua b/runtime/lua/vim/lsp/_watchfiles.lua index 98e9818bcd..c4cdb5aea8 100644 --- a/runtime/lua/vim/lsp/_watchfiles.lua +++ b/runtime/lua/vim/lsp/_watchfiles.lua @@ -44,9 +44,8 @@ M._poll_exclude_pattern = glob.to_lpeg('**/.git/{objects,subtree-cache}/**') --- Registers the workspace/didChangeWatchedFiles capability dynamically. --- ---@param reg lsp.Registration LSP Registration object. ----@param ctx lsp.HandlerContext Context from the |lsp-handler|. -function M.register(reg, ctx) - local client_id = ctx.client_id +---@param client_id integer Client ID. +function M.register(reg, client_id) local client = assert(vim.lsp.get_client_by_id(client_id), 'Client must be running') -- Ill-behaved servers may not honor the client capability and try to register -- anyway, so ignore requests when the user has opted out of the feature. @@ -155,9 +154,8 @@ end --- Unregisters the workspace/didChangeWatchedFiles capability dynamically. --- ---@param unreg lsp.Unregistration LSP Unregistration object. ----@param ctx lsp.HandlerContext Context from the |lsp-handler|. -function M.unregister(unreg, ctx) - local client_id = ctx.client_id +---@param client_id integer Client ID. +function M.unregister(unreg, client_id) local client_cancels = cancels[client_id] local reg_cancels = client_cancels[unreg.id] while #reg_cancels > 0 do diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index a75e322e90..6383855a30 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1131,12 +1131,7 @@ local function on_code_action_results(results, opts) local action = choice.action local bufnr = assert(choice.ctx.bufnr, 'Must have buffer number') - local reg = client.dynamic_capabilities:get(ms.textDocument_codeAction, { bufnr = bufnr }) - - local supports_resolve = vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') - or client.supports_method(ms.codeAction_resolve) - - if not action.edit and client and supports_resolve then + if not action.edit and client.supports_method(ms.codeAction_resolve) then client.request(ms.codeAction_resolve, action, function(err, resolved_action) if err then if action.command then diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 2718f40c96..ba12447c40 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -91,8 +91,8 @@ local validate = vim.validate --- (default: client-id) --- @field name? string --- ---- Language ID as string. Defaults to the filetype. ---- @field get_language_id? fun(bufnr: integer, filetype: string): string +--- Language ID as string. Defaults to the buffer filetype. +--- @field get_language_id? fun(bufnr: integer, filetype?: string): string --- --- The encoding that the LSP server expects. Client does not verify this is correct. --- @field offset_encoding? 'utf-8'|'utf-16'|'utf-32' @@ -212,10 +212,11 @@ local validate = vim.validate --- A table with flags for the client. The current (experimental) flags are: --- @field flags vim.lsp.Client.Flags --- ---- @field get_language_id fun(bufnr: integer, filetype: string): string +--- @field get_language_id fun(bufnr: integer, filetype?: string): string --- --- The capabilities provided by the client (editor or tool) --- @field capabilities lsp.ClientCapabilities +--- @field private registrations table --- @field dynamic_capabilities lsp.DynamicCapabilities --- --- Sends a request to the server. @@ -339,10 +340,10 @@ end --- By default, get_language_id just returns the exact filetype it is passed. --- It is possible to pass in something that will calculate a different filetype, --- to be sent by the client. ---- @param _bufnr integer ---- @param filetype string -local function default_get_language_id(_bufnr, filetype) - return filetype +--- @param bufnr integer +--- @param filetype? string +local function default_get_language_id(bufnr, filetype) + return filetype or vim.bo[bufnr].filetype end --- Validates a client configuration as given to |vim.lsp.start_client()|. @@ -403,18 +404,16 @@ local function get_name(id, config) return tostring(id) end ---- @param workspace_folders lsp.WorkspaceFolder[]? ---- @param root_dir string? +--- @param workspace_folders string|lsp.WorkspaceFolder[]? --- @return lsp.WorkspaceFolder[]? -local function get_workspace_folders(workspace_folders, root_dir) - if workspace_folders then +local function get_workspace_folders(workspace_folders) + if type(workspace_folders) == 'table' then return workspace_folders - end - if root_dir then + elseif type(workspace_folders) == 'string' then return { { - uri = vim.uri_from_fname(root_dir), - name = root_dir, + uri = vim.uri_from_fname(workspace_folders), + name = workspace_folders, }, } end @@ -451,13 +450,13 @@ function Client.create(config) requests = {}, attached_buffers = {}, server_capabilities = {}, - dynamic_capabilities = lsp._dynamic.new(id), + registrations = {}, commands = config.commands or {}, settings = config.settings or {}, flags = config.flags or {}, get_language_id = config.get_language_id or default_get_language_id, capabilities = config.capabilities or lsp.protocol.make_client_capabilities(), - workspace_folders = get_workspace_folders(config.workspace_folders, config.root_dir), + workspace_folders = get_workspace_folders(config.workspace_folders or config.root_dir), root_dir = config.root_dir, _before_init_cb = config.before_init, _on_init_cbs = ensure_list(config.on_init), @@ -478,6 +477,28 @@ function Client.create(config) messages = { name = name, messages = {}, progress = {}, status = {} }, } + --- @class lsp.DynamicCapabilities + --- @nodoc + self.dynamic_capabilities = { + capabilities = self.registrations, + client_id = id, + register = function(_, registrations) + return self:_register_dynamic(registrations) + end, + unregister = function(_, unregistrations) + return self:_unregister_dynamic(unregistrations) + end, + get = function(_, method, opts) + return self:_get_registration(method, opts and opts.bufnr) + end, + supports_registration = function(_, method) + return self:_supports_registration(method) + end, + supports = function(_, method, opts) + return self:_get_registration(method, opts and opts.bufnr) ~= nil + end, + } + self.request = method_wrapper(self, Client._request) self.request_sync = method_wrapper(self, Client._request_sync) self.notify = method_wrapper(self, Client._notify) @@ -846,6 +867,100 @@ function Client:_stop(force) end) end +--- Get options for a method that is registered dynamically. +--- @param method string +function Client:_supports_registration(method) + local capability = vim.tbl_get(self.capabilities, unpack(vim.split(method, '/'))) + return type(capability) == 'table' and capability.dynamicRegistration +end + +--- @private +--- @param registrations lsp.Registration[] +function Client:_register_dynamic(registrations) + -- remove duplicates + self:_unregister_dynamic(registrations) + for _, reg in ipairs(registrations) do + local method = reg.method + if not self.registrations[method] then + self.registrations[method] = {} + end + table.insert(self.registrations[method], reg) + end +end + +--- @param registrations lsp.Registration[] +function Client:_register(registrations) + self:_register_dynamic(registrations) + + local unsupported = {} --- @type string[] + + for _, reg in ipairs(registrations) do + local method = reg.method + if method == ms.workspace_didChangeWatchedFiles then + vim.lsp._watchfiles.register(reg, self.id) + elseif not self:_supports_registration(method) then + unsupported[#unsupported + 1] = method + end + end + + if #unsupported > 0 then + local warning_tpl = 'The language server %s triggers a registerCapability ' + .. 'handler for %s despite dynamicRegistration set to false. ' + .. 'Report upstream, this warning is harmless' + log.warn(string.format(warning_tpl, self.name, table.concat(unsupported, ', '))) + end +end + +--- @private +--- @param unregistrations lsp.Unregistration[] +function Client:_unregister_dynamic(unregistrations) + for _, unreg in ipairs(unregistrations) do + local sreg = self.registrations[unreg.method] + -- Unegister dynamic capability + for i, reg in ipairs(sreg or {}) do + if reg.id == unreg.id then + table.remove(sreg, i) + break + end + end + end +end + +--- @param unregistrations lsp.Unregistration[] +function Client:_unregister(unregistrations) + self:_unregister_dynamic(unregistrations) + for _, unreg in ipairs(unregistrations) do + if unreg.method == ms.workspace_didChangeWatchedFiles then + vim.lsp._watchfiles.unregister(unreg, self.id) + end + end +end + +--- @param method string +--- @param bufnr? integer +--- @return lsp.Registration? +function Client:_get_registration(method, bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + for _, reg in ipairs(self.registrations[method] or {}) do + if not reg.registerOptions or not reg.registerOptions.documentSelector then + return reg + end + local documentSelector = reg.registerOptions.documentSelector + local language = self.get_language_id(bufnr) + local uri = vim.uri_from_bufnr(bufnr) + local fname = vim.uri_to_fname(uri) + for _, filter in ipairs(documentSelector) do + if + not (filter.language and language ~= filter.language) + and not (filter.scheme and not vim.startswith(uri, filter.scheme .. ':')) + and not (filter.pattern and not vim.glob.to_lpeg(filter.pattern):match(fname)) + then + return reg + end + end + end +end + --- @private --- Checks whether a client is stopped. --- @@ -908,12 +1023,11 @@ function Client:_text_document_did_open_handler(bufnr) return end - local filetype = vim.bo[bufnr].filetype self.notify(ms.textDocument_didOpen, { textDocument = { version = lsp.util.buf_versions[bufnr], uri = vim.uri_from_bufnr(bufnr), - languageId = self.get_language_id(bufnr, filetype), + languageId = self.get_language_id(bufnr), text = lsp._buf_get_full_text(bufnr), }, }) @@ -978,12 +1092,37 @@ function Client:_supports_method(method, opts) if vim.tbl_get(self.server_capabilities, unpack(required_capability)) then return true end - if self.dynamic_capabilities:supports_registration(method) then - return self.dynamic_capabilities:supports(method, opts) + + local rmethod = lsp._resolve_to_request[method] + if rmethod then + if self:_supports_registration(rmethod) then + local reg = self:_get_registration(rmethod, opts and opts.bufnr) + return vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') or false + end + else + if self:_supports_registration(method) then + return self:_get_registration(method, opts and opts.bufnr) ~= nil + end end return false end +--- Get options for a method that is registered dynamically. +--- @param method string +--- @param bufnr? integer +--- @return lsp.LSPAny? +function Client:_get_registration_options(method, bufnr) + if not self:_supports_registration(method) then + return + end + + local reg = self:_get_registration(method, bufnr) + + if reg then + return reg.registerOptions + end +end + --- @private --- Handles a notification sent by an LSP server by invoking the --- corresponding handler. @@ -1061,7 +1200,7 @@ function Client:_add_workspace_folder(dir) end end - local wf = assert(get_workspace_folders(nil, dir)) + local wf = assert(get_workspace_folders(dir)) self:_notify(ms.workspace_didChangeWorkspaceFolders, { event = { added = wf, removed = {} }, @@ -1076,7 +1215,7 @@ end --- Remove a directory to the workspace folders. --- @param dir string? function Client:_remove_workspace_folder(dir) - local wf = assert(get_workspace_folders(nil, dir)) + local wf = assert(get_workspace_folders(dir)) self:_notify(ms.workspace_didChangeWorkspaceFolders, { event = { added = {}, removed = wf }, diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 2b7aefe0e1..5c28d88b38 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -122,46 +122,19 @@ end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability --- @param params lsp.RegistrationParams RSC[ms.client_registerCapability] = function(_, params, ctx) - local client_id = ctx.client_id - local client = assert(vim.lsp.get_client_by_id(client_id)) - - client.dynamic_capabilities:register(params.registrations) - for bufnr, _ in pairs(client.attached_buffers) do + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + client:_register(params.registrations) + for bufnr in pairs(client.attached_buffers) do vim.lsp._set_defaults(client, bufnr) end - - ---@type string[] - local unsupported = {} - for _, reg in ipairs(params.registrations) do - if reg.method == ms.workspace_didChangeWatchedFiles then - vim.lsp._watchfiles.register(reg, ctx) - elseif not client.dynamic_capabilities:supports_registration(reg.method) then - unsupported[#unsupported + 1] = reg.method - end - end - if #unsupported > 0 then - local warning_tpl = 'The language server %s triggers a registerCapability ' - .. 'handler for %s despite dynamicRegistration set to false. ' - .. 'Report upstream, this warning is harmless' - local client_name = client and client.name or string.format('id=%d', client_id) - local warning = string.format(warning_tpl, client_name, table.concat(unsupported, ', ')) - log.warn(warning) - end return vim.NIL end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability --- @param params lsp.UnregistrationParams RSC[ms.client_unregisterCapability] = function(_, params, ctx) - local client_id = ctx.client_id - local client = assert(vim.lsp.get_client_by_id(client_id)) - client.dynamic_capabilities:unregister(params.unregisterations) - - for _, unreg in ipairs(params.unregisterations) do - if unreg.method == ms.workspace_didChangeWatchedFiles then - vim.lsp._watchfiles.unregister(unreg, ctx) - end - end + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) + client:_unregister(params.unregisterations) return vim.NIL end @@ -173,8 +146,7 @@ RSC[ms.workspace_applyEdit] = function(_, params, ctx) 'workspace/applyEdit must be called with `ApplyWorkspaceEditParams`. Server is violating the specification' ) -- TODO(ashkan) Do something more with label? - local client_id = ctx.client_id - local client = assert(vim.lsp.get_client_by_id(client_id)) + local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) if params.label then print('Workspace edit', params.label) end @@ -196,12 +168,11 @@ end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration --- @param params lsp.ConfigurationParams RSC[ms.workspace_configuration] = function(_, params, ctx) - local client_id = ctx.client_id - local client = vim.lsp.get_client_by_id(client_id) + local client = vim.lsp.get_client_by_id(ctx.client_id) if not client then err_message( 'LSP[', - client_id, + ctx.client_id, '] client has shut down after sending a workspace/configuration request' ) return @@ -229,10 +200,9 @@ end --- @see # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders RSC[ms.workspace_workspaceFolders] = function(_, _, ctx) - local client_id = ctx.client_id - local client = vim.lsp.get_client_by_id(client_id) + local client = vim.lsp.get_client_by_id(ctx.client_id) if not client then - err_message('LSP[id=', client_id, '] client has shut down after sending the message') + err_message('LSP[id=', ctx.client_id, '] client has shut down after sending the message') return end return client.workspace_folders or vim.NIL -- cgit From 081beb3659bd6d8efc3e977a160b1e72becbd8a2 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 19 Nov 2024 10:11:40 +0000 Subject: fix(lsp): restore get_language_id behaviour Ensure filetype is always passed. Fixes #31262 --- runtime/lua/vim/lsp/client.lua | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) (limited to 'runtime/lua/vim/lsp') diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index ba12447c40..11ecb87507 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -92,7 +92,7 @@ local validate = vim.validate --- @field name? string --- --- Language ID as string. Defaults to the buffer filetype. ---- @field get_language_id? fun(bufnr: integer, filetype?: string): string +--- @field get_language_id? fun(bufnr: integer, filetype: string): string --- --- The encoding that the LSP server expects. Client does not verify this is correct. --- @field offset_encoding? 'utf-8'|'utf-16'|'utf-32' @@ -212,7 +212,7 @@ local validate = vim.validate --- A table with flags for the client. The current (experimental) flags are: --- @field flags vim.lsp.Client.Flags --- ---- @field get_language_id fun(bufnr: integer, filetype?: string): string +--- @field get_language_id fun(bufnr: integer, filetype: string): string --- --- The capabilities provided by the client (editor or tool) --- @field capabilities lsp.ClientCapabilities @@ -340,10 +340,10 @@ end --- By default, get_language_id just returns the exact filetype it is passed. --- It is possible to pass in something that will calculate a different filetype, --- to be sent by the client. ---- @param bufnr integer ---- @param filetype? string -local function default_get_language_id(bufnr, filetype) - return filetype or vim.bo[bufnr].filetype +--- @param _bufnr integer +--- @param filetype string +local function default_get_language_id(_bufnr, filetype) + return filetype end --- Validates a client configuration as given to |vim.lsp.start_client()|. @@ -936,6 +936,11 @@ function Client:_unregister(unregistrations) end end +--- @private +function Client:_get_language_id(bufnr) + return self.get_language_id(bufnr, vim.bo[bufnr].filetype) +end + --- @param method string --- @param bufnr? integer --- @return lsp.Registration? @@ -946,7 +951,7 @@ function Client:_get_registration(method, bufnr) return reg end local documentSelector = reg.registerOptions.documentSelector - local language = self.get_language_id(bufnr) + local language = self:_get_language_id(bufnr) local uri = vim.uri_from_bufnr(bufnr) local fname = vim.uri_to_fname(uri) for _, filter in ipairs(documentSelector) do @@ -1027,7 +1032,7 @@ function Client:_text_document_did_open_handler(bufnr) textDocument = { version = lsp.util.buf_versions[bufnr], uri = vim.uri_from_bufnr(bufnr), - languageId = self.get_language_id(bufnr), + languageId = self:_get_language_id(bufnr), text = lsp._buf_get_full_text(bufnr), }, }) -- cgit