1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,75 @@
let s:bridge_id = 0
let s:sources = {}
"
" cmp#register_source
"
function! cmp#register_source(name, source) abort
let l:methods = []
for l:method in [
\ 'is_available',
\ 'get_debug_name',
\ 'get_position_encoding_kind',
\ 'get_trigger_characters',
\ 'get_keyword_pattern',
\ 'complete',
\ 'execute',
\ 'resolve'
\ ]
if has_key(a:source, l:method) && type(a:source[l:method]) == v:t_func
call add(l:methods, l:method)
endif
endfor
let s:bridge_id += 1
let a:source.bridge_id = s:bridge_id
let a:source.id = luaeval('require("cmp").register_source(_A[1], require("cmp.vim_source").new(_A[2], _A[3]))', [a:name, s:bridge_id, l:methods])
let s:sources[s:bridge_id] = a:source
return a:source.id
endfunction
"
" cmp#unregister_source
"
function! cmp#unregister_source(id) abort
if has_key(s:sources, a:id)
unlet s:sources[a:id]
endif
call luaeval('require("cmp").unregister_source(_A)', a:id)
endfunction
"
" cmp#_method
"
function! cmp#_method(bridge_id, method, args) abort
try
let l:source = s:sources[a:bridge_id]
if a:method ==# 'is_available'
return l:source[a:method]()
elseif a:method ==# 'get_debug_name'
return l:source[a:method]()
elseif a:method ==# 'get_position_encoding_kind'
return l:source[a:method](a:args[0])
elseif a:method ==# 'get_keyword_pattern'
return l:source[a:method](a:args[0])
elseif a:method ==# 'get_trigger_characters'
return l:source[a:method](a:args[0])
elseif a:method ==# 'complete'
return l:source[a:method](a:args[0], s:callback(a:args[1]))
elseif a:method ==# 'resolve'
return l:source[a:method](a:args[0], s:callback(a:args[1]))
elseif a:method ==# 'execute'
return l:source[a:method](a:args[0], s:callback(a:args[1]))
endif
catch /.*/
echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint })
endtry
return v:null
endfunction
"
" s:callback
"
function! s:callback(id) abort
return { ... -> luaeval('require("cmp.vim_source").on_callback(_A[1], _A[2])', [a:id, a:000]) }
endfunction

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
CmpItemAbbr cmp.txt /*CmpItemAbbr*
CmpItemAbbrDeprecated cmp.txt /*CmpItemAbbrDeprecated*
CmpItemAbbrMatch cmp.txt /*CmpItemAbbrMatch*
CmpItemAbbrMatchFuzzy cmp.txt /*CmpItemAbbrMatchFuzzy*
CmpItemKind cmp.txt /*CmpItemKind*
CmpItemKind%KIND_NAME% cmp.txt /*CmpItemKind%KIND_NAME%*
CmpItemMenu cmp.txt /*CmpItemMenu*
CmpReady cmp.txt /*CmpReady*
CmpStatus cmp.txt /*CmpStatus*
cmp cmp.txt /*cmp*
cmp-abstract cmp.txt /*cmp-abstract*
cmp-autocmd cmp.txt /*cmp-autocmd*
cmp-command cmp.txt /*cmp-command*
cmp-concept cmp.txt /*cmp-concept*
cmp-config cmp.txt /*cmp-config*
cmp-config-helper cmp.txt /*cmp-config-helper*
cmp-config.completion.autocomplete cmp.txt /*cmp-config.completion.autocomplete*
cmp-config.completion.completeopt cmp.txt /*cmp-config.completion.completeopt*
cmp-config.completion.keyword_length cmp.txt /*cmp-config.completion.keyword_length*
cmp-config.completion.keyword_pattern cmp.txt /*cmp-config.completion.keyword_pattern*
cmp-config.confirmation.get_commit_characters cmp.txt /*cmp-config.confirmation.get_commit_characters*
cmp-config.enabled cmp.txt /*cmp-config.enabled*
cmp-config.experimental.ghost_text cmp.txt /*cmp-config.experimental.ghost_text*
cmp-config.formatting.expandable_indicator cmp.txt /*cmp-config.formatting.expandable_indicator*
cmp-config.formatting.fields cmp.txt /*cmp-config.formatting.fields*
cmp-config.formatting.format cmp.txt /*cmp-config.formatting.format*
cmp-config.mapping cmp.txt /*cmp-config.mapping*
cmp-config.matching.disallow_fullfuzzy_matching cmp.txt /*cmp-config.matching.disallow_fullfuzzy_matching*
cmp-config.matching.disallow_fuzzy_matching cmp.txt /*cmp-config.matching.disallow_fuzzy_matching*
cmp-config.matching.disallow_partial_fuzzy_matching cmp.txt /*cmp-config.matching.disallow_partial_fuzzy_matching*
cmp-config.matching.disallow_partial_matching cmp.txt /*cmp-config.matching.disallow_partial_matching*
cmp-config.matching.disallow_prefix_unmatching cmp.txt /*cmp-config.matching.disallow_prefix_unmatching*
cmp-config.performance.async_budget cmp.txt /*cmp-config.performance.async_budget*
cmp-config.performance.confirm_resolve_timeout cmp.txt /*cmp-config.performance.confirm_resolve_timeout*
cmp-config.performance.debounce cmp.txt /*cmp-config.performance.debounce*
cmp-config.performance.fetching_timeout cmp.txt /*cmp-config.performance.fetching_timeout*
cmp-config.performance.max_view_entries cmp.txt /*cmp-config.performance.max_view_entries*
cmp-config.performance.throttle cmp.txt /*cmp-config.performance.throttle*
cmp-config.preselect cmp.txt /*cmp-config.preselect*
cmp-config.snippet.expand cmp.txt /*cmp-config.snippet.expand*
cmp-config.sorting.comparators cmp.txt /*cmp-config.sorting.comparators*
cmp-config.sorting.priority_weight cmp.txt /*cmp-config.sorting.priority_weight*
cmp-config.sources cmp.txt /*cmp-config.sources*
cmp-config.sources[n].entry_filter cmp.txt /*cmp-config.sources[n].entry_filter*
cmp-config.sources[n].group_index cmp.txt /*cmp-config.sources[n].group_index*
cmp-config.sources[n].keyword_length cmp.txt /*cmp-config.sources[n].keyword_length*
cmp-config.sources[n].keyword_pattern cmp.txt /*cmp-config.sources[n].keyword_pattern*
cmp-config.sources[n].max_item_count cmp.txt /*cmp-config.sources[n].max_item_count*
cmp-config.sources[n].name cmp.txt /*cmp-config.sources[n].name*
cmp-config.sources[n].option cmp.txt /*cmp-config.sources[n].option*
cmp-config.sources[n].priority cmp.txt /*cmp-config.sources[n].priority*
cmp-config.sources[n].trigger_characters cmp.txt /*cmp-config.sources[n].trigger_characters*
cmp-config.view cmp.txt /*cmp-config.view*
cmp-config.view.docs.auto_open cmp.txt /*cmp-config.view.docs.auto_open*
cmp-config.view.entries.follow_cursor cmp.txt /*cmp-config.view.entries.follow_cursor*
cmp-config.view.entries.selection_order cmp.txt /*cmp-config.view.entries.selection_order*
cmp-config.window.completion.col_offset cmp.txt /*cmp-config.window.completion.col_offset*
cmp-config.window.completion.scrollbar cmp.txt /*cmp-config.window.completion.scrollbar*
cmp-config.window.completion.side_padding cmp.txt /*cmp-config.window.completion.side_padding*
cmp-config.window.documentation.max_height cmp.txt /*cmp-config.window.documentation.max_height*
cmp-config.window.documentation.max_width cmp.txt /*cmp-config.window.documentation.max_width*
cmp-config.window.{completion,documentation}.border cmp.txt /*cmp-config.window.{completion,documentation}.border*
cmp-config.window.{completion,documentation}.scrolloff cmp.txt /*cmp-config.window.{completion,documentation}.scrolloff*
cmp-config.window.{completion,documentation}.winblend cmp.txt /*cmp-config.window.{completion,documentation}.winblend*
cmp-config.window.{completion,documentation}.winhighlight cmp.txt /*cmp-config.window.{completion,documentation}.winhighlight*
cmp-config.window.{completion,documentation}.zindex cmp.txt /*cmp-config.window.{completion,documentation}.zindex*
cmp-contents cmp.txt /*cmp-contents*
cmp-develop cmp.txt /*cmp-develop*
cmp-faq cmp.txt /*cmp-faq*
cmp-filetype cmp.txt /*cmp-filetype*
cmp-function cmp.txt /*cmp-function*
cmp-highlight cmp.txt /*cmp-highlight*
cmp-mapping cmp.txt /*cmp-mapping*
cmp-usage cmp.txt /*cmp-usage*
cmp.abort cmp.txt /*cmp.abort*
cmp.close cmp.txt /*cmp.close*
cmp.close_docs cmp.txt /*cmp.close_docs*
cmp.complete cmp.txt /*cmp.complete*
cmp.complete_common_string cmp.txt /*cmp.complete_common_string*
cmp.config.context.in_syntax_group cmp.txt /*cmp.config.context.in_syntax_group*
cmp.config.context.in_treesitter_capture cmp.txt /*cmp.config.context.in_treesitter_capture*
cmp.config.sources cmp.txt /*cmp.config.sources*
cmp.config.window.bordered cmp.txt /*cmp.config.window.bordered*
cmp.confirm cmp.txt /*cmp.confirm*
cmp.event:on cmp.txt /*cmp.event:on*
cmp.get_active_entry cmp.txt /*cmp.get_active_entry*
cmp.get_entries cmp.txt /*cmp.get_entries*
cmp.get_selected_entry cmp.txt /*cmp.get_selected_entry*
cmp.mapping.abort cmp.txt /*cmp.mapping.abort*
cmp.mapping.close cmp.txt /*cmp.mapping.close*
cmp.mapping.close_docs cmp.txt /*cmp.mapping.close_docs*
cmp.mapping.complete cmp.txt /*cmp.mapping.complete*
cmp.mapping.complete_common_string cmp.txt /*cmp.mapping.complete_common_string*
cmp.mapping.confirm cmp.txt /*cmp.mapping.confirm*
cmp.mapping.open_docs cmp.txt /*cmp.mapping.open_docs*
cmp.mapping.scroll_docs cmp.txt /*cmp.mapping.scroll_docs*
cmp.mapping.select_next_item cmp.txt /*cmp.mapping.select_next_item*
cmp.mapping.select_prev_item cmp.txt /*cmp.mapping.select_prev_item*
cmp.open_docs cmp.txt /*cmp.open_docs*
cmp.scroll_docs cmp.txt /*cmp.scroll_docs*
cmp.select_next_item cmp.txt /*cmp.select_next_item*
cmp.select_prev_item cmp.txt /*cmp.select_prev_item*
cmp.setup cmp.txt /*cmp.setup*
cmp.setup.buffer cmp.txt /*cmp.setup.buffer*
cmp.setup.cmdline cmp.txt /*cmp.setup.cmdline*
cmp.setup.filetype cmp.txt /*cmp.setup.filetype*
cmp.visible cmp.txt /*cmp.visible*
cmp.visible_docs cmp.txt /*cmp.visible_docs*
cmp_docs cmp.txt /*cmp_docs*
cmp_menu cmp.txt /*cmp_menu*
nvim-cmp cmp.txt /*nvim-cmp*

View File

@ -0,0 +1,231 @@
local mapping = require('cmp.config.mapping')
local cache = require('cmp.utils.cache')
local keymap = require('cmp.utils.keymap')
local misc = require('cmp.utils.misc')
local api = require('cmp.utils.api')
---@class cmp.Config
---@field public g cmp.ConfigSchema
local config = {}
---@type cmp.Cache
config.cache = cache.new()
---@type cmp.ConfigSchema
config.global = require('cmp.config.default')()
---@type table<integer, cmp.ConfigSchema>
config.buffers = {}
---@type table<string, cmp.ConfigSchema>
config.filetypes = {}
---@type table<string, cmp.ConfigSchema>
config.cmdline = {}
---@type cmp.ConfigSchema
config.onetime = {}
---Set configuration for global.
---@param c cmp.ConfigSchema
config.set_global = function(c)
config.global = config.normalize(misc.merge(c, config.global))
config.global.revision = config.global.revision or 1
config.global.revision = config.global.revision + 1
end
---Set configuration for buffer
---@param c cmp.ConfigSchema
---@param bufnr integer
config.set_buffer = function(c, bufnr)
local revision = (config.buffers[bufnr] or {}).revision or 1
config.buffers[bufnr] = c or {}
config.buffers[bufnr].revision = revision + 1
end
---Set configuration for filetype
---@param c cmp.ConfigSchema
---@param filetypes string[]|string
config.set_filetype = function(c, filetypes)
for _, filetype in ipairs(type(filetypes) == 'table' and filetypes or { filetypes }) do
local revision = (config.filetypes[filetype] or {}).revision or 1
config.filetypes[filetype] = c or {}
config.filetypes[filetype].revision = revision + 1
end
end
---Set configuration for cmdline
---@param c cmp.ConfigSchema
---@param cmdtypes string|string[]
config.set_cmdline = function(c, cmdtypes)
for _, cmdtype in ipairs(type(cmdtypes) == 'table' and cmdtypes or { cmdtypes }) do
local revision = (config.cmdline[cmdtype] or {}).revision or 1
config.cmdline[cmdtype] = c or {}
config.cmdline[cmdtype].revision = revision + 1
end
end
---Set configuration as oneshot completion.
---@param c cmp.ConfigSchema
config.set_onetime = function(c)
local revision = (config.onetime or {}).revision or 1
config.onetime = c or {}
config.onetime.revision = revision + 1
end
---@return cmp.ConfigSchema
config.get = function()
local global_config = config.global
-- The config object already has `revision` key.
if #vim.tbl_keys(config.onetime) > 1 then
local onetime_config = config.onetime
return config.cache:ensure({
'get',
'onetime',
global_config.revision or 0,
onetime_config.revision or 0,
}, function()
local c = {}
c = misc.merge(c, config.normalize(onetime_config))
c = misc.merge(c, config.normalize(global_config))
return c
end)
elseif api.is_cmdline_mode() then
local cmdtype = vim.fn.getcmdtype()
local cmdline_config = config.cmdline[cmdtype] or { revision = 1, sources = {} }
return config.cache:ensure({
'get',
'cmdline',
global_config.revision or 0,
cmdtype,
cmdline_config.revision or 0,
}, function()
local c = {}
c = misc.merge(c, config.normalize(cmdline_config))
c = misc.merge(c, config.normalize(global_config))
return c
end)
else
local bufnr = vim.api.nvim_get_current_buf()
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
local buffer_config = config.buffers[bufnr] or { revision = 1 }
local filetype_config = config.filetypes[filetype] or { revision = 1 }
return config.cache:ensure({
'get',
'default',
global_config.revision or 0,
filetype,
filetype_config.revision or 0,
bufnr,
buffer_config.revision or 0,
}, function()
local c = {}
c = misc.merge(config.normalize(c), config.normalize(buffer_config))
c = misc.merge(config.normalize(c), config.normalize(filetype_config))
c = misc.merge(config.normalize(c), config.normalize(global_config))
return c
end)
end
end
---Return cmp is enabled or not.
config.enabled = function()
local enabled = config.get().enabled
if type(enabled) == 'function' then
enabled = enabled()
end
return enabled and api.is_suitable_mode()
end
---Return source config
---@param name string
---@return cmp.SourceConfig
config.get_source_config = function(name)
local c = config.get()
for _, s in ipairs(c.sources) do
if s.name == name then
return s
end
end
return nil
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
end
---Normalize mapping key
---@param c any
---@return cmp.ConfigSchema
config.normalize = function(c)
-- make sure c is not 'nil'
---@type any
c = c == nil and {} or c
-- Normalize mapping.
if c.mapping then
local normalized = {}
for k, v in pairs(c.mapping) do
normalized[keymap.normalize(k)] = mapping(v, { 'i' })
end
c.mapping = normalized
end
-- Notice experimental.native_menu.
if c.experimental and c.experimental.native_menu then
vim.api.nvim_echo({
{ '[nvim-cmp] ', 'Normal' },
{ 'experimental.native_menu', 'WarningMsg' },
{ ' is deprecated.\n', 'Normal' },
{ '[nvim-cmp] Please use ', 'Normal' },
{ 'view.entries = "native"', 'WarningMsg' },
{ ' instead.', 'Normal' },
}, true, {})
c.view = c.view or {}
c.view.entries = c.view.entries or 'native'
end
-- Notice documentation.
if c.documentation ~= nil then
vim.api.nvim_echo({
{ '[nvim-cmp] ', 'Normal' },
{ 'documentation', 'WarningMsg' },
{ ' is deprecated.\n', 'Normal' },
{ '[nvim-cmp] Please use ', 'Normal' },
{ 'window.documentation = cmp.config.window.bordered()', 'WarningMsg' },
{ ' instead.', 'Normal' },
}, true, {})
c.window = c.window or {}
c.window.documentation = c.documentation
end
-- Notice sources.[n].opts
if c.sources then
for _, s in ipairs(c.sources) do
if s.opts and not s.option then
s.option = s.opts
s.opts = nil
vim.api.nvim_echo({
{ '[nvim-cmp] ', 'Normal' },
{ 'sources[number].opts', 'WarningMsg' },
{ ' is deprecated.\n', 'Normal' },
{ '[nvim-cmp] Please use ', 'Normal' },
{ 'sources[number].option', 'WarningMsg' },
{ ' instead.', 'Normal' },
}, true, {})
end
s.option = s.option or {}
end
end
return c
end
return config

View File

@ -0,0 +1,270 @@
local types = require('cmp.types')
local cache = require('cmp.utils.cache')
---@type cmp.Comparator[]
local compare = {}
--- Comparators (:help cmp-config.sorting.comparators) should return
--- true when the first entry should come EARLIER (i.e., higher ranking) than the second entry,
--- or nil if no pairwise ordering preference from the comparator.
--- See also :help table.sort() and cmp.view.open() to see how comparators are used.
---@class cmp.ComparatorFunctor
---@overload fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil
---@alias cmp.ComparatorFunction fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil
---@alias cmp.Comparator cmp.ComparatorFunction | cmp.ComparatorFunctor
---offset: Entries with smaller offset will be ranked higher.
---@type cmp.ComparatorFunction
compare.offset = function(entry1, entry2)
local diff = entry1:get_offset() - entry2:get_offset()
if diff < 0 then
return true
elseif diff > 0 then
return false
end
return nil
end
---exact: Entries with exact == true will be ranked higher.
---@type cmp.ComparatorFunction
compare.exact = function(entry1, entry2)
if entry1.exact ~= entry2.exact then
return entry1.exact
end
return nil
end
---score: Entries with higher score will be ranked higher.
---@type cmp.ComparatorFunction
compare.score = function(entry1, entry2)
local diff = entry2.score - entry1.score
if diff < 0 then
return true
elseif diff > 0 then
return false
end
return nil
end
---recently_used: Entries that are used recently will be ranked higher.
---@type cmp.ComparatorFunctor
compare.recently_used = setmetatable({
records = {},
add_entry = function(self, e)
self.records[e.completion_item.label] = vim.loop.now()
end,
}, {
---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
__call = function(self, entry1, entry2)
local t1 = self.records[entry1.completion_item.label] or -1
local t2 = self.records[entry2.completion_item.label] or -1
if t1 ~= t2 then
return t1 > t2
end
return nil
end,
})
---kind: Entires with smaller ordinal value of 'kind' will be ranked higher.
---(see lsp.CompletionItemKind enum).
---Exceptions are that Text(1) will be ranked the lowest, and snippets be the highest.
---@type cmp.ComparatorFunction
compare.kind = function(entry1, entry2)
local kind1 = entry1:get_kind() --- @type lsp.CompletionItemKind | number
local kind2 = entry2:get_kind() --- @type lsp.CompletionItemKind | number
kind1 = kind1 == types.lsp.CompletionItemKind.Text and 100 or kind1
kind2 = kind2 == types.lsp.CompletionItemKind.Text and 100 or kind2
if kind1 ~= kind2 then
if kind1 == types.lsp.CompletionItemKind.Snippet then
return true
end
if kind2 == types.lsp.CompletionItemKind.Snippet then
return false
end
local diff = kind1 - kind2
if diff < 0 then
return true
elseif diff > 0 then
return false
end
end
return nil
end
---sort_text: Entries will be ranked according to the lexicographical order of sortText.
---@type cmp.ComparatorFunction
compare.sort_text = function(entry1, entry2)
if entry1.completion_item.sortText and entry2.completion_item.sortText then
local diff = vim.stricmp(entry1.completion_item.sortText, entry2.completion_item.sortText)
if diff < 0 then
return true
elseif diff > 0 then
return false
end
end
return nil
end
---length: Entires with shorter label length will be ranked higher.
---@type cmp.ComparatorFunction
compare.length = function(entry1, entry2)
local diff = #entry1.completion_item.label - #entry2.completion_item.label
if diff < 0 then
return true
elseif diff > 0 then
return false
end
return nil
end
----order: Entries with smaller id will be ranked higher.
---@type fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
compare.order = function(entry1, entry2)
local diff = entry1.id - entry2.id
if diff < 0 then
return true
elseif diff > 0 then
return false
end
return nil
end
---locality: Entries with higher locality (i.e., words that are closer to the cursor)
---will be ranked higher. See GH-183 for more details.
---@type cmp.ComparatorFunctor
compare.locality = setmetatable({
lines_count = 10,
lines_cache = cache.new(),
locality_map = {},
update = function(self)
local config = require('cmp').get_config()
if not vim.tbl_contains(config.sorting.comparators, compare.locality) then
return
end
local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf()
local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1
local max = vim.api.nvim_buf_line_count(buf)
if self.lines_cache:get('buf') ~= buf then
self.lines_cache:clear()
self.lines_cache:set('buf', buf)
end
self.locality_map = {}
for i = math.max(0, cursor_row - self.lines_count), math.min(max, cursor_row + self.lines_count) do
local is_above = i < cursor_row
local buffer = vim.api.nvim_buf_get_lines(buf, i, i + 1, false)[1] or ''
local locality_map = self.lines_cache:ensure({ 'line', buffer }, function()
local locality_map = {}
local regexp = vim.regex(config.completion.keyword_pattern)
while buffer ~= '' do
local s, e = regexp:match_str(buffer)
if s and e then
local w = string.sub(buffer, s + 1, e)
local d = math.abs(i - cursor_row) - (is_above and 1 or 0)
locality_map[w] = math.min(locality_map[w] or math.huge, d)
buffer = string.sub(buffer, e + 1)
else
break
end
end
return locality_map
end)
for w, d in pairs(locality_map) do
self.locality_map[w] = math.min(self.locality_map[w] or d, math.abs(i - cursor_row))
end
end
end,
}, {
---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
__call = function(self, entry1, entry2)
local local1 = self.locality_map[entry1:get_word()]
local local2 = self.locality_map[entry2:get_word()]
if local1 ~= local2 then
if local1 == nil then
return false
end
if local2 == nil then
return true
end
return local1 < local2
end
return nil
end,
})
---scopes: Entries defined in a closer scope will be ranked higher (e.g., prefer local variables to globals).
---@type cmp.ComparatorFunctor
compare.scopes = setmetatable({
scopes_map = {},
update = function(self)
local config = require('cmp').get_config()
if not vim.tbl_contains(config.sorting.comparators, compare.scopes) then
return
end
local ok, locals = pcall(require, 'nvim-treesitter.locals')
if ok then
local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf()
local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1
-- Cursor scope.
local cursor_scope = nil
for _, scope in ipairs(locals.get_scopes(buf)) do
if scope:start() <= cursor_row and cursor_row <= scope:end_() then
if not cursor_scope then
cursor_scope = scope
else
if cursor_scope:start() <= scope:start() and scope:end_() <= cursor_scope:end_() then
cursor_scope = scope
end
end
elseif cursor_scope and cursor_scope:end_() <= scope:start() then
break
end
end
-- Definitions.
local definitions = locals.get_definitions_lookup_table(buf)
-- Narrow definitions.
local depth = 0
for scope in locals.iter_scope_tree(cursor_scope, buf) do
local s, e = scope:start(), scope:end_()
-- Check scope's direct child.
for _, definition in pairs(definitions) do
if s <= definition.node:start() and definition.node:end_() <= e then
if scope:id() == locals.containing_scope(definition.node, buf):id() then
local get_node_text = vim.treesitter.get_node_text or vim.treesitter.query.get_node_text
local text = get_node_text(definition.node, buf) or ''
if not self.scopes_map[text] then
self.scopes_map[text] = depth
end
end
end
end
depth = depth + 1
end
end
end,
}, {
---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
__call = function(self, entry1, entry2)
local local1 = self.scopes_map[entry1:get_word()]
local local2 = self.scopes_map[entry2:get_word()]
if local1 ~= local2 then
if local1 == nil then
return false
end
if local2 == nil then
return true
end
return local1 < local2
end
end,
})
return compare

View File

@ -0,0 +1,60 @@
local api = require('cmp.utils.api')
local context = {}
---Check if cursor is in syntax group
---@param group string | []string
---@return boolean
context.in_syntax_group = function(group)
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
if not api.is_insert_mode() then
col = col + 1
end
for _, syn_id in ipairs(vim.fn.synstack(row, col)) do
syn_id = vim.fn.synIDtrans(syn_id) -- Resolve :highlight links
local g = vim.fn.synIDattr(syn_id, 'name')
if type(group) == 'string' and g == group then
return true
elseif type(group) == 'table' and vim.tbl_contains(group, g) then
return true
end
end
return false
end
---Check if cursor is in treesitter capture
---@param capture string | []string
---@return boolean
context.in_treesitter_capture = function(capture)
local buf = vim.api.nvim_get_current_buf()
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
row = row - 1
if vim.api.nvim_get_mode().mode == 'i' then
col = col - 1
end
local get_captures_at_pos = -- See neovim/neovim#20331
require('vim.treesitter').get_captures_at_pos -- for neovim >= 0.8 or require('vim.treesitter').get_captures_at_position -- for neovim < 0.8
local captures_at_cursor = vim.tbl_map(function(x)
return x.capture
end, get_captures_at_pos(buf, row, col))
if vim.tbl_isempty(captures_at_cursor) then
return false
elseif type(capture) == 'string' and vim.tbl_contains(captures_at_cursor, capture) then
return true
elseif type(capture) == 'table' then
for _, v in ipairs(capture) do
if vim.tbl_contains(captures_at_cursor, v) then
return true
end
end
end
return false
end
return context

View File

@ -0,0 +1,127 @@
local compare = require('cmp.config.compare')
local types = require('cmp.types')
local WIDE_HEIGHT = 40
---@return cmp.ConfigSchema
return function()
---@type cmp.ConfigSchema
local config = {
enabled = function()
local disabled = false
disabled = disabled or (vim.api.nvim_get_option_value('buftype', { buf = 0 }) == 'prompt')
disabled = disabled or (vim.fn.reg_recording() ~= '')
disabled = disabled or (vim.fn.reg_executing() ~= '')
return not disabled
end,
performance = {
debounce = 60,
throttle = 30,
fetching_timeout = 500,
confirm_resolve_timeout = 80,
async_budget = 1,
max_view_entries = 200,
},
preselect = types.cmp.PreselectMode.Item,
mapping = {},
snippet = {
expand = vim.fn.has('nvim-0.10') == 1 and function(args)
vim.snippet.expand(args.body)
end or function(_)
error('snippet engine is not configured.')
end,
},
completion = {
autocomplete = {
types.cmp.TriggerEvent.TextChanged,
},
completeopt = 'menu,menuone,noselect',
keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]],
keyword_length = 1,
},
formatting = {
expandable_indicator = true,
fields = { 'abbr', 'kind', 'menu' },
format = function(_, vim_item)
return vim_item
end,
},
matching = {
disallow_fuzzy_matching = false,
disallow_fullfuzzy_matching = false,
disallow_partial_fuzzy_matching = true,
disallow_partial_matching = false,
disallow_prefix_unmatching = false,
disallow_symbol_nonprefix_matching = true,
},
sorting = {
priority_weight = 2,
comparators = {
compare.offset,
compare.exact,
-- compare.scopes,
compare.score,
compare.recently_used,
compare.locality,
compare.kind,
-- compare.sort_text,
compare.length,
compare.order,
},
},
sources = {},
confirmation = {
default_behavior = types.cmp.ConfirmBehavior.Insert,
get_commit_characters = function(commit_characters)
return commit_characters
end,
},
event = {},
experimental = {
ghost_text = false,
},
view = {
entries = {
name = 'custom',
selection_order = 'top_down',
follow_cursor = false,
},
docs = {
auto_open = true,
},
},
window = {
completion = {
border = { '', '', '', '', '', '', '', '' },
winhighlight = 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None',
winblend = vim.o.pumblend,
scrolloff = 0,
col_offset = 0,
side_padding = 1,
scrollbar = true,
},
documentation = {
max_height = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)),
max_width = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))),
border = { '', '', '', ' ', '', '', '', ' ' },
winhighlight = 'FloatBorder:NormalFloat',
winblend = vim.o.pumblend,
},
},
}
return config
end

View File

@ -0,0 +1,230 @@
local types = require('cmp.types')
local misc = require('cmp.utils.misc')
local keymap = require('cmp.utils.keymap')
local function merge_keymaps(base, override)
local normalized_base = {}
for k, v in pairs(base) do
normalized_base[keymap.normalize(k)] = v
end
local normalized_override = {}
for k, v in pairs(override) do
normalized_override[keymap.normalize(k)] = v
end
return misc.merge(normalized_base, normalized_override)
end
local mapping = setmetatable({}, {
__call = function(_, invoke, modes)
if type(invoke) == 'function' then
local map = {}
for _, mode in ipairs(modes or { 'i' }) do
map[mode] = invoke
end
return map
end
return invoke
end,
})
---Mapping preset configuration.
mapping.preset = {}
---Mapping preset insert-mode configuration.
mapping.preset.insert = function(override)
return merge_keymaps(override or {}, {
['<Down>'] = {
i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }),
},
['<Up>'] = {
i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }),
},
['<C-n>'] = {
i = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item({ behavior = types.cmp.SelectBehavior.Insert })
else
cmp.complete()
end
end,
},
['<C-p>'] = {
i = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert })
else
cmp.complete()
end
end,
},
['<C-y>'] = {
i = mapping.confirm({ select = false }),
},
['<C-e>'] = {
i = mapping.abort(),
},
})
end
---Mapping preset cmdline-mode configuration.
mapping.preset.cmdline = function(override)
return merge_keymaps(override or {}, {
['<C-z>'] = {
c = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item()
else
cmp.complete()
end
end,
},
['<Tab>'] = {
c = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item()
else
cmp.complete()
end
end,
},
['<S-Tab>'] = {
c = function()
local cmp = require('cmp')
if cmp.visible() then
cmp.select_prev_item()
else
cmp.complete()
end
end,
},
['<C-n>'] = {
c = function(fallback)
local cmp = require('cmp')
if cmp.visible() then
cmp.select_next_item()
else
fallback()
end
end,
},
['<C-p>'] = {
c = function(fallback)
local cmp = require('cmp')
if cmp.visible() then
cmp.select_prev_item()
else
fallback()
end
end,
},
['<C-e>'] = {
c = mapping.abort(),
},
['<C-y>'] = {
c = mapping.confirm({ select = false }),
},
})
end
---Invoke completion
---@param option? cmp.CompleteParams
mapping.complete = function(option)
return function(fallback)
if not require('cmp').complete(option) then
fallback()
end
end
end
---Complete common string.
mapping.complete_common_string = function()
return function(fallback)
if not require('cmp').complete_common_string() then
fallback()
end
end
end
---Close current completion menu if it displayed.
mapping.close = function()
return function(fallback)
if not require('cmp').close() then
fallback()
end
end
end
---Abort current completion menu if it displayed.
mapping.abort = function()
return function(fallback)
if not require('cmp').abort() then
fallback()
end
end
end
---Scroll documentation window.
mapping.scroll_docs = function(delta)
return function(fallback)
if not require('cmp').scroll_docs(delta) then
fallback()
end
end
end
--- Opens the documentation window.
mapping.open_docs = function()
return function(fallback)
if not require('cmp').open_docs() then
fallback()
end
end
end
--- Close the documentation window.
mapping.close_docs = function()
return function(fallback)
if not require('cmp').close_docs() then
fallback()
end
end
end
---Select next completion item.
mapping.select_next_item = function(option)
return function(fallback)
if not require('cmp').select_next_item(option) then
local release = require('cmp').core:suspend()
fallback()
vim.schedule(release)
end
end
end
---Select prev completion item.
mapping.select_prev_item = function(option)
return function(fallback)
if not require('cmp').select_prev_item(option) then
local release = require('cmp').core:suspend()
fallback()
vim.schedule(release)
end
end
end
---Confirm selection
mapping.confirm = function(option)
return function(fallback)
if not require('cmp').confirm(option) then
fallback()
end
end
end
return mapping

View File

@ -0,0 +1,10 @@
return function(...)
local sources = {}
for i, group in ipairs({ ... }) do
for _, source in ipairs(group) do
source.group_index = i
table.insert(sources, source)
end
end
return sources
end

View File

@ -0,0 +1,16 @@
local window = {}
window.bordered = function(opts)
opts = opts or {}
return {
border = opts.border or 'rounded',
winhighlight = opts.winhighlight or 'Normal:Normal,FloatBorder:FloatBorder,CursorLine:Visual,Search:None',
zindex = opts.zindex or 1001,
scrolloff = opts.scrolloff or 0,
col_offset = opts.col_offset or 0,
side_padding = opts.side_padding or 1,
scrollbar = opts.scrollbar == nil and true or opts.scrollbar,
}
end
return window

View File

@ -0,0 +1,111 @@
local misc = require('cmp.utils.misc')
local pattern = require('cmp.utils.pattern')
local types = require('cmp.types')
local cache = require('cmp.utils.cache')
local api = require('cmp.utils.api')
---@class cmp.Context
---@field public id string
---@field public cache cmp.Cache
---@field public prev_context cmp.Context
---@field public option cmp.ContextOption
---@field public filetype string
---@field public time integer
---@field public bufnr integer
---@field public cursor vim.Position|lsp.Position
---@field public cursor_line string
---@field public cursor_after_line string
---@field public cursor_before_line string
---@field public aborted boolean
local context = {}
---Create new empty context
---@return cmp.Context
context.empty = function()
local ctx = context.new({}) -- dirty hack to prevent recursive call `context.empty`.
ctx.bufnr = -1
ctx.input = ''
ctx.cursor = {}
ctx.cursor.row = -1
ctx.cursor.col = -1
return ctx
end
---Create new context
---@param prev_context? cmp.Context
---@param option? cmp.ContextOption
---@return cmp.Context
context.new = function(prev_context, option)
option = option or {}
local self = setmetatable({}, { __index = context })
self.id = misc.id('cmp.context.new')
self.cache = cache.new()
self.prev_context = prev_context or context.empty()
self.option = option or { reason = types.cmp.ContextReason.None }
self.filetype = vim.api.nvim_get_option_value('filetype', { buf = 0 })
self.time = vim.loop.now()
self.bufnr = vim.api.nvim_get_current_buf()
local cursor = api.get_cursor()
self.cursor_line = api.get_current_line()
self.cursor = {}
self.cursor.row = cursor[1]
self.cursor.col = cursor[2] + 1
self.cursor.line = self.cursor.row - 1
self.cursor.character = misc.to_utfindex(self.cursor_line, self.cursor.col)
self.cursor_before_line = string.sub(self.cursor_line, 1, self.cursor.col - 1)
self.cursor_after_line = string.sub(self.cursor_line, self.cursor.col)
self.aborted = false
return self
end
context.abort = function(self)
self.aborted = true
end
---Return context creation reason.
---@return cmp.ContextReason
context.get_reason = function(self)
return self.option.reason
end
---Get keyword pattern offset
---@return integer
context.get_offset = function(self, keyword_pattern)
return self.cache:ensure({ 'get_offset', keyword_pattern, self.cursor_before_line }, function()
return pattern.offset([[\%(]] .. keyword_pattern .. [[\)\m$]], self.cursor_before_line) or self.cursor.col
end)
end
---Return if this context is changed from previous context or not.
---@return boolean
context.changed = function(self, ctx)
local curr = self
if curr.bufnr ~= ctx.bufnr then
return true
end
if curr.cursor.row ~= ctx.cursor.row then
return true
end
if curr.cursor.col ~= ctx.cursor.col then
return true
end
if curr:get_reason() == types.cmp.ContextReason.Manual then
return true
end
return false
end
---Shallow clone
context.clone = function(self)
local cloned = {}
for k, v in pairs(self) do
cloned[k] = v
end
return cloned
end
return context

View File

@ -0,0 +1,31 @@
local spec = require('cmp.utils.spec')
local context = require('cmp.context')
describe('context', function()
before_each(spec.before)
describe('new', function()
it('middle of text', function()
vim.fn.setline('1', 'function! s:name() abort')
vim.bo.filetype = 'vim'
vim.fn.execute('normal! fm')
local ctx = context.new()
assert.are.equal(ctx.filetype, 'vim')
assert.are.equal(ctx.cursor.row, 1)
assert.are.equal(ctx.cursor.col, 15)
assert.are.equal(ctx.cursor_line, 'function! s:name() abort')
end)
it('tab indent', function()
vim.fn.setline('1', '\t\tab')
vim.bo.filetype = 'vim'
vim.fn.execute('normal! fb')
local ctx = context.new()
assert.are.equal(ctx.filetype, 'vim')
assert.are.equal(ctx.cursor.row, 1)
assert.are.equal(ctx.cursor.col, 4)
assert.are.equal(ctx.cursor_line, '\t\tab')
end)
end)
end)

View File

@ -0,0 +1,524 @@
local debug = require('cmp.utils.debug')
local str = require('cmp.utils.str')
local char = require('cmp.utils.char')
local feedkeys = require('cmp.utils.feedkeys')
local async = require('cmp.utils.async')
local keymap = require('cmp.utils.keymap')
local context = require('cmp.context')
local source = require('cmp.source')
local view = require('cmp.view')
local misc = require('cmp.utils.misc')
local config = require('cmp.config')
local types = require('cmp.types')
local api = require('cmp.utils.api')
local event = require('cmp.utils.event')
---@class cmp.Core
---@field public suspending boolean
---@field public view cmp.View
---@field public sources cmp.Source[]
---@field public context cmp.Context
---@field public event cmp.Event
local core = {}
core.new = function()
local self = setmetatable({}, { __index = core })
self.suspending = false
self.sources = {}
self.context = context.new()
self.event = event.new()
self.view = view.new()
self.view.event:on('keymap', function(...)
self:on_keymap(...)
end)
for _, event_name in ipairs({ 'complete_done', 'menu_opened', 'menu_closed' }) do
self.view.event:on(event_name, function(evt)
self.event:emit(event_name, evt)
end)
end
return self
end
---Register source
---@param s cmp.Source
core.register_source = function(self, s)
self.sources[s.id] = s
end
---Unregister source
---@param source_id integer
core.unregister_source = function(self, source_id)
self.sources[source_id] = nil
end
---Get new context
---@param option? cmp.ContextOption
---@return cmp.Context
core.get_context = function(self, option)
self.context:abort()
local prev = self.context:clone()
prev.prev_context = nil
prev.cache = nil
local ctx = context.new(prev, option)
self:set_context(ctx)
return self.context
end
---Set new context
---@param ctx cmp.Context
core.set_context = function(self, ctx)
self.context = ctx
end
---Suspend completion
core.suspend = function(self)
self.suspending = true
-- It's needed to avoid conflicting with autocmd debouncing.
return vim.schedule_wrap(function()
self.suspending = false
end)
end
---Get sources that sorted by priority
---@param filter? cmp.SourceStatus[]|fun(s: cmp.Source): boolean
---@return cmp.Source[]
core.get_sources = function(self, filter)
local f = function(s)
if type(filter) == 'table' then
return vim.tbl_contains(filter, s.status)
elseif type(filter) == 'function' then
return filter(s)
end
return true
end
local sources = {}
for _, c in pairs(config.get().sources) do
for _, s in pairs(self.sources) do
if c.name == s.name then
if s:is_available() and f(s) then
table.insert(sources, s)
end
end
end
end
return sources
end
---Keypress handler
core.on_keymap = function(self, keys, fallback)
local mode = api.get_mode()
for key, mapping in pairs(config.get().mapping) do
if keymap.equals(key, keys) and mapping[mode] then
return mapping[mode](fallback)
end
end
--Commit character. NOTE: This has a lot of cmp specific implementation to make more user-friendly.
local chars = keymap.t(keys)
local e = self.view:get_active_entry()
if e and vim.tbl_contains(config.get().confirmation.get_commit_characters(e:get_commit_characters()), chars) then
local is_printable = char.is_printable(string.byte(chars, 1))
self:confirm(e, {
behavior = is_printable and 'insert' or 'replace',
commit_character = chars,
}, 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
fallback()
else
self:reset()
end
end)
return
end
fallback()
end
---Prepare completion
core.prepare = function(self)
for keys, mapping in pairs(config.get().mapping) do
for mode in pairs(mapping) do
keymap.listen(mode, keys, function(...)
self:on_keymap(...)
end)
end
end
end
---Check auto-completion
core.on_change = function(self, trigger_event)
local ignore = false
ignore = ignore or self.suspending
ignore = ignore or (vim.fn.pumvisible() == 1 and (vim.v.completed_item).word)
ignore = ignore or not self.view:ready()
if ignore then
self:get_context({ reason = types.cmp.ContextReason.Auto })
return
end
self:autoindent(trigger_event, function()
local ctx = self:get_context({ reason = types.cmp.ContextReason.Auto })
debug.log(('ctx: `%s`'):format(ctx.cursor_before_line))
if ctx:changed(ctx.prev_context) then
self.view:on_change()
debug.log('changed')
if vim.tbl_contains(config.get().completion.autocomplete or {}, trigger_event) then
self:complete(ctx)
else
self.filter.timeout = self.view:visible() and config.get().performance.throttle or 0
self:filter()
end
else
debug.log('unchanged')
end
end)
end
---Cursor moved.
core.on_moved = function(self)
local ignore = false
ignore = ignore or self.suspending
ignore = ignore or (vim.fn.pumvisible() == 1 and (vim.v.completed_item).word)
ignore = ignore or not self.view:visible()
if ignore then
return
end
self:filter()
end
---Returns the suffix of the specified `line`.
---
---Contains `%s`: returns everything after the last `%s` in `line`
---Else: returns `line` unmodified
---@param line string
---@return string suffix
local function find_line_suffix(line)
return line:match('%S*$') --[[@as string]]
end
---Check autoindent
---@param trigger_event cmp.TriggerEvent
---@param callback function
core.autoindent = function(self, trigger_event, callback)
if trigger_event ~= types.cmp.TriggerEvent.TextChanged then
return callback()
end
if not api.is_insert_mode() then
return callback()
end
-- Check prefix
local cursor_before_line = api.get_cursor_before_line()
local prefix = find_line_suffix(cursor_before_line) or ''
if #prefix == 0 then
return callback()
end
-- Reset current completion if indentkeys matched.
for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do
if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then
self:reset()
self:set_context(context.empty())
break
end
end
callback()
end
---Complete common string for current completed entries.
core.complete_common_string = function(self)
if not self.view:visible() or self.view:get_selected_entry() then
return false
end
config.set_onetime({
sources = config.get().sources,
matching = {
disallow_prefix_unmatching = true,
disallow_partial_matching = true,
disallow_fuzzy_matching = true,
},
})
self:filter()
self.filter:sync(1000)
config.set_onetime({})
local cursor = api.get_cursor()
local offset = self.view:get_offset() or cursor[2]
local common_string
for _, e in ipairs(self.view:get_entries()) do
local vim_item = e:get_vim_item(offset)
if not common_string then
common_string = vim_item.word
else
common_string = str.get_common_string(common_string, vim_item.word)
end
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
end
---Invoke completion
---@param ctx cmp.Context
core.complete = function(self, ctx)
if not api.is_suitable_mode() then
return
end
self:set_context(ctx)
-- Invoke completion sources.
local sources = self:get_sources()
for _, s in ipairs(sources) do
local callback
callback = (function(s_)
return function()
local new = context.new(ctx)
if s_.incomplete and new:changed(s_.context) then
s_:complete(new, callback)
else
if not self.view:get_active_entry() then
self.filter.stop()
self.filter.timeout = config.get().performance.debounce
self:filter()
end
end
end
end)(s)
s:complete(ctx, callback)
end
if not self.view:get_active_entry() then
self.filter.timeout = self.view:visible() and config.get().performance.throttle or 1
self:filter()
end
end
---Update completion menu
local async_filter = async.wrap(function(self)
self.filter.timeout = config.get().performance.throttle
-- Check invalid condition.
local ignore = false
ignore = ignore or not api.is_suitable_mode()
if ignore then
return
end
-- Check fetching sources.
local sources = {}
for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do
-- Reserve filter call for timeout.
if not s.incomplete and config.get().performance.fetching_timeout > s:get_fetching_time() then
self.filter.timeout = config.get().performance.fetching_timeout - s:get_fetching_time()
self:filter()
if #sources == 0 then
return
end
end
table.insert(sources, s)
end
local ctx = self:get_context()
-- Display completion results.
local did_open = self.view:open(ctx, sources)
local fetching = #self:get_sources(function(s)
return s.status == source.SourceStatus.FETCHING
end)
-- Check onetime config.
if not did_open and fetching == 0 then
config.set_onetime({})
end
end)
core.filter = async.throttle(async_filter, config.get().performance.throttle)
---Confirm completion.
---@param e cmp.Entry
---@param option cmp.ConfirmOption
---@param callback function
core.confirm = function(self, e, option, callback)
if not (e and not e.confirmed) then
if callback then
callback()
end
return
end
e.confirmed = true
debug.log('entry.confirm', e:get_completion_item())
async.sync(function(done)
e:resolve(done)
end, config.get().performance.confirm_resolve_timeout)
local release = self:suspend()
-- 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 #(e:get_completion_item().additionalTextEdits or {}) == 0 then
e:resolve(function()
local new = context.new()
local text_edits = 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())
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 completion_item.textEdit then
completion_item.textEdit = {}
local insertText = completion_item.insertText
if misc.empty(insertText) then
insertText = nil
end
completion_item.textEdit.newText = 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 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.print({ -- luacheck: ignore
item = e:get_completion_item(),
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)
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)
end
---Reset current completion state
core.reset = function(self)
for _, s in pairs(self.sources) do
s:reset()
end
self.context = context.empty()
end
return core

View File

@ -0,0 +1,231 @@
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')
describe('cmp.core', function()
describe('confirm', function()
---@param request string
---@param filter string
---@param completion_item lsp.CompletionItem
---@param option? { position_encoding_kind: lsp.PositionEncodingKind }
---@return table
local confirm = function(request, filter, completion_item, option)
option = option or {}
local c = core.new()
local s = source.new('spec', {
get_position_encoding_kind = function()
return option.position_encoding_kind or types.lsp.PositionEncodingKind.UTF16
end,
complete = function(_, _, callback)
callback({ completion_item })
end,
})
c:register_source(s)
feedkeys.call(request, 'n', function()
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)
return state
end
describe('insert-mode', function()
before_each(spec.before)
it('label', function()
local state = confirm('iA', 'IU', {
label = 'AIUEO',
})
assert.are.same(state.buffer, { 'AIUEO' })
assert.are.same(state.cursor, { 1, 5 })
end)
it('insertText', function()
local state = confirm('iA', 'IU', {
label = 'AIUEO',
insertText = '_AIUEO_',
})
assert.are.same(state.buffer, { '_AIUEO_' })
assert.are.same(state.cursor, { 1, 7 })
end)
it('textEdit', function()
local state = confirm(keymap.t('i***AEO***<Left><Left><Left><Left><Left>'), 'IU', {
label = 'AIUEO',
textEdit = {
range = {
start = {
line = 0,
character = 3,
},
['end'] = {
line = 0,
character = 6,
},
},
newText = 'foo\nbar\nbaz',
},
})
assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' })
assert.are.same(state.cursor, { 3, 3 })
end)
it('#1552', function()
local state = confirm(keymap.t('ios.'), '', {
filterText = 'IsPermission',
insertTextFormat = 2,
label = 'IsPermission',
textEdit = {
newText = 'IsPermission($0)',
range = {
['end'] = {
character = 3,
line = 0,
},
start = {
character = 3,
line = 0,
},
},
},
})
assert.are.same(state.buffer, { 'os.IsPermission()' })
assert.are.same(state.cursor, { 1, 16 })
end)
it('insertText & snippet', function()
local state = confirm('iA', 'IU', {
label = 'AIUEO',
insertText = 'AIUEO($0)',
insertTextFormat = types.lsp.InsertTextFormat.Snippet,
})
assert.are.same(state.buffer, { 'AIUEO()' })
assert.are.same(state.cursor, { 1, 6 })
end)
it('textEdit & snippet', function()
local state = confirm(keymap.t('i***AEO***<Left><Left><Left><Left><Left>'), 'IU', {
label = 'AIUEO',
insertTextFormat = types.lsp.InsertTextFormat.Snippet,
textEdit = {
range = {
start = {
line = 0,
character = 3,
},
['end'] = {
line = 0,
character = 6,
},
},
newText = 'foo\nba$0r\nbaz',
},
})
assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' })
assert.are.same(state.cursor, { 2, 2 })
end)
local char = '🗿'
for _, case in ipairs({
{
encoding = types.lsp.PositionEncodingKind.UTF8,
char_size = #char,
},
{
encoding = types.lsp.PositionEncodingKind.UTF16,
char_size = select(2, vim.str_utfindex(char)),
},
{
encoding = types.lsp.PositionEncodingKind.UTF32,
char_size = select(1, vim.str_utfindex(char)),
},
}) do
it('textEdit & multibyte: ' .. case.encoding, function()
local state = confirm(keymap.t('i%s:%s%s:%s<Left><Left><Left>'):format(char, char, char, char), char, {
label = char .. char .. char,
textEdit = {
range = {
start = {
line = 0,
character = case.char_size + #':',
},
['end'] = {
line = 0,
character = case.char_size + #':' + case.char_size + case.char_size,
},
},
newText = char .. char .. char .. char .. char,
},
}, {
position_encoding_kind = case.encoding,
})
vim.print({ state = state, case = case })
assert.are.same(state.buffer, { ('%s:%s%s%s%s%s:%s'):format(char, char, char, char, char, char, char) })
assert.are.same(state.cursor, { 1, #('%s:%s%s%s%s%s'):format(char, char, char, char, char, char) })
end)
end
end)
describe('cmdline-mode', function()
before_each(spec.before)
it('label', function()
local state = confirm(':A', 'IU', {
label = 'AIUEO',
})
assert.are.same(state.buffer, { 'AIUEO' })
assert.are.same(state.cursor[2], 5)
end)
it('insertText', function()
local state = confirm(':A', 'IU', {
label = 'AIUEO',
insertText = '_AIUEO_',
})
assert.are.same(state.buffer, { '_AIUEO_' })
assert.are.same(state.cursor[2], 7)
end)
it('textEdit', function()
local state = confirm(keymap.t(':***AEO***<Left><Left><Left><Left><Left>'), 'IU', {
label = 'AIUEO',
textEdit = {
range = {
start = {
line = 0,
character = 3,
},
['end'] = {
line = 0,
character = 6,
},
},
newText = 'AIUEO',
},
})
assert.are.same(state.buffer, { '***AIUEO***' })
assert.are.same(state.cursor[2], 6)
end)
end)
end)
end)

View File

@ -0,0 +1,583 @@
local cache = require('cmp.utils.cache')
local char = require('cmp.utils.char')
local misc = require('cmp.utils.misc')
local str = require('cmp.utils.str')
local snippet = require('cmp.utils.snippet')
local config = require('cmp.config')
local types = require('cmp.types')
local matcher = require('cmp.matcher')
---@class cmp.Entry
---@field public id integer
---@field public cache cmp.Cache
---@field public match_cache cmp.Cache
---@field public score integer
---@field public exact boolean
---@field public matches table
---@field public context cmp.Context
---@field public source cmp.Source
---@field public source_offset integer
---@field public source_insert_range lsp.Range
---@field public source_replace_range lsp.Range
---@field public completion_item lsp.CompletionItem
---@field public item_defaults? lsp.internal.CompletionItemDefaults
---@field public resolved_completion_item lsp.CompletionItem|nil
---@field public resolved_callbacks fun()[]
---@field public resolving boolean
---@field public confirmed boolean
local entry = {}
---Create new entry
---@param ctx cmp.Context
---@param source cmp.Source
---@param completion_item lsp.CompletionItem
---@param item_defaults? lsp.internal.CompletionItemDefaults
---@return cmp.Entry
entry.new = function(ctx, source, completion_item, item_defaults)
local self = setmetatable({}, { __index = entry })
self.id = misc.id('entry.new')
self.cache = cache.new()
self.match_cache = cache.new()
self.score = 0
self.exact = false
self.matches = {}
self.context = ctx
self.source = source
self.source_offset = source.request_offset
self.source_insert_range = source:get_default_insert_range()
self.source_replace_range = source:get_default_replace_range()
self.completion_item = self:fill_defaults(completion_item, item_defaults)
self.item_defaults = item_defaults
self.resolved_completion_item = nil
self.resolved_callbacks = {}
self.resolving = false
self.confirmed = false
return self
end
---Make offset value
---@return integer
entry.get_offset = function(self)
return self.cache:ensure('get_offset', function()
local offset = self.source_offset
if self:get_completion_item().textEdit then
local range = self:get_insert_range()
if range then
offset = self.context.cache:ensure('entry:' .. 'get_offset:' .. tostring(range.start.character), function()
local start = math.min(range.start.character + 1, offset)
for idx = start, self.source_offset do
local byte = string.byte(self.context.cursor_line, idx)
if byte == nil or not char.is_white(byte) then
return idx
end
end
return offset
end)
end
else
-- NOTE
-- The VSCode does not implement this but it's useful if the server does not care about word patterns.
-- We should care about this performance.
local word = self:get_word()
for idx = self.source_offset - 1, self.source_offset - #word, -1 do
if char.is_semantic_index(self.context.cursor_line, idx) then
local c = string.byte(self.context.cursor_line, idx)
if char.is_white(c) then
break
end
local match = true
for i = 1, self.source_offset - idx do
local c1 = string.byte(word, i)
local c2 = string.byte(self.context.cursor_line, idx + i - 1)
if not c1 or not c2 or c1 ~= c2 then
match = false
break
end
end
if match then
offset = math.min(offset, idx)
end
end
end
end
return offset
end)
end
---Create word for vim.CompletedItem
---NOTE: This method doesn't clear the cache after completionItem/resolve.
---@return string
entry.get_word = function(self)
return self.cache:ensure('get_word', function()
--NOTE: This is nvim-cmp specific implementation.
if self:get_completion_item().word then
return self:get_completion_item().word
end
local word
if self:get_completion_item().textEdit and not misc.empty(self:get_completion_item().textEdit.newText) then
word = str.trim(self:get_completion_item().textEdit.newText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = tostring(snippet.parse(word))
end
local overwrite = self:get_overwrite()
if 0 < overwrite[2] or self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.get_word(word, string.byte(self.context.cursor_after_line, 1), overwrite[1] or 0)
end
elseif not misc.empty(self:get_completion_item().insertText) then
word = str.trim(self:get_completion_item().insertText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.get_word(tostring(snippet.parse(word)))
end
else
word = str.trim(self:get_completion_item().label)
end
return str.oneline(word)
end) --[[@as string]]
end
---Get overwrite information
---@return integer[]
entry.get_overwrite = function(self)
return self.cache:ensure('get_overwrite', function()
if self:get_completion_item().textEdit then
local range = self:get_insert_range()
if range then
return self.context.cache:ensure('entry:' .. 'get_overwrite:' .. tostring(range.start.character) .. ':' .. tostring(range['end'].character), function()
local vim_start = range.start.character + 1
local vim_end = range['end'].character + 1
local before = self.context.cursor.col - vim_start
local after = vim_end - self.context.cursor.col
return { before, after }
end)
end
end
return { 0, 0 }
end)
end
---Create filter text
---@return string
entry.get_filter_text = function(self)
return self.cache:ensure('get_filter_text', function()
local word
if self:get_completion_item().filterText then
word = self:get_completion_item().filterText
else
word = str.trim(self:get_completion_item().label)
end
return word
end)
end
---Get LSP's insert text
---@return string
entry.get_insert_text = function(self)
return self.cache:ensure('get_insert_text', function()
local word
if self:get_completion_item().textEdit then
word = str.trim(self:get_completion_item().textEdit.newText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
end
elseif self:get_completion_item().insertText then
word = str.trim(self:get_completion_item().insertText)
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
end
else
word = str.trim(self:get_completion_item().label)
end
return word
end)
end
---Return the item is deprecated or not.
---@return boolean
entry.is_deprecated = function(self)
return self:get_completion_item().deprecated or vim.tbl_contains(self:get_completion_item().tags or {}, types.lsp.CompletionItemTag.Deprecated)
end
---Return view information.
---@param suggest_offset integer
---@param entries_buf integer The buffer this entry will be rendered into.
---@return { abbr: { text: string, bytes: integer, width: integer, hl_group: string }, kind: { text: string, bytes: integer, width: integer, hl_group: string }, menu: { text: string, bytes: integer, width: integer, hl_group: string } }
entry.get_view = function(self, suggest_offset, entries_buf)
local item = self:get_vim_item(suggest_offset)
return self.cache:ensure('get_view:' .. tostring(entries_buf), function()
local view = {}
-- The result of vim.fn.strdisplaywidth depends on which buffer it was
-- called in because it reads the values of the option 'tabstop' when
-- rendering <Tab> characters.
vim.api.nvim_buf_call(entries_buf, function()
view.abbr = {}
view.abbr.text = item.abbr or ''
view.abbr.bytes = #view.abbr.text
view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text)
view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr')
view.kind = {}
view.kind.text = item.kind or ''
view.kind.bytes = #view.kind.text
view.kind.width = vim.fn.strdisplaywidth(view.kind.text)
view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or ''))
view.menu = {}
view.menu.text = item.menu or ''
view.menu.bytes = #view.menu.text
view.menu.width = vim.fn.strdisplaywidth(view.menu.text)
view.menu.hl_group = item.menu_hl_group or 'CmpItemMenu'
view.dup = item.dup
end)
return view
end)
end
---Make vim.CompletedItem
---@param suggest_offset integer
---@return vim.CompletedItem
entry.get_vim_item = function(self, suggest_offset)
return self.cache:ensure('get_vim_item:' .. tostring(suggest_offset), function()
local completion_item = self:get_completion_item()
local word = self:get_word()
local abbr = str.oneline(completion_item.label)
-- ~ indicator
local is_expandable = false
local expandable_indicator = config.get().formatting.expandable_indicator
if #(completion_item.additionalTextEdits or {}) > 0 then
is_expandable = true
elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
is_expandable = self:get_insert_text() ~= word
elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then
is_expandable = true
end
if expandable_indicator and is_expandable then
abbr = abbr .. '~'
end
-- append delta text
if suggest_offset < self:get_offset() then
word = string.sub(self.context.cursor_before_line, suggest_offset, self:get_offset() - 1) .. word
end
-- labelDetails.
local menu = nil
if completion_item.labelDetails then
menu = ''
if completion_item.labelDetails.detail then
menu = menu .. completion_item.labelDetails.detail
end
if completion_item.labelDetails.description then
menu = menu .. completion_item.labelDetails.description
end
end
-- remove duplicated string.
if self:get_offset() ~= self.context.cursor.col then
for i = 1, #word do
if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then
word = string.sub(word, 1, i - 1)
break
end
end
end
local cmp_opts = self:get_completion_item().cmp or {}
local vim_item = {
word = word,
abbr = abbr,
kind = cmp_opts.kind_text or types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
kind_hl_group = cmp_opts.kind_hl_group,
menu = menu,
dup = self:get_completion_item().dup or 1,
}
if config.get().formatting.format then
vim_item = config.get().formatting.format(self, vim_item)
end
vim_item.word = str.oneline(vim_item.word or '')
vim_item.abbr = str.oneline(vim_item.abbr or '')
vim_item.kind = str.oneline(vim_item.kind or '')
vim_item.menu = str.oneline(vim_item.menu or '')
vim_item.equal = 1
vim_item.empty = 1
return vim_item
end)
end
---Get commit characters
---@return string[]
entry.get_commit_characters = function(self)
return self:get_completion_item().commitCharacters or {}
end
---Return insert range
---@return lsp.Range|nil
entry.get_insert_range = function(self)
local insert_range
if self:get_completion_item().textEdit then
if self:get_completion_item().textEdit.insert then
insert_range = self:get_completion_item().textEdit.insert
else
insert_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]]
end
insert_range = self:convert_range_encoding(insert_range)
else
insert_range = {
start = {
line = self.context.cursor.row - 1,
character = self:get_offset() - 1,
},
['end'] = self.source_insert_range['end'],
}
end
return insert_range
end
---Return replace range
---@return lsp.Range|nil
entry.get_replace_range = function(self)
return self.cache:ensure('get_replace_range', function()
local replace_range
if self:get_completion_item().textEdit then
if self:get_completion_item().textEdit.replace then
replace_range = self:get_completion_item().textEdit.replace
else
replace_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]]
end
replace_range = self:convert_range_encoding(replace_range)
end
if not replace_range or ((self.context.cursor.col - 1) == replace_range['end'].character) then
replace_range = {
start = {
line = self.source_replace_range.start.line,
character = self:get_offset() - 1,
},
['end'] = self.source_replace_range['end'],
}
end
return replace_range
end)
end
---Match line.
---@param input string
---@param matching_config cmp.MatchingConfig
---@return { score: integer, matches: table[] }
entry.match = function(self, input, matching_config)
return self.match_cache:ensure(input .. ':' .. (self.resolved_completion_item and '1' or '0' .. ':') .. (matching_config.disallow_fuzzy_matching and '1' or '0') .. ':' .. (matching_config.disallow_partial_fuzzy_matching and '1' or '0') .. ':' .. (matching_config.disallow_partial_matching and '1' or '0') .. ':' .. (matching_config.disallow_prefix_unmatching and '1' or '0') .. ':' .. (matching_config.disallow_symbol_nonprefix_matching and '1' or '0'), function()
local option = {
disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching,
disallow_partial_fuzzy_matching = matching_config.disallow_partial_fuzzy_matching,
disallow_partial_matching = matching_config.disallow_partial_matching,
disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching,
disallow_symbol_nonprefix_matching = matching_config.disallow_symbol_nonprefix_matching,
synonyms = {
self:get_word(),
self:get_completion_item().label,
},
}
local score, matches, filter_text, _
local checked = {} ---@type table<string, boolean>
filter_text = self:get_filter_text()
checked[filter_text] = true
score, matches = matcher.match(input, filter_text, option)
-- Support the language server that doesn't respect VSCode's behaviors.
if score == 0 then
if self:get_completion_item().textEdit and not misc.empty(self:get_completion_item().textEdit.newText) then
local diff = self.source_offset - self:get_offset()
if diff > 0 then
local prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff)
local accept = nil
accept = accept or string.match(prefix, '^[^%a]+$')
accept = accept or string.find(self:get_completion_item().textEdit.newText, prefix, 1, true)
if accept then
filter_text = prefix .. self:get_filter_text()
if not checked[filter_text] then
checked[filter_text] = true
score, matches = matcher.match(input, filter_text, option)
end
end
end
end
end
-- Fix highlight if filterText is not the same to vim_item.abbr.
if score > 0 then
local vim_item = self:get_vim_item(self.source_offset)
filter_text = vim_item.abbr or vim_item.word
if not checked[filter_text] then
local diff = self.source_offset - self:get_offset()
_, matches = matcher.match(input:sub(1 + diff), filter_text, option)
end
end
return { score = score, matches = matches }
end)
end
---Get resolved completion item if possible.
---@return lsp.CompletionItem
entry.get_completion_item = function(self)
return self.cache:ensure('get_completion_item', function()
if self.resolved_completion_item then
-- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts#L588
-- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/base/common/objects.ts#L89
-- @see https://github.com/microsoft/vscode/blob/a00f2e64f4fa9a1f774875562e1e9697d7138ed3/src/vs/editor/contrib/suggest/browser/suggest.ts#L147
local completion_item = misc.copy(self.completion_item)
for k, v in pairs(self.resolved_completion_item) do
completion_item[k] = v or completion_item[k]
end
return completion_item
end
return self.completion_item
end)
end
---Create documentation
---@return string[]
entry.get_documentation = function(self)
local item = self:get_completion_item()
local documents = {}
-- detail
if item.detail and item.detail ~= '' then
local ft = self.context.filetype
local dot_index = string.find(ft, '%.')
if dot_index ~= nil then
ft = string.sub(ft, 0, dot_index - 1)
end
table.insert(documents, {
kind = types.lsp.MarkupKind.Markdown,
value = ('```%s\n%s\n```'):format(ft, str.trim(item.detail)),
})
end
local documentation = item.documentation
if type(documentation) == 'string' and documentation ~= '' then
local value = str.trim(documentation)
if value ~= '' then
table.insert(documents, {
kind = types.lsp.MarkupKind.PlainText,
value = value,
})
end
elseif type(documentation) == 'table' and not misc.empty(documentation.value) then
local value = str.trim(documentation.value)
if value ~= '' then
table.insert(documents, {
kind = documentation.kind,
value = value,
})
end
end
return vim.lsp.util.convert_input_to_markdown_lines(documents)
end
---Get completion item kind
---@return lsp.CompletionItemKind
entry.get_kind = function(self)
return self:get_completion_item().kind or types.lsp.CompletionItemKind.Text
end
---Execute completion item's command.
---@param callback fun()
entry.execute = function(self, callback)
self.source:execute(self:get_completion_item(), callback)
end
---Resolve completion item.
---@param callback fun()
entry.resolve = function(self, callback)
if self.resolved_completion_item then
return callback()
end
table.insert(self.resolved_callbacks, callback)
if not self.resolving then
self.resolving = true
self.source:resolve(self.completion_item, function(completion_item)
self.resolving = false
if not completion_item then
return
end
self.resolved_completion_item = self:fill_defaults(completion_item, self.item_defaults)
self.cache:clear()
for _, c in ipairs(self.resolved_callbacks) do
c()
end
end)
end
end
---@param completion_item lsp.CompletionItem
---@param defaults? lsp.internal.CompletionItemDefaults
---@return lsp.CompletionItem
entry.fill_defaults = function(_, completion_item, defaults)
defaults = defaults or {}
if defaults.data then
completion_item.data = completion_item.data or defaults.data
end
if defaults.commitCharacters then
completion_item.commitCharacters = completion_item.commitCharacters or defaults.commitCharacters
end
if defaults.insertTextFormat then
completion_item.insertTextFormat = completion_item.insertTextFormat or defaults.insertTextFormat
end
if defaults.insertTextMode then
completion_item.insertTextMode = completion_item.insertTextMode or defaults.insertTextMode
end
if defaults.editRange then
if not completion_item.textEdit then
if defaults.editRange.insert then
completion_item.textEdit = {
insert = defaults.editRange.insert,
replace = defaults.editRange.replace,
newText = completion_item.textEditText or completion_item.label,
}
else
completion_item.textEdit = {
range = defaults.editRange, --[[@as lsp.Range]]
newText = completion_item.textEditText or completion_item.label,
}
end
end
end
return completion_item
end
---Convert the oneline range encoding.
entry.convert_range_encoding = function(self, range)
local from_encoding = self.source:get_position_encoding_kind()
return self.context.cache:ensure('entry.convert_range_encoding:' .. range.start.character .. ':' .. range['end'].character .. ':' .. from_encoding, function()
return {
start = types.lsp.Position.to_utf8(self.context.cursor_line, range.start, from_encoding),
['end'] = types.lsp.Position.to_utf8(self.context.cursor_line, range['end'], from_encoding),
}
end)
end
---Return true if the entry is invalid.
entry.is_invalid = function(self)
local is_invalid = false
is_invalid = is_invalid or misc.empty(self.completion_item.label)
if self.completion_item.textEdit then
local range = self.completion_item.textEdit.range or self.completion_item.textEdit.insert
is_invalid = is_invalid or range.start.line ~= range['end'].line or range.start.line ~= self.context.cursor.line
end
return is_invalid
end
return entry

View File

@ -0,0 +1,366 @@
local spec = require('cmp.utils.spec')
local entry = require('cmp.entry')
describe('entry', function()
before_each(spec.before)
it('one char', function()
local state = spec.state('@.', 1, 3)
state.input('@')
local e = entry.new(state.manual(), state.source(), {
label = '@',
})
assert.are.equal(e:get_offset(), 3)
assert.are.equal(e:get_vim_item(e:get_offset()).word, '@')
end)
it('word length (no fix)', function()
local state = spec.state('a.b', 1, 4)
state.input('.')
local e = entry.new(state.manual(), state.source(), {
label = 'b',
})
assert.are.equal(e:get_offset(), 5)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b')
end)
it('word length (fix)', function()
local state = spec.state('a.b', 1, 4)
state.input('.')
local e = entry.new(state.manual(), state.source(), {
label = 'b.',
})
assert.are.equal(e:get_offset(), 3)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b.')
end)
it('semantic index (no fix)', function()
local state = spec.state('a.bc', 1, 5)
state.input('.')
local e = entry.new(state.manual(), state.source(), {
label = 'c.',
})
assert.are.equal(e:get_offset(), 6)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'c.')
end)
it('semantic index (fix)', function()
local state = spec.state('a.bc', 1, 5)
state.input('.')
local e = entry.new(state.manual(), state.source(), {
label = 'bc.',
})
assert.are.equal(e:get_offset(), 3)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'bc.')
end)
it('[vscode-html-language-server] 1', function()
local state = spec.state(' </>', 1, 7)
state.input('.')
local e = entry.new(state.manual(), state.source(), {
label = '/div',
textEdit = {
range = {
start = {
line = 0,
character = 0,
},
['end'] = {
line = 0,
character = 6,
},
},
newText = ' </div',
},
})
assert.are.equal(e:get_offset(), 5)
assert.are.equal(e:get_vim_item(e:get_offset()).word, '</div')
end)
it('[clangd] 1', function()
--NOTE: clangd does not return `.foo` as filterText but we should care about it.
--nvim-cmp does care it by special handling in entry.lua.
local state = spec.state('foo', 1, 4)
state.input('.')
local e = entry.new(state.manual(), state.source(), {
insertText = '->foo',
label = ' foo',
textEdit = {
newText = '->foo',
range = {
start = {
character = 3,
line = 1,
},
['end'] = {
character = 4,
line = 1,
},
},
},
})
assert.are.equal(e:get_vim_item(4).word, '->foo')
assert.are.equal(e:get_filter_text(), 'foo')
end)
it('[typescript-language-server] 1', function()
local state = spec.state('Promise.resolve()', 1, 18)
state.input('.')
local e = entry.new(state.manual(), state.source(), {
label = 'catch',
})
-- The offset will be 18 in this situation because the server returns `[Symbol]` as candidate.
assert.are.equal(e:get_vim_item(18).word, '.catch')
assert.are.equal(e:get_filter_text(), 'catch')
end)
it('[typescript-language-server] 2', function()
local state = spec.state('Promise.resolve()', 1, 18)
state.input('.')
local e = entry.new(state.manual(), state.source(), {
filterText = '.Symbol',
label = 'Symbol',
textEdit = {
newText = '[Symbol]',
range = {
['end'] = {
character = 18,
line = 0,
},
start = {
character = 17,
line = 0,
},
},
},
})
assert.are.equal(e:get_vim_item(18).word, '[Symbol]')
assert.are.equal(e:get_filter_text(), '.Symbol')
end)
it('[lua-language-server] 1', function()
local state = spec.state("local m = require'cmp.confi", 1, 28)
local e
-- press g
state.input('g')
e = entry.new(state.manual(), state.source(), {
insertTextFormat = 2,
label = 'cmp.config',
textEdit = {
newText = 'cmp.config',
range = {
['end'] = {
character = 27,
line = 1,
},
start = {
character = 18,
line = 1,
},
},
},
})
assert.are.equal(e:get_vim_item(19).word, 'cmp.config')
assert.are.equal(e:get_filter_text(), 'cmp.config')
-- press '
state.input("'")
e = entry.new(state.manual(), state.source(), {
insertTextFormat = 2,
label = 'cmp.config',
textEdit = {
newText = 'cmp.config',
range = {
['end'] = {
character = 27,
line = 1,
},
start = {
character = 18,
line = 1,
},
},
},
})
assert.are.equal(e:get_vim_item(19).word, 'cmp.config')
assert.are.equal(e:get_filter_text(), 'cmp.config')
end)
it('[lua-language-server] 2', function()
local state = spec.state("local m = require'cmp.confi", 1, 28)
local e
-- press g
state.input('g')
e = entry.new(state.manual(), state.source(), {
insertTextFormat = 2,
label = 'lua.cmp.config',
textEdit = {
newText = 'lua.cmp.config',
range = {
['end'] = {
character = 27,
line = 1,
},
start = {
character = 18,
line = 1,
},
},
},
})
assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config')
assert.are.equal(e:get_filter_text(), 'lua.cmp.config')
-- press '
state.input("'")
e = entry.new(state.manual(), state.source(), {
insertTextFormat = 2,
label = 'lua.cmp.config',
textEdit = {
newText = 'lua.cmp.config',
range = {
['end'] = {
character = 27,
line = 1,
},
start = {
character = 18,
line = 1,
},
},
},
})
assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config')
assert.are.equal(e:get_filter_text(), 'lua.cmp.config')
end)
it('[intelephense] 1', function()
local state = spec.state('\t\t', 1, 4)
-- press g
state.input('$')
local e = entry.new(state.manual(), state.source(), {
kind = 6,
label = '$this',
sortText = '$this',
textEdit = {
newText = '$this',
range = {
['end'] = {
character = 3,
line = 1,
},
start = {
character = 2,
line = 1,
},
},
},
})
assert.are.equal(e:get_vim_item(e:get_offset()).word, '$this')
assert.are.equal(e:get_filter_text(), '$this')
end)
it('[odin-language-server] 1', function()
local state = spec.state('\t\t', 1, 4)
-- press g
state.input('s')
local e = entry.new(state.manual(), state.source(), {
additionalTextEdits = {},
command = {
arguments = {},
command = '',
title = '',
},
deprecated = false,
detail = 'string',
documentation = '',
insertText = '',
insertTextFormat = 1,
kind = 14,
label = 'string',
tags = {},
})
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'string')
end)
it('[#47] word should not contain \\n character', function()
local state = spec.state('', 1, 1)
-- press g
state.input('_')
local e = entry.new(state.manual(), state.source(), {
kind = 6,
label = '__init__',
insertTextFormat = 1,
insertText = '__init__(self) -> None:\n pass',
})
assert.are.equal(e:get_vim_item(e:get_offset()).word, '__init__(self) -> None:')
assert.are.equal(e:get_filter_text(), '__init__')
end)
-- I can't understand this test case...
-- it('[#1533] keyword pattern that include whitespace', function()
-- local state = spec.state(' ', 1, 2)
-- local state_source = state.source()
-- state_source.get_keyword_pattern = function(_)
-- return '.'
-- end
-- state.input(' ')
-- local e = entry.new(state.manual(), state_source, {
-- filterText = "constructor() {\n ... st = 'test';\n ",
-- kind = 1,
-- label = "constructor() {\n ... st = 'test';\n }",
-- textEdit = {
-- newText = "constructor() {\n this.test = 'test';\n }",
-- range = {
-- ['end'] = {
-- character = 2,
-- line = 2,
-- },
-- start = {
-- character = 0,
-- line = 2,
-- },
-- },
-- },
-- })
-- assert.are.equal(e:get_offset(), 2)
-- assert.are.equal(e:get_vim_item(e:get_offset()).word, 'constructor() {')
-- end)
it('[#1533] clang regression test', function()
local state = spec.state('jsonReader', 3, 11)
local state_source = state.source()
state.input('.')
local e = entry.new(state.manual(), state_source, {
filterText = 'getPath()',
kind = 1,
label = 'getPath()',
textEdit = {
newText = 'getPath()',
range = {
['end'] = {
character = 11,
col = 12,
line = 2,
row = 3,
},
start = {
character = 11,
line = 2,
},
},
},
})
assert.are.equal(e:get_offset(), 12)
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'getPath()')
end)
end)

View File

@ -0,0 +1,375 @@
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 cmp = {}
cmp.core = core.new()
---Expose types
for k, v in pairs(require('cmp.types.cmp')) do
cmp[k] = v
end
cmp.lsp = require('cmp.types.lsp')
cmp.vim = require('cmp.types.vim')
---Expose event
cmp.event = cmp.core.event
---Export mapping for special case
cmp.mapping = require('cmp.config.mapping')
---Export default config presets
cmp.config = {}
cmp.config.disable = misc.none
cmp.config.compare = require('cmp.config.compare')
cmp.config.sources = require('cmp.config.sources')
cmp.config.mapping = require('cmp.config.mapping')
cmp.config.window = require('cmp.config.window')
---Sync asynchronous process.
cmp.sync = function(callback)
return function(...)
cmp.core.filter:sync(1000)
if callback then
return callback(...)
end
end
end
---Suspend completion.
cmp.suspend = function()
return cmp.core:suspend()
end
---Register completion sources
---@param name string
---@param s cmp.Source
---@return integer
cmp.register_source = function(name, s)
local src = source.new(name, s)
cmp.core:register_source(src)
return src.id
end
---Unregister completion source
---@param id integer
cmp.unregister_source = function(id)
cmp.core:unregister_source(id)
end
---Get current configuration.
---@return cmp.ConfigSchema
cmp.get_config = function()
return require('cmp.config').get()
end
---Invoke completion manually
---@param option cmp.CompleteParams
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.
cmp.complete_common_string = cmp.sync(function()
return cmp.core:complete_common_string()
end)
---Return view is visible or not.
cmp.visible = cmp.sync(function()
return cmp.core.view:visible() or vim.fn.pumvisible() == 1
end)
---Get current selected entry or nil
cmp.get_selected_entry = cmp.sync(function()
return cmp.core.view:get_selected_entry()
end)
---Get current active entry or nil
cmp.get_active_entry = cmp.sync(function()
return cmp.core.view:get_active_entry()
end)
---Get current all entries
cmp.get_entries = cmp.sync(function()
return cmp.core.view:get_entries()
end)
---Close current completion
cmp.close = cmp.sync(function()
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:close()
cmp.core:reset()
vim.schedule(release)
return true
else
return false
end
end)
---Abort current completion
cmp.abort = cmp.sync(function()
if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:abort()
cmp.core:reset()
vim.schedule(release)
return true
else
return false
end
end)
---Select next item if possible
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')
end
return true
end
return false
end)
---Select prev item if possible
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')
end
return true
end
return false
end)
---Scrolling documentation window if possible
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)
---Whether the documentation window is visible or not.
cmp.visible_docs = cmp.sync(function()
return cmp.core.view.docs_view:visible()
end)
---Opens the documentation window.
cmp.open_docs = cmp.sync(function()
if not cmp.visible_docs() then
cmp.core.view:open_docs()
return true
else
return false
end
end)
---Closes the documentation window.
cmp.close_docs = cmp.sync(function()
if cmp.visible_docs() then
cmp.core.view:close_docs()
return true
else
return false
end
end)
---Confirm completion
cmp.confirm = cmp.sync(function(option, callback)
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()
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)
---Show status
cmp.status = function()
local kinds = {}
kinds.available = {}
kinds.unavailable = {}
kinds.installed = {}
kinds.invalid = {}
local names = {}
for _, s in pairs(cmp.core.sources) do
names[s.name] = true
if config.get_source_config(s.name) then
if s:is_available() then
table.insert(kinds.available, s:get_debug_name())
else
table.insert(kinds.unavailable, s:get_debug_name())
end
else
table.insert(kinds.installed, s:get_debug_name())
end
end
for _, s in ipairs(config.get().sources) do
if not names[s.name] then
table.insert(kinds.invalid, s.name)
end
end
if #kinds.available > 0 then
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
vim.api.nvim_echo({ { '# ready source names\n', 'Special' } }, false, {})
for _, name in ipairs(kinds.available) do
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
end
end
if #kinds.unavailable > 0 then
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
vim.api.nvim_echo({ { '# unavailable source names\n', 'Comment' } }, false, {})
for _, name in ipairs(kinds.unavailable) do
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
end
end
if #kinds.installed > 0 then
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
vim.api.nvim_echo({ { '# unused source names\n', 'WarningMsg' } }, false, {})
for _, name in ipairs(kinds.installed) do
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
end
end
if #kinds.invalid > 0 then
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
vim.api.nvim_echo({ { '# unknown source names\n', 'ErrorMsg' } }, false, {})
for _, name in ipairs(kinds.invalid) do
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
end
end
end
---@type cmp.Setup
cmp.setup = setmetatable({
global = function(c)
config.set_global(c)
end,
filetype = function(filetype, c)
config.set_filetype(c, filetype)
end,
buffer = function(c)
config.set_buffer(c, vim.api.nvim_get_current_buf())
end,
cmdline = function(type, c)
config.set_cmdline(c, type)
end,
}, {
__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()
cmp.config.compare.locality:update()
cmp.core:prepare()
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))
-- 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)
autocmd.subscribe('CmdlineChanged', async.debounce_next_tick(on_text_changed))
autocmd.subscribe('CursorMovedI', function()
if config.enabled() then
cmp.core:on_moved()
else
cmp.core:reset()
cmp.core.view:close()
end
end)
-- If make this asynchronous, the completion menu will not close when the command output is displayed.
autocmd.subscribe({ 'InsertLeave', 'CmdlineLeave' }, function()
cmp.core:reset()
cmp.core.view:close()
end)
cmp.event:on('complete_done', function(evt)
if evt.entry then
cmp.config.compare.recently_used:add_entry(evt.entry)
end
cmp.config.compare.scopes:update()
cmp.config.compare.locality:update()
end)
cmp.event:on('confirm_done', function(evt)
if evt.entry then
cmp.config.compare.recently_used:add_entry(evt.entry)
end
end)
return cmp

View File

@ -0,0 +1,348 @@
local char = require('cmp.utils.char')
local matcher = {}
matcher.WORD_BOUNDALY_ORDER_FACTOR = 10
matcher.PREFIX_FACTOR = 8
matcher.NOT_FUZZY_FACTOR = 6
---@type function
matcher.debug = function(...)
return ...
end
--- score
--
-- ### The score
--
-- The `score` is `matched char count` generally.
--
-- But cmp will fix the score with some of the below points so the actual score is not `matched char count`.
--
-- 1. Word boundary order
--
-- cmp prefers the match that near by word-beggining.
--
-- 2. Strict case
--
-- cmp prefers strict match than ignorecase match.
--
--
-- ### Matching specs.
--
-- 1. Prefix matching per word boundary
--
-- `bora` -> `border-radius` # imaginary score: 4
-- ^^~~ ^^ ~~
--
-- 2. Try sequential match first
--
-- `woroff` -> `word_offset` # imaginary score: 6
-- ^^^~~~ ^^^ ~~~
--
-- * The `woroff`'s second `o` should not match `word_offset`'s first `o`
--
-- 3. Prefer early word boundary
--
-- `call` -> `call` # imaginary score: 4.1
-- ^^^^ ^^^^
-- `call` -> `condition_all` # imaginary score: 4
-- ^~~~ ^ ~~~
--
-- 4. Prefer strict match
--
-- `Buffer` -> `Buffer` # imaginary score: 6.1
-- ^^^^^^ ^^^^^^
-- `buffer` -> `Buffer` # imaginary score: 6
-- ^^^^^^ ^^^^^^
--
-- 5. Use remaining characters for substring match
--
-- `fmodify` -> `fnamemodify` # imaginary score: 1
-- ^~~~~~~ ^ ~~~~~~
--
-- 6. Avoid unexpected match detection
--
-- `candlesingle` -> candle#accept#single
-- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~
-- * The `accept`'s `a` should not match to `candle`'s `a`
--
-- 7. Avoid false positive matching
--
-- `,` -> print,
-- ~
-- * Typically, the middle match with symbol characters only is false positive. should be ignored.
-- This doesn't work for command line completions like ":b foo_" which we like to match
-- "lib/foo_bar.txt". The option disallow_symbol_nonprefix_matching controls this and defaults
-- to preventing matches like these. The documentation recommends it for command line completion.
--
--
---Match entry
---@param input string
---@param word string
---@param option { synonyms: string[], disallow_fullfuzzy_matching: boolean, disallow_fuzzy_matching: boolean, disallow_partial_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean, disallow_symbol_nonprefix_matching: boolean }
---@return integer, table
matcher.match = function(input, word, option)
option = option or {}
-- Empty input
if #input == 0 then
return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR, {}
end
-- Ignore if input is long than word
if #input > #word then
return 0, {}
end
-- Check prefix matching.
if option.disallow_prefix_unmatching then
if not char.match(string.byte(input, 1), string.byte(word, 1)) then
return 0, {}
end
end
-- Gather matched regions
local matches = {}
local input_start_index = 1
local input_end_index = 1
local word_index = 1
local word_bound_index = 1
local no_symbol_match = false
while input_end_index <= #input and word_index <= #word do
local m = matcher.find_match_region(input, input_start_index, input_end_index, word, word_index)
if m and input_end_index <= m.input_match_end then
m.index = word_bound_index
input_start_index = m.input_match_start + 1
input_end_index = m.input_match_end + 1
no_symbol_match = no_symbol_match or m.no_symbol_match
word_index = char.get_next_semantic_index(word, m.word_match_end)
table.insert(matches, m)
else
word_index = char.get_next_semantic_index(word, word_index)
end
word_bound_index = word_bound_index + 1
end
-- Check partial matching.
if option.disallow_partial_matching and #matches > 1 then
return 0, {}
end
if #matches == 0 then
if not option.disallow_fuzzy_matching and not option.disallow_prefix_unmatching and not option.disallow_partial_fuzzy_matching then
if matcher.fuzzy(input, word, matches, option) then
return 1, matches
end
end
return 0, {}
end
matcher.debug(word, matches)
-- Add prefix bonus
local prefix = false
if matches[1].input_match_start == 1 and matches[1].word_match_start == 1 then
prefix = true
else
for _, synonym in ipairs(option.synonyms or {}) do
prefix = true
local o = 1
for i = matches[1].input_match_start, matches[1].input_match_end do
if not char.match(string.byte(synonym, o), string.byte(input, i)) then
prefix = false
break
end
o = o + 1
end
if prefix then
break
end
end
end
if no_symbol_match and not prefix then
if option.disallow_symbol_nonprefix_matching then
return 0, {}
end
end
-- Compute prefix match score
local score = prefix and matcher.PREFIX_FACTOR or 0
local offset = prefix and matches[1].index - 1 or 0
local idx = 1
for _, m in ipairs(matches) do
local s = 0
for i = math.max(idx, m.input_match_start), m.input_match_end do
s = s + 1
idx = i
end
idx = idx + 1
if s > 0 then
s = s * (1 + m.strict_ratio)
s = s * (1 + math.max(0, matcher.WORD_BOUNDALY_ORDER_FACTOR - (m.index - offset)) / matcher.WORD_BOUNDALY_ORDER_FACTOR)
score = score + s
end
end
-- Check remaining input as fuzzy
if matches[#matches].input_match_end < #input then
if not option.disallow_fuzzy_matching then
if not option.disallow_partial_fuzzy_matching or prefix then
if matcher.fuzzy(input, word, matches, option) then
return score, matches
end
end
end
return 0, {}
end
return score + matcher.NOT_FUZZY_FACTOR, matches
end
--- fuzzy
matcher.fuzzy = function(input, word, matches, option)
local input_index = matches[#matches] and (matches[#matches].input_match_end + 1) or 1
-- Lately specified middle of text.
for i = 1, #matches - 1 do
local curr_match = matches[i]
local next_match = matches[i + 1]
local word_offset = 0
local word_index = char.get_next_semantic_index(word, curr_match.word_match_end)
while word_offset + word_index < next_match.word_match_start and input_index <= #input do
if char.match(string.byte(word, word_index + word_offset), string.byte(input, input_index)) then
input_index = input_index + 1
word_offset = word_offset + 1
else
word_index = char.get_next_semantic_index(word, word_index + word_offset)
word_offset = 0
end
end
end
-- Remaining text fuzzy match.
local matched = false
local word_offset = 0
local word_index = matches[#matches] and (matches[#matches].word_match_end + 1) or 1
local input_match_start = -1
local input_match_end = -1
local word_match_start = -1
local strict_count = 0
local match_count = 0
while word_offset + word_index <= #word and input_index <= #input do
local c1, c2 = string.byte(word, word_index + word_offset), string.byte(input, input_index)
if char.match(c1, c2) then
if not matched then
input_match_start = input_index
word_match_start = word_index + word_offset
end
matched = true
input_index = input_index + 1
strict_count = strict_count + (c1 == c2 and 1 or 0)
match_count = match_count + 1
else
if option.disallow_fullfuzzy_matching then
break
else
if matched then
table.insert(matches, {
input_match_start = input_match_start,
input_match_end = input_index - 1,
word_match_start = word_match_start,
word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count,
fuzzy = true,
})
end
end
matched = false
end
word_offset = word_offset + 1
end
if matched and input_index > #input then
table.insert(matches, {
input_match_start = input_match_start,
input_match_end = input_match_end,
word_match_start = word_match_start,
word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count,
fuzzy = true,
})
return true
end
return false
end
--- find_match_region
matcher.find_match_region = function(input, input_start_index, input_end_index, word, word_index)
-- determine input position ( woroff -> word_offset )
while input_start_index < input_end_index do
if char.match(string.byte(input, input_end_index), string.byte(word, word_index)) then
break
end
input_end_index = input_end_index - 1
end
-- Can't determine input position
if input_end_index < input_start_index then
return nil
end
local input_match_start = -1
local input_index = input_end_index
local word_offset = 0
local strict_count = 0
local match_count = 0
local no_symbol_match = false
while input_index <= #input and word_index + word_offset <= #word do
local c1 = string.byte(input, input_index)
local c2 = string.byte(word, word_index + word_offset)
if char.match(c1, c2) then
-- Match start.
if input_match_start == -1 then
input_match_start = input_index
end
strict_count = strict_count + (c1 == c2 and 1 or 0)
match_count = match_count + 1
word_offset = word_offset + 1
no_symbol_match = no_symbol_match or char.is_symbol(c1)
else
-- Match end (partial region)
if input_match_start ~= -1 then
return {
input_match_start = input_match_start,
input_match_end = input_index - 1,
word_match_start = word_index,
word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count,
no_symbol_match = no_symbol_match,
fuzzy = false,
}
else
return nil
end
end
input_index = input_index + 1
end
-- Match end (whole region)
if input_match_start ~= -1 then
return {
input_match_start = input_match_start,
input_match_end = input_index - 1,
word_match_start = word_index,
word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count,
no_symbol_match = no_symbol_match,
fuzzy = false,
}
end
return nil
end
return matcher

View File

@ -0,0 +1,105 @@
local spec = require('cmp.utils.spec')
local default_config = require('cmp.config.default')
local matcher = require('cmp.matcher')
describe('matcher', function()
before_each(spec.before)
it('match', function()
local config = default_config()
assert.is.truthy(matcher.match('', 'a', config.matching) >= 1)
assert.is.truthy(matcher.match('a', 'a', config.matching) >= 1)
assert.is.truthy(matcher.match('ab', 'a', config.matching) == 0)
assert.is.truthy(matcher.match('ab', 'ab', config.matching) > matcher.match('ab', 'a_b', config.matching))
assert.is.truthy(matcher.match('ab', 'a_b_c', config.matching) > matcher.match('ac', 'a_b_c', config.matching))
assert.is.truthy(matcher.match('bora', 'border-radius', config.matching) >= 1)
assert.is.truthy(matcher.match('woroff', 'word_offset', config.matching) >= 1)
assert.is.truthy(matcher.match('call', 'call', config.matching) > matcher.match('call', 'condition_all', config.matching))
assert.is.truthy(matcher.match('Buffer', 'Buffer', config.matching) > matcher.match('Buffer', 'buffer', config.matching))
assert.is.truthy(matcher.match('luacon', 'lua_context', config.matching) > matcher.match('luacon', 'LuaContext', config.matching))
assert.is.truthy(matcher.match('fmodify', 'fnamemodify', config.matching) >= 1)
assert.is.truthy(matcher.match('candlesingle', 'candle#accept#single', config.matching) >= 1)
assert.is.truthy(matcher.match('vi', 'void#', config.matching) >= 1)
assert.is.truthy(matcher.match('vo', 'void#', config.matching) >= 1)
assert.is.truthy(matcher.match('var_', 'var_dump', config.matching) >= 1)
assert.is.truthy(matcher.match('conso', 'console', config.matching) > matcher.match('conso', 'ConstantSourceNode', config.matching))
assert.is.truthy(matcher.match('usela', 'useLayoutEffect', config.matching) > matcher.match('usela', 'useDataLayer', config.matching))
assert.is.truthy(matcher.match('my_', 'my_awesome_variable', config.matching) > matcher.match('my_', 'completion_matching_strategy_list', config.matching))
assert.is.truthy(matcher.match('2', '[[2021', config.matching) >= 1)
assert.is.truthy(matcher.match(',', 'pri,', config.matching) == 0)
assert.is.truthy(matcher.match('/', '/**', config.matching) >= 1)
assert.is.truthy(matcher.match('true', 'v:true', { synonyms = { 'true' } }, config.matching) == matcher.match('true', 'true', config.matching))
assert.is.truthy(matcher.match('g', 'get', { synonyms = { 'get' } }, config.matching) > matcher.match('g', 'dein#get', { 'dein#get' }, config.matching))
assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = true }, config.matching) == 0)
assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = false }, config.matching) >= 1)
assert.is.truthy(matcher.match('emg', 'error_msg', config.matching) >= 1)
assert.is.truthy(matcher.match('sasr', 'saved_splitright', config.matching) >= 1)
-- TODO: #1420 test-case
-- assert.is.truthy(matcher.match('asset_', '????') >= 0)
local score, matches
score, matches = matcher.match('tail', 'HCDetails', {
disallow_fuzzy_matching = false,
disallow_partial_matching = false,
disallow_prefix_unmatching = false,
disallow_partial_fuzzy_matching = false,
disallow_symbol_nonprefix_matching = true,
})
assert.is.truthy(score >= 1)
assert.equals(matches[1].word_match_start, 5)
score = matcher.match('tail', 'HCDetails', {
disallow_fuzzy_matching = false,
disallow_partial_matching = false,
disallow_prefix_unmatching = false,
disallow_partial_fuzzy_matching = true,
disallow_symbol_nonprefix_matching = true,
})
assert.is.truthy(score == 0)
end)
it('disallow_fuzzy_matching', function()
assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = true }) == 0)
assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1)
end)
it('disallow_fullfuzzy_matching', function()
assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = true }) == 0)
assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = false }) >= 1)
end)
it('disallow_partial_matching', function()
assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = true }) == 0)
assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = false }) >= 1)
assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = true }) >= 1)
assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = false }) >= 1)
end)
it('disallow_prefix_unmatching', function()
assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = true }) == 0)
assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = false }) >= 1)
end)
it('disallow_symbol_nonprefix_matching', function()
assert.is.truthy(matcher.match('foo_', 'b foo_bar', { disallow_symbol_nonprefix_matching = true }) == 0)
assert.is.truthy(matcher.match('foo_', 'b foo_bar', { disallow_symbol_nonprefix_matching = false }) >= 1)
end)
it('debug', function()
matcher.debug = function(...)
print(vim.inspect({ ... }))
end
-- print(vim.inspect({
-- a = matcher.match('true', 'v:true', { 'true' }),
-- b = matcher.match('true', 'true'),
-- }))
end)
end)

View File

@ -0,0 +1,401 @@
local context = require('cmp.context')
local config = require('cmp.config')
local entry = require('cmp.entry')
local debug = require('cmp.utils.debug')
local misc = require('cmp.utils.misc')
local cache = require('cmp.utils.cache')
local types = require('cmp.types')
local async = require('cmp.utils.async')
local pattern = require('cmp.utils.pattern')
local char = require('cmp.utils.char')
---@class cmp.Source
---@field public id integer
---@field public name string
---@field public source any
---@field public cache cmp.Cache
---@field public revision integer
---@field public incomplete boolean
---@field public is_triggered_by_symbol boolean
---@field public entries cmp.Entry[]
---@field public offset integer
---@field public request_offset integer
---@field public context cmp.Context
---@field public completion_context lsp.CompletionContext|nil
---@field public status cmp.SourceStatus
---@field public complete_dedup function
local source = {}
---@alias cmp.SourceStatus 1 | 2 | 3
source.SourceStatus = {}
source.SourceStatus.WAITING = 1
source.SourceStatus.FETCHING = 2
source.SourceStatus.COMPLETED = 3
---@return cmp.Source
source.new = function(name, s)
local self = setmetatable({}, { __index = source })
self.id = misc.id('cmp.source.new')
self.name = name
self.source = s
self.cache = cache.new()
self.complete_dedup = async.dedup()
self.revision = 0
self:reset()
return self
end
---Reset current completion state
source.reset = function(self)
self.cache:clear()
self.revision = self.revision + 1
self.context = context.empty()
self.is_triggered_by_symbol = false
self.incomplete = false
self.entries = {}
self.offset = -1
self.request_offset = -1
self.completion_context = nil
self.status = source.SourceStatus.WAITING
self.complete_dedup(function() end)
end
---Return source config
---@return cmp.SourceConfig
source.get_source_config = function(self)
return config.get_source_config(self.name) or {}
end
---Return matching config
---@return cmp.MatchingConfig
source.get_matching_config = function()
return config.get().matching
end
---Get fetching time
source.get_fetching_time = function(self)
if self.status == source.SourceStatus.FETCHING then
return vim.loop.now() - self.context.time
end
return 100 * 1000 -- return pseudo time if source isn't fetching.
end
---Return filtered entries
---@param ctx cmp.Context
---@return cmp.Entry[]
source.get_entries = function(self, ctx)
if self.offset == -1 then
return {}
end
local target_entries = self.entries
if not self.incomplete then
local prev = self.cache:get({ 'get_entries', tostring(self.revision) })
if prev and ctx.cursor.row == prev.ctx.cursor.row and self.offset == prev.offset then
-- only use prev entries when cursor is moved forward.
-- and the pattern offset is the same.
if prev.ctx.cursor.col <= ctx.cursor.col then
target_entries = prev.entries
end
end
end
local entry_filter = self:get_entry_filter()
local inputs = {}
---@type cmp.Entry[]
local entries = {}
local matching_config = self:get_matching_config()
for _, e in ipairs(target_entries) do
local o = e:get_offset()
if not inputs[o] then
inputs[o] = string.sub(ctx.cursor_before_line, o)
end
local match = e:match(inputs[o], matching_config)
e.score = match.score
e.exact = false
if e.score >= 1 then
e.matches = match.matches
e.exact = e:get_filter_text() == inputs[o] or e:get_word() == inputs[o]
if entry_filter(e, ctx) then
entries[#entries + 1] = e
end
end
async.yield()
if ctx.aborted then
async.abort()
end
end
if not self.incomplete then
self.cache:set({ 'get_entries', tostring(self.revision) }, { entries = entries, ctx = ctx, offset = self.offset })
end
return entries
end
---Get default insert range (UTF8 byte index).
---@return lsp.Range
source.get_default_insert_range = function(self)
if not self.context then
error('context is not initialized yet.')
end
return self.cache:ensure({ 'get_default_insert_range', tostring(self.revision) }, function()
return {
start = {
line = self.context.cursor.row - 1,
character = self.offset - 1,
},
['end'] = {
line = self.context.cursor.row - 1,
character = self.context.cursor.col - 1,
},
}
end)
end
---Get default replace range (UTF8 byte index).
---@return lsp.Range
source.get_default_replace_range = function(self)
if not self.context then
error('context is not initialized yet.')
end
return self.cache:ensure({ 'get_default_replace_range', tostring(self.revision) }, function()
local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset))
return {
start = {
line = self.context.cursor.row - 1,
character = self.offset,
},
['end'] = {
line = self.context.cursor.row - 1,
character = (e and self.offset + e - 2 or self.context.cursor.col - 1),
},
}
end)
end
---Return source name.
source.get_debug_name = function(self)
local name = self.name
if self.source.get_debug_name then
name = self.source:get_debug_name()
end
return name
end
---Return the source is available or not.
source.is_available = function(self)
if self.source.is_available then
return self.source:is_available()
end
return true
end
---Get trigger_characters
---@return string[]
source.get_trigger_characters = function(self)
local c = self:get_source_config()
if c.trigger_characters then
return c.trigger_characters
end
local trigger_characters = {}
if self.source.get_trigger_characters then
trigger_characters = self.source:get_trigger_characters(misc.copy(c)) or {}
end
if config.get().completion.get_trigger_characters then
return config.get().completion.get_trigger_characters(trigger_characters)
end
return trigger_characters
end
---Get keyword_pattern
---@return string
source.get_keyword_pattern = function(self)
local c = self:get_source_config()
if c.keyword_pattern then
return c.keyword_pattern
end
if self.source.get_keyword_pattern then
local keyword_pattern = self.source:get_keyword_pattern(misc.copy(c))
if keyword_pattern then
return keyword_pattern
end
end
return config.get().completion.keyword_pattern
end
---Get keyword_length
---@return integer
source.get_keyword_length = function(self)
local c = self:get_source_config()
if c.keyword_length then
return c.keyword_length
end
return config.get().completion.keyword_length or 1
end
---Get filter
--@return fun(entry: cmp.Entry, context: cmp.Context): boolean
source.get_entry_filter = function(self)
local c = self:get_source_config()
if c.entry_filter then
return c.entry_filter --[[@as fun(entry: cmp.Entry, context: cmp.Context): boolean]]
end
return function(_, _)
return true
end
end
---Get lsp.PositionEncodingKind
---@return lsp.PositionEncodingKind
source.get_position_encoding_kind = function(self)
if self.source.get_position_encoding_kind then
return self.source:get_position_encoding_kind()
end
return types.lsp.PositionEncodingKind.UTF16
end
---Invoke completion
---@param ctx cmp.Context
---@param callback function
---@return boolean? Return true if not trigger completion.
source.complete = function(self, ctx, callback)
local offset = ctx:get_offset(self:get_keyword_pattern())
-- NOTE: This implementation is nvim-cmp specific.
-- We trigger new completion after core.confirm but we check only the symbol trigger_character in this case.
local before_char = string.sub(ctx.cursor_before_line, -1)
if ctx:get_reason() == types.cmp.ContextReason.TriggerOnly then
before_char = string.match(ctx.cursor_before_line, '(.)%s*$')
if not before_char or not char.is_symbol(string.byte(before_char)) then
before_char = ''
end
end
local completion_context
if ctx:get_reason() == types.cmp.ContextReason.Manual then
completion_context = {
triggerKind = types.lsp.CompletionTriggerKind.Invoked,
}
elseif vim.tbl_contains(self:get_trigger_characters(), before_char) then
completion_context = {
triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter,
triggerCharacter = before_char,
}
elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then
if offset < ctx.cursor.col and self:get_keyword_length() <= (ctx.cursor.col - offset) then
if self.incomplete and self.context.cursor.col ~= ctx.cursor.col and self.status ~= source.SourceStatus.FETCHING then
completion_context = {
triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions,
}
elseif not vim.tbl_contains({ self.request_offset, self.offset }, offset) then
completion_context = {
triggerKind = types.lsp.CompletionTriggerKind.Invoked,
}
end
else
self:reset() -- Should clear current completion if the TriggerKind isn't TriggerCharacter or Manual and keyword length does not enough.
end
else
self:reset() -- Should clear current completion if ContextReason is TriggerOnly and the triggerCharacter isn't matched
end
-- Does not perform completions.
if not completion_context then
return
end
if completion_context.triggerKind == types.lsp.CompletionTriggerKind.TriggerCharacter then
self.is_triggered_by_symbol = char.is_symbol(string.byte(completion_context.triggerCharacter))
end
debug.log(self:get_debug_name(), 'request', offset, vim.inspect(completion_context))
local prev_status = self.status
self.status = source.SourceStatus.FETCHING
self.offset = offset
self.request_offset = offset
self.context = ctx
self.completion_context = completion_context
self.source:complete(
vim.tbl_extend('keep', misc.copy(self:get_source_config()), {
offset = self.offset,
context = ctx,
completion_context = completion_context,
}),
self.complete_dedup(vim.schedule_wrap(function(response)
if self.context ~= ctx then
return
end
---@type lsp.CompletionResponse
response = response or {}
self.incomplete = response.isIncomplete or false
if #(response.items or response) > 0 then
debug.log(self:get_debug_name(), 'retrieve', #(response.items or response))
local old_offset = self.offset
local old_entries = self.entries
self.status = source.SourceStatus.COMPLETED
self.entries = {}
for _, item in ipairs(response.items or response) do
if (item or {}).label then
local e = entry.new(ctx, self, item, response.itemDefaults)
if not e:is_invalid() then
table.insert(self.entries, e)
self.offset = math.min(self.offset, e:get_offset())
end
end
end
self.revision = self.revision + 1
if #self.entries == 0 then
self.offset = old_offset
self.entries = old_entries
self.revision = self.revision + 1
end
else
-- The completion will be invoked when pressing <CR> if the trigger characters contain the <Space>.
-- If the server returns an empty response in such a case, should invoke the keyword completion on the next keypress.
if offset == ctx.cursor.col then
self:reset()
end
self.status = prev_status
end
callback()
end))
)
return true
end
---Resolve CompletionItem
---@param item lsp.CompletionItem
---@param callback fun(item: lsp.CompletionItem)
source.resolve = function(self, item, callback)
if not self.source.resolve then
return callback(item)
end
self.source:resolve(item, function(resolved_item)
callback(resolved_item or item)
end)
end
---Execute command
---@param item lsp.CompletionItem
---@param callback fun()
source.execute = function(self, item, callback)
if not self.source.execute then
return callback()
end
self.source:execute(item, function()
callback()
end)
end
return source

View File

@ -0,0 +1,109 @@
local config = require('cmp.config')
local spec = require('cmp.utils.spec')
local source = require('cmp.source')
describe('source', function()
before_each(spec.before)
describe('keyword length', function()
it('not enough', function()
config.set_buffer({
completion = {
keyword_length = 3,
},
}, vim.api.nvim_get_current_buf())
local state = spec.state('', 1, 1)
local s = source.new('spec', {
complete = function(_, _, callback)
callback({ { label = 'spec' } })
end,
})
assert.is.truthy(not s:complete(state.input('a'), function() end))
end)
it('enough', function()
config.set_buffer({
completion = {
keyword_length = 3,
},
}, vim.api.nvim_get_current_buf())
local state = spec.state('', 1, 1)
local s = source.new('spec', {
complete = function(_, _, callback)
callback({ { label = 'spec' } })
end,
})
assert.is.truthy(s:complete(state.input('aiu'), function() end))
end)
it('enough -> not enough', function()
config.set_buffer({
completion = {
keyword_length = 3,
},
}, vim.api.nvim_get_current_buf())
local state = spec.state('', 1, 1)
local s = source.new('spec', {
complete = function(_, _, callback)
callback({ { label = 'spec' } })
end,
})
assert.is.truthy(s:complete(state.input('aiu'), function() end))
assert.is.truthy(not s:complete(state.backspace(), function() end))
end)
it('continue', function()
config.set_buffer({
completion = {
keyword_length = 3,
},
}, vim.api.nvim_get_current_buf())
local state = spec.state('', 1, 1)
local s = source.new('spec', {
complete = function(_, _, callback)
callback({ { label = 'spec' } })
end,
})
assert.is.truthy(s:complete(state.input('aiu'), function() end))
assert.is.truthy(not s:complete(state.input('eo'), function() end))
end)
end)
describe('isIncomplete', function()
it('isIncomplete=true', function()
local state = spec.state('', 1, 1)
local s = source.new('spec', {
complete = function(_, _, callback)
callback({
items = { { label = 'spec' } },
isIncomplete = true,
})
end,
})
vim.wait(100, function()
return s.status == source.SourceStatus.COMPLETED
end, 100, false)
assert.is.truthy(s:complete(state.input('s'), function() end))
vim.wait(100, function()
return s.status == source.SourceStatus.COMPLETED
end, 100, false)
assert.is.truthy(s:complete(state.input('p'), function() end))
vim.wait(100, function()
return s.status == source.SourceStatus.COMPLETED
end, 100, false)
assert.is.truthy(s:complete(state.input('e'), function() end))
vim.wait(100, function()
return s.status == source.SourceStatus.COMPLETED
end, 100, false)
assert.is.truthy(s:complete(state.input('c'), function() end))
vim.wait(100, function()
return s.status == source.SourceStatus.COMPLETED
end, 100, false)
end)
end)
end)

View File

@ -0,0 +1,199 @@
local cmp = {}
---@alias cmp.ConfirmBehavior 'insert' | 'replace'
cmp.ConfirmBehavior = {
Insert = 'insert',
Replace = 'replace',
}
---@alias cmp.SelectBehavior 'insert' | 'select'
cmp.SelectBehavior = {
Insert = 'insert',
Select = 'select',
}
---@alias cmp.ContextReason 'auto' | 'manual' | 'triggerOnly' | 'none'
cmp.ContextReason = {
Auto = 'auto',
Manual = 'manual',
TriggerOnly = 'triggerOnly',
None = 'none',
}
---@alias cmp.TriggerEvent 'InsertEnter' | 'TextChanged'
cmp.TriggerEvent = {
InsertEnter = 'InsertEnter',
TextChanged = 'TextChanged',
}
---@alias cmp.PreselectMode 'item' | 'None'
cmp.PreselectMode = {
Item = 'item',
None = 'none',
}
---@alias cmp.ItemField 'abbr' | 'kind' | 'menu'
cmp.ItemField = {
Abbr = 'abbr',
Kind = 'kind',
Menu = 'menu',
}
---@class cmp.ContextOption
---@field public reason cmp.ContextReason|nil
---@class cmp.ConfirmOption
---@field public behavior cmp.ConfirmBehavior
---@field public commit_character? string
---@class cmp.SelectOption
---@field public behavior cmp.SelectBehavior
---@class cmp.SnippetExpansionParams
---@field public body string
---@field public insert_text_mode integer
---@class cmp.CompleteParams
---@field public reason? cmp.ContextReason
---@field public config? cmp.ConfigSchema
---@class cmp.SetupProperty
---@field public buffer fun(c: cmp.ConfigSchema)
---@field public global fun(c: cmp.ConfigSchema)
---@field public cmdline fun(type: string|string[], c: cmp.ConfigSchema)
---@field public filetype fun(type: string|string[], c: cmp.ConfigSchema)
---@alias cmp.Setup cmp.SetupProperty | fun(c: cmp.ConfigSchema)
---@class cmp.SourceApiParams: cmp.SourceConfig
---@class cmp.SourceCompletionApiParams : cmp.SourceConfig
---@field public offset integer
---@field public context cmp.Context
---@field public completion_context lsp.CompletionContext
---@alias cmp.MappingFunction fun(fallback: function): nil
---@class cmp.MappingClass
---@field public i nil|cmp.MappingFunction
---@field public c nil|cmp.MappingFunction
---@field public x nil|cmp.MappingFunction
---@field public s nil|cmp.MappingFunction
---@alias cmp.Mapping cmp.MappingFunction | cmp.MappingClass
---@class cmp.ConfigSchema
---@field private revision? integer
---@field public enabled? boolean | fun(): boolean
---@field public performance? cmp.PerformanceConfig
---@field public preselect? cmp.PreselectMode
---@field public completion? cmp.CompletionConfig
---@field public window? cmp.WindowConfig|nil
---@field public confirmation? cmp.ConfirmationConfig
---@field public matching? cmp.MatchingConfig
---@field public sorting? cmp.SortingConfig
---@field public formatting? cmp.FormattingConfig
---@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
---@field public debounce integer
---@field public throttle integer
---@field public fetching_timeout integer
---@field public confirm_resolve_timeout integer
---@field public async_budget integer Maximum time (in ms) an async function is allowed to run during one step of the event loop.
---@field public max_view_entries integer
---@class cmp.CompletionConfig
---@field public autocomplete? cmp.TriggerEvent[]|false
---@field public completeopt? string
---@field public get_trigger_characters? fun(trigger_characters: string[]): string[]
---@field public keyword_length? integer
---@field public keyword_pattern? string
---@class cmp.WindowConfig
---@field public completion? cmp.CompletionWindowOptions
---@field public documentation? cmp.DocumentationWindowOptions|nil
---@class cmp.WindowOptions
---@field public border? string|string[]
---@field public winhighlight? string
---@field public winblend? number
---@field public zindex? integer|nil
---@class cmp.CompletionWindowOptions: cmp.WindowOptions
---@field public scrolloff? integer|nil
---@field public col_offset? integer|nil
---@field public side_padding? integer|nil
---@field public scrollbar? boolean|true
---@class cmp.DocumentationWindowOptions: cmp.WindowOptions
---@field public max_height? integer|nil
---@field public max_width? integer|nil
---@class cmp.ConfirmationConfig
---@field public default_behavior cmp.ConfirmBehavior
---@field public get_commit_characters fun(commit_characters: string[]): string[]
---@class cmp.MatchingConfig
---@field public disallow_fuzzy_matching boolean
---@field public disallow_fullfuzzy_matching boolean
---@field public disallow_partial_fuzzy_matching boolean
---@field public disallow_partial_matching boolean
---@field public disallow_prefix_unmatching boolean
---@field public disallow_symbol_nonprefix_matching boolean
---@class cmp.SortingConfig
---@field public priority_weight integer
---@field public comparators cmp.Comparator[]
---@class cmp.FormattingConfig
---@field public fields cmp.ItemField[]
---@field public expandable_indicator boolean
---@field public format fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem
---@class cmp.SnippetConfig
---@field public expand fun(args: cmp.SnippetExpansionParams)
---@class cmp.ExperimentalConfig
---@field public ghost_text cmp.GhostTextConfig|boolean
---@class cmp.GhostTextConfig
---@field hl_group string
---@class cmp.SourceConfig
---@field public name string
---@field public option table|nil
---@field public priority integer|nil
---@field public trigger_characters string[]|nil
---@field public keyword_pattern string|nil
---@field public keyword_length integer|nil
---@field public max_item_count integer|nil
---@field public group_index integer|nil
---@field public entry_filter nil|function(entry: cmp.Entry, ctx: cmp.Context): boolean
---@class cmp.ViewConfig
---@field public entries? cmp.EntriesViewConfig
---@field public docs? cmp.DocsViewConfig
---@alias cmp.EntriesViewConfig cmp.CustomEntriesViewConfig|cmp.NativeEntriesViewConfig|cmp.WildmenuEntriesViewConfig|string
---@class cmp.CustomEntriesViewConfig
---@field name 'custom'
---@field selection_order 'top_down'|'near_cursor'
---@field follow_cursor boolean
---@class cmp.NativeEntriesViewConfig
---@field name 'native'
---@class cmp.WildmenuEntriesViewConfig
---@field name 'wildmenu'
---@field separator string|nil
---@class cmp.DocsViewConfig
---@field public auto_open boolean
return cmp

View File

@ -0,0 +1,7 @@
local types = {}
types.cmp = require('cmp.types.cmp')
types.lsp = require('cmp.types.lsp')
types.vim = require('cmp.types.vim')
return types

View File

@ -0,0 +1,292 @@
local misc = require('cmp.utils.misc')
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/
---@class lsp
local lsp = {}
---@enum lsp.PositionEncodingKind
lsp.PositionEncodingKind = {
UTF8 = 'utf-8',
UTF16 = 'utf-16',
UTF32 = 'utf-32',
}
lsp.Position = {
---Convert lsp.Position to vim.Position
---@param buf integer
---@param position lsp.Position
--
---@return vim.Position
to_vim = function(buf, position)
if not vim.api.nvim_buf_is_loaded(buf) then
vim.fn.bufload(buf)
end
local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false)
if #lines > 0 then
return {
row = position.line + 1,
col = misc.to_vimindex(lines[1], position.character),
}
end
return {
row = position.line + 1,
col = position.character + 1,
}
end,
---Convert vim.Position to lsp.Position
---@param buf integer
---@param position vim.Position
---@return lsp.Position
to_lsp = function(buf, position)
if not vim.api.nvim_buf_is_loaded(buf) then
vim.fn.bufload(buf)
end
local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false)
if #lines > 0 then
return {
line = position.row - 1,
character = misc.to_utfindex(lines[1], position.col),
}
end
return {
line = position.row - 1,
character = position.col - 1,
}
end,
---Convert position to utf8 from specified encoding.
---@param text string
---@param position lsp.Position
---@param from_encoding? lsp.PositionEncodingKind
---@return lsp.Position
to_utf8 = function(text, position, from_encoding)
from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
if from_encoding == lsp.PositionEncodingKind.UTF8 then
return position
end
local ok, byteindex = pcall(function()
return vim.str_byteindex(text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16)
end)
if not ok then
return position
end
return { line = position.line, character = byteindex }
end,
---Convert position to utf16 from specified encoding.
---@param text string
---@param position lsp.Position
---@param from_encoding? lsp.PositionEncodingKind
---@return lsp.Position
to_utf16 = function(text, position, from_encoding)
from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
if from_encoding == lsp.PositionEncodingKind.UTF16 then
return position
end
local utf8 = lsp.Position.to_utf8(text, position, from_encoding)
for index = utf8.character, 0, -1 do
local ok, utf16index = pcall(function()
return select(2, vim.str_utfindex(text, index))
end)
if ok then
return { line = utf8.line, character = utf16index }
end
end
return position
end,
---Convert position to utf32 from specified encoding.
---@param text string
---@param position lsp.Position
---@param from_encoding? lsp.PositionEncodingKind
---@return lsp.Position
to_utf32 = function(text, position, from_encoding)
from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
if from_encoding == lsp.PositionEncodingKind.UTF32 then
return position
end
local utf8 = lsp.Position.to_utf8(text, position, from_encoding)
for index = utf8.character, 0, -1 do
local ok, utf32index = pcall(function()
return select(1, vim.str_utfindex(text, index))
end)
if ok then
return { line = utf8.line, character = utf32index }
end
end
return position
end,
}
lsp.Range = {
---Convert lsp.Range to vim.Range
---@param buf integer
---@param range lsp.Range
---@return vim.Range
to_vim = function(buf, range)
return {
start = lsp.Position.to_vim(buf, range.start),
['end'] = lsp.Position.to_vim(buf, range['end']),
}
end,
---Convert vim.Range to lsp.Range
---@param buf integer
---@param range vim.Range
---@return lsp.Range
to_lsp = function(buf, range)
return {
start = lsp.Position.to_lsp(buf, range.start),
['end'] = lsp.Position.to_lsp(buf, range['end']),
}
end,
}
---@alias lsp.CompletionTriggerKind 1 | 2 | 3
lsp.CompletionTriggerKind = {
Invoked = 1,
TriggerCharacter = 2,
TriggerForIncompleteCompletions = 3,
}
---@alias lsp.InsertTextFormat 1 | 2
lsp.InsertTextFormat = {}
lsp.InsertTextFormat.PlainText = 1
lsp.InsertTextFormat.Snippet = 2
---@alias lsp.InsertTextMode 1 | 2
lsp.InsertTextMode = {
AsIs = 1,
AdjustIndentation = 2,
}
---@alias lsp.MarkupKind 'plaintext' | 'markdown'
lsp.MarkupKind = {
PlainText = 'plaintext',
Markdown = 'markdown',
}
---@alias lsp.CompletionItemTag 1
lsp.CompletionItemTag = {
Deprecated = 1,
}
---@alias lsp.CompletionItemKind 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25
lsp.CompletionItemKind = {
Text = 1,
Method = 2,
Function = 3,
Constructor = 4,
Field = 5,
Variable = 6,
Class = 7,
Interface = 8,
Module = 9,
Property = 10,
Unit = 11,
Value = 12,
Enum = 13,
Keyword = 14,
Snippet = 15,
Color = 16,
File = 17,
Reference = 18,
Folder = 19,
EnumMember = 20,
Constant = 21,
Struct = 22,
Event = 23,
Operator = 24,
TypeParameter = 25,
}
for k, v in pairs(lsp.CompletionItemKind) do
lsp.CompletionItemKind[v] = k
end
---@class lsp.internal.CompletionItemDefaults
---@field public commitCharacters? string[]
---@field public editRange? lsp.Range | { insert: lsp.Range, replace: lsp.Range }
---@field public insertTextFormat? lsp.InsertTextFormat
---@field public insertTextMode? lsp.InsertTextMode
---@field public data? any
---@class lsp.CompletionContext
---@field public triggerKind lsp.CompletionTriggerKind
---@field public triggerCharacter string|nil
---@class lsp.CompletionList
---@field public isIncomplete boolean
---@field public itemDefaults? lsp.internal.CompletionItemDefaults
---@field public items lsp.CompletionItem[]
---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]
---@class lsp.MarkupContent
---@field public kind lsp.MarkupKind
---@field public value string
---@class lsp.Position
---@field public line integer
---@field public character integer
---@class lsp.Range
---@field public start lsp.Position
---@field public end lsp.Position
---@class lsp.Command
---@field public title string
---@field public command string
---@field public arguments any[]|nil
---@class lsp.TextEdit
---@field public range lsp.Range|nil
---@field public newText string
---@alias lsp.InsertReplaceTextEdit lsp.internal.InsertTextEdit|lsp.internal.ReplaceTextEdit
---@class lsp.internal.InsertTextEdit
---@field public insert lsp.Range
---@field public newText string
---@class lsp.internal.ReplaceTextEdit
---@field public replace lsp.Range
---@field public newText string
---@class lsp.CompletionItemLabelDetails
---@field public detail? string
---@field public description? string
---@class lsp.internal.CmpCompletionExtension
---@field public kind_text string
---@field public kind_hl_group string
---@class lsp.CompletionItem
---@field public label string
---@field public labelDetails? lsp.CompletionItemLabelDetails
---@field public kind? lsp.CompletionItemKind
---@field public tags? lsp.CompletionItemTag[]
---@field public detail? string
---@field public documentation? lsp.MarkupContent|string
---@field public deprecated? boolean
---@field public preselect? boolean
---@field public sortText? string
---@field public filterText? string
---@field public insertText? string
---@field public insertTextFormat? lsp.InsertTextFormat
---@field public insertTextMode? lsp.InsertTextMode
---@field public textEdit? lsp.TextEdit|lsp.InsertReplaceTextEdit
---@field public textEditText? string
---@field public additionalTextEdits? lsp.TextEdit[]
---@field public commitCharacters? string[]
---@field public command? lsp.Command
---@field public data? any
---@field public cmp? lsp.internal.CmpCompletionExtension
---
---TODO: Should send the issue for upstream?
---@field public word string|nil
---@field public dup boolean|nil
return lsp

View File

@ -0,0 +1,47 @@
local spec = require('cmp.utils.spec')
local lsp = require('cmp.types.lsp')
describe('types.lsp', function()
before_each(spec.before)
describe('Position', function()
vim.fn.setline('1', {
'あいうえお',
'かきくけこ',
'さしすせそ',
})
local vim_position, lsp_position
local bufnr = vim.api.nvim_get_current_buf()
vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 3 })
assert.are.equal(vim_position.row, 2)
assert.are.equal(vim_position.col, 10)
lsp_position = lsp.Position.to_lsp(bufnr, vim_position)
assert.are.equal(lsp_position.line, 1)
assert.are.equal(lsp_position.character, 3)
vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 0 })
assert.are.equal(vim_position.row, 2)
assert.are.equal(vim_position.col, 1)
lsp_position = lsp.Position.to_lsp(bufnr, vim_position)
assert.are.equal(lsp_position.line, 1)
assert.are.equal(lsp_position.character, 0)
vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 5 })
assert.are.equal(vim_position.row, 2)
assert.are.equal(vim_position.col, 16)
lsp_position = lsp.Position.to_lsp(bufnr, vim_position)
assert.are.equal(lsp_position.line, 1)
assert.are.equal(lsp_position.character, 5)
-- overflow (lsp -> vim)
vim_position = lsp.Position.to_vim(bufnr, { line = 1, character = 6 })
assert.are.equal(vim_position.row, 2)
assert.are.equal(vim_position.col, 16)
-- overflow(vim -> lsp)
vim_position.col = vim_position.col + 1
lsp_position = lsp.Position.to_lsp(bufnr, vim_position)
assert.are.equal(lsp_position.line, 1)
assert.are.equal(lsp_position.character, 5)
end)
end)

View File

@ -0,0 +1,20 @@
---@class vim.CompletedItem
---@field public word string
---@field public abbr string|nil
---@field public kind string|nil
---@field public menu string|nil
---@field public equal 1|nil
---@field public empty 1|nil
---@field public dup 1|nil
---@field public id any
---@field public abbr_hl_group string|nil
---@field public kind_hl_group string|nil
---@field public menu_hl_group string|nil
---@class vim.Position 1-based index
---@field public row integer
---@field public col integer
---@class vim.Range
---@field public start vim.Position
---@field public end vim.Position

View File

@ -0,0 +1,70 @@
local api = {}
local CTRL_V = vim.api.nvim_replace_termcodes('<C-v>', true, true, true)
local CTRL_S = vim.api.nvim_replace_termcodes('<C-s>', true, true, true)
api.get_mode = function()
local mode = vim.api.nvim_get_mode().mode:sub(1, 1)
if mode == 'i' then
return 'i' -- insert
elseif mode == 'v' or mode == 'V' or mode == CTRL_V then
return 'x' -- visual
elseif mode == 's' or mode == 'S' or mode == CTRL_S then
return 's' -- select
elseif mode == 'c' and vim.fn.getcmdtype() ~= '=' then
return 'c' -- cmdline
end
end
api.is_insert_mode = function()
return api.get_mode() == 'i'
end
api.is_cmdline_mode = function()
return api.get_mode() == 'c'
end
api.is_select_mode = function()
return api.get_mode() == 's'
end
api.is_visual_mode = function()
return api.get_mode() == 'x'
end
api.is_suitable_mode = function()
local mode = api.get_mode()
return mode == 'i' or mode == 'c'
end
api.get_current_line = function()
if api.is_cmdline_mode() then
return vim.fn.getcmdline()
end
return vim.api.nvim_get_current_line()
end
---@return { [1]: integer, [2]: integer }
api.get_cursor = function()
if api.is_cmdline_mode() then
return { math.min(vim.o.lines, vim.o.lines - (vim.api.nvim_get_option_value('cmdheight', {}) - 1)), vim.fn.getcmdpos() - 1 }
end
return vim.api.nvim_win_get_cursor(0)
end
api.get_screen_cursor = function()
if api.is_cmdline_mode() then
local cursor = api.get_cursor()
return { cursor[1], vim.fn.strdisplaywidth(string.sub(vim.fn.getcmdline(), 1, cursor[2] + 1)) }
end
local cursor = api.get_cursor()
local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1)
return { pos.row, pos.col - 1 }
end
api.get_cursor_before_line = function()
local cursor = api.get_cursor()
return string.sub(api.get_current_line(), 1, cursor[2])
end
return api

View File

@ -0,0 +1,64 @@
local spec = require('cmp.utils.spec')
local keymap = require('cmp.utils.keymap')
local feedkeys = require('cmp.utils.feedkeys')
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)
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)
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])
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])
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')
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')
end)
end)
end)

View File

@ -0,0 +1,303 @@
local feedkeys = require('cmp.utils.feedkeys')
local config = require('cmp.config')
local async = {}
---@class cmp.AsyncThrottle
---@field public running boolean
---@field public timeout integer
---@field public sync function(self: cmp.AsyncThrottle, timeout: integer|nil)
---@field public stop function
---@field public __call function
---@type uv_timer_t[]
local timers = {}
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
for _, timer in pairs(timers) do
if timer and not timer:is_closing() then
timer:stop()
timer:close()
end
end
end,
})
---@param fn function
---@param timeout integer
---@return cmp.AsyncThrottle
async.throttle = function(fn, timeout)
local time = nil
local timer = assert(vim.loop.new_timer())
local _async = nil ---@type Async?
timers[#timers + 1] = timer
local throttle
throttle = setmetatable({
running = false,
timeout = timeout,
sync = function(self, timeout_)
if not self.running then
return
end
vim.wait(timeout_ or 1000, function()
return not self.running
end, 10)
end,
stop = function(reset_time)
if reset_time ~= false then
time = nil
end
-- can't use self here unfortunately
throttle.running = false
timer:stop()
if _async then
_async:cancel()
_async = nil
end
end,
}, {
__call = function(self, ...)
local args = { ... }
if time == nil then
time = vim.loop.now()
end
self.stop(false)
self.running = true
timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function()
vim.schedule(function()
time = nil
local ret = fn(unpack(args))
if async.is_async(ret) then
---@cast ret Async
_async = ret
_async:await(function(_, error)
_async = nil
self.running = false
if error and error ~= 'abort' then
vim.notify(error, vim.log.levels.ERROR)
end
end)
else
self.running = false
end
end)
end)
end,
})
return throttle
end
---Control async tasks.
async.step = function(...)
local tasks = { ... }
local next
next = function(...)
if #tasks > 0 then
table.remove(tasks, 1)(next, ...)
end
end
table.remove(tasks, 1)(next)
end
---Timeout callback function
---@param fn function
---@param timeout integer
---@return function
async.timeout = function(fn, timeout)
local timer
local done = false
local callback = function(...)
if not done then
done = true
timer:stop()
timer:close()
fn(...)
end
end
timer = vim.loop.new_timer()
timer:start(timeout, 0, function()
callback()
end)
return callback
end
---@alias cmp.AsyncDedup fun(callback: function): function
---Create deduplicated callback
---@return function
async.dedup = function()
local id = 0
return function(callback)
id = id + 1
local current = id
return function(...)
if current == id then
callback(...)
end
end
end
end
---Convert async process as sync
async.sync = function(runner, timeout)
local done = false
runner(function()
done = true
end)
vim.wait(timeout, function()
return done
end, 10, false)
end
---Wait and callback for next safe state.
async.debounce_next_tick = function(callback)
local running = false
return function()
if running then
return
end
running = true
vim.schedule(function()
running = false
callback()
end)
end
end
---Wait and callback for consuming next keymap.
async.debounce_next_tick_by_keymap = function(callback)
return function()
feedkeys.call('', '', callback)
end
end
local Scheduler = {}
Scheduler._queue = {}
Scheduler._executor = assert(vim.loop.new_check())
function Scheduler.step()
local budget = config.get().performance.async_budget * 1e6
local start = vim.loop.hrtime()
while #Scheduler._queue > 0 and vim.loop.hrtime() - start < budget do
local a = table.remove(Scheduler._queue, 1)
a:_step()
if a.running then
table.insert(Scheduler._queue, a)
end
end
if #Scheduler._queue == 0 then
return Scheduler._executor:stop()
end
end
---@param a Async
function Scheduler.add(a)
table.insert(Scheduler._queue, a)
if not Scheduler._executor:is_active() then
Scheduler._executor:start(vim.schedule_wrap(Scheduler.step))
end
end
--- @alias AsyncCallback fun(result?:any, error?:string)
--- @class Async
--- @field running boolean
--- @field result? any
--- @field error? string
--- @field callbacks AsyncCallback[]
--- @field thread thread
local Async = {}
Async.__index = Async
function Async.new(fn)
local self = setmetatable({}, Async)
self.callbacks = {}
self.running = true
self.thread = coroutine.create(fn)
Scheduler.add(self)
return self
end
---@param result? any
---@param error? string
function Async:_done(result, error)
if self.running then
self.running = false
self.result = result
self.error = error
end
for _, callback in ipairs(self.callbacks) do
callback(result, error)
end
-- only run each callback once.
-- _done can possibly be called multiple times.
-- so we need to clear callbacks after executing them.
self.callbacks = {}
end
function Async:_step()
local ok, res = coroutine.resume(self.thread)
if not ok then
return self:_done(nil, res)
elseif res == 'abort' then
return self:_done(nil, 'abort')
elseif coroutine.status(self.thread) == 'dead' then
return self:_done(res)
end
end
function Async:cancel()
self:_done(nil, 'abort')
end
---@param cb AsyncCallback
function Async:await(cb)
if not cb then
error('callback is required')
end
if self.running then
table.insert(self.callbacks, cb)
else
cb(self.result, self.error)
end
end
function Async:sync()
while self.running do
vim.wait(10)
end
return self.error and error(self.error) or self.result
end
--- @return boolean
function async.is_async(obj)
return obj and type(obj) == 'table' and getmetatable(obj) == Async
end
--- @return fun(...): Async
function async.wrap(fn)
return function(...)
local args = { ... }
return Async.new(function()
return fn(unpack(args))
end)
end
end
-- This will yield when called from a coroutine
function async.yield(...)
if coroutine.running() == nil then
error('Trying to yield from a non-yieldable context')
return ...
end
return coroutine.yield(...)
end
function async.abort()
return async.yield('abort')
end
return async

View File

@ -0,0 +1,69 @@
local async = require('cmp.utils.async')
describe('utils.async', function()
it('throttle', function()
local count = 0
local now
local f = async.throttle(function()
count = count + 1
end, 100)
-- 1. delay for 100ms
now = vim.loop.now()
f.timeout = 100
f()
vim.wait(1000, function()
return count == 1
end)
assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10)
-- 2. delay for 500ms
now = vim.loop.now()
f.timeout = 500
f()
vim.wait(1000, function()
return count == 2
end)
assert.is.truthy(math.abs(f.timeout - (vim.loop.now() - now)) < 10)
-- 4. delay for 500ms and wait 100ms (remain 400ms)
f.timeout = 500
f()
vim.wait(100) -- remain 400ms
-- 5. call immediately (100ms already elapsed from No.4)
now = vim.loop.now()
f.timeout = 100
f()
vim.wait(1000, function()
return count == 3
end)
assert.is.truthy(math.abs(vim.loop.now() - now) < 10)
end)
it('step', function()
local done = false
local step = {}
async.step(function(next)
vim.defer_fn(function()
table.insert(step, 1)
next()
end, 10)
end, function(next)
vim.defer_fn(function()
table.insert(step, 2)
next()
end, 10)
end, function(next)
vim.defer_fn(function()
table.insert(step, 3)
next()
end, 10)
end, function()
done = true
end)
vim.wait(1000, function()
return done
end)
assert.are.same(step, { 1, 2, 3 })
end)
end)

View File

@ -0,0 +1,53 @@
local debug = require('cmp.utils.debug')
local autocmd = {}
autocmd.group = vim.api.nvim_create_augroup('___cmp___', { clear = true })
autocmd.events = {}
---Subscribe autocmd
---@param events string|string[]
---@param callback function
---@return function
autocmd.subscribe = function(events, callback)
events = type(events) == 'string' and { events } or events
for _, event in ipairs(events) do
if not autocmd.events[event] then
autocmd.events[event] = {}
vim.api.nvim_create_autocmd(event, {
desc = ('nvim-cmp: autocmd: %s'):format(event),
group = autocmd.group,
callback = function()
autocmd.emit(event)
end,
})
end
table.insert(autocmd.events[event], callback)
end
return function()
for _, event in ipairs(events) do
for i, callback_ in ipairs(autocmd.events[event]) do
if callback_ == callback then
table.remove(autocmd.events[event], i)
break
end
end
end
end
end
---Emit autocmd
---@param event string
autocmd.emit = function(event)
debug.log(' ')
debug.log(string.format('>>> %s', event))
autocmd.events[event] = autocmd.events[event] or {}
for _, callback in ipairs(autocmd.events[event]) do
callback()
end
end
return autocmd

View File

@ -0,0 +1,33 @@
local binary = {}
---Insert item to list to ordered index
---@param list any[]
---@param item any
---@param func fun(a: any, b: any): 1|-1|0
binary.insort = function(list, item, func)
table.insert(list, binary.search(list, item, func), item)
end
---Search suitable index from list
---@param list any[]
---@param item any
---@param func fun(a: any, b: any): 1|-1|0
---@return integer
binary.search = function(list, item, func)
local s = 1
local e = #list
while s <= e do
local idx = math.floor((e + s) / 2)
local diff = func(item, list[idx])
if diff > 0 then
s = idx + 1
elseif diff < 0 then
e = idx - 1
else
return idx + 1
end
end
return s
end
return binary

View File

@ -0,0 +1,28 @@
local binary = require('cmp.utils.binary')
describe('utils.binary', function()
it('insort', function()
local func = function(a, b)
return a.score - b.score
end
local list = {}
binary.insort(list, { id = 'a', score = 1 }, func)
binary.insort(list, { id = 'b', score = 5 }, func)
binary.insort(list, { id = 'c', score = 2.5 }, func)
binary.insort(list, { id = 'd', score = 2 }, func)
binary.insort(list, { id = 'e', score = 8 }, func)
binary.insort(list, { id = 'g', score = 8 }, func)
binary.insort(list, { id = 'h', score = 7 }, func)
binary.insort(list, { id = 'i', score = 6 }, func)
binary.insort(list, { id = 'j', score = 4 }, func)
assert.are.equal(list[1].id, 'a')
assert.are.equal(list[2].id, 'd')
assert.are.equal(list[3].id, 'c')
assert.are.equal(list[4].id, 'j')
assert.are.equal(list[5].id, 'b')
assert.are.equal(list[6].id, 'i')
assert.are.equal(list[7].id, 'h')
assert.are.equal(list[8].id, 'e')
assert.are.equal(list[9].id, 'g')
end)
end)

View File

@ -0,0 +1,28 @@
local buffer = {}
buffer.cache = {}
---@return integer buf
buffer.get = function(name)
local buf = buffer.cache[name]
if buf and vim.api.nvim_buf_is_valid(buf) then
return buf
else
return nil
end
end
---@return integer buf
---@return boolean created_new
buffer.ensure = function(name)
local created_new = false
local buf = buffer.get(name)
if not buf then
created_new = true
buf = vim.api.nvim_create_buf(false, true)
buffer.cache[name] = buf
end
return buf, created_new
end
return buffer

View File

@ -0,0 +1,60 @@
---@class cmp.Cache
---@field public entries any
local cache = {}
cache.new = function()
local self = setmetatable({}, { __index = cache })
self.entries = {}
return self
end
---Get cache value
---@param key string|string[]
---@return any|nil
cache.get = function(self, key)
key = self:key(key)
if self.entries[key] ~= nil then
return self.entries[key]
end
return nil
end
---Set cache value explicitly
---@param key string|string[]
---@vararg any
cache.set = function(self, key, value)
key = self:key(key)
self.entries[key] = value
end
---Ensure value by callback
---@generic T
---@param key string|string[]
---@param callback fun(): T
---@return T
cache.ensure = function(self, key, callback)
local value = self:get(key)
if value == nil then
local v = callback()
self:set(key, v)
return v
end
return value
end
---Clear all cache entries
cache.clear = function(self)
self.entries = {}
end
---Create key
---@param key string|string[]
---@return string
cache.key = function(_, key)
if type(key) == 'table' then
return table.concat(key, ':')
end
return key
end
return cache

View File

@ -0,0 +1,117 @@
local _
local alpha = {}
_ = string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char)
alpha[string.byte(char)] = true
end)
local ALPHA = {}
_ = string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char)
ALPHA[string.byte(char)] = true
end)
local digit = {}
_ = string.gsub('1234567890', '.', function(char)
digit[string.byte(char)] = true
end)
local white = {}
_ = string.gsub(' \t\n', '.', function(char)
white[string.byte(char)] = true
end)
local char = {}
---@param byte integer
---@return boolean
char.is_upper = function(byte)
return ALPHA[byte]
end
---@param byte integer
---@return boolean
char.is_alpha = function(byte)
return alpha[byte] or ALPHA[byte]
end
---@param byte integer
---@return boolean
char.is_digit = function(byte)
return digit[byte]
end
---@param byte integer
---@return boolean
char.is_white = function(byte)
return white[byte]
end
---@param byte integer
---@return boolean
char.is_symbol = function(byte)
return not (char.is_alnum(byte) or char.is_white(byte))
end
---@param byte integer
---@return boolean
char.is_printable = function(byte)
return string.match(string.char(byte), '^%c$') == nil
end
---@param byte integer
---@return boolean
char.is_alnum = function(byte)
return char.is_alpha(byte) or char.is_digit(byte)
end
---@param text string
---@param index integer
---@return boolean
char.is_semantic_index = function(text, index)
if index <= 1 then
return true
end
local prev = string.byte(text, index - 1)
local curr = string.byte(text, index)
if not char.is_upper(prev) and char.is_upper(curr) then
return true
end
if char.is_symbol(curr) or char.is_white(curr) then
return true
end
if not char.is_alpha(prev) and char.is_alpha(curr) then
return true
end
if not char.is_digit(prev) and char.is_digit(curr) then
return true
end
return false
end
---@param text string
---@param current_index integer
---@return integer
char.get_next_semantic_index = function(text, current_index)
for i = current_index + 1, #text do
if char.is_semantic_index(text, i) then
return i
end
end
return #text + 1
end
---Ignore case match
---@param byte1 integer
---@param byte2 integer
---@return boolean
char.match = function(byte1, byte2)
if not char.is_alpha(byte1) or not char.is_alpha(byte2) then
return byte1 == byte2
end
local diff = byte1 - byte2
return diff == 0 or diff == 32 or diff == -32
end
return char

View File

@ -0,0 +1,20 @@
local debug = {}
debug.flag = false
---Print log
---@vararg any
debug.log = function(...)
if debug.flag then
local data = {}
for _, v in ipairs({ ... }) do
if not vim.tbl_contains({ 'string', 'number', 'boolean' }, type(v)) then
v = vim.inspect(v)
end
table.insert(data, v)
end
print(table.concat(data, '\t'))
end
end
return debug

View File

@ -0,0 +1,51 @@
---@class cmp.Event
---@field private events table<string, function[]>
local event = {}
---Create vents
event.new = function()
local self = setmetatable({}, { __index = event })
self.events = {}
return self
end
---Add event listener
---@param name string
---@param callback function
---@return function
event.on = function(self, name, callback)
if not self.events[name] then
self.events[name] = {}
end
table.insert(self.events[name], callback)
return function()
self:off(name, callback)
end
end
---Remove event listener
---@param name string
---@param callback function
event.off = function(self, name, callback)
for i, callback_ in ipairs(self.events[name] or {}) do
if callback_ == callback then
table.remove(self.events[name], i)
break
end
end
end
---Remove all events
event.clear = function(self)
self.events = {}
end
---Emit event
---@param name string
event.emit = function(self, name, ...)
for _, callback in ipairs(self.events[name] or {}) do
callback(...)
end
end
return event

View File

@ -0,0 +1,53 @@
local keymap = require('cmp.utils.keymap')
local misc = require('cmp.utils.misc')
local feedkeys = {}
feedkeys.call = setmetatable({
callbacks = {},
}, {
__call = function(self, keys, mode, callback)
local is_insert = string.match(mode, 'i') ~= nil
local is_immediate = string.match(mode, 'x') ~= nil
local queue = {}
if #keys > 0 then
table.insert(queue, { keymap.t('<Cmd>setlocal lazyredraw<CR>'), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal textwidth=0<CR>'), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal backspace=2<CR>'), 'n' })
table.insert(queue, { keys, string.gsub(mode, '[itx]', ''), true })
table.insert(queue, { keymap.t('<Cmd>setlocal %slazyredraw<CR>'):format(vim.o.lazyredraw and '' or 'no'), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal textwidth=%s<CR>'):format(vim.bo.textwidth or 0), 'n' })
table.insert(queue, { keymap.t('<Cmd>setlocal backspace=%s<CR>'):format(vim.go.backspace or 2), 'n' })
end
if callback then
local id = misc.id('cmp.utils.feedkeys.call')
self.callbacks[id] = callback
table.insert(queue, { keymap.t('<Cmd>lua require"cmp.utils.feedkeys".run(%s)<CR>'):format(id), 'n', true })
end
if is_insert then
for i = #queue, 1, -1 do
vim.api.nvim_feedkeys(queue[i][1], queue[i][2] .. 'i', queue[i][3])
end
else
for i = 1, #queue do
vim.api.nvim_feedkeys(queue[i][1], queue[i][2], queue[i][3])
end
end
if is_immediate then
vim.api.nvim_feedkeys('', 'x', true)
end
end,
})
feedkeys.run = function(id)
if feedkeys.call.callbacks[id] then
feedkeys.call.callbacks[id]()
feedkeys.call.callbacks[id] = nil
end
return ''
end
return feedkeys

View File

@ -0,0 +1,56 @@
local spec = require('cmp.utils.spec')
local keymap = require('cmp.utils.keymap')
local feedkeys = require('cmp.utils.feedkeys')
describe('feedkeys', function()
before_each(spec.before)
it('dot-repeat', function()
local reg
feedkeys.call(keymap.t('iaiueo<Esc>'), 'nx', function()
reg = vim.fn.getreg('.')
end)
assert.are.equal(reg, keymap.t('aiueo'))
end)
it('textwidth', function()
vim.cmd([[setlocal textwidth=6]])
feedkeys.call(keymap.t('iaiueo '), 'nx')
feedkeys.call(keymap.t('aaiueoaiueo'), 'nx')
assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), {
'aiueo aiueoaiueo',
})
end)
it('backspace', function()
vim.cmd([[setlocal backspace=""]])
feedkeys.call(keymap.t('iaiueo'), 'nx')
feedkeys.call(keymap.t('a<BS><BS>'), 'nx')
assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), {
'aiu',
})
end)
it('testability', function()
feedkeys.call('i', 'n', function()
feedkeys.call('', 'n', function()
feedkeys.call('aiueo', 'in')
end)
feedkeys.call('', 'n', function()
feedkeys.call(keymap.t('<BS><BS><BS><BS><BS>'), 'in')
end)
feedkeys.call('', 'n', function()
feedkeys.call(keymap.t('abcde'), 'in')
end)
feedkeys.call('', 'n', function()
feedkeys.call(keymap.t('<BS><BS><BS><BS><BS>'), 'in')
end)
feedkeys.call('', 'n', function()
feedkeys.call(keymap.t('12345'), 'in')
end)
end)
feedkeys.call('', 'x')
assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { '12345' })
end)
end)

View File

@ -0,0 +1,31 @@
local highlight = {}
highlight.keys = {
'fg',
'bg',
'bold',
'italic',
'reverse',
'standout',
'underline',
'undercurl',
'strikethrough',
}
highlight.inherit = function(name, source, settings)
for _, key in ipairs(highlight.keys) do
if not settings[key] then
local v = vim.fn.synIDattr(vim.fn.hlID(source), key)
if key == 'fg' or key == 'bg' then
local n = tonumber(v, 10)
v = type(n) == 'number' and n or v
else
v = v == 1
end
settings[key] = v == '' and 'NONE' or v
end
end
vim.api.nvim_set_hl(0, name, settings)
end
return highlight

View File

@ -0,0 +1,273 @@
local misc = require('cmp.utils.misc')
local buffer = require('cmp.utils.buffer')
local api = require('cmp.utils.api')
local keymap = {}
---Shortcut for nvim_replace_termcodes
---@param keys string
---@return string
keymap.t = function(keys)
return (string.gsub(keys, '(<[A-Za-z0-9\\%-%[%]%^@]->)', function(match)
return vim.api.nvim_eval(string.format([["\%s"]], match))
end))
end
---Normalize key sequence.
---@param keys string
---@return string
keymap.normalize = function(keys)
local normalize_buf = buffer.ensure('cmp.util.keymap.normalize')
vim.api.nvim_buf_set_keymap(normalize_buf, 't', keys, '<Plug>(cmp.utils.keymap.normalize)', {})
for _, map in ipairs(vim.api.nvim_buf_get_keymap(normalize_buf, 't')) do
if keymap.t(map.rhs) == keymap.t('<Plug>(cmp.utils.keymap.normalize)') then
vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys)
return map.lhs
end
end
vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys)
vim.api.nvim_buf_delete(normalize_buf, {})
return keys
end
---Return vim notation keymapping (simple conversion).
---@param s string
---@return string
keymap.to_keymap = setmetatable({
['<CR>'] = { '\n', '\r', '\r\n' },
['<Tab>'] = { '\t' },
['<BSlash>'] = { '\\' },
['<Bar>'] = { '|' },
['<Space>'] = { ' ' },
}, {
__call = function(self, s)
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,
})
---Mode safe break undo
keymap.undobreak = function()
if not api.is_insert_mode() then
return ''
end
return keymap.t('<C-g>u')
end
---Mode safe join undo
keymap.undojoin = function()
if not api.is_insert_mode() then
return ''
end
return keymap.t('<C-g>U')
end
---Create backspace keys.
---@param count string|integer
---@return string
keymap.backspace = function(count)
if type(count) == 'string' then
count = vim.fn.strchars(count, true)
end
if count <= 0 then
return ''
end
local keys = {}
table.insert(keys, keymap.t(string.rep('<BS>', count)))
return table.concat(keys, '')
end
---Create delete keys.
---@param count string|integer
---@return string
keymap.delete = function(count)
if type(count) == 'string' then
count = vim.fn.strchars(count, true)
end
if count <= 0 then
return ''
end
local keys = {}
table.insert(keys, keymap.t(string.rep('<Del>', count)))
return table.concat(keys, '')
end
---Update indentkeys.
---@param expr? string
---@return string
keymap.indentkeys = function(expr)
return string.format(keymap.t('<Cmd>set indentkeys=%s<CR>'), expr and vim.fn.escape(expr, '| \t\\') or '')
end
---Return two key sequence are equal or not.
---@param a string
---@param b string
---@return boolean
keymap.equals = function(a, b)
return keymap.normalize(a) == keymap.normalize(b)
end
---Register keypress handler.
keymap.listen = function(mode, lhs, callback)
lhs = keymap.normalize(keymap.to_keymap(lhs))
local existing = keymap.get_map(mode, lhs)
if existing.desc == 'cmp.utils.keymap.set_map' then
return
end
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()
local ignore = false
ignore = ignore or (mode == 'c' and vim.fn.getcmdtype() == '=')
if ignore then
fallback()
else
callback(lhs, misc.once(fallback))
end
end, {
expr = false,
noremap = true,
silent = true,
})
end
---Fallback
keymap.fallback = function(bufnr, mode, map)
return function()
if map.expr then
local fallback_lhs = string.format('<Plug>(cmp.u.k.fallback_expr:%s)', map.lhs)
keymap.set_map(bufnr, mode, fallback_lhs, function()
return keymap.solve(bufnr, mode, map).keys
end, {
expr = true,
noremap = map.noremap,
script = map.script,
nowait = map.nowait,
silent = map.silent and mode ~= 'c',
replace_keycodes = map.replace_keycodes,
})
vim.api.nvim_feedkeys(keymap.t(fallback_lhs), 'im', true)
elseif map.callback then
map.callback()
else
local solved = keymap.solve(bufnr, mode, map)
vim.api.nvim_feedkeys(solved.keys, solved.mode, true)
end
end
end
---Solve
keymap.solve = function(bufnr, mode, map)
local lhs = keymap.t(map.lhs)
local rhs = keymap.t(map.rhs)
if map.expr then
if map.callback then
rhs = map.callback()
else
rhs = vim.api.nvim_eval(keymap.t(map.rhs))
end
end
if map.noremap then
return { keys = rhs, mode = 'in' }
end
if string.find(rhs, lhs, 1, true) == 1 then
local recursive = string.format('<SNR>0_(cmp.u.k.recursive:%s)', lhs)
keymap.set_map(bufnr, mode, recursive, lhs, {
noremap = true,
script = true,
nowait = map.nowait,
silent = map.silent and mode ~= 'c',
replace_keycodes = map.replace_keycodes,
})
return { keys = keymap.t(recursive) .. string.gsub(rhs, '^' .. vim.pesc(lhs), ''), mode = 'im' }
end
return { keys = rhs, mode = 'im' }
end
---Get map
---@param mode string
---@param lhs string
---@return table
keymap.get_map = function(mode, lhs)
lhs = keymap.normalize(lhs)
for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do
if keymap.equals(map.lhs, lhs) then
return {
lhs = map.lhs,
rhs = map.rhs or '',
expr = map.expr == 1,
callback = map.callback,
desc = map.desc,
noremap = map.noremap == 1,
script = map.script == 1,
silent = map.silent == 1,
nowait = map.nowait == 1,
buffer = true,
replace_keycodes = map.replace_keycodes == 1,
}
end
end
for _, map in ipairs(vim.api.nvim_get_keymap(mode)) do
if keymap.equals(map.lhs, lhs) then
return {
lhs = map.lhs,
rhs = map.rhs or '',
expr = map.expr == 1,
callback = map.callback,
desc = map.desc,
noremap = map.noremap == 1,
script = map.script == 1,
silent = map.silent == 1,
nowait = map.nowait == 1,
buffer = false,
replace_keycodes = map.replace_keycodes == 1,
}
end
end
return {
lhs = lhs,
rhs = lhs,
expr = false,
callback = nil,
noremap = true,
script = false,
silent = true,
nowait = false,
buffer = false,
replace_keycodes = true,
}
end
---Set keymapping
keymap.set_map = function(bufnr, mode, lhs, rhs, opts)
if type(rhs) == 'function' then
opts.callback = rhs
rhs = ''
end
opts.desc = 'cmp.utils.keymap.set_map'
if vim.fn.has('nvim-0.8') == 0 then
opts.replace_keycodes = nil
end
if bufnr == -1 then
vim.api.nvim_set_keymap(mode, lhs, rhs, opts)
else
vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts)
end
end
return keymap

View File

@ -0,0 +1,187 @@
local spec = require('cmp.utils.spec')
local api = require('cmp.utils.api')
local feedkeys = require('cmp.utils.feedkeys')
local keymap = require('cmp.utils.keymap')
describe('keymap', function()
before_each(spec.before)
it('t', function()
for _, key in ipairs({
'<F1>',
'<C-a>',
'<C-]>',
'<C-[>',
'<C-^>',
'<C-@>',
'<C-\\>',
'<Tab>',
'<S-Tab>',
'<Plug>(example)',
'<C-r>="abc"<CR>',
'<Cmd>normal! ==<CR>',
}) do
assert.are.equal(keymap.t(key), vim.api.nvim_replace_termcodes(key, true, true, true))
assert.are.equal(keymap.t(key .. key), vim.api.nvim_replace_termcodes(key .. key, true, true, true))
assert.are.equal(keymap.t(key .. key .. key), vim.api.nvim_replace_termcodes(key .. key .. key, true, true, true))
end
end)
it('to_keymap', function()
assert.are.equal(keymap.to_keymap('\n'), '<CR>')
assert.are.equal(keymap.to_keymap('<CR>'), '<CR>')
assert.are.equal(keymap.to_keymap('|'), '<Bar>')
end)
describe('fallback', function()
before_each(spec.before)
local run_fallback = function(keys, fallback)
local state = {}
feedkeys.call(keys, '', function()
fallback()
end)
feedkeys.call('', '', 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)
feedkeys.call('', 'x')
return state
end
describe('basic', function()
it('<Plug>', function()
vim.api.nvim_buf_set_keymap(0, 'i', '<Plug>(pairs)', '()<Left>', { noremap = true })
vim.api.nvim_buf_set_keymap(0, 'i', '(', '<Plug>(pairs)', { noremap = false })
local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '('))
local state = run_fallback('i', fallback)
assert.are.same({ '()' }, state.buffer)
assert.are.same({ 1, 1 }, state.cursor)
end)
it('<C-r>=', function()
vim.api.nvim_buf_set_keymap(0, 'i', '(', '<C-r>="()"<CR><Left>', {})
local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '('))
local state = run_fallback('i', fallback)
assert.are.same({ '()' }, state.buffer)
assert.are.same({ 1, 1 }, state.cursor)
end)
it('callback', function()
vim.api.nvim_buf_set_keymap(0, 'i', '(', '', {
callback = function()
vim.api.nvim_feedkeys('()' .. keymap.t('<Left>'), 'int', true)
end,
})
local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '('))
local state = run_fallback('i', fallback)
assert.are.same({ '()' }, state.buffer)
assert.are.same({ 1, 1 }, state.cursor)
end)
it('expr-callback', function()
vim.api.nvim_buf_set_keymap(0, 'i', '(', '', {
expr = true,
noremap = false,
silent = true,
callback = function()
return '()' .. keymap.t('<Left>')
end,
})
local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '('))
local state = run_fallback('i', fallback)
assert.are.same({ '()' }, state.buffer)
assert.are.same({ 1, 1 }, state.cursor)
end)
-- it('cmdline default <Tab>', function()
-- local fallback = keymap.fallback(0, 'c', keymap.get_map('c', '<Tab>'))
-- local state = run_fallback(':', fallback)
-- assert.are.same({ '' }, state.buffer)
-- assert.are.same({ 1, 0 }, state.cursor)
-- end)
end)
describe('recursive', function()
it('non-expr', function()
vim.api.nvim_buf_set_keymap(0, 'i', '(', '()<Left>', {
expr = false,
noremap = false,
silent = true,
})
local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '('))
local state = run_fallback('i', fallback)
assert.are.same({ '()' }, state.buffer)
assert.are.same({ 1, 1 }, state.cursor)
end)
it('expr', function()
vim.api.nvim_buf_set_keymap(0, 'i', '(', '"()<Left>"', {
expr = true,
noremap = false,
silent = true,
})
local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '('))
local state = run_fallback('i', fallback)
assert.are.same({ '()' }, state.buffer)
assert.are.same({ 1, 1 }, state.cursor)
end)
it('expr-callback', function()
pcall(function()
vim.api.nvim_buf_set_keymap(0, 'i', '(', '', {
expr = true,
noremap = false,
silent = true,
callback = function()
return keymap.t('()<Left>')
end,
})
local fallback = keymap.fallback(0, 'i', keymap.get_map('i', '('))
local state = run_fallback('i', fallback)
assert.are.same({ '()' }, state.buffer)
assert.are.same({ 1, 1 }, state.cursor)
end)
end)
end)
end)
describe('realworld', function()
before_each(spec.before)
it('#226', function()
keymap.listen('i', '<c-n>', function(_, fallback)
fallback()
end)
vim.api.nvim_feedkeys(keymap.t('iaiueo<CR>a<C-n><C-n>'), 'tx', true)
assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true))
end)
it('#414', function()
keymap.listen('i', '<M-j>', function()
vim.api.nvim_feedkeys(keymap.t('<C-n>'), 'int', true)
end)
vim.api.nvim_feedkeys(keymap.t('iaiueo<CR>a<M-j><M-j>'), 'tx', true)
assert.are.same({ 'aiueo', 'aiueo' }, vim.api.nvim_buf_get_lines(0, 0, -1, true))
end)
it('#744', function()
vim.api.nvim_buf_set_keymap(0, 'i', '<C-r>', 'recursive', {
noremap = true,
})
vim.api.nvim_buf_set_keymap(0, 'i', '<CR>', '<CR>recursive', {
noremap = false,
})
keymap.listen('i', '<CR>', function(_, fallback)
fallback()
end)
feedkeys.call(keymap.t('i<CR>'), 'tx')
assert.are.same({ '', 'recursive' }, vim.api.nvim_buf_get_lines(0, 0, -1, true))
end)
end)
end)

View File

@ -0,0 +1,253 @@
local misc = {}
local islist = vim.islist or vim.tbl_islist
---Create once callback
---@param callback function
---@return function
misc.once = function(callback)
local done = false
return function(...)
if done then
return
end
done = true
callback(...)
end
end
---Return concatenated list
---@param list1 any[]
---@param list2 any[]
---@return any[]
misc.concat = function(list1, list2)
local new_list = {}
for _, v in ipairs(list1) do
table.insert(new_list, v)
end
for _, v in ipairs(list2) do
table.insert(new_list, v)
end
return new_list
end
---Repeat values
---@generic T
---@param str_or_tbl T
---@param count integer
---@return T
misc.rep = function(str_or_tbl, count)
if type(str_or_tbl) == 'string' then
return string.rep(str_or_tbl, count)
end
local rep = {}
for _ = 1, count do
for _, v in ipairs(str_or_tbl) do
table.insert(rep, v)
end
end
return rep
end
---Return the valu is empty or not.
---@param v any
---@return boolean
misc.empty = function(v)
if not v then
return true
end
if v == vim.NIL then
return true
end
if type(v) == 'string' and v == '' then
return true
end
if type(v) == 'table' and vim.tbl_isempty(v) then
return true
end
if type(v) == 'number' and v == 0 then
return true
end
return false
end
---Search value in table
misc.contains = function(tbl, v)
for _, value in ipairs(tbl) do
if value == v then
return true
end
end
return false
end
---The symbol to remove key in misc.merge.
misc.none = vim.NIL
---Merge two tables recursively
---@generic T
---@param tbl1 T
---@param tbl2 T
---@return T
misc.merge = function(tbl1, tbl2)
local is_dict1 = type(tbl1) == 'table' and (not islist(tbl1) or vim.tbl_isempty(tbl1))
local is_dict2 = type(tbl2) == 'table' and (not islist(tbl2) or vim.tbl_isempty(tbl2))
if is_dict1 and is_dict2 then
local new_tbl = {}
for k, v in pairs(tbl2) do
if tbl1[k] ~= misc.none then
new_tbl[k] = misc.merge(tbl1[k], v)
end
end
for k, v in pairs(tbl1) do
if tbl2[k] == nil then
if v ~= misc.none then
new_tbl[k] = misc.merge(v, {})
else
new_tbl[k] = nil
end
end
end
return new_tbl
end
if tbl1 == misc.none then
return nil
elseif tbl1 == nil then
return misc.merge(tbl2, {})
else
return tbl1
end
end
---Generate id for group name
misc.id = setmetatable({
group = {},
}, {
__call = function(_, group)
misc.id.group[group] = misc.id.group[group] or 0
misc.id.group[group] = misc.id.group[group] + 1
return misc.id.group[group]
end,
})
---Treat 1/0 as bool value
---@param v boolean|1|0
---@param def boolean
---@return boolean
misc.bool = function(v, def)
if v == nil then
return def
end
return v == true or v == 1
end
---Set value to deep object
---@param t table
---@param keys string[]
---@param v any
misc.set = function(t, keys, v)
local c = t
for i = 1, #keys - 1 do
local key = keys[i]
c[key] = c[key] or {}
c = c[key]
end
c[keys[#keys]] = v
end
---Copy table
---@generic T
---@param tbl T
---@return T
misc.copy = function(tbl)
if type(tbl) ~= 'table' then
return tbl
end
if islist(tbl) then
local copy = {}
for i, value in ipairs(tbl) do
copy[i] = misc.copy(value)
end
return copy
end
local copy = {}
for key, value in pairs(tbl) do
copy[key] = misc.copy(value)
end
return copy
end
---Safe version of vim.str_utfindex
---@param text string
---@param vimindex integer|nil
---@return integer
misc.to_utfindex = function(text, vimindex)
vimindex = vimindex or #text + 1
return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text)))
end
---Safe version of vim.str_byteindex
---@param text string
---@param utfindex integer
---@return integer
misc.to_vimindex = function(text, utfindex)
utfindex = utfindex or #text
for i = utfindex, 1, -1 do
local s, v = pcall(function()
return vim.str_byteindex(text, i) + 1
end)
if s then
return v
end
end
return utfindex + 1
end
---Mark the function as deprecated
misc.deprecated = function(fn, msg)
local printed = false
return function(...)
if not printed then
print(msg)
printed = true
end
return fn(...)
end
end
--Redraw
misc.redraw = setmetatable({
doing = false,
force = false,
-- We use `<Up><Down>` to redraw the screen. (Previously, We use <C-r><ESC>. it will remove the unmatches search history.)
incsearch_redraw_keys = ' <BS>',
}, {
__call = function(self, force)
local termcode = vim.api.nvim_replace_termcodes(self.incsearch_redraw_keys, true, true, true)
if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then
if vim.o.incsearch then
return vim.api.nvim_feedkeys(termcode, 'ni', true)
end
end
if self.doing then
return
end
self.doing = true
self.force = not not force
vim.schedule(function()
if self.force then
vim.cmd([[redraw!]])
else
vim.cmd([[redraw]])
end
self.doing = false
self.force = false
end)
end,
})
return misc

View File

@ -0,0 +1,63 @@
local spec = require('cmp.utils.spec')
local misc = require('cmp.utils.misc')
describe('misc', function()
before_each(spec.before)
it('merge', function()
local merged
merged = misc.merge({
a = {},
}, {
a = {
b = 1,
},
})
assert.are.equal(merged.a.b, 1)
merged = misc.merge({
a = {
i = 1,
},
}, {
a = {
c = 2,
},
})
assert.are.equal(merged.a.i, 1)
assert.are.equal(merged.a.c, 2)
merged = misc.merge({
a = false,
}, {
a = {
b = 1,
},
})
assert.are.equal(merged.a, false)
merged = misc.merge({
a = misc.none,
}, {
a = {
b = 1,
},
})
assert.are.equal(merged.a, nil)
merged = misc.merge({
a = misc.none,
}, {
a = nil,
})
assert.are.equal(merged.a, nil)
merged = misc.merge({
a = nil,
}, {
a = misc.none,
})
assert.are.equal(merged.a, nil)
end)
end)

View File

@ -0,0 +1,25 @@
local M = {}
-- Set window option without triggering the OptionSet event
---@param window number
---@param name string
---@param value any
M.win_set_option = function(window, name, value)
local eventignore = vim.opt.eventignore:get()
vim.opt.eventignore:append('OptionSet')
vim.api.nvim_win_set_option(window, name, value)
vim.opt.eventignore = eventignore
end
-- Set buffer option without triggering the OptionSet event
---@param buffer number
---@param name string
---@param value any
M.buf_set_option = function(buffer, name, value)
local eventignore = vim.opt.eventignore:get()
vim.opt.eventignore:append('OptionSet')
vim.api.nvim_buf_set_option(buffer, name, value)
vim.opt.eventignore = eventignore
end
return M

View File

@ -0,0 +1,28 @@
local pattern = {}
pattern._regexes = {}
pattern.regex = function(p)
if not pattern._regexes[p] then
pattern._regexes[p] = vim.regex(p)
end
return pattern._regexes[p]
end
pattern.offset = function(p, text)
local s, e = pattern.regex(p):match_str(text)
if s then
return s + 1, e + 1
end
return nil, nil
end
pattern.matchstr = function(p, text)
local s, e = pattern.offset(p, text)
if s then
return string.sub(text, s, e)
end
return nil
end
return pattern

View File

@ -0,0 +1,414 @@
local misc = require('cmp.utils.misc')
local P = {}
---Take characters until the target characters (The escape sequence is '\' + char)
---@param targets string[] The character list for stop consuming text.
---@param specials string[] If the character isn't contained in targets/specials, '\' will be left.
P.take_until = function(targets, specials)
targets = targets or {}
specials = specials or {}
return function(input, pos)
local new_pos = pos
local raw = {}
local esc = {}
while new_pos <= #input do
local c = string.sub(input, new_pos, new_pos)
if c == '\\' then
table.insert(raw, '\\')
new_pos = new_pos + 1
c = string.sub(input, new_pos, new_pos)
if not misc.contains(targets, c) and not misc.contains(specials, c) then
table.insert(esc, '\\')
end
table.insert(raw, c)
table.insert(esc, c)
new_pos = new_pos + 1
else
if misc.contains(targets, c) then
break
end
table.insert(raw, c)
table.insert(esc, c)
new_pos = new_pos + 1
end
end
if new_pos == pos then
return P.unmatch(pos)
end
return {
parsed = true,
value = {
raw = table.concat(raw, ''),
esc = table.concat(esc, ''),
},
pos = new_pos,
}
end
end
P.unmatch = function(pos)
return {
parsed = false,
value = nil,
pos = pos,
}
end
P.map = function(parser, map)
return function(input, pos)
local result = parser(input, pos)
if result.parsed then
return {
parsed = true,
value = map(result.value),
pos = result.pos,
}
end
return P.unmatch(pos)
end
end
P.lazy = function(factory)
return function(input, pos)
return factory()(input, pos)
end
end
P.token = function(token)
return function(input, pos)
local maybe_token = string.sub(input, pos, pos + #token - 1)
if token == maybe_token then
return {
parsed = true,
value = maybe_token,
pos = pos + #token,
}
end
return P.unmatch(pos)
end
end
P.pattern = function(p)
return function(input, pos)
local maybe_match = string.match(string.sub(input, pos), '^' .. p)
if maybe_match then
return {
parsed = true,
value = maybe_match,
pos = pos + #maybe_match,
}
end
return P.unmatch(pos)
end
end
P.many = function(parser)
return function(input, pos)
local values = {}
local new_pos = pos
while new_pos <= #input do
local result = parser(input, new_pos)
if not result.parsed then
break
end
table.insert(values, result.value)
new_pos = result.pos
end
if #values > 0 then
return {
parsed = true,
value = values,
pos = new_pos,
}
end
return P.unmatch(pos)
end
end
P.any = function(...)
local parsers = { ... }
return function(input, pos)
for _, parser in ipairs(parsers) do
local result = parser(input, pos)
if result.parsed then
return result
end
end
return P.unmatch(pos)
end
end
P.opt = function(parser)
return function(input, pos)
local result = parser(input, pos)
return {
parsed = true,
value = result.value,
pos = result.pos,
}
end
end
P.seq = function(...)
local parsers = { ... }
return function(input, pos)
local values = {}
local new_pos = pos
for i, parser in ipairs(parsers) do
local result = parser(input, new_pos)
if result.parsed then
values[i] = result.value
new_pos = result.pos
else
return P.unmatch(pos)
end
end
return {
parsed = true,
value = values,
pos = new_pos,
}
end
end
local Node = {}
Node.Type = {
SNIPPET = 0,
TABSTOP = 1,
PLACEHOLDER = 2,
VARIABLE = 3,
CHOICE = 4,
TRANSFORM = 5,
FORMAT = 6,
TEXT = 7,
}
function Node:__tostring()
local insert_text = {}
if self.type == Node.Type.SNIPPET then
for _, c in ipairs(self.children) do
table.insert(insert_text, tostring(c))
end
elseif self.type == Node.Type.CHOICE then
table.insert(insert_text, self.items[1])
elseif self.type == Node.Type.PLACEHOLDER then
for _, c in ipairs(self.children or {}) do
table.insert(insert_text, tostring(c))
end
elseif self.type == Node.Type.TEXT then
table.insert(insert_text, self.esc)
end
return table.concat(insert_text, '')
end
--@see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
local S = {}
S.dollar = P.token('$')
S.open = P.token('{')
S.close = P.token('}')
S.colon = P.token(':')
S.slash = P.token('/')
S.comma = P.token(',')
S.pipe = P.token('|')
S.plus = P.token('+')
S.minus = P.token('-')
S.question = P.token('?')
S.int = P.map(P.pattern('[0-9]+'), function(value)
return tonumber(value, 10)
end)
S.var = P.pattern('[%a_][%w_]+')
S.text = function(targets, specials)
return P.map(P.take_until(targets, specials), function(value)
return setmetatable({
type = Node.Type.TEXT,
raw = value.raw,
esc = value.esc,
}, Node)
end)
end
S.toplevel = P.lazy(function()
return P.any(S.placeholder, S.tabstop, S.variable, S.choice)
end)
S.format = P.any(
P.map(P.seq(S.dollar, S.int), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[2],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[3],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.slash, P.any(P.token('upcase'), P.token('downcase'), P.token('capitalize'), P.token('camelcase'), P.token('pascalcase')), S.close), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[3],
modifier = values[6],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.seq(S.question, P.opt(P.take_until({ ':' }, { '\\' })), S.colon, P.opt(P.take_until({ '}' }, { '\\' }))), S.close), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[3],
if_text = values[5][2] and values[5][2].esc or '',
else_text = values[5][4] and values[5][4].esc or '',
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.seq(S.plus, P.opt(P.take_until({ '}' }, { '\\' }))), S.close), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[3],
if_text = values[5][2] and values[5][2].esc or '',
else_text = '',
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.minus, P.opt(P.take_until({ '}' }, { '\\' })), S.close), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[3],
if_text = '',
else_text = values[6] and values[6].esc or '',
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.opt(P.take_until({ '}' }, { '\\' })), S.close), function(values)
return setmetatable({
type = Node.Type.FORMAT,
capture_index = values[3],
if_text = '',
else_text = values[5] and values[5].esc or '',
}, Node)
end)
)
S.transform = P.map(P.seq(S.slash, P.take_until({ '/' }, { '\\' }), S.slash, P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))), S.slash, P.opt(P.pattern('[ig]+'))), function(values)
return setmetatable({
type = Node.Type.TRANSFORM,
pattern = values[2].raw,
format = values[4],
option = values[6],
}, Node)
end)
S.tabstop = P.any(
P.map(P.seq(S.dollar, S.int), function(values)
return setmetatable({
type = Node.Type.TABSTOP,
tabstop = values[2],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
return setmetatable({
type = Node.Type.TABSTOP,
tabstop = values[3],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.int, S.transform, S.close), function(values)
return setmetatable({
type = Node.Type.TABSTOP,
tabstop = values[3],
transform = values[4],
}, Node)
end)
)
S.placeholder = P.any(P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.opt(P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' })))), S.close), function(values)
return setmetatable({
type = Node.Type.PLACEHOLDER,
tabstop = values[3],
-- insert empty text if opt did not match.
children = values[5] or {
setmetatable({
type = Node.Type.TEXT,
raw = '',
esc = '',
}, Node),
},
}, Node)
end))
S.choice = P.map(
P.seq(
S.dollar,
S.open,
S.int,
S.pipe,
P.many(P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values)
return values[1].esc
end)),
S.pipe,
S.close
),
function(values)
return setmetatable({
type = Node.Type.CHOICE,
tabstop = values[3],
items = values[5],
}, Node)
end
)
S.variable = P.any(
P.map(P.seq(S.dollar, S.var), function(values)
return setmetatable({
type = Node.Type.VARIABLE,
name = values[2],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.var, S.close), function(values)
return setmetatable({
type = Node.Type.VARIABLE,
name = values[3],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.var, S.transform, S.close), function(values)
return setmetatable({
type = Node.Type.VARIABLE,
name = values[3],
transform = values[4],
}, Node)
end),
P.map(P.seq(S.dollar, S.open, S.var, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
return setmetatable({
type = Node.Type.VARIABLE,
name = values[3],
children = values[5],
}, Node)
end)
)
S.snippet = P.map(P.many(P.any(S.toplevel, S.text({ '$' }, { '}', '\\' }))), function(values)
return setmetatable({
type = Node.Type.SNIPPET,
children = values,
}, Node)
end)
local M = {}
---The snippet node type enum
---@types table<string, integer>
M.NodeType = Node.Type
---Parse snippet string and returns the AST
---@param input string
---@return table
function M.parse(input)
local result = S.snippet(input, 1)
if not result.parsed then
error('snippet parsing failed.')
end
return result.value
end
return M

View File

@ -0,0 +1,92 @@
local context = require('cmp.context')
local source = require('cmp.source')
local types = require('cmp.types')
local config = require('cmp.config')
local spec = {}
spec.before = function()
vim.cmd([[
bdelete!
enew!
imapclear
imapclear <buffer>
cmapclear
cmapclear <buffer>
smapclear
smapclear <buffer>
xmapclear
xmapclear <buffer>
tmapclear
tmapclear <buffer>
setlocal noswapfile
setlocal virtualedit=all
setlocal completeopt=menu,menuone,noselect
]])
config.set_global({
sources = {
{ name = 'spec' },
},
snippet = {
expand = function(args)
local ctx = context.new()
vim.api.nvim_buf_set_text(ctx.bufnr, ctx.cursor.row - 1, ctx.cursor.col - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, vim.split(string.gsub(args.body, '%$0', ''), '\n'))
for i, t in ipairs(vim.split(args.body, '\n')) do
local s = string.find(t, '$0', 1, true)
if s then
if i == 1 then
vim.api.nvim_win_set_cursor(0, { ctx.cursor.row, ctx.cursor.col + s - 2 })
else
vim.api.nvim_win_set_cursor(0, { ctx.cursor.row + i - 1, s - 1 })
end
break
end
end
end,
},
})
config.set_cmdline({
sources = {
{ name = 'spec' },
},
}, ':')
end
spec.state = function(text, row, col)
vim.fn.setline(1, text)
vim.fn.cursor(row, col)
local ctx = context.empty()
local s = source.new('spec', {
complete = function() end,
})
return {
context = function()
return ctx
end,
source = function()
return s
end,
backspace = function()
vim.fn.feedkeys('x', 'nx')
vim.fn.feedkeys('h', 'nx')
ctx = context.new(ctx, { reason = types.cmp.ContextReason.Auto })
s:complete(ctx, function() end)
return ctx
end,
input = function(char)
vim.fn.feedkeys(('i%s'):format(char), 'nx')
vim.fn.feedkeys(string.rep('l', #char), 'nx')
ctx.prev_context = nil
ctx = context.new(ctx, { reason = types.cmp.ContextReason.Auto })
s:complete(ctx, function() end)
return ctx
end,
manual = function()
ctx = context.new(ctx, { reason = types.cmp.ContextReason.Manual })
s:complete(ctx, function() end)
return ctx
end,
}
end
return spec

View File

@ -0,0 +1,178 @@
local char = require('cmp.utils.char')
local str = {}
local INVALIDS = {}
INVALIDS[string.byte("'")] = true
INVALIDS[string.byte('"')] = true
INVALIDS[string.byte('=')] = true
INVALIDS[string.byte('$')] = true
INVALIDS[string.byte('(')] = true
INVALIDS[string.byte('[')] = true
INVALIDS[string.byte('<')] = true
INVALIDS[string.byte('{')] = true
INVALIDS[string.byte(' ')] = true
INVALIDS[string.byte('\t')] = true
INVALIDS[string.byte('\n')] = true
INVALIDS[string.byte('\r')] = true
local NR_BYTE = string.byte('\n')
local PAIRS = {}
PAIRS[string.byte('<')] = string.byte('>')
PAIRS[string.byte('[')] = string.byte(']')
PAIRS[string.byte('(')] = string.byte(')')
PAIRS[string.byte('{')] = string.byte('}')
PAIRS[string.byte('"')] = string.byte('"')
PAIRS[string.byte("'")] = string.byte("'")
---Return if specified text has prefix or not
---@param text string
---@param prefix string
---@return boolean
str.has_prefix = function(text, prefix)
if #text < #prefix then
return false
end
for i = 1, #prefix do
if not char.match(string.byte(text, i), string.byte(prefix, i)) then
return false
end
end
return true
end
---get_common_string
str.get_common_string = function(text1, text2)
local min = math.min(#text1, #text2)
for i = 1, min do
if not char.match(string.byte(text1, i), string.byte(text2, i)) then
return string.sub(text1, 1, i - 1)
end
end
return string.sub(text1, 1, min)
end
---Remove suffix
---@param text string
---@param suffix string
---@return string
str.remove_suffix = function(text, suffix)
if #text < #suffix then
return text
end
local i = 0
while i < #suffix do
if string.byte(text, #text - i) ~= string.byte(suffix, #suffix - i) then
return text
end
i = i + 1
end
return string.sub(text, 1, -#suffix - 1)
end
---trim
---@param text string
---@return string
str.trim = function(text)
local s = 1
for i = 1, #text do
if not char.is_white(string.byte(text, i)) then
s = i
break
end
end
local e = #text
for i = #text, 1, -1 do
if not char.is_white(string.byte(text, i)) then
e = i
break
end
end
if s == 1 and e == #text then
return text
end
return string.sub(text, s, e)
end
---get_word
---@param text string
---@param stop_char? integer
---@param min_length? integer
---@return string
str.get_word = function(text, stop_char, min_length)
min_length = min_length or 0
local has_alnum = false
local stack = {}
local word = {}
local add = function(c)
table.insert(word, string.char(c))
if stack[#stack] == c then
table.remove(stack, #stack)
else
if PAIRS[c] then
table.insert(stack, c)
end
end
end
for i = 1, #text do
local c = string.byte(text, i, i)
if #word < min_length then
table.insert(word, string.char(c))
elseif not INVALIDS[c] then
add(c)
has_alnum = has_alnum or char.is_alnum(c)
elseif not has_alnum then
add(c)
elseif #stack ~= 0 then
add(c)
if has_alnum and #stack == 0 then
break
end
else
break
end
end
if stop_char and word[#word] == string.char(stop_char) then
table.remove(word, #word)
end
return table.concat(word, '')
end
---Oneline
---@param text string
---@return string
str.oneline = function(text)
for i = 1, #text do
if string.byte(text, i) == NR_BYTE then
return string.sub(text, 1, i - 1)
end
end
return text
end
---Escape special chars
---@param text string
---@param chars string[]
---@return string
str.escape = function(text, chars)
table.insert(chars, '\\')
local escaped = {}
local i = 1
while i <= #text do
local c = string.sub(text, i, i)
if vim.tbl_contains(chars, c) then
table.insert(escaped, '\\')
table.insert(escaped, c)
else
table.insert(escaped, c)
end
i = i + 1
end
return table.concat(escaped, '')
end
return str

View File

@ -0,0 +1,29 @@
local str = require('cmp.utils.str')
describe('utils.str', function()
it('get_word', function()
assert.are.equal(str.get_word('print'), 'print')
assert.are.equal(str.get_word('$variable'), '$variable')
assert.are.equal(str.get_word('print()'), 'print')
assert.are.equal(str.get_word('["cmp#confirm"]'), '["cmp#confirm"]')
assert.are.equal(str.get_word('"devDependencies":', string.byte('"')), '"devDependencies')
assert.are.equal(str.get_word('"devDependencies": ${1},', string.byte('"')), '"devDependencies')
assert.are.equal(str.get_word('#[cfg(test)]'), '#[cfg(test)]')
assert.are.equal(str.get_word('import { GetStaticProps$1 } from "next";', nil, 9), 'import { GetStaticProps')
end)
it('remove_suffix', function()
assert.are.equal(str.remove_suffix('log()', '$0'), 'log()')
assert.are.equal(str.remove_suffix('log()$0', '$0'), 'log()')
assert.are.equal(str.remove_suffix('log()${0}', '${0}'), 'log()')
assert.are.equal(str.remove_suffix('log()${0:placeholder}', '${0}'), 'log()${0:placeholder}')
end)
it('escape', function()
assert.are.equal(str.escape('plain', {}), 'plain')
assert.are.equal(str.escape('plain\\', {}), 'plain\\\\')
assert.are.equal(str.escape('plain\\"', {}), 'plain\\\\"')
assert.are.equal(str.escape('pla"in', { '"' }), 'pla\\"in')
assert.are.equal(str.escape('call("")', { '"' }), 'call(\\"\\")')
end)
end)

View File

@ -0,0 +1,318 @@
local misc = require('cmp.utils.misc')
local opt = require('cmp.utils.options')
local buffer = require('cmp.utils.buffer')
local api = require('cmp.utils.api')
local config = require('cmp.config')
---@class cmp.WindowStyle
---@field public relative string
---@field public row integer
---@field public col integer
---@field public width integer|float
---@field public height integer|float
---@field public border string|string[]|nil
---@field public zindex integer|nil
---@class cmp.Window
---@field public name string
---@field public win integer|nil
---@field public thumb_win integer|nil
---@field public sbar_win integer|nil
---@field public style cmp.WindowStyle
---@field public opt table<string, any>
---@field public buffer_opt table<string, any>
local window = {}
---new
---@return cmp.Window
window.new = function()
local self = setmetatable({}, { __index = window })
self.name = misc.id('cmp.utils.window.new')
self.win = nil
self.sbar_win = nil
self.thumb_win = nil
self.style = {}
self.opt = {}
self.buffer_opt = {}
return self
end
---Set window option.
---NOTE: If the window already visible, immediately applied to it.
---@param key string
---@param value any
window.option = function(self, key, value)
if vim.fn.exists('+' .. key) == 0 then
return
end
if value == nil then
return self.opt[key]
end
self.opt[key] = value
if self:visible() then
opt.win_set_option(self.win, key, value)
end
end
---Set buffer option.
---NOTE: If the buffer already visible, immediately applied to it.
---@param key string
---@param value any
window.buffer_option = function(self, key, value)
if vim.fn.exists('+' .. key) == 0 then
return
end
if value == nil then
return self.buffer_opt[key]
end
self.buffer_opt[key] = value
local existing_buf = buffer.get(self.name)
if existing_buf then
opt.buf_set_option(existing_buf, key, value)
end
end
---Set style.
---@param style cmp.WindowStyle
window.set_style = function(self, style)
self.style = style
local info = self:info()
if vim.o.lines and vim.o.lines <= info.row + info.height + 1 then
self.style.height = vim.o.lines - info.row - info.border_info.vert - 1
end
self.style.zindex = self.style.zindex or 1
--- GUI clients are allowed to return fractional bounds, but we need integer
--- bounds to open the window
self.style.width = math.ceil(self.style.width)
self.style.height = math.ceil(self.style.height)
end
---Return buffer id.
---@return integer
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
opt.buf_set_option(buf, k, v)
end
end
return buf
end
---Open window
---@param style cmp.WindowStyle
window.open = function(self, style)
if style then
self:set_style(style)
end
if self.style.width < 1 or self.style.height < 1 then
return
end
if self.win and vim.api.nvim_win_is_valid(self.win) then
vim.api.nvim_win_set_config(self.win, self.style)
else
local s = misc.copy(self.style)
s.noautocmd = true
self.win = vim.api.nvim_open_win(self:get_buffer(), false, s)
for k, v in pairs(self.opt) do
opt.win_set_option(self.win, k, v)
end
end
self:update()
end
---Update
window.update = function(self)
local info = self:info()
if info.scrollable and self.style.height > 0 then
-- Draw the background of the scrollbar
if not info.border_info.visible then
local style = {
relative = 'editor',
style = 'minimal',
width = 1,
height = self.style.height,
row = info.row,
col = info.col + info.width - info.scrollbar_offset, -- info.col was already contained the scrollbar offset.
zindex = (self.style.zindex and (self.style.zindex + 1) or 1),
}
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
vim.api.nvim_win_set_config(self.sbar_win, style)
else
style.noautocmd = true
self.sbar_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbar_buf'), false, style)
opt.win_set_option(self.sbar_win, 'winhighlight', 'EndOfBuffer:PmenuSbar,NormalFloat:PmenuSbar')
end
end
-- Draw the scrollbar thumb
local thumb_height = math.floor(info.inner_height * (info.inner_height / self:get_content_height()) + 0.5)
local thumb_offset = math.floor(info.inner_height * (vim.fn.getwininfo(self.win)[1].topline / self:get_content_height()))
local style = {
relative = 'editor',
style = 'minimal',
width = 1,
height = math.max(1, thumb_height),
row = info.row + thumb_offset + (info.border_info.visible and info.border_info.top or 0),
col = info.col + info.width - 1, -- info.col was already added scrollbar offset.
zindex = (self.style.zindex and (self.style.zindex + 2) or 2),
}
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
vim.api.nvim_win_set_config(self.thumb_win, style)
else
style.noautocmd = true
self.thumb_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'thumb_buf'), false, style)
opt.win_set_option(self.thumb_win, 'winhighlight', 'EndOfBuffer:PmenuThumb,NormalFloat:PmenuThumb')
end
else
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
vim.api.nvim_win_hide(self.sbar_win)
self.sbar_win = nil
end
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
vim.api.nvim_win_hide(self.thumb_win)
self.thumb_win = nil
end
end
-- In cmdline, vim does not redraw automatically.
if api.is_cmdline_mode() then
vim.api.nvim_win_call(self.win, function()
misc.redraw()
end)
end
end
---Close window
window.close = function(self)
if self.win and vim.api.nvim_win_is_valid(self.win) then
if self.win and vim.api.nvim_win_is_valid(self.win) then
vim.api.nvim_win_hide(self.win)
self.win = nil
end
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
vim.api.nvim_win_hide(self.sbar_win)
self.sbar_win = nil
end
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
vim.api.nvim_win_hide(self.thumb_win)
self.thumb_win = nil
end
end
end
---Return the window is visible or not.
window.visible = function(self)
return self.win and vim.api.nvim_win_is_valid(self.win)
end
---Return win info.
window.info = function(self)
local border_info = self:get_border_info()
local scrollbar = config.get().window.completion.scrollbar
local info = {
row = self.style.row,
col = self.style.col,
width = self.style.width + border_info.left + border_info.right,
height = self.style.height + border_info.top + border_info.bottom,
inner_width = self.style.width,
inner_height = self.style.height,
border_info = border_info,
scrollable = false,
scrollbar_offset = 0,
}
if self:get_content_height() > info.inner_height and scrollbar then
info.scrollable = true
if not border_info.visible then
info.scrollbar_offset = 1
info.width = info.width + 1
end
end
return info
end
---Return border information.
---@return { top: integer, left: integer, right: integer, bottom: integer, vert: integer, horiz: integer, visible: boolean }
window.get_border_info = function(self)
local border = self.style.border
if not border or border == 'none' then
return {
top = 0,
left = 0,
right = 0,
bottom = 0,
vert = 0,
horiz = 0,
visible = false,
}
end
if type(border) == 'string' then
if border == 'shadow' then
return {
top = 0,
left = 0,
right = 1,
bottom = 1,
vert = 1,
horiz = 1,
visible = false,
}
end
return {
top = 1,
left = 1,
right = 1,
bottom = 1,
vert = 2,
horiz = 2,
visible = true,
}
end
local new_border = {}
while #new_border <= 8 do
for _, b in ipairs(border) do
table.insert(new_border, type(b) == 'string' and b or b[1])
end
end
local info = {}
info.top = new_border[2] == '' and 0 or 1
info.right = new_border[4] == '' and 0 or 1
info.bottom = new_border[6] == '' and 0 or 1
info.left = new_border[8] == '' and 0 or 1
info.vert = info.top + info.bottom
info.horiz = info.left + info.right
info.visible = not (vim.tbl_contains({ '', ' ' }, new_border[2]) and vim.tbl_contains({ '', ' ' }, new_border[4]) and vim.tbl_contains({ '', ' ' }, new_border[6]) and vim.tbl_contains({ '', ' ' }, new_border[8]))
return info
end
---Get scroll height.
---NOTE: The result of vim.fn.strdisplaywidth depends on the buffer it was called in (see comment in cmp.Entry.get_view).
---@return integer
window.get_content_height = function(self)
if not self:option('wrap') then
return vim.api.nvim_buf_line_count(self:get_buffer())
end
local height = 0
vim.api.nvim_buf_call(self:get_buffer(), function()
for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do
height = height + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / self.style.width))
end
end)
return height
end
return window

View File

@ -0,0 +1,307 @@
local config = require('cmp.config')
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 ghost_text_view = require('cmp.view.ghost_text_view')
---@class cmp.View
---@field public event cmp.Event
---@field private is_docs_view_pinned boolean
---@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 docs_view cmp.DocsView
---@field private ghost_text_view cmp.GhostTextView
local view = {}
---Create menu
view.new = function()
local self = setmetatable({}, { __index = view })
self.resolve_dedup = async.dedup()
self.is_docs_view_pinned = false
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.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()
end
---OnChange handler.
view.on_change = function(self)
self:_get_entries_view():on_change()
end
---Open menu
---@param ctx cmp.Context
---@param sources cmp.Source[]
---@return boolean did_open
view.open = function(self, ctx, sources)
local source_group_map = {}
for _, s in ipairs(sources) do
local group_index = s:get_source_config().group_index or 0
if not source_group_map[group_index] then
source_group_map[group_index] = {}
end
table.insert(source_group_map[group_index], s)
end
local group_indexes = vim.tbl_keys(source_group_map)
table.sort(group_indexes, function(a, b)
return a ~= b and (a < b) or nil
end)
local entries = {}
for _, group_index in ipairs(group_indexes) do
local source_group = source_group_map[group_index] or {}
-- check the source triggered by character
local has_triggered_by_symbol_source = false
for _, s in ipairs(source_group) do
if #s:get_entries(ctx) > 0 then
if s.is_triggered_by_symbol then
has_triggered_by_symbol_source = true
break
end
end
end
-- create filtered entries.
local offset = ctx.cursor.col
local group_entries = {}
local max_item_counts = {}
for i, s in ipairs(source_group) do
if s.offset <= ctx.cursor.col then
if not has_triggered_by_symbol_source or s.is_triggered_by_symbol then
-- prepare max_item_counts map for filtering after sort.
local max_item_count = s:get_source_config().max_item_count
if max_item_count ~= nil then
max_item_counts[s.name] = max_item_count
end
-- source order priority bonus.
local priority = s:get_source_config().priority or ((#source_group - (i - 1)) * config.get().sorting.priority_weight)
for _, e in ipairs(s:get_entries(ctx)) do
e.score = e.score + priority
table.insert(group_entries, e)
offset = math.min(offset, e:get_offset())
end
end
end
end
-- sort.
local comparetors = config.get().sorting.comparators
table.sort(group_entries, function(e1, e2)
for _, fn in ipairs(comparetors) do
local diff = fn(e1, e2)
if diff ~= nil then
return diff
end
end
end)
-- filter by max_item_count.
for _, e in ipairs(group_entries) do
if max_item_counts[e.source.name] ~= nil then
if max_item_counts[e.source.name] > 0 then
max_item_counts[e.source.name] = max_item_counts[e.source.name] - 1
table.insert(entries, e)
end
else
table.insert(entries, e)
end
end
local max_view_entries = config.get().performance.max_view_entries or 200
entries = vim.list_slice(entries, 1, max_view_entries)
-- open
if #entries > 0 then
self:_get_entries_view():open(offset, entries)
self.event:emit('menu_opened', {
window = self:_get_entries_view(),
})
break
end
end
-- complete_done.
if #entries == 0 then
self:close()
end
return #entries > 0
end
---Close menu
view.close = function(self)
if self:visible() then
self.is_docs_view_pinned = false
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)
if self:visible() then
self.is_docs_view_pinned = false
end
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
---Opens the documentation window.
view.open_docs = function(self)
self.is_docs_view_pinned = true
local e = self:get_selected_entry()
if e then
e:resolve(vim.schedule_wrap(self.resolve_dedup(function()
if not self:visible() then
return
end
self.docs_view:open(e, self:_get_entries_view():info())
end)))
end
end
---Closes the documentation window.
view.close_docs = function(self)
self.is_docs_view_pinned = false
if self:get_selected_entry() then
self.docs_view:close()
end
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)
end
---Select prev menu item.
---@param option cmp.SelectOption
view.select_prev_item = function(self, option)
self:_get_entries_view():select_prev_item(option)
end
---Get offset.
view.get_offset = function(self)
return self:_get_entries_view():get_offset()
end
---Get entries.
---@return cmp.Entry[]
view.get_entries = function(self)
return self:_get_entries_view():get_entries()
end
---Get first entry
---@param self cmp.Entry|nil
view.get_first_entry = function(self)
return self:_get_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()
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
end
---On entry change
view.on_entry_change = async.throttle(function(self)
if not self:visible() then
return
end
local e = self:get_selected_entry()
if e then
for _, c in ipairs(config.get().confirmation.get_commit_characters(e:get_commit_characters())) do
keymap.listen('i', c, function(...)
self.event:emit('keymap', ...)
end)
end
e:resolve(vim.schedule_wrap(self.resolve_dedup(function()
if not self:visible() then
return
end
if self.is_docs_view_pinned or config.get().view.docs.auto_open then
self.docs_view:open(e, self:_get_entries_view():info())
end
end)))
else
self.docs_view:close()
end
e = e or self:get_first_entry()
if e then
self.ghost_text_view:show(e)
else
self.ghost_text_view:hide()
end
end, 20)
return view

View File

@ -0,0 +1,482 @@
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 DEFAULT_HEIGHT = 10 -- @see https://github.com/vim/vim/blob/master/src/popupmenu.c#L45
---@class cmp.CustomEntriesView
---@field private entries_win cmp.Window
---@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 = {}
custom_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.custom_entries_view')
custom_entries_view.new = function()
local self = setmetatable({}, { __index = custom_entries_view })
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)
-- This is done so that strdisplaywidth calculations for lines in the
-- custom_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.
self.entries_win:buffer_option('tabstop', 1)
self.entries_win:buffer_option('filetype', 'cmp_menu')
self.entries_win:buffer_option('buftype', 'nofile')
self.event = event.new()
self.offset = -1
self.active = false
self.entries = {}
self.bottom_up = false
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(custom_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
end
local fields = config.get().formatting.fields
for i = top, bot do
local e = self.entries[i + 1]
if e then
local v = e:get_view(self.offset, buf)
local o = config.get().window.completion.side_padding
local a = 0
for _, field in ipairs(fields) do
if field == types.cmp.ItemField.Abbr then
a = o
end
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, o, {
end_line = i,
end_col = o + v[field].bytes,
hl_group = v[field].hl_group,
hl_mode = 'combine',
ephemeral = true,
})
o = o + v[field].bytes + (self.column_width[field] - v[field].width) + 1
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, {
end_line = i,
end_col = a + m.word_match_end,
hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch',
hl_mode = 'combine',
ephemeral = true,
})
end
end
end
end,
})
return self
end
custom_entries_view.ready = function()
return vim.fn.pumvisible() == 0
end
custom_entries_view.on_change = function(self)
self.active = false
end
custom_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
elseif c.view.entries == nil or c.view.entries.selection_order == nil then
return true
else
return not self.bottom_up
end
end
custom_entries_view.open = function(self, offset, entries)
local completion = config.get().window.completion
assert(completion, 'config.get() must resolve window.completion with defaults')
self.offset = offset
self.entries = {}
self.column_width = { abbr = 0, kind = 0, menu = 0 }
local entries_buf = self.entries_win:get_buffer()
local lines = {}
local dedup = {}
local preselect_index = 0
for _, e in ipairs(entries) do
local view = e:get_view(offset, entries_buf)
if view.dup == 1 or not dedup[e.completion_item.label] then
dedup[e.completion_item.label] = true
self.column_width.abbr = math.max(self.column_width.abbr, view.abbr.width)
self.column_width.kind = math.max(self.column_width.kind, view.kind.width)
self.column_width.menu = math.max(self.column_width.menu, view.menu.width)
table.insert(self.entries, e)
table.insert(lines, ' ')
if preselect_index == 0 and e.completion_item.preselect then
preselect_index = #self.entries
end
end
end
if vim.bo[entries_buf].modifiable == false then
vim.bo[entries_buf].modifiable = true
vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines)
vim.bo[entries_buf].modifiable = false
else
vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines)
end
vim.api.nvim_buf_set_option(entries_buf, 'modified', false)
local width = 0
width = width + 1
width = width + self.column_width.abbr + (self.column_width.kind > 0 and 1 or 0)
width = width + self.column_width.kind + (self.column_width.menu > 0 and 1 or 0)
width = width + self.column_width.menu + 1
local height = vim.api.nvim_get_option_value('pumheight', {})
height = height ~= 0 and height or #self.entries
height = math.min(height, #self.entries)
local delta = 0
if not config.get().view.entries.follow_cursor then
local cursor_before_line = api.get_cursor_before_line()
delta = vim.fn.strdisplaywidth(cursor_before_line:sub(self.offset))
end
local pos = api.get_screen_cursor()
local row, col = pos[1], pos[2] - delta - 1
local border_info = window.get_border_info({ style = completion })
local border_offset_row = border_info.top + border_info.bottom
local border_offset_col = border_info.left + border_info.right
if math.floor(vim.o.lines * 0.5) <= row + border_offset_row and vim.o.lines - row - border_offset_row <= math.min(DEFAULT_HEIGHT, height) then
height = math.min(height, row - 1)
row = row - height - border_offset_row - 1
if row < 0 then
height = height + row
end
end
if math.floor(vim.o.columns * 0.5) <= col + border_offset_col and vim.o.columns - col - border_offset_col <= width then
width = math.min(width, vim.o.columns - 1)
col = vim.o.columns - width - border_offset_col - 1
if col < 0 then
width = width + col
end
end
if pos[1] > row then
self.bottom_up = true
else
self.bottom_up = false
end
if not self:is_direction_top_down() then
local n = #self.entries
for i = 1, math.floor(n / 2) do
self.entries[i], self.entries[n - i + 1] = self.entries[n - i + 1], self.entries[i]
end
if preselect_index ~= 0 then
preselect_index = #self.entries - preselect_index + 1
end
end
-- Apply window options (that might be changed) on the custom completion menu.
self.entries_win:option('winblend', completion.winblend)
self.entries_win:option('winhighlight', completion.winhighlight)
self.entries_win:option('scrolloff', completion.scrolloff)
self.entries_win:open({
relative = 'editor',
style = 'minimal',
row = math.max(0, row),
col = math.max(0, col + completion.col_offset),
width = width,
height = height,
border = completion.border,
zindex = completion.zindex or 1001,
})
-- Don't set the cursor if the entries_win:open function fails
-- due to the window's width or height being less than 1
if self.entries_win.win == nil then
return
end
-- Always set cursor when starting. It will be adjusted on the call to _select
vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 0 })
if preselect_index > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
self:_select(preselect_index, { behavior = types.cmp.SelectBehavior.Select, active = false })
elseif not string.match(config.get().completion.completeopt, 'noselect') then
if self:is_direction_top_down() then
self:_select(1, { behavior = types.cmp.SelectBehavior.Select, active = false })
else
self:_select(#self.entries, { behavior = types.cmp.SelectBehavior.Select, active = false })
end
else
if self:is_direction_top_down() then
self:_select(0, { behavior = types.cmp.SelectBehavior.Select, active = false })
else
self:_select(#self.entries + 1, { behavior = types.cmp.SelectBehavior.Select, active = false })
end
end
end
custom_entries_view.close = function(self)
self.prefix = nil
self.offset = -1
self.active = 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)
end
custom_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
local texts = {}
local fields = config.get().formatting.fields
local entries_buf = self.entries_win:get_buffer()
for i = topline, botline - 1 do
local e = self.entries[i + 1]
if e then
local view = e:get_view(self.offset, entries_buf)
local text = {}
table.insert(text, string.rep(' ', config.get().window.completion.side_padding))
for _, field in ipairs(fields) do
table.insert(text, view[field].text)
table.insert(text, string.rep(' ', 1 + self.column_width[field] - view[field].width))
end
table.insert(text, string.rep(' ', config.get().window.completion.side_padding))
table.insert(texts, table.concat(text, ''))
end
end
if vim.bo[entries_buf].modifiable == false then
vim.bo[entries_buf].modifiable = true
vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts)
vim.bo[entries_buf].modifiable = false
else
vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts)
end
vim.api.nvim_buf_set_option(entries_buf, 'modified', false)
if api.is_cmdline_mode() then
vim.api.nvim_win_call(self.entries_win.win, function()
misc.redraw()
end)
end
end
custom_entries_view.visible = function(self)
return self.entries_win:visible()
end
custom_entries_view.info = function(self)
return self.entries_win:info()
end
custom_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()
local last = #self.entries
if not self.entries_win:option('cursorline') then
cursor = (is_top_down and 1) or last
else
if is_top_down then
if cursor == last then
cursor = 0
else
cursor = cursor + option.count
if last < cursor then
cursor = last
end
end
else
if cursor == 0 then
cursor = last
else
cursor = cursor - option.count
if cursor < 0 then
cursor = 0
end
end
end
end
self:_select(cursor, {
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
active = true,
})
end
end
custom_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()
local last = #self.entries
if not self.entries_win:option('cursorline') then
cursor = (is_top_down and last) or 1
else
if is_top_down then
if cursor == 1 then
cursor = 0
else
cursor = cursor - option.count
if cursor < 0 then
cursor = 1
end
end
else
if cursor == last then
cursor = 0
else
cursor = cursor + option.count
if last < cursor then
cursor = last
end
end
end
end
self:_select(cursor, {
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
active = true,
})
end
end
custom_entries_view.get_offset = function(self)
if self:visible() then
return self.offset
end
return nil
end
custom_entries_view.get_entries = function(self)
if self:visible() then
return self.entries
end
return {}
end
custom_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)
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)
if self:visible() and self.active then
return self:get_selected_entry()
end
end
custom_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 ''
end
self.active = (0 < cursor and cursor <= #self.entries and option.active == true)
self.entries_win:option('cursorline', cursor > 0 and cursor <= #self.entries)
vim.api.nvim_win_set_cursor(self.entries_win.win, {
math.max(math.min(cursor, #self.entries), 1),
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')
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()
-- setcmdline() added in v0.8.0
if vim.fn.has('nvim-0.8') == 1 then
local current_line = api.get_current_line()
local before_line = current_line:sub(1, self.offset - 1)
local after_line = current_line:sub(cursor[2] + 1)
local pos = #before_line + #word + 1
vim.fn.setcmdline(before_line .. word .. after_line, pos)
vim.api.nvim_feedkeys(keymap.t('<Cmd>redraw<CR>'), 'ni', false)
else
vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true)
end
else
if this.pending then
return
end
this.pending = true
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

View File

@ -0,0 +1,151 @@
local window = require('cmp.utils.window')
local config = require('cmp.config')
---@class cmp.DocsView
---@field public window cmp.Window
local docs_view = {}
---Create new floating window module
docs_view.new = function()
local self = setmetatable({}, { __index = docs_view })
self.entry = nil
self.window = window.new()
self.window:option('conceallevel', 2)
self.window:option('concealcursor', 'n')
self.window:option('foldenable', false)
self.window:option('linebreak', true)
self.window:option('scrolloff', 0)
self.window:option('showbreak', 'NONE')
self.window:option('wrap', true)
self.window:buffer_option('filetype', 'cmp_docs')
self.window:buffer_option('buftype', 'nofile')
return self
end
---Open documentation window
---@param e cmp.Entry
---@param view cmp.WindowStyle
docs_view.open = function(self, e, view)
local documentation = config.get().window.documentation
if not documentation then
return
end
if not e or not view then
return self:close()
end
local border_info = window.get_border_info({ style = documentation })
local right_space = vim.o.columns - (view.col + view.width) - 1
local left_space = view.col - 1
local max_width = math.max(left_space, right_space)
if documentation.max_width > 0 then
max_width = math.min(documentation.max_width, max_width)
end
-- Update buffer content if needed.
if not self.entry or e.id ~= self.entry.id then
local documents = e:get_documentation()
if #documents == 0 then
return self:close()
end
self.entry = e
vim.api.nvim_buf_call(self.window:get_buffer(), function()
vim.cmd([[syntax clear]])
vim.api.nvim_buf_set_lines(self.window:get_buffer(), 0, -1, false, {})
end)
local opts = {
max_width = max_width - border_info.horiz,
}
if documentation.max_height > 0 then
opts.max_height = documentation.max_height
end
vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, opts)
end
-- Set buffer as not modified, so it can be removed without errors
vim.api.nvim_buf_set_option(self.window:get_buffer(), 'modified', false)
-- Calculate window size.
local opts = {
max_width = max_width - border_info.horiz,
}
if documentation.max_height > 0 then
opts.max_height = documentation.max_height - border_info.vert
end
local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window:get_buffer(), 0, -1, false), opts)
if width <= 0 or height <= 0 then
return self:close()
end
-- Calculate window position.
local right_col = view.col + view.width
local left_col = view.col - width - border_info.horiz
local col, left
if right_space >= width and left_space >= width then
if right_space < left_space then
col = left_col
left = true
else
col = right_col
end
elseif right_space >= width then
col = right_col
elseif left_space >= width then
col = left_col
left = true
else
return self:close()
end
-- Render window.
self.window:option('winblend', documentation.winblend)
self.window:option('winhighlight', documentation.winhighlight)
local style = {
relative = 'editor',
style = 'minimal',
width = width,
height = height,
row = view.row,
col = col,
border = documentation.border,
zindex = documentation.zindex or 50,
}
self.window:open(style)
-- Correct left-col for scrollbar existence.
if left then
style.col = style.col - self.window:info().scrollbar_offset
self.window:open(style)
end
end
---Close floating window
docs_view.close = function(self)
self.window:close()
self.entry = nil
end
docs_view.scroll = function(self, delta)
if self:visible() then
local info = vim.fn.getwininfo(self.window.win)[1] or {}
local top = info.topline or 1
top = top + delta
top = math.max(top, 1)
top = math.min(top, self.window:get_content_height() - info.height + 1)
vim.defer_fn(function()
vim.api.nvim_buf_call(self.window:get_buffer(), function()
vim.api.nvim_command('normal! ' .. top .. 'zt')
self.window:update()
end)
end, 0)
end
end
docs_view.visible = function(self)
return self.window:visible()
end
return docs_view

View File

@ -0,0 +1,127 @@
local config = require('cmp.config')
local misc = require('cmp.utils.misc')
local snippet = require('cmp.utils.snippet')
local str = require('cmp.utils.str')
local api = require('cmp.utils.api')
local types = require('cmp.types')
---@class cmp.GhostTextView
---@field win number|nil
---@field entry cmp.Entry|nil
local ghost_text_view = {}
ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT')
local has_inline = (function()
return (pcall(function()
local id = vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, 0, 0, {
virt_text = { { ' ', 'Comment' } },
virt_text_pos = 'inline',
hl_mode = 'combine',
ephemeral = false,
})
vim.api.nvim_buf_del_extmark(0, ghost_text_view.ns, id)
end))
end)()
ghost_text_view.new = function()
local self = setmetatable({}, { __index = ghost_text_view })
self.win = nil
self.entry = nil
self.extmark_id = nil
vim.api.nvim_set_decoration_provider(ghost_text_view.ns, {
on_win = function(_, win)
if self.extmark_id then
vim.api.nvim_buf_del_extmark(self.extmark_buf, ghost_text_view.ns, self.extmark_id)
self.extmark_id = nil
end
if win ~= self.win then
return false
end
local c = config.get().experimental.ghost_text
if not c then
return
end
if not self.entry then
return
end
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local line = vim.api.nvim_get_current_line()
if not has_inline then
if string.sub(line, col + 1) ~= '' then
return
end
end
local text = self.text_gen(self, line, col)
if #text > 0 then
self.extmark_buf = vim.api.nvim_get_current_buf()
self.extmark_id = vim.api.nvim_buf_set_extmark(self.extmark_buf, ghost_text_view.ns, row - 1, col, {
right_gravity = true,
virt_text = { { text, type(c) == 'table' and c.hl_group or 'Comment' } },
virt_text_pos = has_inline and 'inline' or 'overlay',
hl_mode = 'combine',
ephemeral = false,
})
end
end,
})
return self
end
---Generate the ghost text
--- This function calculates the bytes of the entry to display calculating the number
--- of character differences instead of just byte difference.
ghost_text_view.text_gen = function(self, line, cursor_col)
local word = self.entry:get_insert_text()
if self.entry:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = tostring(snippet.parse(word))
end
word = str.oneline(word)
local word_clen = vim.str_utfindex(word)
local cword = string.sub(line, self.entry:get_offset(), cursor_col)
local cword_clen = vim.str_utfindex(cword)
-- Number of characters from entry text (word) to be displayed as ghost thext
local nchars = word_clen - cword_clen
-- Missing characters to complete the entry text
local text
if nchars > 0 then
text = string.sub(word, vim.str_byteindex(word, word_clen - nchars) + 1)
else
text = ''
end
return text
end
---Show ghost text
---@param e cmp.Entry
ghost_text_view.show = function(self, e)
if not api.is_insert_mode() then
return
end
local c = config.get().experimental.ghost_text
if not c then
return
end
local changed = e ~= self.entry
self.win = vim.api.nvim_get_current_win()
self.entry = e
if changed then
misc.redraw(true) -- force invoke decoration provider.
end
end
ghost_text_view.hide = function(self)
if self.win and self.entry then
self.win = nil
self.entry = nil
misc.redraw(true) -- force invoke decoration provider.
end
end
return ghost_text_view

View File

@ -0,0 +1,180 @@
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

@ -0,0 +1,264 @@
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

View File

@ -0,0 +1,53 @@
local misc = require('cmp.utils.misc')
local vim_source = {}
---@param id integer
---@param args any[]
vim_source.on_callback = function(id, args)
if vim_source.to_callback.callbacks[id] then
vim_source.to_callback.callbacks[id](unpack(args))
end
end
---@param callback function
---@return integer
vim_source.to_callback = setmetatable({
callbacks = {},
}, {
__call = function(self, callback)
local id = misc.id('cmp.vim_source.to_callback')
self.callbacks[id] = function(...)
callback(...)
self.callbacks[id] = nil
end
return id
end,
})
---Convert to serializable args.
---@param args any[]
vim_source.to_args = function(args)
for i, arg in ipairs(args) do
if type(arg) == 'function' then
args[i] = vim_source.to_callback(arg)
end
end
return args
end
---@param bridge_id integer
---@param methods string[]
vim_source.new = function(bridge_id, methods)
local self = {}
for _, method in ipairs(methods) do
self[method] = (function(m)
return function(_, ...)
return vim.fn['cmp#_method'](bridge_id, m, vim_source.to_args({ ... }))
end
end)(method)
end
return self
end
return vim_source

View File

@ -0,0 +1 @@
/nix/store/mqbhz05llkddfb5wni0m48kw22ixxps4-lua-5.1.5

View File

@ -0,0 +1,263 @@
commands = {}
dependencies = {
["nvim-cmp"] = {
["scm-1"] = {
{
constraints = {
{
op = ">=",
version = {
5, 1, string = "5.1"
}
},
{
op = "<",
version = {
5, 4, string = "5.4"
}
}
},
name = "lua"
}
}
}
}
modules = {
["cmp.config"] = {
"nvim-cmp/scm-1"
},
["cmp.config.compare"] = {
"nvim-cmp/scm-1"
},
["cmp.config.context"] = {
"nvim-cmp/scm-1"
},
["cmp.config.default"] = {
"nvim-cmp/scm-1"
},
["cmp.config.mapping"] = {
"nvim-cmp/scm-1"
},
["cmp.config.sources"] = {
"nvim-cmp/scm-1"
},
["cmp.config.window"] = {
"nvim-cmp/scm-1"
},
["cmp.context"] = {
"nvim-cmp/scm-1"
},
["cmp.context_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.core"] = {
"nvim-cmp/scm-1"
},
["cmp.core_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.entry"] = {
"nvim-cmp/scm-1"
},
["cmp.entry_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.init"] = {
"nvim-cmp/scm-1"
},
["cmp.matcher"] = {
"nvim-cmp/scm-1"
},
["cmp.matcher_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.source"] = {
"nvim-cmp/scm-1"
},
["cmp.source_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.types.cmp"] = {
"nvim-cmp/scm-1"
},
["cmp.types.init"] = {
"nvim-cmp/scm-1"
},
["cmp.types.lsp"] = {
"nvim-cmp/scm-1"
},
["cmp.types.lsp_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.types.vim"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.api"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.api_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.async"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.async_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.autocmd"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.binary"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.binary_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.buffer"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.cache"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.char"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.debug"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.event"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.feedkeys"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.feedkeys_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.highlight"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.keymap"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.keymap_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.misc"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.misc_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.options"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.pattern"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.snippet"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.spec"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.str"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.str_spec"] = {
"nvim-cmp/scm-1"
},
["cmp.utils.window"] = {
"nvim-cmp/scm-1"
},
["cmp.view"] = {
"nvim-cmp/scm-1"
},
["cmp.view.custom_entries_view"] = {
"nvim-cmp/scm-1"
},
["cmp.view.docs_view"] = {
"nvim-cmp/scm-1"
},
["cmp.view.ghost_text_view"] = {
"nvim-cmp/scm-1"
},
["cmp.view.native_entries_view"] = {
"nvim-cmp/scm-1"
},
["cmp.view.wildmenu_entries_view"] = {
"nvim-cmp/scm-1"
},
["cmp.vim_source"] = {
"nvim-cmp/scm-1"
}
}
repository = {
["nvim-cmp"] = {
["scm-1"] = {
{
arch = "installed",
commands = {},
dependencies = {},
modules = {
["cmp.config"] = "cmp/config.lua",
["cmp.config.compare"] = "cmp/config/compare.lua",
["cmp.config.context"] = "cmp/config/context.lua",
["cmp.config.default"] = "cmp/config/default.lua",
["cmp.config.mapping"] = "cmp/config/mapping.lua",
["cmp.config.sources"] = "cmp/config/sources.lua",
["cmp.config.window"] = "cmp/config/window.lua",
["cmp.context"] = "cmp/context.lua",
["cmp.context_spec"] = "cmp/context_spec.lua",
["cmp.core"] = "cmp/core.lua",
["cmp.core_spec"] = "cmp/core_spec.lua",
["cmp.entry"] = "cmp/entry.lua",
["cmp.entry_spec"] = "cmp/entry_spec.lua",
["cmp.init"] = "cmp/init.lua",
["cmp.matcher"] = "cmp/matcher.lua",
["cmp.matcher_spec"] = "cmp/matcher_spec.lua",
["cmp.source"] = "cmp/source.lua",
["cmp.source_spec"] = "cmp/source_spec.lua",
["cmp.types.cmp"] = "cmp/types/cmp.lua",
["cmp.types.init"] = "cmp/types/init.lua",
["cmp.types.lsp"] = "cmp/types/lsp.lua",
["cmp.types.lsp_spec"] = "cmp/types/lsp_spec.lua",
["cmp.types.vim"] = "cmp/types/vim.lua",
["cmp.utils.api"] = "cmp/utils/api.lua",
["cmp.utils.api_spec"] = "cmp/utils/api_spec.lua",
["cmp.utils.async"] = "cmp/utils/async.lua",
["cmp.utils.async_spec"] = "cmp/utils/async_spec.lua",
["cmp.utils.autocmd"] = "cmp/utils/autocmd.lua",
["cmp.utils.binary"] = "cmp/utils/binary.lua",
["cmp.utils.binary_spec"] = "cmp/utils/binary_spec.lua",
["cmp.utils.buffer"] = "cmp/utils/buffer.lua",
["cmp.utils.cache"] = "cmp/utils/cache.lua",
["cmp.utils.char"] = "cmp/utils/char.lua",
["cmp.utils.debug"] = "cmp/utils/debug.lua",
["cmp.utils.event"] = "cmp/utils/event.lua",
["cmp.utils.feedkeys"] = "cmp/utils/feedkeys.lua",
["cmp.utils.feedkeys_spec"] = "cmp/utils/feedkeys_spec.lua",
["cmp.utils.highlight"] = "cmp/utils/highlight.lua",
["cmp.utils.keymap"] = "cmp/utils/keymap.lua",
["cmp.utils.keymap_spec"] = "cmp/utils/keymap_spec.lua",
["cmp.utils.misc"] = "cmp/utils/misc.lua",
["cmp.utils.misc_spec"] = "cmp/utils/misc_spec.lua",
["cmp.utils.options"] = "cmp/utils/options.lua",
["cmp.utils.pattern"] = "cmp/utils/pattern.lua",
["cmp.utils.snippet"] = "cmp/utils/snippet.lua",
["cmp.utils.spec"] = "cmp/utils/spec.lua",
["cmp.utils.str"] = "cmp/utils/str.lua",
["cmp.utils.str_spec"] = "cmp/utils/str_spec.lua",
["cmp.utils.window"] = "cmp/utils/window.lua",
["cmp.view"] = "cmp/view.lua",
["cmp.view.custom_entries_view"] = "cmp/view/custom_entries_view.lua",
["cmp.view.docs_view"] = "cmp/view/docs_view.lua",
["cmp.view.ghost_text_view"] = "cmp/view/ghost_text_view.lua",
["cmp.view.native_entries_view"] = "cmp/view/native_entries_view.lua",
["cmp.view.wildmenu_entries_view"] = "cmp/view/wildmenu_entries_view.lua",
["cmp.vim_source"] = "cmp/vim_source.lua"
}
}
}
}
}

View File

@ -0,0 +1,75 @@
let s:bridge_id = 0
let s:sources = {}
"
" cmp#register_source
"
function! cmp#register_source(name, source) abort
let l:methods = []
for l:method in [
\ 'is_available',
\ 'get_debug_name',
\ 'get_position_encoding_kind',
\ 'get_trigger_characters',
\ 'get_keyword_pattern',
\ 'complete',
\ 'execute',
\ 'resolve'
\ ]
if has_key(a:source, l:method) && type(a:source[l:method]) == v:t_func
call add(l:methods, l:method)
endif
endfor
let s:bridge_id += 1
let a:source.bridge_id = s:bridge_id
let a:source.id = luaeval('require("cmp").register_source(_A[1], require("cmp.vim_source").new(_A[2], _A[3]))', [a:name, s:bridge_id, l:methods])
let s:sources[s:bridge_id] = a:source
return a:source.id
endfunction
"
" cmp#unregister_source
"
function! cmp#unregister_source(id) abort
if has_key(s:sources, a:id)
unlet s:sources[a:id]
endif
call luaeval('require("cmp").unregister_source(_A)', a:id)
endfunction
"
" cmp#_method
"
function! cmp#_method(bridge_id, method, args) abort
try
let l:source = s:sources[a:bridge_id]
if a:method ==# 'is_available'
return l:source[a:method]()
elseif a:method ==# 'get_debug_name'
return l:source[a:method]()
elseif a:method ==# 'get_position_encoding_kind'
return l:source[a:method](a:args[0])
elseif a:method ==# 'get_keyword_pattern'
return l:source[a:method](a:args[0])
elseif a:method ==# 'get_trigger_characters'
return l:source[a:method](a:args[0])
elseif a:method ==# 'complete'
return l:source[a:method](a:args[0], s:callback(a:args[1]))
elseif a:method ==# 'resolve'
return l:source[a:method](a:args[0], s:callback(a:args[1]))
elseif a:method ==# 'execute'
return l:source[a:method](a:args[0], s:callback(a:args[1]))
endif
catch /.*/
echomsg string({ 'exception': v:exception, 'throwpoint': v:throwpoint })
endtry
return v:null
endfunction
"
" s:callback
"
function! s:callback(id) abort
return { ... -> luaeval('require("cmp.vim_source").on_callback(_A[1], _A[2])', [a:id, a:000]) }
endfunction

