mirror of
https://github.com/neovim/neovim.git
synced 2026-07-16 14:11:28 +00:00
refactor(lsp): centralise complete item info generation logic
Problem: The logic for generating the complete item `info` is spread across `_lsp_to_complete_items`, the `CompleteChanged` event handler, and `CompletionResolver:request`. This has previously caused the `info` shown for resolved (via `completionItem/resolve`) and unresolved items to differ. Solution: Centralise the logic in a new `complete_item_info` function which is now solely responsible for determining: 1. The `info` to show. 2. The markup kind of the `info`. 3. Whether the `info` is complete. This simplifies the interaction between the 3 functions mentioned in the problem: - `_lsp_to_complete_items` calls `complete_item_info` and passes along the markup kind and whether the item needs resolving via the complete item's `user_data`. - The `CompleteChanged` consumes the markup kind and whether the item needs resolving from the complete item's `user_data`. - `CompletionResolver:request`, like `_lsp_to_complete_items` calls `complete_item_info` again and updates the current `info` if it's changed.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user