mirror of
https://github.com/neovim/neovim.git
synced 2024-12-24 13:15:09 -07:00
refactor(lsp): move completion logic into _completion module
To reduce cross-chatter between modules and for https://github.com/neovim/neovim/issues/25272 Also preparing for https://github.com/neovim/neovim/issues/25714
This commit is contained in:
parent
9971bea6f1
commit
1e10310f4c
@ -1719,7 +1719,7 @@ extract_completion_items({result})
|
|||||||
• {result} (table) The result of a `textDocument/completion` request
|
• {result} (table) The result of a `textDocument/completion` request
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(table) List of completion items
|
lsp.CompletionItem[] List of completion items
|
||||||
|
|
||||||
See also: ~
|
See also: ~
|
||||||
• https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
|
• https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
|
||||||
@ -2014,7 +2014,7 @@ text_document_completion_list_to_complete_items({result}, {prefix})
|
|||||||
• {prefix} (string) the prefix to filter the completion items
|
• {prefix} (string) the prefix to filter the completion items
|
||||||
|
|
||||||
Return: ~
|
Return: ~
|
||||||
(table) { matches = complete-items table, incomplete = bool }
|
table[] items
|
||||||
|
|
||||||
See also: ~
|
See also: ~
|
||||||
• complete-items
|
• complete-items
|
||||||
|
@ -2273,24 +2273,6 @@ function lsp.buf_notify(bufnr, method, params)
|
|||||||
return resp
|
return resp
|
||||||
end
|
end
|
||||||
|
|
||||||
---@private
|
|
||||||
local function adjust_start_col(lnum, line, items, encoding)
|
|
||||||
local min_start_char = nil
|
|
||||||
for _, item in pairs(items) do
|
|
||||||
if item.textEdit and item.textEdit.range.start.line == lnum - 1 then
|
|
||||||
if min_start_char and min_start_char ~= item.textEdit.range.start.character then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
min_start_char = item.textEdit.range.start.character
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if min_start_char then
|
|
||||||
return util._str_byteindex_enc(line, min_start_char, encoding)
|
|
||||||
else
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Implements 'omnifunc' compatible LSP completion.
|
--- Implements 'omnifunc' compatible LSP completion.
|
||||||
---
|
---
|
||||||
---@see |complete-functions|
|
---@see |complete-functions|
|
||||||
@ -2307,82 +2289,7 @@ function lsp.omnifunc(findstart, base)
|
|||||||
if log.debug() then
|
if log.debug() then
|
||||||
log.debug('omnifunc.findstart', { findstart = findstart, base = base })
|
log.debug('omnifunc.findstart', { findstart = findstart, base = base })
|
||||||
end
|
end
|
||||||
|
return require('vim.lsp._completion').omnifunc(findstart, base)
|
||||||
local bufnr = resolve_bufnr()
|
|
||||||
local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion })
|
|
||||||
local remaining = #clients
|
|
||||||
if remaining == 0 then
|
|
||||||
return findstart == 1 and -1 or {}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Then, perform standard completion request
|
|
||||||
if log.info() then
|
|
||||||
log.info('base ', base)
|
|
||||||
end
|
|
||||||
|
|
||||||
local win = api.nvim_get_current_win()
|
|
||||||
local pos = api.nvim_win_get_cursor(win)
|
|
||||||
local line = api.nvim_get_current_line()
|
|
||||||
local line_to_cursor = line:sub(1, pos[2])
|
|
||||||
local _ = log.trace() and log.trace('omnifunc.line', pos, line)
|
|
||||||
|
|
||||||
-- Get the start position of the current keyword
|
|
||||||
local match_pos = vim.fn.match(line_to_cursor, '\\k*$') + 1
|
|
||||||
local items = {}
|
|
||||||
|
|
||||||
local startbyte
|
|
||||||
|
|
||||||
local function on_done()
|
|
||||||
local mode = api.nvim_get_mode()['mode']
|
|
||||||
if mode == 'i' or mode == 'ic' then
|
|
||||||
vim.fn.complete(startbyte or match_pos, items)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, client in ipairs(clients) do
|
|
||||||
local params = util.make_position_params(win, client.offset_encoding)
|
|
||||||
client.request(ms.textDocument_completion, params, function(err, result)
|
|
||||||
if err then
|
|
||||||
log.warn(err.message)
|
|
||||||
end
|
|
||||||
if result and vim.fn.mode() == 'i' then
|
|
||||||
-- Completion response items may be relative to a position different than `textMatch`.
|
|
||||||
-- Concrete example, with sumneko/lua-language-server:
|
|
||||||
--
|
|
||||||
-- require('plenary.asy|
|
|
||||||
-- ▲ ▲ ▲
|
|
||||||
-- │ │ └── cursor_pos: 20
|
|
||||||
-- │ └────── textMatch: 17
|
|
||||||
-- └────────────── textEdit.range.start.character: 9
|
|
||||||
-- .newText = 'plenary.async'
|
|
||||||
-- ^^^
|
|
||||||
-- prefix (We'd remove everything not starting with `asy`,
|
|
||||||
-- so we'd eliminate the `plenary.async` result
|
|
||||||
--
|
|
||||||
-- `adjust_start_col` is used to prefer the language server boundary.
|
|
||||||
--
|
|
||||||
local encoding = client.offset_encoding
|
|
||||||
local candidates = util.extract_completion_items(result)
|
|
||||||
local curstartbyte = adjust_start_col(pos[1], line, candidates, encoding)
|
|
||||||
if startbyte == nil then
|
|
||||||
startbyte = curstartbyte
|
|
||||||
elseif curstartbyte ~= nil and curstartbyte ~= startbyte then
|
|
||||||
startbyte = match_pos
|
|
||||||
end
|
|
||||||
local prefix = startbyte and line:sub(startbyte + 1) or line_to_cursor:sub(match_pos)
|
|
||||||
local matches = util.text_document_completion_list_to_complete_items(result, prefix)
|
|
||||||
vim.list_extend(items, matches)
|
|
||||||
end
|
|
||||||
remaining = remaining - 1
|
|
||||||
if remaining == 0 then
|
|
||||||
vim.schedule(on_done)
|
|
||||||
end
|
|
||||||
end, bufnr)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Return -2 to signal that we should continue completion so that we can
|
|
||||||
-- async complete.
|
|
||||||
return -2
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Provides an interface between the built-in client and a `formatexpr` function.
|
--- Provides an interface between the built-in client and a `formatexpr` function.
|
||||||
|
210
runtime/lua/vim/lsp/_completion.lua
Normal file
210
runtime/lua/vim/lsp/_completion.lua
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
local M = {}
|
||||||
|
local api = vim.api
|
||||||
|
local lsp = vim.lsp
|
||||||
|
local protocol = lsp.protocol
|
||||||
|
local ms = protocol.Methods
|
||||||
|
|
||||||
|
---@param input string unparsed snippet
|
||||||
|
---@return string parsed snippet
|
||||||
|
local function parse_snippet(input)
|
||||||
|
local ok, parsed = pcall(function()
|
||||||
|
return require('vim.lsp._snippet_grammar').parse(input)
|
||||||
|
end)
|
||||||
|
return ok and tostring(parsed) or input
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns text that should be inserted when selecting completion item. The
|
||||||
|
--- precedence is as follows: textEdit.newText > insertText > label
|
||||||
|
---
|
||||||
|
--- See https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
|
||||||
|
---
|
||||||
|
---@param item lsp.CompletionItem
|
||||||
|
---@return string
|
||||||
|
local function get_completion_word(item)
|
||||||
|
if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
|
||||||
|
if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
|
||||||
|
return item.textEdit.newText
|
||||||
|
else
|
||||||
|
return parse_snippet(item.textEdit.newText)
|
||||||
|
end
|
||||||
|
elseif item.insertText ~= nil and item.insertText ~= '' then
|
||||||
|
if item.insertTextFormat == protocol.InsertTextFormat.PlainText then
|
||||||
|
return item.insertText
|
||||||
|
else
|
||||||
|
return parse_snippet(item.insertText)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return item.label
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param result lsp.CompletionList|lsp.CompletionItem[]
|
||||||
|
---@return lsp.CompletionItem[]
|
||||||
|
local function get_items(result)
|
||||||
|
if result.items then
|
||||||
|
return result.items
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Turns the result of a `textDocument/completion` request into vim-compatible
|
||||||
|
--- |complete-items|.
|
||||||
|
---
|
||||||
|
---@param result lsp.CompletionList|lsp.CompletionItem[] Result of `textDocument/completion`
|
||||||
|
---@param prefix string prefix to filter the completion items
|
||||||
|
---@return table[]
|
||||||
|
---@see complete-items
|
||||||
|
function M._lsp_to_complete_items(result, prefix)
|
||||||
|
local items = get_items(result)
|
||||||
|
if vim.tbl_isempty(items) then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function matches_prefix(item)
|
||||||
|
return vim.startswith(get_completion_word(item), prefix)
|
||||||
|
end
|
||||||
|
|
||||||
|
items = vim.tbl_filter(matches_prefix, items) --[[@as lsp.CompletionItem[]|]]
|
||||||
|
table.sort(items, function(a, b)
|
||||||
|
return (a.sortText or a.label) < (b.sortText or b.label)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local matches = {}
|
||||||
|
for _, item in ipairs(items) do
|
||||||
|
local info = ''
|
||||||
|
local documentation = item.documentation
|
||||||
|
if documentation then
|
||||||
|
if type(documentation) == 'string' and documentation ~= '' then
|
||||||
|
info = documentation
|
||||||
|
elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
|
||||||
|
info = documentation.value
|
||||||
|
else
|
||||||
|
vim.notify(
|
||||||
|
('invalid documentation value %s'):format(vim.inspect(documentation)),
|
||||||
|
vim.log.levels.WARN
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local word = get_completion_word(item)
|
||||||
|
table.insert(matches, {
|
||||||
|
word = word,
|
||||||
|
abbr = item.label,
|
||||||
|
kind = protocol.CompletionItemKind[item.kind] or 'Unknown',
|
||||||
|
menu = item.detail or '',
|
||||||
|
info = #info > 0 and info or nil,
|
||||||
|
icase = 1,
|
||||||
|
dup = 1,
|
||||||
|
empty = 1,
|
||||||
|
user_data = {
|
||||||
|
nvim = {
|
||||||
|
lsp = {
|
||||||
|
completion_item = item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return matches
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param items lsp.CompletionItem[]
|
||||||
|
local function adjust_start_col(lnum, line, items, encoding)
|
||||||
|
local min_start_char = nil
|
||||||
|
for _, item in pairs(items) do
|
||||||
|
if item.textEdit and item.textEdit.range.start.line == lnum - 1 then
|
||||||
|
if min_start_char and min_start_char ~= item.textEdit.range.start.character then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
min_start_char = item.textEdit.range.start.character
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if min_start_char then
|
||||||
|
return vim.lsp.util._str_byteindex_enc(line, min_start_char, encoding)
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param findstart integer 0 or 1, decides behavior
|
||||||
|
---@param base integer findstart=0, text to match against
|
||||||
|
---@return integer|table Decided by {findstart}:
|
||||||
|
--- - findstart=0: column where the completion starts, or -2 or -3
|
||||||
|
--- - findstart=1: list of matches (actually just calls |complete()|)
|
||||||
|
function M.omnifunc(findstart, base)
|
||||||
|
local bufnr = api.nvim_get_current_buf()
|
||||||
|
local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_completion })
|
||||||
|
local remaining = #clients
|
||||||
|
if remaining == 0 then
|
||||||
|
return findstart == 1 and -1 or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local log = require('vim.lsp.log')
|
||||||
|
-- Then, perform standard completion request
|
||||||
|
if log.info() then
|
||||||
|
log.info('base ', base)
|
||||||
|
end
|
||||||
|
|
||||||
|
local win = api.nvim_get_current_win()
|
||||||
|
local pos = api.nvim_win_get_cursor(win)
|
||||||
|
local line = api.nvim_get_current_line()
|
||||||
|
local line_to_cursor = line:sub(1, pos[2])
|
||||||
|
log.trace('omnifunc.line', pos, line)
|
||||||
|
|
||||||
|
local word_boundary = vim.fn.match(line_to_cursor, '\\k*$') + 1 --[[@as integer]]
|
||||||
|
local items = {}
|
||||||
|
local startbyte = nil
|
||||||
|
|
||||||
|
local function on_done()
|
||||||
|
local mode = api.nvim_get_mode()['mode']
|
||||||
|
if mode == 'i' or mode == 'ic' then
|
||||||
|
vim.fn.complete(startbyte or word_boundary, items)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local util = vim.lsp.util
|
||||||
|
for _, client in ipairs(clients) do
|
||||||
|
local params = util.make_position_params(win, client.offset_encoding)
|
||||||
|
client.request(ms.textDocument_completion, params, function(err, result)
|
||||||
|
if err then
|
||||||
|
log.warn(err.message)
|
||||||
|
end
|
||||||
|
if result and vim.fn.mode() == 'i' then
|
||||||
|
-- Completion response items may be relative to a position different than `textMatch`.
|
||||||
|
-- Concrete example, with sumneko/lua-language-server:
|
||||||
|
--
|
||||||
|
-- require('plenary.asy|
|
||||||
|
-- ▲ ▲ ▲
|
||||||
|
-- │ │ └── cursor_pos: 20
|
||||||
|
-- │ └────── textMatch: 17
|
||||||
|
-- └────────────── textEdit.range.start.character: 9
|
||||||
|
-- .newText = 'plenary.async'
|
||||||
|
-- ^^^
|
||||||
|
-- prefix (We'd remove everything not starting with `asy`,
|
||||||
|
-- so we'd eliminate the `plenary.async` result
|
||||||
|
--
|
||||||
|
-- `adjust_start_col` is used to prefer the language server boundary.
|
||||||
|
--
|
||||||
|
local encoding = client.offset_encoding
|
||||||
|
local candidates = get_items(result)
|
||||||
|
local curstartbyte = adjust_start_col(pos[1], line, candidates, encoding)
|
||||||
|
if startbyte == nil then
|
||||||
|
startbyte = curstartbyte
|
||||||
|
elseif curstartbyte ~= nil and curstartbyte ~= startbyte then
|
||||||
|
startbyte = word_boundary
|
||||||
|
end
|
||||||
|
local prefix = startbyte and line:sub(startbyte + 1) or line_to_cursor:sub(word_boundary)
|
||||||
|
local matches = M._lsp_to_complete_items(result, prefix)
|
||||||
|
vim.list_extend(items, matches)
|
||||||
|
end
|
||||||
|
remaining = remaining - 1
|
||||||
|
if remaining == 0 then
|
||||||
|
vim.schedule(on_done)
|
||||||
|
end
|
||||||
|
end, bufnr)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Return -2 to signal that we should continue completion so that we can
|
||||||
|
-- async complete.
|
||||||
|
return -2
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
@ -242,6 +242,7 @@ local constants = {
|
|||||||
|
|
||||||
-- Defines whether the insert text in a completion item should be interpreted as
|
-- Defines whether the insert text in a completion item should be interpreted as
|
||||||
-- plain text or a snippet.
|
-- plain text or a snippet.
|
||||||
|
--- @enum lsp.InsertTextFormat
|
||||||
InsertTextFormat = {
|
InsertTextFormat = {
|
||||||
-- The primary text to be inserted is treated as a plain string.
|
-- The primary text to be inserted is treated as a plain string.
|
||||||
PlainText = 1,
|
PlainText = 1,
|
||||||
|
@ -548,7 +548,7 @@ end
|
|||||||
--- `textDocument/completion` request, which may return one of
|
--- `textDocument/completion` request, which may return one of
|
||||||
--- `CompletionItem[]`, `CompletionList` or null.
|
--- `CompletionItem[]`, `CompletionList` or null.
|
||||||
---@param result table The result of a `textDocument/completion` request
|
---@param result table The result of a `textDocument/completion` request
|
||||||
---@return table List of completion items
|
---@return lsp.CompletionItem[] List of completion items
|
||||||
---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
|
---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
|
||||||
function M.extract_completion_items(result)
|
function M.extract_completion_items(result)
|
||||||
if type(result) == 'table' and result.items then
|
if type(result) == 'table' and result.items then
|
||||||
@ -619,47 +619,6 @@ function M.parse_snippet(input)
|
|||||||
return tostring(parsed)
|
return tostring(parsed)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Sorts by CompletionItem.sortText.
|
|
||||||
---
|
|
||||||
--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
|
|
||||||
local function sort_completion_items(items)
|
|
||||||
table.sort(items, function(a, b)
|
|
||||||
return (a.sortText or a.label) < (b.sortText or b.label)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns text that should be inserted when selecting completion item. The
|
|
||||||
--- precedence is as follows: textEdit.newText > insertText > label
|
|
||||||
--see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
|
|
||||||
local function get_completion_word(item)
|
|
||||||
if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= '' then
|
|
||||||
local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
|
|
||||||
if insert_text_format == 'PlainText' or insert_text_format == nil then
|
|
||||||
return item.textEdit.newText
|
|
||||||
else
|
|
||||||
return M.parse_snippet(item.textEdit.newText)
|
|
||||||
end
|
|
||||||
elseif item.insertText ~= nil and item.insertText ~= '' then
|
|
||||||
local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
|
|
||||||
if insert_text_format == 'PlainText' or insert_text_format == nil then
|
|
||||||
return item.insertText
|
|
||||||
else
|
|
||||||
return M.parse_snippet(item.insertText)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return item.label
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Some language servers return complementary candidates whose prefixes do not
|
|
||||||
--- match are also returned. So we exclude completion candidates whose prefix
|
|
||||||
--- does not match.
|
|
||||||
local function remove_unmatch_completion_items(items, prefix)
|
|
||||||
return vim.tbl_filter(function(item)
|
|
||||||
local word = get_completion_word(item)
|
|
||||||
return vim.startswith(word, prefix)
|
|
||||||
end, items)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- According to LSP spec, if the client set `completionItemKind.valueSet`,
|
--- According to LSP spec, if the client set `completionItemKind.valueSet`,
|
||||||
--- the client must handle it properly even if it receives a value outside the
|
--- the client must handle it properly even if it receives a value outside the
|
||||||
--- specification.
|
--- specification.
|
||||||
@ -678,56 +637,10 @@ end
|
|||||||
--- from |vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
|
--- from |vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
|
||||||
--- `CompletionList` or `null`
|
--- `CompletionList` or `null`
|
||||||
---@param prefix (string) the prefix to filter the completion items
|
---@param prefix (string) the prefix to filter the completion items
|
||||||
---@return table { matches = complete-items table, incomplete = bool }
|
---@return table[] items
|
||||||
---@see complete-items
|
---@see complete-items
|
||||||
function M.text_document_completion_list_to_complete_items(result, prefix)
|
function M.text_document_completion_list_to_complete_items(result, prefix)
|
||||||
local items = M.extract_completion_items(result)
|
return require('vim.lsp._completion')._lsp_to_complete_items(result, prefix)
|
||||||
if vim.tbl_isempty(items) then
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
|
|
||||||
items = remove_unmatch_completion_items(items, prefix)
|
|
||||||
sort_completion_items(items)
|
|
||||||
|
|
||||||
local matches = {}
|
|
||||||
|
|
||||||
for _, completion_item in ipairs(items) do
|
|
||||||
local info = ''
|
|
||||||
local documentation = completion_item.documentation
|
|
||||||
if documentation then
|
|
||||||
if type(documentation) == 'string' and documentation ~= '' then
|
|
||||||
info = documentation
|
|
||||||
elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
|
|
||||||
info = documentation.value
|
|
||||||
else
|
|
||||||
vim.notify(
|
|
||||||
('invalid documentation value %s'):format(vim.inspect(documentation)),
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local word = get_completion_word(completion_item)
|
|
||||||
table.insert(matches, {
|
|
||||||
word = word,
|
|
||||||
abbr = completion_item.label,
|
|
||||||
kind = M._get_completion_item_kind_name(completion_item.kind),
|
|
||||||
menu = completion_item.detail or '',
|
|
||||||
info = #info > 0 and info or nil,
|
|
||||||
icase = 1,
|
|
||||||
dup = 1,
|
|
||||||
empty = 1,
|
|
||||||
user_data = {
|
|
||||||
nvim = {
|
|
||||||
lsp = {
|
|
||||||
completion_item = completion_item,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
return matches
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Like vim.fn.bufwinid except it works across tabpages.
|
--- Like vim.fn.bufwinid except it works across tabpages.
|
||||||
|
Loading…
Reference in New Issue
Block a user