View File

@ -0,0 +1,31 @@
local MODREV, SPECREV = 'scm', '-1'
rockspec_format = '3.0'
package = 'nvim-cmp'
version = MODREV .. SPECREV
description = {
summary = 'A completion plugin for neovim',
labels = { 'neovim' },
detailed = [[
A completion engine plugin for neovim written in Lua. Completion sources are installed from external repositories and "sourced".
]],
homepage = 'https://github.com/hrsh7th/nvim-cmp',
license = 'MIT',
}
dependencies = {
'lua >= 5.1, < 5.4',
}
source = {
url = 'git://github.com/hrsh7th/nvim-cmp',
}
build = {
type = 'builtin',
copy_directories = {
'autoload',
'plugin',
'doc'
}
}

View File

@ -0,0 +1,61 @@
if vim.g.loaded_cmp then
return
end
vim.g.loaded_cmp = true
if not vim.api.nvim_create_autocmd then
return print('[nvim-cmp] Your nvim does not has `nvim_create_autocmd` function. Please update to latest nvim.')
end
local api = require('cmp.utils.api')
local types = require('cmp.types')
local highlight = require('cmp.utils.highlight')
local autocmd = require('cmp.utils.autocmd')
vim.api.nvim_set_hl(0, 'CmpItemAbbr', { link = 'CmpItemAbbrDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemAbbrDeprecated', { link = 'CmpItemAbbrDeprecatedDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemAbbrMatch', { link = 'CmpItemAbbrMatchDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemAbbrMatchFuzzy', { link = 'CmpItemAbbrMatchFuzzyDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemKind', { link = 'CmpItemKindDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemMenu', { link = 'CmpItemMenuDefault', default = true })
for kind in pairs(types.lsp.CompletionItemKind) do
if type(kind) == 'string' then
local name = ('CmpItemKind%s'):format(kind)
vim.api.nvim_set_hl(0, name, { link = ('%sDefault'):format(name), default = true })
end
end
autocmd.subscribe({ 'ColorScheme', 'UIEnter' }, function()
highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { bg = 'NONE', default = false })
highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { bg = 'NONE', default = false })
highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { bg = 'NONE', default = false })
highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { bg = 'NONE', default = false })
highlight.inherit('CmpItemKindDefault', 'Special', { bg = 'NONE', default = false })
highlight.inherit('CmpItemMenuDefault', 'Pmenu', { bg = 'NONE', default = false })
for name in pairs(types.lsp.CompletionItemKind) do
if type(name) == 'string' then
vim.api.nvim_set_hl(0, ('CmpItemKind%sDefault'):format(name), { link = 'CmpItemKind', default = false })
end
end
end)
autocmd.emit('ColorScheme')
if vim.on_key then
local control_c_termcode = vim.api.nvim_replace_termcodes('<C-c>', true, true, true)
vim.on_key(function(keys)
if keys == control_c_termcode then
vim.schedule(function()
if not api.is_suitable_mode() then
autocmd.emit('InsertLeave')
end
end)
end
end, vim.api.nvim_create_namespace('cmp.plugin'))
end
vim.api.nvim_create_user_command('CmpStatus', function()
require('cmp').status()
end, { desc = 'Check status of cmp sources' })
vim.cmd([[doautocmd <nomodeline> User CmpReady]])

View File

@ -0,0 +1,80 @@
rock_manifest = {
autoload = {
["cmp.vim"] = "3486f63ca3381e0b6bf094569f028665"
},
doc = {
["cmp.txt"] = "0ed9ef42d5a72bfd6f805b64cd00276a"
},
lua = {
cmp = {
config = {
["compare.lua"] = "a3570d9e528aba104a6ba6bd7f510da8",
["context.lua"] = "28e9f4e9575f6183d1d05bbee4c4408c",
["default.lua"] = "2f2648b4528925dc6a98f0fa4d93c6fc",
["mapping.lua"] = "39b88ca1fa06ced8522ec0ee27cb24f2",
["sources.lua"] = "81bb51dbe8997bfabd46541a394b4b41",
["window.lua"] = "ae25fe313122f0e90b651c5e9d7dfd2e"
},
["config.lua"] = "3a196d30b4572b8935ea4e879f900f59",
["context.lua"] = "aa01e370d568a387e98389103e3e01df",
["context_spec.lua"] = "d5b2fb143dd2efaee3f71bfe1bdc31a9",
["core.lua"] = "0d7de2518b5be40093c777416bb36af1",
["core_spec.lua"] = "edbefcd3d75e48e9d7bc4326f100a864",
["entry.lua"] = "6bc6aba58cea17879cd2ccdffee61631",
["entry_spec.lua"] = "ca2aff02436ad84ca62d3df331173f01",
["init.lua"] = "dd38eea91cabba39091ce75117f3dda7",
["matcher.lua"] = "55af0d4c7ce03827a3dbda8715a1b472",
["matcher_spec.lua"] = "59da9197d47615ada5bf93011766356c",
["source.lua"] = "60cc28eea79e72914142fe5dc5ad95d1",
["source_spec.lua"] = "2b54677be2c92d7c3984783140b7018c",
types = {
["cmp.lua"] = "174d92640f57fb2f3c0f2c62eb5f8654",
["init.lua"] = "d372a58d9966fbc24e7ccd3d6c30a834",
["lsp.lua"] = "401502e8b85568aee57584948953feb5",
["lsp_spec.lua"] = "72e9da2b231e4716ac39d0759c387b44",
["vim.lua"] = "a6796b027eb76f5d1cce7fa8172eabea"
},
utils = {
["api.lua"] = "53067718473650f4d109780a1a7bcdc6",
["api_spec.lua"] = "89fe537ce824cad191576272d8cdc47a",
["async.lua"] = "2b968476962ba1c22a86a5392b62da9f",
["async_spec.lua"] = "679338ab70a4c1ea72d7173e77143b68",
["autocmd.lua"] = "322672f88beaeec084dbcd4086a78b8c",
["binary.lua"] = "385a0844080b9d281a2ea0a254e09ddd",
["binary_spec.lua"] = "cb428e4eded68b6bf27c2dd778cba44c",
["buffer.lua"] = "0d72e16d90fecb8dbed6843d4ce46553",
["cache.lua"] = "622af76c4c7bf212b9fd6f2c6c832ddd",
["char.lua"] = "723a8066d2e67d40034ffce692250a58",
["debug.lua"] = "c87003d9480670c22d73a1ba670e8c89",
["event.lua"] = "38b59f97402a0c71ac21fb1408536c0d",
["feedkeys.lua"] = "abbd5d1828baec177ffbc9a1c47ce7c5",
["feedkeys_spec.lua"] = "76b9acca3b02ef4163ae070b9fd78ed9",
["highlight.lua"] = "f9bf48596410a2105e7e1f9214ff3a33",
["keymap.lua"] = "791262dc36492c6daa8a485c8c855f28",
["keymap_spec.lua"] = "6cd866f56f97a1d5ed70fa8dd20c6d53",
["misc.lua"] = "4a5ef5ee10bf1dd5ada4b4cd75c840c1",
["misc_spec.lua"] = "691adf08c16f1312976594986340dab4",
["options.lua"] = "b076c4f8f9c915f6334944ec9749fdb0",
["pattern.lua"] = "e77acfc76798494a14c6e62472a2ef7b",
["snippet.lua"] = "de00ee60d79f76eb7d42defb969e1eb8",
["spec.lua"] = "484dcd49b1a4cfb8fc195f16b2dc38c1",
["str.lua"] = "075a6cbb4683b4b2bd52ceeb3af96755",
["str_spec.lua"] = "5b17025edaa3a49892951521da0bc9c4",
["window.lua"] = "8b90f3cbc1992529e7326cc5ec109b47"
},
view = {
["custom_entries_view.lua"] = "7011cd4c8a63f645c1e8bab3ca30d293",
["docs_view.lua"] = "527e58b879a27b67382048808b1c599b",
["ghost_text_view.lua"] = "7cc83940d6b631caf1e9643faf647a0a",
["native_entries_view.lua"] = "fbf5f842464ee3e399f3c7825bb2604b",
["wildmenu_entries_view.lua"] = "002776dbd93c238824d042a1a1ef41b6"
},
["view.lua"] = "4604db1f3975e50e724fa14b76ffc932",
["vim_source.lua"] = "205a75409f6ae2debad0bf96e7041466"
}
},
["nvim-cmp-scm-1.rockspec"] = "92079ac860d2bbb56eb8dc343423d1ac",
plugin = {
["cmp.lua"] = "070df415c09de0e23bb521a820f1614d"
}
}

View File

@ -0,0 +1,31 @@
local MODREV, SPECREV = 'scm', '-1'
rockspec_format = '3.0'
package = 'nvim-cmp'
version = MODREV .. SPECREV
description = {
summary = 'A completion plugin for neovim',
labels = { 'neovim' },
detailed = [[
A completion engine plugin for neovim written in Lua. Completion sources are installed from external repositories and "sourced".
]],
homepage = 'https://github.com/hrsh7th/nvim-cmp',
license = 'MIT',
}
dependencies = {
'lua >= 5.1, < 5.4',
}
source = {
url = 'git://github.com/hrsh7th/nvim-cmp',
}
build = {
type = 'builtin',
copy_directories = {
'autoload',
'plugin',
'doc'
}
}

View File

@ -0,0 +1,61 @@
if vim.g.loaded_cmp then
return
end
vim.g.loaded_cmp = true
if not vim.api.nvim_create_autocmd then
return print('[nvim-cmp] Your nvim does not has `nvim_create_autocmd` function. Please update to latest nvim.')
end
local api = require('cmp.utils.api')
local types = require('cmp.types')
local highlight = require('cmp.utils.highlight')
local autocmd = require('cmp.utils.autocmd')
vim.api.nvim_set_hl(0, 'CmpItemAbbr', { link = 'CmpItemAbbrDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemAbbrDeprecated', { link = 'CmpItemAbbrDeprecatedDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemAbbrMatch', { link = 'CmpItemAbbrMatchDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemAbbrMatchFuzzy', { link = 'CmpItemAbbrMatchFuzzyDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemKind', { link = 'CmpItemKindDefault', default = true })
vim.api.nvim_set_hl(0, 'CmpItemMenu', { link = 'CmpItemMenuDefault', default = true })
for kind in pairs(types.lsp.CompletionItemKind) do
if type(kind) == 'string' then
local name = ('CmpItemKind%s'):format(kind)
vim.api.nvim_set_hl(0, name, { link = ('%sDefault'):format(name), default = true })
end
end
autocmd.subscribe({ 'ColorScheme', 'UIEnter' }, function()
highlight.inherit('CmpItemAbbrDefault', 'Pmenu', { bg = 'NONE', default = false })
highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', { bg = 'NONE', default = false })
highlight.inherit('CmpItemAbbrMatchDefault', 'Pmenu', { bg = 'NONE', default = false })
highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Pmenu', { bg = 'NONE', default = false })
highlight.inherit('CmpItemKindDefault', 'Special', { bg = 'NONE', default = false })
highlight.inherit('CmpItemMenuDefault', 'Pmenu', { bg = 'NONE', default = false })
for name in pairs(types.lsp.CompletionItemKind) do
if type(name) == 'string' then
vim.api.nvim_set_hl(0, ('CmpItemKind%sDefault'):format(name), { link = 'CmpItemKind', default = false })
end
end
end)
autocmd.emit('ColorScheme')
if vim.on_key then
local control_c_termcode = vim.api.nvim_replace_termcodes('<C-c>', true, true, true)
vim.on_key(function(keys)
if keys == control_c_termcode then
vim.schedule(function()
if not api.is_suitable_mode() then
autocmd.emit('InsertLeave')
end
end)
end
end, vim.api.nvim_create_namespace('cmp.plugin'))
end
vim.api.nvim_create_user_command('CmpStatus', function()
require('cmp').status()
end, { desc = 'Check status of cmp sources' })
vim.cmd([[doautocmd <nomodeline> User CmpReady]])

View File

@ -0,0 +1,80 @@
rock_manifest = {
autoload = {
["cmp.vim"] = "3486f63ca3381e0b6bf094569f028665"
},
doc = {
["cmp.txt"] = "0ed9ef42d5a72bfd6f805b64cd00276a"
},
lua = {
cmp = {
config = {
["compare.lua"] = "a3570d9e528aba104a6ba6bd7f510da8",
["context.lua"] = "28e9f4e9575f6183d1d05bbee4c4408c",
["default.lua"] = "2f2648b4528925dc6a98f0fa4d93c6fc",
["mapping.lua"] = "39b88ca1fa06ced8522ec0ee27cb24f2",
["sources.lua"] = "81bb51dbe8997bfabd46541a394b4b41",
["window.lua"] = "ae25fe313122f0e90b651c5e9d7dfd2e"
},
["config.lua"] = "3a196d30b4572b8935ea4e879f900f59",
["context.lua"] = "aa01e370d568a387e98389103e3e01df",
["context_spec.lua"] = "d5b2fb143dd2efaee3f71bfe1bdc31a9",
["core.lua"] = "0d7de2518b5be40093c777416bb36af1",
["core_spec.lua"] = "edbefcd3d75e48e9d7bc4326f100a864",
["entry.lua"] = "6bc6aba58cea17879cd2ccdffee61631",
["entry_spec.lua"] = "ca2aff02436ad84ca62d3df331173f01",
["init.lua"] = "dd38eea91cabba39091ce75117f3dda7",
["matcher.lua"] = "55af0d4c7ce03827a3dbda8715a1b472",
["matcher_spec.lua"] = "59da9197d47615ada5bf93011766356c",
["source.lua"] = "60cc28eea79e72914142fe5dc5ad95d1",
["source_spec.lua"] = "2b54677be2c92d7c3984783140b7018c",
types = {
["cmp.lua"] = "174d92640f57fb2f3c0f2c62eb5f8654",
["init.lua"] = "d372a58d9966fbc24e7ccd3d6c30a834",
["lsp.lua"] = "401502e8b85568aee57584948953feb5",
["lsp_spec.lua"] = "72e9da2b231e4716ac39d0759c387b44",
["vim.lua"] = "a6796b027eb76f5d1cce7fa8172eabea"
},
utils = {
["api.lua"] = "53067718473650f4d109780a1a7bcdc6",
["api_spec.lua"] = "89fe537ce824cad191576272d8cdc47a",
["async.lua"] = "2b968476962ba1c22a86a5392b62da9f",
["async_spec.lua"] = "679338ab70a4c1ea72d7173e77143b68",
["autocmd.lua"] = "322672f88beaeec084dbcd4086a78b8c",
["binary.lua"] = "385a0844080b9d281a2ea0a254e09ddd",
["binary_spec.lua"] = "cb428e4eded68b6bf27c2dd778cba44c",
["buffer.lua"] = "0d72e16d90fecb8dbed6843d4ce46553",
["cache.lua"] = "622af76c4c7bf212b9fd6f2c6c832ddd",
["char.lua"] = "723a8066d2e67d40034ffce692250a58",
["debug.lua"] = "c87003d9480670c22d73a1ba670e8c89",
["event.lua"] = "38b59f97402a0c71ac21fb1408536c0d",
["feedkeys.lua"] = "abbd5d1828baec177ffbc9a1c47ce7c5",
["feedkeys_spec.lua"] = "76b9acca3b02ef4163ae070b9fd78ed9",
["highlight.lua"] = "f9bf48596410a2105e7e1f9214ff3a33",
["keymap.lua"] = "791262dc36492c6daa8a485c8c855f28",
["keymap_spec.lua"] = "6cd866f56f97a1d5ed70fa8dd20c6d53",
["misc.lua"] = "4a5ef5ee10bf1dd5ada4b4cd75c840c1",
["misc_spec.lua"] = "691adf08c16f1312976594986340dab4",
["options.lua"] = "b076c4f8f9c915f6334944ec9749fdb0",
["pattern.lua"] = "e77acfc76798494a14c6e62472a2ef7b",
["snippet.lua"] = "de00ee60d79f76eb7d42defb969e1eb8",
["spec.lua"] = "484dcd49b1a4cfb8fc195f16b2dc38c1",
["str.lua"] = "075a6cbb4683b4b2bd52ceeb3af96755",
["str_spec.lua"] = "5b17025edaa3a49892951521da0bc9c4",
["window.lua"] = "8b90f3cbc1992529e7326cc5ec109b47"
},
view = {
["custom_entries_view.lua"] = "7011cd4c8a63f645c1e8bab3ca30d293",
["docs_view.lua"] = "527e58b879a27b67382048808b1c599b",
["ghost_text_view.lua"] = "7cc83940d6b631caf1e9643faf647a0a",
["native_entries_view.lua"] = "fbf5f842464ee3e399f3c7825bb2604b",
["wildmenu_entries_view.lua"] = "002776dbd93c238824d042a1a1ef41b6"
},
["view.lua"] = "4604db1f3975e50e724fa14b76ffc932",
["vim_source.lua"] = "205a75409f6ae2debad0bf96e7041466"
}
},
["nvim-cmp-scm-1.rockspec"] = "92079ac860d2bbb56eb8dc343423d1ac",
plugin = {
["cmp.lua"] = "070df415c09de0e23bb521a820f1614d"
}
}