diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 111405bc05..44de65ca1b 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -435,12 +435,21 @@ 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 completion_item = { word = word, abbr = ('%s%s'):format(item.label, vim.tbl_get(item, 'labelDetails', 'detail') or ''), kind = kind, menu = vim.tbl_get(item, 'labelDetails', 'description') or '', - info = get_doc(item), + info = info, icase = 1, dup = 1, empty = 1, @@ -800,7 +809,8 @@ local function on_completechanged(group, bufnr) return end local data = vim.fn.complete_info({ 'selected' }) - if (completed_item.info or '') ~= '' then + local info = completed_item.info or '' + if info ~= '' then local kind = vim.tbl_get(lsp_item, 'documentation', 'kind') update_popup_window( data.preview_winid, @@ -818,6 +828,7 @@ local function on_completechanged(group, bufnr) then if has_completeopt('popup') + and info == '' and lsp_item.insertTextFormat == protocol.InsertTextFormat.Snippet then -- Shows snippet preview in doc popup if completeopt=popup. diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index 8e5035a315..ac556d826b 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -1414,255 +1414,236 @@ describe('vim.lsp.completion: integration', function() eq('w-1/2', n.api.nvim_get_current_line()) end) - 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 }, - }) - local completion_list = { - isIncomplete = false, - items = { - { - -- detail populated but not documentation - detail = '(method) nvim__id_array_1(arr: any[]): any[]', - insertText = 'nvim__id_array_1', - insertTextFormat = 1, - kind = 3, - label = 'nvim__id_array_1(arr)', - sortText = '0001', + describe('selecting an item triggers (snippet) preview', function() + ---@type lsp.CompletionItem[] + local incomplete_items = { + { + -- detail populated but not documentation + detail = '(method) nvim__id_array_1(arr: any[]): any[]', + insertText = 'nvim__id_array_1', + insertTextFormat = 1, + kind = 3, + label = 'nvim__id_array_1(arr)', + sortText = '0001', + }, + { + -- documentation populated but not detail + documentation = { + kind = 'markdown', + value = [[```lua\nfunction vim.api.nvim__id_array_2(arr: any[])\n -> any[]\n```]], }, - { - -- documentation populated but not detail - documentation = { - kind = 'markdown', - value = [[```lua\nfunction vim.api.nvim__id_array_2(arr: any[])\n -> any[]\n```]], + insertText = 'nvim__id_array_2', + insertTextFormat = 1, + kind = 3, + label = 'nvim__id_array_2(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', + }, + { + textEdit = { + newText = 'for ${1:j} = ${2:1}, ${3:10, 1} do\n\t$0\nend', + range = { + start = { character = 0, line = 0 }, + ['end'] = { character = 0, line = 0 }, }, - insertText = 'nvim__id_array_2', - insertTextFormat = 1, - kind = 3, - label = 'nvim__id_array_2(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', - }, - { - textEdit = { - newText = 'for ${1:j} = ${2:1}, ${3:10, 1} do\n\t$0\nend', - range = { - start = { character = 0, line = 0 }, - ['end'] = { character = 0, line = 0 }, - }, - }, - insertTextFormat = 2, - kind = 15, - label = 'for j = ..', - sortText = '0004', - }, - { - insertText = '_assert_integer(${1:x}, ${2:base?})', - insertTextFormat = 2, - kind = 3, - label = '_assert_integer(x, base)', - sortText = '0005', }, + insertTextFormat = 2, + kind = 15, + label = 'for j = ..', + sortText = '0004', + }, + { + insertText = '_assert_integer(${1:x}, ${2:base?})', + insertTextFormat = 2, + kind = 3, + label = '_assert_integer(x, base)', + sortText = '0005', }, } - exec_lua(function() - vim.o.completeopt = 'menuone,popup' - end) - local dummy_client_id = create_server('dummy', completion_list, { - resolve_result = { - { - -- detail not in documentation, should be prepended as code block - detail = '(method) nvim__id_array_1(arr: any[]): any[]', - documentation = { - kind = 'markdown', - value = [[```lua\nfunction vim.api.nvim__id_array_1(arr: any[])\n -> any[]\n```]], - }, - insertText = 'nvim__id_array_1', - insertTextFormat = 1, - kind = 3, - label = 'nvim__id_array_1(arr)', - sortText = '0001', - }, - { - -- detail not in documentation, should be prepended as code block - detail = '(method) nvim__id_array_2(arr: any[]): any[]', - documentation = { - kind = 'markdown', - value = [[```lua\nfunction vim.api.nvim__id_array_2(arr: any[])\n -> any[]\n```]], - }, - insertText = 'nvim__id_array_2', - insertTextFormat = 1, - kind = 3, - label = 'nvim__id_array_2(arr)', - sortText = '0002', - }, - { - -- snippet populated in insertText - insertText = 'for ${1:i} = ${2:1}, ${3:10, 1} do\n\t$0\nend', - insertTextFormat = 2, - kind = 15, - label = 'for i = ..', - sortText = '0003', - }, - { - -- snippet populated in textEdit.newText - textEdit = { - newText = 'for ${1:j} = ${2:1}, ${3:10, 1} do\n\t$0\nend', - range = { - start = { character = 0, line = 0 }, - ['end'] = { character = 0, line = 0 }, - }, - }, - insertTextFormat = 2, - kind = 15, - label = 'for j = ..', - sortText = '0004', - }, - { - -- detail is in documentation, should not be duplicated - detail = '_assert_integer', - 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', + ---@type lsp.CompletionItem[] + local complete_items = { + { + -- detail not in documentation, should be prepended as code block + detail = '(method) nvim__id_array_1(arr: any[]): any[]', + documentation = { + kind = 'markdown', + value = [[```lua\nfunction vim.api.nvim__id_array_1(arr: any[])\n -> any[]\n```]], }, + insertText = 'nvim__id_array_1', + insertTextFormat = 1, + kind = 3, + label = 'nvim__id_array_1(arr)', + sortText = '0001', }, - }) - - feed('S') - retry(nil, nil, function() - 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_1(arr: any[]): any[]', 1, true)) - neq(nil, info:find('function vim.api.nvim__id_array_1', 1, true)) - end) - screen:expect([[ - nvim__id_array_1^ | - {12:nvim__id_array_1 Function }{100:(method) nvim__id_array}{1: }| - {4:nvim__id_array_2 Function }{100:_1(arr: any[]): any[]}{4: }{1: }| - {4:for i = .. Snippet }{100:lua\nfunction vim.ap}{4: }{1: }| - {4:for j = .. Snippet }{100:i.nvim__id_array_1(arr:}{1: }| - {4:_assert_integer Function }{100: any[])\n -> any[]\n}{4: }{1: }| - {1:~ }{4: }{1: }| - {1:~ }|*12 - {5:-- INSERT --} | - ]]) - feed('') - screen:expect([[ - nvim__id_array_2^ | - {4:nvim__id_array_1 Function }{100:(method) nvim__id_array}{1: }| - {12:nvim__id_array_2 Function }{100:_2(arr: any[]): any[]}{4: }{1: }| - {4:for i = .. Snippet }{100:lua\nfunction vim.ap}{4: }{1: }| - {4:for j = .. Snippet }{100:i.nvim__id_array_2(arr:}{1: }| - {4:_assert_integer Function }{100: any[])\n -> any[]\n}{4: }{1: }| - {1:~ }{4: }{1: }| - {1:~ }|*12 - {5:-- INSERT --} | - ]]) - feed('') - screen:expect([[ - for i = ..^ | - {4:nvim__id_array_1 Function }{100:for i = 1, 10, 1 do}{1: }| - {4:nvim__id_array_2 Function }{100: }{4: }{1: }| - {12:for i = .. Snippet }{100:end}{4: }{1: }| - {4:for j = .. Snippet }{1: }| - {4:_assert_integer Function }{1: }| - {1:~ }|*13 - {5:-- INSERT --} | - ]]) - feed('') - screen:expect([[ - for j = ..^ | - {4:nvim__id_array_1 Function }{100:for j = 1, 10, 1 do}{1: }| - {4:nvim__id_array_2 Function }{100: }{4: }{1: }| - {4:for i = .. Snippet }{100:end}{4: }{1: }| - {12:for j = .. Snippet }{1: }| - {4:_assert_integer Function }{1: }| - {1:~ }|*13 - {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_1 Function }{100:lua\nmore doc for vi}{4: }{1: }| - {4:nvim__id_array_2 Function }{100:m._assert_integer\n}{4: }{1: }| - {4:for i = .. Snippet }{1: }| - {4:for j = .. Snippet }{1: }| - {12:_assert_integer Function }{1: }| - {1:~ }|*13 - {5:-- INSERT --} | - ]]) - - n.command('lua vim.lsp.buf_detach_client(0, ' .. dummy_client_id .. ')') - -- Server which doesn't support completionItem/resolve - create_server('dummy2', { - isIncomplete = false, - items = { - { - insertText = 'package main', - insertTextFormat = 1, - kind = 9, - label = 'package main', - sortText = '0001', - }, - { - insertText = 'package ${1:name}', - insertTextFormat = 2, - kind = 9, - label = 'package', - sortText = '0002', + { + -- detail not in documentation, should be prepended as code block + detail = '(method) nvim__id_array_2(arr: any[]): any[]', + documentation = { + kind = 'markdown', + value = [[```lua\nfunction vim.api.nvim__id_array_2(arr: any[])\n -> any[]\n```]], }, + insertText = 'nvim__id_array_2', + insertTextFormat = 1, + kind = 3, + label = 'nvim__id_array_2(arr)', + sortText = '0002', }, - }) - feed('S') - -- No popup shown for item without snippet - wait_for_pum() - eq(true, n.fn.complete_info({ 'selected' }).preview_bufnr == nil) - feed('') - -- Popup shown for item with snippet - screen:expect([[ - package^ | - {4:package main Module }{100:package name}{1: }| - {12:package Module }{1: }| - {1:~ }|*16 - {5:-- INSERT --} | - ]]) + { + -- snippet populated in insertText + insertText = 'for ${1:i} = ${2:1}, ${3:10, 1} do\n\t$0\nend', + insertTextFormat = 2, + kind = 15, + label = 'for i = ..', + sortText = '0003', + }, + { + -- snippet populated in textEdit.newText + textEdit = { + newText = 'for ${1:j} = ${2:1}, ${3:10, 1} do\n\t$0\nend', + range = { + start = { character = 0, line = 0 }, + ['end'] = { character = 0, line = 0 }, + }, + }, + insertTextFormat = 2, + kind = 15, + label = 'for j = ..', + sortText = '0004', + }, + { + -- detail is in documentation, should not be duplicated + detail = '_assert_integer', + documentation = { + kind = 'markdown', + value = [[```lua\nmore doc for vim._assert_integer\n```]], + }, + insertText = '_assert_integer(${1:x}, ${2:base?})', + insertTextFormat = 2, + kind = 3, + label = '_assert_integer(x, base)', + sortText = '0005', + }, + } + + ---@param opts {items:lsp.CompletionItem[], resolved_items:lsp.CompletionItem[]} + local function run_test(opts) + local screen = Screen.new(50, 20) + screen:add_extra_attr_ids({ + [100] = { background = Screen.colors.Plum1, foreground = Screen.colors.Blue }, + }) + local completion_list = { + isIncomplete = false, + items = opts.items, + } + exec_lua(function() + vim.o.completeopt = 'menuone,popup' + end) + create_server('dummy', completion_list, { + resolve_result = opts.resolved_items, + }) + + feed('S') + retry(nil, nil, function() + 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_1(arr: any[]): any[]', 1, true)) + neq(nil, info:find('function vim.api.nvim__id_array_1', 1, true)) + end) + screen:expect([[ + nvim__id_array_1^ | + {12:nvim__id_array_1 Function }{100:(method) nvim__id_array}{1: }| + {4:nvim__id_array_2 Function }{100:_1(arr: any[]): any[]}{4: }{1: }| + {4:for i = .. Snippet }{100:lua\nfunction vim.ap}{4: }{1: }| + {4:for j = .. Snippet }{100:i.nvim__id_array_1(arr:}{1: }| + {4:_assert_integer Function }{100: any[])\n -> any[]\n}{4: }{1: }| + {1:~ }{4: }{1: }| + {1:~ }|*12 + {5:-- INSERT --} | + ]]) + feed('') + screen:expect([[ + nvim__id_array_2^ | + {4:nvim__id_array_1 Function }{100:(method) nvim__id_array}{1: }| + {12:nvim__id_array_2 Function }{100:_2(arr: any[]): any[]}{4: }{1: }| + {4:for i = .. Snippet }{100:lua\nfunction vim.ap}{4: }{1: }| + {4:for j = .. Snippet }{100:i.nvim__id_array_2(arr:}{1: }| + {4:_assert_integer Function }{100: any[])\n -> any[]\n}{4: }{1: }| + {1:~ }{4: }{1: }| + {1:~ }|*12 + {5:-- INSERT --} | + ]]) + feed('') + screen:expect([[ + for i = ..^ | + {4:nvim__id_array_1 Function }{100:for i = 1, 10, 1 do}{1: }| + {4:nvim__id_array_2 Function }{100: }{4: }{1: }| + {12:for i = .. Snippet }{100:end}{4: }{1: }| + {4:for j = .. Snippet }{1: }| + {4:_assert_integer Function }{1: }| + {1:~ }|*13 + {5:-- INSERT --} | + ]]) + feed('') + screen:expect([[ + for j = ..^ | + {4:nvim__id_array_1 Function }{100:for j = 1, 10, 1 do}{1: }| + {4:nvim__id_array_2 Function }{100: }{4: }{1: }| + {4:for i = .. Snippet }{100:end}{4: }{1: }| + {12:for j = .. Snippet }{1: }| + {4:_assert_integer Function }{1: }| + {1:~ }|*13 + {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_1 Function }{100:lua\nmore doc for vi}{4: }{1: }| + {4:nvim__id_array_2 Function }{100:m._assert_integer\n}{4: }{1: }| + {4:for i = .. Snippet }{1: }| + {4:for j = .. Snippet }{1: }| + {12:_assert_integer Function }{1: }| + {1:~ }|*13 + {5:-- INSERT --} | + ]]) + end + + it('when server supports completionItem/resolve', function() + run_test({ items = incomplete_items, resolved_items = complete_items }) + end) + + it('when server does not support completionItem/resolve', function() + run_test({ items = complete_items }) + end) end) it('omnifunc works without enable() #38252', function()