From 34cbfeca9c28b060fb4869dfd9000cc5fa653aaa Mon Sep 17 00:00:00 2001 From: glepnir Date: Sun, 19 Apr 2026 03:43:20 +0800 Subject: [PATCH] fix(lsp): show CompletionItem.detail in info popup #38904 Problem: completionItem/resolve response's `detail` field is silently dropped. Only `documentation` is shown in the popup. Solution: Prepend `detail` as a fenced code block before `documentation` in the info popup, skipping if documentation already contains it. (cherry picked from commit b351afb1b1c564c5a4e6856764d4db21a873242c) --- runtime/lua/vim/lsp/completion.lua | 14 ++++- .../functional/plugin/lsp/completion_spec.lua | 55 +++++++++++++------ 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 51f23915d1..b9c5b52a00 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -747,9 +747,21 @@ function CompletionResolver:request(bufnr, param, selected_word) return end - local value = vim.tbl_get(result, 'documentation', 'value') + 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 + end + if not value then if text_format ~= protocol.InsertTextFormat.Snippet then return diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index 9315f723fb..1f6b0ba1d9 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -1401,7 +1401,8 @@ describe('vim.lsp.completion: integration', function() local dummy_client_id = create_server('dummy', completion_list, { resolve_result = { { - detail = 'function', + -- detail not in documentation, should be prepended as code block + detail = '(method) nvim__id_array(arr: any[]): any[]', documentation = { kind = 'markdown', value = [[```lua\nfunction vim.api.nvim__id_array(arr: any[])\n -> any[]\n```]], @@ -1420,7 +1421,8 @@ describe('vim.lsp.completion: integration', function() sortText = '0003', }, { - detail = 'function', + -- detail is in documentation, should not be duplicated + detail = '_assert_integer', documentation = { kind = 'markdown', value = [[```lua\nmore doc for vim._assert_integer\n```]], @@ -1436,24 +1438,30 @@ describe('vim.lsp.completion: integration', function() feed('S') retry(nil, nil, function() - eq( - { true, true, [[```lua\nfunction vim.api.nvim__id_array(arr: any[])\n -> any[]\n```]] }, - exec_lua(function() - local data = vim.fn.complete_info({ 'selected' }) - return { - vim.api.nvim_win_is_valid(data.preview_winid), - vim.api.nvim_buf_is_valid(data.preview_bufnr), - vim.api.nvim_buf_get_lines(data.preview_bufnr, 0, -1, false)[1], - } - end) - ) + local info = exec_lua(function() + local data = vim.fn.complete_info({ 'selected' }) + if + not data.preview_winid + or not vim.api.nvim_win_is_valid(data.preview_winid) + or not data.preview_bufnr + or not vim.api.nvim_buf_is_valid(data.preview_bufnr) + then + error('preview not ready') + end + return table.concat(vim.api.nvim_buf_get_lines(data.preview_bufnr, 0, -1, false), '\n') + end) + -- item 1: detail is not in documentation, should be prepended + neq(nil, info:find('(method) nvim__id_array(arr: any[]): any[]', 1, true)) + neq(nil, info:find('function vim.api.nvim__id_array', 1, true)) end) screen:expect([[ nvim__id_array^ | - {12:nvim__id_array Function }{100:lua\nfunction vim.api}{4: }{1: }| - {4:for i = .. Snippet }{100:.nvim__id_array(arr: any}{1: }| - {4:_assert_integer Function }{100:[])\n -> any[]\n}{4: }{1: }| - {1:~ }|*15 + {12:nvim__id_array Function }{100:(method) nvim__id_array(}{1: }| + {4:for i = .. Snippet }{100:arr: any[]): any[]}{4: }{1: }| + {4:_assert_integer Function }{100:lua\nfunction vim.api}{4: }{1: }| + {1:~ }{100:.nvim__id_array(arr: any}{1: }| + {1:~ }{100:[])\n -> any[]\n}{4: }{1: }| + {1:~ }|*13 {5:-- INSERT --} | ]]) feed('') @@ -1466,6 +1474,19 @@ describe('vim.lsp.completion: integration', function() {5:-- INSERT --} | ]]) feed('') + retry(nil, nil, function() + local info = exec_lua(function() + local data = vim.fn.complete_info({ 'selected' }) + if not data.preview_bufnr or not vim.api.nvim_buf_is_valid(data.preview_bufnr) then + error('preview not ready') + end + return table.concat(vim.api.nvim_buf_get_lines(data.preview_bufnr, 0, -1, false), '\n') + end) + neq(nil, info:find('more doc for vim._assert_integer', 1, true)) + local _, count = info:gsub('_assert_integer', '') + -- item 3: detail '_assert_integer' is in documentation, should not be duplicated + eq(1, count) + end) screen:expect([[ _assert_integer(x, base)^ | {4:nvim__id_array Function }{100:lua\nmore doc for vim}{4: }{1: }|