8000 mini.nvim/lua/mini/completion.lua at main · mtmatye/mini.nvim · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
1381 lines (1180 loc) · 52 KB

completion.lua

File metadata and controls

1381 lines (1180 loc) · 52 KB
['window.signature.height'] = { config.window.signature.height, 'number' },
['window.signature.width'] = { config.window.signature.width, 'number' },
['window.signature.border'] = {
config.window.signature.border,
is_string_or_array,
'(mini.completion) `config.window.signature.border` can be either string or array.',
},
})
return config
end
H.apply_config = function(config)
MiniCompletion.config = config
--stylua: ignore start
H.map('i', config.mappings.force_twostep, '<cmd>lua MiniCompletion.complete_twostage()<cr>', { desc = 'Complete with two-stage' })
H.map('i', config.mappings.force_fallback, '<cmd>lua MiniCompletion.complete_fallback()<cr>', { desc = 'Complete with fallback' })
--stylua: ignore end
if config.set_vim_settings then
-- Don't give ins-completion-menu messages
vim.cmd('set shortmess+=c')
-- More common completion behavior
vim.cmd('set completeopt=menuone,noinsert,noselect')
end
end
H.is_disabled = function() return vim.g.minicompletion_disable == true or vim.b.minicompletion_disable == true end
H.get_config = function(config)
return vim.tbl_deep_extend('force', MiniCompletion.config, vim.b.minicompletion_config or {}, config or {})
end
-- Completion triggers --------------------------------------------------------
H.trigger_twostep = function()
-- Trigger only in Insert mode and if text didn't change after trigger
-- request, unless completion is forced
-- NOTE: check for `text_changed_id` equality is still not 100% solution as
-- there are cases when, for example, `<CR>` is hit just before this check.
-- Because of asynchronous id update and this function call (called after
-- delay), these still match.
local allow_trigger = (vim.fn.mode() == 'i')
and (H.completion.force or (H.completion.text_changed_id == H.text_changed_id))
if not allow_trigger then return end
if H.has_lsp_clients('completionProvider') and H.has_lsp_completion() then
H.trigger_lsp()
elseif H.completion.fallback then
H.trigger_fallback()
end
end
H.trigger_lsp = function()
-- Check for popup visibility is needed to reduce flickering.
-- Possible issue timeline (with 100ms delay with set up LSP):
-- 0ms: Key is pressed.
-- 100ms: LSP is triggered from first key press.
-- 110ms: Another key is pressed.
-- 200ms: LSP callback is processed, triggers complete-function which
-- processes "received" LSP request.
-- 201ms: LSP request is processed, completion is (should be almost
-- immediately) provided, request is marked as "done".
-- 210ms: LSP is triggered from second key press. As previous request is
-- "done", it will once make whole LSP request. Having check for visible
-- popup should prevent here the call to complete-function.
-- When `force` is `true` then presence of popup shouldn't matter.
local no_popup = H.completion.force or (not H.pumvisible())
if no_popup and vim.fn.mode() == 'i' then
local key = H.keys[H.get_config().lsp_completion.source_func]
vim.api.nvim_feedkeys(key, 'n', false)
end
end
H.trigger_fallback = function()
local no_popup = H.completion.force or (not H.pumvisible())
if no_popup and vim.fn.mode() == 'i' then
-- Track from which source is current popup
H.completion.source = 'fallback'
local config = H.get_config()
if type(config.fallback_action) == 'string' then
-- Having `<C-g><C-g>` also (for some mysterious reason) helps to avoid
-- some weird behavior. For example, if `keys = '<C-x><C-l>'` then Neovim
-- starts new line when there is no suggestions.
local keys = string.format('<C-g><C-g>%s', config.fallback_action)
local trigger_keys = vim.api.nvim_replace_termcodes(keys, true, false, true)
vim.api.nvim_feedkeys(trigger_keys, 'n', false)
else
config.fallback_action()
end
end
end
-- Stop actions ---------------------------------------------------------------
H.stop_completion = function(keep_source)
H.completion.timer:stop()
H.cancel_lsp({ H.completion })
H.completion.fallback, H.completion.force = true, false
if not keep_source then H.completion.source = nil end
end
H.stop_info = function()
-- Id update is needed to notify that all previous work is not current
H.info.id = H.info.id + 1
H.info.timer:stop()
H.cancel_lsp({ H.info })
H.close_action_window(H.info)
end
H.stop_signature = function()
H.signature.text = nil
H.signature.timer:stop()
H.cancel_lsp({ H.signature })
H.close_action_window(H.signature)
end
H.stop_actions = {
completion = H.stop_completion,
info = H.stop_info,
signature = H.stop_signature,
}
-- LSP ------------------------------------------------------------------------
---@param capability string|table|nil Server capability (possibly nested
--- supplied via table) to check.
---
---@return boolean Whether at least one LSP client supports `capability`.
---@private
H.has_lsp_clients = function(capability)
local clients = vim.lsp.buf_get_clients()
if vim.tbl_isempty(clients) then return false end
if not capability then return true end
for _, c in pairs(clients) do
local has_capability = H.table_get(c.server_capabilities, capability)
if has_capability then return true end
end
return false
end
H.has_lsp_completion = function()
local func = vim.api.nvim_buf_get_option(0, H.get_config().lsp_completion.source_func)
return func == 'v:lua.MiniCompletion.completefunc_lsp'
end
H.is_lsp_trigger = function(char, type)
local triggers
local providers = {
completion = 'completionProvider',
signature = 'signatureHelpProvider',
}
for _, client in pairs(vim.lsp.buf_get_clients()) do
triggers = H.table_get(client, { 'server_capabilities', providers[type], 'triggerCharacters' })
if vim.tbl_contains(triggers or {}, char) then return true end
end
return false
end
H.cancel_lsp = function(caches)
caches = caches or { H.completion, H.info, H.signature }
for _, c in pairs(caches) do
if vim.tbl_contains({ 'sent', 'received' }, c.lsp.status) then
if c.lsp.cancel_fun then c.lsp.cancel_fun() end
c.lsp.status = 'canceled'
end
c.lsp.result = nil
c.lsp.cancel_fun = nil
end
end
H.process_lsp_response = function(request_result, processor)
if not request_result then return {} end
local res = {}
for client_id, item in pairs(request_result) do
if not item.err and item.result then vim.list_extend(res, processor(item.result, client_id) or {}) end
end
return res
end
H.is_lsp_current = function(cache, id) return cache.lsp.id == id and cache.lsp.status == 'sent' end
-- Completion -----------------------------------------------------------------
-- This is a truncated version of
-- `vim.lsp.util.text_document_completion_list_to_complete_items` which does
-- not filter and sort items.
-- For extra information see 'Response' section:
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion
H.lsp_completion_response_items_to_complete_items = function(items, client_id)
if vim.tbl_count(items) == 0 then return {} end
local res = {}
local docs, info
for _, item in pairs(items) do
-- Documentation info
docs = item.documentation
info = H.table_get(docs, { 'value' })
if not info and type(docs) == 'string' then info = docs end
info = info or ''
table.insert(res, {
word = H.get_completion_word(item),
abbr = item.label,
kind = vim.lsp.protocol.CompletionItemKind[item.kind] or 'Unknown',
menu = item.detail or '',
info = info,
icase = 1,
dup = 1,
empty = 1,
user_data = { nvim = { lsp = { completion_item = item, client_id = client_id } } },
})
end
return res
end
H.get_completion_word = function(item)
-- Completion word (textEdit.newText > insertText > label). This doesn't
-- support snippet expansion.
return H.table_get(item, { 'textEdit', 'newText' }) or item.insertText or item.label or ''
end
H.apply_additional_text_edits = function()
-- Code originally.inspired by https://github.com/neovim/neovim/issues/12310
-- Try to get `additionalTextEdits`. First from 'completionItem/resolve';
-- then - from selected item. The reason for this is inconsistency in how
-- servers provide `additionTextEdits`: on 'textDocument/completion' or
-- 'completionItem/resolve'.
local resolve_data = H.process_lsp_response(H.info.lsp.result, function(response, client_id)
-- Return nested table because this will be a second argument of
-- `vim.list_extend()` and the whole inner table is a target value here.
return { { edits = response.additionalTextEdits, client_id = client_id } }
end)
local edits, client_id
if #resolve_data >= 1 then
edits, client_id = resolve_data[1].edits, resolve_data[1].client_id
else
local lsp_data = H.table_get(vim.v.completed_item, { 'user_data', 'nvim', 'lsp' }) or {}
edits = H.table_get(lsp_data, { 'completion_item', 'additionalTextEdits' })
client_id = lsp_data.client_id
end
if edits == nil then return end
client_id = client_id or 0
-- Use extmark to track relevant cursor postion after text edits
local cur_pos = vim.api.nvim_win_get_cursor(0)
local extmark_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, cur_pos[1] - 1, cur_pos[2], {})
local offset_encoding = vim.lsp.get_client_by_id(client_id).offset_encoding
vim.lsp.util.apply_text_edits(edits, vim.api.nvim_get_current_buf(), offset_encoding)
local extmark_data = vim.api.nvim_buf_get_extmark_by_id(0, H.ns_id, extmark_id, {})
pcall(vim.api.nvim_buf_del_extmark, 0, H.ns_id, extmark_id)
pcall(vim.api.nvim_win_set_cursor, 0, { extmark_data[1] + 1, extmark_data[2] })
end
-- Completion item info -------------------------------------------------------
H.show_info_window = function()
local event = H.info.event
if not event then return end
-- Try first to take lines from LSP request result.
local lines
if H.info.lsp.status == 'received' then
lines = H.process_lsp_response(H.info.lsp.result, function(response)
if not response.documentation then return {} end
local res = vim.lsp.util.convert_input_to_markdown_lines(response.documentation)
return vim.lsp.util.trim_empty_lines(res)
end)
H.info.lsp.status = 'done'
else
lines = H.info_window_lines(H.info.id)
end
-- Don't show anything if there is nothing to show
if not lines or H.is_whitespace(lines) then return end
-- If not already, create a permanent buffer where info will be
-- displayed. For some reason, it is important to have it created not in
-- `setup()` because in that case there is a small flash (which is really a
-- brief open of window at screen top, focus on it, and its close) on the
-- first show of info window.
H.ensure_buffer(H.info, 'MiniCompletion:completion-item-info')
-- Add `lines` to info buffer. Use `wrap_at` to have proper width of
-- 'non-UTF8' section separators.
vim.lsp.util.stylize_markdown(H.info.bufnr, lines, { wrap_at = H.get_config().window.info.width })
-- Compute floating window options
local opts = H.info_window_options()
-- Defer execution because of textlock during `CompleteChanged` event
vim.defer_fn(function()
-- Ensure that window doesn't open when it shouldn't be
if not (H.pumvisible() and vim.fn.mode() == 'i') then return end
H.open_action_window(H.info, opts)
end, 0)
end
H.info_window_lines = function(info_id)
-- Try to use 'info' field of Neovim's completion item
local completed_item = H.table_get(H.info, { 'event', 'completed_item' }) or {}
local text = completed_item.info or ''
if not H.is_whitespace(text) then
-- Use `<text></text>` to be properly processed by `stylize_markdown()`
local lines = { '<text>' }
vim.list_extend(lines, vim.split(text, '\n', false))
table.insert(lines, '</text>')
return lines
end
-- If popup is not from LSP then there is nothing more to do
if H.completion.source ~= 'lsp' then return nil end
-- Try to get documentation from LSP's initial completion result
local lsp_completion_item = H.table_get(completed_item, { 'user_data', 'nvim', 'lsp', 'completion_item' })
-- If there is no LSP's completion item, then there is no point to proceed as
-- it should serve as parameters to LSP request
if not lsp_completion_item then return end
local doc = lsp_completion_item.documentation
if doc then
local lines = vim.lsp.util.convert_input_to_markdown_lines(doc)
return vim.lsp.util.trim_empty_lines(lines)
end
-- Finally, try request to resolve current completion to add documentation
local bufnr = vim.api.nvim_get_current_buf()
local params = lsp_completion_item
local current_id = H.info.lsp.id + 1
H.info.lsp.id = current_id
H.info.lsp.status = 'sent'
local cancel_fun = vim.lsp.buf_request_all(bufnr, 'completionItem/resolve', params, function(result)
-- Don't do anything if there is other LSP request in action
if not H.is_lsp_current(H.info, current_id) then return end
H.info.lsp.status = 'received'
0