Files
neovim/runtime/lua/vim/lsp/_completion.lua
2023-10-21 13:49:05 +02:00

211 lines
6.8 KiB
Lua

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