summaryrefslogtreecommitdiff
path: root/start/cmp/lua/cmp
diff options
context:
space:
mode:
Diffstat (limited to 'start/cmp/lua/cmp')
-rw-r--r--start/cmp/lua/cmp/config.lua229
-rw-r--r--start/cmp/lua/cmp/config/compare.lua234
-rw-r--r--start/cmp/lua/cmp/config/context.lua65
-rw-r--r--start/cmp/lua/cmp/config/default.lua97
-rw-r--r--start/cmp/lua/cmp/config/mapping.lua172
-rw-r--r--start/cmp/lua/cmp/config/sources.lua10
-rw-r--r--start/cmp/lua/cmp/config/window.lua12
-rw-r--r--start/cmp/lua/cmp/context.lua105
-rw-r--r--start/cmp/lua/cmp/context_spec.lua31
-rw-r--r--start/cmp/lua/cmp/core.lua486
-rw-r--r--start/cmp/lua/cmp/core_spec.lua158
-rw-r--r--start/cmp/lua/cmp/entry.lua468
-rw-r--r--start/cmp/lua/cmp/entry_spec.lua342
-rw-r--r--start/cmp/lua/cmp/init.lua336
-rw-r--r--start/cmp/lua/cmp/matcher.lua324
-rw-r--r--start/cmp/lua/cmp/matcher_spec.lua64
-rw-r--r--start/cmp/lua/cmp/source.lua365
-rw-r--r--start/cmp/lua/cmp/source_spec.lua109
-rw-r--r--start/cmp/lua/cmp/types/cmp.lua166
-rw-r--r--start/cmp/lua/cmp/types/init.lua7
-rw-r--r--start/cmp/lua/cmp/types/lsp.lua197
-rw-r--r--start/cmp/lua/cmp/types/lsp_spec.lua47
-rw-r--r--start/cmp/lua/cmp/types/vim.lua20
-rw-r--r--start/cmp/lua/cmp/utils/api.lua69
-rw-r--r--start/cmp/lua/cmp/utils/api_spec.lua46
-rw-r--r--start/cmp/lua/cmp/utils/async.lua127
-rw-r--r--start/cmp/lua/cmp/utils/async_spec.lua69
-rw-r--r--start/cmp/lua/cmp/utils/autocmd.lua53
-rw-r--r--start/cmp/lua/cmp/utils/binary.lua33
-rw-r--r--start/cmp/lua/cmp/utils/binary_spec.lua28
-rw-r--r--start/cmp/lua/cmp/utils/buffer.lua28
-rw-r--r--start/cmp/lua/cmp/utils/cache.lua58
-rw-r--r--start/cmp/lua/cmp/utils/char.lua117
-rw-r--r--start/cmp/lua/cmp/utils/debug.lua20
-rw-r--r--start/cmp/lua/cmp/utils/event.lua51
-rw-r--r--start/cmp/lua/cmp/utils/feedkeys.lua53
-rw-r--r--start/cmp/lua/cmp/utils/feedkeys_spec.lua56
-rw-r--r--start/cmp/lua/cmp/utils/highlight.lua31
-rw-r--r--start/cmp/lua/cmp/utils/keymap.lua251
-rw-r--r--start/cmp/lua/cmp/utils/keymap_spec.lua187
-rw-r--r--start/cmp/lua/cmp/utils/misc.lua253
-rw-r--r--start/cmp/lua/cmp/utils/misc_spec.lua63
-rw-r--r--start/cmp/lua/cmp/utils/pattern.lua28
-rw-r--r--start/cmp/lua/cmp/utils/spec.lua92
-rw-r--r--start/cmp/lua/cmp/utils/str.lua178
-rw-r--r--start/cmp/lua/cmp/utils/str_spec.lua29
-rw-r--r--start/cmp/lua/cmp/utils/window.lua313
-rw-r--r--start/cmp/lua/cmp/view.lua243
-rw-r--r--start/cmp/lua/cmp/view/custom_entries_view.lua409
-rw-r--r--start/cmp/lua/cmp/view/docs_view.lua136
-rw-r--r--start/cmp/lua/cmp/view/ghost_text_view.lua97
-rw-r--r--start/cmp/lua/cmp/view/native_entries_view.lua181
-rw-r--r--start/cmp/lua/cmp/view/wildmenu_entries_view.lua261
-rw-r--r--start/cmp/lua/cmp/vim_source.lua53
54 files changed, 7657 insertions, 0 deletions
diff --git a/start/cmp/lua/cmp/config.lua b/start/cmp/lua/cmp/config.lua
new file mode 100644
index 0000000..42e9a43
--- /dev/null
+++ b/start/cmp/lua/cmp/config.lua
@@ -0,0 +1,229 @@
+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<number, 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 number|nil
+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 cmdtype string
+config.set_cmdline = function(c, cmdtype)
+ local revision = (config.cmdline[cmdtype] or {}).revision or 1
+ config.cmdline[cmdtype] = c or {}
+ config.cmdline[cmdtype].revision = revision + 1
+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
+ if config.onetime.sources 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_buf_get_option(bufnr, 'filetype')
+ 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.experimental and c.experimental.native_menu then
+ return true
+ end
+ 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'
+ 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
diff --git a/start/cmp/lua/cmp/config/compare.lua b/start/cmp/lua/cmp/config/compare.lua
new file mode 100644
index 0000000..4b4fadc
--- /dev/null
+++ b/start/cmp/lua/cmp/config/compare.lua
@@ -0,0 +1,234 @@
+local types = require('cmp.types')
+local cache = require('cmp.utils.cache')
+local misc = require('cmp.utils.misc')
+
+local compare = {}
+
+-- offset
+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
+end
+
+-- exact
+compare.exact = function(entry1, entry2)
+ if entry1.exact ~= entry2.exact then
+ return entry1.exact
+ end
+end
+
+-- score
+compare.score = function(entry1, entry2)
+ local diff = entry2.score - entry1.score
+ if diff < 0 then
+ return true
+ elseif diff > 0 then
+ return false
+ end
+end
+
+-- recently_used
+compare.recently_used = setmetatable({
+ records = {},
+ add_entry = function(self, e)
+ self.records[e.completion_item.label] = vim.loop.now()
+ end,
+}, {
+ __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
+ end,
+})
+
+-- kind
+compare.kind = function(entry1, entry2)
+ local kind1 = entry1:get_kind()
+ kind1 = kind1 == types.lsp.CompletionItemKind.Text and 100 or kind1
+ local kind2 = entry2:get_kind()
+ 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
+end
+
+-- sortText
+compare.sort_text = function(entry1, entry2)
+ if misc.safe(entry1.completion_item.sortText) and misc.safe(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
+end
+
+-- length
+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
+end
+
+-- order
+compare.order = function(entry1, entry2)
+ local diff = entry1.id - entry2.id
+ if diff < 0 then
+ return true
+ elseif diff > 0 then
+ return false
+ end
+end
+
+-- locality
+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.scopes) 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 0.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,
+}, {
+ __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
+ end,
+})
+
+-- scopes
+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 text = vim.treesitter.query.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,
+}, {
+ __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
diff --git a/start/cmp/lua/cmp/config/context.lua b/start/cmp/lua/cmp/config/context.lua
new file mode 100644
index 0000000..584f38a
--- /dev/null
+++ b/start/cmp/lua/cmp/config/context.lua
@@ -0,0 +1,65 @@
+local context = {}
+
+---Check if cursor is in syntax group
+---@param group string
+---@return boolean
+context.in_syntax_group = function(group)
+ local lnum, col = vim.fn.line('.'), math.min(vim.fn.col('.'), #vim.fn.getline('.'))
+ for _, syn_id in ipairs(vim.fn.synstack(lnum, col)) do
+ syn_id = vim.fn.synIDtrans(syn_id) -- Resolve :highlight links
+ if vim.fn.synIDattr(syn_id, 'name') == group then
+ return true
+ end
+ end
+ return false
+end
+
+---Check if cursor is in treesitter capture
+---@param capture string
+---@return boolean
+context.in_treesitter_capture = function(capture)
+ local highlighter = require('vim.treesitter.highlighter')
+ local ts_utils = require('nvim-treesitter.ts_utils')
+ 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 self = highlighter.active[buf]
+ if not self then
+ return false
+ end
+
+ local node_types = {}
+
+ self.tree:for_each_tree(function(tstree, tree)
+ if not tstree then
+ return
+ end
+
+ local root = tstree:root()
+ local root_start_row, _, root_end_row, _ = root:range()
+ if root_start_row > row or root_end_row < row then
+ return
+ end
+
+ local query = self:get_query(tree:lang())
+ if not query:query() then
+ return
+ end
+
+ local iter = query:query():iter_captures(root, self.bufnr, row, row + 1)
+ for _, node, _ in iter do
+ if ts_utils.is_in_node_range(node, row, col) then
+ table.insert(node_types, node:type())
+ end
+ end
+ end, true)
+
+ return vim.tbl_contains(node_types, capture)
+end
+
+return context
diff --git a/start/cmp/lua/cmp/config/default.lua b/start/cmp/lua/cmp/config/default.lua
new file mode 100644
index 0000000..dce5168
--- /dev/null
+++ b/start/cmp/lua/cmp/config/default.lua
@@ -0,0 +1,97 @@
+local compare = require('cmp.config.compare')
+local types = require('cmp.types')
+
+local WIDE_HEIGHT = 40
+
+---@return cmp.ConfigSchema
+return function()
+ return {
+ enabled = function()
+ local disabled = false
+ disabled = disabled or (vim.api.nvim_buf_get_option(0, 'buftype') == 'prompt')
+ disabled = disabled or (vim.fn.reg_recording() ~= '')
+ disabled = disabled or (vim.fn.reg_executing() ~= '')
+ return not disabled
+ end,
+
+ preselect = types.cmp.PreselectMode.Item,
+
+ mapping = {},
+
+ snippet = {
+ expand = 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 = {
+ fields = { 'abbr', 'kind', 'menu' },
+ format = function(_, vim_item)
+ return vim_item
+ end,
+ },
+
+ matching = {
+ disallow_fuzzy_matching = false,
+ disallow_partial_matching = false,
+ disallow_prefix_unmatching = false,
+ },
+
+ 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' },
+ },
+
+ window = {
+ completion = {
+ border = { '', '', '', '', '', '', '', '' },
+ winhighlight = 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None',
+ },
+ 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',
+ },
+ },
+ }
+end
diff --git a/start/cmp/lua/cmp/config/mapping.lua b/start/cmp/lua/cmp/config/mapping.lua
new file mode 100644
index 0000000..c2028c8
--- /dev/null
+++ b/start/cmp/lua/cmp/config/mapping.lua
@@ -0,0 +1,172 @@
+local types = require('cmp.types')
+local misc = require('cmp.utils.misc')
+local feedkeys = require('cmp.utils.feedkeys')
+local keymap = require('cmp.utils.keymap')
+
+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 misc.merge(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 = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Insert }),
+ },
+ ['<C-p>'] = {
+ i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert }),
+ },
+ ['<C-y>'] = {
+ i = mapping.confirm({ select = false }),
+ },
+ ['<C-e>'] = {
+ i = mapping.abort(),
+ },
+ })
+end
+
+---Mapping preset cmdline-mode configuration.
+mapping.preset.cmdline = function(override)
+ return misc.merge(override or {}, {
+ ['<Tab>'] = {
+ c = function()
+ local cmp = require('cmp')
+ if cmp.visible() then
+ cmp.select_next_item()
+ else
+ feedkeys.call(keymap.t('<C-z>'), 'n')
+ end
+ end,
+ },
+ ['<S-Tab>'] = {
+ c = function()
+ local cmp = require('cmp')
+ if cmp.visible() then
+ cmp.select_prev_item()
+ else
+ feedkeys.call(keymap.t('<C-z>'), 'n')
+ 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.close(),
+ },
+ })
+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
+
+---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
diff --git a/start/cmp/lua/cmp/config/sources.lua b/start/cmp/lua/cmp/config/sources.lua
new file mode 100644
index 0000000..cfb09c0
--- /dev/null
+++ b/start/cmp/lua/cmp/config/sources.lua
@@ -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
diff --git a/start/cmp/lua/cmp/config/window.lua b/start/cmp/lua/cmp/config/window.lua
new file mode 100644
index 0000000..b6fec0f
--- /dev/null
+++ b/start/cmp/lua/cmp/config/window.lua
@@ -0,0 +1,12 @@
+local window = {}
+
+window.bordered = function(opts)
+ opts = opts or {}
+ return {
+ border = opts.border or 'rounded',
+ winhighlight = opts.winhighlight or 'Normal:Normal,FloatBorder:Normal,CursorLine:Visual,Search:None',
+ zindex = opts.zindex or 1001,
+ }
+end
+
+return window
diff --git a/start/cmp/lua/cmp/context.lua b/start/cmp/lua/cmp/context.lua
new file mode 100644
index 0000000..6188259
--- /dev/null
+++ b/start/cmp/lua/cmp/context.lua
@@ -0,0 +1,105 @@
+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 number
+---@field public bufnr number
+---@field public cursor vim.Position|lsp.Position
+---@field public cursor_line string
+---@field public cursor_after_line string
+---@field public cursor_before_line string
+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_buf_get_option(0, 'filetype')
+ 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)
+ return self
+end
+
+---Return context creation reason.
+---@return cmp.ContextReason
+context.get_reason = function(self)
+ return self.option.reason
+end
+
+---Get keyword pattern offset
+---@return number|nil
+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
diff --git a/start/cmp/lua/cmp/context_spec.lua b/start/cmp/lua/cmp/context_spec.lua
new file mode 100644
index 0000000..976e194
--- /dev/null
+++ b/start/cmp/lua/cmp/context_spec.lua
@@ -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)
diff --git a/start/cmp/lua/cmp/core.lua b/start/cmp/lua/cmp/core.lua
new file mode 100644
index 0000000..08949a9
--- /dev/null
+++ b/start/cmp/lua/cmp/core.lua
@@ -0,0 +1,486 @@
+local debug = require('cmp.utils.debug')
+local str = require('cmp.utils.str')
+local char = require('cmp.utils.char')
+local pattern = require('cmp.utils.pattern')
+local feedkeys = require('cmp.utils.feedkeys')
+local async = require('cmp.utils.async')
+local keymap = require('cmp.utils.keymap')
+local context = require('cmp.context')
+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')
+
+local SOURCE_TIMEOUT = 500
+local DEBOUNCE_TIME = 80
+local THROTTLE_TIME = 40
+
+---@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)
+ self.view.event:on('complete_done', function(evt)
+ self.event:emit('complete_done', evt)
+ 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 string
+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)
+ local prev = self.context:clone()
+ prev.prev_context = 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 THROTTLE_TIME 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
+
+---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 = pattern.matchstr('[^[:blank:]]\\+$', 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_active_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()
+ 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
+ if common_string and #common_string > (1 + cursor[2] - offset) then
+ feedkeys.call(keymap.backspace(string.sub(api.get_current_line(), offset, cursor[2])) .. 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 = DEBOUNCE_TIME
+ 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 THROTTLE_TIME or 1
+ self:filter()
+ end
+end
+
+---Update completion menu
+core.filter = async.throttle(function(self)
+ self.filter.timeout = THROTTLE_TIME
+
+ -- 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 SOURCE_TIMEOUT > s:get_fetching_time() then
+ self.filter.timeout = SOURCE_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.
+ self.view:open(ctx, sources)
+
+ -- Check onetime config.
+ if #self:get_sources(function(s)
+ if s.status == source.SourceStatus.FETCHING then
+ return true
+ elseif #s:get_entries(ctx) > 0 then
+ return true
+ end
+ return false
+ end) == 0 then
+ config.set_onetime({})
+ end
+end, THROTTLE_TIME)
+
+---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
+ return callback()
+ end
+ e.confirmed = true
+
+ debug.log('entry.confirm', e:get_completion_item())
+
+ local release = self:suspend()
+
+ -- Close menus.
+ self.view:close()
+
+ feedkeys.call(keymap.indentkeys(), 'n')
+ feedkeys.call('', 'n', function()
+ local ctx = context.new()
+ local keys = {}
+ table.insert(keys, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, 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()
+ local ctx = context.new()
+ if api.is_cmdline_mode() then
+ local keys = {}
+ table.insert(keys, keymap.backspace(ctx.cursor.character - misc.to_utfindex(ctx.cursor_line, e:get_offset())))
+ table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset()))
+ feedkeys.call(table.concat(keys, ''), 'in')
+ else
+ vim.api.nvim_buf_set_text(0, ctx.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, {
+ string.sub(e.context.cursor_before_line, 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()
+ local ctx = context.new()
+ if #(misc.safe(e:get_completion_item().additionalTextEdits) or {}) == 0 then
+ e:resolve(function()
+ local new = context.new()
+ local text_edits = misc.safe(e:get_completion_item().additionalTextEdits) or {}
+ if #text_edits == 0 then
+ return
+ end
+
+ local has_cursor_line_text_edit = (function()
+ local minrow = math.min(ctx.cursor.row, new.cursor.row)
+ local maxrow = math.max(ctx.cursor.row, new.cursor.row)
+ for _, te in ipairs(text_edits) do
+ local srow = te.range.start.line + 1
+ local erow = te.range['end'].line + 1
+ if srow <= minrow and maxrow <= erow then
+ return true
+ end
+ end
+ return false
+ end)()
+ if has_cursor_line_text_edit then
+ return
+ end
+ vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, 'utf-16')
+ end)
+ else
+ vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, 'utf-16')
+ end
+ end)
+ feedkeys.call('', 'n', function()
+ local ctx = context.new()
+ local completion_item = misc.copy(e:get_completion_item())
+ if not misc.safe(completion_item.textEdit) then
+ completion_item.textEdit = {}
+ completion_item.textEdit.newText = misc.safe(completion_item.insertText) or completion_item.word or completion_item.label
+ end
+ local behavior = option.behavior or config.get().confirmation.default_behavior
+ if behavior == types.cmp.ConfirmBehavior.Replace then
+ completion_item.textEdit.range = e:get_replace_range()
+ else
+ completion_item.textEdit.range = e:get_insert_range()
+ end
+
+ local diff_before = math.max(0, e.context.cursor.character - completion_item.textEdit.range.start.character)
+ local diff_after = math.max(0, completion_item.textEdit.range['end'].character - e.context.cursor.character)
+ local new_text = completion_item.textEdit.newText
+
+ if api.is_insert_mode() then
+ local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet
+ completion_item.textEdit.range.start.line = ctx.cursor.line
+ completion_item.textEdit.range.start.character = ctx.cursor.character - diff_before
+ completion_item.textEdit.range['end'].line = ctx.cursor.line
+ completion_item.textEdit.range['end'].character = ctx.cursor.character + diff_after
+ if is_snippet then
+ completion_item.textEdit.newText = ''
+ end
+ vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-16')
+ local texts = vim.split(completion_item.textEdit.newText, '\n')
+ local position = completion_item.textEdit.range.start
+ position.line = position.line + (#texts - 1)
+ if #texts == 1 then
+ position.character = position.character + misc.to_utfindex(texts[1])
+ else
+ position.character = misc.to_utfindex(texts[#texts])
+ end
+ local pos = types.lsp.Position.to_vim(0, position)
+ vim.api.nvim_win_set_cursor(0, { pos.row, pos.col - 1 })
+ if is_snippet then
+ config.get().snippet.expand({
+ body = new_text,
+ insert_text_mode = completion_item.insertTextMode,
+ })
+ end
+ else
+ local keys = {}
+ table.insert(keys, string.rep(keymap.t('<BS>'), diff_before))
+ table.insert(keys, string.rep(keymap.t('<Del>'), diff_after))
+ 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
diff --git a/start/cmp/lua/cmp/core_spec.lua b/start/cmp/lua/cmp/core_spec.lua
new file mode 100644
index 0000000..c37090e
--- /dev/null
+++ b/start/cmp/lua/cmp/core_spec.lua
@@ -0,0 +1,158 @@
+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()
+ local confirm = function(request, filter, completion_item)
+ local c = core.new()
+ local s = source.new('spec', {
+ 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], {})
+ 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('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)
+ 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 = 'foobarbaz',
+ },
+ })
+ assert.are.same(state.buffer, { '***foobarbaz***' })
+ assert.are.same(state.cursor[2], 12)
+ end)
+ end)
+ end)
+end)
diff --git a/start/cmp/lua/cmp/entry.lua b/start/cmp/lua/cmp/entry.lua
new file mode 100644
index 0000000..83c53a2
--- /dev/null
+++ b/start/cmp/lua/cmp/entry.lua
@@ -0,0 +1,468 @@
+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 config = require('cmp.config')
+local types = require('cmp.types')
+local matcher = require('cmp.matcher')
+
+---@class cmp.Entry
+---@field public id number
+---@field public cache cmp.Cache
+---@field public match_cache cmp.Cache
+---@field public score number
+---@field public exact boolean
+---@field public matches table
+---@field public context cmp.Context
+---@field public source cmp.Source
+---@field public source_offset number
+---@field public source_insert_range lsp.Range
+---@field public source_replace_range lsp.Range
+---@field public completion_item lsp.CompletionItem
+---@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
+---@return cmp.Entry
+entry.new = function(ctx, source, completion_item)
+ 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 = completion_item
+ self.resolved_completion_item = nil
+ self.resolved_callbacks = {}
+ self.resolving = false
+ self.confirmed = false
+ return self
+end
+
+---Make offset value
+---@return number
+entry.get_offset = function(self)
+ return self.cache:ensure({ 'get_offset', self.resolved_completion_item and 1 or 0 }, function()
+ local offset = self.source_offset
+ if misc.safe(self:get_completion_item().textEdit) then
+ local range = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range)
+ if range then
+ local c = misc.to_vimindex(self.context.cursor_line, range.start.character)
+ for idx = c, self.source_offset do
+ if not char.is_white(string.byte(self.context.cursor_line, idx)) then
+ offset = idx
+ break
+ end
+ 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 misc.safe(self:get_completion_item().word) then
+ return self:get_completion_item().word
+ end
+
+ local word
+ if misc.safe(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 = vim.lsp.util.parse_snippet(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(vim.lsp.util.parse_snippet(word))
+ end
+ else
+ word = str.trim(self:get_completion_item().label)
+ end
+ return str.oneline(word)
+ end)
+end
+
+---Get overwrite information
+---@return number, number
+entry.get_overwrite = function(self)
+ return self.cache:ensure({ 'get_overwrite', self.resolved_completion_item and 1 or 0 }, function()
+ if misc.safe(self:get_completion_item().textEdit) then
+ local r = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range)
+ local s = misc.to_vimindex(self.context.cursor_line, r.start.character)
+ local e = misc.to_vimindex(self.context.cursor_line, r['end'].character)
+ local before = self.context.cursor.col - s
+ local after = e - self.context.cursor.col
+ return { before, after }
+ end
+ return { 0, 0 }
+ end)
+end
+
+---Create filter text
+---@return string
+entry.get_filter_text = function(self)
+ return self.cache:ensure({ 'get_filter_text', self.resolved_completion_item and 1 or 0 }, function()
+ local word
+ if misc.safe(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', self.resolved_completion_item and 1 or 0 }, function()
+ local word
+ if misc.safe(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 misc.safe(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 number
+---@param entries_buf number The buffer this entry will be rendered into.
+---@return { abbr: { text: string, bytes: number, width: number, hl_group: string }, kind: { text: string, bytes: number, width: number, hl_group: string }, menu: { text: string, bytes: number, width: number, 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', self.resolved_completion_item and 1 or 0, 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 number
+---@return vim.CompletedItem
+entry.get_vim_item = function(self, suggest_offset)
+ return self.cache:ensure({ 'get_vim_item', suggest_offset, self.resolved_completion_item and 1 or 0 }, function()
+ local completion_item = self:get_completion_item()
+ local word = self:get_word()
+ local abbr = str.oneline(completion_item.label)
+
+ -- ~ indicator
+ local is_snippet = false
+ if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then
+ is_snippet = true
+ elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
+ is_snippet = self:get_insert_text() ~= word
+ elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then
+ is_snippet = true
+ end
+ if is_snippet 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 misc.safe(completion_item.labelDetails) then
+ menu = ''
+ if misc.safe(completion_item.labelDetails.detail) then
+ menu = menu .. completion_item.labelDetails.detail
+ end
+ if misc.safe(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 - 1 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 vim_item = {
+ word = word,
+ abbr = abbr,
+ kind = types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
+ 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 misc.safe(self:get_completion_item().commitCharacters) or {}
+end
+
+---Return insert range
+---@return lsp.Range|nil
+entry.get_insert_range = function(self)
+ local insert_range
+ if misc.safe(self:get_completion_item().textEdit) then
+ if misc.safe(self:get_completion_item().textEdit.insert) then
+ insert_range = self:get_completion_item().textEdit.insert
+ else
+ insert_range = self:get_completion_item().textEdit.range
+ end
+ else
+ insert_range = {
+ start = {
+ line = self.context.cursor.row - 1,
+ character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_insert_range.start.character),
+ },
+ ['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', self.resolved_completion_item and 1 or 0 }, function()
+ local replace_range
+ if misc.safe(self:get_completion_item().textEdit) and misc.safe(self:get_completion_item().textEdit.replace) then
+ replace_range = self:get_completion_item().textEdit.replace
+ else
+ replace_range = {
+ start = {
+ line = self.source_replace_range.start.line,
+ character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_replace_range.start.character),
+ },
+ ['end'] = self.source_replace_range['end'],
+ }
+ end
+ return replace_range
+ end)
+end
+
+---Match line.
+---@param input string
+---@param matching_config cmp.MatchingConfig
+---@return { score: number, 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_matching and 1 or 0,
+ matching_config.disallow_prefix_unmatching and 1 or 0,
+ }, function()
+ local option = {
+ disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching,
+ disallow_partial_matching = matching_config.disallow_partial_matching,
+ disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching,
+ synonyms = {
+ self:get_word(),
+ self:get_completion_item().label,
+ },
+ }
+
+ local score, matches, _
+ score, matches = matcher.match(input, self:get_filter_text(), option)
+
+ -- Support the language server that doesn't respect VSCode's behaviors.
+ if score == 0 then
+ if misc.safe(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 = false
+ accept = accept or string.match(prefix, '^[^%a]+$')
+ accept = accept or string.find(self:get_completion_item().textEdit.newText, prefix, 1, true)
+ if accept then
+ score, matches = matcher.match(input, prefix .. self:get_filter_text(), option)
+ end
+ end
+ end
+ end
+
+ if self:get_filter_text() ~= self:get_completion_item().label then
+ _, matches = matcher.match(input, self:get_completion_item().label, { self:get_word() })
+ 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', self.resolved_completion_item and 1 or 0 }, function()
+ if self.resolved_completion_item then
+ 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 misc.safe(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
+
+ if type(item.documentation) == 'string' and item.documentation ~= '' then
+ table.insert(documents, {
+ kind = types.lsp.MarkupKind.PlainText,
+ value = str.trim(item.documentation),
+ })
+ elseif type(item.documentation) == 'table' and item.documentation.value ~= '' then
+ table.insert(documents, item.documentation)
+ 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 misc.safe(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.resolved_completion_item = misc.safe(completion_item) or self.completion_item
+ for _, c in ipairs(self.resolved_callbacks) do
+ c()
+ end
+ end)
+ end
+end
+
+return entry
diff --git a/start/cmp/lua/cmp/entry_spec.lua b/start/cmp/lua/cmp/entry_spec.lua
new file mode 100644
index 0000000..d01125c
--- /dev/null
+++ b/start/cmp/lua/cmp/entry_spec.lua
@@ -0,0 +1,342 @@
+local spec = require('cmp.utils.spec')
+local source = require('cmp.source')
+local async = require('cmp.utils.async')
+
+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('[ansiblels] 1', function()
+ local item = {
+ detail = 'ansible.builtin',
+ filterText = 'blockinfile ansible.builtin.blockinfile',
+ kind = 7,
+ label = 'blockinfile',
+ sortText = '2_blockinfile',
+ textEdit = {
+ newText = '',
+ range = {
+ ['end'] = {
+ character = 7,
+ line = 15,
+ },
+ start = {
+ character = 6,
+ line = 15,
+ },
+ },
+ },
+ }
+ local s = source.new('dummy', {
+ resolve = function(_, _, callback)
+ item.textEdit.newText = 'modified'
+ callback(item)
+ end,
+ })
+ local e = entry.new(spec.state('', 1, 1).manual(), s, item)
+ assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile')
+ async.sync(function(done)
+ e:resolve(done)
+ end, 100)
+ assert.are.equal(e:get_vim_item(e:get_offset()).word, 'blockinfile')
+ 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)
+end)
diff --git a/start/cmp/lua/cmp/init.lua b/start/cmp/lua/cmp/init.lua
new file mode 100644
index 0000000..06f5f41
--- /dev/null
+++ b/start/cmp/lua/cmp/init.lua
@@ -0,0 +1,336 @@
+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 number
+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 number
+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()
+ 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 {}
+
+ 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
+ -- Special handling for native pum. Required to facilitate key mapping processing.
+ if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then
+ feedkeys.call(keymap.t('<C-n>'), 'in')
+ else
+ feedkeys.call(keymap.t('<Down>'), 'in')
+ end
+ return true
+ end
+ return false
+end)
+
+---Select prev item if possible
+cmp.select_prev_item = cmp.sync(function(option)
+ option = option or {}
+
+ 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
+ -- Special handling for native pum. Required to facilitate key mapping processing.
+ if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then
+ feedkeys.call(keymap.t('<C-p>'), 'in')
+ else
+ feedkeys.call(keymap.t('<Up>'), 'in')
+ end
+ return true
+ end
+ return false
+end)
+
+---Scrolling documentation window if possible
+cmp.scroll_docs = cmp.sync(function(delta)
+ if cmp.core.view:visible() then
+ cmp.core.view:scroll_docs(delta)
+ return true
+ else
+ return false
+ end
+end)
+
+---Confirm completion
+cmp.confirm = cmp.sync(function(option, callback)
+ option = option or {}
+ callback = callback or function() end
+
+ local e = cmp.core.view:get_selected_entry() or (option.select and cmp.core.view:get_first_entry() or nil)
+ 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
+ else
+ -- Special handling for native puma. Required to facilitate key mapping processing.
+ if vim.fn.complete_info({ 'selected' }).selected ~= -1 then
+ feedkeys.call(keymap.t('<C-y>'), 'in')
+ return true
+ end
+ return false
+ end
+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({ 'InsertEnter', 'CmdlineEnter' }, async.debounce_next_tick(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
diff --git a/start/cmp/lua/cmp/matcher.lua b/start/cmp/lua/cmp/matcher.lua
new file mode 100644
index 0000000..7a22d9e
--- /dev/null
+++ b/start/cmp/lua/cmp/matcher.lua
@@ -0,0 +1,324 @@
+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.
+--
+--
+---Match entry
+---@param input string
+---@param word string
+---@param option { synonyms: string[], disallow_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean }
+---@return number
+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
+ 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
+ return 0, {}
+ 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 prefix and matcher.fuzzy(input, word, matches) then
+ return score, matches
+ end
+ end
+ return 0, {}
+ end
+
+ return score + matcher.NOT_FUZZY_FACTOR, matches
+end
+
+--- fuzzy
+matcher.fuzzy = function(input, word, matches)
+ local last_match = matches[#matches]
+
+ -- Lately specified middle of text.
+ local input_index = last_match.input_match_end + 1
+ 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 last_input_index = input_index
+ local matched = false
+ local word_offset = 0
+ local word_index = last_match.word_match_end + 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
+ elseif matched then
+ input_index = last_input_index
+ input_match_end = input_index - 1
+ end
+ word_offset = word_offset + 1
+ end
+ if 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
diff --git a/start/cmp/lua/cmp/matcher_spec.lua b/start/cmp/lua/cmp/matcher_spec.lua
new file mode 100644
index 0000000..c95dfc3
--- /dev/null
+++ b/start/cmp/lua/cmp/matcher_spec.lua
@@ -0,0 +1,64 @@
+local spec = require('cmp.utils.spec')
+
+local matcher = require('cmp.matcher')
+
+describe('matcher', function()
+ before_each(spec.before)
+
+ it('match', function()
+ assert.is.truthy(matcher.match('', 'a') >= 1)
+ assert.is.truthy(matcher.match('a', 'a') >= 1)
+ assert.is.truthy(matcher.match('ab', 'a') == 0)
+ assert.is.truthy(matcher.match('ab', 'ab') > matcher.match('ab', 'a_b'))
+ assert.is.truthy(matcher.match('ab', 'a_b_c') > matcher.match('ac', 'a_b_c'))
+
+ assert.is.truthy(matcher.match('bora', 'border-radius') >= 1)
+ assert.is.truthy(matcher.match('woroff', 'word_offset') >= 1)
+ assert.is.truthy(matcher.match('call', 'call') > matcher.match('call', 'condition_all'))
+ assert.is.truthy(matcher.match('Buffer', 'Buffer') > matcher.match('Buffer', 'buffer'))
+ assert.is.truthy(matcher.match('luacon', 'lua_context') > matcher.match('luacon', 'LuaContext'))
+ assert.is.truthy(matcher.match('fmodify', 'fnamemodify') >= 1)
+ assert.is.truthy(matcher.match('candlesingle', 'candle#accept#single') >= 1)
+
+ assert.is.truthy(matcher.match('vi', 'void#') >= 1)
+ assert.is.truthy(matcher.match('vo', 'void#') >= 1)
+ assert.is.truthy(matcher.match('var_', 'var_dump') >= 1)
+ assert.is.truthy(matcher.match('conso', 'console') > matcher.match('conso', 'ConstantSourceNode'))
+ assert.is.truthy(matcher.match('usela', 'useLayoutEffect') > matcher.match('usela', 'useDataLayer'))
+ assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list'))
+ assert.is.truthy(matcher.match('2', '[[2021') >= 1)
+
+ assert.is.truthy(matcher.match(',', 'pri,') == 0)
+ assert.is.truthy(matcher.match('/', '/**') >= 1)
+
+ assert.is.truthy(matcher.match('true', 'v:true', { synonyms = { 'true' } }) == matcher.match('true', 'true'))
+ assert.is.truthy(matcher.match('g', 'get', { synonyms = { 'get' } }) > matcher.match('g', 'dein#get', { 'dein#get' }))
+ 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_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('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)
diff --git a/start/cmp/lua/cmp/source.lua b/start/cmp/lua/cmp/source.lua
new file mode 100644
index 0000000..bee0634
--- /dev/null
+++ b/start/cmp/lua/cmp/source.lua
@@ -0,0 +1,365 @@
+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 number
+---@field public name string
+---@field public source any
+---@field public cache cmp.Cache
+---@field public revision number
+---@field public incomplete boolean
+---@field public is_triggered_by_symbol boolean
+---@field public entries cmp.Entry[]
+---@field public offset number
+---@field public request_offset number
+---@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
+---@return boolean
+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 = (function()
+ local key = { 'get_entries', self.revision }
+ for i = ctx.cursor.col, self.offset, -1 do
+ key[3] = string.sub(ctx.cursor_before_line, 1, i)
+ local prev_entries = self.cache:get(key)
+ if prev_entries then
+ return prev_entries
+ end
+ end
+ return self.entries
+ end)()
+
+ local inputs = {}
+ local entries = {}
+ 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], self:get_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]
+ table.insert(entries, e)
+ end
+ end
+ self.cache:set({ 'get_entries', self.revision, ctx.cursor_before_line }, entries)
+
+ local max_item_count = self:get_source_config().max_item_count or 200
+ local limited_entries = {}
+ for _, e in ipairs(entries) do
+ table.insert(limited_entries, e)
+ if max_item_count and #limited_entries >= max_item_count then
+ break
+ end
+ end
+ return limited_entries
+end
+
+---Get default insert range
+---@return lsp.Range|nil
+source.get_default_insert_range = function(self)
+ if not self.context then
+ return nil
+ end
+
+ return self.cache:ensure({ 'get_default_insert_range', self.revision }, function()
+ return {
+ start = {
+ line = self.context.cursor.row - 1,
+ character = misc.to_utfindex(self.context.cursor_line, self.offset),
+ },
+ ['end'] = {
+ line = self.context.cursor.row - 1,
+ character = misc.to_utfindex(self.context.cursor_line, self.context.cursor.col),
+ },
+ }
+ end)
+end
+
+---Get default replace range
+---@return lsp.Range|nil
+source.get_default_replace_range = function(self)
+ if not self.context then
+ return nil
+ end
+
+ return self.cache:ensure({ 'get_default_replace_range', 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 = misc.to_utfindex(self.context.cursor_line, self.offset),
+ },
+ ['end'] = {
+ line = self.context.cursor.row - 1,
+ character = misc.to_utfindex(self.context.cursor_line, e and self.offset + e - 1 or self.context.cursor.col),
+ },
+ }
+ 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
+ return self.source:get_keyword_pattern(misc.copy(c))
+ end
+ return config.get().completion.keyword_pattern
+end
+
+---Get keyword_length
+---@return number
+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
+
+---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 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)
+ 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 i, item in ipairs(response.items or response) do
+ if (misc.safe(item) or {}).label then
+ local e = entry.new(ctx, self, item)
+ self.entries[i] = e
+ self.offset = math.min(self.offset, e:get_offset())
+ end
+ end
+ self.revision = self.revision + 1
+ if #self:get_entries(ctx) == 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
diff --git a/start/cmp/lua/cmp/source_spec.lua b/start/cmp/lua/cmp/source_spec.lua
new file mode 100644
index 0000000..339149f
--- /dev/null
+++ b/start/cmp/lua/cmp/source_spec.lua
@@ -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)
diff --git a/start/cmp/lua/cmp/types/cmp.lua b/start/cmp/lua/cmp/types/cmp.lua
new file mode 100644
index 0000000..8759aca
--- /dev/null
+++ b/start/cmp/lua/cmp/types/cmp.lua
@@ -0,0 +1,166 @@
+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 number
+
+---@class cmp.CompleteParams
+---@field public reason? cmp.ContextReason
+---@field public config? cmp.ConfigSchema
+
+---@class cmp.Setup
+---@field public __call fun(c: cmp.ConfigSchema)
+---@field public buffer fun(c: cmp.ConfigSchema)
+---@field public global fun(c: cmp.ConfigSchema)
+---@field public cmdline fun(type: string, c: cmp.ConfigSchema)
+---@field public filetype fun(type: string|string[], c: cmp.ConfigSchema)
+
+---@class cmp.SourceApiParams: cmp.SourceConfig
+
+---@class cmp.SourceCompletionApiParams : cmp.SourceConfig
+---@field public offset number
+---@field public context cmp.Context
+---@field public completion_context lsp.CompletionContext
+
+---@class cmp.Mapping
+---@field public i nil|function(fallback: function): void
+---@field public c nil|function(fallback: function): void
+---@field public x nil|function(fallback: function): void
+---@field public s nil|function(fallback: function): void
+
+---@class cmp.ConfigSchema
+---@field private revision number
+---@field public enabled fun():boolean|boolean
+---@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.WindowConfig
+---@field completion cmp.WindowConfig
+---@field documentation cmp.WindowConfig|nil
+
+---@class cmp.CompletionConfig
+---@field public autocomplete cmp.TriggerEvent[]
+---@field public completeopt string
+---@field public get_trigger_characters fun(trigger_characters: string[]): string[]
+---@field public keyword_length number
+---@field public keyword_pattern string
+
+---@class cmp.WindowConfig
+---@field public border string|string[]
+---@field public winhighlight string
+---@field public zindex number|nil
+---@field public max_width number|nil
+---@field public max_height number|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_partial_matching boolean
+---@field public disallow_prefix_unmatching boolean
+
+---@class cmp.SortingConfig
+---@field public priority_weight number
+---@field public comparators function[]
+
+---@class cmp.FormattingConfig
+---@field public fields cmp.ItemField[]
+---@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|false
+
+---@class cmp.GhostTextConfig
+---@field hl_group string
+
+---@class cmp.SourceConfig
+---@field public name string
+---@field public option table|nil
+---@field public priority number|nil
+---@field public trigger_characters string[]|nil
+---@field public keyword_pattern string|nil
+---@field public keyword_length number|nil
+---@field public max_item_count number|nil
+---@field public group_index number|nil
+
+---@class cmp.ViewConfig
+---@field public entries cmp.EntriesConfig
+
+---@alias cmp.EntriesConfig cmp.CustomEntriesConfig|cmp.NativeEntriesConfig|cmp.WildmenuEntriesConfig|string
+
+---@class cmp.CustomEntriesConfig
+---@field name 'custom'
+---@field selection_order 'top_down'|'near_cursor'
+
+---@class cmp.NativeEntriesConfig
+---@field name 'native'
+
+---@class cmp.WildmenuEntriesConfig
+---@field name 'wildmenu'
+---@field separator string|nil
+
+return cmp
diff --git a/start/cmp/lua/cmp/types/init.lua b/start/cmp/lua/cmp/types/init.lua
new file mode 100644
index 0000000..c4f601e
--- /dev/null
+++ b/start/cmp/lua/cmp/types/init.lua
@@ -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
diff --git a/start/cmp/lua/cmp/types/lsp.lua b/start/cmp/lua/cmp/types/lsp.lua
new file mode 100644
index 0000000..4af54a9
--- /dev/null
+++ b/start/cmp/lua/cmp/types/lsp.lua
@@ -0,0 +1,197 @@
+local misc = require('cmp.utils.misc')
+
+---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/
+---@class lsp
+local lsp = {}
+
+lsp.Position = {
+ ---Convert lsp.Position to vim.Position
+ ---@param buf number|string
+ ---@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 number|string
+ ---@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,
+}
+
+lsp.Range = {
+ ---Convert lsp.Range to vim.Range
+ ---@param buf number|string
+ ---@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 number|string
+ ---@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,
+}
+lsp.CompletionItemKind = vim.tbl_add_reverse_lookup(lsp.CompletionItemKind)
+
+---@class lsp.CompletionContext
+---@field public triggerKind lsp.CompletionTriggerKind
+---@field public triggerCharacter string|nil
+
+---@class lsp.CompletionList
+---@field public isIncomplete boolean
+---@field public items lsp.CompletionItem[]
+
+---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]|nil
+
+---@class lsp.MarkupContent
+---@field public kind lsp.MarkupKind
+---@field public value string
+
+---@class lsp.Position
+---@field public line number
+---@field public character number
+
+---@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
+
+---@class lsp.InsertReplaceTextEdit
+---@field public insert lsp.Range|nil
+---@field public replace lsp.Range|nil
+---@field public newText string
+
+---@class lsp.CompletionItemLabelDetails
+---@field public detail string|nil
+---@field public description string|nil
+
+---@class lsp.CompletionItem
+---@field public label string
+---@field public labelDetails lsp.CompletionItemLabelDetails|nil
+---@field public kind lsp.CompletionItemKind|nil
+---@field public tags lsp.CompletionItemTag[]|nil
+---@field public detail string|nil
+---@field public documentation lsp.MarkupContent|string|nil
+---@field public deprecated boolean|nil
+---@field public preselect boolean|nil
+---@field public sortText string|nil
+---@field public filterText string|nil
+---@field public insertText string|nil
+---@field public insertTextFormat lsp.InsertTextFormat
+---@field public insertTextMode lsp.InsertTextMode
+---@field public textEdit lsp.TextEdit|lsp.InsertReplaceTextEdit|nil
+---@field public additionalTextEdits lsp.TextEdit[]
+---@field public commitCharacters string[]|nil
+---@field public command lsp.Command|nil
+---@field public data any|nil
+---
+---TODO: Should send the issue for upstream?
+---@field public word string|nil
+---@field public dup boolean|nil
+
+return lsp
diff --git a/start/cmp/lua/cmp/types/lsp_spec.lua b/start/cmp/lua/cmp/types/lsp_spec.lua
new file mode 100644
index 0000000..fc2b857
--- /dev/null
+++ b/start/cmp/lua/cmp/types/lsp_spec.lua
@@ -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)
diff --git a/start/cmp/lua/cmp/types/vim.lua b/start/cmp/lua/cmp/types/vim.lua
new file mode 100644
index 0000000..95e757d
--- /dev/null
+++ b/start/cmp/lua/cmp/types/vim.lua
@@ -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
+---@field public row number
+---@field public col number
+
+---@class vim.Range
+---@field public start vim.Position
+---@field public end vim.Position
diff --git a/start/cmp/lua/cmp/utils/api.lua b/start/cmp/lua/cmp/utils/api.lua
new file mode 100644
index 0000000..d053409
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/api.lua
@@ -0,0 +1,69 @@
+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
+
+api.get_cursor = function()
+ if api.is_cmdline_mode() then
+ return { vim.o.lines - (vim.api.nvim_get_option('cmdheight') or 1) + 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], 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
diff --git a/start/cmp/lua/cmp/utils/api_spec.lua b/start/cmp/lua/cmp/utils/api_spec.lua
new file mode 100644
index 0000000..5363b48
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/api_spec.lua
@@ -0,0 +1,46 @@
+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()
+ describe('get_cursor', function()
+ before_each(spec.before)
+ 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_cursor_before_line', function()
+ before_each(spec.before)
+ 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)
diff --git a/start/cmp/lua/cmp/utils/async.lua b/start/cmp/lua/cmp/utils/async.lua
new file mode 100644
index 0000000..13f126b
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/async.lua
@@ -0,0 +1,127 @@
+local async = {}
+
+---@class cmp.AsyncThrottle
+---@field public running boolean
+---@field public timeout number
+---@field public sync function(self: cmp.AsyncThrottle, timeout: number|nil)
+---@field public stop function
+---@field public __call function
+
+---@param fn function
+---@param timeout number
+---@return cmp.AsyncThrottle
+async.throttle = function(fn, timeout)
+ local time = nil
+ local timer = vim.loop.new_timer()
+ return setmetatable({
+ running = false,
+ timeout = timeout,
+ sync = function(self, timeout_)
+ vim.wait(timeout_ or 1000, function()
+ return not self.running
+ end)
+ end,
+ stop = function()
+ time = nil
+ timer:stop()
+ end,
+ }, {
+ __call = function(self, ...)
+ local args = { ... }
+
+ if time == nil then
+ time = vim.loop.now()
+ end
+
+ self.running = true
+ timer:stop()
+ timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function()
+ vim.schedule(function()
+ time = nil
+ fn(unpack(args))
+ self.running = false
+ end)
+ end)
+ end,
+ })
+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 number
+---@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
+
+return async
diff --git a/start/cmp/lua/cmp/utils/async_spec.lua b/start/cmp/lua/cmp/utils/async_spec.lua
new file mode 100644
index 0000000..62f5379
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/async_spec.lua
@@ -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)
diff --git a/start/cmp/lua/cmp/utils/autocmd.lua b/start/cmp/lua/cmp/utils/autocmd.lua
new file mode 100644
index 0000000..438e231
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/autocmd.lua
@@ -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
diff --git a/start/cmp/lua/cmp/utils/binary.lua b/start/cmp/lua/cmp/utils/binary.lua
new file mode 100644
index 0000000..c6a7088
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/binary.lua
@@ -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 number
+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
diff --git a/start/cmp/lua/cmp/utils/binary_spec.lua b/start/cmp/lua/cmp/utils/binary_spec.lua
new file mode 100644
index 0000000..92fe129
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/binary_spec.lua
@@ -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)
diff --git a/start/cmp/lua/cmp/utils/buffer.lua b/start/cmp/lua/cmp/utils/buffer.lua
new file mode 100644
index 0000000..63171c9
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/buffer.lua
@@ -0,0 +1,28 @@
+local buffer = {}
+
+buffer.cache = {}
+
+---@return number 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 number 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
diff --git a/start/cmp/lua/cmp/utils/cache.lua b/start/cmp/lua/cmp/utils/cache.lua
new file mode 100644
index 0000000..8607b2a
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/cache.lua
@@ -0,0 +1,58 @@
+---@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
+---@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
+---@vararg any
+cache.set = function(self, key, value)
+ key = self:key(key)
+ self.entries[key] = value
+end
+
+---Ensure value by callback
+---@param key string
+---@param callback fun(): any
+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|table
+---@return string
+cache.key = function(_, key)
+ if type(key) == 'table' then
+ return table.concat(key, ':')
+ end
+ return key
+end
+
+return cache
diff --git a/start/cmp/lua/cmp/utils/char.lua b/start/cmp/lua/cmp/utils/char.lua
new file mode 100644
index 0000000..6e18994
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/char.lua
@@ -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 number
+---@return boolean
+char.is_upper = function(byte)
+ return ALPHA[byte]
+end
+
+---@param byte number
+---@return boolean
+char.is_alpha = function(byte)
+ return alpha[byte] or ALPHA[byte]
+end
+
+---@param byte number
+---@return boolean
+char.is_digit = function(byte)
+ return digit[byte]
+end
+
+---@param byte number
+---@return boolean
+char.is_white = function(byte)
+ return white[byte]
+end
+
+---@param byte number
+---@return boolean
+char.is_symbol = function(byte)
+ return not (char.is_alnum(byte) or char.is_white(byte))
+end
+
+---@param byte number
+---@return boolean
+char.is_printable = function(byte)
+ return string.match(string.char(byte), '^%c$') == nil
+end
+
+---@param byte number
+---@return boolean
+char.is_alnum = function(byte)
+ return char.is_alpha(byte) or char.is_digit(byte)
+end
+
+---@param text string
+---@param index number
+---@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 number
+---@return boolean
+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 number
+---@param byte2 number
+---@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
diff --git a/start/cmp/lua/cmp/utils/debug.lua b/start/cmp/lua/cmp/utils/debug.lua
new file mode 100644
index 0000000..c8b0dba
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/debug.lua
@@ -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
diff --git a/start/cmp/lua/cmp/utils/event.lua b/start/cmp/lua/cmp/utils/event.lua
new file mode 100644
index 0000000..662d573
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/event.lua
@@ -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
diff --git a/start/cmp/lua/cmp/utils/feedkeys.lua b/start/cmp/lua/cmp/utils/feedkeys.lua
new file mode 100644
index 0000000..cd20f60
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/feedkeys.lua
@@ -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>call v:lua.cmp.utils.feedkeys.call.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,
+})
+misc.set(_G, { 'cmp', 'utils', 'feedkeys', 'call', 'run' }, function(id)
+ if feedkeys.call.callbacks[id] then
+ feedkeys.call.callbacks[id]()
+ feedkeys.call.callbacks[id] = nil
+ end
+ return ''
+end)
+
+return feedkeys
diff --git a/start/cmp/lua/cmp/utils/feedkeys_spec.lua b/start/cmp/lua/cmp/utils/feedkeys_spec.lua
new file mode 100644
index 0000000..24fba71
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/feedkeys_spec.lua
@@ -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('bacckspace', function()
+ vim.cmd([[setlocal backspace=0]])
+ 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)
diff --git a/start/cmp/lua/cmp/utils/highlight.lua b/start/cmp/lua/cmp/utils/highlight.lua
new file mode 100644
index 0000000..867632a
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/highlight.lua
@@ -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
diff --git a/start/cmp/lua/cmp/utils/keymap.lua b/start/cmp/lua/cmp/utils/keymap.lua
new file mode 100644
index 0000000..aea5c1d
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/keymap.lua
@@ -0,0 +1,251 @@
+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.equals(map.rhs, '<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)
+ 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 number
+---@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
+
+---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.t(a) == keymap.t(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)
+ local id = string.match(existing.rhs, 'v:lua%.cmp%.utils%.keymap%.set_map%((%d+)%)')
+ if id and keymap.set_map.callbacks[tonumber(id, 10)] 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_expr = string.format('<Plug>(cmp.u.k.fallback_expr:%s)', map.lhs)
+ keymap.set_map(bufnr, mode, fallback_expr, 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',
+ })
+ vim.api.nvim_feedkeys(keymap.t(fallback_expr), 'im', true)
+ elseif not map.callback then
+ local solved = keymap.solve(bufnr, mode, map)
+ vim.api.nvim_feedkeys(solved.keys, solved.mode, true)
+ else
+ map.callback()
+ end
+ end
+end
+
+---Solve
+keymap.solve = function(bufnr, mode, map)
+ local lhs = keymap.t(map.lhs)
+ local rhs = map.expr and (map.callback and map.callback() or vim.api.nvim_eval(keymap.t(map.rhs))) or keymap.t(map.rhs)
+
+ 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 = map.script,
+ nowait = map.nowait,
+ silent = map.silent and mode ~= 'c',
+ })
+ 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,
+ noremap = map.noremap == 1,
+ script = map.script == 1,
+ silent = map.silent == 1,
+ nowait = map.nowait == 1,
+ buffer = true,
+ }
+ 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,
+ noremap = map.noremap == 1,
+ script = map.script == 1,
+ silent = map.silent == 1,
+ nowait = map.nowait == 1,
+ buffer = false,
+ }
+ end
+ end
+
+ return {
+ lhs = lhs,
+ rhs = lhs,
+ expr = false,
+ callback = nil,
+ noremap = true,
+ script = false,
+ silent = true,
+ nowait = false,
+ buffer = false,
+ }
+end
+
+---Set keymapping
+keymap.set_map = setmetatable({
+ callbacks = {},
+}, {
+ __call = function(self, bufnr, mode, lhs, rhs, opts)
+ if type(rhs) == 'function' then
+ local id = misc.id('cmp.utils.keymap.set_map')
+ self.callbacks[id] = rhs
+ if opts.expr then
+ rhs = ('v:lua.cmp.utils.keymap.set_map(%s)'):format(id)
+ else
+ rhs = ('<Cmd>call v:lua.cmp.utils.keymap.set_map(%s)<CR>'):format(id)
+ end
+ 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,
+})
+misc.set(_G, { 'cmp', 'utils', 'keymap', 'set_map' }, function(id)
+ return keymap.set_map.callbacks[id]() or ''
+end)
+
+return keymap
diff --git a/start/cmp/lua/cmp/utils/keymap_spec.lua b/start/cmp/lua/cmp/utils/keymap_spec.lua
new file mode 100644
index 0000000..959783f
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/keymap_spec.lua
@@ -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)
diff --git a/start/cmp/lua/cmp/utils/misc.lua b/start/cmp/lua/cmp/utils/misc.lua
new file mode 100644
index 0000000..7c6d0e7
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/misc.lua
@@ -0,0 +1,253 @@
+local misc = {}
+
+---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 number
+---@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
+
+---The symbol to remove key in misc.merge.
+misc.none = vim.NIL
+
+---Merge two tables recursively
+---@generic T
+---@param v1 T
+---@param v2 T
+---@return T
+misc.merge = function(v1, v2)
+ local merge1 = type(v1) == 'table' and (not vim.tbl_islist(v1) or vim.tbl_isempty(v1))
+ local merge2 = type(v2) == 'table' and (not vim.tbl_islist(v2) or vim.tbl_isempty(v2))
+ if merge1 and merge2 then
+ local new_tbl = {}
+ for k, v in pairs(v2) do
+ new_tbl[k] = misc.merge(v1[k], v)
+ end
+ for k, v in pairs(v1) do
+ if v2[k] == nil and v ~= misc.none then
+ new_tbl[k] = v
+ end
+ end
+ return new_tbl
+ end
+ if v1 == misc.none then
+ return nil
+ end
+ if v1 == nil then
+ if v2 == misc.none then
+ return nil
+ else
+ return v2
+ end
+ end
+ if v1 == true then
+ if merge2 then
+ return v2
+ end
+ return {}
+ end
+
+ return v1
+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,
+})
+
+---Check the value is nil or not.
+---@param v boolean
+---@return boolean
+misc.safe = function(v)
+ if v == nil or v == vim.NIL then
+ return nil
+ end
+ return v
+end
+
+---Treat 1/0 as bool value
+---@param v boolean|1|0
+---@param def boolean
+---@return boolean
+misc.bool = function(v, def)
+ if misc.safe(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] = misc.safe(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 vim.tbl_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 number|nil
+---@return number
+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 number
+---@return number
+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,
+ termcode = vim.api.nvim_replace_termcodes('<C-r><Esc>', true, true, true),
+}, {
+ __call = function(self, force)
+ if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then
+ if vim.o.incsearch then
+ return vim.api.nvim_feedkeys(self.termcode, 'in', 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
diff --git a/start/cmp/lua/cmp/utils/misc_spec.lua b/start/cmp/lua/cmp/utils/misc_spec.lua
new file mode 100644
index 0000000..f687155
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/misc_spec.lua
@@ -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)
diff --git a/start/cmp/lua/cmp/utils/pattern.lua b/start/cmp/lua/cmp/utils/pattern.lua
new file mode 100644
index 0000000..1481e84
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/pattern.lua
@@ -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
diff --git a/start/cmp/lua/cmp/utils/spec.lua b/start/cmp/lua/cmp/utils/spec.lua
new file mode 100644
index 0000000..a4b2c83
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/spec.lua
@@ -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
diff --git a/start/cmp/lua/cmp/utils/str.lua b/start/cmp/lua/cmp/utils/str.lua
new file mode 100644
index 0000000..bca210c
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/str.lua
@@ -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 number
+---@param min_length number
+---@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
diff --git a/start/cmp/lua/cmp/utils/str_spec.lua b/start/cmp/lua/cmp/utils/str_spec.lua
new file mode 100644
index 0000000..1a21855
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/str_spec.lua
@@ -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)
diff --git a/start/cmp/lua/cmp/utils/window.lua b/start/cmp/lua/cmp/utils/window.lua
new file mode 100644
index 0000000..a8a271e
--- /dev/null
+++ b/start/cmp/lua/cmp/utils/window.lua
@@ -0,0 +1,313 @@
+local cache = require('cmp.utils.cache')
+local misc = require('cmp.utils.misc')
+local buffer = require('cmp.utils.buffer')
+local api = require('cmp.utils.api')
+
+---@class cmp.WindowStyle
+---@field public relative string
+---@field public row number
+---@field public col number
+---@field public width number
+---@field public height number
+---@field public border string|string[]|nil
+---@field public zindex number|nil
+
+---@class cmp.Window
+---@field public name string
+---@field public win number|nil
+---@field public thumb_win number|nil
+---@field public sbar_win number|nil
+---@field public style cmp.WindowStyle
+---@field public opt table<string, any>
+---@field public buffer_opt table<string, any>
+---@field public cache cmp.Cache
+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.cache = cache.new()
+ 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
+ vim.api.nvim_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
+ vim.api.nvim_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
+end
+
+---Return buffer id.
+---@return number
+window.get_buffer = function(self)
+ local buf, created_new = buffer.ensure(self.name)
+ if created_new then
+ for k, v in pairs(self.buffer_opt) do
+ vim.api.nvim_buf_set_option(buf, k, v)
+ 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
+ vim.api.nvim_win_set_option(self.win, k, v)
+ end
+ end
+ self:update()
+end
+
+---Update
+window.update = function(self)
+ local info = self:info()
+ if info.scrollable 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)
+ vim.api.nvim_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)
+ vim.api.nvim_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 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 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: number, left: number, right: number, bottom: number, vert: number, horiz: number, 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 number
+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
diff --git a/start/cmp/lua/cmp/view.lua b/start/cmp/lua/cmp/view.lua
new file mode 100644
index 0000000..981378b
--- /dev/null
+++ b/start/cmp/lua/cmp/view.lua
@@ -0,0 +1,243 @@
+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 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.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[]
+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
+ 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
+ -- 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(entries, e)
+ offset = math.min(offset, e:get_offset())
+ end
+ end
+ end
+ end
+
+ -- sort.
+ local comparetors = config.get().sorting.comparators
+ table.sort(entries, function(e1, e2)
+ for _, fn in ipairs(comparetors) do
+ local diff = fn(e1, e2)
+ if diff ~= nil then
+ return diff
+ end
+ end
+ end)
+
+ -- open
+ if #entries > 0 then
+ self:_get_entries_view():open(offset, entries)
+ break
+ end
+ end
+
+ -- complete_done.
+ if #entries == 0 then
+ self:close()
+ end
+end
+
+---Close menu
+view.close = function(self)
+ if self:visible() then
+ self.event:emit('complete_done', {
+ entry = self:_get_entries_view():get_selected_entry(),
+ })
+ end
+ self:_get_entries_view():close()
+ self.docs_view:close()
+ self.ghost_text_view:hide()
+end
+
+---Abort menu
+view.abort = function(self)
+ self:_get_entries_view():abort()
+ self.docs_view:close()
+ self.ghost_text_view:hide()
+end
+
+---Return the view is visible or not.
+---@return boolean
+view.visible = function(self)
+ return self:_get_entries_view():visible()
+end
+
+---Scroll documentation window if possible.
+---@param delta number
+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
+ self.docs_view:open(e, self:_get_entries_view():info())
+ 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
diff --git a/start/cmp/lua/cmp/view/custom_entries_view.lua b/start/cmp/lua/cmp/view/custom_entries_view.lua
new file mode 100644
index 0000000..8020a9b
--- /dev/null
+++ b/start/cmp/lua/cmp/view/custom_entries_view.lua
@@ -0,0 +1,409 @@
+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 SIDE_PADDING = 1
+
+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 number
+---@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)
+ self.entries_win:option('scrolloff', 0)
+ -- 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.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 = 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
+ 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 = 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 == 0 and e.completion_item.preselect then
+ preselect = #self.entries
+ end
+ end
+ end
+ vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines)
+ 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('pumheight')
+ height = height ~= 0 and height or #self.entries
+ height = math.min(height, #self.entries)
+
+ local pos = api.get_screen_cursor()
+ local cursor = api.get_cursor()
+ local delta = cursor[2] + 1 - self.offset
+ 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 ~= 0 then
+ preselect = #self.entries - preselect + 1
+ end
+ end
+
+ -- Apply window options (that might be changed) on the custom completion menu.
+ self.entries_win:option('winblend', vim.o.pumblend)
+ self.entries_win:option('winhighlight', completion.winhighlight)
+ self.entries_win:open({
+ relative = 'editor',
+ style = 'minimal',
+ row = math.max(0, row),
+ col = math.max(0, col),
+ width = width,
+ height = height,
+ border = completion.border,
+ zindex = completion.zindex or 1001,
+ })
+ -- 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 > 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
+ if self:is_direction_top_down() then
+ self:_select(1, { behavior = types.cmp.SelectBehavior.Select })
+ else
+ self:_select(#self.entries - 1, { behavior = types.cmp.SelectBehavior.Select })
+ end
+ else
+ if self:is_direction_top_down() then
+ self:_select(0, { behavior = types.cmp.SelectBehavior.Select })
+ else
+ self:_select(#self.entries + 1, { behavior = types.cmp.SelectBehavior.Select })
+ 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(' ', 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(' ', SIDE_PADDING))
+ table.insert(texts, table.concat(text, ''))
+ end
+ end
+ vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts)
+ 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]
+ if self:is_direction_top_down() then
+ cursor = cursor + 1
+ else
+ cursor = cursor - 1
+ end
+ if not self.entries_win:option('cursorline') then
+ cursor = (self:is_direction_top_down() and 1) or #self.entries
+ elseif #self.entries < cursor then
+ cursor = (not self:is_direction_top_down() and #self.entries + 1) or 0
+ end
+ self:_select(cursor, option)
+ 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]
+ if self:is_direction_top_down() then
+ cursor = cursor - 1
+ else
+ cursor = cursor + 1
+ end
+ if not self.entries_win:option('cursorline') then
+ cursor = (self:is_direction_top_down() and #self.entries) or 1
+ elseif #self.entries < cursor then
+ cursor = (not self:is_direction_top_down() and 0) or #self.entries + 1
+ end
+ self:_select(cursor, option)
+ 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 = cursor > 0 and cursor <= #self.entries and is_insert
+ 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()
+ vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true)
+ else
+ if this.pending then
+ return
+ end
+ this.pending = true
+
+ 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
diff --git a/start/cmp/lua/cmp/view/docs_view.lua b/start/cmp/lua/cmp/view/docs_view.lua
new file mode 100644
index 0000000..9d2cd3f
--- /dev/null
+++ b/start/cmp/lua/cmp/view/docs_view.lua
@@ -0,0 +1,136 @@
+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('wrap', true)
+ 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.min(documentation.max_width, math.max(left_space, right_space))
+
+ -- 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)
+ vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, {
+ max_width = max_width,
+ max_height = documentation.max_height,
+ })
+ end
+
+ -- Calculate window size.
+ local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window:get_buffer(), 0, -1, false), {
+ max_width = max_width - border_info.horiz,
+ max_height = documentation.max_height - border_info.vert,
+ })
+ 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', vim.o.pumblend)
+ 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
diff --git a/start/cmp/lua/cmp/view/ghost_text_view.lua b/start/cmp/lua/cmp/view/ghost_text_view.lua
new file mode 100644
index 0000000..b798ebb
--- /dev/null
+++ b/start/cmp/lua/cmp/view/ghost_text_view.lua
@@ -0,0 +1,97 @@
+local config = require('cmp.config')
+local misc = require('cmp.utils.misc')
+local str = require('cmp.utils.str')
+local types = require('cmp.types')
+local api = require('cmp.utils.api')
+
+---@class cmp.GhostTextView
+local ghost_text_view = {}
+
+ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT')
+
+ghost_text_view.new = function()
+ local self = setmetatable({}, { __index = ghost_text_view })
+ self.win = nil
+ self.entry = nil
+ vim.api.nvim_set_decoration_provider(ghost_text_view.ns, {
+ on_win = function(_, win)
+ return win == self.win
+ end,
+ on_line = function(_)
+ 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 string.sub(line, col + 1) ~= '' then
+ return
+ end
+
+ local text = self.text_gen(self, line, col)
+ if #text > 0 then
+ vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, row - 1, col, {
+ right_gravity = false,
+ virt_text = { { text, c.hl_group or 'Comment' } },
+ virt_text_pos = 'overlay',
+ hl_mode = 'combine',
+ ephemeral = true,
+ })
+ 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.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
+ word = vim.lsp.util.parse_snippet(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 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
diff --git a/start/cmp/lua/cmp/view/native_entries_view.lua b/start/cmp/lua/cmp/view/native_entries_view.lua
new file mode 100644
index 0000000..d8b0b1b
--- /dev/null
+++ b/start/cmp/lua/cmp/view/native_entries_view.lua
@@ -0,0 +1,181 @@
+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 number
+---@field private items vim.CompletedItem
+---@field private entries cmp.Entry[]
+---@field private preselect_index number
+---@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_suitable_mode() and self:visible() then
+ vim.fn.complete(1, {})
+ 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.scrollable and 1 or 0),
+ height = info.height,
+ row = info.row,
+ col = info.col,
+ }
+ 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('<C-n>'), 'n', callback)
+ else
+ feedkeys.call(keymap.t('<Down>'), '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('<C-p>'), 'n', callback)
+ else
+ feedkeys.call(keymap.t('<Up>'), '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
diff --git a/start/cmp/lua/cmp/view/wildmenu_entries_view.lua b/start/cmp/lua/cmp/view/wildmenu_entries_view.lua
new file mode 100644
index 0000000..3419164
--- /dev/null
+++ b/start/cmp/lua/cmp/view/wildmenu_entries_view.lua
@@ -0,0 +1,261 @@
+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 number
+---@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
+ if self.selected_index == 0 or self.selected_index == #self.entries then
+ self:_select(1, option)
+ else
+ self:_select(self.selected_index + 1, option)
+ end
+ 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(self.selected_index - 1, 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
diff --git a/start/cmp/lua/cmp/vim_source.lua b/start/cmp/lua/cmp/vim_source.lua
new file mode 100644
index 0000000..2ee8fbf
--- /dev/null
+++ b/start/cmp/lua/cmp/vim_source.lua
@@ -0,0 +1,53 @@
+local misc = require('cmp.utils.misc')
+
+local vim_source = {}
+
+---@param id number
+---@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 number
+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 number
+---@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