fix(lsp): snippet preview blocked completionItem/resolve request #38428

Problem: Generating snippet preview in get_doc() populated the
documentation field before resolve, so the resolve request was
never sent.

Solution: Move snippet preview logic into on_completechanged and
the resolve callback so it no longer blocks the resolve request.
This commit is contained in:
glepnir
2026-03-23 19:57:36 +08:00
committed by GitHub
parent d6a6eed4f3
commit 4ed597389c
2 changed files with 106 additions and 85 deletions

View File

@@ -258,21 +258,6 @@ end
---@param item lsp.CompletionItem
---@return string
local function get_doc(item)
if
has_completeopt('popup')
and item.insertTextFormat == protocol.InsertTextFormat.Snippet
and (type(item.documentation) ~= 'string' or #item.documentation == 0)
and vim.bo.filetype ~= ''
and (item.textEdit or (item.insertText and item.insertText ~= ''))
then
-- Shows snippet preview in doc popup if completeopt=popup.
local text = parse_snippet(item.insertText or item.textEdit.newText)
item.documentation = {
kind = lsp.protocol.MarkupKind.Markdown,
value = ('```%s\n%s\n```'):format(vim.bo.filetype, text),
}
end
local doc = item.documentation
if not doc then
return ''
@@ -759,13 +744,22 @@ function CompletionResolver:request(bufnr, param, selected_word)
end
local value = vim.tbl_get(result, 'documentation', 'value')
local kind = vim.tbl_get(result, 'documentation', 'kind')
local text_format = vim.tbl_get(result, 'insertTextFormat')
if not value then
return
if text_format ~= protocol.InsertTextFormat.Snippet then
return
end
-- generate snippet preview info
local insert_text = vim.tbl_get(result, 'insertText')
if insert_text then
value = ('```%s\n%s\n```'):format(vim.bo.filetype, parse_snippet(insert_text))
kind = lsp.protocol.MarkupKind.Markdown
end
end
local windata = vim.api.nvim__complete_set(cmp_info.selected, {
info = value,
})
local kind = vim.tbl_get(result, 'documentation', 'kind')
update_popup_window(windata.winid, windata.bufnr, kind)
end, bufnr)
end, debounce_time)
@@ -779,21 +773,14 @@ local function on_completechanged(group, bufnr)
buffer = bufnr,
callback = 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')
local data = vim.fn.complete_info({ 'selected' })
if (completed_item.info or '') ~= '' then
local data = vim.fn.complete_info({ 'selected' })
local kind = vim.tbl_get(
completed_item,
'user_data',
'nvim',
'lsp',
'completion_item',
'documentation',
'kind'
)
local kind = vim.tbl_get(lsp_item or {}, 'documentation', 'kind')
update_popup_window(
data.preview_winid,
data.preview_bufnr,
kind or lsp.protocol.MarkupKind.PlainText
kind or protocol.MarkupKind.Markdown
)
return
end
@@ -805,15 +792,26 @@ local function on_completechanged(group, bufnr)
bufnr = ev.buf,
}) == 0
then
if
has_completeopt('popup')
and lsp_item
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)
api.nvim__complete_set(
data.selected,
{ info = ('```%s\n%s\n```'):format(vim.bo.filetype, text) }
)
end
return
end
-- Retrieve the raw LSP completionItem from completed_item as the parameter for
-- the completionItem/resolve request
local param = vim.tbl_get(completed_item, 'user_data', 'nvim', 'lsp', 'completion_item')
if param then
if lsp_item then
Context.resolve_handler = Context.resolve_handler or CompletionResolver.new()
Context.resolve_handler:request(ev.buf, param, completed_item.word)
Context.resolve_handler:request(ev.buf, lsp_item, completed_item.word)
end
end,
desc = 'Request and display LSP completion item documentation via completionItem/resolve',

View File

