mirror of
https://github.com/neovim/neovim.git
synced 2024-12-27 14:21:31 -07:00
277 lines
9.0 KiB
Lua
277 lines
9.0 KiB
Lua
local M = {}
|
|
local api = vim.api
|
|
local lsp = vim.lsp
|
|
local protocol = lsp.protocol
|
|
local ms = protocol.Methods
|
|
|
|
--- @alias vim.lsp.CompletionResult lsp.CompletionList | lsp.CompletionItem[]
|
|
|
|
-- TODO(mariasolos): Remove this declaration once we figure out a better way to handle
|
|
-- literal/anonymous types (see https://github.com/neovim/neovim/pull/27542/files#r1495259331).
|
|
--- @class lsp.ItemDefaults
|
|
--- @field editRange lsp.Range | { insert: lsp.Range, replace: lsp.Range } | nil
|
|
--- @field insertTextFormat lsp.InsertTextFormat?
|
|
--- @field insertTextMode lsp.InsertTextMode?
|
|
--- @field data any
|
|
|
|
---@param input string unparsed snippet
|
|
---@return string parsed snippet
|
|
local function parse_snippet(input)
|
|
local ok, parsed = pcall(function()
|
|
return 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
|
|
|
|
--- Applies the given defaults to the completion item, modifying it in place.
|
|
---
|
|
--- @param item lsp.CompletionItem
|
|
--- @param defaults lsp.ItemDefaults?
|
|
local function apply_defaults(item, defaults)
|
|
if not defaults then
|
|
return
|
|
end
|
|
|
|
item.insertTextFormat = item.insertTextFormat or defaults.insertTextFormat
|
|
item.insertTextMode = item.insertTextMode or defaults.insertTextMode
|
|
item.data = item.data or defaults.data
|
|
if defaults.editRange then
|
|
local textEdit = item.textEdit or {}
|
|
item.textEdit = textEdit
|
|
textEdit.newText = textEdit.newText or item.textEditText or item.insertText
|
|
if defaults.editRange.start then
|
|
textEdit.range = textEdit.range or defaults.editRange
|
|
elseif defaults.editRange.insert then
|
|
textEdit.insert = defaults.editRange.insert
|
|
textEdit.replace = defaults.editRange.replace
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param result vim.lsp.CompletionResult
|
|
---@return lsp.CompletionItem[]
|
|
local function get_items(result)
|
|
if result.items then
|
|
for _, item in ipairs(result.items) do
|
|
---@diagnostic disable-next-line: param-type-mismatch
|
|
apply_defaults(item, result.itemDefaults)
|
|
end
|
|
return result.items
|
|
else
|
|
return result
|
|
end
|
|
end
|
|
|
|
--- Turns the result of a `textDocument/completion` request into vim-compatible
|
|
--- |complete-items|.
|
|
---
|
|
---@param result vim.lsp.CompletionResult 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 lnum integer 0-indexed
|
|
---@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 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
|
|
|
|
---@private
|
|
---@param line string line content
|
|
---@param lnum integer 0-indexed line number
|
|
---@param client_start_boundary integer 0-indexed word boundary
|
|
---@param server_start_boundary? integer 0-indexed word boundary, based on textEdit.range.start.character
|
|
---@param result vim.lsp.CompletionResult
|
|
---@param encoding string
|
|
---@return table[] matches
|
|
---@return integer? server_start_boundary
|
|
function M._convert_results(
|
|
line,
|
|
lnum,
|
|
cursor_col,
|
|
client_start_boundary,
|
|
server_start_boundary,
|
|
result,
|
|
encoding
|
|
)
|
|
-- Completion response items may be relative to a position different than `client_start_boundary`.
|
|
-- Concrete example, with lua-language-server:
|
|
--
|
|
-- require('plenary.asy|
|
|
-- ▲ ▲ ▲
|
|
-- │ │ └── cursor_pos: 20
|
|
-- │ └────── client_start_boundary: 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 candidates = get_items(result)
|
|
local curstartbyte = adjust_start_col(lnum, line, candidates, encoding)
|
|
if server_start_boundary == nil then
|
|
server_start_boundary = curstartbyte
|
|
elseif curstartbyte ~= nil and curstartbyte ~= server_start_boundary then
|
|
server_start_boundary = client_start_boundary
|
|
end
|
|
local prefix = line:sub((server_start_boundary or client_start_boundary) + 1, cursor_col)
|
|
local matches = M._lsp_to_complete_items(result, prefix)
|
|
return matches, server_start_boundary
|
|
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)
|
|
assert(base) -- silence luals
|
|
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 win = api.nvim_get_current_win()
|
|
local cursor = api.nvim_win_get_cursor(win)
|
|
local lnum = cursor[1] - 1
|
|
local cursor_col = cursor[2]
|
|
local line = api.nvim_get_current_line()
|
|
local line_to_cursor = line:sub(1, cursor_col)
|
|
local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$') --[[@as integer]]
|
|
local server_start_boundary = nil
|
|
local items = {}
|
|
|
|
local function on_done()
|
|
local mode = api.nvim_get_mode()['mode']
|
|
if mode == 'i' or mode == 'ic' then
|
|
vim.fn.complete((server_start_boundary or client_start_boundary) + 1, 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
|
|
vim.lsp.log.warn(err.message)
|
|
end
|
|
if result and vim.fn.mode() == 'i' then
|
|
local matches
|
|
matches, server_start_boundary = M._convert_results(
|
|
line,
|
|
lnum,
|
|
cursor_col,
|
|
client_start_boundary,
|
|
server_start_boundary,
|
|
result,
|
|
client.offset_encoding
|
|
)
|
|
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
|