From 4ed597389c33fe22911d1d8bad93173ec24920cb Mon Sep 17 00:00:00 2001 From: glepnir Date: Mon, 23 Mar 2026 19:57:36 +0800 Subject: [PATCH] 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. --- runtime/lua/vim/lsp/completion.lua | 60 ++++---- .../functional/plugin/lsp/completion_spec.lua | 131 ++++++++++-------- 2 files changed, 106 insertions(+), 85 deletions(-) diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index e5c7d1c803..4cce188798 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -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', diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index dfe1c67e89..a713b47b93 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -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.') + feed('S') 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('') + 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('') + 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)