diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 44de65ca1b..bd0e2528b2 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -264,22 +264,25 @@ local function get_items(result) end end +---Returns an item's documentation value and its markup kind. ---@param item lsp.CompletionItem ---@return string +---@return lsp.MarkupKind local function get_doc(item) local doc = item.documentation + local default_kind = vim.lsp.protocol.MarkupKind.Markdown if not doc then - return '' + return '', default_kind end if type(doc) == 'string' then - return doc + return doc, default_kind end if type(doc) == 'table' and type(doc.value) == 'string' then - return doc.value + return doc.value, doc.kind end vim.notify('invalid documentation value: ' .. vim.inspect(doc), vim.log.levels.WARN) - return '' + return '', default_kind end ---@param value string @@ -352,6 +355,43 @@ local function generate_kind(item) return '■', group end +---Returns the [complete-items] info for an LSP completion item, its markup kind, and whether the +---info is complete. +---The info is complete when all of the fields of the item required to build it are present. If the +---info is not complete, resolving the item (via completionItem/resolve) may populate the missing +---fields. +---@param item lsp.CompletionItem +---@return string +---@return lsp.MarkupKind +---@return boolean complete +local function complete_item_info(item) + local info, kind = get_doc(item) + + if item.detail and item.detail ~= '' then + local detail_block = ('```%s\n%s\n```'):format(vim.bo.filetype, item.detail) + if info == '' then + info = detail_block + elseif not info:find(item.detail, 1, true) then + info = detail_block .. '\n' .. info + end + end + + if + info == '' + and has_completeopt('popup') + and item.insertTextFormat == protocol.InsertTextFormat.Snippet + then + local text = item.insertText or (item.textEdit and item.textEdit.newText) + if text then + local snippet = parse_snippet(text) + info = ('```%s\n%s\n```'):format(vim.bo.filetype, snippet) + end + end + + local complete = item.detail ~= nil and item.documentation ~= nil + return info, kind, complete +end + --- Turns the result of a `textDocument/completion` request into vim-compatible --- |complete-items|. --- @@ -404,6 +444,8 @@ function M._lsp_to_complete_items( local bufnr = api.nvim_get_current_buf() local user_convert = vim.tbl_get(buf_handles, bufnr, 'convert') local user_cmp = vim.tbl_get(buf_handles, bufnr, 'cmp') + local client = client_id and vim.lsp.get_client_by_id(client_id) + local server_supports_resolve = client and client:supports_method('completionItem/resolve') for _, item in ipairs(items) do local match, score = matches(item) if match then @@ -435,15 +477,7 @@ function M._lsp_to_complete_items( hl_group = 'DiagnosticDeprecated' end local kind, kind_hlgroup = generate_kind(item) - local info = get_doc(item) - if item.detail and item.detail ~= '' then - if info == '' then - info = ('```%s\n%s\n```'):format(vim.bo.filetype, item.detail) - elseif not info:find(item.detail, 1, true) then - local detail_block = ('```%s\n%s\n```'):format(vim.bo.filetype, item.detail) - info = detail_block .. '\n' .. info - end - end + local info, info_kind, info_complete = complete_item_info(item) local completion_item = { word = word, abbr = ('%s%s'):format(item.label, vim.tbl_get(item, 'labelDetails', 'detail') or ''), @@ -460,6 +494,8 @@ function M._lsp_to_complete_items( nvim = { lsp = { completion_item = item, + info_kind = info_kind, + completion_item_needs_resolving = server_supports_resolve and not info_complete, client_id = client_id, }, }, @@ -745,6 +781,7 @@ function CompletionResolver:request(bufnr, param, selected_word) local start_time = vim.uv.hrtime() self.last_request_time = start_time + ---@param result lsp.CompletionItem client:request('completionItem/resolve', param, function(err, result) local end_time = vim.uv.hrtime() local response_time = (end_time - start_time) * ns_to_ms @@ -762,91 +799,35 @@ function CompletionResolver:request(bufnr, param, selected_word) return end - local value = vim.tbl_get(result, 'documentation', 'value') --[[@as string?]] - local kind = vim.tbl_get(result, 'documentation', 'kind') - local text_format = vim.tbl_get(result, 'insertTextFormat') - - if result.detail and result.detail ~= '' then - if not value then - value = ('```%s\n%s\n```'):format(vim.bo.filetype, result.detail) - kind = kind or lsp.protocol.MarkupKind.Markdown - elseif not value:find(result.detail, 1, true) then - local detail_block = ('```%s\n%s\n```'):format(vim.bo.filetype, result.detail) - value = detail_block .. '\n' .. value - kind = kind or lsp.protocol.MarkupKind.Markdown - end + local info, kind = complete_item_info(result) + if info ~= '' and info ~= cmp_info.completed.info then + local windata = vim.api.nvim__complete_set(cmp_info.selected, { info = info }) + update_popup_window(windata.winid, windata.bufnr, kind) end - - if not value then - if text_format ~= protocol.InsertTextFormat.Snippet then - return - end - -- generate snippet preview info - local text = vim.tbl_get(result, 'insertText') or vim.tbl_get(result, 'textEdit', 'newText') - if text then - value = ('```%s\n%s\n```'):format(vim.bo.filetype, parse_snippet(text)) - kind = lsp.protocol.MarkupKind.Markdown - end - end - local windata = vim.api.nvim__complete_set(cmp_info.selected, { - info = value, - }) - update_popup_window(windata.winid, windata.bufnr, kind) end, bufnr) end, debounce_time) end ---- Defines a CompleteChanged handler to request and display LSP completion item documentation ---- via completionItem/resolve +--- Defines a CompleteChanged handler to highlight the completion info popup and request/display LSP +--- completion item documentation via completionItem/resolve local function on_completechanged(group, bufnr) nvim_on('CompleteChanged', group, { buf = bufnr, - desc = 'Request and display LSP completion item documentation via completionItem/resolve', + desc = 'Highlight completion info popup and request/display LSP completion item documentation via completionItem/resolve', }, function(ev) local completed_item = vim.v.event.completed_item or {} - local lsp_item = vim.tbl_get(completed_item, 'user_data', 'nvim', 'lsp', 'completion_item') --[[@as lsp.CompletionItem?]] - if not lsp_item then + local user_data = vim.tbl_get(completed_item, 'user_data', 'nvim', 'lsp') + if not user_data then return end - local data = vim.fn.complete_info({ 'selected' }) - local info = completed_item.info or '' - if info ~= '' then - local kind = vim.tbl_get(lsp_item, 'documentation', 'kind') - update_popup_window( - data.preview_winid, - data.preview_bufnr, - kind or protocol.MarkupKind.Markdown - ) + if (completed_item.info or '') ~= '' then + local data = vim.fn.complete_info({ 'selected' }) + update_popup_window(data.preview_winid, data.preview_bufnr, user_data.info_kind) end - if - #lsp.get_clients({ - id = vim.tbl_get(completed_item, 'user_data', 'nvim', 'lsp', 'client_id'), - method = 'completionItem/resolve', - bufnr = ev.buf, - }) == 0 - then - if - has_completeopt('popup') - and info == '' - and lsp_item.insertTextFormat == protocol.InsertTextFormat.Snippet - then - -- Shows snippet preview in doc popup if completeopt=popup. - local text = parse_snippet(lsp_item.insertText or lsp_item.textEdit.newText) - local windata = api.nvim__complete_set( - data.selected, - { info = ('```%s\n%s\n```'):format(vim.bo.filetype, text) } - ) - update_popup_window(windata.winid, windata.bufnr, protocol.MarkupKind.Markdown) - end - return - end - - -- Retrieve the raw LSP completionItem from completed_item as the parameter for - -- the completionItem/resolve request - if not lsp_item.detail or not lsp_item.documentation then + if user_data.completion_item_needs_resolving then Context.resolve_handler = Context.resolve_handler or CompletionResolver.new() - Context.resolve_handler:request(ev.buf, lsp_item, completed_item.word) + Context.resolve_handler:request(ev.buf, user_data.completion_item, completed_item.word) end end) end diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index ac556d826b..ac1d23de0a 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -927,7 +927,12 @@ describe('vim.lsp.completion: protocol', function() abbr_hlgroup = '', user_data = { nvim = { - lsp = { client_id = 1, completion_item = { label = 'hello' } }, + lsp = { + client_id = 1, + completion_item = { label = 'hello' }, + info_kind = 'markdown', + completion_item_needs_resolving = false, + }, }, }, word = 'hello', @@ -946,6 +951,8 @@ describe('vim.lsp.completion: protocol', function() lsp = { client_id = 1, completion_item = { label = 'hercules', tags = { 1 } }, + info_kind = 'markdown', + completion_item_needs_resolving = false, }, }, }, @@ -965,6 +972,8 @@ describe('vim.lsp.completion: protocol', function() lsp = { client_id = 1, completion_item = { label = 'hero', deprecated = true }, + info_kind = 'markdown', + completion_item_needs_resolving = false, }, }, },