start organize

This commit is contained in:
hrsh7th 2023-01-02 12:12:08 +09:00
parent 9efea38255
commit de53527063
19 changed files with 484 additions and 963 deletions

View file

@ -102,7 +102,7 @@ lua <<EOF
})
})
-- Use buffer source for `/` and `?` (if you enabled `native_menu`, this won't work anymore).
-- Use buffer source for `/` and `?`.
cmp.setup.cmdline({ '/', '?' }, {
mapping = cmp.mapping.preset.cmdline(),
sources = {
@ -110,7 +110,7 @@ lua <<EOF
}
})
-- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore).
-- Use cmdline & path source for ':'.
cmp.setup.cmdline(':', {
mapping = cmp.mapping.preset.cmdline(),
sources = cmp.config.sources({

View file

@ -42,8 +42,7 @@ Usage *cmp-usage*
A recommended configuration can be found below.
NOTE:
1. You must provide a `snippet.expand` function.
2. `cmp.setup.cmdline` won't work if you use the `native` completion menu.
3. You can disable the `default` options by specifying `cmp.config.disable` value.
2. You can disable the `default` options by specifying `cmp.config.disable` value.
>vim
call plug#begin(s:plug_dir)
Plug 'neovim/nvim-lspconfig'
@ -596,15 +595,10 @@ sources[n].entry_filter~
Using the `ctx` parameter, you can further customize the behaviour of the
source.
*cmp-config.view*
view~
`{ entries: cmp.CustomEntriesConfig|NativeEntriesConfig|cmp.WildmenuEntriesConfig|string }`
The view class used to customize nvim-cmp's appearance.
*cmp-config.window.{completion,documentation}.border*
window.{completion,documentation}.border~
`string | string[] | nil`
Border characters used for the completion popup menu when |experimental.native_menu| is disabled.
Border characters used for the completion popup menu.
See |nvim_open_win|.
*cmp-config.window.{completion,documentation}.winhighlight*

View file

@ -158,16 +158,7 @@ config.get_source_config = function(name)
return s
end
end
error('Specified source is not found: ' .. name)
end
---Return the current menu is native or not.
config.is_native_menu = function()
local c = config.get()
if c.view and c.view.entries then
return c.view.entries == 'native' or c.view.entries.name == 'native'
end
return false
return {}
end
---Normalize mapping key
@ -187,21 +178,6 @@ config.normalize = function(c)
c.mapping = normalized
end
-- Notice experimental.native_menu.
if c.experimental and c.experimental.native_menu then
echo({
'[nvim-cmp] ',
{ 'experimental.native_menu', 'WarningMsg' },
' is deprecated.\n',
'[nvim-cmp] Please use ',
{ 'view.entries = "native"', 'WarningMsg' },
' instead.',
})
c.view = c.view or {}
c.view.entries = c.view.entries or 'native'
end
-- Notice documentation.
if c.documentation ~= nil then
echo({

View file

@ -1,6 +1,6 @@
local types = require('cmp.types')
local misc = require('cmp.utils.misc')
local feedkeys = require('cmp.utils.feedkeys')
local Keymap = require('cmp.kit.Vim.Keymap')
local keymap = require('cmp.utils.keymap')
local function merge_keymaps(base, override)
@ -66,7 +66,7 @@ mapping.preset.cmdline = function(override)
if cmp.visible() then
cmp.select_next_item()
else
feedkeys.call(keymap.t('<C-z>'), 'n')
Keymap.send(keymap.t('<C-z>'), 'n')
end
end,
},
@ -76,7 +76,7 @@ mapping.preset.cmdline = function(override)
if cmp.visible() then
cmp.select_prev_item()
else
feedkeys.call(keymap.t('<C-z>'), 'n')
Keymap.send(keymap.t('<C-z>'), 'n')
end
end,
},
@ -154,22 +154,24 @@ end
---Select next completion item.
mapping.select_next_item = function(option)
local cmp = require('cmp')
return function(fallback)
if not require('cmp').select_next_item(option) then
local release = require('cmp').core:suspend()
if cmp.visible() then
cmp.select_next_item(option):await()
else
fallback()
vim.schedule(release)
end
end
end
---Select prev completion item.
mapping.select_prev_item = function(option)
local cmp = require('cmp')
return function(fallback)
if not require('cmp').select_prev_item(option) then
local release = require('cmp').core:suspend()
if cmp.visible() then
cmp.select_prev_item(option):await()
else
fallback()
vim.schedule(release)
end
end
end
@ -177,7 +179,7 @@ end
---Confirm selection
mapping.confirm = function(option)
return function(fallback)
if not require('cmp').confirm(option) then
if not require('cmp').confirm(option):await() then
fallback()
end
end

View file

@ -2,7 +2,6 @@ local debug = require('cmp.utils.debug')
local str = require('cmp.utils.str')
local char = require('cmp.utils.char')
local pattern = require('cmp.utils.pattern')
local feedkeys = require('cmp.utils.feedkeys')
local async = require('cmp.utils.async')
local keymap = require('cmp.utils.keymap')
local context = require('cmp.context')
@ -13,6 +12,8 @@ local config = require('cmp.config')
local types = require('cmp.types')
local api = require('cmp.utils.api')
local event = require('cmp.utils.event')
local Keymap = require('cmp.kit.Vim.Keymap')
local AsyncTask = require('cmp.kit.Async.AsyncTask')
---@class cmp.Core
---@field public suspending boolean
@ -73,10 +74,12 @@ end
---Suspend completion
core.suspend = function(self)
self.suspending = true
-- It's needed to avoid conflicting with autocmd debouncing.
return vim.schedule_wrap(function()
-- Basically used to skip unexpected TextChanged/CmdlineChanged autocmd.
-- 1. The autocmd fires after script processing, so schedule is necessary
-- 2. The CmdlineChanged uses schedule for debouncing, so schedule is needed here additionally.
return vim.schedule_wrap(vim.schedule_wrap(function()
self.suspending = false
end)
end))
end
---Get sources that sorted by priority
@ -125,7 +128,7 @@ core.on_keymap = function(self, keys, fallback)
}, function()
local ctx = self:get_context()
local word = e:get_word()
if string.sub(ctx.cursor_before_line, -#word, ctx.cursor.col - 1) == word and is_printable then
if string.sub(ctx.cursor_before_line, - #word, ctx.cursor.col - 1) == word and is_printable then
fallback()
else
self:reset()
@ -220,9 +223,10 @@ core.autoindent = function(self, trigger_event, callback)
end
---Complete common string for current completed entries.
---@return cmp.kit.Async.AsyncTask boolean
core.complete_common_string = function(self)
if not self.view:visible() or self.view:get_selected_entry() then
return false
return AsyncTask.resolve(false)
end
config.set_onetime({
@ -252,11 +256,14 @@ core.complete_common_string = function(self)
end
local cursor_before_line = api.get_cursor_before_line()
local pretext = cursor_before_line:sub(offset)
if common_string and #common_string > #pretext then
feedkeys.call(keymap.backspace(pretext) .. common_string, 'n')
return true
end
return false
return AsyncTask.resolve():next(function()
if common_string and #common_string > #pretext then
return Keymap.send(keymap.backspace(pretext) .. common_string, 'ni'):next(function()
return true
end)
end
return false
end)
end
---Invoke completion
@ -341,13 +348,10 @@ end, config.get().performance.throttle)
---Confirm completion.
---@param e cmp.Entry
---@param option cmp.ConfirmOption
---@param callback function
core.confirm = function(self, e, option, callback)
---@return cmp.kit.Async.AsyncTask
core.confirm = function(self, e, option)
if not (e and not e.confirmed) then
if callback then
callback()
end
return
return AsyncTask.resolve()
end
e.confirmed = true
@ -358,143 +362,146 @@ core.confirm = function(self, e, option, callback)
-- Close menus.
self.view:close()
feedkeys.call(keymap.indentkeys(), 'n')
feedkeys.call('', 'n', function()
-- Emulate `<C-y>` behavior to save `.` register.
local ctx = context.new()
local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset())))
table.insert(keys, e:get_word())
table.insert(keys, keymap.undobreak())
feedkeys.call(table.concat(keys, ''), 'in')
end)
feedkeys.call('', 'n', function()
-- Restore the line at the time of request.
local ctx = context.new()
if api.is_cmdline_mode() then
local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset())))
table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset()))
feedkeys.call(table.concat(keys, ''), 'in')
else
vim.cmd([[silent! undojoin]])
-- This logic must be used nvim_buf_set_text.
-- If not used, the snippet engine's placeholder wil be broken.
vim.api.nvim_buf_set_text(0, e.context.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, {
e.context.cursor_before_line:sub(e:get_offset()),
})
vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 })
end
end)
feedkeys.call('', 'n', function()
-- Apply additionalTextEdits.
local ctx = context.new()
if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then
e:resolve(function()
local new = context.new()
local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {}
if #text_edits == 0 then
return
end
local has_cursor_line_text_edit = (function()
local minrow = math.min(ctx.cursor.row, new.cursor.row)
local maxrow = math.max(ctx.cursor.row, new.cursor.row)
for _, te in ipairs(text_edits) do
local srow = te.range.start.line + 1
local erow = te.range['end'].line + 1
if srow <= minrow and maxrow <= erow then
return true
end
end
return false
end)()
if has_cursor_line_text_edit then
return
end
vim.cmd([[silent! undojoin]])
vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, e.source:get_position_encoding_kind())
local indentkeys = vim.bo.indentkeys
return AsyncTask.resolve()
:next(function()
return Keymap.send(keymap.indentkeys(), 'ni')
end)
else
vim.cmd([[silent! undojoin]])
vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, e.source:get_position_encoding_kind())
end
end)
feedkeys.call('', 'n', function()
local ctx = context.new()
local completion_item = misc.copy(e:get_completion_item())
if not misc.safe(completion_item.textEdit) then
completion_item.textEdit = {}
completion_item.textEdit.newText = misc.safe(completion_item.insertText) or completion_item.word or completion_item.label
end
local behavior = option.behavior or config.get().confirmation.default_behavior
if behavior == types.cmp.ConfirmBehavior.Replace then
completion_item.textEdit.range = e:get_replace_range()
else
completion_item.textEdit.range = e:get_insert_range()
end
:next(function()
-- Emulate `<C-y>` behavior to save `.` register.
local ctx = context.new()
local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset())))
table.insert(keys, e:get_word())
table.insert(keys, keymap.undobreak())
return Keymap.send(table.concat(keys, ''), 'ni')
end)
:next(function()
-- Restore the line at the time of request.
local ctx = context.new()
if api.is_cmdline_mode() then
local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset())))
table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset()))
return Keymap.send(table.concat(keys, ''), 'ni')
else
vim.cmd([[silent! undojoin]])
-- This logic must be used nvim_buf_set_text.
-- If not used, the snippet engine's placeholder wil be broken.
vim.api.nvim_buf_set_text(0, e.context.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, {
e.context.cursor_before_line:sub(e:get_offset()),
})
vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 })
end
end)
:next(function()
-- Apply additionalTextEdits.
local ctx = context.new()
if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then
e:resolve(function()
local new = context.new()
local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {}
if #text_edits == 0 then
return
end
local diff_before = math.max(0, e.context.cursor.col - (completion_item.textEdit.range.start.character + 1))
local diff_after = math.max(0, (completion_item.textEdit.range['end'].character + 1) - e.context.cursor.col)
local new_text = completion_item.textEdit.newText
completion_item.textEdit.range.start.line = ctx.cursor.line
completion_item.textEdit.range.start.character = (ctx.cursor.col - 1) - diff_before
completion_item.textEdit.range['end'].line = ctx.cursor.line
completion_item.textEdit.range['end'].character = (ctx.cursor.col - 1) + diff_after
if api.is_insert_mode() then
if false then
--To use complex expansion debug.
vim.pretty_print({ -- luacheck: ignore
diff_before = diff_before,
diff_after = diff_after,
new_text = new_text,
text_edit_new_text = completion_item.textEdit.newText,
range_start = completion_item.textEdit.range.start.character,
range_end = completion_item.textEdit.range['end'].character,
original_range_start = e:get_completion_item().textEdit.range.start.character,
original_range_end = e:get_completion_item().textEdit.range['end'].character,
cursor_line = ctx.cursor_line,
cursor_col0 = ctx.cursor.col - 1,
})
end
local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet
if is_snippet then
completion_item.textEdit.newText = ''
end
vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-8')
local has_cursor_line_text_edit = (function()
local minrow = math.min(ctx.cursor.row, new.cursor.row)
local maxrow = math.max(ctx.cursor.row, new.cursor.row)
for _, te in ipairs(text_edits) do
local srow = te.range.start.line + 1
local erow = te.range['end'].line + 1
if srow <= minrow and maxrow <= erow then
return true
end
end
return false
end)()
if has_cursor_line_text_edit then
return
end
vim.cmd([[silent! undojoin]])
vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, e.source:get_position_encoding_kind())
end)
else
vim.cmd([[silent! undojoin]])
vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, e.source:get_position_encoding_kind())
end
end)
:next(function()
local ctx = context.new()
local completion_item = misc.copy(e:get_completion_item())
if not misc.safe(completion_item.textEdit) then
completion_item.textEdit = {}
completion_item.textEdit.newText = misc.safe(completion_item.insertText) or completion_item.word or completion_item.label
end
local behavior = option.behavior or config.get().confirmation.default_behavior
if behavior == types.cmp.ConfirmBehavior.Replace then
completion_item.textEdit.range = e:get_replace_range()
else
completion_item.textEdit.range = e:get_insert_range()
end
local texts = vim.split(completion_item.textEdit.newText, '\n')
vim.api.nvim_win_set_cursor(0, {
completion_item.textEdit.range.start.line + #texts,
(#texts == 1 and (completion_item.textEdit.range.start.character + #texts[1]) or #texts[#texts]),
})
if is_snippet then
config.get().snippet.expand({
body = new_text,
insert_text_mode = completion_item.insertTextMode,
})
end
else
local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor_line:sub(completion_item.textEdit.range.start.character + 1, ctx.cursor.col - 1)))
table.insert(keys, keymap.delete(ctx.cursor_line:sub(ctx.cursor.col, completion_item.textEdit.range['end'].character)))
table.insert(keys, new_text)
feedkeys.call(table.concat(keys, ''), 'in')
end
end)
feedkeys.call(keymap.indentkeys(vim.bo.indentkeys), 'n')
feedkeys.call('', 'n', function()
e:execute(vim.schedule_wrap(function()
release()
self.event:emit('confirm_done', {
entry = e,
commit_character = option.commit_character,
})
if callback then
callback()
end
end))
end)
local diff_before = math.max(0, e.context.cursor.col - (completion_item.textEdit.range.start.character + 1))
local diff_after = math.max(0, (completion_item.textEdit.range['end'].character + 1) - e.context.cursor.col)
local new_text = completion_item.textEdit.newText
completion_item.textEdit.range.start.line = ctx.cursor.line
completion_item.textEdit.range.start.character = (ctx.cursor.col - 1) - diff_before
completion_item.textEdit.range['end'].line = ctx.cursor.line
completion_item.textEdit.range['end'].character = (ctx.cursor.col - 1) + diff_after
if api.is_insert_mode() then
if false then
--To use complex expansion debug.
vim.pretty_print({ -- luacheck: ignore
diff_before = diff_before,
diff_after = diff_after,
new_text = new_text,
text_edit_new_text = completion_item.textEdit.newText,
range_start = completion_item.textEdit.range.start.character,
range_end = completion_item.textEdit.range['end'].character,
original_range_start = e:get_completion_item().textEdit.range.start.character,
original_range_end = e:get_completion_item().textEdit.range['end'].character,
cursor_line = ctx.cursor_line,
cursor_col0 = ctx.cursor.col - 1,
})
end
local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet
if is_snippet then
completion_item.textEdit.newText = ''
end
vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-8')
local texts = vim.split(completion_item.textEdit.newText, '\n')
vim.api.nvim_win_set_cursor(0, {
completion_item.textEdit.range.start.line + #texts,
(#texts == 1 and (completion_item.textEdit.range.start.character + #texts[1]) or #texts[#texts]),
})
if is_snippet then
config.get().snippet.expand({
body = new_text,
insert_text_mode = completion_item.insertTextMode,
})
end
else
local keys = {}
table.insert(keys, keymap.backspace(ctx.cursor_line:sub(completion_item.textEdit.range.start.character + 1, ctx.cursor.col - 1)))
table.insert(keys, keymap.delete(ctx.cursor_line:sub(ctx.cursor.col, completion_item.textEdit.range['end'].character)))
table.insert(keys, new_text)
return Keymap.send(table.concat(keys, ''), 'ni')
end
end)
:next(function()
return Keymap.send(keymap.indentkeys(indentkeys), 'ni')
end)
:next(function()
e:execute(vim.schedule_wrap(function()
release()
self.event:emit('confirm_done', {
entry = e,
commit_character = option.commit_character,
})
end))
end)
end
---Reset current completion state