@@ -766,45 +766,11 @@ describe('vim.lsp.completion: item conversion', function()
eq(1, #result.items)
eq('foobar', result.items[1].user_data.nvim.lsp.completion_item.textEdit.newText)
end)
it('shows snippet source in doc popup if completeopt=popup', function()
exec_lua([[
vim.opt.completeopt:append('popup')
vim.bo.filetype = 'lua'
]])
local completion_list = {
isIncomplete = false,
items = {
{
insertText = 'for ${1:index}, ${2:value} in ipairs(${3:t}) do\n\t$0\nend',
insertTextFormat = 2,
kind = 15,
label = 'for .. ipairs',
sortText = '0001',
},
{
insertText = 'for ${1:i}, ${2:v} in ipairs(${3:t}) do\n\t$0\nend',
insertTextFormat = 2,
kind = 15,
label = 'for .. ipairs 2',
sortText = '0002',
documentation = vim.NIL,
},
},
}
local result = complete('|', completion_list)
eq('for .. ipairs', result.items[1].word)
eq('```lua\nfor index, value in ipairs(t) do\n\t\nend\n```', result.items[1].info)
eq('markdown', result.items[1].user_data.nvim.lsp.completion_item.documentation.kind)
eq('for .. ipairs 2', result.items[2].word)
eq('```lua\nfor i, v in ipairs(t) do\n\t\nend\n```', result.items[2].info)
eq('markdown', result.items[2].user_data.nvim.lsp.completion_item.documentation.kind)
end)
end)
--- @param name string
--- @param completion_result vim.lsp.CompletionResult
--- @param opts? {trigger_chars?: string[], resolve_result?: lsp.CompletionItem, delay?: integer, cmp?: string}
--- @param opts? {trigger_chars?: string[], resolve_result?: lsp.CompletionItem|lsp.CompletionItem[], delay?: integer, cmp?: string}
--- @return integer
local function create_server(name, completion_result, opts)
opts = opts or {}
@@ -827,8 +793,13 @@ local function create_server(name, completion_result, opts)
callback(nil, completion_result)
end
end,
['completionItem/resolve'] = function(_, _, callback)
callback(nil, opts.resolve_result)
['completionItem/resolve'] = function(_, request_item, callback)
if type(opts.resolve_result) == 'table' and not opts.resolve_result.label then
local selected = vim.fn.complete_info({ 'selected' }).selected
callback(nil, opts.resolve_result[selected + 1] or request_item)
else
callback(nil, opts.resolve_result)
end
end,
},
})
@@ -1383,7 +1354,7 @@ describe('vim.lsp.completion: integration', function()
eq('w-1/2', n.api.nvim_get_current_line())
end)
it('selecting an item triggers completionItem/resolve + preview', function()
it('selecting an item triggers completionItem/resolve + (snippet) preview', function()
local screen = Screen.new(50, 20)
screen:add_extra_attr_ids({
[100] = { background = Screen.colors.Plum1, foreground = Screen.colors.Blue },
@@ -1398,6 +1369,20 @@ describe('vim.lsp.completion: integration', function()
label = 'nvim__id_array(arr)',
sortText = '0002',
},
{
insertText = 'for ${1:i} = ${2:1}, ${3:10, 1} do\n\t$0\nend',
insertTextFormat = 2,
kind = 15,
label = 'for i = ..',
sortText = '0003',
},
{
insertText = '_assert_integer(${1:x}, ${2:base?})',
insertTextFormat = 2,
kind = 3,
label = '_assert_integer(x, base)',
sortText = '0005',
},
},
}
exec_lua(function()
@@ -1405,20 +1390,41 @@ describe('vim.lsp.completion: integration', function()
end)
create_server('dummy', completion_list, {
resolve_result = {
detail = 'function',
documentation = {
kind = 'markdown',
value = [[```lua\nfunction vim.api.nvim__id_array(arr: any[])\n -> any[]\n```]],
{
detail = 'function',
documentation = {
kind = 'markdown',
value = [[```lua\nfunction vim.api.nvim__id_array(arr: any[])\n -> any[]\n```]],
},
insertText = 'nvim__id_array',
insertTextFormat = 1,
kind = 3,
label = 'nvim__id_array(arr)',
sortText = '0002',
},
{
insertText = 'for ${1:i} = ${2:1}, ${3:10, 1} do\n\t$0\nend',
insertTextFormat = 2,
kind = 15,
label = 'for i = ..',
sortText = '0003',
},
{
detail = 'function',
documentation = {
kind = 'markdown',
value = [[```lua\nmore doc for vim._assert_integer\n```]],
},
insertText = 'nvim__id_array',
insertTextFormat = 2,
kind = 3,
label = '_assert_integer',
sortText = '0005',
},
insertText = 'nvim__id_array',
insertTextFormat = 1,
kind = 3,
label = 'nvim__id_array(arr)',
sortText = '0002',
},
})
feed('Sapi.<C-X><C-O>')
feed('S<C-X><C-O>')
retry(nil, nil, function()
eq(
{ true, true, [[```lua\nfunction vim.api.nvim__id_array(arr: any[])\n -> any[]\n```]] },
@@ -1433,12 +1439,29 @@ describe('vim.lsp.completion: integration', function()
)
end)
screen:expect([[
api.nvim__id_array^ |
{1:~ }{12: nvim__id_array Function }{100:lua\nfunction vim.}{4: }{1: }|
{1:~ }{100:api.nvim__id_array(ar}{1: }|
{1:~ }{100:r: any[])\n -> any[]}{1: }|
{1:~ }{100:\n}{4: }{1: }|
{1:~ }|*14
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
{5:-- INSERT --} |
]])
feed('<C-N>')
screen:expect([[
for i = ..^ |
{4:nvim__id_array Function }{100:for i = 1, 10, 1 do}{1: }|
{12:for i = .. Snippet }{100: }{4: }{1: }|
{4:_assert_integer Function }{100:end}{4: }{1: }|
{1:~ }|*15
{5:-- INSERT --} |
]])
feed('<C-N>')
screen:expect([[
_assert_integer(x, base)^ |
{4:nvim__id_array Function }{100:lua\nmore doc for vim}{4: }{1: }|
{4:for i = .. Snippet }{100:._assert_integer\n}{4: }{1: }|
{12:_assert_integer Function }{1: }|
{1:~ }|*15
{5:-- INSERT --} |
]])
end)