View file

@ -1,10 +1,11 @@
local spec = require('cmp.utils.spec')
local feedkeys = require('cmp.utils.feedkeys')
local types = require('cmp.types')
local core = require('cmp.core')
local source = require('cmp.source')
local keymap = require('cmp.utils.keymap')
local api = require('cmp.utils.api')
local Keymap = require('cmp.kit.Vim.Keymap')
local Async = require('cmp.kit.Async')
describe('cmp.core', function()
describe('confirm', function()
@ -18,7 +19,7 @@ describe('cmp.core', function()
local c = core.new()
local s = source.new('spec', {
get_position_encoding_kind = function()
get_position_encoding_kind = function(_)
return option.position_encoding_kind or types.lsp.PositionEncodingKind.UTF16
end,
complete = function(_, _, callback)
@ -26,26 +27,22 @@ describe('cmp.core', function()
end,
})
c:register_source(s)
feedkeys.call(request, 'n', function()
local state = {}
Keymap.spec(Async.async(function()
Keymap.send(request, 'n'):await()
c:complete(c:get_context({ reason = types.cmp.ContextReason.Manual }))
vim.wait(5000, function()
return #c.sources[s.id].entries > 0
end)
end)
feedkeys.call(filter, 'n', function()
c:confirm(c.sources[s.id].entries[1], {}, function() end)
end)
local state = {}
feedkeys.call('', 'x', function()
feedkeys.call('', 'n', function()
if api.is_cmdline_mode() then
state.buffer = { api.get_current_line() }
else
state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false)
end
state.cursor = api.get_cursor()
end)
end)
Keymap.send(filter, 'n'):await()
c:confirm(c.sources[s.id].entries[1], {}, function() end):await()
if api.is_cmdline_mode() then
state.buffer = { api.get_current_line() }
else
state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false)
end
state.cursor = api.get_cursor()
end))
return state
end

View file

@ -1,11 +1,12 @@
local core = require('cmp.core')
local source = require('cmp.source')
local config = require('cmp.config')
local feedkeys = require('cmp.utils.feedkeys')
local autocmd = require('cmp.utils.autocmd')
local keymap = require('cmp.utils.keymap')
local misc = require('cmp.utils.misc')
local async = require('cmp.utils.async')
local Keymap = require('cmp.kit.Vim.Keymap')
local AsyncTask = require('cmp.kit.Async.AsyncTask')
local cmp = {}
@ -32,7 +33,10 @@ cmp.config.sources = require('cmp.config.sources')
cmp.config.mapping = require('cmp.config.mapping')
cmp.config.window = require('cmp.config.window')
---Sync asynchronous process.
---Sync waiting filter process.
---@generic T: fun(...: any[]): any
---@param callback T
---@return T
cmp.sync = function(callback)
return function(...)
cmp.core.filter:sync(1000)
@ -43,13 +47,14 @@ cmp.sync = function(callback)
end
---Suspend completion.
---@return fun()
cmp.suspend = function()
return cmp.core:suspend()
end
---Register completion sources
---@param name string
---@param s cmp.Source
---@param s cmp.CustomSource
---@return integer
cmp.register_source = function(name, s)
local src = source.new(name, s)
@ -75,144 +80,139 @@ cmp.complete = cmp.sync(function(option)
option = option or {}
config.set_onetime(option.config)
cmp.core:complete(cmp.core:get_context({ reason = option.reason or cmp.ContextReason.Manual }))
return true
end)
---Complete common string in current entries.
---@return cmp.kit.Async.AsyncTask boolean
cmp.complete_common_string = cmp.sync(function()
return cmp.core:complete_common_string()
end)
---Return view is visible or not.
---@return boolean
cmp.visible = cmp.sync(function()
return cmp.core.view:visible() or vim.fn.pumvisible() == 1
end)
---Get current selected entry or nil
---@return cmp.Entry?
cmp.get_selected_entry = cmp.sync(function()
return cmp.core.view:get_selected_entry()
end)
---Get current active entry or nil
---@return cmp.Entry?
cmp.get_active_entry = cmp.sync(function()
return cmp.core.view:get_active_entry()
end)
---Get current all entries
---@return cmp.Entry[]
cmp.get_entries = cmp.sync(function()
return cmp.core.view:get_entries()
end)
---Close current completion
---@return cmp.kit.Async.AsyncTask
cmp.close = cmp.sync(function()
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:close()
vim.schedule(release)
return true
else
return false
end
return AsyncTask.resolve():next(function()
if cmp.core.view:visible() then
return cmp.core.view:close()
end
end)
end)
---Abort current completion
---@return cmp.kit.Async.AsyncTask
cmp.abort = cmp.sync(function()
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:abort()
vim.schedule(release)
return true
else
return false
end
return AsyncTask.resolve():next(function()
if cmp.core.view:visible() then
return cmp.core.view:abort():next(cmp.core:suspend())
end
end)
end)
---Select next item if possible
---@param option? cmp.SelectOption
---@return cmp.kit.Async.AsyncTask
cmp.select_next_item = cmp.sync(function(option)
option = option or {}
option.behavior = option.behavior or cmp.SelectBehavior.Insert
option.count = option.count or 1
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:select_next_item(option)
vim.schedule(release)
return true
elseif vim.fn.pumvisible() == 1 then
if option.behavior == cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t(string.rep('<C-n>', option.count)), 'in')
else
feedkeys.call(keymap.t(string.rep('<Down>', option.count)), 'in')
return AsyncTask.resolve():next(function()
if cmp.core.view:visible() then
return cmp.core.view:select_next_item(option):next(cmp.core:suspend())
elseif vim.fn.pumvisible() == 1 then
if option.behavior == cmp.SelectBehavior.Insert then
return Keymap.send(keymap.t(string.rep('<C-n>', option.count)), 'in')
else
return Keymap.send(keymap.t(string.rep('<Down>', option.count)), 'in')
end
end
return true
end
return false
end)
end)
---Select prev item if possible
---@param option? cmp.SelectOption
---@return cmp.kit.Async.AsyncTask
cmp.select_prev_item = cmp.sync(function(option)
option = option or {}
option.behavior = option.behavior or cmp.SelectBehavior.Insert
option.count = option.count or 1
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:select_prev_item(option)
vim.schedule(release)
return true
elseif vim.fn.pumvisible() == 1 then
if option.behavior == cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t(string.rep('<C-p>', option.count)), 'in')
else
feedkeys.call(keymap.t(string.rep('<Up>', option.count)), 'in')
return AsyncTask.resolve():next(function()
if cmp.core.view:visible() then
return cmp.core.view:select_prev_item(option):next(cmp.core:suspend())
elseif vim.fn.pumvisible() == 1 then
if option.behavior == cmp.SelectBehavior.Insert then
return Keymap.send(keymap.t(string.rep('<C-p>', option.count)), 'in')
else
return Keymap.send(keymap.t(string.rep('<Up>', option.count)), 'in')
end
end
return true
end
return false
end)
end)
---Scrolling documentation window if possible
---@param delta integer
cmp.scroll_docs = cmp.sync(function(delta)
if cmp.core.view.docs_view:visible() then
cmp.core.view:scroll_docs(delta)
return true
else
return false
end
end)
---Confirm completion
cmp.confirm = cmp.sync(function(option, callback)
---@param option? cmp.ConfirmOption
---@return cmp.kit.Async.AsyncTask boolean
cmp.confirm = cmp.sync(function(option)
option = option or {}
option.select = option.select or false
option.behavior = option.behavior or cmp.get_config().confirmation.default_behavior or cmp.ConfirmBehavior.Insert
callback = callback or function() end
if cmp.core.view:visible() then
local e = cmp.core.view:get_selected_entry()
if not e and option.select then
e = cmp.core.view:get_first_entry()
return AsyncTask.resolve():next(function()
if cmp.core.view:visible() then
local e = cmp.core.view:get_selected_entry()
if not e and option.select then
e = cmp.core.view:get_first_entry()
end
if e then
return cmp.core:confirm(e, {
behavior = option.behavior,
}):next(function()
cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly }))
end)
end
elseif vim.fn.pumvisible() == 1 then
local index = vim.fn.complete_info({ 'selected' }).selected
if index == -1 and option.select then
index = 0
end
if index ~= -1 then
vim.api.nvim_select_popupmenu_item(index, true, true, {})
end
end
if e then
cmp.core:confirm(e, {
behavior = option.behavior,
}, function()
callback()
cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly }))
end)
return true
end
elseif vim.fn.pumvisible() == 1 then
local index = vim.fn.complete_info({ 'selected' }).selected
if index == -1 and option.select then
index = 0
end
if index ~= -1 then
vim.api.nvim_select_popupmenu_item(index, true, true, {})
return true
end
end
return false
end)
end)
---Show status
@ -277,25 +277,32 @@ end
---@type cmp.Setup
cmp.setup = setmetatable({
---@param c cmp.ConfigSchema
global = function(c)
config.set_global(c)
end,
---@param filetype string
---@param c cmp.ConfigSchema
filetype = function(filetype, c)
config.set_filetype(c, filetype)
end,
---@param c cmp.ConfigSchema
buffer = function(c)
config.set_buffer(c, vim.api.nvim_get_current_buf())
end,
---@param type ':'|'/'|'?'
---@param c cmp.ConfigSchema
cmdline = function(type, c)
config.set_cmdline(c, type)
end,
}, {
---@param self unknown
---@param c cmp.ConfigSchema
__call = function(self, c)
self.global(c)
end,
})
-- In InsertEnter autocmd, vim will detects mode=normal unexpectedly.
local on_insert_enter = function()
if config.enabled() then
cmp.config.compare.scopes:update()
@ -304,16 +311,17 @@ local on_insert_enter = function()
cmp.core:on_change('InsertEnter')
end
end
autocmd.subscribe({ 'CmdlineEnter' }, async.debounce_next_tick(on_insert_enter))
autocmd.subscribe({ 'InsertEnter' }, async.debounce_next_tick_by_keymap(on_insert_enter))
autocmd.subscribe({ 'InsertEnter' }, async.debounce_next_tick_by_keymap(on_insert_enter)) -- Debouncing is needed to solve InsertEnter's mode problem.
autocmd.subscribe({ 'CmdlineEnter' }, on_insert_enter)
-- async.throttle is needed for performance. The mapping `:<C-u>...<CR>` will fire `CmdlineChanged` for each character.
local on_text_changed = function()
if config.enabled() then
cmp.core:on_change('TextChanged')
end
end
autocmd.subscribe({ 'TextChangedI', 'TextChangedP' }, on_text_changed)
-- async.debounce_next_tick is needed for performance. The mapping `:<C-u>...<CR>` will fire `CmdlineChanged` for each character.
-- I don't know why but 'We can't use `async.debounce_next_tick_by_keymap` here.
autocmd.subscribe('CmdlineChanged', async.debounce_next_tick(on_text_changed))
autocmd.subscribe('CursorMovedI', function()
@ -325,6 +333,7 @@ autocmd.subscribe('CursorMovedI', function()
end
end)
-- The follwoing autocmds must not be debounced.
-- If make this asynchronous, the completion menu will not close when the command output is displayed.
autocmd.subscribe({ 'InsertLeave', 'CmdlineLeave' }, function()
cmp.core:reset()

View file

@ -174,6 +174,15 @@ function AsyncTask:await(schedule)
return res
end
---Return current state of task.
---@return { status: cmp.kit.Async.AsyncTask.Status, value: any }
function AsyncTask:state()
return {
status = self.status,
value = self.value,
}
end
---Register next step.
---@param on_fulfilled fun(value: any): any
function AsyncTask:next(on_fulfilled)

View file

@ -2,7 +2,7 @@ local AsyncTask = require('cmp.kit.Async.AsyncTask')
local Async = {}
---@type string<thread, integer>
---@type table<thread, integer>
Async.___threads___ = {}
---Run async function immediately.
@ -14,6 +14,12 @@ function Async.run(runner, ...)
return Async.async(runner)(...)
end
---Return current context is async coroutine or not.
---@return boolean
function Async.in_context()
return Async.___threads___[coroutine.running()] ~= nil
end
---Create async function.
---@generic T: fun(...): cmp.kit.Async.AsyncTask
---@param runner T

View file

@ -103,7 +103,6 @@ cmp.ItemField = {
---@field public snippet cmp.SnippetConfig
---@field public mapping table<string, cmp.Mapping>
---@field public sources cmp.SourceConfig[]
---@field public view cmp.ViewConfig
---@field public experimental cmp.ExperimentalConfig
---@class cmp.PerformanceConfig
@ -182,18 +181,4 @@ cmp.ItemField = {
---@field public resolve fun(completion_item: lsp.CompletionItem, callback: fun(completion_item: lsp.CompletionItem), resolve: fun(completion_item: lsp.CompletionItem, callback: fun(completion_item: lsp.CompletionItem)))
---@field public execute fun(completion_item: lsp.CompletionItem, callback: fun(), execute: fun(completion_item: lsp.CompletionItem, callback: fun()))
---@class cmp.ViewConfig
---@field public entries cmp.CustomEntriesConfig|cmp.NativeEntriesConfig|cmp.WildmenuEntriesConfig|string
---@class cmp.CustomEntriesConfig
---@field name 'custom'
---@field selection_order 'top_down'|'near_cursor'
---@class cmp.NativeEntriesConfig
---@field name 'native'
---@class cmp.WildmenuEntriesConfig
---@field name 'wildmenu'
---@field separator string|nil
return cmp

View file

@ -1,64 +1,53 @@
local spec = require('cmp.utils.spec')
local keymap = require('cmp.utils.keymap')
local feedkeys = require('cmp.utils.feedkeys')
local Keymap = require('cmp.kit.Vim.Keymap')
local Async = require('cmp.kit.Async')
local api = require('cmp.utils.api')
describe('api', function()
before_each(spec.before)
describe('get_cursor', function()
it('insert-mode', function()
local cursor
feedkeys.call(keymap.t('i\t1234567890'), 'nx', function()
cursor = api.get_cursor()
end)
assert.are.equal(cursor[2], 11)
Keymap.spec(Async.async(function()
Keymap.send(keymap.t('i\t1234567890'), 'n'):await()
assert.are.equal(api.get_cursor()[2], 11)
end))
end)
it('cmdline-mode', function()
local cursor
keymap.set_map(0, 'c', '<Plug>(cmp-spec-spy)', function()
cursor = api.get_cursor()
end, { expr = true, noremap = true })
feedkeys.call(keymap.t(':\t1234567890'), 'n')
feedkeys.call(keymap.t('<Plug>(cmp-spec-spy)'), 'x')
assert.are.equal(cursor[2], 11)
Keymap.spec(Async.async(function()
Keymap.send(keymap.t(':\t1234567890'), 'n'):await()
assert.are.equal(api.get_cursor()[2], 11)
end))
end)
end)
describe('get_screen_cursor', function()
it('insert-mode', function()
local screen_cursor
feedkeys.call(keymap.t('iあいうえお'), 'nx', function()
screen_cursor = api.get_screen_cursor()
end)
assert.are.equal(10, screen_cursor[2])
Keymap.spec(Async.async(function()
Keymap.send(keymap.t('iあいうえお'), 'n'):await()
assert.are.equal(api.get_screen_cursor()[2], 10)
end))
end)
it('cmdline-mode', function()
local screen_cursor
keymap.set_map(0, 'c', '<Plug>(cmp-spec-spy)', function()
screen_cursor = api.get_screen_cursor()
end, { expr = true, noremap = true })
feedkeys.call(keymap.t(':あいうえお'), 'n')
feedkeys.call(keymap.t('<Plug>(cmp-spec-spy)'), 'x')
assert.are.equal(10, screen_cursor[2])
Keymap.spec(Async.async(function()
Keymap.send(keymap.t(':あいうえお'), 'n'):await()
assert.are.equal(10, api.get_screen_cursor()[2])
end))
end)
end)
describe('get_cursor_before_line', function()
it('insert-mode', function()
local cursor_before_line
feedkeys.call(keymap.t('i\t1234567890<Left><Left>'), 'nx', function()
cursor_before_line = api.get_cursor_before_line()
end)
assert.are.same(cursor_before_line, '\t12345678')
Keymap.spec(Async.async(function()
Keymap.send(keymap.t('i\t1234567890<Left><Left>'), 'n'):await()
assert.are.same(api.get_cursor_before_line(), '\t12345678')
end))
end)
it('cmdline-mode', function()
local cursor_before_line
keymap.set_map(0, 'c', '<Plug>(cmp-spec-spy)', function()
cursor_before_line = api.get_cursor_before_line()
end, { expr = true, noremap = true })
feedkeys.call(keymap.t(':\t1234567890<Left><Left>'), 'n')
feedkeys.call(keymap.t('<Plug>(cmp-spec-spy)'), 'x')
assert.are.same(cursor_before_line, '\t12345678')
Keymap.spec(Async.async(function()
Keymap.send(keymap.t(':\t1234567890<Left><Left>'), 'n'):await()
assert.are.same(api.get_cursor_before_line(), '\t12345678')
end))
end)
end)
end)

View file

@ -1,4 +1,5 @@
local feedkeys = require('cmp.utils.feedkeys')
local Keymap = require('cmp.kit.Vim.Keymap')
local AsyncTask = require('cmp.kit.Async.AsyncTask')
local async = {}
@ -142,8 +143,14 @@ end
---Wait and callback for consuming next keymap.
async.debounce_next_tick_by_keymap = function(callback)
local queue = nil
return function()
feedkeys.call('', '', callback)
if queue then
return
end
queue = Keymap.send('', 'n'):next(callback):next(function()
queue = nil
end)
end
end

View file

@ -1,6 +1,7 @@
local misc = require('cmp.utils.misc')
local buffer = require('cmp.utils.buffer')
local api = require('cmp.utils.api')
local Async = require('cmp.kit.Async')
local keymap = {}
@ -31,8 +32,6 @@ keymap.normalize = function(keys)
end
---Return vim notation keymapping (simple conversion).
---@param s string
---@return string
keymap.to_keymap = setmetatable({
['<CR>'] = { '\n', '\r', '\r\n' },
['<Tab>'] = { '\t' },
@ -40,15 +39,17 @@ keymap.to_keymap = setmetatable({
['<Bar>'] = { '|' },
['<Space>'] = { ' ' },
}, {
---@param s string
---@return string
__call = function(self, s)
return string.gsub(s, '.', function(c)
return (string.gsub(s, '.', function(c)
for key, chars in pairs(self) do
if vim.tbl_contains(chars, c) then
return key
end
end
return c
end)
end))
end,
})
@ -124,15 +125,18 @@ keymap.listen = function(mode, lhs, callback)
local bufnr = existing.buffer and vim.api.nvim_get_current_buf() or -1
local fallback = keymap.fallback(bufnr, mode, existing)
keymap.set_map(bufnr, mode, lhs, function()
keymap.set_map(bufnr, mode, lhs, Async.async(function()
vim.pretty_print('mapping', lhs)
local ignore = false
ignore = ignore or (mode == 'c' and vim.fn.getcmdtype() == '=')
if ignore then
vim.pretty_print('ignore', lhs)
fallback()
else
vim.pretty_print('call callback', lhs)
callback(lhs, misc.once(fallback))
end
end, {
end), {
expr = false,
noremap = true,
silent = true,
@ -207,6 +211,7 @@ keymap.get_map = function(mode, lhs)
lhs = map.lhs,
rhs = map.rhs or '',
expr = map.expr == 1,
desc = map.desc,
callback = map.callback,
noremap = map.noremap == 1,
script = map.script == 1,
@ -224,6 +229,7 @@ keymap.get_map = function(mode, lhs)
lhs = map.lhs,
rhs = map.rhs or '',
expr = map.expr == 1,
desc = map.desc,
callback = map.callback,
noremap = map.noremap == 1,
script = map.script == 1,
@ -239,6 +245,7 @@ keymap.get_map = function(mode, lhs)
lhs = lhs,
rhs = lhs,
expr = false,
desc = nil,
callback = nil,
noremap = true,
script = false,

View file

@ -1,6 +1,7 @@
local spec = require('cmp.utils.spec')
local api = require('cmp.utils.api')
local feedkeys = require('cmp.utils.feedkeys')
local Keymap = require('cmp.kit.Vim.Keymap')
local Async = require('cmp.kit.Async')
local keymap = require('cmp.utils.keymap')
@ -39,18 +40,17 @@ describe('keymap', function()
local run_fallback = function(keys, fallback)
local state = {}
feedkeys.call(keys, '', function()
Keymap.spec(Async.async(function()
Keymap.send(keys, ''):await()
fallback()
end)
feedkeys.call('', '', function()
Keymap.send('', ''):await()
if api.is_cmdline_mode() then
state.buffer = { api.get_current_line() }
else
state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false)
end
state.cursor = api.get_cursor()
end)
feedkeys.call('', 'x')
end))
return state
end
@ -180,7 +180,7 @@ describe('keymap', function()
keymap.listen('i', '<CR>', function(_, fallback)
fallback()
end)
feedkeys.call(keymap.t('i<CR>'), 'tx')
Keymap.send(keymap.t('i<CR>'), 'x')
assert.are.same({ '', 'recursive' }, vim.api.nvim_buf_get_lines(0, 0, -1, true))
end)
end)

View file

@ -99,7 +99,11 @@ window.get_buffer = function(self)
local buf, created_new = buffer.ensure(self.name)
if created_new then
for k, v in pairs(self.buffer_opt) do
vim.api.nvim_buf_set_option(buf, k, v)
pcall(function()
vim.api.nvim_set_option_value(k, v, {
buf = buf
})
end)
end
end
return buf

View file

@ -3,18 +3,15 @@ local async = require('cmp.utils.async')
local event = require('cmp.utils.event')
local keymap = require('cmp.utils.keymap')
local docs_view = require('cmp.view.docs_view')
local custom_entries_view = require('cmp.view.custom_entries_view')
local wildmenu_entries_view = require('cmp.view.wildmenu_entries_view')
local native_entries_view = require('cmp.view.native_entries_view')
local entries_view = require('cmp.view.entries_view')
local ghost_text_view = require('cmp.view.ghost_text_view')
local AsyncTask = require('cmp.kit.Async.AsyncTask')
---@class cmp.View
---@field public event cmp.Event
---@field private resolve_dedup cmp.AsyncDedup
---@field private native_entries_view cmp.NativeEntriesView
---@field private custom_entries_view cmp.CustomEntriesView
---@field private wildmenu_entries_view cmp.CustomEntriesView
---@field private change_dedup cmp.AsyncDedup
---@field private entries_view cmp.EntriesView
---@field private docs_view cmp.DocsView
---@field private ghost_text_view cmp.GhostTextView
local view = {}
@ -23,25 +20,22 @@ local view = {}
view.new = function()
local self = setmetatable({}, { __index = view })
self.resolve_dedup = async.dedup()
self.custom_entries_view = custom_entries_view.new()
self.native_entries_view = native_entries_view.new()
self.wildmenu_entries_view = wildmenu_entries_view.new()
self.entries_view = entries_view.new()
self.docs_view = docs_view.new()
self.ghost_text_view = ghost_text_view.new()
self.event = event.new()
return self
end
---Return the view components are available or not.
---@return boolean
view.ready = function(self)
return self:_get_entries_view():ready()
return self.entries_view:ready()
end
---OnChange handler.
view.on_change = function(self)
self:_get_entries_view():on_change()
self.entries_view:on_change()
end
---Open menu
@ -107,9 +101,9 @@ view.open = function(self, ctx, sources)
-- open
if #entries > 0 then
self:_get_entries_view():open(offset, entries)
self.entries_view:open(offset, entries)
self.event:emit('menu_opened', {
window = self:_get_entries_view(),
window = self.entries_view,
})
break
end
@ -121,102 +115,88 @@ view.open = function(self, ctx, sources)
end
end
---Close menu
view.close = function(self)
if self:visible() then
self.event:emit('complete_done', {
entry = self:_get_entries_view():get_selected_entry(),
})
end
self:_get_entries_view():close()
self.docs_view:close()
self.ghost_text_view:hide()
self.event:emit('menu_closed', {
window = self:_get_entries_view(),
})
end
---Abort menu
view.abort = function(self)
self:_get_entries_view():abort()
self.docs_view:close()
self.ghost_text_view:hide()
self.event:emit('menu_closed', {
window = self:_get_entries_view(),
})
end
---Return the view is visible or not.
---@return boolean
view.visible = function(self)
return self:_get_entries_view():visible()
end
---Scroll documentation window if possible.
---@param delta integer
view.scroll_docs = function(self, delta)
self.docs_view:scroll(delta)
end
---Select prev menu item.
---@param option cmp.SelectOption
view.select_next_item = function(self, option)
self:_get_entries_view():select_next_item(option)
---Return the view is visible or not.
---@return boolean
view.visible = function(self)
return not not self.entries_view:visible()
end
---Close menu
view.close = function(self)
if self:visible() then
self.event:emit('complete_done', {
entry = self.entries_view:get_selected_entry(),
})
end
self.entries_view:close()
self.docs_view:close()
self.ghost_text_view:hide()
self.event:emit('menu_closed', {
window = self.entries_view,
})
end
---Abort menu
---@return cmp.kit.Async.AsyncTask
view.abort = function(self)
return AsyncTask.resolve():next(function()
return self.entries_view:abort()
end):next(function()
self.docs_view:close()
self.ghost_text_view:hide()
self.event:emit('menu_closed', {
window = self.entries_view,
})
end)
end
---Select prev menu item.
---@param option cmp.SelectOption
---@return cmp.kit.Async.AsyncTask
view.select_next_item = function(self, option)
return self.entries_view:select_next_item(option)
end
---Select prev menu item.
---@param option cmp.SelectOption
---@return cmp.kit.Async.AsyncTask
view.select_prev_item = function(self, option)
self:_get_entries_view():select_prev_item(option)
return self.entries_view:select_prev_item(option)
end
---Get offset.
view.get_offset = function(self)
return self:_get_entries_view():get_offset()
return self.entries_view:get_offset()
end
---Get entries.
---@return cmp.Entry[]
view.get_entries = function(self)
return self:_get_entries_view():get_entries()
return self.entries_view:get_entries()
end
---Get first entry
---@param self cmp.Entry|nil
---@return cmp.Entry|nil
view.get_first_entry = function(self)
return self:_get_entries_view():get_first_entry()
return self.entries_view:get_first_entry()
end
---Get current selected entry
---@return cmp.Entry|nil
view.get_selected_entry = function(self)
return self:_get_entries_view():get_selected_entry()
return self.entries_view:get_selected_entry()
end
---Get current active entry
---@return cmp.Entry|nil
view.get_active_entry = function(self)
return self:_get_entries_view():get_active_entry()
end
---Return current configured entries_view
---@return cmp.CustomEntriesView|cmp.NativeEntriesView
view._get_entries_view = function(self)
self.native_entries_view.event:clear()
self.custom_entries_view.event:clear()
self.wildmenu_entries_view.event:clear()
local c = config.get()
local v = self.custom_entries_view
if (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'wildmenu' then
v = self.wildmenu_entries_view
elseif (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'native' then
v = self.native_entries_view
end
v.event:on('change', function()
self:on_entry_change()
end)
return v
return self.entries_view:get_active_entry()
end
---On entry change
@ -235,7 +215,7 @@ view.on_entry_change = async.throttle(function(self)
if not self:visible() then
return
end
self.docs_view:open(e, self:_get_entries_view():info())
self.docs_view:open(e, self.entries_view:info())
end)))
else
self.docs_view:close()

View file

@ -1,28 +1,30 @@
local event = require('cmp.utils.event')
local autocmd = require('cmp.utils.autocmd')
local feedkeys = require('cmp.utils.feedkeys')
local window = require('cmp.utils.window')
local config = require('cmp.config')
local types = require('cmp.types')
local keymap = require('cmp.utils.keymap')
local misc = require('cmp.utils.misc')
local api = require('cmp.utils.api')
local Keymap = require('cmp.kit.Vim.Keymap')
local AsyncTask = require('cmp.kit.Async.AsyncTask')
local DEFAULT_HEIGHT = 10 -- @see https://github.com/vim/vim/blob/master/src/popupmenu.c#L45
---@class cmp.CustomEntriesView
---@class cmp.EntriesView
---@field private entries_win cmp.Window
---@field private prefix? string
---@field private offset integer
---@field private active boolean
---@field private entries cmp.Entry[]
---@field private column_width any
---@field public event cmp.Event
local custom_entries_view = {}
local entries_view = {}
custom_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.custom_entries_view')
entries_view.ns = vim.api.nvim_create_namespace('cmp.view.entries_view')
custom_entries_view.new = function()
local self = setmetatable({}, { __index = custom_entries_view })
entries_view.new = function()
local self = setmetatable({}, { __index = entries_view })
self.entries_win = window.new()
self.entries_win:option('conceallevel', 2)
@ -31,7 +33,7 @@ custom_entries_view.new = function()
self.entries_win:option('foldenable', false)
self.entries_win:option('wrap', false)
-- This is done so that strdisplaywidth calculations for lines in the
-- custom_entries_view window exactly match with what is really displayed,
-- entries_view window exactly match with what is really displayed,
-- see comment in cmp.Entry.get_view. Setting tabstop to 1 makes all tabs be
-- always rendered one column wide, which removes the unpredictability coming
-- from variable width of the tab character.
@ -52,7 +54,7 @@ custom_entries_view.new = function()
end)
)
vim.api.nvim_set_decoration_provider(custom_entries_view.ns, {
vim.api.nvim_set_decoration_provider(entries_view.ns, {
on_win = function(_, win, buf, top, bot)
if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then
return
@ -69,7 +71,7 @@ custom_entries_view.new = function()
if field == types.cmp.ItemField.Abbr then
a = o
end
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, o, {
vim.api.nvim_buf_set_extmark(buf, entries_view.ns, i, o, {
end_line = i,
end_col = o + v[field].bytes,
hl_group = v[field].hl_group,
@ -80,7 +82,7 @@ custom_entries_view.new = function()
end
for _, m in ipairs(e.matches or {}) do
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, a + m.word_match_start - 1, {
vim.api.nvim_buf_set_extmark(buf, entries_view.ns, i, a + m.word_match_start - 1, {
end_line = i,
end_col = a + m.word_match_end,
hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch',
@ -96,15 +98,15 @@ custom_entries_view.new = function()
return self
end
custom_entries_view.ready = function()
entries_view.ready = function()
return vim.fn.pumvisible() == 0
end
custom_entries_view.on_change = function(self)
entries_view.on_change = function(self)
self.active = false
end
custom_entries_view.is_direction_top_down = function(self)
entries_view.is_direction_top_down = function(self)
local c = config.get()
if (c.view and c.view.entries and c.view.entries.selection_order) == 'top_down' then
return true
@ -115,7 +117,7 @@ custom_entries_view.is_direction_top_down = function(self)
end
end
custom_entries_view.open = function(self, offset, entries)
entries_view.open = function(self, offset, entries)
local completion = config.get().window.completion
self.offset = offset
self.entries = {}
@ -224,25 +226,29 @@ custom_entries_view.open = function(self, offset, entries)
end
end
custom_entries_view.close = function(self)
entries_view.close = function(self)
self.prefix = nil
self.offset = -1
self.active = false
self.bottom_up = false
self.entries = {}
self.entries_win:close()
self.bottom_up = false
end
custom_entries_view.abort = function(self)
if self.prefix then
self:_insert(self.prefix)
end
feedkeys.call('', 'n', function()
self:close()
end)
---@return cmp.kit.Async.AsyncTask
entries_view.abort = function(self)
return AsyncTask.resolve()
:next(function()
if self.prefix then
return self:_insert(self.prefix)
end
end)
:next(function()
self:close()
end)
end
custom_entries_view.draw = function(self)
entries_view.draw = function(self)
local info = vim.fn.getwininfo(self.entries_win.win)[1]
local topline = info.topline - 1
local botline = info.topline + info.height - 1
@ -273,15 +279,16 @@ custom_entries_view.draw = function(self)
end
end
custom_entries_view.visible = function(self)
entries_view.visible = function(self)
return self.entries_win:visible()
end
custom_entries_view.info = function(self)
entries_view.info = function(self)
return self.entries_win:info()
end
custom_entries_view.select_next_item = function(self, option)
---@return cmp.kit.Async.AsyncTask
entries_view.select_next_item = function(self, option)
if self:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1]
local is_top_down = self:is_direction_top_down()
@ -310,12 +317,13 @@ custom_entries_view.select_next_item = function(self, option)
end
end
end
self:_select(cursor, option)
return self:_select(cursor, option)
end
return AsyncTask.resolve()
end
custom_entries_view.select_prev_item = function(self, option)
---@return cmp.kit.Async.AsyncTask
entries_view.select_prev_item = function(self, option)
if self:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1]
local is_top_down = self:is_direction_top_down()
@ -344,44 +352,45 @@ custom_entries_view.select_prev_item = function(self, option)
end
end
end
self:_select(cursor, option)
return self:_select(cursor, option)
end
return AsyncTask.resolve()
end
custom_entries_view.get_offset = function(self)
entries_view.get_offset = function(self)
if self:visible() then
return self.offset
end
return nil
end
custom_entries_view.get_entries = function(self)
entries_view.get_entries = function(self)
if self:visible() then
return self.entries
end
return {}
end
custom_entries_view.get_first_entry = function(self)
entries_view.get_first_entry = function(self)
if self:visible() then
return (self:is_direction_top_down() and self.entries[1]) or self.entries[#self.entries]
end
end
custom_entries_view.get_selected_entry = function(self)
entries_view.get_selected_entry = function(self)
if self:visible() and self.entries_win:option('cursorline') then
return self.entries[vim.api.nvim_win_get_cursor(self.entries_win.win)[1]]
end
end
custom_entries_view.get_active_entry = function(self)
entries_view.get_active_entry = function(self)
if self:visible() and self.active then
return self:get_selected_entry()
end
end
custom_entries_view._select = function(self, cursor, option)
---@return cmp.kit.Async.AsyncTask
entries_view._select = function(self, cursor, option)
local is_insert = (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert
if is_insert and not self.active then
self.prefix = string.sub(api.get_current_line(), self.offset, api.get_cursor()[2]) or ''
@ -395,48 +404,32 @@ custom_entries_view._select = function(self, cursor, option)
0,
})
if is_insert then
self:_insert(self.entries[cursor] and self.entries[cursor]:get_vim_item(self.offset).word or self.prefix)
end
self.entries_win:update()
self:draw()
self.event:emit('change')
return AsyncTask.resolve():next(function()
if is_insert then
return self:_insert(self.entries[cursor] and self.entries[cursor]:get_vim_item(self.offset).word or self.prefix)
end
end):next(function()
self.entries_win:update()
self:draw()
self.event:emit('change')
end)
end
custom_entries_view._insert = setmetatable({
pending = false,
}, {
__call = function(this, self, word)
word = word or ''
if api.is_cmdline_mode() then
local cursor = api.get_cursor()
vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true)
else
if this.pending then
return
end
this.pending = true
---@return cmp.kit.Async.AsyncTask
entries_view._insert = function(self, word)
word = word or ''
if api.is_cmdline_mode() then
local cursor = api.get_cursor()
return Keymap.send(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'ni')
else
local cursor = api.get_cursor()
local keys = {}
table.insert(keys, keymap.indentkeys())
table.insert(keys, keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])))
table.insert(keys, word)
table.insert(keys, keymap.indentkeys(vim.bo.indentkeys))
return Keymap.send(table.concat(keys, ''), 'ni')
end
end
local release = require('cmp').suspend()
feedkeys.call('', '', function()
local cursor = api.get_cursor()
local keys = {}
table.insert(keys, keymap.indentkeys())
table.insert(keys, keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])))
table.insert(keys, word)
table.insert(keys, keymap.indentkeys(vim.bo.indentkeys))
feedkeys.call(
table.concat(keys, ''),
'int',
vim.schedule_wrap(function()
this.pending = false
release()
end)
)
end)
end
end,
})
return custom_entries_view
return entries_view

View file

@ -1,180 +0,0 @@
local event = require('cmp.utils.event')
local autocmd = require('cmp.utils.autocmd')
local keymap = require('cmp.utils.keymap')
local feedkeys = require('cmp.utils.feedkeys')
local types = require('cmp.types')
local config = require('cmp.config')
local api = require('cmp.utils.api')
---@class cmp.NativeEntriesView
---@field private offset integer
---@field private items vim.CompletedItem
---@field private entries cmp.Entry[]
---@field private preselect_index integer
---@field public event cmp.Event
local native_entries_view = {}
native_entries_view.new = function()
local self = setmetatable({}, { __index = native_entries_view })
self.event = event.new()
self.offset = -1
self.items = {}
self.entries = {}
self.preselect_index = 0
autocmd.subscribe('CompleteChanged', function()
self.event:emit('change')
end)
return self
end
native_entries_view.ready = function(_)
if vim.fn.pumvisible() == 0 then
return true
end
return vim.fn.complete_info({ 'mode' }).mode == 'eval'
end
native_entries_view.on_change = function(self)
if #self.entries > 0 and self.offset <= vim.api.nvim_win_get_cursor(0)[2] + 1 then
local preselect_enabled = config.get().preselect == types.cmp.PreselectMode.Item
local completeopt = vim.o.completeopt
if self.preselect_index == 1 and preselect_enabled then
vim.o.completeopt = 'menu,menuone,noinsert'
else
vim.o.completeopt = config.get().completion.completeopt
end
vim.fn.complete(self.offset, self.items)
vim.o.completeopt = completeopt
if self.preselect_index > 1 and preselect_enabled then
self:preselect(self.preselect_index)
end
end
end
native_entries_view.open = function(self, offset, entries)
local dedup = {}
local items = {}
local dedup_entries = {}
local preselect_index = 0
for _, e in ipairs(entries) do
local item = e:get_vim_item(offset)
if item.dup == 1 or not dedup[item.abbr] then
dedup[item.abbr] = true
table.insert(items, item)
table.insert(dedup_entries, e)
if preselect_index == 0 and e.completion_item.preselect then
preselect_index = #dedup_entries
end
end
end
self.offset = offset
self.items = items
self.entries = dedup_entries
self.preselect_index = preselect_index
self:on_change()
end
native_entries_view.close = function(self)
if api.is_insert_mode() and self:visible() then
vim.api.nvim_select_popupmenu_item(-1, false, true, {})
end
self.offset = -1
self.entries = {}
self.items = {}
self.preselect_index = 0
end
native_entries_view.abort = function(_)
if api.is_suitable_mode() then
vim.api.nvim_select_popupmenu_item(-1, true, true, {})
end
end
native_entries_view.visible = function(_)
return vim.fn.pumvisible() == 1
end
native_entries_view.info = function(self)
if self:visible() then
local info = vim.fn.pum_getpos()
return {
width = info.width + (info.scrollbar and 1 or 0) + (info.col == 0 and 0 or 1),
height = info.height,
row = info.row,
col = info.col == 0 and 0 or info.col - 1,
}
end
end
native_entries_view.preselect = function(self, index)
if self:visible() then
if index <= #self.entries then
vim.api.nvim_select_popupmenu_item(index - 1, false, false, {})
end
end
end
native_entries_view.select_next_item = function(self, option)
local callback = function()
self.event:emit('change')
end
if self:visible() then
if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t(string.rep('<C-n>', option.count)), 'n', callback)
else
feedkeys.call(keymap.t(string.rep('<Down>', option.count)), 'n', callback)
end
end
end
native_entries_view.select_prev_item = function(self, option)
local callback = function()
self.event:emit('change')
end
if self:visible() then
if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then
feedkeys.call(keymap.t(string.rep('<C-p>', option.count)), 'n', callback)
else
feedkeys.call(keymap.t(string.rep('<Up>', option.count)), 'n', callback)
end
end
end
native_entries_view.get_offset = function(self)
if self:visible() then
return self.offset
end
return nil
end
native_entries_view.get_entries = function(self)
if self:visible() then
return self.entries
end
return {}
end
native_entries_view.get_first_entry = function(self)
if self:visible() then
return self.entries[1]
end
end
native_entries_view.get_selected_entry = function(self)
if self:visible() then
local idx = vim.fn.complete_info({ 'selected' }).selected
if idx > -1 then
return self.entries[math.max(0, idx) + 1]
end
end
end
native_entries_view.get_active_entry = function(self)
if self:visible() and (vim.v.completed_item or {}).word then
return self:get_selected_entry()
end
end
return native_entries_view

View file

@ -1,264 +0,0 @@
local event = require('cmp.utils.event')
local autocmd = require('cmp.utils.autocmd')
local feedkeys = require('cmp.utils.feedkeys')
local config = require('cmp.config')
local window = require('cmp.utils.window')
local types = require('cmp.types')
local keymap = require('cmp.utils.keymap')
local misc = require('cmp.utils.misc')
local api = require('cmp.utils.api')
---@class cmp.CustomEntriesView
---@field private offset integer
---@field private entries_win cmp.Window
---@field private active boolean
---@field private entries cmp.Entry[]
---@field public event cmp.Event
local wildmenu_entries_view = {}
wildmenu_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.statusline_entries_view')
wildmenu_entries_view.new = function()
local self = setmetatable({}, { __index = wildmenu_entries_view })
self.event = event.new()
self.offset = -1
self.active = false
self.entries = {}
self.offsets = {}
self.selected_index = 0
self.entries_win = window.new()
self.entries_win:option('conceallevel', 2)
self.entries_win:option('concealcursor', 'n')
self.entries_win:option('cursorlineopt', 'line')
self.entries_win:option('foldenable', false)
self.entries_win:option('wrap', false)
self.entries_win:option('scrolloff', 0)
self.entries_win:option('sidescrolloff', 0)
self.entries_win:option('winhighlight', 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None')
self.entries_win:buffer_option('tabstop', 1)
autocmd.subscribe(
'CompleteChanged',
vim.schedule_wrap(function()
if self:visible() and vim.fn.pumvisible() == 1 then
self:close()
end
end)
)
vim.api.nvim_set_decoration_provider(wildmenu_entries_view.ns, {
on_win = function(_, win, buf, _, _)
if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then
return
end
for i, e in ipairs(self.entries) do
if e then
local view = e:get_view(self.offset, buf)
vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i], {
end_line = 0,
end_col = self.offsets[i] + view.abbr.bytes,
hl_group = view.abbr.hl_group,
hl_mode = 'combine',
ephemeral = true,
})
if i == self.selected_index then
vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i], {
end_line = 0,
end_col = self.offsets[i] + view.abbr.bytes,
hl_group = 'PmenuSel',
hl_mode = 'combine',
ephemeral = true,
})
end
for _, m in ipairs(e.matches or {}) do
vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i] + m.word_match_start - 1, {
end_line = 0,
end_col = self.offsets[i] + m.word_match_end,
hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch',
hl_mode = 'combine',
ephemeral = true,
})
end
end
end
end,
})
return self
end
wildmenu_entries_view.close = function(self)
self.entries_win:close()
end
wildmenu_entries_view.ready = function()
return vim.fn.pumvisible() == 0
end
wildmenu_entries_view.on_change = function(self)
self.active = false
end
wildmenu_entries_view.open = function(self, offset, entries)
self.offset = offset
self.entries = {}
-- Apply window options (that might be changed) on the custom completion menu.
self.entries_win:option('winblend', vim.o.pumblend)
local dedup = {}
local preselect = 0
local i = 1
for _, e in ipairs(entries) do
local view = e:get_view(offset, 0)
if view.dup == 1 or not dedup[e.completion_item.label] then
dedup[e.completion_item.label] = true
table.insert(self.entries, e)
if preselect == 0 and e.completion_item.preselect then
preselect = i
end
i = i + 1
end
end
self.entries_win:open({
relative = 'editor',
style = 'minimal',
row = vim.o.lines - 2,
col = 0,
width = vim.o.columns,
height = 1,
zindex = 1001,
})
self:draw()
if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
self:_select(preselect, { behavior = types.cmp.SelectBehavior.Select })
elseif not string.match(config.get().completion.completeopt, 'noselect') then
self:_select(1, { behavior = types.cmp.SelectBehavior.Select })
else
self:_select(0, { behavior = types.cmp.SelectBehavior.Select })
end
end
wildmenu_entries_view.abort = function(self)
feedkeys.call('', 'n', function()
self:close()
end)
end
wildmenu_entries_view.draw = function(self)
self.offsets = {}
local entries_buf = self.entries_win:get_buffer()
local texts = {}
local offset = 0
for _, e in ipairs(self.entries) do
local view = e:get_view(self.offset, entries_buf)
table.insert(self.offsets, offset)
table.insert(texts, view.abbr.text)
offset = offset + view.abbr.bytes + #self:_get_separator()
end
vim.api.nvim_buf_set_lines(entries_buf, 0, 1, false, { table.concat(texts, self:_get_separator()) })
vim.api.nvim_buf_set_option(entries_buf, 'modified', false)
vim.api.nvim_win_call(0, function()
misc.redraw()
end)
end
wildmenu_entries_view.visible = function(self)
return self.entries_win:visible()
end
wildmenu_entries_view.info = function(self)
return self.entries_win:info()
end
wildmenu_entries_view.select_next_item = function(self, option)
if self:visible() then
local cursor
if self.selected_index == 0 or self.selected_index == #self.entries then
cursor = option.count
else
cursor = self.selected_index + option.count
end
cursor = math.max(math.min(cursor, #self.entries), 0)
self:_select(cursor, option)
end
end
wildmenu_entries_view.select_prev_item = function(self, option)
if self:visible() then
if self.selected_index == 0 or self.selected_index <= 1 then
self:_select(#self.entries, option)
else
self:_select(math.max(self.selected_index - option.count, 0), option)
end
end
end
wildmenu_entries_view.get_offset = function(self)
if self:visible() then
return self.offset
end
return nil
end
wildmenu_entries_view.get_entries = function(self)
if self:visible() then
return self.entries
end
return {}
end
wildmenu_entries_view.get_first_entry = function(self)
if self:visible() then
return self.entries[1]
end
end
wildmenu_entries_view.get_selected_entry = function(self)
if self:visible() and self.active then
return self.entries[self.selected_index]
end
end
wildmenu_entries_view.get_active_entry = function(self)
if self:visible() and self.active then
return self:get_selected_entry()
end
end
wildmenu_entries_view._select = function(self, selected_index, option)
local is_next = self.selected_index < selected_index
self.selected_index = selected_index
self.active = (selected_index ~= 0)
if self.active then
local e = self:get_active_entry()
if option.behavior == types.cmp.SelectBehavior.Insert then
local cursor = api.get_cursor()
local word = e:get_vim_item(self.offset).word
vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true)
end
vim.api.nvim_win_call(self.entries_win.win, function()
local view = e:get_view(self.offset, self.entries_win:get_buffer())
vim.api.nvim_win_set_cursor(0, { 1, self.offsets[selected_index] + (is_next and view.abbr.bytes or 0) })
vim.cmd([[redraw!]]) -- Force refresh for vim.api.nvim_set_decoration_provider
end)
end
self.event:emit('change')
end
wildmenu_entries_view._get_separator = function()
local c = config.get()
return (c and c.view and c.view.entries and c.view.entries.separator) or ' '
end
return wildmenu_entries_view