Regenerate nvim config
This commit is contained in:
231
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/config.lua
Normal file
231
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/config.lua
Normal file
@ -0,0 +1,231 @@
|
||||
local mapping = require('cmp.config.mapping')
|
||||
local cache = require('cmp.utils.cache')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local api = require('cmp.utils.api')
|
||||
|
||||
---@class cmp.Config
|
||||
---@field public g cmp.ConfigSchema
|
||||
local config = {}
|
||||
|
||||
---@type cmp.Cache
|
||||
config.cache = cache.new()
|
||||
|
||||
---@type cmp.ConfigSchema
|
||||
config.global = require('cmp.config.default')()
|
||||
|
||||
---@type table<integer, cmp.ConfigSchema>
|
||||
config.buffers = {}
|
||||
|
||||
---@type table<string, cmp.ConfigSchema>
|
||||
config.filetypes = {}
|
||||
|
||||
---@type table<string, cmp.ConfigSchema>
|
||||
config.cmdline = {}
|
||||
|
||||
---@type cmp.ConfigSchema
|
||||
config.onetime = {}
|
||||
|
||||
---Set configuration for global.
|
||||
---@param c cmp.ConfigSchema
|
||||
config.set_global = function(c)
|
||||
config.global = config.normalize(misc.merge(c, config.global))
|
||||
config.global.revision = config.global.revision or 1
|
||||
config.global.revision = config.global.revision + 1
|
||||
end
|
||||
|
||||
---Set configuration for buffer
|
||||
---@param c cmp.ConfigSchema
|
||||
---@param bufnr integer
|
||||
config.set_buffer = function(c, bufnr)
|
||||
local revision = (config.buffers[bufnr] or {}).revision or 1
|
||||
config.buffers[bufnr] = c or {}
|
||||
config.buffers[bufnr].revision = revision + 1
|
||||
end
|
||||
|
||||
---Set configuration for filetype
|
||||
---@param c cmp.ConfigSchema
|
||||
---@param filetypes string[]|string
|
||||
config.set_filetype = function(c, filetypes)
|
||||
for _, filetype in ipairs(type(filetypes) == 'table' and filetypes or { filetypes }) do
|
||||
local revision = (config.filetypes[filetype] or {}).revision or 1
|
||||
config.filetypes[filetype] = c or {}
|
||||
config.filetypes[filetype].revision = revision + 1
|
||||
end
|
||||
end
|
||||
|
||||
---Set configuration for cmdline
|
||||
---@param c cmp.ConfigSchema
|
||||
---@param cmdtypes string|string[]
|
||||
config.set_cmdline = function(c, cmdtypes)
|
||||
for _, cmdtype in ipairs(type(cmdtypes) == 'table' and cmdtypes or { cmdtypes }) do
|
||||
local revision = (config.cmdline[cmdtype] or {}).revision or 1
|
||||
config.cmdline[cmdtype] = c or {}
|
||||
config.cmdline[cmdtype].revision = revision + 1
|
||||
end
|
||||
end
|
||||
|
||||
---Set configuration as oneshot completion.
|
||||
---@param c cmp.ConfigSchema
|
||||
config.set_onetime = function(c)
|
||||
local revision = (config.onetime or {}).revision or 1
|
||||
config.onetime = c or {}
|
||||
config.onetime.revision = revision + 1
|
||||
end
|
||||
|
||||
---@return cmp.ConfigSchema
|
||||
config.get = function()
|
||||
local global_config = config.global
|
||||
|
||||
-- The config object already has `revision` key.
|
||||
if #vim.tbl_keys(config.onetime) > 1 then
|
||||
local onetime_config = config.onetime
|
||||
return config.cache:ensure({
|
||||
'get',
|
||||
'onetime',
|
||||
global_config.revision or 0,
|
||||
onetime_config.revision or 0,
|
||||
}, function()
|
||||
local c = {}
|
||||
c = misc.merge(c, config.normalize(onetime_config))
|
||||
c = misc.merge(c, config.normalize(global_config))
|
||||
return c
|
||||
end)
|
||||
elseif api.is_cmdline_mode() then
|
||||
local cmdtype = vim.fn.getcmdtype()
|
||||
local cmdline_config = config.cmdline[cmdtype] or { revision = 1, sources = {} }
|
||||
return config.cache:ensure({
|
||||
'get',
|
||||
'cmdline',
|
||||
global_config.revision or 0,
|
||||
cmdtype,
|
||||
cmdline_config.revision or 0,
|
||||
}, function()
|
||||
local c = {}
|
||||
c = misc.merge(c, config.normalize(cmdline_config))
|
||||
c = misc.merge(c, config.normalize(global_config))
|
||||
return c
|
||||
end)
|
||||
else
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })
|
||||
local buffer_config = config.buffers[bufnr] or { revision = 1 }
|
||||
local filetype_config = config.filetypes[filetype] or { revision = 1 }
|
||||
return config.cache:ensure({
|
||||
'get',
|
||||
'default',
|
||||
global_config.revision or 0,
|
||||
filetype,
|
||||
filetype_config.revision or 0,
|
||||
bufnr,
|
||||
buffer_config.revision or 0,
|
||||
}, function()
|
||||
local c = {}
|
||||
c = misc.merge(config.normalize(c), config.normalize(buffer_config))
|
||||
c = misc.merge(config.normalize(c), config.normalize(filetype_config))
|
||||
c = misc.merge(config.normalize(c), config.normalize(global_config))
|
||||
return c
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---Return cmp is enabled or not.
|
||||
config.enabled = function()
|
||||
local enabled = config.get().enabled
|
||||
if type(enabled) == 'function' then
|
||||
enabled = enabled()
|
||||
end
|
||||
return enabled and api.is_suitable_mode()
|
||||
end
|
||||
|
||||
---Return source config
|
||||
---@param name string
|
||||
---@return cmp.SourceConfig
|
||||
config.get_source_config = function(name)
|
||||
local c = config.get()
|
||||
for _, s in ipairs(c.sources) do
|
||||
if s.name == name then
|
||||
return s
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---Return the current menu is native or not.
|
||||
config.is_native_menu = function()
|
||||
local c = config.get()
|
||||
if c.view and c.view.entries then
|
||||
return c.view.entries == 'native' or c.view.entries.name == 'native'
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Normalize mapping key
|
||||
---@param c any
|
||||
---@return cmp.ConfigSchema
|
||||
config.normalize = function(c)
|
||||
-- make sure c is not 'nil'
|
||||
---@type any
|
||||
c = c == nil and {} or c
|
||||
|
||||
-- Normalize mapping.
|
||||
if c.mapping then
|
||||
local normalized = {}
|
||||
for k, v in pairs(c.mapping) do
|
||||
normalized[keymap.normalize(k)] = mapping(v, { 'i' })
|
||||
end
|
||||
c.mapping = normalized
|
||||
end
|
||||
|
||||
-- Notice experimental.native_menu.
|
||||
if c.experimental and c.experimental.native_menu then
|
||||
vim.api.nvim_echo({
|
||||
{ '[nvim-cmp] ', 'Normal' },
|
||||
{ 'experimental.native_menu', 'WarningMsg' },
|
||||
{ ' is deprecated.\n', 'Normal' },
|
||||
{ '[nvim-cmp] Please use ', 'Normal' },
|
||||
{ 'view.entries = "native"', 'WarningMsg' },
|
||||
{ ' instead.', 'Normal' },
|
||||
}, true, {})
|
||||
|
||||
c.view = c.view or {}
|
||||
c.view.entries = c.view.entries or 'native'
|
||||
end
|
||||
|
||||
-- Notice documentation.
|
||||
if c.documentation ~= nil then
|
||||
vim.api.nvim_echo({
|
||||
{ '[nvim-cmp] ', 'Normal' },
|
||||
{ 'documentation', 'WarningMsg' },
|
||||
{ ' is deprecated.\n', 'Normal' },
|
||||
{ '[nvim-cmp] Please use ', 'Normal' },
|
||||
{ 'window.documentation = cmp.config.window.bordered()', 'WarningMsg' },
|
||||
{ ' instead.', 'Normal' },
|
||||
}, true, {})
|
||||
c.window = c.window or {}
|
||||
c.window.documentation = c.documentation
|
||||
end
|
||||
|
||||
-- Notice sources.[n].opts
|
||||
if c.sources then
|
||||
for _, s in ipairs(c.sources) do
|
||||
if s.opts and not s.option then
|
||||
s.option = s.opts
|
||||
s.opts = nil
|
||||
vim.api.nvim_echo({
|
||||
{ '[nvim-cmp] ', 'Normal' },
|
||||
{ 'sources[number].opts', 'WarningMsg' },
|
||||
{ ' is deprecated.\n', 'Normal' },
|
||||
{ '[nvim-cmp] Please use ', 'Normal' },
|
||||
{ 'sources[number].option', 'WarningMsg' },
|
||||
{ ' instead.', 'Normal' },
|
||||
}, true, {})
|
||||
end
|
||||
s.option = s.option or {}
|
||||
end
|
||||
end
|
||||
|
||||
return c
|
||||
end
|
||||
|
||||
return config
|
||||
@ -0,0 +1,270 @@
|
||||
local types = require('cmp.types')
|
||||
local cache = require('cmp.utils.cache')
|
||||
|
||||
---@type cmp.Comparator[]
|
||||
local compare = {}
|
||||
|
||||
--- Comparators (:help cmp-config.sorting.comparators) should return
|
||||
--- true when the first entry should come EARLIER (i.e., higher ranking) than the second entry,
|
||||
--- or nil if no pairwise ordering preference from the comparator.
|
||||
--- See also :help table.sort() and cmp.view.open() to see how comparators are used.
|
||||
|
||||
---@class cmp.ComparatorFunctor
|
||||
---@overload fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil
|
||||
---@alias cmp.ComparatorFunction fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean | nil
|
||||
---@alias cmp.Comparator cmp.ComparatorFunction | cmp.ComparatorFunctor
|
||||
|
||||
---offset: Entries with smaller offset will be ranked higher.
|
||||
---@type cmp.ComparatorFunction
|
||||
compare.offset = function(entry1, entry2)
|
||||
local diff = entry1:get_offset() - entry2:get_offset()
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---exact: Entries with exact == true will be ranked higher.
|
||||
---@type cmp.ComparatorFunction
|
||||
compare.exact = function(entry1, entry2)
|
||||
if entry1.exact ~= entry2.exact then
|
||||
return entry1.exact
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---score: Entries with higher score will be ranked higher.
|
||||
---@type cmp.ComparatorFunction
|
||||
compare.score = function(entry1, entry2)
|
||||
local diff = entry2.score - entry1.score
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---recently_used: Entries that are used recently will be ranked higher.
|
||||
---@type cmp.ComparatorFunctor
|
||||
compare.recently_used = setmetatable({
|
||||
records = {},
|
||||
add_entry = function(self, e)
|
||||
self.records[e.completion_item.label] = vim.loop.now()
|
||||
end,
|
||||
}, {
|
||||
---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
|
||||
__call = function(self, entry1, entry2)
|
||||
local t1 = self.records[entry1.completion_item.label] or -1
|
||||
local t2 = self.records[entry2.completion_item.label] or -1
|
||||
if t1 ~= t2 then
|
||||
return t1 > t2
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
})
|
||||
|
||||
---kind: Entires with smaller ordinal value of 'kind' will be ranked higher.
|
||||
---(see lsp.CompletionItemKind enum).
|
||||
---Exceptions are that Text(1) will be ranked the lowest, and snippets be the highest.
|
||||
---@type cmp.ComparatorFunction
|
||||
compare.kind = function(entry1, entry2)
|
||||
local kind1 = entry1:get_kind() --- @type lsp.CompletionItemKind | number
|
||||
local kind2 = entry2:get_kind() --- @type lsp.CompletionItemKind | number
|
||||
kind1 = kind1 == types.lsp.CompletionItemKind.Text and 100 or kind1
|
||||
kind2 = kind2 == types.lsp.CompletionItemKind.Text and 100 or kind2
|
||||
if kind1 ~= kind2 then
|
||||
if kind1 == types.lsp.CompletionItemKind.Snippet then
|
||||
return true
|
||||
end
|
||||
if kind2 == types.lsp.CompletionItemKind.Snippet then
|
||||
return false
|
||||
end
|
||||
local diff = kind1 - kind2
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---sort_text: Entries will be ranked according to the lexicographical order of sortText.
|
||||
---@type cmp.ComparatorFunction
|
||||
compare.sort_text = function(entry1, entry2)
|
||||
if entry1.completion_item.sortText and entry2.completion_item.sortText then
|
||||
local diff = vim.stricmp(entry1.completion_item.sortText, entry2.completion_item.sortText)
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---length: Entires with shorter label length will be ranked higher.
|
||||
---@type cmp.ComparatorFunction
|
||||
compare.length = function(entry1, entry2)
|
||||
local diff = #entry1.completion_item.label - #entry2.completion_item.label
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
----order: Entries with smaller id will be ranked higher.
|
||||
---@type fun(entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
|
||||
compare.order = function(entry1, entry2)
|
||||
local diff = entry1.id - entry2.id
|
||||
if diff < 0 then
|
||||
return true
|
||||
elseif diff > 0 then
|
||||
return false
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---locality: Entries with higher locality (i.e., words that are closer to the cursor)
|
||||
---will be ranked higher. See GH-183 for more details.
|
||||
---@type cmp.ComparatorFunctor
|
||||
compare.locality = setmetatable({
|
||||
lines_count = 10,
|
||||
lines_cache = cache.new(),
|
||||
locality_map = {},
|
||||
update = function(self)
|
||||
local config = require('cmp').get_config()
|
||||
if not vim.tbl_contains(config.sorting.comparators, compare.locality) then
|
||||
return
|
||||
end
|
||||
|
||||
local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf()
|
||||
local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1
|
||||
local max = vim.api.nvim_buf_line_count(buf)
|
||||
|
||||
if self.lines_cache:get('buf') ~= buf then
|
||||
self.lines_cache:clear()
|
||||
self.lines_cache:set('buf', buf)
|
||||
end
|
||||
|
||||
self.locality_map = {}
|
||||
for i = math.max(0, cursor_row - self.lines_count), math.min(max, cursor_row + self.lines_count) do
|
||||
local is_above = i < cursor_row
|
||||
local buffer = vim.api.nvim_buf_get_lines(buf, i, i + 1, false)[1] or ''
|
||||
local locality_map = self.lines_cache:ensure({ 'line', buffer }, function()
|
||||
local locality_map = {}
|
||||
local regexp = vim.regex(config.completion.keyword_pattern)
|
||||
while buffer ~= '' do
|
||||
local s, e = regexp:match_str(buffer)
|
||||
if s and e then
|
||||
local w = string.sub(buffer, s + 1, e)
|
||||
local d = math.abs(i - cursor_row) - (is_above and 1 or 0)
|
||||
locality_map[w] = math.min(locality_map[w] or math.huge, d)
|
||||
buffer = string.sub(buffer, e + 1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
return locality_map
|
||||
end)
|
||||
for w, d in pairs(locality_map) do
|
||||
self.locality_map[w] = math.min(self.locality_map[w] or d, math.abs(i - cursor_row))
|
||||
end
|
||||
end
|
||||
end,
|
||||
}, {
|
||||
---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
|
||||
__call = function(self, entry1, entry2)
|
||||
local local1 = self.locality_map[entry1:get_word()]
|
||||
local local2 = self.locality_map[entry2:get_word()]
|
||||
if local1 ~= local2 then
|
||||
if local1 == nil then
|
||||
return false
|
||||
end
|
||||
if local2 == nil then
|
||||
return true
|
||||
end
|
||||
return local1 < local2
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
})
|
||||
|
||||
---scopes: Entries defined in a closer scope will be ranked higher (e.g., prefer local variables to globals).
|
||||
---@type cmp.ComparatorFunctor
|
||||
compare.scopes = setmetatable({
|
||||
scopes_map = {},
|
||||
update = function(self)
|
||||
local config = require('cmp').get_config()
|
||||
if not vim.tbl_contains(config.sorting.comparators, compare.scopes) then
|
||||
return
|
||||
end
|
||||
|
||||
local ok, locals = pcall(require, 'nvim-treesitter.locals')
|
||||
if ok then
|
||||
local win, buf = vim.api.nvim_get_current_win(), vim.api.nvim_get_current_buf()
|
||||
local cursor_row = vim.api.nvim_win_get_cursor(win)[1] - 1
|
||||
|
||||
-- Cursor scope.
|
||||
local cursor_scope = nil
|
||||
for _, scope in ipairs(locals.get_scopes(buf)) do
|
||||
if scope:start() <= cursor_row and cursor_row <= scope:end_() then
|
||||
if not cursor_scope then
|
||||
cursor_scope = scope
|
||||
else
|
||||
if cursor_scope:start() <= scope:start() and scope:end_() <= cursor_scope:end_() then
|
||||
cursor_scope = scope
|
||||
end
|
||||
end
|
||||
elseif cursor_scope and cursor_scope:end_() <= scope:start() then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Definitions.
|
||||
local definitions = locals.get_definitions_lookup_table(buf)
|
||||
|
||||
-- Narrow definitions.
|
||||
local depth = 0
|
||||
for scope in locals.iter_scope_tree(cursor_scope, buf) do
|
||||
local s, e = scope:start(), scope:end_()
|
||||
|
||||
-- Check scope's direct child.
|
||||
for _, definition in pairs(definitions) do
|
||||
if s <= definition.node:start() and definition.node:end_() <= e then
|
||||
if scope:id() == locals.containing_scope(definition.node, buf):id() then
|
||||
local get_node_text = vim.treesitter.get_node_text or vim.treesitter.query.get_node_text
|
||||
local text = get_node_text(definition.node, buf) or ''
|
||||
if not self.scopes_map[text] then
|
||||
self.scopes_map[text] = depth
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
depth = depth + 1
|
||||
end
|
||||
end
|
||||
end,
|
||||
}, {
|
||||
---@type fun(self: table, entry1: cmp.Entry, entry2: cmp.Entry): boolean|nil
|
||||
__call = function(self, entry1, entry2)
|
||||
local local1 = self.scopes_map[entry1:get_word()]
|
||||
local local2 = self.scopes_map[entry2:get_word()]
|
||||
if local1 ~= local2 then
|
||||
if local1 == nil then
|
||||
return false
|
||||
end
|
||||
if local2 == nil then
|
||||
return true
|
||||
end
|
||||
return local1 < local2
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
return compare
|
||||
@ -0,0 +1,60 @@
|
||||
local api = require('cmp.utils.api')
|
||||
|
||||
local context = {}
|
||||
|
||||
---Check if cursor is in syntax group
|
||||
---@param group string | []string
|
||||
---@return boolean
|
||||
context.in_syntax_group = function(group)
|
||||
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
if not api.is_insert_mode() then
|
||||
col = col + 1
|
||||
end
|
||||
|
||||
for _, syn_id in ipairs(vim.fn.synstack(row, col)) do
|
||||
syn_id = vim.fn.synIDtrans(syn_id) -- Resolve :highlight links
|
||||
local g = vim.fn.synIDattr(syn_id, 'name')
|
||||
if type(group) == 'string' and g == group then
|
||||
return true
|
||||
elseif type(group) == 'table' and vim.tbl_contains(group, g) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---Check if cursor is in treesitter capture
|
||||
---@param capture string | []string
|
||||
---@return boolean
|
||||
context.in_treesitter_capture = function(capture)
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
row = row - 1
|
||||
if vim.api.nvim_get_mode().mode == 'i' then
|
||||
col = col - 1
|
||||
end
|
||||
|
||||
local get_captures_at_pos = -- See neovim/neovim#20331
|
||||
require('vim.treesitter').get_captures_at_pos -- for neovim >= 0.8 or require('vim.treesitter').get_captures_at_position -- for neovim < 0.8
|
||||
|
||||
local captures_at_cursor = vim.tbl_map(function(x)
|
||||
return x.capture
|
||||
end, get_captures_at_pos(buf, row, col))
|
||||
|
||||
if vim.tbl_isempty(captures_at_cursor) then
|
||||
return false
|
||||
elseif type(capture) == 'string' and vim.tbl_contains(captures_at_cursor, capture) then
|
||||
return true
|
||||
elseif type(capture) == 'table' then
|
||||
for _, v in ipairs(capture) do
|
||||
if vim.tbl_contains(captures_at_cursor, v) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
return context
|
||||
@ -0,0 +1,127 @@
|
||||
local compare = require('cmp.config.compare')
|
||||
local types = require('cmp.types')
|
||||
|
||||
local WIDE_HEIGHT = 40
|
||||
|
||||
---@return cmp.ConfigSchema
|
||||
return function()
|
||||
---@type cmp.ConfigSchema
|
||||
local config = {
|
||||
enabled = function()
|
||||
local disabled = false
|
||||
disabled = disabled or (vim.api.nvim_get_option_value('buftype', { buf = 0 }) == 'prompt')
|
||||
disabled = disabled or (vim.fn.reg_recording() ~= '')
|
||||
disabled = disabled or (vim.fn.reg_executing() ~= '')
|
||||
return not disabled
|
||||
end,
|
||||
|
||||
performance = {
|
||||
debounce = 60,
|
||||
throttle = 30,
|
||||
fetching_timeout = 500,
|
||||
confirm_resolve_timeout = 80,
|
||||
async_budget = 1,
|
||||
max_view_entries = 200,
|
||||
},
|
||||
|
||||
preselect = types.cmp.PreselectMode.Item,
|
||||
|
||||
mapping = {},
|
||||
|
||||
snippet = {
|
||||
expand = vim.fn.has('nvim-0.10') == 1 and function(args)
|
||||
vim.snippet.expand(args.body)
|
||||
end or function(_)
|
||||
error('snippet engine is not configured.')
|
||||
end,
|
||||
},
|
||||
|
||||
completion = {
|
||||
autocomplete = {
|
||||
types.cmp.TriggerEvent.TextChanged,
|
||||
},
|
||||
completeopt = 'menu,menuone,noselect',
|
||||
keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%(-\w*\)*\)]],
|
||||
keyword_length = 1,
|
||||
},
|
||||
|
||||
formatting = {
|
||||
expandable_indicator = true,
|
||||
fields = { 'abbr', 'kind', 'menu' },
|
||||
format = function(_, vim_item)
|
||||
return vim_item
|
||||
end,
|
||||
},
|
||||
|
||||
matching = {
|
||||
disallow_fuzzy_matching = false,
|
||||
disallow_fullfuzzy_matching = false,
|
||||
disallow_partial_fuzzy_matching = true,
|
||||
disallow_partial_matching = false,
|
||||
disallow_prefix_unmatching = false,
|
||||
disallow_symbol_nonprefix_matching = true,
|
||||
},
|
||||
|
||||
sorting = {
|
||||
priority_weight = 2,
|
||||
comparators = {
|
||||
compare.offset,
|
||||
compare.exact,
|
||||
-- compare.scopes,
|
||||
compare.score,
|
||||
compare.recently_used,
|
||||
compare.locality,
|
||||
compare.kind,
|
||||
-- compare.sort_text,
|
||||
compare.length,
|
||||
compare.order,
|
||||
},
|
||||
},
|
||||
|
||||
sources = {},
|
||||
|
||||
confirmation = {
|
||||
default_behavior = types.cmp.ConfirmBehavior.Insert,
|
||||
get_commit_characters = function(commit_characters)
|
||||
return commit_characters
|
||||
end,
|
||||
},
|
||||
|
||||
event = {},
|
||||
|
||||
experimental = {
|
||||
ghost_text = false,
|
||||
},
|
||||
|
||||
view = {
|
||||
entries = {
|
||||
name = 'custom',
|
||||
selection_order = 'top_down',
|
||||
follow_cursor = false,
|
||||
},
|
||||
docs = {
|
||||
auto_open = true,
|
||||
},
|
||||
},
|
||||
|
||||
window = {
|
||||
completion = {
|
||||
border = { '', '', '', '', '', '', '', '' },
|
||||
winhighlight = 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None',
|
||||
winblend = vim.o.pumblend,
|
||||
scrolloff = 0,
|
||||
col_offset = 0,
|
||||
side_padding = 1,
|
||||
scrollbar = true,
|
||||
},
|
||||
documentation = {
|
||||
max_height = math.floor(WIDE_HEIGHT * (WIDE_HEIGHT / vim.o.lines)),
|
||||
max_width = math.floor((WIDE_HEIGHT * 2) * (vim.o.columns / (WIDE_HEIGHT * 2 * 16 / 9))),
|
||||
border = { '', '', '', ' ', '', '', '', ' ' },
|
||||
winhighlight = 'FloatBorder:NormalFloat',
|
||||
winblend = vim.o.pumblend,
|
||||
},
|
||||
},
|
||||
}
|
||||
return config
|
||||
end
|
||||
@ -0,0 +1,230 @@
|
||||
local types = require('cmp.types')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
|
||||
local function merge_keymaps(base, override)
|
||||
local normalized_base = {}
|
||||
for k, v in pairs(base) do
|
||||
normalized_base[keymap.normalize(k)] = v
|
||||
end
|
||||
|
||||
local normalized_override = {}
|
||||
for k, v in pairs(override) do
|
||||
normalized_override[keymap.normalize(k)] = v
|
||||
end
|
||||
|
||||
return misc.merge(normalized_base, normalized_override)
|
||||
end
|
||||
|
||||
local mapping = setmetatable({}, {
|
||||
__call = function(_, invoke, modes)
|
||||
if type(invoke) == 'function' then
|
||||
local map = {}
|
||||
for _, mode in ipairs(modes or { 'i' }) do
|
||||
map[mode] = invoke
|
||||
end
|
||||
return map
|
||||
end
|
||||
return invoke
|
||||
end,
|
||||
})
|
||||
|
||||
---Mapping preset configuration.
|
||||
mapping.preset = {}
|
||||
|
||||
---Mapping preset insert-mode configuration.
|
||||
mapping.preset.insert = function(override)
|
||||
return merge_keymaps(override or {}, {
|
||||
['<Down>'] = {
|
||||
i = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }),
|
||||
},
|
||||
['<Up>'] = {
|
||||
i = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }),
|
||||
},
|
||||
['<C-n>'] = {
|
||||
i = function()
|
||||
local cmp = require('cmp')
|
||||
if cmp.visible() then
|
||||
cmp.select_next_item({ behavior = types.cmp.SelectBehavior.Insert })
|
||||
else
|
||||
cmp.complete()
|
||||
end
|
||||
end,
|
||||
},
|
||||
['<C-p>'] = {
|
||||
i = function()
|
||||
local cmp = require('cmp')
|
||||
if cmp.visible() then
|
||||
cmp.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert })
|
||||
else
|
||||
cmp.complete()
|
||||
end
|
||||
end,
|
||||
},
|
||||
['<C-y>'] = {
|
||||
i = mapping.confirm({ select = false }),
|
||||
},
|
||||
['<C-e>'] = {
|
||||
i = mapping.abort(),
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
---Mapping preset cmdline-mode configuration.
|
||||
mapping.preset.cmdline = function(override)
|
||||
return merge_keymaps(override or {}, {
|
||||
['<C-z>'] = {
|
||||
c = function()
|
||||
local cmp = require('cmp')
|
||||
if cmp.visible() then
|
||||
cmp.select_next_item()
|
||||
else
|
||||
cmp.complete()
|
||||
end
|
||||
end,
|
||||
},
|
||||
['<Tab>'] = {
|
||||
c = function()
|
||||
local cmp = require('cmp')
|
||||
if cmp.visible() then
|
||||
cmp.select_next_item()
|
||||
else
|
||||
cmp.complete()
|
||||
end
|
||||
end,
|
||||
},
|
||||
['<S-Tab>'] = {
|
||||
c = function()
|
||||
local cmp = require('cmp')
|
||||
if cmp.visible() then
|
||||
cmp.select_prev_item()
|
||||
else
|
||||
cmp.complete()
|
||||
end
|
||||
end,
|
||||
},
|
||||
['<C-n>'] = {
|
||||
c = function(fallback)
|
||||
local cmp = require('cmp')
|
||||
if cmp.visible() then
|
||||
cmp.select_next_item()
|
||||
else
|
||||
fallback()
|
||||
end
|
||||
end,
|
||||
},
|
||||
['<C-p>'] = {
|
||||
c = function(fallback)
|
||||
local cmp = require('cmp')
|
||||
if cmp.visible() then
|
||||
cmp.select_prev_item()
|
||||
else
|
||||
fallback()
|
||||
end
|
||||
end,
|
||||
},
|
||||
['<C-e>'] = {
|
||||
c = mapping.abort(),
|
||||
},
|
||||
['<C-y>'] = {
|
||||
c = mapping.confirm({ select = false }),
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
---Invoke completion
|
||||
---@param option? cmp.CompleteParams
|
||||
mapping.complete = function(option)
|
||||
return function(fallback)
|
||||
if not require('cmp').complete(option) then
|
||||
fallback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Complete common string.
|
||||
mapping.complete_common_string = function()
|
||||
return function(fallback)
|
||||
if not require('cmp').complete_common_string() then
|
||||
fallback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Close current completion menu if it displayed.
|
||||
mapping.close = function()
|
||||
return function(fallback)
|
||||
if not require('cmp').close() then
|
||||
fallback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Abort current completion menu if it displayed.
|
||||
mapping.abort = function()
|
||||
return function(fallback)
|
||||
if not require('cmp').abort() then
|
||||
fallback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Scroll documentation window.
|
||||
mapping.scroll_docs = function(delta)
|
||||
return function(fallback)
|
||||
if not require('cmp').scroll_docs(delta) then
|
||||
fallback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Opens the documentation window.
|
||||
mapping.open_docs = function()
|
||||
return function(fallback)
|
||||
if not require('cmp').open_docs() then
|
||||
fallback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Close the documentation window.
|
||||
mapping.close_docs = function()
|
||||
return function(fallback)
|
||||
if not require('cmp').close_docs() then
|
||||
fallback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Select next completion item.
|
||||
mapping.select_next_item = function(option)
|
||||
return function(fallback)
|
||||
if not require('cmp').select_next_item(option) then
|
||||
local release = require('cmp').core:suspend()
|
||||
fallback()
|
||||
vim.schedule(release)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Select prev completion item.
|
||||
mapping.select_prev_item = function(option)
|
||||
return function(fallback)
|
||||
if not require('cmp').select_prev_item(option) then
|
||||
local release = require('cmp').core:suspend()
|
||||
fallback()
|
||||
vim.schedule(release)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Confirm selection
|
||||
mapping.confirm = function(option)
|
||||
return function(fallback)
|
||||
if not require('cmp').confirm(option) then
|
||||
fallback()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return mapping
|
||||
@ -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
|
||||
@ -0,0 +1,16 @@
|
||||
local window = {}
|
||||
|
||||
window.bordered = function(opts)
|
||||
opts = opts or {}
|
||||
return {
|
||||
border = opts.border or 'rounded',
|
||||
winhighlight = opts.winhighlight or 'Normal:Normal,FloatBorder:FloatBorder,CursorLine:Visual,Search:None',
|
||||
zindex = opts.zindex or 1001,
|
||||
scrolloff = opts.scrolloff or 0,
|
||||
col_offset = opts.col_offset or 0,
|
||||
side_padding = opts.side_padding or 1,
|
||||
scrollbar = opts.scrollbar == nil and true or opts.scrollbar,
|
||||
}
|
||||
end
|
||||
|
||||
return window
|
||||
111
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/context.lua
Normal file
111
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/context.lua
Normal file
@ -0,0 +1,111 @@
|
||||
local misc = require('cmp.utils.misc')
|
||||
local pattern = require('cmp.utils.pattern')
|
||||
local types = require('cmp.types')
|
||||
local cache = require('cmp.utils.cache')
|
||||
local api = require('cmp.utils.api')
|
||||
|
||||
---@class cmp.Context
|
||||
---@field public id string
|
||||
---@field public cache cmp.Cache
|
||||
---@field public prev_context cmp.Context
|
||||
---@field public option cmp.ContextOption
|
||||
---@field public filetype string
|
||||
---@field public time integer
|
||||
---@field public bufnr integer
|
||||
---@field public cursor vim.Position|lsp.Position
|
||||
---@field public cursor_line string
|
||||
---@field public cursor_after_line string
|
||||
---@field public cursor_before_line string
|
||||
---@field public aborted boolean
|
||||
local context = {}
|
||||
|
||||
---Create new empty context
|
||||
---@return cmp.Context
|
||||
context.empty = function()
|
||||
local ctx = context.new({}) -- dirty hack to prevent recursive call `context.empty`.
|
||||
ctx.bufnr = -1
|
||||
ctx.input = ''
|
||||
ctx.cursor = {}
|
||||
ctx.cursor.row = -1
|
||||
ctx.cursor.col = -1
|
||||
return ctx
|
||||
end
|
||||
|
||||
---Create new context
|
||||
---@param prev_context? cmp.Context
|
||||
---@param option? cmp.ContextOption
|
||||
---@return cmp.Context
|
||||
context.new = function(prev_context, option)
|
||||
option = option or {}
|
||||
|
||||
local self = setmetatable({}, { __index = context })
|
||||
self.id = misc.id('cmp.context.new')
|
||||
self.cache = cache.new()
|
||||
self.prev_context = prev_context or context.empty()
|
||||
self.option = option or { reason = types.cmp.ContextReason.None }
|
||||
self.filetype = vim.api.nvim_get_option_value('filetype', { buf = 0 })
|
||||
self.time = vim.loop.now()
|
||||
self.bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
local cursor = api.get_cursor()
|
||||
self.cursor_line = api.get_current_line()
|
||||
self.cursor = {}
|
||||
self.cursor.row = cursor[1]
|
||||
self.cursor.col = cursor[2] + 1
|
||||
self.cursor.line = self.cursor.row - 1
|
||||
self.cursor.character = misc.to_utfindex(self.cursor_line, self.cursor.col)
|
||||
self.cursor_before_line = string.sub(self.cursor_line, 1, self.cursor.col - 1)
|
||||
self.cursor_after_line = string.sub(self.cursor_line, self.cursor.col)
|
||||
self.aborted = false
|
||||
return self
|
||||
end
|
||||
|
||||
context.abort = function(self)
|
||||
self.aborted = true
|
||||
end
|
||||
|
||||
---Return context creation reason.
|
||||
---@return cmp.ContextReason
|
||||
context.get_reason = function(self)
|
||||
return self.option.reason
|
||||
end
|
||||
|
||||
---Get keyword pattern offset
|
||||
---@return integer
|
||||
context.get_offset = function(self, keyword_pattern)
|
||||
return self.cache:ensure({ 'get_offset', keyword_pattern, self.cursor_before_line }, function()
|
||||
return pattern.offset([[\%(]] .. keyword_pattern .. [[\)\m$]], self.cursor_before_line) or self.cursor.col
|
||||
end)
|
||||
end
|
||||
|
||||
---Return if this context is changed from previous context or not.
|
||||
---@return boolean
|
||||
context.changed = function(self, ctx)
|
||||
local curr = self
|
||||
|
||||
if curr.bufnr ~= ctx.bufnr then
|
||||
return true
|
||||
end
|
||||
if curr.cursor.row ~= ctx.cursor.row then
|
||||
return true
|
||||
end
|
||||
if curr.cursor.col ~= ctx.cursor.col then
|
||||
return true
|
||||
end
|
||||
if curr:get_reason() == types.cmp.ContextReason.Manual then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---Shallow clone
|
||||
context.clone = function(self)
|
||||
local cloned = {}
|
||||
for k, v in pairs(self) do
|
||||
cloned[k] = v
|
||||
end
|
||||
return cloned
|
||||
end
|
||||
|
||||
return context
|
||||
@ -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)
|
||||
524
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/core.lua
Normal file
524
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/core.lua
Normal file
@ -0,0 +1,524 @@
|
||||
local debug = require('cmp.utils.debug')
|
||||
local str = require('cmp.utils.str')
|
||||
local char = require('cmp.utils.char')
|
||||
local feedkeys = require('cmp.utils.feedkeys')
|
||||
local async = require('cmp.utils.async')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local context = require('cmp.context')
|
||||
local source = require('cmp.source')
|
||||
local view = require('cmp.view')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local config = require('cmp.config')
|
||||
local types = require('cmp.types')
|
||||
local api = require('cmp.utils.api')
|
||||
local event = require('cmp.utils.event')
|
||||
|
||||
---@class cmp.Core
|
||||
---@field public suspending boolean
|
||||
---@field public view cmp.View
|
||||
---@field public sources cmp.Source[]
|
||||
---@field public context cmp.Context
|
||||
---@field public event cmp.Event
|
||||
local core = {}
|
||||
|
||||
core.new = function()
|
||||
local self = setmetatable({}, { __index = core })
|
||||
self.suspending = false
|
||||
self.sources = {}
|
||||
self.context = context.new()
|
||||
self.event = event.new()
|
||||
self.view = view.new()
|
||||
self.view.event:on('keymap', function(...)
|
||||
self:on_keymap(...)
|
||||
end)
|
||||
for _, event_name in ipairs({ 'complete_done', 'menu_opened', 'menu_closed' }) do
|
||||
self.view.event:on(event_name, function(evt)
|
||||
self.event:emit(event_name, evt)
|
||||
end)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
---Register source
|
||||
---@param s cmp.Source
|
||||
core.register_source = function(self, s)
|
||||
self.sources[s.id] = s
|
||||
end
|
||||
|
||||
---Unregister source
|
||||
---@param source_id integer
|
||||
core.unregister_source = function(self, source_id)
|
||||
self.sources[source_id] = nil
|
||||
end
|
||||
|
||||
---Get new context
|
||||
---@param option? cmp.ContextOption
|
||||
---@return cmp.Context
|
||||
core.get_context = function(self, option)
|
||||
self.context:abort()
|
||||
local prev = self.context:clone()
|
||||
prev.prev_context = nil
|
||||
prev.cache = nil
|
||||
local ctx = context.new(prev, option)
|
||||
self:set_context(ctx)
|
||||
return self.context
|
||||
end
|
||||
|
||||
---Set new context
|
||||
---@param ctx cmp.Context
|
||||
core.set_context = function(self, ctx)
|
||||
self.context = ctx
|
||||
end
|
||||
|
||||
---Suspend completion
|
||||
core.suspend = function(self)
|
||||
self.suspending = true
|
||||
-- It's needed to avoid conflicting with autocmd debouncing.
|
||||
return vim.schedule_wrap(function()
|
||||
self.suspending = false
|
||||
end)
|
||||
end
|
||||
|
||||
---Get sources that sorted by priority
|
||||
---@param filter? cmp.SourceStatus[]|fun(s: cmp.Source): boolean
|
||||
---@return cmp.Source[]
|
||||
core.get_sources = function(self, filter)
|
||||
local f = function(s)
|
||||
if type(filter) == 'table' then
|
||||
return vim.tbl_contains(filter, s.status)
|
||||
elseif type(filter) == 'function' then
|
||||
return filter(s)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local sources = {}
|
||||
for _, c in pairs(config.get().sources) do
|
||||
for _, s in pairs(self.sources) do
|
||||
if c.name == s.name then
|
||||
if s:is_available() and f(s) then
|
||||
table.insert(sources, s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return sources
|
||||
end
|
||||
|
||||
---Keypress handler
|
||||
core.on_keymap = function(self, keys, fallback)
|
||||
local mode = api.get_mode()
|
||||
for key, mapping in pairs(config.get().mapping) do
|
||||
if keymap.equals(key, keys) and mapping[mode] then
|
||||
return mapping[mode](fallback)
|
||||
end
|
||||
end
|
||||
|
||||
--Commit character. NOTE: This has a lot of cmp specific implementation to make more user-friendly.
|
||||
local chars = keymap.t(keys)
|
||||
local e = self.view:get_active_entry()
|
||||
if e and vim.tbl_contains(config.get().confirmation.get_commit_characters(e:get_commit_characters()), chars) then
|
||||
local is_printable = char.is_printable(string.byte(chars, 1))
|
||||
self:confirm(e, {
|
||||
behavior = is_printable and 'insert' or 'replace',
|
||||
commit_character = chars,
|
||||
}, function()
|
||||
local ctx = self:get_context()
|
||||
local word = e:get_word()
|
||||
if string.sub(ctx.cursor_before_line, -#word, ctx.cursor.col - 1) == word and is_printable then
|
||||
fallback()
|
||||
else
|
||||
self:reset()
|
||||
end
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
fallback()
|
||||
end
|
||||
|
||||
---Prepare completion
|
||||
core.prepare = function(self)
|
||||
for keys, mapping in pairs(config.get().mapping) do
|
||||
for mode in pairs(mapping) do
|
||||
keymap.listen(mode, keys, function(...)
|
||||
self:on_keymap(...)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Check auto-completion
|
||||
core.on_change = function(self, trigger_event)
|
||||
local ignore = false
|
||||
ignore = ignore or self.suspending
|
||||
ignore = ignore or (vim.fn.pumvisible() == 1 and (vim.v.completed_item).word)
|
||||
ignore = ignore or not self.view:ready()
|
||||
if ignore then
|
||||
self:get_context({ reason = types.cmp.ContextReason.Auto })
|
||||
return
|
||||
end
|
||||
self:autoindent(trigger_event, function()
|
||||
local ctx = self:get_context({ reason = types.cmp.ContextReason.Auto })
|
||||
debug.log(('ctx: `%s`'):format(ctx.cursor_before_line))
|
||||
if ctx:changed(ctx.prev_context) then
|
||||
self.view:on_change()
|
||||
debug.log('changed')
|
||||
|
||||
if vim.tbl_contains(config.get().completion.autocomplete or {}, trigger_event) then
|
||||
self:complete(ctx)
|
||||
else
|
||||
self.filter.timeout = self.view:visible() and config.get().performance.throttle or 0
|
||||
self:filter()
|
||||
end
|
||||
else
|
||||
debug.log('unchanged')
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Cursor moved.
|
||||
core.on_moved = function(self)
|
||||
local ignore = false
|
||||
ignore = ignore or self.suspending
|
||||
ignore = ignore or (vim.fn.pumvisible() == 1 and (vim.v.completed_item).word)
|
||||
ignore = ignore or not self.view:visible()
|
||||
if ignore then
|
||||
return
|
||||
end
|
||||
self:filter()
|
||||
end
|
||||
|
||||
---Returns the suffix of the specified `line`.
|
||||
---
|
||||
---Contains `%s`: returns everything after the last `%s` in `line`
|
||||
---Else: returns `line` unmodified
|
||||
---@param line string
|
||||
---@return string suffix
|
||||
local function find_line_suffix(line)
|
||||
return line:match('%S*$') --[[@as string]]
|
||||
end
|
||||
|
||||
---Check autoindent
|
||||
---@param trigger_event cmp.TriggerEvent
|
||||
---@param callback function
|
||||
core.autoindent = function(self, trigger_event, callback)
|
||||
if trigger_event ~= types.cmp.TriggerEvent.TextChanged then
|
||||
return callback()
|
||||
end
|
||||
if not api.is_insert_mode() then
|
||||
return callback()
|
||||
end
|
||||
|
||||
-- Check prefix
|
||||
local cursor_before_line = api.get_cursor_before_line()
|
||||
local prefix = find_line_suffix(cursor_before_line) or ''
|
||||
if #prefix == 0 then
|
||||
return callback()
|
||||
end
|
||||
|
||||
-- Reset current completion if indentkeys matched.
|
||||
for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do
|
||||
if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then
|
||||
self:reset()
|
||||
self:set_context(context.empty())
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
callback()
|
||||
end
|
||||
|
||||
---Complete common string for current completed entries.
|
||||
core.complete_common_string = function(self)
|
||||
if not self.view:visible() or self.view:get_selected_entry() then
|
||||
return false
|
||||
end
|
||||
|
||||
config.set_onetime({
|
||||
sources = config.get().sources,
|
||||
matching = {
|
||||
disallow_prefix_unmatching = true,
|
||||
disallow_partial_matching = true,
|
||||
disallow_fuzzy_matching = true,
|
||||
},
|
||||
})
|
||||
|
||||
self:filter()
|
||||
self.filter:sync(1000)
|
||||
|
||||
config.set_onetime({})
|
||||
|
||||
local cursor = api.get_cursor()
|
||||
local offset = self.view:get_offset() or cursor[2]
|
||||
local common_string
|
||||
for _, e in ipairs(self.view:get_entries()) do
|
||||
local vim_item = e:get_vim_item(offset)
|
||||
if not common_string then
|
||||
common_string = vim_item.word
|
||||
else
|
||||
common_string = str.get_common_string(common_string, vim_item.word)
|
||||
end
|
||||
end
|
||||
local cursor_before_line = api.get_cursor_before_line()
|
||||
local pretext = cursor_before_line:sub(offset)
|
||||
if common_string and #common_string > #pretext then
|
||||
feedkeys.call(keymap.backspace(pretext) .. common_string, 'n')
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Invoke completion
|
||||
---@param ctx cmp.Context
|
||||
core.complete = function(self, ctx)
|
||||
if not api.is_suitable_mode() then
|
||||
return
|
||||
end
|
||||
|
||||
self:set_context(ctx)
|
||||
|
||||
-- Invoke completion sources.
|
||||
local sources = self:get_sources()
|
||||
for _, s in ipairs(sources) do
|
||||
local callback
|
||||
callback = (function(s_)
|
||||
return function()
|
||||
local new = context.new(ctx)
|
||||
if s_.incomplete and new:changed(s_.context) then
|
||||
s_:complete(new, callback)
|
||||
else
|
||||
if not self.view:get_active_entry() then
|
||||
self.filter.stop()
|
||||
self.filter.timeout = config.get().performance.debounce
|
||||
self:filter()
|
||||
end
|
||||
end
|
||||
end
|
||||
end)(s)
|
||||
s:complete(ctx, callback)
|
||||
end
|
||||
|
||||
if not self.view:get_active_entry() then
|
||||
self.filter.timeout = self.view:visible() and config.get().performance.throttle or 1
|
||||
self:filter()
|
||||
end
|
||||
end
|
||||
|
||||
---Update completion menu
|
||||
local async_filter = async.wrap(function(self)
|
||||
self.filter.timeout = config.get().performance.throttle
|
||||
|
||||
-- Check invalid condition.
|
||||
local ignore = false
|
||||
ignore = ignore or not api.is_suitable_mode()
|
||||
if ignore then
|
||||
return
|
||||
end
|
||||
|
||||
-- Check fetching sources.
|
||||
local sources = {}
|
||||
for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do
|
||||
-- Reserve filter call for timeout.
|
||||
if not s.incomplete and config.get().performance.fetching_timeout > s:get_fetching_time() then
|
||||
self.filter.timeout = config.get().performance.fetching_timeout - s:get_fetching_time()
|
||||
self:filter()
|
||||
if #sources == 0 then
|
||||
return
|
||||
end
|
||||
end
|
||||
table.insert(sources, s)
|
||||
end
|
||||
|
||||
local ctx = self:get_context()
|
||||
|
||||
-- Display completion results.
|
||||
local did_open = self.view:open(ctx, sources)
|
||||
local fetching = #self:get_sources(function(s)
|
||||
return s.status == source.SourceStatus.FETCHING
|
||||
end)
|
||||
|
||||
-- Check onetime config.
|
||||
if not did_open and fetching == 0 then
|
||||
config.set_onetime({})
|
||||
end
|
||||
end)
|
||||
core.filter = async.throttle(async_filter, config.get().performance.throttle)
|
||||
|
||||
---Confirm completion.
|
||||
---@param e cmp.Entry
|
||||
---@param option cmp.ConfirmOption
|
||||
---@param callback function
|
||||
core.confirm = function(self, e, option, callback)
|
||||
if not (e and not e.confirmed) then
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
return
|
||||
end
|
||||
e.confirmed = true
|
||||
|
||||
debug.log('entry.confirm', e:get_completion_item())
|
||||
|
||||
async.sync(function(done)
|
||||
e:resolve(done)
|
||||
end, config.get().performance.confirm_resolve_timeout)
|
||||
|
||||
local release = self:suspend()
|
||||
|
||||
-- Close menus.
|
||||
self.view:close()
|
||||
|
||||
feedkeys.call(keymap.indentkeys(), 'n')
|
||||
feedkeys.call('', 'n', function()
|
||||
-- Emulate `<C-y>` behavior to save `.` register.
|
||||
local ctx = context.new()
|
||||
local keys = {}
|
||||
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset())))
|
||||
table.insert(keys, e:get_word())
|
||||
table.insert(keys, keymap.undobreak())
|
||||
feedkeys.call(table.concat(keys, ''), 'in')
|
||||
end)
|
||||
feedkeys.call('', 'n', function()
|
||||
-- Restore the line at the time of request.
|
||||
local ctx = context.new()
|
||||
if api.is_cmdline_mode() then
|
||||
local keys = {}
|
||||
table.insert(keys, keymap.backspace(ctx.cursor_before_line:sub(e:get_offset())))
|
||||
table.insert(keys, string.sub(e.context.cursor_before_line, e:get_offset()))
|
||||
feedkeys.call(table.concat(keys, ''), 'in')
|
||||
else
|
||||
vim.cmd([[silent! undojoin]])
|
||||
-- This logic must be used nvim_buf_set_text.
|
||||
-- If not used, the snippet engine's placeholder wil be broken.
|
||||
vim.api.nvim_buf_set_text(0, e.context.cursor.row - 1, e:get_offset() - 1, ctx.cursor.row - 1, ctx.cursor.col - 1, {
|
||||
e.context.cursor_before_line:sub(e:get_offset()),
|
||||
})
|
||||
vim.api.nvim_win_set_cursor(0, { e.context.cursor.row, e.context.cursor.col - 1 })
|
||||
end
|
||||
end)
|
||||
feedkeys.call('', 'n', function()
|
||||
-- Apply additionalTextEdits.
|
||||
local ctx = context.new()
|
||||
if #(e:get_completion_item().additionalTextEdits or {}) == 0 then
|
||||
e:resolve(function()
|
||||
local new = context.new()
|
||||
local text_edits = e:get_completion_item().additionalTextEdits or {}
|
||||
if #text_edits == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local has_cursor_line_text_edit = (function()
|
||||
local minrow = math.min(ctx.cursor.row, new.cursor.row)
|
||||
local maxrow = math.max(ctx.cursor.row, new.cursor.row)
|
||||
for _, te in ipairs(text_edits) do
|
||||
local srow = te.range.start.line + 1
|
||||
local erow = te.range['end'].line + 1
|
||||
if srow <= minrow and maxrow <= erow then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end)()
|
||||
if has_cursor_line_text_edit then
|
||||
return
|
||||
end
|
||||
vim.cmd([[silent! undojoin]])
|
||||
vim.lsp.util.apply_text_edits(text_edits, ctx.bufnr, e.source:get_position_encoding_kind())
|
||||
end)
|
||||
else
|
||||
vim.cmd([[silent! undojoin]])
|
||||
vim.lsp.util.apply_text_edits(e:get_completion_item().additionalTextEdits, ctx.bufnr, e.source:get_position_encoding_kind())
|
||||
end
|
||||
end)
|
||||
feedkeys.call('', 'n', function()
|
||||
local ctx = context.new()
|
||||
local completion_item = misc.copy(e:get_completion_item())
|
||||
if not completion_item.textEdit then
|
||||
completion_item.textEdit = {}
|
||||
local insertText = completion_item.insertText
|
||||
if misc.empty(insertText) then
|
||||
insertText = nil
|
||||
end
|
||||
completion_item.textEdit.newText = insertText or completion_item.word or completion_item.label
|
||||
end
|
||||
local behavior = option.behavior or config.get().confirmation.default_behavior
|
||||
if behavior == types.cmp.ConfirmBehavior.Replace then
|
||||
completion_item.textEdit.range = e:get_replace_range()
|
||||
else
|
||||
completion_item.textEdit.range = e:get_insert_range()
|
||||
end
|
||||
|
||||
local diff_before = math.max(0, e.context.cursor.col - (completion_item.textEdit.range.start.character + 1))
|
||||
local diff_after = math.max(0, (completion_item.textEdit.range['end'].character + 1) - e.context.cursor.col)
|
||||
local new_text = completion_item.textEdit.newText
|
||||
completion_item.textEdit.range.start.line = ctx.cursor.line
|
||||
completion_item.textEdit.range.start.character = (ctx.cursor.col - 1) - diff_before
|
||||
completion_item.textEdit.range['end'].line = ctx.cursor.line
|
||||
completion_item.textEdit.range['end'].character = (ctx.cursor.col - 1) + diff_after
|
||||
if api.is_insert_mode() then
|
||||
if false then
|
||||
--To use complex expansion debug.
|
||||
vim.print({ -- luacheck: ignore
|
||||
item = e:get_completion_item(),
|
||||
diff_before = diff_before,
|
||||
diff_after = diff_after,
|
||||
new_text = new_text,
|
||||
text_edit_new_text = completion_item.textEdit.newText,
|
||||
range_start = completion_item.textEdit.range.start.character,
|
||||
range_end = completion_item.textEdit.range['end'].character,
|
||||
original_range_start = e:get_completion_item().textEdit.range.start.character,
|
||||
original_range_end = e:get_completion_item().textEdit.range['end'].character,
|
||||
cursor_line = ctx.cursor_line,
|
||||
cursor_col0 = ctx.cursor.col - 1,
|
||||
})
|
||||
end
|
||||
local is_snippet = completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet
|
||||
if is_snippet then
|
||||
completion_item.textEdit.newText = ''
|
||||
end
|
||||
vim.lsp.util.apply_text_edits({ completion_item.textEdit }, ctx.bufnr, 'utf-8')
|
||||
|
||||
local texts = vim.split(completion_item.textEdit.newText, '\n')
|
||||
vim.api.nvim_win_set_cursor(0, {
|
||||
completion_item.textEdit.range.start.line + #texts,
|
||||
(#texts == 1 and (completion_item.textEdit.range.start.character + #texts[1]) or #texts[#texts]),
|
||||
})
|
||||
if is_snippet then
|
||||
config.get().snippet.expand({
|
||||
body = new_text,
|
||||
insert_text_mode = completion_item.insertTextMode,
|
||||
})
|
||||
end
|
||||
else
|
||||
local keys = {}
|
||||
table.insert(keys, keymap.backspace(ctx.cursor_line:sub(completion_item.textEdit.range.start.character + 1, ctx.cursor.col - 1)))
|
||||
table.insert(keys, keymap.delete(ctx.cursor_line:sub(ctx.cursor.col, completion_item.textEdit.range['end'].character)))
|
||||
table.insert(keys, new_text)
|
||||
feedkeys.call(table.concat(keys, ''), 'in')
|
||||
end
|
||||
end)
|
||||
feedkeys.call(keymap.indentkeys(vim.bo.indentkeys), 'n')
|
||||
feedkeys.call('', 'n', function()
|
||||
e:execute(vim.schedule_wrap(function()
|
||||
release()
|
||||
self.event:emit('confirm_done', {
|
||||
entry = e,
|
||||
commit_character = option.commit_character,
|
||||
})
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
end))
|
||||
end)
|
||||
end
|
||||
|
||||
---Reset current completion state
|
||||
core.reset = function(self)
|
||||
for _, s in pairs(self.sources) do
|
||||
s:reset()
|
||||
end
|
||||
self.context = context.empty()
|
||||
end
|
||||
|
||||
return core
|
||||
231
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/core_spec.lua
Normal file
231
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/core_spec.lua
Normal file
@ -0,0 +1,231 @@
|
||||
local spec = require('cmp.utils.spec')
|
||||
local feedkeys = require('cmp.utils.feedkeys')
|
||||
local types = require('cmp.types')
|
||||
local core = require('cmp.core')
|
||||
local source = require('cmp.source')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local api = require('cmp.utils.api')
|
||||
|
||||
describe('cmp.core', function()
|
||||
describe('confirm', function()
|
||||
---@param request string
|
||||
---@param filter string
|
||||
---@param completion_item lsp.CompletionItem
|
||||
---@param option? { position_encoding_kind: lsp.PositionEncodingKind }
|
||||
---@return table
|
||||
local confirm = function(request, filter, completion_item, option)
|
||||
option = option or {}
|
||||
|
||||
local c = core.new()
|
||||
local s = source.new('spec', {
|
||||
get_position_encoding_kind = function()
|
||||
return option.position_encoding_kind or types.lsp.PositionEncodingKind.UTF16
|
||||
end,
|
||||
complete = function(_, _, callback)
|
||||
callback({ completion_item })
|
||||
end,
|
||||
})
|
||||
c:register_source(s)
|
||||
feedkeys.call(request, 'n', function()
|
||||
c:complete(c:get_context({ reason = types.cmp.ContextReason.Manual }))
|
||||
vim.wait(5000, function()
|
||||
return #c.sources[s.id].entries > 0
|
||||
end)
|
||||
end)
|
||||
feedkeys.call(filter, 'n', function()
|
||||
c:confirm(c.sources[s.id].entries[1], {}, function() end)
|
||||
end)
|
||||
local state = {}
|
||||
feedkeys.call('', 'x', function()
|
||||
feedkeys.call('', 'n', function()
|
||||
if api.is_cmdline_mode() then
|
||||
state.buffer = { api.get_current_line() }
|
||||
else
|
||||
state.buffer = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||
end
|
||||
state.cursor = api.get_cursor()
|
||||
end)
|
||||
end)
|
||||
return state
|
||||
end
|
||||
|
||||
describe('insert-mode', function()
|
||||
before_each(spec.before)
|
||||
|
||||
it('label', function()
|
||||
local state = confirm('iA', 'IU', {
|
||||
label = 'AIUEO',
|
||||
})
|
||||
assert.are.same(state.buffer, { 'AIUEO' })
|
||||
assert.are.same(state.cursor, { 1, 5 })
|
||||
end)
|
||||
|
||||
it('insertText', function()
|
||||
local state = confirm('iA', 'IU', {
|
||||
label = 'AIUEO',
|
||||
insertText = '_AIUEO_',
|
||||
})
|
||||
assert.are.same(state.buffer, { '_AIUEO_' })
|
||||
assert.are.same(state.cursor, { 1, 7 })
|
||||
end)
|
||||
|
||||
it('textEdit', function()
|
||||
local state = confirm(keymap.t('i***AEO***<Left><Left><Left><Left><Left>'), 'IU', {
|
||||
label = 'AIUEO',
|
||||
textEdit = {
|
||||
range = {
|
||||
start = {
|
||||
line = 0,
|
||||
character = 3,
|
||||
},
|
||||
['end'] = {
|
||||
line = 0,
|
||||
character = 6,
|
||||
},
|
||||
},
|
||||
newText = 'foo\nbar\nbaz',
|
||||
},
|
||||
})
|
||||
assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' })
|
||||
assert.are.same(state.cursor, { 3, 3 })
|
||||
end)
|
||||
|
||||
it('#1552', function()
|
||||
local state = confirm(keymap.t('ios.'), '', {
|
||||
filterText = 'IsPermission',
|
||||
insertTextFormat = 2,
|
||||
label = 'IsPermission',
|
||||
textEdit = {
|
||||
newText = 'IsPermission($0)',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 3,
|
||||
line = 0,
|
||||
},
|
||||
start = {
|
||||
character = 3,
|
||||
line = 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.same(state.buffer, { 'os.IsPermission()' })
|
||||
assert.are.same(state.cursor, { 1, 16 })
|
||||
end)
|
||||
|
||||
it('insertText & snippet', function()
|
||||
local state = confirm('iA', 'IU', {
|
||||
label = 'AIUEO',
|
||||
insertText = 'AIUEO($0)',
|
||||
insertTextFormat = types.lsp.InsertTextFormat.Snippet,
|
||||
})
|
||||
assert.are.same(state.buffer, { 'AIUEO()' })
|
||||
assert.are.same(state.cursor, { 1, 6 })
|
||||
end)
|
||||
|
||||
it('textEdit & snippet', function()
|
||||
local state = confirm(keymap.t('i***AEO***<Left><Left><Left><Left><Left>'), 'IU', {
|
||||
label = 'AIUEO',
|
||||
insertTextFormat = types.lsp.InsertTextFormat.Snippet,
|
||||
textEdit = {
|
||||
range = {
|
||||
start = {
|
||||
line = 0,
|
||||
character = 3,
|
||||
},
|
||||
['end'] = {
|
||||
line = 0,
|
||||
character = 6,
|
||||
},
|
||||
},
|
||||
newText = 'foo\nba$0r\nbaz',
|
||||
},
|
||||
})
|
||||
assert.are.same(state.buffer, { '***foo', 'bar', 'baz***' })
|
||||
assert.are.same(state.cursor, { 2, 2 })
|
||||
end)
|
||||
|
||||
local char = '🗿'
|
||||
for _, case in ipairs({
|
||||
{
|
||||
encoding = types.lsp.PositionEncodingKind.UTF8,
|
||||
char_size = #char,
|
||||
},
|
||||
{
|
||||
encoding = types.lsp.PositionEncodingKind.UTF16,
|
||||
char_size = select(2, vim.str_utfindex(char)),
|
||||
},
|
||||
{
|
||||
encoding = types.lsp.PositionEncodingKind.UTF32,
|
||||
char_size = select(1, vim.str_utfindex(char)),
|
||||
},
|
||||
}) do
|
||||
it('textEdit & multibyte: ' .. case.encoding, function()
|
||||
local state = confirm(keymap.t('i%s:%s%s:%s<Left><Left><Left>'):format(char, char, char, char), char, {
|
||||
label = char .. char .. char,
|
||||
textEdit = {
|
||||
range = {
|
||||
start = {
|
||||
line = 0,
|
||||
character = case.char_size + #':',
|
||||
},
|
||||
['end'] = {
|
||||
line = 0,
|
||||
character = case.char_size + #':' + case.char_size + case.char_size,
|
||||
},
|
||||
},
|
||||
newText = char .. char .. char .. char .. char,
|
||||
},
|
||||
}, {
|
||||
position_encoding_kind = case.encoding,
|
||||
})
|
||||
vim.print({ state = state, case = case })
|
||||
assert.are.same(state.buffer, { ('%s:%s%s%s%s%s:%s'):format(char, char, char, char, char, char, char) })
|
||||
assert.are.same(state.cursor, { 1, #('%s:%s%s%s%s%s'):format(char, char, char, char, char, char) })
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
describe('cmdline-mode', function()
|
||||
before_each(spec.before)
|
||||
|
||||
it('label', function()
|
||||
local state = confirm(':A', 'IU', {
|
||||
label = 'AIUEO',
|
||||
})
|
||||
assert.are.same(state.buffer, { 'AIUEO' })
|
||||
assert.are.same(state.cursor[2], 5)
|
||||
end)
|
||||
|
||||
it('insertText', function()
|
||||
local state = confirm(':A', 'IU', {
|
||||
label = 'AIUEO',
|
||||
insertText = '_AIUEO_',
|
||||
})
|
||||
assert.are.same(state.buffer, { '_AIUEO_' })
|
||||
assert.are.same(state.cursor[2], 7)
|
||||
end)
|
||||
|
||||
it('textEdit', function()
|
||||
local state = confirm(keymap.t(':***AEO***<Left><Left><Left><Left><Left>'), 'IU', {
|
||||
label = 'AIUEO',
|
||||
textEdit = {
|
||||
range = {
|
||||
start = {
|
||||
line = 0,
|
||||
character = 3,
|
||||
},
|
||||
['end'] = {
|
||||
line = 0,
|
||||
character = 6,
|
||||
},
|
||||
},
|
||||
newText = 'AIUEO',
|
||||
},
|
||||
})
|
||||
assert.are.same(state.buffer, { '***AIUEO***' })
|
||||
assert.are.same(state.cursor[2], 6)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
583
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/entry.lua
Normal file
583
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/entry.lua
Normal file
@ -0,0 +1,583 @@
|
||||
local cache = require('cmp.utils.cache')
|
||||
local char = require('cmp.utils.char')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local str = require('cmp.utils.str')
|
||||
local snippet = require('cmp.utils.snippet')
|
||||
local config = require('cmp.config')
|
||||
local types = require('cmp.types')
|
||||
local matcher = require('cmp.matcher')
|
||||
|
||||
---@class cmp.Entry
|
||||
---@field public id integer
|
||||
---@field public cache cmp.Cache
|
||||
---@field public match_cache cmp.Cache
|
||||
---@field public score integer
|
||||
---@field public exact boolean
|
||||
---@field public matches table
|
||||
---@field public context cmp.Context
|
||||
---@field public source cmp.Source
|
||||
---@field public source_offset integer
|
||||
---@field public source_insert_range lsp.Range
|
||||
---@field public source_replace_range lsp.Range
|
||||
---@field public completion_item lsp.CompletionItem
|
||||
---@field public item_defaults? lsp.internal.CompletionItemDefaults
|
||||
---@field public resolved_completion_item lsp.CompletionItem|nil
|
||||
---@field public resolved_callbacks fun()[]
|
||||
---@field public resolving boolean
|
||||
---@field public confirmed boolean
|
||||
local entry = {}
|
||||
|
||||
---Create new entry
|
||||
---@param ctx cmp.Context
|
||||
---@param source cmp.Source
|
||||
---@param completion_item lsp.CompletionItem
|
||||
---@param item_defaults? lsp.internal.CompletionItemDefaults
|
||||
---@return cmp.Entry
|
||||
entry.new = function(ctx, source, completion_item, item_defaults)
|
||||
local self = setmetatable({}, { __index = entry })
|
||||
self.id = misc.id('entry.new')
|
||||
self.cache = cache.new()
|
||||
self.match_cache = cache.new()
|
||||
self.score = 0
|
||||
self.exact = false
|
||||
self.matches = {}
|
||||
self.context = ctx
|
||||
self.source = source
|
||||
self.source_offset = source.request_offset
|
||||
self.source_insert_range = source:get_default_insert_range()
|
||||
self.source_replace_range = source:get_default_replace_range()
|
||||
self.completion_item = self:fill_defaults(completion_item, item_defaults)
|
||||
self.item_defaults = item_defaults
|
||||
self.resolved_completion_item = nil
|
||||
self.resolved_callbacks = {}
|
||||
self.resolving = false
|
||||
self.confirmed = false
|
||||
return self
|
||||
end
|
||||
|
||||
---Make offset value
|
||||
---@return integer
|
||||
entry.get_offset = function(self)
|
||||
return self.cache:ensure('get_offset', function()
|
||||
local offset = self.source_offset
|
||||
if self:get_completion_item().textEdit then
|
||||
local range = self:get_insert_range()
|
||||
if range then
|
||||
offset = self.context.cache:ensure('entry:' .. 'get_offset:' .. tostring(range.start.character), function()
|
||||
local start = math.min(range.start.character + 1, offset)
|
||||
for idx = start, self.source_offset do
|
||||
local byte = string.byte(self.context.cursor_line, idx)
|
||||
if byte == nil or not char.is_white(byte) then
|
||||
return idx
|
||||
end
|
||||
end
|
||||
return offset
|
||||
end)
|
||||
end
|
||||
else
|
||||
-- NOTE
|
||||
-- The VSCode does not implement this but it's useful if the server does not care about word patterns.
|
||||
-- We should care about this performance.
|
||||
local word = self:get_word()
|
||||
for idx = self.source_offset - 1, self.source_offset - #word, -1 do
|
||||
if char.is_semantic_index(self.context.cursor_line, idx) then
|
||||
local c = string.byte(self.context.cursor_line, idx)
|
||||
if char.is_white(c) then
|
||||
break
|
||||
end
|
||||
local match = true
|
||||
for i = 1, self.source_offset - idx do
|
||||
local c1 = string.byte(word, i)
|
||||
local c2 = string.byte(self.context.cursor_line, idx + i - 1)
|
||||
if not c1 or not c2 or c1 ~= c2 then
|
||||
match = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if match then
|
||||
offset = math.min(offset, idx)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return offset
|
||||
end)
|
||||
end
|
||||
|
||||
---Create word for vim.CompletedItem
|
||||
---NOTE: This method doesn't clear the cache after completionItem/resolve.
|
||||
---@return string
|
||||
entry.get_word = function(self)
|
||||
return self.cache:ensure('get_word', function()
|
||||
--NOTE: This is nvim-cmp specific implementation.
|
||||
if self:get_completion_item().word then
|
||||
return self:get_completion_item().word
|
||||
end
|
||||
|
||||
local word
|
||||
if self:get_completion_item().textEdit and not misc.empty(self:get_completion_item().textEdit.newText) then
|
||||
word = str.trim(self:get_completion_item().textEdit.newText)
|
||||
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = tostring(snippet.parse(word))
|
||||
end
|
||||
local overwrite = self:get_overwrite()
|
||||
if 0 < overwrite[2] or self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = str.get_word(word, string.byte(self.context.cursor_after_line, 1), overwrite[1] or 0)
|
||||
end
|
||||
elseif not misc.empty(self:get_completion_item().insertText) then
|
||||
word = str.trim(self:get_completion_item().insertText)
|
||||
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = str.get_word(tostring(snippet.parse(word)))
|
||||
end
|
||||
else
|
||||
word = str.trim(self:get_completion_item().label)
|
||||
end
|
||||
return str.oneline(word)
|
||||
end) --[[@as string]]
|
||||
end
|
||||
|
||||
---Get overwrite information
|
||||
---@return integer[]
|
||||
entry.get_overwrite = function(self)
|
||||
return self.cache:ensure('get_overwrite', function()
|
||||
if self:get_completion_item().textEdit then
|
||||
local range = self:get_insert_range()
|
||||
if range then
|
||||
return self.context.cache:ensure('entry:' .. 'get_overwrite:' .. tostring(range.start.character) .. ':' .. tostring(range['end'].character), function()
|
||||
local vim_start = range.start.character + 1
|
||||
local vim_end = range['end'].character + 1
|
||||
local before = self.context.cursor.col - vim_start
|
||||
local after = vim_end - self.context.cursor.col
|
||||
return { before, after }
|
||||
end)
|
||||
end
|
||||
end
|
||||
return { 0, 0 }
|
||||
end)
|
||||
end
|
||||
|
||||
---Create filter text
|
||||
---@return string
|
||||
entry.get_filter_text = function(self)
|
||||
return self.cache:ensure('get_filter_text', function()
|
||||
local word
|
||||
if self:get_completion_item().filterText then
|
||||
word = self:get_completion_item().filterText
|
||||
else
|
||||
word = str.trim(self:get_completion_item().label)
|
||||
end
|
||||
return word
|
||||
end)
|
||||
end
|
||||
|
||||
---Get LSP's insert text
|
||||
---@return string
|
||||
entry.get_insert_text = function(self)
|
||||
return self.cache:ensure('get_insert_text', function()
|
||||
local word
|
||||
if self:get_completion_item().textEdit then
|
||||
word = str.trim(self:get_completion_item().textEdit.newText)
|
||||
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
|
||||
end
|
||||
elseif self:get_completion_item().insertText then
|
||||
word = str.trim(self:get_completion_item().insertText)
|
||||
if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
|
||||
end
|
||||
else
|
||||
word = str.trim(self:get_completion_item().label)
|
||||
end
|
||||
return word
|
||||
end)
|
||||
end
|
||||
|
||||
---Return the item is deprecated or not.
|
||||
---@return boolean
|
||||
entry.is_deprecated = function(self)
|
||||
return self:get_completion_item().deprecated or vim.tbl_contains(self:get_completion_item().tags or {}, types.lsp.CompletionItemTag.Deprecated)
|
||||
end
|
||||
|
||||
---Return view information.
|
||||
---@param suggest_offset integer
|
||||
---@param entries_buf integer The buffer this entry will be rendered into.
|
||||
---@return { abbr: { text: string, bytes: integer, width: integer, hl_group: string }, kind: { text: string, bytes: integer, width: integer, hl_group: string }, menu: { text: string, bytes: integer, width: integer, hl_group: string } }
|
||||
entry.get_view = function(self, suggest_offset, entries_buf)
|
||||
local item = self:get_vim_item(suggest_offset)
|
||||
return self.cache:ensure('get_view:' .. tostring(entries_buf), function()
|
||||
local view = {}
|
||||
-- The result of vim.fn.strdisplaywidth depends on which buffer it was
|
||||
-- called in because it reads the values of the option 'tabstop' when
|
||||
-- rendering <Tab> characters.
|
||||
vim.api.nvim_buf_call(entries_buf, function()
|
||||
view.abbr = {}
|
||||
view.abbr.text = item.abbr or ''
|
||||
view.abbr.bytes = #view.abbr.text
|
||||
view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text)
|
||||
view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr')
|
||||
view.kind = {}
|
||||
view.kind.text = item.kind or ''
|
||||
view.kind.bytes = #view.kind.text
|
||||
view.kind.width = vim.fn.strdisplaywidth(view.kind.text)
|
||||
view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or ''))
|
||||
view.menu = {}
|
||||
view.menu.text = item.menu or ''
|
||||
view.menu.bytes = #view.menu.text
|
||||
view.menu.width = vim.fn.strdisplaywidth(view.menu.text)
|
||||
view.menu.hl_group = item.menu_hl_group or 'CmpItemMenu'
|
||||
view.dup = item.dup
|
||||
end)
|
||||
return view
|
||||
end)
|
||||
end
|
||||
|
||||
---Make vim.CompletedItem
|
||||
---@param suggest_offset integer
|
||||
---@return vim.CompletedItem
|
||||
entry.get_vim_item = function(self, suggest_offset)
|
||||
return self.cache:ensure('get_vim_item:' .. tostring(suggest_offset), function()
|
||||
local completion_item = self:get_completion_item()
|
||||
local word = self:get_word()
|
||||
local abbr = str.oneline(completion_item.label)
|
||||
|
||||
-- ~ indicator
|
||||
local is_expandable = false
|
||||
local expandable_indicator = config.get().formatting.expandable_indicator
|
||||
if #(completion_item.additionalTextEdits or {}) > 0 then
|
||||
is_expandable = true
|
||||
elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
is_expandable = self:get_insert_text() ~= word
|
||||
elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then
|
||||
is_expandable = true
|
||||
end
|
||||
if expandable_indicator and is_expandable then
|
||||
abbr = abbr .. '~'
|
||||
end
|
||||
|
||||
-- append delta text
|
||||
if suggest_offset < self:get_offset() then
|
||||
word = string.sub(self.context.cursor_before_line, suggest_offset, self:get_offset() - 1) .. word
|
||||
end
|
||||
|
||||
-- labelDetails.
|
||||
local menu = nil
|
||||
if completion_item.labelDetails then
|
||||
menu = ''
|
||||
if completion_item.labelDetails.detail then
|
||||
menu = menu .. completion_item.labelDetails.detail
|
||||
end
|
||||
if completion_item.labelDetails.description then
|
||||
menu = menu .. completion_item.labelDetails.description
|
||||
end
|
||||
end
|
||||
|
||||
-- remove duplicated string.
|
||||
if self:get_offset() ~= self.context.cursor.col then
|
||||
for i = 1, #word do
|
||||
if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then
|
||||
word = string.sub(word, 1, i - 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local cmp_opts = self:get_completion_item().cmp or {}
|
||||
|
||||
local vim_item = {
|
||||
word = word,
|
||||
abbr = abbr,
|
||||
kind = cmp_opts.kind_text or types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
|
||||
kind_hl_group = cmp_opts.kind_hl_group,
|
||||
menu = menu,
|
||||
dup = self:get_completion_item().dup or 1,
|
||||
}
|
||||
if config.get().formatting.format then
|
||||
vim_item = config.get().formatting.format(self, vim_item)
|
||||
end
|
||||
vim_item.word = str.oneline(vim_item.word or '')
|
||||
vim_item.abbr = str.oneline(vim_item.abbr or '')
|
||||
vim_item.kind = str.oneline(vim_item.kind or '')
|
||||
vim_item.menu = str.oneline(vim_item.menu or '')
|
||||
vim_item.equal = 1
|
||||
vim_item.empty = 1
|
||||
|
||||
return vim_item
|
||||
end)
|
||||
end
|
||||
|
||||
---Get commit characters
|
||||
---@return string[]
|
||||
entry.get_commit_characters = function(self)
|
||||
return self:get_completion_item().commitCharacters or {}
|
||||
end
|
||||
|
||||
---Return insert range
|
||||
---@return lsp.Range|nil
|
||||
entry.get_insert_range = function(self)
|
||||
local insert_range
|
||||
if self:get_completion_item().textEdit then
|
||||
if self:get_completion_item().textEdit.insert then
|
||||
insert_range = self:get_completion_item().textEdit.insert
|
||||
else
|
||||
insert_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]]
|
||||
end
|
||||
insert_range = self:convert_range_encoding(insert_range)
|
||||
else
|
||||
insert_range = {
|
||||
start = {
|
||||
line = self.context.cursor.row - 1,
|
||||
character = self:get_offset() - 1,
|
||||
},
|
||||
['end'] = self.source_insert_range['end'],
|
||||
}
|
||||
end
|
||||
return insert_range
|
||||
end
|
||||
|
||||
---Return replace range
|
||||
---@return lsp.Range|nil
|
||||
entry.get_replace_range = function(self)
|
||||
return self.cache:ensure('get_replace_range', function()
|
||||
local replace_range
|
||||
if self:get_completion_item().textEdit then
|
||||
if self:get_completion_item().textEdit.replace then
|
||||
replace_range = self:get_completion_item().textEdit.replace
|
||||
else
|
||||
replace_range = self:get_completion_item().textEdit.range --[[@as lsp.Range]]
|
||||
end
|
||||
replace_range = self:convert_range_encoding(replace_range)
|
||||
end
|
||||
|
||||
if not replace_range or ((self.context.cursor.col - 1) == replace_range['end'].character) then
|
||||
replace_range = {
|
||||
start = {
|
||||
line = self.source_replace_range.start.line,
|
||||
character = self:get_offset() - 1,
|
||||
},
|
||||
['end'] = self.source_replace_range['end'],
|
||||
}
|
||||
end
|
||||
return replace_range
|
||||
end)
|
||||
end
|
||||
|
||||
---Match line.
|
||||
---@param input string
|
||||
---@param matching_config cmp.MatchingConfig
|
||||
---@return { score: integer, matches: table[] }
|
||||
entry.match = function(self, input, matching_config)
|
||||
return self.match_cache:ensure(input .. ':' .. (self.resolved_completion_item and '1' or '0' .. ':') .. (matching_config.disallow_fuzzy_matching and '1' or '0') .. ':' .. (matching_config.disallow_partial_fuzzy_matching and '1' or '0') .. ':' .. (matching_config.disallow_partial_matching and '1' or '0') .. ':' .. (matching_config.disallow_prefix_unmatching and '1' or '0') .. ':' .. (matching_config.disallow_symbol_nonprefix_matching and '1' or '0'), function()
|
||||
local option = {
|
||||
disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching,
|
||||
disallow_partial_fuzzy_matching = matching_config.disallow_partial_fuzzy_matching,
|
||||
disallow_partial_matching = matching_config.disallow_partial_matching,
|
||||
disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching,
|
||||
disallow_symbol_nonprefix_matching = matching_config.disallow_symbol_nonprefix_matching,
|
||||
synonyms = {
|
||||
self:get_word(),
|
||||
self:get_completion_item().label,
|
||||
},
|
||||
}
|
||||
|
||||
local score, matches, filter_text, _
|
||||
local checked = {} ---@type table<string, boolean>
|
||||
|
||||
filter_text = self:get_filter_text()
|
||||
checked[filter_text] = true
|
||||
score, matches = matcher.match(input, filter_text, option)
|
||||
|
||||
-- Support the language server that doesn't respect VSCode's behaviors.
|
||||
if score == 0 then
|
||||
if self:get_completion_item().textEdit and not misc.empty(self:get_completion_item().textEdit.newText) then
|
||||
local diff = self.source_offset - self:get_offset()
|
||||
if diff > 0 then
|
||||
local prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff)
|
||||
local accept = nil
|
||||
accept = accept or string.match(prefix, '^[^%a]+$')
|
||||
accept = accept or string.find(self:get_completion_item().textEdit.newText, prefix, 1, true)
|
||||
if accept then
|
||||
filter_text = prefix .. self:get_filter_text()
|
||||
if not checked[filter_text] then
|
||||
checked[filter_text] = true
|
||||
score, matches = matcher.match(input, filter_text, option)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Fix highlight if filterText is not the same to vim_item.abbr.
|
||||
if score > 0 then
|
||||
local vim_item = self:get_vim_item(self.source_offset)
|
||||
filter_text = vim_item.abbr or vim_item.word
|
||||
if not checked[filter_text] then
|
||||
local diff = self.source_offset - self:get_offset()
|
||||
_, matches = matcher.match(input:sub(1 + diff), filter_text, option)
|
||||
end
|
||||
end
|
||||
|
||||
return { score = score, matches = matches }
|
||||
end)
|
||||
end
|
||||
|
||||
---Get resolved completion item if possible.
|
||||
---@return lsp.CompletionItem
|
||||
entry.get_completion_item = function(self)
|
||||
return self.cache:ensure('get_completion_item', function()
|
||||
if self.resolved_completion_item then
|
||||
-- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts#L588
|
||||
-- @see https://github.com/microsoft/vscode/blob/85eea4a9b2ccc99615e970bf2181edbc1781d0f9/src/vs/base/common/objects.ts#L89
|
||||
-- @see https://github.com/microsoft/vscode/blob/a00f2e64f4fa9a1f774875562e1e9697d7138ed3/src/vs/editor/contrib/suggest/browser/suggest.ts#L147
|
||||
local completion_item = misc.copy(self.completion_item)
|
||||
for k, v in pairs(self.resolved_completion_item) do
|
||||
completion_item[k] = v or completion_item[k]
|
||||
end
|
||||
return completion_item
|
||||
end
|
||||
return self.completion_item
|
||||
end)
|
||||
end
|
||||
|
||||
---Create documentation
|
||||
---@return string[]
|
||||
entry.get_documentation = function(self)
|
||||
local item = self:get_completion_item()
|
||||
|
||||
local documents = {}
|
||||
|
||||
-- detail
|
||||
if item.detail and item.detail ~= '' then
|
||||
local ft = self.context.filetype
|
||||
local dot_index = string.find(ft, '%.')
|
||||
if dot_index ~= nil then
|
||||
ft = string.sub(ft, 0, dot_index - 1)
|
||||
end
|
||||
table.insert(documents, {
|
||||
kind = types.lsp.MarkupKind.Markdown,
|
||||
value = ('```%s\n%s\n```'):format(ft, str.trim(item.detail)),
|
||||
})
|
||||
end
|
||||
|
||||
local documentation = item.documentation
|
||||
if type(documentation) == 'string' and documentation ~= '' then
|
||||
local value = str.trim(documentation)
|
||||
if value ~= '' then
|
||||
table.insert(documents, {
|
||||
kind = types.lsp.MarkupKind.PlainText,
|
||||
value = value,
|
||||
})
|
||||
end
|
||||
elseif type(documentation) == 'table' and not misc.empty(documentation.value) then
|
||||
local value = str.trim(documentation.value)
|
||||
if value ~= '' then
|
||||
table.insert(documents, {
|
||||
kind = documentation.kind,
|
||||
value = value,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return vim.lsp.util.convert_input_to_markdown_lines(documents)
|
||||
end
|
||||
|
||||
---Get completion item kind
|
||||
---@return lsp.CompletionItemKind
|
||||
entry.get_kind = function(self)
|
||||
return self:get_completion_item().kind or types.lsp.CompletionItemKind.Text
|
||||
end
|
||||
|
||||
---Execute completion item's command.
|
||||
---@param callback fun()
|
||||
entry.execute = function(self, callback)
|
||||
self.source:execute(self:get_completion_item(), callback)
|
||||
end
|
||||
|
||||
---Resolve completion item.
|
||||
---@param callback fun()
|
||||
entry.resolve = function(self, callback)
|
||||
if self.resolved_completion_item then
|
||||
return callback()
|
||||
end
|
||||
table.insert(self.resolved_callbacks, callback)
|
||||
|
||||
if not self.resolving then
|
||||
self.resolving = true
|
||||
self.source:resolve(self.completion_item, function(completion_item)
|
||||
self.resolving = false
|
||||
if not completion_item then
|
||||
return
|
||||
end
|
||||
self.resolved_completion_item = self:fill_defaults(completion_item, self.item_defaults)
|
||||
self.cache:clear()
|
||||
for _, c in ipairs(self.resolved_callbacks) do
|
||||
c()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param completion_item lsp.CompletionItem
|
||||
---@param defaults? lsp.internal.CompletionItemDefaults
|
||||
---@return lsp.CompletionItem
|
||||
entry.fill_defaults = function(_, completion_item, defaults)
|
||||
defaults = defaults or {}
|
||||
|
||||
if defaults.data then
|
||||
completion_item.data = completion_item.data or defaults.data
|
||||
end
|
||||
|
||||
if defaults.commitCharacters then
|
||||
completion_item.commitCharacters = completion_item.commitCharacters or defaults.commitCharacters
|
||||
end
|
||||
|
||||
if defaults.insertTextFormat then
|
||||
completion_item.insertTextFormat = completion_item.insertTextFormat or defaults.insertTextFormat
|
||||
end
|
||||
|
||||
if defaults.insertTextMode then
|
||||
completion_item.insertTextMode = completion_item.insertTextMode or defaults.insertTextMode
|
||||
end
|
||||
|
||||
if defaults.editRange then
|
||||
if not completion_item.textEdit then
|
||||
if defaults.editRange.insert then
|
||||
completion_item.textEdit = {
|
||||
insert = defaults.editRange.insert,
|
||||
replace = defaults.editRange.replace,
|
||||
newText = completion_item.textEditText or completion_item.label,
|
||||
}
|
||||
else
|
||||
completion_item.textEdit = {
|
||||
range = defaults.editRange, --[[@as lsp.Range]]
|
||||
newText = completion_item.textEditText or completion_item.label,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return completion_item
|
||||
end
|
||||
|
||||
---Convert the oneline range encoding.
|
||||
entry.convert_range_encoding = function(self, range)
|
||||
local from_encoding = self.source:get_position_encoding_kind()
|
||||
return self.context.cache:ensure('entry.convert_range_encoding:' .. range.start.character .. ':' .. range['end'].character .. ':' .. from_encoding, function()
|
||||
return {
|
||||
start = types.lsp.Position.to_utf8(self.context.cursor_line, range.start, from_encoding),
|
||||
['end'] = types.lsp.Position.to_utf8(self.context.cursor_line, range['end'], from_encoding),
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
---Return true if the entry is invalid.
|
||||
entry.is_invalid = function(self)
|
||||
local is_invalid = false
|
||||
is_invalid = is_invalid or misc.empty(self.completion_item.label)
|
||||
if self.completion_item.textEdit then
|
||||
local range = self.completion_item.textEdit.range or self.completion_item.textEdit.insert
|
||||
is_invalid = is_invalid or range.start.line ~= range['end'].line or range.start.line ~= self.context.cursor.line
|
||||
end
|
||||
return is_invalid
|
||||
end
|
||||
|
||||
return entry
|
||||
366
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/entry_spec.lua
Normal file
366
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/entry_spec.lua
Normal file
@ -0,0 +1,366 @@
|
||||
local spec = require('cmp.utils.spec')
|
||||
|
||||
local entry = require('cmp.entry')
|
||||
|
||||
describe('entry', function()
|
||||
before_each(spec.before)
|
||||
|
||||
it('one char', function()
|
||||
local state = spec.state('@.', 1, 3)
|
||||
state.input('@')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
label = '@',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 3)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, '@')
|
||||
end)
|
||||
|
||||
it('word length (no fix)', function()
|
||||
local state = spec.state('a.b', 1, 4)
|
||||
state.input('.')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
label = 'b',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 5)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b')
|
||||
end)
|
||||
|
||||
it('word length (fix)', function()
|
||||
local state = spec.state('a.b', 1, 4)
|
||||
state.input('.')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
label = 'b.',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 3)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'b.')
|
||||
end)
|
||||
|
||||
it('semantic index (no fix)', function()
|
||||
local state = spec.state('a.bc', 1, 5)
|
||||
state.input('.')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
label = 'c.',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 6)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'c.')
|
||||
end)
|
||||
|
||||
it('semantic index (fix)', function()
|
||||
local state = spec.state('a.bc', 1, 5)
|
||||
state.input('.')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
label = 'bc.',
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 3)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'bc.')
|
||||
end)
|
||||
|
||||
it('[vscode-html-language-server] 1', function()
|
||||
local state = spec.state(' </>', 1, 7)
|
||||
state.input('.')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
label = '/div',
|
||||
textEdit = {
|
||||
range = {
|
||||
start = {
|
||||
line = 0,
|
||||
character = 0,
|
||||
},
|
||||
['end'] = {
|
||||
line = 0,
|
||||
character = 6,
|
||||
},
|
||||
},
|
||||
newText = ' </div',
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 5)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, '</div')
|
||||
end)
|
||||
|
||||
it('[clangd] 1', function()
|
||||
--NOTE: clangd does not return `.foo` as filterText but we should care about it.
|
||||
--nvim-cmp does care it by special handling in entry.lua.
|
||||
local state = spec.state('foo', 1, 4)
|
||||
state.input('.')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
insertText = '->foo',
|
||||
label = ' foo',
|
||||
textEdit = {
|
||||
newText = '->foo',
|
||||
range = {
|
||||
start = {
|
||||
character = 3,
|
||||
line = 1,
|
||||
},
|
||||
['end'] = {
|
||||
character = 4,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(4).word, '->foo')
|
||||
assert.are.equal(e:get_filter_text(), 'foo')
|
||||
end)
|
||||
|
||||
it('[typescript-language-server] 1', function()
|
||||
local state = spec.state('Promise.resolve()', 1, 18)
|
||||
state.input('.')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
label = 'catch',
|
||||
})
|
||||
-- The offset will be 18 in this situation because the server returns `[Symbol]` as candidate.
|
||||
assert.are.equal(e:get_vim_item(18).word, '.catch')
|
||||
assert.are.equal(e:get_filter_text(), 'catch')
|
||||
end)
|
||||
|
||||
it('[typescript-language-server] 2', function()
|
||||
local state = spec.state('Promise.resolve()', 1, 18)
|
||||
state.input('.')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
filterText = '.Symbol',
|
||||
label = 'Symbol',
|
||||
textEdit = {
|
||||
newText = '[Symbol]',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 18,
|
||||
line = 0,
|
||||
},
|
||||
start = {
|
||||
character = 17,
|
||||
line = 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(18).word, '[Symbol]')
|
||||
assert.are.equal(e:get_filter_text(), '.Symbol')
|
||||
end)
|
||||
|
||||
it('[lua-language-server] 1', function()
|
||||
local state = spec.state("local m = require'cmp.confi", 1, 28)
|
||||
local e
|
||||
|
||||
-- press g
|
||||
state.input('g')
|
||||
e = entry.new(state.manual(), state.source(), {
|
||||
insertTextFormat = 2,
|
||||
label = 'cmp.config',
|
||||
textEdit = {
|
||||
newText = 'cmp.config',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 27,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 18,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(19).word, 'cmp.config')
|
||||
assert.are.equal(e:get_filter_text(), 'cmp.config')
|
||||
|
||||
-- press '
|
||||
state.input("'")
|
||||
e = entry.new(state.manual(), state.source(), {
|
||||
insertTextFormat = 2,
|
||||
label = 'cmp.config',
|
||||
textEdit = {
|
||||
newText = 'cmp.config',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 27,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 18,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(19).word, 'cmp.config')
|
||||
assert.are.equal(e:get_filter_text(), 'cmp.config')
|
||||
end)
|
||||
|
||||
it('[lua-language-server] 2', function()
|
||||
local state = spec.state("local m = require'cmp.confi", 1, 28)
|
||||
local e
|
||||
|
||||
-- press g
|
||||
state.input('g')
|
||||
e = entry.new(state.manual(), state.source(), {
|
||||
insertTextFormat = 2,
|
||||
label = 'lua.cmp.config',
|
||||
textEdit = {
|
||||
newText = 'lua.cmp.config',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 27,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 18,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config')
|
||||
assert.are.equal(e:get_filter_text(), 'lua.cmp.config')
|
||||
|
||||
-- press '
|
||||
state.input("'")
|
||||
e = entry.new(state.manual(), state.source(), {
|
||||
insertTextFormat = 2,
|
||||
label = 'lua.cmp.config',
|
||||
textEdit = {
|
||||
newText = 'lua.cmp.config',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 27,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 18,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(19).word, 'lua.cmp.config')
|
||||
assert.are.equal(e:get_filter_text(), 'lua.cmp.config')
|
||||
end)
|
||||
|
||||
it('[intelephense] 1', function()
|
||||
local state = spec.state('\t\t', 1, 4)
|
||||
|
||||
-- press g
|
||||
state.input('$')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
kind = 6,
|
||||
label = '$this',
|
||||
sortText = '$this',
|
||||
textEdit = {
|
||||
newText = '$this',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 3,
|
||||
line = 1,
|
||||
},
|
||||
start = {
|
||||
character = 2,
|
||||
line = 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, '$this')
|
||||
assert.are.equal(e:get_filter_text(), '$this')
|
||||
end)
|
||||
|
||||
it('[odin-language-server] 1', function()
|
||||
local state = spec.state('\t\t', 1, 4)
|
||||
|
||||
-- press g
|
||||
state.input('s')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
additionalTextEdits = {},
|
||||
command = {
|
||||
arguments = {},
|
||||
command = '',
|
||||
title = '',
|
||||
},
|
||||
deprecated = false,
|
||||
detail = 'string',
|
||||
documentation = '',
|
||||
insertText = '',
|
||||
insertTextFormat = 1,
|
||||
kind = 14,
|
||||
label = 'string',
|
||||
tags = {},
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'string')
|
||||
end)
|
||||
|
||||
it('[#47] word should not contain \\n character', function()
|
||||
local state = spec.state('', 1, 1)
|
||||
|
||||
-- press g
|
||||
state.input('_')
|
||||
local e = entry.new(state.manual(), state.source(), {
|
||||
kind = 6,
|
||||
label = '__init__',
|
||||
insertTextFormat = 1,
|
||||
insertText = '__init__(self) -> None:\n pass',
|
||||
})
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, '__init__(self) -> None:')
|
||||
assert.are.equal(e:get_filter_text(), '__init__')
|
||||
end)
|
||||
|
||||
-- I can't understand this test case...
|
||||
-- it('[#1533] keyword pattern that include whitespace', function()
|
||||
-- local state = spec.state(' ', 1, 2)
|
||||
-- local state_source = state.source()
|
||||
|
||||
-- state_source.get_keyword_pattern = function(_)
|
||||
-- return '.'
|
||||
-- end
|
||||
|
||||
-- state.input(' ')
|
||||
-- local e = entry.new(state.manual(), state_source, {
|
||||
-- filterText = "constructor() {\n ... st = 'test';\n ",
|
||||
-- kind = 1,
|
||||
-- label = "constructor() {\n ... st = 'test';\n }",
|
||||
-- textEdit = {
|
||||
-- newText = "constructor() {\n this.test = 'test';\n }",
|
||||
-- range = {
|
||||
-- ['end'] = {
|
||||
-- character = 2,
|
||||
-- line = 2,
|
||||
-- },
|
||||
-- start = {
|
||||
-- character = 0,
|
||||
-- line = 2,
|
||||
-- },
|
||||
-- },
|
||||
-- },
|
||||
-- })
|
||||
-- assert.are.equal(e:get_offset(), 2)
|
||||
-- assert.are.equal(e:get_vim_item(e:get_offset()).word, 'constructor() {')
|
||||
-- end)
|
||||
|
||||
it('[#1533] clang regression test', function()
|
||||
local state = spec.state('jsonReader', 3, 11)
|
||||
local state_source = state.source()
|
||||
|
||||
state.input('.')
|
||||
local e = entry.new(state.manual(), state_source, {
|
||||
filterText = 'getPath()',
|
||||
kind = 1,
|
||||
label = 'getPath()',
|
||||
textEdit = {
|
||||
newText = 'getPath()',
|
||||
range = {
|
||||
['end'] = {
|
||||
character = 11,
|
||||
col = 12,
|
||||
line = 2,
|
||||
row = 3,
|
||||
},
|
||||
start = {
|
||||
character = 11,
|
||||
line = 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.are.equal(e:get_offset(), 12)
|
||||
assert.are.equal(e:get_vim_item(e:get_offset()).word, 'getPath()')
|
||||
end)
|
||||
end)
|
||||
375
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/init.lua
Normal file
375
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/init.lua
Normal file
@ -0,0 +1,375 @@
|
||||
local core = require('cmp.core')
|
||||
local source = require('cmp.source')
|
||||
local config = require('cmp.config')
|
||||
local feedkeys = require('cmp.utils.feedkeys')
|
||||
local autocmd = require('cmp.utils.autocmd')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local async = require('cmp.utils.async')
|
||||
|
||||
local cmp = {}
|
||||
|
||||
cmp.core = core.new()
|
||||
|
||||
---Expose types
|
||||
for k, v in pairs(require('cmp.types.cmp')) do
|
||||
cmp[k] = v
|
||||
end
|
||||
cmp.lsp = require('cmp.types.lsp')
|
||||
cmp.vim = require('cmp.types.vim')
|
||||
|
||||
---Expose event
|
||||
cmp.event = cmp.core.event
|
||||
|
||||
---Export mapping for special case
|
||||
cmp.mapping = require('cmp.config.mapping')
|
||||
|
||||
---Export default config presets
|
||||
cmp.config = {}
|
||||
cmp.config.disable = misc.none
|
||||
cmp.config.compare = require('cmp.config.compare')
|
||||
cmp.config.sources = require('cmp.config.sources')
|
||||
cmp.config.mapping = require('cmp.config.mapping')
|
||||
cmp.config.window = require('cmp.config.window')
|
||||
|
||||
---Sync asynchronous process.
|
||||
cmp.sync = function(callback)
|
||||
return function(...)
|
||||
cmp.core.filter:sync(1000)
|
||||
if callback then
|
||||
return callback(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Suspend completion.
|
||||
cmp.suspend = function()
|
||||
return cmp.core:suspend()
|
||||
end
|
||||
|
||||
---Register completion sources
|
||||
---@param name string
|
||||
---@param s cmp.Source
|
||||
---@return integer
|
||||
cmp.register_source = function(name, s)
|
||||
local src = source.new(name, s)
|
||||
cmp.core:register_source(src)
|
||||
return src.id
|
||||
end
|
||||
|
||||
---Unregister completion source
|
||||
---@param id integer
|
||||
cmp.unregister_source = function(id)
|
||||
cmp.core:unregister_source(id)
|
||||
end
|
||||
|
||||
---Get current configuration.
|
||||
---@return cmp.ConfigSchema
|
||||
cmp.get_config = function()
|
||||
return require('cmp.config').get()
|
||||
end
|
||||
|
||||
---Invoke completion manually
|
||||
---@param option cmp.CompleteParams
|
||||
cmp.complete = cmp.sync(function(option)
|
||||
option = option or {}
|
||||
config.set_onetime(option.config)
|
||||
cmp.core:complete(cmp.core:get_context({ reason = option.reason or cmp.ContextReason.Manual }))
|
||||
return true
|
||||
end)
|
||||
|
||||
---Complete common string in current entries.
|
||||
cmp.complete_common_string = cmp.sync(function()
|
||||
return cmp.core:complete_common_string()
|
||||
end)
|
||||
|
||||
---Return view is visible or not.
|
||||
cmp.visible = cmp.sync(function()
|
||||
return cmp.core.view:visible() or vim.fn.pumvisible() == 1
|
||||
end)
|
||||
|
||||
---Get current selected entry or nil
|
||||
cmp.get_selected_entry = cmp.sync(function()
|
||||
return cmp.core.view:get_selected_entry()
|
||||
end)
|
||||
|
||||
---Get current active entry or nil
|
||||
cmp.get_active_entry = cmp.sync(function()
|
||||
return cmp.core.view:get_active_entry()
|
||||
end)
|
||||
|
||||
---Get current all entries
|
||||
cmp.get_entries = cmp.sync(function()
|
||||
return cmp.core.view:get_entries()
|
||||
end)
|
||||
|
||||
---Close current completion
|
||||
cmp.close = cmp.sync(function()
|
||||
if cmp.core.view:visible() then
|
||||
local release = cmp.core:suspend()
|
||||
cmp.core.view:close()
|
||||
cmp.core:reset()
|
||||
vim.schedule(release)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
|
||||
---Abort current completion
|
||||
cmp.abort = cmp.sync(function()
|
||||
if cmp.core.view:visible() then
|
||||
local release = cmp.core:suspend()
|
||||
cmp.core.view:abort()
|
||||
cmp.core:reset()
|
||||
vim.schedule(release)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
|
||||
---Select next item if possible
|
||||
cmp.select_next_item = cmp.sync(function(option)
|
||||
option = option or {}
|
||||
option.behavior = option.behavior or cmp.SelectBehavior.Insert
|
||||
option.count = option.count or 1
|
||||
|
||||
if cmp.core.view:visible() then
|
||||
local release = cmp.core:suspend()
|
||||
cmp.core.view:select_next_item(option)
|
||||
vim.schedule(release)
|
||||
return true
|
||||
elseif vim.fn.pumvisible() == 1 then
|
||||
if option.behavior == cmp.SelectBehavior.Insert then
|
||||
feedkeys.call(keymap.t(string.rep('<C-n>', option.count)), 'in')
|
||||
else
|
||||
feedkeys.call(keymap.t(string.rep('<Down>', option.count)), 'in')
|
||||
end
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end)
|
||||
|
||||
---Select prev item if possible
|
||||
cmp.select_prev_item = cmp.sync(function(option)
|
||||
option = option or {}
|
||||
option.behavior = option.behavior or cmp.SelectBehavior.Insert
|
||||
option.count = option.count or 1
|
||||
|
||||
if cmp.core.view:visible() then
|
||||
local release = cmp.core:suspend()
|
||||
cmp.core.view:select_prev_item(option)
|
||||
vim.schedule(release)
|
||||
return true
|
||||
elseif vim.fn.pumvisible() == 1 then
|
||||
if option.behavior == cmp.SelectBehavior.Insert then
|
||||
feedkeys.call(keymap.t(string.rep('<C-p>', option.count)), 'in')
|
||||
else
|
||||
feedkeys.call(keymap.t(string.rep('<Up>', option.count)), 'in')
|
||||
end
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end)
|
||||
|
||||
---Scrolling documentation window if possible
|
||||
cmp.scroll_docs = cmp.sync(function(delta)
|
||||
if cmp.core.view.docs_view:visible() then
|
||||
cmp.core.view:scroll_docs(delta)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
|
||||
---Whether the documentation window is visible or not.
|
||||
cmp.visible_docs = cmp.sync(function()
|
||||
return cmp.core.view.docs_view:visible()
|
||||
end)
|
||||
|
||||
---Opens the documentation window.
|
||||
cmp.open_docs = cmp.sync(function()
|
||||
if not cmp.visible_docs() then
|
||||
cmp.core.view:open_docs()
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
|
||||
---Closes the documentation window.
|
||||
cmp.close_docs = cmp.sync(function()
|
||||
if cmp.visible_docs() then
|
||||
cmp.core.view:close_docs()
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
|
||||
---Confirm completion
|
||||
cmp.confirm = cmp.sync(function(option, callback)
|
||||
option = option or {}
|
||||
option.select = option.select or false
|
||||
option.behavior = option.behavior or cmp.get_config().confirmation.default_behavior or cmp.ConfirmBehavior.Insert
|
||||
callback = callback or function() end
|
||||
|
||||
if cmp.core.view:visible() then
|
||||
local e = cmp.core.view:get_selected_entry()
|
||||
if not e and option.select then
|
||||
e = cmp.core.view:get_first_entry()
|
||||
end
|
||||
if e then
|
||||
cmp.core:confirm(e, {
|
||||
behavior = option.behavior,
|
||||
}, function()
|
||||
callback()
|
||||
cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly }))
|
||||
end)
|
||||
return true
|
||||
end
|
||||
elseif vim.fn.pumvisible() == 1 then
|
||||
local index = vim.fn.complete_info({ 'selected' }).selected
|
||||
if index == -1 and option.select then
|
||||
index = 0
|
||||
end
|
||||
if index ~= -1 then
|
||||
vim.api.nvim_select_popupmenu_item(index, true, true, {})
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end)
|
||||
|
||||
---Show status
|
||||
cmp.status = function()
|
||||
local kinds = {}
|
||||
kinds.available = {}
|
||||
kinds.unavailable = {}
|
||||
kinds.installed = {}
|
||||
kinds.invalid = {}
|
||||
local names = {}
|
||||
for _, s in pairs(cmp.core.sources) do
|
||||
names[s.name] = true
|
||||
|
||||
if config.get_source_config(s.name) then
|
||||
if s:is_available() then
|
||||
table.insert(kinds.available, s:get_debug_name())
|
||||
else
|
||||
table.insert(kinds.unavailable, s:get_debug_name())
|
||||
end
|
||||
else
|
||||
table.insert(kinds.installed, s:get_debug_name())
|
||||
end
|
||||
end
|
||||
for _, s in ipairs(config.get().sources) do
|
||||
if not names[s.name] then
|
||||
table.insert(kinds.invalid, s.name)
|
||||
end
|
||||
end
|
||||
|
||||
if #kinds.available > 0 then
|
||||
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
|
||||
vim.api.nvim_echo({ { '# ready source names\n', 'Special' } }, false, {})
|
||||
for _, name in ipairs(kinds.available) do
|
||||
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
|
||||
end
|
||||
end
|
||||
|
||||
if #kinds.unavailable > 0 then
|
||||
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
|
||||
vim.api.nvim_echo({ { '# unavailable source names\n', 'Comment' } }, false, {})
|
||||
for _, name in ipairs(kinds.unavailable) do
|
||||
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
|
||||
end
|
||||
end
|
||||
|
||||
if #kinds.installed > 0 then
|
||||
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
|
||||
vim.api.nvim_echo({ { '# unused source names\n', 'WarningMsg' } }, false, {})
|
||||
for _, name in ipairs(kinds.installed) do
|
||||
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
|
||||
end
|
||||
end
|
||||
|
||||
if #kinds.invalid > 0 then
|
||||
vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
|
||||
vim.api.nvim_echo({ { '# unknown source names\n', 'ErrorMsg' } }, false, {})
|
||||
for _, name in ipairs(kinds.invalid) do
|
||||
vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@type cmp.Setup
|
||||
cmp.setup = setmetatable({
|
||||
global = function(c)
|
||||
config.set_global(c)
|
||||
end,
|
||||
filetype = function(filetype, c)
|
||||
config.set_filetype(c, filetype)
|
||||
end,
|
||||
buffer = function(c)
|
||||
config.set_buffer(c, vim.api.nvim_get_current_buf())
|
||||
end,
|
||||
cmdline = function(type, c)
|
||||
config.set_cmdline(c, type)
|
||||
end,
|
||||
}, {
|
||||
__call = function(self, c)
|
||||
self.global(c)
|
||||
end,
|
||||
})
|
||||
|
||||
-- In InsertEnter autocmd, vim will detects mode=normal unexpectedly.
|
||||
local on_insert_enter = function()
|
||||
if config.enabled() then
|
||||
cmp.config.compare.scopes:update()
|
||||
cmp.config.compare.locality:update()
|
||||
cmp.core:prepare()
|
||||
cmp.core:on_change('InsertEnter')
|
||||
end
|
||||
end
|
||||
autocmd.subscribe({ 'CmdlineEnter' }, async.debounce_next_tick(on_insert_enter))
|
||||
autocmd.subscribe({ 'InsertEnter' }, async.debounce_next_tick_by_keymap(on_insert_enter))
|
||||
|
||||
-- async.throttle is needed for performance. The mapping `:<C-u>...<CR>` will fire `CmdlineChanged` for each character.
|
||||
local on_text_changed = function()
|
||||
if config.enabled() then
|
||||
cmp.core:on_change('TextChanged')
|
||||
end
|
||||
end
|
||||
autocmd.subscribe({ 'TextChangedI', 'TextChangedP' }, on_text_changed)
|
||||
autocmd.subscribe('CmdlineChanged', async.debounce_next_tick(on_text_changed))
|
||||
|
||||
autocmd.subscribe('CursorMovedI', function()
|
||||
if config.enabled() then
|
||||
cmp.core:on_moved()
|
||||
else
|
||||
cmp.core:reset()
|
||||
cmp.core.view:close()
|
||||
end
|
||||
end)
|
||||
|
||||
-- If make this asynchronous, the completion menu will not close when the command output is displayed.
|
||||
autocmd.subscribe({ 'InsertLeave', 'CmdlineLeave' }, function()
|
||||
cmp.core:reset()
|
||||
cmp.core.view:close()
|
||||
end)
|
||||
|
||||
cmp.event:on('complete_done', function(evt)
|
||||
if evt.entry then
|
||||
cmp.config.compare.recently_used:add_entry(evt.entry)
|
||||
end
|
||||
cmp.config.compare.scopes:update()
|
||||
cmp.config.compare.locality:update()
|
||||
end)
|
||||
|
||||
cmp.event:on('confirm_done', function(evt)
|
||||
if evt.entry then
|
||||
cmp.config.compare.recently_used:add_entry(evt.entry)
|
||||
end
|
||||
end)
|
||||
|
||||
return cmp
|
||||
348
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/matcher.lua
Normal file
348
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/matcher.lua
Normal file
@ -0,0 +1,348 @@
|
||||
local char = require('cmp.utils.char')
|
||||
|
||||
local matcher = {}
|
||||
|
||||
matcher.WORD_BOUNDALY_ORDER_FACTOR = 10
|
||||
|
||||
matcher.PREFIX_FACTOR = 8
|
||||
matcher.NOT_FUZZY_FACTOR = 6
|
||||
|
||||
---@type function
|
||||
matcher.debug = function(...)
|
||||
return ...
|
||||
end
|
||||
|
||||
--- score
|
||||
--
|
||||
-- ### The score
|
||||
--
|
||||
-- The `score` is `matched char count` generally.
|
||||
--
|
||||
-- But cmp will fix the score with some of the below points so the actual score is not `matched char count`.
|
||||
--
|
||||
-- 1. Word boundary order
|
||||
--
|
||||
-- cmp prefers the match that near by word-beggining.
|
||||
--
|
||||
-- 2. Strict case
|
||||
--
|
||||
-- cmp prefers strict match than ignorecase match.
|
||||
--
|
||||
--
|
||||
-- ### Matching specs.
|
||||
--
|
||||
-- 1. Prefix matching per word boundary
|
||||
--
|
||||
-- `bora` -> `border-radius` # imaginary score: 4
|
||||
-- ^^~~ ^^ ~~
|
||||
--
|
||||
-- 2. Try sequential match first
|
||||
--
|
||||
-- `woroff` -> `word_offset` # imaginary score: 6
|
||||
-- ^^^~~~ ^^^ ~~~
|
||||
--
|
||||
-- * The `woroff`'s second `o` should not match `word_offset`'s first `o`
|
||||
--
|
||||
-- 3. Prefer early word boundary
|
||||
--
|
||||
-- `call` -> `call` # imaginary score: 4.1
|
||||
-- ^^^^ ^^^^
|
||||
-- `call` -> `condition_all` # imaginary score: 4
|
||||
-- ^~~~ ^ ~~~
|
||||
--
|
||||
-- 4. Prefer strict match
|
||||
--
|
||||
-- `Buffer` -> `Buffer` # imaginary score: 6.1
|
||||
-- ^^^^^^ ^^^^^^
|
||||
-- `buffer` -> `Buffer` # imaginary score: 6
|
||||
-- ^^^^^^ ^^^^^^
|
||||
--
|
||||
-- 5. Use remaining characters for substring match
|
||||
--
|
||||
-- `fmodify` -> `fnamemodify` # imaginary score: 1
|
||||
-- ^~~~~~~ ^ ~~~~~~
|
||||
--
|
||||
-- 6. Avoid unexpected match detection
|
||||
--
|
||||
-- `candlesingle` -> candle#accept#single
|
||||
-- ^^^^^^~~~~~~ ^^^^^^ ~~~~~~
|
||||
-- * The `accept`'s `a` should not match to `candle`'s `a`
|
||||
--
|
||||
-- 7. Avoid false positive matching
|
||||
--
|
||||
-- `,` -> print,
|
||||
-- ~
|
||||
-- * Typically, the middle match with symbol characters only is false positive. should be ignored.
|
||||
-- This doesn't work for command line completions like ":b foo_" which we like to match
|
||||
-- "lib/foo_bar.txt". The option disallow_symbol_nonprefix_matching controls this and defaults
|
||||
-- to preventing matches like these. The documentation recommends it for command line completion.
|
||||
--
|
||||
--
|
||||
---Match entry
|
||||
---@param input string
|
||||
---@param word string
|
||||
---@param option { synonyms: string[], disallow_fullfuzzy_matching: boolean, disallow_fuzzy_matching: boolean, disallow_partial_fuzzy_matching: boolean, disallow_partial_matching: boolean, disallow_prefix_unmatching: boolean, disallow_symbol_nonprefix_matching: boolean }
|
||||
---@return integer, table
|
||||
matcher.match = function(input, word, option)
|
||||
option = option or {}
|
||||
|
||||
-- Empty input
|
||||
if #input == 0 then
|
||||
return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR, {}
|
||||
end
|
||||
|
||||
-- Ignore if input is long than word
|
||||
if #input > #word then
|
||||
return 0, {}
|
||||
end
|
||||
|
||||
-- Check prefix matching.
|
||||
if option.disallow_prefix_unmatching then
|
||||
if not char.match(string.byte(input, 1), string.byte(word, 1)) then
|
||||
return 0, {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Gather matched regions
|
||||
local matches = {}
|
||||
local input_start_index = 1
|
||||
local input_end_index = 1
|
||||
local word_index = 1
|
||||
local word_bound_index = 1
|
||||
local no_symbol_match = false
|
||||
while input_end_index <= #input and word_index <= #word do
|
||||
local m = matcher.find_match_region(input, input_start_index, input_end_index, word, word_index)
|
||||
if m and input_end_index <= m.input_match_end then
|
||||
m.index = word_bound_index
|
||||
input_start_index = m.input_match_start + 1
|
||||
input_end_index = m.input_match_end + 1
|
||||
no_symbol_match = no_symbol_match or m.no_symbol_match
|
||||
word_index = char.get_next_semantic_index(word, m.word_match_end)
|
||||
table.insert(matches, m)
|
||||
else
|
||||
word_index = char.get_next_semantic_index(word, word_index)
|
||||
end
|
||||
word_bound_index = word_bound_index + 1
|
||||
end
|
||||
|
||||
-- Check partial matching.
|
||||
if option.disallow_partial_matching and #matches > 1 then
|
||||
return 0, {}
|
||||
end
|
||||
|
||||
if #matches == 0 then
|
||||
if not option.disallow_fuzzy_matching and not option.disallow_prefix_unmatching and not option.disallow_partial_fuzzy_matching then
|
||||
if matcher.fuzzy(input, word, matches, option) then
|
||||
return 1, matches
|
||||
end
|
||||
end
|
||||
return 0, {}
|
||||
end
|
||||
|
||||
matcher.debug(word, matches)
|
||||
|
||||
-- Add prefix bonus
|
||||
local prefix = false
|
||||
if matches[1].input_match_start == 1 and matches[1].word_match_start == 1 then
|
||||
prefix = true
|
||||
else
|
||||
for _, synonym in ipairs(option.synonyms or {}) do
|
||||
prefix = true
|
||||
local o = 1
|
||||
for i = matches[1].input_match_start, matches[1].input_match_end do
|
||||
if not char.match(string.byte(synonym, o), string.byte(input, i)) then
|
||||
prefix = false
|
||||
break
|
||||
end
|
||||
o = o + 1
|
||||
end
|
||||
if prefix then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if no_symbol_match and not prefix then
|
||||
if option.disallow_symbol_nonprefix_matching then
|
||||
return 0, {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Compute prefix match score
|
||||
local score = prefix and matcher.PREFIX_FACTOR or 0
|
||||
local offset = prefix and matches[1].index - 1 or 0
|
||||
local idx = 1
|
||||
for _, m in ipairs(matches) do
|
||||
local s = 0
|
||||
for i = math.max(idx, m.input_match_start), m.input_match_end do
|
||||
s = s + 1
|
||||
idx = i
|
||||
end
|
||||
idx = idx + 1
|
||||
if s > 0 then
|
||||
s = s * (1 + m.strict_ratio)
|
||||
s = s * (1 + math.max(0, matcher.WORD_BOUNDALY_ORDER_FACTOR - (m.index - offset)) / matcher.WORD_BOUNDALY_ORDER_FACTOR)
|
||||
score = score + s
|
||||
end
|
||||
end
|
||||
|
||||
-- Check remaining input as fuzzy
|
||||
if matches[#matches].input_match_end < #input then
|
||||
if not option.disallow_fuzzy_matching then
|
||||
if not option.disallow_partial_fuzzy_matching or prefix then
|
||||
if matcher.fuzzy(input, word, matches, option) then
|
||||
return score, matches
|
||||
end
|
||||
end
|
||||
end
|
||||
return 0, {}
|
||||
end
|
||||
|
||||
return score + matcher.NOT_FUZZY_FACTOR, matches
|
||||
end
|
||||
|
||||
--- fuzzy
|
||||
matcher.fuzzy = function(input, word, matches, option)
|
||||
local input_index = matches[#matches] and (matches[#matches].input_match_end + 1) or 1
|
||||
|
||||
-- Lately specified middle of text.
|
||||
for i = 1, #matches - 1 do
|
||||
local curr_match = matches[i]
|
||||
local next_match = matches[i + 1]
|
||||
local word_offset = 0
|
||||
local word_index = char.get_next_semantic_index(word, curr_match.word_match_end)
|
||||
while word_offset + word_index < next_match.word_match_start and input_index <= #input do
|
||||
if char.match(string.byte(word, word_index + word_offset), string.byte(input, input_index)) then
|
||||
input_index = input_index + 1
|
||||
word_offset = word_offset + 1
|
||||
else
|
||||
word_index = char.get_next_semantic_index(word, word_index + word_offset)
|
||||
word_offset = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Remaining text fuzzy match.
|
||||
local matched = false
|
||||
local word_offset = 0
|
||||
local word_index = matches[#matches] and (matches[#matches].word_match_end + 1) or 1
|
||||
local input_match_start = -1
|
||||
local input_match_end = -1
|
||||
local word_match_start = -1
|
||||
local strict_count = 0
|
||||
local match_count = 0
|
||||
while word_offset + word_index <= #word and input_index <= #input do
|
||||
local c1, c2 = string.byte(word, word_index + word_offset), string.byte(input, input_index)
|
||||
if char.match(c1, c2) then
|
||||
if not matched then
|
||||
input_match_start = input_index
|
||||
word_match_start = word_index + word_offset
|
||||
end
|
||||
matched = true
|
||||
input_index = input_index + 1
|
||||
strict_count = strict_count + (c1 == c2 and 1 or 0)
|
||||
match_count = match_count + 1
|
||||
else
|
||||
if option.disallow_fullfuzzy_matching then
|
||||
break
|
||||
else
|
||||
if matched then
|
||||
table.insert(matches, {
|
||||
input_match_start = input_match_start,
|
||||
input_match_end = input_index - 1,
|
||||
word_match_start = word_match_start,
|
||||
word_match_end = word_index + word_offset - 1,
|
||||
strict_ratio = strict_count / match_count,
|
||||
fuzzy = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
matched = false
|
||||
end
|
||||
word_offset = word_offset + 1
|
||||
end
|
||||
|
||||
if matched and input_index > #input then
|
||||
table.insert(matches, {
|
||||
input_match_start = input_match_start,
|
||||
input_match_end = input_match_end,
|
||||
word_match_start = word_match_start,
|
||||
word_match_end = word_index + word_offset - 1,
|
||||
strict_ratio = strict_count / match_count,
|
||||
fuzzy = true,
|
||||
})
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- find_match_region
|
||||
matcher.find_match_region = function(input, input_start_index, input_end_index, word, word_index)
|
||||
-- determine input position ( woroff -> word_offset )
|
||||
while input_start_index < input_end_index do
|
||||
if char.match(string.byte(input, input_end_index), string.byte(word, word_index)) then
|
||||
break
|
||||
end
|
||||
input_end_index = input_end_index - 1
|
||||
end
|
||||
|
||||
-- Can't determine input position
|
||||
if input_end_index < input_start_index then
|
||||
return nil
|
||||
end
|
||||
|
||||
local input_match_start = -1
|
||||
local input_index = input_end_index
|
||||
local word_offset = 0
|
||||
local strict_count = 0
|
||||
local match_count = 0
|
||||
local no_symbol_match = false
|
||||
while input_index <= #input and word_index + word_offset <= #word do
|
||||
local c1 = string.byte(input, input_index)
|
||||
local c2 = string.byte(word, word_index + word_offset)
|
||||
if char.match(c1, c2) then
|
||||
-- Match start.
|
||||
if input_match_start == -1 then
|
||||
input_match_start = input_index
|
||||
end
|
||||
|
||||
strict_count = strict_count + (c1 == c2 and 1 or 0)
|
||||
match_count = match_count + 1
|
||||
word_offset = word_offset + 1
|
||||
no_symbol_match = no_symbol_match or char.is_symbol(c1)
|
||||
else
|
||||
-- Match end (partial region)
|
||||
if input_match_start ~= -1 then
|
||||
return {
|
||||
input_match_start = input_match_start,
|
||||
input_match_end = input_index - 1,
|
||||
word_match_start = word_index,
|
||||
word_match_end = word_index + word_offset - 1,
|
||||
strict_ratio = strict_count / match_count,
|
||||
no_symbol_match = no_symbol_match,
|
||||
fuzzy = false,
|
||||
}
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
input_index = input_index + 1
|
||||
end
|
||||
|
||||
-- Match end (whole region)
|
||||
if input_match_start ~= -1 then
|
||||
return {
|
||||
input_match_start = input_match_start,
|
||||
input_match_end = input_index - 1,
|
||||
word_match_start = word_index,
|
||||
word_match_end = word_index + word_offset - 1,
|
||||
strict_ratio = strict_count / match_count,
|
||||
no_symbol_match = no_symbol_match,
|
||||
fuzzy = false,
|
||||
}
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
return matcher
|
||||
@ -0,0 +1,105 @@
|
||||
local spec = require('cmp.utils.spec')
|
||||
local default_config = require('cmp.config.default')
|
||||
|
||||
local matcher = require('cmp.matcher')
|
||||
|
||||
describe('matcher', function()
|
||||
before_each(spec.before)
|
||||
|
||||
it('match', function()
|
||||
local config = default_config()
|
||||
assert.is.truthy(matcher.match('', 'a', config.matching) >= 1)
|
||||
assert.is.truthy(matcher.match('a', 'a', config.matching) >= 1)
|
||||
assert.is.truthy(matcher.match('ab', 'a', config.matching) == 0)
|
||||
assert.is.truthy(matcher.match('ab', 'ab', config.matching) > matcher.match('ab', 'a_b', config.matching))
|
||||
assert.is.truthy(matcher.match('ab', 'a_b_c', config.matching) > matcher.match('ac', 'a_b_c', config.matching))
|
||||
|
||||
assert.is.truthy(matcher.match('bora', 'border-radius', config.matching) >= 1)
|
||||
assert.is.truthy(matcher.match('woroff', 'word_offset', config.matching) >= 1)
|
||||
assert.is.truthy(matcher.match('call', 'call', config.matching) > matcher.match('call', 'condition_all', config.matching))
|
||||
assert.is.truthy(matcher.match('Buffer', 'Buffer', config.matching) > matcher.match('Buffer', 'buffer', config.matching))
|
||||
assert.is.truthy(matcher.match('luacon', 'lua_context', config.matching) > matcher.match('luacon', 'LuaContext', config.matching))
|
||||
assert.is.truthy(matcher.match('fmodify', 'fnamemodify', config.matching) >= 1)
|
||||
assert.is.truthy(matcher.match('candlesingle', 'candle#accept#single', config.matching) >= 1)
|
||||
|
||||
assert.is.truthy(matcher.match('vi', 'void#', config.matching) >= 1)
|
||||
assert.is.truthy(matcher.match('vo', 'void#', config.matching) >= 1)
|
||||
assert.is.truthy(matcher.match('var_', 'var_dump', config.matching) >= 1)
|
||||
assert.is.truthy(matcher.match('conso', 'console', config.matching) > matcher.match('conso', 'ConstantSourceNode', config.matching))
|
||||
assert.is.truthy(matcher.match('usela', 'useLayoutEffect', config.matching) > matcher.match('usela', 'useDataLayer', config.matching))
|
||||
assert.is.truthy(matcher.match('my_', 'my_awesome_variable', config.matching) > matcher.match('my_', 'completion_matching_strategy_list', config.matching))
|
||||
assert.is.truthy(matcher.match('2', '[[2021', config.matching) >= 1)
|
||||
|
||||
assert.is.truthy(matcher.match(',', 'pri,', config.matching) == 0)
|
||||
assert.is.truthy(matcher.match('/', '/**', config.matching) >= 1)
|
||||
|
||||
assert.is.truthy(matcher.match('true', 'v:true', { synonyms = { 'true' } }, config.matching) == matcher.match('true', 'true', config.matching))
|
||||
assert.is.truthy(matcher.match('g', 'get', { synonyms = { 'get' } }, config.matching) > matcher.match('g', 'dein#get', { 'dein#get' }, config.matching))
|
||||
|
||||
assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = true }, config.matching) == 0)
|
||||
assert.is.truthy(matcher.match('Unit', 'net.UnixListener', { disallow_partial_fuzzy_matching = false }, config.matching) >= 1)
|
||||
|
||||
assert.is.truthy(matcher.match('emg', 'error_msg', config.matching) >= 1)
|
||||
assert.is.truthy(matcher.match('sasr', 'saved_splitright', config.matching) >= 1)
|
||||
|
||||
-- TODO: #1420 test-case
|
||||
-- assert.is.truthy(matcher.match('asset_', '????') >= 0)
|
||||
|
||||
local score, matches
|
||||
score, matches = matcher.match('tail', 'HCDetails', {
|
||||
disallow_fuzzy_matching = false,
|
||||
disallow_partial_matching = false,
|
||||
disallow_prefix_unmatching = false,
|
||||
disallow_partial_fuzzy_matching = false,
|
||||
disallow_symbol_nonprefix_matching = true,
|
||||
})
|
||||
assert.is.truthy(score >= 1)
|
||||
assert.equals(matches[1].word_match_start, 5)
|
||||
|
||||
score = matcher.match('tail', 'HCDetails', {
|
||||
disallow_fuzzy_matching = false,
|
||||
disallow_partial_matching = false,
|
||||
disallow_prefix_unmatching = false,
|
||||
disallow_partial_fuzzy_matching = true,
|
||||
disallow_symbol_nonprefix_matching = true,
|
||||
})
|
||||
assert.is.truthy(score == 0)
|
||||
end)
|
||||
|
||||
it('disallow_fuzzy_matching', function()
|
||||
assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = true }) == 0)
|
||||
assert.is.truthy(matcher.match('fmodify', 'fnamemodify', { disallow_fuzzy_matching = false }) >= 1)
|
||||
end)
|
||||
|
||||
it('disallow_fullfuzzy_matching', function()
|
||||
assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = true }) == 0)
|
||||
assert.is.truthy(matcher.match('svd', 'saved_splitright', { disallow_fullfuzzy_matching = false }) >= 1)
|
||||
end)
|
||||
|
||||
it('disallow_partial_matching', function()
|
||||
assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = true }) == 0)
|
||||
assert.is.truthy(matcher.match('fb', 'foo_bar', { disallow_partial_matching = false }) >= 1)
|
||||
assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = true }) >= 1)
|
||||
assert.is.truthy(matcher.match('fb', 'fboo_bar', { disallow_partial_matching = false }) >= 1)
|
||||
end)
|
||||
|
||||
it('disallow_prefix_unmatching', function()
|
||||
assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = true }) == 0)
|
||||
assert.is.truthy(matcher.match('bar', 'foo_bar', { disallow_prefix_unmatching = false }) >= 1)
|
||||
end)
|
||||
|
||||
it('disallow_symbol_nonprefix_matching', function()
|
||||
assert.is.truthy(matcher.match('foo_', 'b foo_bar', { disallow_symbol_nonprefix_matching = true }) == 0)
|
||||
assert.is.truthy(matcher.match('foo_', 'b foo_bar', { disallow_symbol_nonprefix_matching = false }) >= 1)
|
||||
end)
|
||||
|
||||
it('debug', function()
|
||||
matcher.debug = function(...)
|
||||
print(vim.inspect({ ... }))
|
||||
end
|
||||
-- print(vim.inspect({
|
||||
-- a = matcher.match('true', 'v:true', { 'true' }),
|
||||
-- b = matcher.match('true', 'true'),
|
||||
-- }))
|
||||
end)
|
||||
end)
|
||||
401
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/source.lua
Normal file
401
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/source.lua
Normal file
@ -0,0 +1,401 @@
|
||||
local context = require('cmp.context')
|
||||
local config = require('cmp.config')
|
||||
local entry = require('cmp.entry')
|
||||
local debug = require('cmp.utils.debug')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local cache = require('cmp.utils.cache')
|
||||
local types = require('cmp.types')
|
||||
local async = require('cmp.utils.async')
|
||||
local pattern = require('cmp.utils.pattern')
|
||||
local char = require('cmp.utils.char')
|
||||
|
||||
---@class cmp.Source
|
||||
---@field public id integer
|
||||
---@field public name string
|
||||
---@field public source any
|
||||
---@field public cache cmp.Cache
|
||||
---@field public revision integer
|
||||
---@field public incomplete boolean
|
||||
---@field public is_triggered_by_symbol boolean
|
||||
---@field public entries cmp.Entry[]
|
||||
---@field public offset integer
|
||||
---@field public request_offset integer
|
||||
---@field public context cmp.Context
|
||||
---@field public completion_context lsp.CompletionContext|nil
|
||||
---@field public status cmp.SourceStatus
|
||||
---@field public complete_dedup function
|
||||
local source = {}
|
||||
|
||||
---@alias cmp.SourceStatus 1 | 2 | 3
|
||||
source.SourceStatus = {}
|
||||
source.SourceStatus.WAITING = 1
|
||||
source.SourceStatus.FETCHING = 2
|
||||
source.SourceStatus.COMPLETED = 3
|
||||
|
||||
---@return cmp.Source
|
||||
source.new = function(name, s)
|
||||
local self = setmetatable({}, { __index = source })
|
||||
self.id = misc.id('cmp.source.new')
|
||||
self.name = name
|
||||
self.source = s
|
||||
self.cache = cache.new()
|
||||
self.complete_dedup = async.dedup()
|
||||
self.revision = 0
|
||||
self:reset()
|
||||
return self
|
||||
end
|
||||
|
||||
---Reset current completion state
|
||||
source.reset = function(self)
|
||||
self.cache:clear()
|
||||
self.revision = self.revision + 1
|
||||
self.context = context.empty()
|
||||
self.is_triggered_by_symbol = false
|
||||
self.incomplete = false
|
||||
self.entries = {}
|
||||
self.offset = -1
|
||||
self.request_offset = -1
|
||||
self.completion_context = nil
|
||||
self.status = source.SourceStatus.WAITING
|
||||
self.complete_dedup(function() end)
|
||||
end
|
||||
|
||||
---Return source config
|
||||
---@return cmp.SourceConfig
|
||||
source.get_source_config = function(self)
|
||||
return config.get_source_config(self.name) or {}
|
||||
end
|
||||
|
||||
---Return matching config
|
||||
---@return cmp.MatchingConfig
|
||||
source.get_matching_config = function()
|
||||
return config.get().matching
|
||||
end
|
||||
|
||||
---Get fetching time
|
||||
source.get_fetching_time = function(self)
|
||||
if self.status == source.SourceStatus.FETCHING then
|
||||
return vim.loop.now() - self.context.time
|
||||
end
|
||||
return 100 * 1000 -- return pseudo time if source isn't fetching.
|
||||
end
|
||||
|
||||
---Return filtered entries
|
||||
---@param ctx cmp.Context
|
||||
---@return cmp.Entry[]
|
||||
source.get_entries = function(self, ctx)
|
||||
if self.offset == -1 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local target_entries = self.entries
|
||||
|
||||
if not self.incomplete then
|
||||
local prev = self.cache:get({ 'get_entries', tostring(self.revision) })
|
||||
if prev and ctx.cursor.row == prev.ctx.cursor.row and self.offset == prev.offset then
|
||||
-- only use prev entries when cursor is moved forward.
|
||||
-- and the pattern offset is the same.
|
||||
if prev.ctx.cursor.col <= ctx.cursor.col then
|
||||
target_entries = prev.entries
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local entry_filter = self:get_entry_filter()
|
||||
|
||||
local inputs = {}
|
||||
---@type cmp.Entry[]
|
||||
local entries = {}
|
||||
local matching_config = self:get_matching_config()
|
||||
for _, e in ipairs(target_entries) do
|
||||
local o = e:get_offset()
|
||||
if not inputs[o] then
|
||||
inputs[o] = string.sub(ctx.cursor_before_line, o)
|
||||
end
|
||||
|
||||
local match = e:match(inputs[o], matching_config)
|
||||
e.score = match.score
|
||||
e.exact = false
|
||||
if e.score >= 1 then
|
||||
e.matches = match.matches
|
||||
e.exact = e:get_filter_text() == inputs[o] or e:get_word() == inputs[o]
|
||||
|
||||
if entry_filter(e, ctx) then
|
||||
entries[#entries + 1] = e
|
||||
end
|
||||
end
|
||||
async.yield()
|
||||
if ctx.aborted then
|
||||
async.abort()
|
||||
end
|
||||
end
|
||||
|
||||
if not self.incomplete then
|
||||
self.cache:set({ 'get_entries', tostring(self.revision) }, { entries = entries, ctx = ctx, offset = self.offset })
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
---Get default insert range (UTF8 byte index).
|
||||
---@return lsp.Range
|
||||
source.get_default_insert_range = function(self)
|
||||
if not self.context then
|
||||
error('context is not initialized yet.')
|
||||
end
|
||||
|
||||
return self.cache:ensure({ 'get_default_insert_range', tostring(self.revision) }, function()
|
||||
return {
|
||||
start = {
|
||||
line = self.context.cursor.row - 1,
|
||||
character = self.offset - 1,
|
||||
},
|
||||
['end'] = {
|
||||
line = self.context.cursor.row - 1,
|
||||
character = self.context.cursor.col - 1,
|
||||
},
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
---Get default replace range (UTF8 byte index).
|
||||
---@return lsp.Range
|
||||
source.get_default_replace_range = function(self)
|
||||
if not self.context then
|
||||
error('context is not initialized yet.')
|
||||
end
|
||||
|
||||
return self.cache:ensure({ 'get_default_replace_range', tostring(self.revision) }, function()
|
||||
local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset))
|
||||
return {
|
||||
start = {
|
||||
line = self.context.cursor.row - 1,
|
||||
character = self.offset,
|
||||
},
|
||||
['end'] = {
|
||||
line = self.context.cursor.row - 1,
|
||||
character = (e and self.offset + e - 2 or self.context.cursor.col - 1),
|
||||
},
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
---Return source name.
|
||||
source.get_debug_name = function(self)
|
||||
local name = self.name
|
||||
if self.source.get_debug_name then
|
||||
name = self.source:get_debug_name()
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
---Return the source is available or not.
|
||||
source.is_available = function(self)
|
||||
if self.source.is_available then
|
||||
return self.source:is_available()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---Get trigger_characters
|
||||
---@return string[]
|
||||
source.get_trigger_characters = function(self)
|
||||
local c = self:get_source_config()
|
||||
if c.trigger_characters then
|
||||
return c.trigger_characters
|
||||
end
|
||||
|
||||
local trigger_characters = {}
|
||||
if self.source.get_trigger_characters then
|
||||
trigger_characters = self.source:get_trigger_characters(misc.copy(c)) or {}
|
||||
end
|
||||
if config.get().completion.get_trigger_characters then
|
||||
return config.get().completion.get_trigger_characters(trigger_characters)
|
||||
end
|
||||
return trigger_characters
|
||||
end
|
||||
|
||||
---Get keyword_pattern
|
||||
---@return string
|
||||
source.get_keyword_pattern = function(self)
|
||||
local c = self:get_source_config()
|
||||
if c.keyword_pattern then
|
||||
return c.keyword_pattern
|
||||
end
|
||||
if self.source.get_keyword_pattern then
|
||||
local keyword_pattern = self.source:get_keyword_pattern(misc.copy(c))
|
||||
if keyword_pattern then
|
||||
return keyword_pattern
|
||||
end
|
||||
end
|
||||
return config.get().completion.keyword_pattern
|
||||
end
|
||||
|
||||
---Get keyword_length
|
||||
---@return integer
|
||||
source.get_keyword_length = function(self)
|
||||
local c = self:get_source_config()
|
||||
if c.keyword_length then
|
||||
return c.keyword_length
|
||||
end
|
||||
return config.get().completion.keyword_length or 1
|
||||
end
|
||||
|
||||
---Get filter
|
||||
--@return fun(entry: cmp.Entry, context: cmp.Context): boolean
|
||||
source.get_entry_filter = function(self)
|
||||
local c = self:get_source_config()
|
||||
if c.entry_filter then
|
||||
return c.entry_filter --[[@as fun(entry: cmp.Entry, context: cmp.Context): boolean]]
|
||||
end
|
||||
return function(_, _)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
---Get lsp.PositionEncodingKind
|
||||
---@return lsp.PositionEncodingKind
|
||||
source.get_position_encoding_kind = function(self)
|
||||
if self.source.get_position_encoding_kind then
|
||||
return self.source:get_position_encoding_kind()
|
||||
end
|
||||
return types.lsp.PositionEncodingKind.UTF16
|
||||
end
|
||||
|
||||
---Invoke completion
|
||||
---@param ctx cmp.Context
|
||||
---@param callback function
|
||||
---@return boolean? Return true if not trigger completion.
|
||||
source.complete = function(self, ctx, callback)
|
||||
local offset = ctx:get_offset(self:get_keyword_pattern())
|
||||
|
||||
-- NOTE: This implementation is nvim-cmp specific.
|
||||
-- We trigger new completion after core.confirm but we check only the symbol trigger_character in this case.
|
||||
local before_char = string.sub(ctx.cursor_before_line, -1)
|
||||
if ctx:get_reason() == types.cmp.ContextReason.TriggerOnly then
|
||||
before_char = string.match(ctx.cursor_before_line, '(.)%s*$')
|
||||
if not before_char or not char.is_symbol(string.byte(before_char)) then
|
||||
before_char = ''
|
||||
end
|
||||
end
|
||||
|
||||
local completion_context
|
||||
if ctx:get_reason() == types.cmp.ContextReason.Manual then
|
||||
completion_context = {
|
||||
triggerKind = types.lsp.CompletionTriggerKind.Invoked,
|
||||
}
|
||||
elseif vim.tbl_contains(self:get_trigger_characters(), before_char) then
|
||||
completion_context = {
|
||||
triggerKind = types.lsp.CompletionTriggerKind.TriggerCharacter,
|
||||
triggerCharacter = before_char,
|
||||
}
|
||||
elseif ctx:get_reason() ~= types.cmp.ContextReason.TriggerOnly then
|
||||
if offset < ctx.cursor.col and self:get_keyword_length() <= (ctx.cursor.col - offset) then
|
||||
if self.incomplete and self.context.cursor.col ~= ctx.cursor.col and self.status ~= source.SourceStatus.FETCHING then
|
||||
completion_context = {
|
||||
triggerKind = types.lsp.CompletionTriggerKind.TriggerForIncompleteCompletions,
|
||||
}
|
||||
elseif not vim.tbl_contains({ self.request_offset, self.offset }, offset) then
|
||||
completion_context = {
|
||||
triggerKind = types.lsp.CompletionTriggerKind.Invoked,
|
||||
}
|
||||
end
|
||||
else
|
||||
self:reset() -- Should clear current completion if the TriggerKind isn't TriggerCharacter or Manual and keyword length does not enough.
|
||||
end
|
||||
else
|
||||
self:reset() -- Should clear current completion if ContextReason is TriggerOnly and the triggerCharacter isn't matched
|
||||
end
|
||||
|
||||
-- Does not perform completions.
|
||||
if not completion_context then
|
||||
return
|
||||
end
|
||||
|
||||
if completion_context.triggerKind == types.lsp.CompletionTriggerKind.TriggerCharacter then
|
||||
self.is_triggered_by_symbol = char.is_symbol(string.byte(completion_context.triggerCharacter))
|
||||
end
|
||||
|
||||
debug.log(self:get_debug_name(), 'request', offset, vim.inspect(completion_context))
|
||||
local prev_status = self.status
|
||||
self.status = source.SourceStatus.FETCHING
|
||||
self.offset = offset
|
||||
self.request_offset = offset
|
||||
self.context = ctx
|
||||
self.completion_context = completion_context
|
||||
self.source:complete(
|
||||
vim.tbl_extend('keep', misc.copy(self:get_source_config()), {
|
||||
offset = self.offset,
|
||||
context = ctx,
|
||||
completion_context = completion_context,
|
||||
}),
|
||||
self.complete_dedup(vim.schedule_wrap(function(response)
|
||||
if self.context ~= ctx then
|
||||
return
|
||||
end
|
||||
---@type lsp.CompletionResponse
|
||||
response = response or {}
|
||||
|
||||
self.incomplete = response.isIncomplete or false
|
||||
|
||||
if #(response.items or response) > 0 then
|
||||
debug.log(self:get_debug_name(), 'retrieve', #(response.items or response))
|
||||
local old_offset = self.offset
|
||||
local old_entries = self.entries
|
||||
|
||||
self.status = source.SourceStatus.COMPLETED
|
||||
self.entries = {}
|
||||
for _, item in ipairs(response.items or response) do
|
||||
if (item or {}).label then
|
||||
local e = entry.new(ctx, self, item, response.itemDefaults)
|
||||
if not e:is_invalid() then
|
||||
table.insert(self.entries, e)
|
||||
self.offset = math.min(self.offset, e:get_offset())
|
||||
end
|
||||
end
|
||||
end
|
||||
self.revision = self.revision + 1
|
||||
if #self.entries == 0 then
|
||||
self.offset = old_offset
|
||||
self.entries = old_entries
|
||||
self.revision = self.revision + 1
|
||||
end
|
||||
else
|
||||
-- The completion will be invoked when pressing <CR> if the trigger characters contain the <Space>.
|
||||
-- If the server returns an empty response in such a case, should invoke the keyword completion on the next keypress.
|
||||
if offset == ctx.cursor.col then
|
||||
self:reset()
|
||||
end
|
||||
self.status = prev_status
|
||||
end
|
||||
callback()
|
||||
end))
|
||||
)
|
||||
return true
|
||||
end
|
||||
|
||||
---Resolve CompletionItem
|
||||
---@param item lsp.CompletionItem
|
||||
---@param callback fun(item: lsp.CompletionItem)
|
||||
source.resolve = function(self, item, callback)
|
||||
if not self.source.resolve then
|
||||
return callback(item)
|
||||
end
|
||||
self.source:resolve(item, function(resolved_item)
|
||||
callback(resolved_item or item)
|
||||
end)
|
||||
end
|
||||
|
||||
---Execute command
|
||||
---@param item lsp.CompletionItem
|
||||
---@param callback fun()
|
||||
source.execute = function(self, item, callback)
|
||||
if not self.source.execute then
|
||||
return callback()
|
||||
end
|
||||
self.source:execute(item, function()
|
||||
callback()
|
||||
end)
|
||||
end
|
||||
|
||||
return source
|
||||
@ -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)
|
||||
199
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/types/cmp.lua
Normal file
199
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/types/cmp.lua
Normal file
@ -0,0 +1,199 @@
|
||||
local cmp = {}
|
||||
|
||||
---@alias cmp.ConfirmBehavior 'insert' | 'replace'
|
||||
cmp.ConfirmBehavior = {
|
||||
Insert = 'insert',
|
||||
Replace = 'replace',
|
||||
}
|
||||
|
||||
---@alias cmp.SelectBehavior 'insert' | 'select'
|
||||
cmp.SelectBehavior = {
|
||||
Insert = 'insert',
|
||||
Select = 'select',
|
||||
}
|
||||
|
||||
---@alias cmp.ContextReason 'auto' | 'manual' | 'triggerOnly' | 'none'
|
||||
cmp.ContextReason = {
|
||||
Auto = 'auto',
|
||||
Manual = 'manual',
|
||||
TriggerOnly = 'triggerOnly',
|
||||
None = 'none',
|
||||
}
|
||||
|
||||
---@alias cmp.TriggerEvent 'InsertEnter' | 'TextChanged'
|
||||
cmp.TriggerEvent = {
|
||||
InsertEnter = 'InsertEnter',
|
||||
TextChanged = 'TextChanged',
|
||||
}
|
||||
|
||||
---@alias cmp.PreselectMode 'item' | 'None'
|
||||
cmp.PreselectMode = {
|
||||
Item = 'item',
|
||||
None = 'none',
|
||||
}
|
||||
|
||||
---@alias cmp.ItemField 'abbr' | 'kind' | 'menu'
|
||||
cmp.ItemField = {
|
||||
Abbr = 'abbr',
|
||||
Kind = 'kind',
|
||||
Menu = 'menu',
|
||||
}
|
||||
|
||||
---@class cmp.ContextOption
|
||||
---@field public reason cmp.ContextReason|nil
|
||||
|
||||
---@class cmp.ConfirmOption
|
||||
---@field public behavior cmp.ConfirmBehavior
|
||||
---@field public commit_character? string
|
||||
|
||||
---@class cmp.SelectOption
|
||||
---@field public behavior cmp.SelectBehavior
|
||||
|
||||
---@class cmp.SnippetExpansionParams
|
||||
---@field public body string
|
||||
---@field public insert_text_mode integer
|
||||
|
||||
---@class cmp.CompleteParams
|
||||
---@field public reason? cmp.ContextReason
|
||||
---@field public config? cmp.ConfigSchema
|
||||
|
||||
---@class cmp.SetupProperty
|
||||
---@field public buffer fun(c: cmp.ConfigSchema)
|
||||
---@field public global fun(c: cmp.ConfigSchema)
|
||||
---@field public cmdline fun(type: string|string[], c: cmp.ConfigSchema)
|
||||
---@field public filetype fun(type: string|string[], c: cmp.ConfigSchema)
|
||||
|
||||
---@alias cmp.Setup cmp.SetupProperty | fun(c: cmp.ConfigSchema)
|
||||
|
||||
---@class cmp.SourceApiParams: cmp.SourceConfig
|
||||
|
||||
---@class cmp.SourceCompletionApiParams : cmp.SourceConfig
|
||||
---@field public offset integer
|
||||
---@field public context cmp.Context
|
||||
---@field public completion_context lsp.CompletionContext
|
||||
|
||||
---@alias cmp.MappingFunction fun(fallback: function): nil
|
||||
|
||||
---@class cmp.MappingClass
|
||||
---@field public i nil|cmp.MappingFunction
|
||||
---@field public c nil|cmp.MappingFunction
|
||||
---@field public x nil|cmp.MappingFunction
|
||||
---@field public s nil|cmp.MappingFunction
|
||||
|
||||
---@alias cmp.Mapping cmp.MappingFunction | cmp.MappingClass
|
||||
|
||||
---@class cmp.ConfigSchema
|
||||
---@field private revision? integer
|
||||
---@field public enabled? boolean | fun(): boolean
|
||||
---@field public performance? cmp.PerformanceConfig
|
||||
---@field public preselect? cmp.PreselectMode
|
||||
---@field public completion? cmp.CompletionConfig
|
||||
---@field public window? cmp.WindowConfig|nil
|
||||
---@field public confirmation? cmp.ConfirmationConfig
|
||||
---@field public matching? cmp.MatchingConfig
|
||||
---@field public sorting? cmp.SortingConfig
|
||||
---@field public formatting? cmp.FormattingConfig
|
||||
---@field public snippet? cmp.SnippetConfig
|
||||
---@field public mapping? table<string, cmp.Mapping>
|
||||
---@field public sources? cmp.SourceConfig[]
|
||||
---@field public view? cmp.ViewConfig
|
||||
---@field public experimental? cmp.ExperimentalConfig
|
||||
|
||||
---@class cmp.PerformanceConfig
|
||||
---@field public debounce integer
|
||||
---@field public throttle integer
|
||||
---@field public fetching_timeout integer
|
||||
---@field public confirm_resolve_timeout integer
|
||||
---@field public async_budget integer Maximum time (in ms) an async function is allowed to run during one step of the event loop.
|
||||
---@field public max_view_entries integer
|
||||
|
||||
---@class cmp.CompletionConfig
|
||||
---@field public autocomplete? cmp.TriggerEvent[]|false
|
||||
---@field public completeopt? string
|
||||
---@field public get_trigger_characters? fun(trigger_characters: string[]): string[]
|
||||
---@field public keyword_length? integer
|
||||
---@field public keyword_pattern? string
|
||||
|
||||
---@class cmp.WindowConfig
|
||||
---@field public completion? cmp.CompletionWindowOptions
|
||||
---@field public documentation? cmp.DocumentationWindowOptions|nil
|
||||
|
||||
---@class cmp.WindowOptions
|
||||
---@field public border? string|string[]
|
||||
---@field public winhighlight? string
|
||||
---@field public winblend? number
|
||||
---@field public zindex? integer|nil
|
||||
|
||||
---@class cmp.CompletionWindowOptions: cmp.WindowOptions
|
||||
---@field public scrolloff? integer|nil
|
||||
---@field public col_offset? integer|nil
|
||||
---@field public side_padding? integer|nil
|
||||
---@field public scrollbar? boolean|true
|
||||
|
||||
---@class cmp.DocumentationWindowOptions: cmp.WindowOptions
|
||||
---@field public max_height? integer|nil
|
||||
---@field public max_width? integer|nil
|
||||
|
||||
---@class cmp.ConfirmationConfig
|
||||
---@field public default_behavior cmp.ConfirmBehavior
|
||||
---@field public get_commit_characters fun(commit_characters: string[]): string[]
|
||||
|
||||
---@class cmp.MatchingConfig
|
||||
---@field public disallow_fuzzy_matching boolean
|
||||
---@field public disallow_fullfuzzy_matching boolean
|
||||
---@field public disallow_partial_fuzzy_matching boolean
|
||||
---@field public disallow_partial_matching boolean
|
||||
---@field public disallow_prefix_unmatching boolean
|
||||
---@field public disallow_symbol_nonprefix_matching boolean
|
||||
|
||||
---@class cmp.SortingConfig
|
||||
---@field public priority_weight integer
|
||||
---@field public comparators cmp.Comparator[]
|
||||
|
||||
---@class cmp.FormattingConfig
|
||||
---@field public fields cmp.ItemField[]
|
||||
---@field public expandable_indicator boolean
|
||||
---@field public format fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem
|
||||
|
||||
---@class cmp.SnippetConfig
|
||||
---@field public expand fun(args: cmp.SnippetExpansionParams)
|
||||
|
||||
---@class cmp.ExperimentalConfig
|
||||
---@field public ghost_text cmp.GhostTextConfig|boolean
|
||||
|
||||
---@class cmp.GhostTextConfig
|
||||
---@field hl_group string
|
||||
|
||||
---@class cmp.SourceConfig
|
||||
---@field public name string
|
||||
---@field public option table|nil
|
||||
---@field public priority integer|nil
|
||||
---@field public trigger_characters string[]|nil
|
||||
---@field public keyword_pattern string|nil
|
||||
---@field public keyword_length integer|nil
|
||||
---@field public max_item_count integer|nil
|
||||
---@field public group_index integer|nil
|
||||
---@field public entry_filter nil|function(entry: cmp.Entry, ctx: cmp.Context): boolean
|
||||
|
||||
---@class cmp.ViewConfig
|
||||
---@field public entries? cmp.EntriesViewConfig
|
||||
---@field public docs? cmp.DocsViewConfig
|
||||
|
||||
---@alias cmp.EntriesViewConfig cmp.CustomEntriesViewConfig|cmp.NativeEntriesViewConfig|cmp.WildmenuEntriesViewConfig|string
|
||||
|
||||
---@class cmp.CustomEntriesViewConfig
|
||||
---@field name 'custom'
|
||||
---@field selection_order 'top_down'|'near_cursor'
|
||||
---@field follow_cursor boolean
|
||||
|
||||
---@class cmp.NativeEntriesViewConfig
|
||||
---@field name 'native'
|
||||
|
||||
---@class cmp.WildmenuEntriesViewConfig
|
||||
---@field name 'wildmenu'
|
||||
---@field separator string|nil
|
||||
|
||||
---@class cmp.DocsViewConfig
|
||||
---@field public auto_open boolean
|
||||
|
||||
return cmp
|
||||
@ -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
|
||||
292
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/types/lsp.lua
Normal file
292
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/types/lsp.lua
Normal file
@ -0,0 +1,292 @@
|
||||
local misc = require('cmp.utils.misc')
|
||||
|
||||
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/
|
||||
---@class lsp
|
||||
local lsp = {}
|
||||
|
||||
---@enum lsp.PositionEncodingKind
|
||||
lsp.PositionEncodingKind = {
|
||||
UTF8 = 'utf-8',
|
||||
UTF16 = 'utf-16',
|
||||
UTF32 = 'utf-32',
|
||||
}
|
||||
|
||||
lsp.Position = {
|
||||
---Convert lsp.Position to vim.Position
|
||||
---@param buf integer
|
||||
---@param position lsp.Position
|
||||
--
|
||||
---@return vim.Position
|
||||
to_vim = function(buf, position)
|
||||
if not vim.api.nvim_buf_is_loaded(buf) then
|
||||
vim.fn.bufload(buf)
|
||||
end
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, position.line, position.line + 1, false)
|
||||
if #lines > 0 then
|
||||
return {
|
||||
row = position.line + 1,
|
||||
col = misc.to_vimindex(lines[1], position.character),
|
||||
}
|
||||
end
|
||||
return {
|
||||
row = position.line + 1,
|
||||
col = position.character + 1,
|
||||
}
|
||||
end,
|
||||
---Convert vim.Position to lsp.Position
|
||||
---@param buf integer
|
||||
---@param position vim.Position
|
||||
---@return lsp.Position
|
||||
to_lsp = function(buf, position)
|
||||
if not vim.api.nvim_buf_is_loaded(buf) then
|
||||
vim.fn.bufload(buf)
|
||||
end
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, position.row - 1, position.row, false)
|
||||
if #lines > 0 then
|
||||
return {
|
||||
line = position.row - 1,
|
||||
character = misc.to_utfindex(lines[1], position.col),
|
||||
}
|
||||
end
|
||||
return {
|
||||
line = position.row - 1,
|
||||
character = position.col - 1,
|
||||
}
|
||||
end,
|
||||
|
||||
---Convert position to utf8 from specified encoding.
|
||||
---@param text string
|
||||
---@param position lsp.Position
|
||||
---@param from_encoding? lsp.PositionEncodingKind
|
||||
---@return lsp.Position
|
||||
to_utf8 = function(text, position, from_encoding)
|
||||
from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
|
||||
if from_encoding == lsp.PositionEncodingKind.UTF8 then
|
||||
return position
|
||||
end
|
||||
|
||||
local ok, byteindex = pcall(function()
|
||||
return vim.str_byteindex(text, position.character, from_encoding == lsp.PositionEncodingKind.UTF16)
|
||||
end)
|
||||
if not ok then
|
||||
return position
|
||||
end
|
||||
return { line = position.line, character = byteindex }
|
||||
end,
|
||||
|
||||
---Convert position to utf16 from specified encoding.
|
||||
---@param text string
|
||||
---@param position lsp.Position
|
||||
---@param from_encoding? lsp.PositionEncodingKind
|
||||
---@return lsp.Position
|
||||
to_utf16 = function(text, position, from_encoding)
|
||||
from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
|
||||
if from_encoding == lsp.PositionEncodingKind.UTF16 then
|
||||
return position
|
||||
end
|
||||
|
||||
local utf8 = lsp.Position.to_utf8(text, position, from_encoding)
|
||||
for index = utf8.character, 0, -1 do
|
||||
local ok, utf16index = pcall(function()
|
||||
return select(2, vim.str_utfindex(text, index))
|
||||
end)
|
||||
if ok then
|
||||
return { line = utf8.line, character = utf16index }
|
||||
end
|
||||
end
|
||||
return position
|
||||
end,
|
||||
|
||||
---Convert position to utf32 from specified encoding.
|
||||
---@param text string
|
||||
---@param position lsp.Position
|
||||
---@param from_encoding? lsp.PositionEncodingKind
|
||||
---@return lsp.Position
|
||||
to_utf32 = function(text, position, from_encoding)
|
||||
from_encoding = from_encoding or lsp.PositionEncodingKind.UTF16
|
||||
if from_encoding == lsp.PositionEncodingKind.UTF32 then
|
||||
return position
|
||||
end
|
||||
|
||||
local utf8 = lsp.Position.to_utf8(text, position, from_encoding)
|
||||
for index = utf8.character, 0, -1 do
|
||||
local ok, utf32index = pcall(function()
|
||||
return select(1, vim.str_utfindex(text, index))
|
||||
end)
|
||||
if ok then
|
||||
return { line = utf8.line, character = utf32index }
|
||||
end
|
||||
end
|
||||
return position
|
||||
end,
|
||||
}
|
||||
|
||||
lsp.Range = {
|
||||
---Convert lsp.Range to vim.Range
|
||||
---@param buf integer
|
||||
---@param range lsp.Range
|
||||
---@return vim.Range
|
||||
to_vim = function(buf, range)
|
||||
return {
|
||||
start = lsp.Position.to_vim(buf, range.start),
|
||||
['end'] = lsp.Position.to_vim(buf, range['end']),
|
||||
}
|
||||
end,
|
||||
|
||||
---Convert vim.Range to lsp.Range
|
||||
---@param buf integer
|
||||
---@param range vim.Range
|
||||
---@return lsp.Range
|
||||
to_lsp = function(buf, range)
|
||||
return {
|
||||
start = lsp.Position.to_lsp(buf, range.start),
|
||||
['end'] = lsp.Position.to_lsp(buf, range['end']),
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
---@alias lsp.CompletionTriggerKind 1 | 2 | 3
|
||||
lsp.CompletionTriggerKind = {
|
||||
Invoked = 1,
|
||||
TriggerCharacter = 2,
|
||||
TriggerForIncompleteCompletions = 3,
|
||||
}
|
||||
|
||||
---@alias lsp.InsertTextFormat 1 | 2
|
||||
lsp.InsertTextFormat = {}
|
||||
lsp.InsertTextFormat.PlainText = 1
|
||||
lsp.InsertTextFormat.Snippet = 2
|
||||
|
||||
---@alias lsp.InsertTextMode 1 | 2
|
||||
lsp.InsertTextMode = {
|
||||
AsIs = 1,
|
||||
AdjustIndentation = 2,
|
||||
}
|
||||
|
||||
---@alias lsp.MarkupKind 'plaintext' | 'markdown'
|
||||
lsp.MarkupKind = {
|
||||
PlainText = 'plaintext',
|
||||
Markdown = 'markdown',
|
||||
}
|
||||
|
||||
---@alias lsp.CompletionItemTag 1
|
||||
lsp.CompletionItemTag = {
|
||||
Deprecated = 1,
|
||||
}
|
||||
|
||||
---@alias lsp.CompletionItemKind 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25
|
||||
lsp.CompletionItemKind = {
|
||||
Text = 1,
|
||||
Method = 2,
|
||||
Function = 3,
|
||||
Constructor = 4,
|
||||
Field = 5,
|
||||
Variable = 6,
|
||||
Class = 7,
|
||||
Interface = 8,
|
||||
Module = 9,
|
||||
Property = 10,
|
||||
Unit = 11,
|
||||
Value = 12,
|
||||
Enum = 13,
|
||||
Keyword = 14,
|
||||
Snippet = 15,
|
||||
Color = 16,
|
||||
File = 17,
|
||||
Reference = 18,
|
||||
Folder = 19,
|
||||
EnumMember = 20,
|
||||
Constant = 21,
|
||||
Struct = 22,
|
||||
Event = 23,
|
||||
Operator = 24,
|
||||
TypeParameter = 25,
|
||||
}
|
||||
for k, v in pairs(lsp.CompletionItemKind) do
|
||||
lsp.CompletionItemKind[v] = k
|
||||
end
|
||||
|
||||
---@class lsp.internal.CompletionItemDefaults
|
||||
---@field public commitCharacters? string[]
|
||||
---@field public editRange? lsp.Range | { insert: lsp.Range, replace: lsp.Range }
|
||||
---@field public insertTextFormat? lsp.InsertTextFormat
|
||||
---@field public insertTextMode? lsp.InsertTextMode
|
||||
---@field public data? any
|
||||
|
||||
---@class lsp.CompletionContext
|
||||
---@field public triggerKind lsp.CompletionTriggerKind
|
||||
---@field public triggerCharacter string|nil
|
||||
|
||||
---@class lsp.CompletionList
|
||||
---@field public isIncomplete boolean
|
||||
---@field public itemDefaults? lsp.internal.CompletionItemDefaults
|
||||
---@field public items lsp.CompletionItem[]
|
||||
|
||||
---@alias lsp.CompletionResponse lsp.CompletionList|lsp.CompletionItem[]
|
||||
|
||||
---@class lsp.MarkupContent
|
||||
---@field public kind lsp.MarkupKind
|
||||
---@field public value string
|
||||
|
||||
---@class lsp.Position
|
||||
---@field public line integer
|
||||
---@field public character integer
|
||||
|
||||
---@class lsp.Range
|
||||
---@field public start lsp.Position
|
||||
---@field public end lsp.Position
|
||||
|
||||
---@class lsp.Command
|
||||
---@field public title string
|
||||
---@field public command string
|
||||
---@field public arguments any[]|nil
|
||||
|
||||
---@class lsp.TextEdit
|
||||
---@field public range lsp.Range|nil
|
||||
---@field public newText string
|
||||
|
||||
---@alias lsp.InsertReplaceTextEdit lsp.internal.InsertTextEdit|lsp.internal.ReplaceTextEdit
|
||||
|
||||
---@class lsp.internal.InsertTextEdit
|
||||
---@field public insert lsp.Range
|
||||
---@field public newText string
|
||||
|
||||
---@class lsp.internal.ReplaceTextEdit
|
||||
---@field public replace lsp.Range
|
||||
---@field public newText string
|
||||
|
||||
---@class lsp.CompletionItemLabelDetails
|
||||
---@field public detail? string
|
||||
---@field public description? string
|
||||
|
||||
---@class lsp.internal.CmpCompletionExtension
|
||||
---@field public kind_text string
|
||||
---@field public kind_hl_group string
|
||||
|
||||
---@class lsp.CompletionItem
|
||||
---@field public label string
|
||||
---@field public labelDetails? lsp.CompletionItemLabelDetails
|
||||
---@field public kind? lsp.CompletionItemKind
|
||||
---@field public tags? lsp.CompletionItemTag[]
|
||||
---@field public detail? string
|
||||
---@field public documentation? lsp.MarkupContent|string
|
||||
---@field public deprecated? boolean
|
||||
---@field public preselect? boolean
|
||||
---@field public sortText? string
|
||||
---@field public filterText? string
|
||||
---@field public insertText? string
|
||||
---@field public insertTextFormat? lsp.InsertTextFormat
|
||||
---@field public insertTextMode? lsp.InsertTextMode
|
||||
---@field public textEdit? lsp.TextEdit|lsp.InsertReplaceTextEdit
|
||||
---@field public textEditText? string
|
||||
---@field public additionalTextEdits? lsp.TextEdit[]
|
||||
---@field public commitCharacters? string[]
|
||||
---@field public command? lsp.Command
|
||||
---@field public data? any
|
||||
---@field public cmp? lsp.internal.CmpCompletionExtension
|
||||
---
|
||||
---TODO: Should send the issue for upstream?
|
||||
---@field public word string|nil
|
||||
---@field public dup boolean|nil
|
||||
|
||||
return lsp
|
||||
@ -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)
|
||||
@ -0,0 +1,20 @@
|
||||
---@class vim.CompletedItem
|
||||
---@field public word string
|
||||
---@field public abbr string|nil
|
||||
---@field public kind string|nil
|
||||
---@field public menu string|nil
|
||||
---@field public equal 1|nil
|
||||
---@field public empty 1|nil
|
||||
---@field public dup 1|nil
|
||||
---@field public id any
|
||||
---@field public abbr_hl_group string|nil
|
||||
---@field public kind_hl_group string|nil
|
||||
---@field public menu_hl_group string|nil
|
||||
|
||||
---@class vim.Position 1-based index
|
||||
---@field public row integer
|
||||
---@field public col integer
|
||||
|
||||
---@class vim.Range
|
||||
---@field public start vim.Position
|
||||
---@field public end vim.Position
|
||||
@ -0,0 +1,70 @@
|
||||
local api = {}
|
||||
|
||||
local CTRL_V = vim.api.nvim_replace_termcodes('<C-v>', true, true, true)
|
||||
local CTRL_S = vim.api.nvim_replace_termcodes('<C-s>', true, true, true)
|
||||
|
||||
api.get_mode = function()
|
||||
local mode = vim.api.nvim_get_mode().mode:sub(1, 1)
|
||||
if mode == 'i' then
|
||||
return 'i' -- insert
|
||||
elseif mode == 'v' or mode == 'V' or mode == CTRL_V then
|
||||
return 'x' -- visual
|
||||
elseif mode == 's' or mode == 'S' or mode == CTRL_S then
|
||||
return 's' -- select
|
||||
elseif mode == 'c' and vim.fn.getcmdtype() ~= '=' then
|
||||
return 'c' -- cmdline
|
||||
end
|
||||
end
|
||||
|
||||
api.is_insert_mode = function()
|
||||
return api.get_mode() == 'i'
|
||||
end
|
||||
|
||||
api.is_cmdline_mode = function()
|
||||
return api.get_mode() == 'c'
|
||||
end
|
||||
|
||||
api.is_select_mode = function()
|
||||
return api.get_mode() == 's'
|
||||
end
|
||||
|
||||
api.is_visual_mode = function()
|
||||
return api.get_mode() == 'x'
|
||||
end
|
||||
|
||||
api.is_suitable_mode = function()
|
||||
local mode = api.get_mode()
|
||||
return mode == 'i' or mode == 'c'
|
||||
end
|
||||
|
||||
api.get_current_line = function()
|
||||
if api.is_cmdline_mode() then
|
||||
return vim.fn.getcmdline()
|
||||
end
|
||||
return vim.api.nvim_get_current_line()
|
||||
end
|
||||
|
||||
---@return { [1]: integer, [2]: integer }
|
||||
api.get_cursor = function()
|
||||
if api.is_cmdline_mode() then
|
||||
return { math.min(vim.o.lines, vim.o.lines - (vim.api.nvim_get_option_value('cmdheight', {}) - 1)), vim.fn.getcmdpos() - 1 }
|
||||
end
|
||||
return vim.api.nvim_win_get_cursor(0)
|
||||
end
|
||||
|
||||
api.get_screen_cursor = function()
|
||||
if api.is_cmdline_mode() then
|
||||
local cursor = api.get_cursor()
|
||||
return { cursor[1], vim.fn.strdisplaywidth(string.sub(vim.fn.getcmdline(), 1, cursor[2] + 1)) }
|
||||
end
|
||||
local cursor = api.get_cursor()
|
||||
local pos = vim.fn.screenpos(0, cursor[1], cursor[2] + 1)
|
||||
return { pos.row, pos.col - 1 }
|
||||
end
|
||||
|
||||
api.get_cursor_before_line = function()
|
||||
local cursor = api.get_cursor()
|
||||
return string.sub(api.get_current_line(), 1, cursor[2])
|
||||
end
|
||||
|
||||
return api
|
||||
@ -0,0 +1,64 @@
|
||||
local spec = require('cmp.utils.spec')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local feedkeys = require('cmp.utils.feedkeys')
|
||||
local api = require('cmp.utils.api')
|
||||
|
||||
describe('api', function()
|
||||
before_each(spec.before)
|
||||
describe('get_cursor', function()
|
||||
it('insert-mode', function()
|
||||
local cursor
|
||||
feedkeys.call(keymap.t('i\t1234567890'), 'nx', function()
|
||||
cursor = api.get_cursor()
|
||||
end)
|
||||
assert.are.equal(cursor[2], 11)
|
||||
end)
|
||||
it('cmdline-mode', function()
|
||||
local cursor
|
||||
keymap.set_map(0, 'c', '<Plug>(cmp-spec-spy)', function()
|
||||
cursor = api.get_cursor()
|
||||
end, { expr = true, noremap = true })
|
||||
feedkeys.call(keymap.t(':\t1234567890'), 'n')
|
||||
feedkeys.call(keymap.t('<Plug>(cmp-spec-spy)'), 'x')
|
||||
assert.are.equal(cursor[2], 11)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('get_screen_cursor', function()
|
||||
it('insert-mode', function()
|
||||
local screen_cursor
|
||||
feedkeys.call(keymap.t('iあいうえお'), 'nx', function()
|
||||
screen_cursor = api.get_screen_cursor()
|
||||
end)
|
||||
assert.are.equal(10, screen_cursor[2])
|
||||
end)
|
||||
it('cmdline-mode', function()
|
||||
local screen_cursor
|
||||
keymap.set_map(0, 'c', '<Plug>(cmp-spec-spy)', function()
|
||||
screen_cursor = api.get_screen_cursor()
|
||||
end, { expr = true, noremap = true })
|
||||
feedkeys.call(keymap.t(':あいうえお'), 'n')
|
||||
feedkeys.call(keymap.t('<Plug>(cmp-spec-spy)'), 'x')
|
||||
assert.are.equal(10, screen_cursor[2])
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('get_cursor_before_line', function()
|
||||
it('insert-mode', function()
|
||||
local cursor_before_line
|
||||
feedkeys.call(keymap.t('i\t1234567890<Left><Left>'), 'nx', function()
|
||||
cursor_before_line = api.get_cursor_before_line()
|
||||
end)
|
||||
assert.are.same(cursor_before_line, '\t12345678')
|
||||
end)
|
||||
it('cmdline-mode', function()
|
||||
local cursor_before_line
|
||||
keymap.set_map(0, 'c', '<Plug>(cmp-spec-spy)', function()
|
||||
cursor_before_line = api.get_cursor_before_line()
|
||||
end, { expr = true, noremap = true })
|
||||
feedkeys.call(keymap.t(':\t1234567890<Left><Left>'), 'n')
|
||||
feedkeys.call(keymap.t('<Plug>(cmp-spec-spy)'), 'x')
|
||||
assert.are.same(cursor_before_line, '\t12345678')
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
@ -0,0 +1,303 @@
|
||||
local feedkeys = require('cmp.utils.feedkeys')
|
||||
local config = require('cmp.config')
|
||||
|
||||
local async = {}
|
||||
|
||||
---@class cmp.AsyncThrottle
|
||||
---@field public running boolean
|
||||
---@field public timeout integer
|
||||
---@field public sync function(self: cmp.AsyncThrottle, timeout: integer|nil)
|
||||
---@field public stop function
|
||||
---@field public __call function
|
||||
|
||||
---@type uv_timer_t[]
|
||||
local timers = {}
|
||||
|
||||
vim.api.nvim_create_autocmd('VimLeavePre', {
|
||||
callback = function()
|
||||
for _, timer in pairs(timers) do
|
||||
if timer and not timer:is_closing() then
|
||||
timer:stop()
|
||||
timer:close()
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
---@param fn function
|
||||
---@param timeout integer
|
||||
---@return cmp.AsyncThrottle
|
||||
async.throttle = function(fn, timeout)
|
||||
local time = nil
|
||||
local timer = assert(vim.loop.new_timer())
|
||||
local _async = nil ---@type Async?
|
||||
timers[#timers + 1] = timer
|
||||
local throttle
|
||||
throttle = setmetatable({
|
||||
running = false,
|
||||
timeout = timeout,
|
||||
sync = function(self, timeout_)
|
||||
if not self.running then
|
||||
return
|
||||
end
|
||||
vim.wait(timeout_ or 1000, function()
|
||||
return not self.running
|
||||
end, 10)
|
||||
end,
|
||||
stop = function(reset_time)
|
||||
if reset_time ~= false then
|
||||
time = nil
|
||||
end
|
||||
-- can't use self here unfortunately
|
||||
throttle.running = false
|
||||
timer:stop()
|
||||
if _async then
|
||||
_async:cancel()
|
||||
_async = nil
|
||||
end
|
||||
end,
|
||||
}, {
|
||||
__call = function(self, ...)
|
||||
local args = { ... }
|
||||
|
||||
if time == nil then
|
||||
time = vim.loop.now()
|
||||
end
|
||||
self.stop(false)
|
||||
self.running = true
|
||||
timer:start(math.max(1, self.timeout - (vim.loop.now() - time)), 0, function()
|
||||
vim.schedule(function()
|
||||
time = nil
|
||||
local ret = fn(unpack(args))
|
||||
if async.is_async(ret) then
|
||||
---@cast ret Async
|
||||
_async = ret
|
||||
_async:await(function(_, error)
|
||||
_async = nil
|
||||
self.running = false
|
||||
if error and error ~= 'abort' then
|
||||
vim.notify(error, vim.log.levels.ERROR)
|
||||
end
|
||||
end)
|
||||
else
|
||||
self.running = false
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end,
|
||||
})
|
||||
return throttle
|
||||
end
|
||||
|
||||
---Control async tasks.
|
||||
async.step = function(...)
|
||||
local tasks = { ... }
|
||||
local next
|
||||
next = function(...)
|
||||
if #tasks > 0 then
|
||||
table.remove(tasks, 1)(next, ...)
|
||||
end
|
||||
end
|
||||
table.remove(tasks, 1)(next)
|
||||
end
|
||||
|
||||
---Timeout callback function
|
||||
---@param fn function
|
||||
---@param timeout integer
|
||||
---@return function
|
||||
async.timeout = function(fn, timeout)
|
||||
local timer
|
||||
local done = false
|
||||
local callback = function(...)
|
||||
if not done then
|
||||
done = true
|
||||
timer:stop()
|
||||
timer:close()
|
||||
fn(...)
|
||||
end
|
||||
end
|
||||
timer = vim.loop.new_timer()
|
||||
timer:start(timeout, 0, function()
|
||||
callback()
|
||||
end)
|
||||
return callback
|
||||
end
|
||||
|
||||
---@alias cmp.AsyncDedup fun(callback: function): function
|
||||
|
||||
---Create deduplicated callback
|
||||
---@return function
|
||||
async.dedup = function()
|
||||
local id = 0
|
||||
return function(callback)
|
||||
id = id + 1
|
||||
|
||||
local current = id
|
||||
return function(...)
|
||||
if current == id then
|
||||
callback(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Convert async process as sync
|
||||
async.sync = function(runner, timeout)
|
||||
local done = false
|
||||
runner(function()
|
||||
done = true
|
||||
end)
|
||||
vim.wait(timeout, function()
|
||||
return done
|
||||
end, 10, false)
|
||||
end
|
||||
|
||||
---Wait and callback for next safe state.
|
||||
async.debounce_next_tick = function(callback)
|
||||
local running = false
|
||||
return function()
|
||||
if running then
|
||||
return
|
||||
end
|
||||
running = true
|
||||
vim.schedule(function()
|
||||
running = false
|
||||
callback()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---Wait and callback for consuming next keymap.
|
||||
async.debounce_next_tick_by_keymap = function(callback)
|
||||
return function()
|
||||
feedkeys.call('', '', callback)
|
||||
end
|
||||
end
|
||||
|
||||
local Scheduler = {}
|
||||
Scheduler._queue = {}
|
||||
Scheduler._executor = assert(vim.loop.new_check())
|
||||
|
||||
function Scheduler.step()
|
||||
local budget = config.get().performance.async_budget * 1e6
|
||||
local start = vim.loop.hrtime()
|
||||
while #Scheduler._queue > 0 and vim.loop.hrtime() - start < budget do
|
||||
local a = table.remove(Scheduler._queue, 1)
|
||||
a:_step()
|
||||
if a.running then
|
||||
table.insert(Scheduler._queue, a)
|
||||
end
|
||||
end
|
||||
if #Scheduler._queue == 0 then
|
||||
return Scheduler._executor:stop()
|
||||
end
|
||||
end
|
||||
|
||||
---@param a Async
|
||||
function Scheduler.add(a)
|
||||
table.insert(Scheduler._queue, a)
|
||||
if not Scheduler._executor:is_active() then
|
||||
Scheduler._executor:start(vim.schedule_wrap(Scheduler.step))
|
||||
end
|
||||
end
|
||||
|
||||
--- @alias AsyncCallback fun(result?:any, error?:string)
|
||||
|
||||
--- @class Async
|
||||
--- @field running boolean
|
||||
--- @field result? any
|
||||
--- @field error? string
|
||||
--- @field callbacks AsyncCallback[]
|
||||
--- @field thread thread
|
||||
local Async = {}
|
||||
Async.__index = Async
|
||||
|
||||
function Async.new(fn)
|
||||
local self = setmetatable({}, Async)
|
||||
self.callbacks = {}
|
||||
self.running = true
|
||||
self.thread = coroutine.create(fn)
|
||||
Scheduler.add(self)
|
||||
return self
|
||||
end
|
||||
|
||||
---@param result? any
|
||||
---@param error? string
|
||||
function Async:_done(result, error)
|
||||
if self.running then
|
||||
self.running = false
|
||||
self.result = result
|
||||
self.error = error
|
||||
end
|
||||
for _, callback in ipairs(self.callbacks) do
|
||||
callback(result, error)
|
||||
end
|
||||
-- only run each callback once.
|
||||
-- _done can possibly be called multiple times.
|
||||
-- so we need to clear callbacks after executing them.
|
||||
self.callbacks = {}
|
||||
end
|
||||
|
||||
function Async:_step()
|
||||
local ok, res = coroutine.resume(self.thread)
|
||||
if not ok then
|
||||
return self:_done(nil, res)
|
||||
elseif res == 'abort' then
|
||||
return self:_done(nil, 'abort')
|
||||
elseif coroutine.status(self.thread) == 'dead' then
|
||||
return self:_done(res)
|
||||
end
|
||||
end
|
||||
|
||||
function Async:cancel()
|
||||
self:_done(nil, 'abort')
|
||||
end
|
||||
|
||||
---@param cb AsyncCallback
|
||||
function Async:await(cb)
|
||||
if not cb then
|
||||
error('callback is required')
|
||||
end
|
||||
if self.running then
|
||||
table.insert(self.callbacks, cb)
|
||||
else
|
||||
cb(self.result, self.error)
|
||||
end
|
||||
end
|
||||
|
||||
function Async:sync()
|
||||
while self.running do
|
||||
vim.wait(10)
|
||||
end
|
||||
return self.error and error(self.error) or self.result
|
||||
end
|
||||
|
||||
--- @return boolean
|
||||
function async.is_async(obj)
|
||||
return obj and type(obj) == 'table' and getmetatable(obj) == Async
|
||||
end
|
||||
|
||||
--- @return fun(...): Async
|
||||
function async.wrap(fn)
|
||||
return function(...)
|
||||
local args = { ... }
|
||||
return Async.new(function()
|
||||
return fn(unpack(args))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- This will yield when called from a coroutine
|
||||
function async.yield(...)
|
||||
if coroutine.running() == nil then
|
||||
error('Trying to yield from a non-yieldable context')
|
||||
return ...
|
||||
end
|
||||
return coroutine.yield(...)
|
||||
end
|
||||
|
||||
function async.abort()
|
||||
return async.yield('abort')
|
||||
end
|
||||
|
||||
return async
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -0,0 +1,33 @@
|
||||
local binary = {}
|
||||
|
||||
---Insert item to list to ordered index
|
||||
---@param list any[]
|
||||
---@param item any
|
||||
---@param func fun(a: any, b: any): 1|-1|0
|
||||
binary.insort = function(list, item, func)
|
||||
table.insert(list, binary.search(list, item, func), item)
|
||||
end
|
||||
|
||||
---Search suitable index from list
|
||||
---@param list any[]
|
||||
---@param item any
|
||||
---@param func fun(a: any, b: any): 1|-1|0
|
||||
---@return integer
|
||||
binary.search = function(list, item, func)
|
||||
local s = 1
|
||||
local e = #list
|
||||
while s <= e do
|
||||
local idx = math.floor((e + s) / 2)
|
||||
local diff = func(item, list[idx])
|
||||
if diff > 0 then
|
||||
s = idx + 1
|
||||
elseif diff < 0 then
|
||||
e = idx - 1
|
||||
else
|
||||
return idx + 1
|
||||
end
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
return binary
|
||||
@ -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)
|
||||
@ -0,0 +1,28 @@
|
||||
local buffer = {}
|
||||
|
||||
buffer.cache = {}
|
||||
|
||||
---@return integer buf
|
||||
buffer.get = function(name)
|
||||
local buf = buffer.cache[name]
|
||||
if buf and vim.api.nvim_buf_is_valid(buf) then
|
||||
return buf
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
---@return integer buf
|
||||
---@return boolean created_new
|
||||
buffer.ensure = function(name)
|
||||
local created_new = false
|
||||
local buf = buffer.get(name)
|
||||
if not buf then
|
||||
created_new = true
|
||||
buf = vim.api.nvim_create_buf(false, true)
|
||||
buffer.cache[name] = buf
|
||||
end
|
||||
return buf, created_new
|
||||
end
|
||||
|
||||
return buffer
|
||||
@ -0,0 +1,60 @@
|
||||
---@class cmp.Cache
|
||||
---@field public entries any
|
||||
local cache = {}
|
||||
|
||||
cache.new = function()
|
||||
local self = setmetatable({}, { __index = cache })
|
||||
self.entries = {}
|
||||
return self
|
||||
end
|
||||
|
||||
---Get cache value
|
||||
---@param key string|string[]
|
||||
---@return any|nil
|
||||
cache.get = function(self, key)
|
||||
key = self:key(key)
|
||||
if self.entries[key] ~= nil then
|
||||
return self.entries[key]
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---Set cache value explicitly
|
||||
---@param key string|string[]
|
||||
---@vararg any
|
||||
cache.set = function(self, key, value)
|
||||
key = self:key(key)
|
||||
self.entries[key] = value
|
||||
end
|
||||
|
||||
---Ensure value by callback
|
||||
---@generic T
|
||||
---@param key string|string[]
|
||||
---@param callback fun(): T
|
||||
---@return T
|
||||
cache.ensure = function(self, key, callback)
|
||||
local value = self:get(key)
|
||||
if value == nil then
|
||||
local v = callback()
|
||||
self:set(key, v)
|
||||
return v
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
---Clear all cache entries
|
||||
cache.clear = function(self)
|
||||
self.entries = {}
|
||||
end
|
||||
|
||||
---Create key
|
||||
---@param key string|string[]
|
||||
---@return string
|
||||
cache.key = function(_, key)
|
||||
if type(key) == 'table' then
|
||||
return table.concat(key, ':')
|
||||
end
|
||||
return key
|
||||
end
|
||||
|
||||
return cache
|
||||
117
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/utils/char.lua
Normal file
117
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/utils/char.lua
Normal file
@ -0,0 +1,117 @@
|
||||
local _
|
||||
|
||||
local alpha = {}
|
||||
_ = string.gsub('abcdefghijklmnopqrstuvwxyz', '.', function(char)
|
||||
alpha[string.byte(char)] = true
|
||||
end)
|
||||
|
||||
local ALPHA = {}
|
||||
_ = string.gsub('ABCDEFGHIJKLMNOPQRSTUVWXYZ', '.', function(char)
|
||||
ALPHA[string.byte(char)] = true
|
||||
end)
|
||||
|
||||
local digit = {}
|
||||
_ = string.gsub('1234567890', '.', function(char)
|
||||
digit[string.byte(char)] = true
|
||||
end)
|
||||
|
||||
local white = {}
|
||||
_ = string.gsub(' \t\n', '.', function(char)
|
||||
white[string.byte(char)] = true
|
||||
end)
|
||||
|
||||
local char = {}
|
||||
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
char.is_upper = function(byte)
|
||||
return ALPHA[byte]
|
||||
end
|
||||
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
char.is_alpha = function(byte)
|
||||
return alpha[byte] or ALPHA[byte]
|
||||
end
|
||||
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
char.is_digit = function(byte)
|
||||
return digit[byte]
|
||||
end
|
||||
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
char.is_white = function(byte)
|
||||
return white[byte]
|
||||
end
|
||||
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
char.is_symbol = function(byte)
|
||||
return not (char.is_alnum(byte) or char.is_white(byte))
|
||||
end
|
||||
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
char.is_printable = function(byte)
|
||||
return string.match(string.char(byte), '^%c$') == nil
|
||||
end
|
||||
|
||||
---@param byte integer
|
||||
---@return boolean
|
||||
char.is_alnum = function(byte)
|
||||
return char.is_alpha(byte) or char.is_digit(byte)
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param index integer
|
||||
---@return boolean
|
||||
char.is_semantic_index = function(text, index)
|
||||
if index <= 1 then
|
||||
return true
|
||||
end
|
||||
|
||||
local prev = string.byte(text, index - 1)
|
||||
local curr = string.byte(text, index)
|
||||
|
||||
if not char.is_upper(prev) and char.is_upper(curr) then
|
||||
return true
|
||||
end
|
||||
if char.is_symbol(curr) or char.is_white(curr) then
|
||||
return true
|
||||
end
|
||||
if not char.is_alpha(prev) and char.is_alpha(curr) then
|
||||
return true
|
||||
end
|
||||
if not char.is_digit(prev) and char.is_digit(curr) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param current_index integer
|
||||
---@return integer
|
||||
char.get_next_semantic_index = function(text, current_index)
|
||||
for i = current_index + 1, #text do
|
||||
if char.is_semantic_index(text, i) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #text + 1
|
||||
end
|
||||
|
||||
---Ignore case match
|
||||
---@param byte1 integer
|
||||
---@param byte2 integer
|
||||
---@return boolean
|
||||
char.match = function(byte1, byte2)
|
||||
if not char.is_alpha(byte1) or not char.is_alpha(byte2) then
|
||||
return byte1 == byte2
|
||||
end
|
||||
local diff = byte1 - byte2
|
||||
return diff == 0 or diff == 32 or diff == -32
|
||||
end
|
||||
|
||||
return char
|
||||
@ -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
|
||||
@ -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
|
||||
@ -0,0 +1,53 @@
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local misc = require('cmp.utils.misc')
|
||||
|
||||
local feedkeys = {}
|
||||
|
||||
feedkeys.call = setmetatable({
|
||||
callbacks = {},
|
||||
}, {
|
||||
__call = function(self, keys, mode, callback)
|
||||
local is_insert = string.match(mode, 'i') ~= nil
|
||||
local is_immediate = string.match(mode, 'x') ~= nil
|
||||
|
||||
local queue = {}
|
||||
if #keys > 0 then
|
||||
table.insert(queue, { keymap.t('<Cmd>setlocal lazyredraw<CR>'), 'n' })
|
||||
table.insert(queue, { keymap.t('<Cmd>setlocal textwidth=0<CR>'), 'n' })
|
||||
table.insert(queue, { keymap.t('<Cmd>setlocal backspace=2<CR>'), 'n' })
|
||||
table.insert(queue, { keys, string.gsub(mode, '[itx]', ''), true })
|
||||
table.insert(queue, { keymap.t('<Cmd>setlocal %slazyredraw<CR>'):format(vim.o.lazyredraw and '' or 'no'), 'n' })
|
||||
table.insert(queue, { keymap.t('<Cmd>setlocal textwidth=%s<CR>'):format(vim.bo.textwidth or 0), 'n' })
|
||||
table.insert(queue, { keymap.t('<Cmd>setlocal backspace=%s<CR>'):format(vim.go.backspace or 2), 'n' })
|
||||
end
|
||||
|
||||
if callback then
|
||||
local id = misc.id('cmp.utils.feedkeys.call')
|
||||
self.callbacks[id] = callback
|
||||
table.insert(queue, { keymap.t('<Cmd>lua require"cmp.utils.feedkeys".run(%s)<CR>'):format(id), 'n', true })
|
||||
end
|
||||
|
||||
if is_insert then
|
||||
for i = #queue, 1, -1 do
|
||||
vim.api.nvim_feedkeys(queue[i][1], queue[i][2] .. 'i', queue[i][3])
|
||||
end
|
||||
else
|
||||
for i = 1, #queue do
|
||||
vim.api.nvim_feedkeys(queue[i][1], queue[i][2], queue[i][3])
|
||||
end
|
||||
end
|
||||
|
||||
if is_immediate then
|
||||
vim.api.nvim_feedkeys('', 'x', true)
|
||||
end
|
||||
end,
|
||||
})
|
||||
feedkeys.run = function(id)
|
||||
if feedkeys.call.callbacks[id] then
|
||||
feedkeys.call.callbacks[id]()
|
||||
feedkeys.call.callbacks[id] = nil
|
||||
end
|
||||
return ''
|
||||
end
|
||||
|
||||
return feedkeys
|
||||
@ -0,0 +1,56 @@
|
||||
local spec = require('cmp.utils.spec')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
|
||||
local feedkeys = require('cmp.utils.feedkeys')
|
||||
|
||||
describe('feedkeys', function()
|
||||
before_each(spec.before)
|
||||
|
||||
it('dot-repeat', function()
|
||||
local reg
|
||||
feedkeys.call(keymap.t('iaiueo<Esc>'), 'nx', function()
|
||||
reg = vim.fn.getreg('.')
|
||||
end)
|
||||
assert.are.equal(reg, keymap.t('aiueo'))
|
||||
end)
|
||||
|
||||
it('textwidth', function()
|
||||
vim.cmd([[setlocal textwidth=6]])
|
||||
feedkeys.call(keymap.t('iaiueo '), 'nx')
|
||||
feedkeys.call(keymap.t('aaiueoaiueo'), 'nx')
|
||||
assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), {
|
||||
'aiueo aiueoaiueo',
|
||||
})
|
||||
end)
|
||||
|
||||
it('backspace', function()
|
||||
vim.cmd([[setlocal backspace=""]])
|
||||
feedkeys.call(keymap.t('iaiueo'), 'nx')
|
||||
feedkeys.call(keymap.t('a<BS><BS>'), 'nx')
|
||||
assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), {
|
||||
'aiu',
|
||||
})
|
||||
end)
|
||||
|
||||
it('testability', function()
|
||||
feedkeys.call('i', 'n', function()
|
||||
feedkeys.call('', 'n', function()
|
||||
feedkeys.call('aiueo', 'in')
|
||||
end)
|
||||
feedkeys.call('', 'n', function()
|
||||
feedkeys.call(keymap.t('<BS><BS><BS><BS><BS>'), 'in')
|
||||
end)
|
||||
feedkeys.call('', 'n', function()
|
||||
feedkeys.call(keymap.t('abcde'), 'in')
|
||||
end)
|
||||
feedkeys.call('', 'n', function()
|
||||
feedkeys.call(keymap.t('<BS><BS><BS><BS><BS>'), 'in')
|
||||
end)
|
||||
feedkeys.call('', 'n', function()
|
||||
feedkeys.call(keymap.t('12345'), 'in')
|
||||
end)
|
||||
end)
|
||||
feedkeys.call('', 'x')
|
||||
assert.are.same(vim.api.nvim_buf_get_lines(0, 0, -1, false), { '12345' })
|
||||
end)
|
||||
end)
|
||||
@ -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
|
||||
@ -0,0 +1,273 @@
|
||||
local misc = require('cmp.utils.misc')
|
||||
local buffer = require('cmp.utils.buffer')
|
||||
local api = require('cmp.utils.api')
|
||||
|
||||
local keymap = {}
|
||||
|
||||
---Shortcut for nvim_replace_termcodes
|
||||
---@param keys string
|
||||
---@return string
|
||||
keymap.t = function(keys)
|
||||
return (string.gsub(keys, '(<[A-Za-z0-9\\%-%[%]%^@]->)', function(match)
|
||||
return vim.api.nvim_eval(string.format([["\%s"]], match))
|
||||
end))
|
||||
end
|
||||
|
||||
---Normalize key sequence.
|
||||
---@param keys string
|
||||
---@return string
|
||||
keymap.normalize = function(keys)
|
||||
local normalize_buf = buffer.ensure('cmp.util.keymap.normalize')
|
||||
vim.api.nvim_buf_set_keymap(normalize_buf, 't', keys, '<Plug>(cmp.utils.keymap.normalize)', {})
|
||||
for _, map in ipairs(vim.api.nvim_buf_get_keymap(normalize_buf, 't')) do
|
||||
if keymap.t(map.rhs) == keymap.t('<Plug>(cmp.utils.keymap.normalize)') then
|
||||
vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys)
|
||||
return map.lhs
|
||||
end
|
||||
end
|
||||
vim.api.nvim_buf_del_keymap(normalize_buf, 't', keys)
|
||||
vim.api.nvim_buf_delete(normalize_buf, {})
|
||||
return keys
|
||||
end
|
||||
|
||||
---Return vim notation keymapping (simple conversion).
|
||||
---@param s string
|
||||
---@return string
|
||||
keymap.to_keymap = setmetatable({
|
||||
['<CR>'] = { '\n', '\r', '\r\n' },
|
||||
['<Tab>'] = { '\t' },
|
||||
['<BSlash>'] = { '\\' },
|
||||
['<Bar>'] = { '|' },
|
||||
['<Space>'] = { ' ' },
|
||||
}, {
|
||||
__call = function(self, s)
|
||||
return string.gsub(s, '.', function(c)
|
||||
for key, chars in pairs(self) do
|
||||
if vim.tbl_contains(chars, c) then
|
||||
return key
|
||||
end
|
||||
end
|
||||
return c
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
||||
---Mode safe break undo
|
||||
keymap.undobreak = function()
|
||||
if not api.is_insert_mode() then
|
||||
return ''
|
||||
end
|
||||
return keymap.t('<C-g>u')
|
||||
end
|
||||
|
||||
---Mode safe join undo
|
||||
keymap.undojoin = function()
|
||||
if not api.is_insert_mode() then
|
||||
return ''
|
||||
end
|
||||
return keymap.t('<C-g>U')
|
||||
end
|
||||
|
||||
---Create backspace keys.
|
||||
---@param count string|integer
|
||||
---@return string
|
||||
keymap.backspace = function(count)
|
||||
if type(count) == 'string' then
|
||||
count = vim.fn.strchars(count, true)
|
||||
end
|
||||
if count <= 0 then
|
||||
return ''
|
||||
end
|
||||
local keys = {}
|
||||
table.insert(keys, keymap.t(string.rep('<BS>', count)))
|
||||
return table.concat(keys, '')
|
||||
end
|
||||
|
||||
---Create delete keys.
|
||||
---@param count string|integer
|
||||
---@return string
|
||||
keymap.delete = function(count)
|
||||
if type(count) == 'string' then
|
||||
count = vim.fn.strchars(count, true)
|
||||
end
|
||||
if count <= 0 then
|
||||
return ''
|
||||
end
|
||||
local keys = {}
|
||||
table.insert(keys, keymap.t(string.rep('<Del>', count)))
|
||||
return table.concat(keys, '')
|
||||
end
|
||||
|
||||
---Update indentkeys.
|
||||
---@param expr? string
|
||||
---@return string
|
||||
keymap.indentkeys = function(expr)
|
||||
return string.format(keymap.t('<Cmd>set indentkeys=%s<CR>'), expr and vim.fn.escape(expr, '| \t\\') or '')
|
||||
end
|
||||
|
||||
---Return two key sequence are equal or not.
|
||||
---@param a string
|
||||
---@param b string
|
||||
---@return boolean
|
||||
keymap.equals = function(a, b)
|
||||
return keymap.normalize(a) == keymap.normalize(b)
|
||||
end
|
||||
|
||||
---Register keypress handler.
|
||||
keymap.listen = function(mode, lhs, callback)
|
||||
lhs = keymap.normalize(keymap.to_keymap(lhs))
|
||||
|
||||
local existing = keymap.get_map(mode, lhs)
|
||||
if existing.desc == 'cmp.utils.keymap.set_map' then
|
||||
return
|
||||
end
|
||||
|
||||
local bufnr = existing.buffer and vim.api.nvim_get_current_buf() or -1
|
||||
local fallback = keymap.fallback(bufnr, mode, existing)
|
||||
keymap.set_map(bufnr, mode, lhs, function()
|
||||
local ignore = false
|
||||
ignore = ignore or (mode == 'c' and vim.fn.getcmdtype() == '=')
|
||||
if ignore then
|
||||
fallback()
|
||||
else
|
||||
callback(lhs, misc.once(fallback))
|
||||
end
|
||||
end, {
|
||||
expr = false,
|
||||
noremap = true,
|
||||
silent = true,
|
||||
})
|
||||
end
|
||||
|
||||
---Fallback
|
||||
keymap.fallback = function(bufnr, mode, map)
|
||||
return function()
|
||||
if map.expr then
|
||||
local fallback_lhs = string.format('<Plug>(cmp.u.k.fallback_expr:%s)', map.lhs)
|
||||
keymap.set_map(bufnr, mode, fallback_lhs, function()
|
||||
return keymap.solve(bufnr, mode, map).keys
|
||||
end, {
|
||||
expr = true,
|
||||
noremap = map.noremap,
|
||||
script = map.script,
|
||||
nowait = map.nowait,
|
||||
silent = map.silent and mode ~= 'c',
|
||||
replace_keycodes = map.replace_keycodes,
|
||||
})
|
||||
vim.api.nvim_feedkeys(keymap.t(fallback_lhs), 'im', true)
|
||||
elseif map.callback then
|
||||
map.callback()
|
||||
else
|
||||
local solved = keymap.solve(bufnr, mode, map)
|
||||
vim.api.nvim_feedkeys(solved.keys, solved.mode, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Solve
|
||||
keymap.solve = function(bufnr, mode, map)
|
||||
local lhs = keymap.t(map.lhs)
|
||||
local rhs = keymap.t(map.rhs)
|
||||
if map.expr then
|
||||
if map.callback then
|
||||
rhs = map.callback()
|
||||
else
|
||||
rhs = vim.api.nvim_eval(keymap.t(map.rhs))
|
||||
end
|
||||
end
|
||||
|
||||
if map.noremap then
|
||||
return { keys = rhs, mode = 'in' }
|
||||
end
|
||||
|
||||
if string.find(rhs, lhs, 1, true) == 1 then
|
||||
local recursive = string.format('<SNR>0_(cmp.u.k.recursive:%s)', lhs)
|
||||
keymap.set_map(bufnr, mode, recursive, lhs, {
|
||||
noremap = true,
|
||||
script = true,
|
||||
nowait = map.nowait,
|
||||
silent = map.silent and mode ~= 'c',
|
||||
replace_keycodes = map.replace_keycodes,
|
||||
})
|
||||
return { keys = keymap.t(recursive) .. string.gsub(rhs, '^' .. vim.pesc(lhs), ''), mode = 'im' }
|
||||
end
|
||||
return { keys = rhs, mode = 'im' }
|
||||
end
|
||||
|
||||
---Get map
|
||||
---@param mode string
|
||||
---@param lhs string
|
||||
---@return table
|
||||
keymap.get_map = function(mode, lhs)
|
||||
lhs = keymap.normalize(lhs)
|
||||
|
||||
for _, map in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do
|
||||
if keymap.equals(map.lhs, lhs) then
|
||||
return {
|
||||
lhs = map.lhs,
|
||||
rhs = map.rhs or '',
|
||||
expr = map.expr == 1,
|
||||
callback = map.callback,
|
||||
desc = map.desc,
|
||||
noremap = map.noremap == 1,
|
||||
script = map.script == 1,
|
||||
silent = map.silent == 1,
|
||||
nowait = map.nowait == 1,
|
||||
buffer = true,
|
||||
replace_keycodes = map.replace_keycodes == 1,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
for _, map in ipairs(vim.api.nvim_get_keymap(mode)) do
|
||||
if keymap.equals(map.lhs, lhs) then
|
||||
return {
|
||||
lhs = map.lhs,
|
||||
rhs = map.rhs or '',
|
||||
expr = map.expr == 1,
|
||||
callback = map.callback,
|
||||
desc = map.desc,
|
||||
noremap = map.noremap == 1,
|
||||
script = map.script == 1,
|
||||
silent = map.silent == 1,
|
||||
nowait = map.nowait == 1,
|
||||
buffer = false,
|
||||
replace_keycodes = map.replace_keycodes == 1,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
lhs = lhs,
|
||||
rhs = lhs,
|
||||
expr = false,
|
||||
callback = nil,
|
||||
noremap = true,
|
||||
script = false,
|
||||
silent = true,
|
||||
nowait = false,
|
||||
buffer = false,
|
||||
replace_keycodes = true,
|
||||
}
|
||||
end
|
||||
|
||||
---Set keymapping
|
||||
keymap.set_map = function(bufnr, mode, lhs, rhs, opts)
|
||||
if type(rhs) == 'function' then
|
||||
opts.callback = rhs
|
||||
rhs = ''
|
||||
end
|
||||
opts.desc = 'cmp.utils.keymap.set_map'
|
||||
|
||||
if vim.fn.has('nvim-0.8') == 0 then
|
||||
opts.replace_keycodes = nil
|
||||
end
|
||||
|
||||
if bufnr == -1 then
|
||||
vim.api.nvim_set_keymap(mode, lhs, rhs, opts)
|
||||
else
|
||||
vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts)
|
||||
end
|
||||
end
|
||||
|
||||
return keymap
|
||||
@ -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)
|
||||
253
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/utils/misc.lua
Normal file
253
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/utils/misc.lua
Normal file
@ -0,0 +1,253 @@
|
||||
local misc = {}
|
||||
|
||||
local islist = vim.islist or vim.tbl_islist
|
||||
|
||||
---Create once callback
|
||||
---@param callback function
|
||||
---@return function
|
||||
misc.once = function(callback)
|
||||
local done = false
|
||||
return function(...)
|
||||
if done then
|
||||
return
|
||||
end
|
||||
done = true
|
||||
callback(...)
|
||||
end
|
||||
end
|
||||
|
||||
---Return concatenated list
|
||||
---@param list1 any[]
|
||||
---@param list2 any[]
|
||||
---@return any[]
|
||||
misc.concat = function(list1, list2)
|
||||
local new_list = {}
|
||||
for _, v in ipairs(list1) do
|
||||
table.insert(new_list, v)
|
||||
end
|
||||
for _, v in ipairs(list2) do
|
||||
table.insert(new_list, v)
|
||||
end
|
||||
return new_list
|
||||
end
|
||||
|
||||
---Repeat values
|
||||
---@generic T
|
||||
---@param str_or_tbl T
|
||||
---@param count integer
|
||||
---@return T
|
||||
misc.rep = function(str_or_tbl, count)
|
||||
if type(str_or_tbl) == 'string' then
|
||||
return string.rep(str_or_tbl, count)
|
||||
end
|
||||
local rep = {}
|
||||
for _ = 1, count do
|
||||
for _, v in ipairs(str_or_tbl) do
|
||||
table.insert(rep, v)
|
||||
end
|
||||
end
|
||||
return rep
|
||||
end
|
||||
|
||||
---Return the valu is empty or not.
|
||||
---@param v any
|
||||
---@return boolean
|
||||
misc.empty = function(v)
|
||||
if not v then
|
||||
return true
|
||||
end
|
||||
if v == vim.NIL then
|
||||
return true
|
||||
end
|
||||
if type(v) == 'string' and v == '' then
|
||||
return true
|
||||
end
|
||||
if type(v) == 'table' and vim.tbl_isempty(v) then
|
||||
return true
|
||||
end
|
||||
if type(v) == 'number' and v == 0 then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---Search value in table
|
||||
misc.contains = function(tbl, v)
|
||||
for _, value in ipairs(tbl) do
|
||||
if value == v then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---The symbol to remove key in misc.merge.
|
||||
misc.none = vim.NIL
|
||||
|
||||
---Merge two tables recursively
|
||||
---@generic T
|
||||
---@param tbl1 T
|
||||
---@param tbl2 T
|
||||
---@return T
|
||||
misc.merge = function(tbl1, tbl2)
|
||||
local is_dict1 = type(tbl1) == 'table' and (not islist(tbl1) or vim.tbl_isempty(tbl1))
|
||||
local is_dict2 = type(tbl2) == 'table' and (not islist(tbl2) or vim.tbl_isempty(tbl2))
|
||||
if is_dict1 and is_dict2 then
|
||||
local new_tbl = {}
|
||||
for k, v in pairs(tbl2) do
|
||||
if tbl1[k] ~= misc.none then
|
||||
new_tbl[k] = misc.merge(tbl1[k], v)
|
||||
end
|
||||
end
|
||||
for k, v in pairs(tbl1) do
|
||||
if tbl2[k] == nil then
|
||||
if v ~= misc.none then
|
||||
new_tbl[k] = misc.merge(v, {})
|
||||
else
|
||||
new_tbl[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return new_tbl
|
||||
end
|
||||
|
||||
if tbl1 == misc.none then
|
||||
return nil
|
||||
elseif tbl1 == nil then
|
||||
return misc.merge(tbl2, {})
|
||||
else
|
||||
return tbl1
|
||||
end
|
||||
end
|
||||
|
||||
---Generate id for group name
|
||||
misc.id = setmetatable({
|
||||
group = {},
|
||||
}, {
|
||||
__call = function(_, group)
|
||||
misc.id.group[group] = misc.id.group[group] or 0
|
||||
misc.id.group[group] = misc.id.group[group] + 1
|
||||
return misc.id.group[group]
|
||||
end,
|
||||
})
|
||||
|
||||
---Treat 1/0 as bool value
|
||||
---@param v boolean|1|0
|
||||
---@param def boolean
|
||||
---@return boolean
|
||||
misc.bool = function(v, def)
|
||||
if v == nil then
|
||||
return def
|
||||
end
|
||||
return v == true or v == 1
|
||||
end
|
||||
|
||||
---Set value to deep object
|
||||
---@param t table
|
||||
---@param keys string[]
|
||||
---@param v any
|
||||
misc.set = function(t, keys, v)
|
||||
local c = t
|
||||
for i = 1, #keys - 1 do
|
||||
local key = keys[i]
|
||||
c[key] = c[key] or {}
|
||||
c = c[key]
|
||||
end
|
||||
c[keys[#keys]] = v
|
||||
end
|
||||
|
||||
---Copy table
|
||||
---@generic T
|
||||
---@param tbl T
|
||||
---@return T
|
||||
misc.copy = function(tbl)
|
||||
if type(tbl) ~= 'table' then
|
||||
return tbl
|
||||
end
|
||||
|
||||
if islist(tbl) then
|
||||
local copy = {}
|
||||
for i, value in ipairs(tbl) do
|
||||
copy[i] = misc.copy(value)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
local copy = {}
|
||||
for key, value in pairs(tbl) do
|
||||
copy[key] = misc.copy(value)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
---Safe version of vim.str_utfindex
|
||||
---@param text string
|
||||
---@param vimindex integer|nil
|
||||
---@return integer
|
||||
misc.to_utfindex = function(text, vimindex)
|
||||
vimindex = vimindex or #text + 1
|
||||
return vim.str_utfindex(text, math.max(0, math.min(vimindex - 1, #text)))
|
||||
end
|
||||
|
||||
---Safe version of vim.str_byteindex
|
||||
---@param text string
|
||||
---@param utfindex integer
|
||||
---@return integer
|
||||
misc.to_vimindex = function(text, utfindex)
|
||||
utfindex = utfindex or #text
|
||||
for i = utfindex, 1, -1 do
|
||||
local s, v = pcall(function()
|
||||
return vim.str_byteindex(text, i) + 1
|
||||
end)
|
||||
if s then
|
||||
return v
|
||||
end
|
||||
end
|
||||
return utfindex + 1
|
||||
end
|
||||
|
||||
---Mark the function as deprecated
|
||||
misc.deprecated = function(fn, msg)
|
||||
local printed = false
|
||||
return function(...)
|
||||
if not printed then
|
||||
print(msg)
|
||||
printed = true
|
||||
end
|
||||
return fn(...)
|
||||
end
|
||||
end
|
||||
|
||||
--Redraw
|
||||
misc.redraw = setmetatable({
|
||||
doing = false,
|
||||
force = false,
|
||||
-- We use `<Up><Down>` to redraw the screen. (Previously, We use <C-r><ESC>. it will remove the unmatches search history.)
|
||||
incsearch_redraw_keys = ' <BS>',
|
||||
}, {
|
||||
__call = function(self, force)
|
||||
local termcode = vim.api.nvim_replace_termcodes(self.incsearch_redraw_keys, true, true, true)
|
||||
if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then
|
||||
if vim.o.incsearch then
|
||||
return vim.api.nvim_feedkeys(termcode, 'ni', true)
|
||||
end
|
||||
end
|
||||
|
||||
if self.doing then
|
||||
return
|
||||
end
|
||||
self.doing = true
|
||||
self.force = not not force
|
||||
vim.schedule(function()
|
||||
if self.force then
|
||||
vim.cmd([[redraw!]])
|
||||
else
|
||||
vim.cmd([[redraw]])
|
||||
end
|
||||
self.doing = false
|
||||
self.force = false
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
||||
return misc
|
||||
@ -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)
|
||||
@ -0,0 +1,25 @@
|
||||
local M = {}
|
||||
|
||||
-- Set window option without triggering the OptionSet event
|
||||
---@param window number
|
||||
---@param name string
|
||||
---@param value any
|
||||
M.win_set_option = function(window, name, value)
|
||||
local eventignore = vim.opt.eventignore:get()
|
||||
vim.opt.eventignore:append('OptionSet')
|
||||
vim.api.nvim_win_set_option(window, name, value)
|
||||
vim.opt.eventignore = eventignore
|
||||
end
|
||||
|
||||
-- Set buffer option without triggering the OptionSet event
|
||||
---@param buffer number
|
||||
---@param name string
|
||||
---@param value any
|
||||
M.buf_set_option = function(buffer, name, value)
|
||||
local eventignore = vim.opt.eventignore:get()
|
||||
vim.opt.eventignore:append('OptionSet')
|
||||
vim.api.nvim_buf_set_option(buffer, name, value)
|
||||
vim.opt.eventignore = eventignore
|
||||
end
|
||||
|
||||
return M
|
||||
@ -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
|
||||
@ -0,0 +1,414 @@
|
||||
local misc = require('cmp.utils.misc')
|
||||
|
||||
local P = {}
|
||||
|
||||
---Take characters until the target characters (The escape sequence is '\' + char)
|
||||
---@param targets string[] The character list for stop consuming text.
|
||||
---@param specials string[] If the character isn't contained in targets/specials, '\' will be left.
|
||||
P.take_until = function(targets, specials)
|
||||
targets = targets or {}
|
||||
specials = specials or {}
|
||||
|
||||
return function(input, pos)
|
||||
local new_pos = pos
|
||||
local raw = {}
|
||||
local esc = {}
|
||||
while new_pos <= #input do
|
||||
local c = string.sub(input, new_pos, new_pos)
|
||||
if c == '\\' then
|
||||
table.insert(raw, '\\')
|
||||
new_pos = new_pos + 1
|
||||
c = string.sub(input, new_pos, new_pos)
|
||||
if not misc.contains(targets, c) and not misc.contains(specials, c) then
|
||||
table.insert(esc, '\\')
|
||||
end
|
||||
table.insert(raw, c)
|
||||
table.insert(esc, c)
|
||||
new_pos = new_pos + 1
|
||||
else
|
||||
if misc.contains(targets, c) then
|
||||
break
|
||||
end
|
||||
table.insert(raw, c)
|
||||
table.insert(esc, c)
|
||||
new_pos = new_pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if new_pos == pos then
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
|
||||
return {
|
||||
parsed = true,
|
||||
value = {
|
||||
raw = table.concat(raw, ''),
|
||||
esc = table.concat(esc, ''),
|
||||
},
|
||||
pos = new_pos,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
P.unmatch = function(pos)
|
||||
return {
|
||||
parsed = false,
|
||||
value = nil,
|
||||
pos = pos,
|
||||
}
|
||||
end
|
||||
|
||||
P.map = function(parser, map)
|
||||
return function(input, pos)
|
||||
local result = parser(input, pos)
|
||||
if result.parsed then
|
||||
return {
|
||||
parsed = true,
|
||||
value = map(result.value),
|
||||
pos = result.pos,
|
||||
}
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
P.lazy = function(factory)
|
||||
return function(input, pos)
|
||||
return factory()(input, pos)
|
||||
end
|
||||
end
|
||||
|
||||
P.token = function(token)
|
||||
return function(input, pos)
|
||||
local maybe_token = string.sub(input, pos, pos + #token - 1)
|
||||
if token == maybe_token then
|
||||
return {
|
||||
parsed = true,
|
||||
value = maybe_token,
|
||||
pos = pos + #token,
|
||||
}
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
P.pattern = function(p)
|
||||
return function(input, pos)
|
||||
local maybe_match = string.match(string.sub(input, pos), '^' .. p)
|
||||
if maybe_match then
|
||||
return {
|
||||
parsed = true,
|
||||
value = maybe_match,
|
||||
pos = pos + #maybe_match,
|
||||
}
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
P.many = function(parser)
|
||||
return function(input, pos)
|
||||
local values = {}
|
||||
local new_pos = pos
|
||||
while new_pos <= #input do
|
||||
local result = parser(input, new_pos)
|
||||
if not result.parsed then
|
||||
break
|
||||
end
|
||||
table.insert(values, result.value)
|
||||
new_pos = result.pos
|
||||
end
|
||||
if #values > 0 then
|
||||
return {
|
||||
parsed = true,
|
||||
value = values,
|
||||
pos = new_pos,
|
||||
}
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
P.any = function(...)
|
||||
local parsers = { ... }
|
||||
return function(input, pos)
|
||||
for _, parser in ipairs(parsers) do
|
||||
local result = parser(input, pos)
|
||||
if result.parsed then
|
||||
return result
|
||||
end
|
||||
end
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
|
||||
P.opt = function(parser)
|
||||
return function(input, pos)
|
||||
local result = parser(input, pos)
|
||||
return {
|
||||
parsed = true,
|
||||
value = result.value,
|
||||
pos = result.pos,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
P.seq = function(...)
|
||||
local parsers = { ... }
|
||||
return function(input, pos)
|
||||
local values = {}
|
||||
local new_pos = pos
|
||||
for i, parser in ipairs(parsers) do
|
||||
local result = parser(input, new_pos)
|
||||
if result.parsed then
|
||||
values[i] = result.value
|
||||
new_pos = result.pos
|
||||
else
|
||||
return P.unmatch(pos)
|
||||
end
|
||||
end
|
||||
return {
|
||||
parsed = true,
|
||||
value = values,
|
||||
pos = new_pos,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local Node = {}
|
||||
|
||||
Node.Type = {
|
||||
SNIPPET = 0,
|
||||
TABSTOP = 1,
|
||||
PLACEHOLDER = 2,
|
||||
VARIABLE = 3,
|
||||
CHOICE = 4,
|
||||
TRANSFORM = 5,
|
||||
FORMAT = 6,
|
||||
TEXT = 7,
|
||||
}
|
||||
|
||||
function Node:__tostring()
|
||||
local insert_text = {}
|
||||
if self.type == Node.Type.SNIPPET then
|
||||
for _, c in ipairs(self.children) do
|
||||
table.insert(insert_text, tostring(c))
|
||||
end
|
||||
elseif self.type == Node.Type.CHOICE then
|
||||
table.insert(insert_text, self.items[1])
|
||||
elseif self.type == Node.Type.PLACEHOLDER then
|
||||
for _, c in ipairs(self.children or {}) do
|
||||
table.insert(insert_text, tostring(c))
|
||||
end
|
||||
elseif self.type == Node.Type.TEXT then
|
||||
table.insert(insert_text, self.esc)
|
||||
end
|
||||
return table.concat(insert_text, '')
|
||||
end
|
||||
|
||||
--@see https://code.visualstudio.com/docs/editor/userdefinedsnippets#_grammar
|
||||
|
||||
local S = {}
|
||||
S.dollar = P.token('$')
|
||||
S.open = P.token('{')
|
||||
S.close = P.token('}')
|
||||
S.colon = P.token(':')
|
||||
S.slash = P.token('/')
|
||||
S.comma = P.token(',')
|
||||
S.pipe = P.token('|')
|
||||
S.plus = P.token('+')
|
||||
S.minus = P.token('-')
|
||||
S.question = P.token('?')
|
||||
S.int = P.map(P.pattern('[0-9]+'), function(value)
|
||||
return tonumber(value, 10)
|
||||
end)
|
||||
S.var = P.pattern('[%a_][%w_]+')
|
||||
S.text = function(targets, specials)
|
||||
return P.map(P.take_until(targets, specials), function(value)
|
||||
return setmetatable({
|
||||
type = Node.Type.TEXT,
|
||||
raw = value.raw,
|
||||
esc = value.esc,
|
||||
}, Node)
|
||||
end)
|
||||
end
|
||||
|
||||
S.toplevel = P.lazy(function()
|
||||
return P.any(S.placeholder, S.tabstop, S.variable, S.choice)
|
||||
end)
|
||||
|
||||
S.format = P.any(
|
||||
P.map(P.seq(S.dollar, S.int), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.FORMAT,
|
||||
capture_index = values[2],
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.FORMAT,
|
||||
capture_index = values[3],
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.slash, P.any(P.token('upcase'), P.token('downcase'), P.token('capitalize'), P.token('camelcase'), P.token('pascalcase')), S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.FORMAT,
|
||||
capture_index = values[3],
|
||||
modifier = values[6],
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.seq(S.question, P.opt(P.take_until({ ':' }, { '\\' })), S.colon, P.opt(P.take_until({ '}' }, { '\\' }))), S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.FORMAT,
|
||||
capture_index = values[3],
|
||||
if_text = values[5][2] and values[5][2].esc or '',
|
||||
else_text = values[5][4] and values[5][4].esc or '',
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.seq(S.plus, P.opt(P.take_until({ '}' }, { '\\' }))), S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.FORMAT,
|
||||
capture_index = values[3],
|
||||
if_text = values[5][2] and values[5][2].esc or '',
|
||||
else_text = '',
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.colon, S.minus, P.opt(P.take_until({ '}' }, { '\\' })), S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.FORMAT,
|
||||
capture_index = values[3],
|
||||
if_text = '',
|
||||
else_text = values[6] and values[6].esc or '',
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.opt(P.take_until({ '}' }, { '\\' })), S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.FORMAT,
|
||||
capture_index = values[3],
|
||||
if_text = '',
|
||||
else_text = values[5] and values[5].esc or '',
|
||||
}, Node)
|
||||
end)
|
||||
)
|
||||
|
||||
S.transform = P.map(P.seq(S.slash, P.take_until({ '/' }, { '\\' }), S.slash, P.many(P.any(S.format, S.text({ '$', '/' }, { '\\' }))), S.slash, P.opt(P.pattern('[ig]+'))), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.TRANSFORM,
|
||||
pattern = values[2].raw,
|
||||
format = values[4],
|
||||
option = values[6],
|
||||
}, Node)
|
||||
end)
|
||||
|
||||
S.tabstop = P.any(
|
||||
P.map(P.seq(S.dollar, S.int), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.TABSTOP,
|
||||
tabstop = values[2],
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.TABSTOP,
|
||||
tabstop = values[3],
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.int, S.transform, S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.TABSTOP,
|
||||
tabstop = values[3],
|
||||
transform = values[4],
|
||||
}, Node)
|
||||
end)
|
||||
)
|
||||
|
||||
S.placeholder = P.any(P.map(P.seq(S.dollar, S.open, S.int, S.colon, P.opt(P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' })))), S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.PLACEHOLDER,
|
||||
tabstop = values[3],
|
||||
-- insert empty text if opt did not match.
|
||||
children = values[5] or {
|
||||
setmetatable({
|
||||
type = Node.Type.TEXT,
|
||||
raw = '',
|
||||
esc = '',
|
||||
}, Node),
|
||||
},
|
||||
}, Node)
|
||||
end))
|
||||
|
||||
S.choice = P.map(
|
||||
P.seq(
|
||||
S.dollar,
|
||||
S.open,
|
||||
S.int,
|
||||
S.pipe,
|
||||
P.many(P.map(P.seq(S.text({ ',', '|' }), P.opt(S.comma)), function(values)
|
||||
return values[1].esc
|
||||
end)),
|
||||
S.pipe,
|
||||
S.close
|
||||
),
|
||||
function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.CHOICE,
|
||||
tabstop = values[3],
|
||||
items = values[5],
|
||||
}, Node)
|
||||
end
|
||||
)
|
||||
|
||||
S.variable = P.any(
|
||||
P.map(P.seq(S.dollar, S.var), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.VARIABLE,
|
||||
name = values[2],
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.var, S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.VARIABLE,
|
||||
name = values[3],
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.var, S.transform, S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.VARIABLE,
|
||||
name = values[3],
|
||||
transform = values[4],
|
||||
}, Node)
|
||||
end),
|
||||
P.map(P.seq(S.dollar, S.open, S.var, S.colon, P.many(P.any(S.toplevel, S.text({ '$', '}' }, { '\\' }))), S.close), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.VARIABLE,
|
||||
name = values[3],
|
||||
children = values[5],
|
||||
}, Node)
|
||||
end)
|
||||
)
|
||||
|
||||
S.snippet = P.map(P.many(P.any(S.toplevel, S.text({ '$' }, { '}', '\\' }))), function(values)
|
||||
return setmetatable({
|
||||
type = Node.Type.SNIPPET,
|
||||
children = values,
|
||||
}, Node)
|
||||
end)
|
||||
|
||||
local M = {}
|
||||
|
||||
---The snippet node type enum
|
||||
---@types table<string, integer>
|
||||
M.NodeType = Node.Type
|
||||
|
||||
---Parse snippet string and returns the AST
|
||||
---@param input string
|
||||
---@return table
|
||||
function M.parse(input)
|
||||
local result = S.snippet(input, 1)
|
||||
if not result.parsed then
|
||||
error('snippet parsing failed.')
|
||||
end
|
||||
return result.value
|
||||
end
|
||||
|
||||
return M
|
||||
@ -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
|
||||
178
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/utils/str.lua
Normal file
178
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/utils/str.lua
Normal file
@ -0,0 +1,178 @@
|
||||
local char = require('cmp.utils.char')
|
||||
|
||||
local str = {}
|
||||
|
||||
local INVALIDS = {}
|
||||
INVALIDS[string.byte("'")] = true
|
||||
INVALIDS[string.byte('"')] = true
|
||||
INVALIDS[string.byte('=')] = true
|
||||
INVALIDS[string.byte('$')] = true
|
||||
INVALIDS[string.byte('(')] = true
|
||||
INVALIDS[string.byte('[')] = true
|
||||
INVALIDS[string.byte('<')] = true
|
||||
INVALIDS[string.byte('{')] = true
|
||||
INVALIDS[string.byte(' ')] = true
|
||||
INVALIDS[string.byte('\t')] = true
|
||||
INVALIDS[string.byte('\n')] = true
|
||||
INVALIDS[string.byte('\r')] = true
|
||||
|
||||
local NR_BYTE = string.byte('\n')
|
||||
|
||||
local PAIRS = {}
|
||||
PAIRS[string.byte('<')] = string.byte('>')
|
||||
PAIRS[string.byte('[')] = string.byte(']')
|
||||
PAIRS[string.byte('(')] = string.byte(')')
|
||||
PAIRS[string.byte('{')] = string.byte('}')
|
||||
PAIRS[string.byte('"')] = string.byte('"')
|
||||
PAIRS[string.byte("'")] = string.byte("'")
|
||||
|
||||
---Return if specified text has prefix or not
|
||||
---@param text string
|
||||
---@param prefix string
|
||||
---@return boolean
|
||||
str.has_prefix = function(text, prefix)
|
||||
if #text < #prefix then
|
||||
return false
|
||||
end
|
||||
for i = 1, #prefix do
|
||||
if not char.match(string.byte(text, i), string.byte(prefix, i)) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---get_common_string
|
||||
str.get_common_string = function(text1, text2)
|
||||
local min = math.min(#text1, #text2)
|
||||
for i = 1, min do
|
||||
if not char.match(string.byte(text1, i), string.byte(text2, i)) then
|
||||
return string.sub(text1, 1, i - 1)
|
||||
end
|
||||
end
|
||||
return string.sub(text1, 1, min)
|
||||
end
|
||||
|
||||
---Remove suffix
|
||||
---@param text string
|
||||
---@param suffix string
|
||||
---@return string
|
||||
str.remove_suffix = function(text, suffix)
|
||||
if #text < #suffix then
|
||||
return text
|
||||
end
|
||||
|
||||
local i = 0
|
||||
while i < #suffix do
|
||||
if string.byte(text, #text - i) ~= string.byte(suffix, #suffix - i) then
|
||||
return text
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
return string.sub(text, 1, -#suffix - 1)
|
||||
end
|
||||
|
||||
---trim
|
||||
---@param text string
|
||||
---@return string
|
||||
str.trim = function(text)
|
||||
local s = 1
|
||||
for i = 1, #text do
|
||||
if not char.is_white(string.byte(text, i)) then
|
||||
s = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local e = #text
|
||||
for i = #text, 1, -1 do
|
||||
if not char.is_white(string.byte(text, i)) then
|
||||
e = i
|
||||
break
|
||||
end
|
||||
end
|
||||
if s == 1 and e == #text then
|
||||
return text
|
||||
end
|
||||
return string.sub(text, s, e)
|
||||
end
|
||||
|
||||
---get_word
|
||||
---@param text string
|
||||
---@param stop_char? integer
|
||||
---@param min_length? integer
|
||||
---@return string
|
||||
str.get_word = function(text, stop_char, min_length)
|
||||
min_length = min_length or 0
|
||||
|
||||
local has_alnum = false
|
||||
local stack = {}
|
||||
local word = {}
|
||||
local add = function(c)
|
||||
table.insert(word, string.char(c))
|
||||
if stack[#stack] == c then
|
||||
table.remove(stack, #stack)
|
||||
else
|
||||
if PAIRS[c] then
|
||||
table.insert(stack, c)
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = 1, #text do
|
||||
local c = string.byte(text, i, i)
|
||||
if #word < min_length then
|
||||
table.insert(word, string.char(c))
|
||||
elseif not INVALIDS[c] then
|
||||
add(c)
|
||||
has_alnum = has_alnum or char.is_alnum(c)
|
||||
elseif not has_alnum then
|
||||
add(c)
|
||||
elseif #stack ~= 0 then
|
||||
add(c)
|
||||
if has_alnum and #stack == 0 then
|
||||
break
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
if stop_char and word[#word] == string.char(stop_char) then
|
||||
table.remove(word, #word)
|
||||
end
|
||||
return table.concat(word, '')
|
||||
end
|
||||
|
||||
---Oneline
|
||||
---@param text string
|
||||
---@return string
|
||||
str.oneline = function(text)
|
||||
for i = 1, #text do
|
||||
if string.byte(text, i) == NR_BYTE then
|
||||
return string.sub(text, 1, i - 1)
|
||||
end
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
---Escape special chars
|
||||
---@param text string
|
||||
---@param chars string[]
|
||||
---@return string
|
||||
str.escape = function(text, chars)
|
||||
table.insert(chars, '\\')
|
||||
local escaped = {}
|
||||
local i = 1
|
||||
while i <= #text do
|
||||
local c = string.sub(text, i, i)
|
||||
if vim.tbl_contains(chars, c) then
|
||||
table.insert(escaped, '\\')
|
||||
table.insert(escaped, c)
|
||||
else
|
||||
table.insert(escaped, c)
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
return table.concat(escaped, '')
|
||||
end
|
||||
|
||||
return str
|
||||
@ -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)
|
||||
@ -0,0 +1,318 @@
|
||||
local misc = require('cmp.utils.misc')
|
||||
local opt = require('cmp.utils.options')
|
||||
local buffer = require('cmp.utils.buffer')
|
||||
local api = require('cmp.utils.api')
|
||||
local config = require('cmp.config')
|
||||
|
||||
---@class cmp.WindowStyle
|
||||
---@field public relative string
|
||||
---@field public row integer
|
||||
---@field public col integer
|
||||
---@field public width integer|float
|
||||
---@field public height integer|float
|
||||
---@field public border string|string[]|nil
|
||||
---@field public zindex integer|nil
|
||||
|
||||
---@class cmp.Window
|
||||
---@field public name string
|
||||
---@field public win integer|nil
|
||||
---@field public thumb_win integer|nil
|
||||
---@field public sbar_win integer|nil
|
||||
---@field public style cmp.WindowStyle
|
||||
---@field public opt table<string, any>
|
||||
---@field public buffer_opt table<string, any>
|
||||
local window = {}
|
||||
|
||||
---new
|
||||
---@return cmp.Window
|
||||
window.new = function()
|
||||
local self = setmetatable({}, { __index = window })
|
||||
self.name = misc.id('cmp.utils.window.new')
|
||||
self.win = nil
|
||||
self.sbar_win = nil
|
||||
self.thumb_win = nil
|
||||
self.style = {}
|
||||
self.opt = {}
|
||||
self.buffer_opt = {}
|
||||
return self
|
||||
end
|
||||
|
||||
---Set window option.
|
||||
---NOTE: If the window already visible, immediately applied to it.
|
||||
---@param key string
|
||||
---@param value any
|
||||
window.option = function(self, key, value)
|
||||
if vim.fn.exists('+' .. key) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if value == nil then
|
||||
return self.opt[key]
|
||||
end
|
||||
|
||||
self.opt[key] = value
|
||||
if self:visible() then
|
||||
opt.win_set_option(self.win, key, value)
|
||||
end
|
||||
end
|
||||
|
||||
---Set buffer option.
|
||||
---NOTE: If the buffer already visible, immediately applied to it.
|
||||
---@param key string
|
||||
---@param value any
|
||||
window.buffer_option = function(self, key, value)
|
||||
if vim.fn.exists('+' .. key) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if value == nil then
|
||||
return self.buffer_opt[key]
|
||||
end
|
||||
|
||||
self.buffer_opt[key] = value
|
||||
local existing_buf = buffer.get(self.name)
|
||||
if existing_buf then
|
||||
opt.buf_set_option(existing_buf, key, value)
|
||||
end
|
||||
end
|
||||
|
||||
---Set style.
|
||||
---@param style cmp.WindowStyle
|
||||
window.set_style = function(self, style)
|
||||
self.style = style
|
||||
local info = self:info()
|
||||
|
||||
if vim.o.lines and vim.o.lines <= info.row + info.height + 1 then
|
||||
self.style.height = vim.o.lines - info.row - info.border_info.vert - 1
|
||||
end
|
||||
|
||||
self.style.zindex = self.style.zindex or 1
|
||||
|
||||
--- GUI clients are allowed to return fractional bounds, but we need integer
|
||||
--- bounds to open the window
|
||||
self.style.width = math.ceil(self.style.width)
|
||||
self.style.height = math.ceil(self.style.height)
|
||||
end
|
||||
|
||||
---Return buffer id.
|
||||
---@return integer
|
||||
window.get_buffer = function(self)
|
||||
local buf, created_new = buffer.ensure(self.name)
|
||||
if created_new then
|
||||
for k, v in pairs(self.buffer_opt) do
|
||||
opt.buf_set_option(buf, k, v)
|
||||
end
|
||||
end
|
||||
return buf
|
||||
end
|
||||
|
||||
---Open window
|
||||
---@param style cmp.WindowStyle
|
||||
window.open = function(self, style)
|
||||
if style then
|
||||
self:set_style(style)
|
||||
end
|
||||
|
||||
if self.style.width < 1 or self.style.height < 1 then
|
||||
return
|
||||
end
|
||||
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
vim.api.nvim_win_set_config(self.win, self.style)
|
||||
else
|
||||
local s = misc.copy(self.style)
|
||||
s.noautocmd = true
|
||||
self.win = vim.api.nvim_open_win(self:get_buffer(), false, s)
|
||||
for k, v in pairs(self.opt) do
|
||||
opt.win_set_option(self.win, k, v)
|
||||
end
|
||||
end
|
||||
self:update()
|
||||
end
|
||||
|
||||
---Update
|
||||
window.update = function(self)
|
||||
local info = self:info()
|
||||
if info.scrollable and self.style.height > 0 then
|
||||
-- Draw the background of the scrollbar
|
||||
|
||||
if not info.border_info.visible then
|
||||
local style = {
|
||||
relative = 'editor',
|
||||
style = 'minimal',
|
||||
width = 1,
|
||||
height = self.style.height,
|
||||
row = info.row,
|
||||
col = info.col + info.width - info.scrollbar_offset, -- info.col was already contained the scrollbar offset.
|
||||
zindex = (self.style.zindex and (self.style.zindex + 1) or 1),
|
||||
}
|
||||
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
|
||||
vim.api.nvim_win_set_config(self.sbar_win, style)
|
||||
else
|
||||
style.noautocmd = true
|
||||
self.sbar_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbar_buf'), false, style)
|
||||
opt.win_set_option(self.sbar_win, 'winhighlight', 'EndOfBuffer:PmenuSbar,NormalFloat:PmenuSbar')
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw the scrollbar thumb
|
||||
local thumb_height = math.floor(info.inner_height * (info.inner_height / self:get_content_height()) + 0.5)
|
||||
local thumb_offset = math.floor(info.inner_height * (vim.fn.getwininfo(self.win)[1].topline / self:get_content_height()))
|
||||
|
||||
local style = {
|
||||
relative = 'editor',
|
||||
style = 'minimal',
|
||||
width = 1,
|
||||
height = math.max(1, thumb_height),
|
||||
row = info.row + thumb_offset + (info.border_info.visible and info.border_info.top or 0),
|
||||
col = info.col + info.width - 1, -- info.col was already added scrollbar offset.
|
||||
zindex = (self.style.zindex and (self.style.zindex + 2) or 2),
|
||||
}
|
||||
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
|
||||
vim.api.nvim_win_set_config(self.thumb_win, style)
|
||||
else
|
||||
style.noautocmd = true
|
||||
self.thumb_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'thumb_buf'), false, style)
|
||||
opt.win_set_option(self.thumb_win, 'winhighlight', 'EndOfBuffer:PmenuThumb,NormalFloat:PmenuThumb')
|
||||
end
|
||||
else
|
||||
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
|
||||
vim.api.nvim_win_hide(self.sbar_win)
|
||||
self.sbar_win = nil
|
||||
end
|
||||
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
|
||||
vim.api.nvim_win_hide(self.thumb_win)
|
||||
self.thumb_win = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- In cmdline, vim does not redraw automatically.
|
||||
if api.is_cmdline_mode() then
|
||||
vim.api.nvim_win_call(self.win, function()
|
||||
misc.redraw()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---Close window
|
||||
window.close = function(self)
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
if self.win and vim.api.nvim_win_is_valid(self.win) then
|
||||
vim.api.nvim_win_hide(self.win)
|
||||
self.win = nil
|
||||
end
|
||||
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
|
||||
vim.api.nvim_win_hide(self.sbar_win)
|
||||
self.sbar_win = nil
|
||||
end
|
||||
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
|
||||
vim.api.nvim_win_hide(self.thumb_win)
|
||||
self.thumb_win = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Return the window is visible or not.
|
||||
window.visible = function(self)
|
||||
return self.win and vim.api.nvim_win_is_valid(self.win)
|
||||
end
|
||||
|
||||
---Return win info.
|
||||
window.info = function(self)
|
||||
local border_info = self:get_border_info()
|
||||
local scrollbar = config.get().window.completion.scrollbar
|
||||
local info = {
|
||||
row = self.style.row,
|
||||
col = self.style.col,
|
||||
width = self.style.width + border_info.left + border_info.right,
|
||||
height = self.style.height + border_info.top + border_info.bottom,
|
||||
inner_width = self.style.width,
|
||||
inner_height = self.style.height,
|
||||
border_info = border_info,
|
||||
scrollable = false,
|
||||
scrollbar_offset = 0,
|
||||
}
|
||||
|
||||
if self:get_content_height() > info.inner_height and scrollbar then
|
||||
info.scrollable = true
|
||||
if not border_info.visible then
|
||||
info.scrollbar_offset = 1
|
||||
info.width = info.width + 1
|
||||
end
|
||||
end
|
||||
|
||||
return info
|
||||
end
|
||||
|
||||
---Return border information.
|
||||
---@return { top: integer, left: integer, right: integer, bottom: integer, vert: integer, horiz: integer, visible: boolean }
|
||||
window.get_border_info = function(self)
|
||||
local border = self.style.border
|
||||
if not border or border == 'none' then
|
||||
return {
|
||||
top = 0,
|
||||
left = 0,
|
||||
right = 0,
|
||||
bottom = 0,
|
||||
vert = 0,
|
||||
horiz = 0,
|
||||
visible = false,
|
||||
}
|
||||
end
|
||||
if type(border) == 'string' then
|
||||
if border == 'shadow' then
|
||||
return {
|
||||
top = 0,
|
||||
left = 0,
|
||||
right = 1,
|
||||
bottom = 1,
|
||||
vert = 1,
|
||||
horiz = 1,
|
||||
visible = false,
|
||||
}
|
||||
end
|
||||
return {
|
||||
top = 1,
|
||||
left = 1,
|
||||
right = 1,
|
||||
bottom = 1,
|
||||
vert = 2,
|
||||
horiz = 2,
|
||||
visible = true,
|
||||
}
|
||||
end
|
||||
|
||||
local new_border = {}
|
||||
while #new_border <= 8 do
|
||||
for _, b in ipairs(border) do
|
||||
table.insert(new_border, type(b) == 'string' and b or b[1])
|
||||
end
|
||||
end
|
||||
local info = {}
|
||||
info.top = new_border[2] == '' and 0 or 1
|
||||
info.right = new_border[4] == '' and 0 or 1
|
||||
info.bottom = new_border[6] == '' and 0 or 1
|
||||
info.left = new_border[8] == '' and 0 or 1
|
||||
info.vert = info.top + info.bottom
|
||||
info.horiz = info.left + info.right
|
||||
info.visible = not (vim.tbl_contains({ '', ' ' }, new_border[2]) and vim.tbl_contains({ '', ' ' }, new_border[4]) and vim.tbl_contains({ '', ' ' }, new_border[6]) and vim.tbl_contains({ '', ' ' }, new_border[8]))
|
||||
return info
|
||||
end
|
||||
|
||||
---Get scroll height.
|
||||
---NOTE: The result of vim.fn.strdisplaywidth depends on the buffer it was called in (see comment in cmp.Entry.get_view).
|
||||
---@return integer
|
||||
window.get_content_height = function(self)
|
||||
if not self:option('wrap') then
|
||||
return vim.api.nvim_buf_line_count(self:get_buffer())
|
||||
end
|
||||
local height = 0
|
||||
vim.api.nvim_buf_call(self:get_buffer(), function()
|
||||
for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do
|
||||
height = height + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / self.style.width))
|
||||
end
|
||||
end)
|
||||
return height
|
||||
end
|
||||
|
||||
return window
|
||||
307
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/view.lua
Normal file
307
config/neovim/store/lazy-plugins/nvim-cmp/lua/cmp/view.lua
Normal file
@ -0,0 +1,307 @@
|
||||
local config = require('cmp.config')
|
||||
local async = require('cmp.utils.async')
|
||||
local event = require('cmp.utils.event')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local docs_view = require('cmp.view.docs_view')
|
||||
local custom_entries_view = require('cmp.view.custom_entries_view')
|
||||
local wildmenu_entries_view = require('cmp.view.wildmenu_entries_view')
|
||||
local native_entries_view = require('cmp.view.native_entries_view')
|
||||
local ghost_text_view = require('cmp.view.ghost_text_view')
|
||||
|
||||
---@class cmp.View
|
||||
---@field public event cmp.Event
|
||||
---@field private is_docs_view_pinned boolean
|
||||
---@field private resolve_dedup cmp.AsyncDedup
|
||||
---@field private native_entries_view cmp.NativeEntriesView
|
||||
---@field private custom_entries_view cmp.CustomEntriesView
|
||||
---@field private wildmenu_entries_view cmp.CustomEntriesView
|
||||
---@field private change_dedup cmp.AsyncDedup
|
||||
---@field private docs_view cmp.DocsView
|
||||
---@field private ghost_text_view cmp.GhostTextView
|
||||
local view = {}
|
||||
|
||||
---Create menu
|
||||
view.new = function()
|
||||
local self = setmetatable({}, { __index = view })
|
||||
self.resolve_dedup = async.dedup()
|
||||
self.is_docs_view_pinned = false
|
||||
self.custom_entries_view = custom_entries_view.new()
|
||||
self.native_entries_view = native_entries_view.new()
|
||||
self.wildmenu_entries_view = wildmenu_entries_view.new()
|
||||
self.docs_view = docs_view.new()
|
||||
self.ghost_text_view = ghost_text_view.new()
|
||||
self.event = event.new()
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---Return the view components are available or not.
|
||||
---@return boolean
|
||||
view.ready = function(self)
|
||||
return self:_get_entries_view():ready()
|
||||
end
|
||||
|
||||
---OnChange handler.
|
||||
view.on_change = function(self)
|
||||
self:_get_entries_view():on_change()
|
||||
end
|
||||
|
||||
---Open menu
|
||||
---@param ctx cmp.Context
|
||||
---@param sources cmp.Source[]
|
||||
---@return boolean did_open
|
||||
view.open = function(self, ctx, sources)
|
||||
local source_group_map = {}
|
||||
for _, s in ipairs(sources) do
|
||||
local group_index = s:get_source_config().group_index or 0
|
||||
if not source_group_map[group_index] then
|
||||
source_group_map[group_index] = {}
|
||||
end
|
||||
table.insert(source_group_map[group_index], s)
|
||||
end
|
||||
|
||||
local group_indexes = vim.tbl_keys(source_group_map)
|
||||
table.sort(group_indexes, function(a, b)
|
||||
return a ~= b and (a < b) or nil
|
||||
end)
|
||||
|
||||
local entries = {}
|
||||
for _, group_index in ipairs(group_indexes) do
|
||||
local source_group = source_group_map[group_index] or {}
|
||||
|
||||
-- check the source triggered by character
|
||||
local has_triggered_by_symbol_source = false
|
||||
for _, s in ipairs(source_group) do
|
||||
if #s:get_entries(ctx) > 0 then
|
||||
if s.is_triggered_by_symbol then
|
||||
has_triggered_by_symbol_source = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- create filtered entries.
|
||||
local offset = ctx.cursor.col
|
||||
local group_entries = {}
|
||||
local max_item_counts = {}
|
||||
for i, s in ipairs(source_group) do
|
||||
if s.offset <= ctx.cursor.col then
|
||||
if not has_triggered_by_symbol_source or s.is_triggered_by_symbol then
|
||||
-- prepare max_item_counts map for filtering after sort.
|
||||
local max_item_count = s:get_source_config().max_item_count
|
||||
if max_item_count ~= nil then
|
||||
max_item_counts[s.name] = max_item_count
|
||||
end
|
||||
|
||||
-- source order priority bonus.
|
||||
local priority = s:get_source_config().priority or ((#source_group - (i - 1)) * config.get().sorting.priority_weight)
|
||||
|
||||
for _, e in ipairs(s:get_entries(ctx)) do
|
||||
e.score = e.score + priority
|
||||
table.insert(group_entries, e)
|
||||
offset = math.min(offset, e:get_offset())
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- sort.
|
||||
local comparetors = config.get().sorting.comparators
|
||||
table.sort(group_entries, function(e1, e2)
|
||||
for _, fn in ipairs(comparetors) do
|
||||
local diff = fn(e1, e2)
|
||||
if diff ~= nil then
|
||||
return diff
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- filter by max_item_count.
|
||||
for _, e in ipairs(group_entries) do
|
||||
if max_item_counts[e.source.name] ~= nil then
|
||||
if max_item_counts[e.source.name] > 0 then
|
||||
max_item_counts[e.source.name] = max_item_counts[e.source.name] - 1
|
||||
table.insert(entries, e)
|
||||
end
|
||||
else
|
||||
table.insert(entries, e)
|
||||
end
|
||||
end
|
||||
|
||||
local max_view_entries = config.get().performance.max_view_entries or 200
|
||||
entries = vim.list_slice(entries, 1, max_view_entries)
|
||||
|
||||
-- open
|
||||
if #entries > 0 then
|
||||
self:_get_entries_view():open(offset, entries)
|
||||
self.event:emit('menu_opened', {
|
||||
window = self:_get_entries_view(),
|
||||
})
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- complete_done.
|
||||
if #entries == 0 then
|
||||
self:close()
|
||||
end
|
||||
return #entries > 0
|
||||
end
|
||||
|
||||
---Close menu
|
||||
view.close = function(self)
|
||||
if self:visible() then
|
||||
self.is_docs_view_pinned = false
|
||||
self.event:emit('complete_done', {
|
||||
entry = self:_get_entries_view():get_selected_entry(),
|
||||
})
|
||||
end
|
||||
self:_get_entries_view():close()
|
||||
self.docs_view:close()
|
||||
self.ghost_text_view:hide()
|
||||
self.event:emit('menu_closed', {
|
||||
window = self:_get_entries_view(),
|
||||
})
|
||||
end
|
||||
|
||||
---Abort menu
|
||||
view.abort = function(self)
|
||||
if self:visible() then
|
||||
self.is_docs_view_pinned = false
|
||||
end
|
||||
self:_get_entries_view():abort()
|
||||
self.docs_view:close()
|
||||
self.ghost_text_view:hide()
|
||||
self.event:emit('menu_closed', {
|
||||
window = self:_get_entries_view(),
|
||||
})
|
||||
end
|
||||
|
||||
---Return the view is visible or not.
|
||||
---@return boolean
|
||||
view.visible = function(self)
|
||||
return self:_get_entries_view():visible()
|
||||
end
|
||||
|
||||
---Opens the documentation window.
|
||||
view.open_docs = function(self)
|
||||
self.is_docs_view_pinned = true
|
||||
local e = self:get_selected_entry()
|
||||
if e then
|
||||
e:resolve(vim.schedule_wrap(self.resolve_dedup(function()
|
||||
if not self:visible() then
|
||||
return
|
||||
end
|
||||
self.docs_view:open(e, self:_get_entries_view():info())
|
||||
end)))
|
||||
end
|
||||
end
|
||||
|
||||
---Closes the documentation window.
|
||||
view.close_docs = function(self)
|
||||
self.is_docs_view_pinned = false
|
||||
if self:get_selected_entry() then
|
||||
self.docs_view:close()
|
||||
end
|
||||
end
|
||||
|
||||
---Scroll documentation window if possible.
|
||||
---@param delta integer
|
||||
view.scroll_docs = function(self, delta)
|
||||
self.docs_view:scroll(delta)
|
||||
end
|
||||
|
||||
---Select prev menu item.
|
||||
---@param option cmp.SelectOption
|
||||
view.select_next_item = function(self, option)
|
||||
self:_get_entries_view():select_next_item(option)
|
||||
end
|
||||
|
||||
---Select prev menu item.
|
||||
---@param option cmp.SelectOption
|
||||
view.select_prev_item = function(self, option)
|
||||
self:_get_entries_view():select_prev_item(option)
|
||||
end
|
||||
|
||||
---Get offset.
|
||||
view.get_offset = function(self)
|
||||
return self:_get_entries_view():get_offset()
|
||||
end
|
||||
|
||||
---Get entries.
|
||||
---@return cmp.Entry[]
|
||||
view.get_entries = function(self)
|
||||
return self:_get_entries_view():get_entries()
|
||||
end
|
||||
|
||||
---Get first entry
|
||||
---@param self cmp.Entry|nil
|
||||
view.get_first_entry = function(self)
|
||||
return self:_get_entries_view():get_first_entry()
|
||||
end
|
||||
|
||||
---Get current selected entry
|
||||
---@return cmp.Entry|nil
|
||||
view.get_selected_entry = function(self)
|
||||
return self:_get_entries_view():get_selected_entry()
|
||||
end
|
||||
|
||||
---Get current active entry
|
||||
---@return cmp.Entry|nil
|
||||
view.get_active_entry = function(self)
|
||||
return self:_get_entries_view():get_active_entry()
|
||||
end
|
||||
|
||||
---Return current configured entries_view
|
||||
---@return cmp.CustomEntriesView|cmp.NativeEntriesView
|
||||
view._get_entries_view = function(self)
|
||||
self.native_entries_view.event:clear()
|
||||
self.custom_entries_view.event:clear()
|
||||
self.wildmenu_entries_view.event:clear()
|
||||
|
||||
local c = config.get()
|
||||
local v = self.custom_entries_view
|
||||
if (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'wildmenu' then
|
||||
v = self.wildmenu_entries_view
|
||||
elseif (c.view and c.view.entries and (c.view.entries.name or c.view.entries)) == 'native' then
|
||||
v = self.native_entries_view
|
||||
end
|
||||
v.event:on('change', function()
|
||||
self:on_entry_change()
|
||||
end)
|
||||
return v
|
||||
end
|
||||
|
||||
---On entry change
|
||||
view.on_entry_change = async.throttle(function(self)
|
||||
if not self:visible() then
|
||||
return
|
||||
end
|
||||
local e = self:get_selected_entry()
|
||||
if e then
|
||||
for _, c in ipairs(config.get().confirmation.get_commit_characters(e:get_commit_characters())) do
|
||||
keymap.listen('i', c, function(...)
|
||||
self.event:emit('keymap', ...)
|
||||
end)
|
||||
end
|
||||
e:resolve(vim.schedule_wrap(self.resolve_dedup(function()
|
||||
if not self:visible() then
|
||||
return
|
||||
end
|
||||
if self.is_docs_view_pinned or config.get().view.docs.auto_open then
|
||||
self.docs_view:open(e, self:_get_entries_view():info())
|
||||
end
|
||||
end)))
|
||||
else
|
||||
self.docs_view:close()
|
||||
end
|
||||
|
||||
e = e or self:get_first_entry()
|
||||
if e then
|
||||
self.ghost_text_view:show(e)
|
||||
else
|
||||
self.ghost_text_view:hide()
|
||||
end
|
||||
end, 20)
|
||||
|
||||
return view
|
||||
@ -0,0 +1,482 @@
|
||||
local event = require('cmp.utils.event')
|
||||
local autocmd = require('cmp.utils.autocmd')
|
||||
local feedkeys = require('cmp.utils.feedkeys')
|
||||
local window = require('cmp.utils.window')
|
||||
local config = require('cmp.config')
|
||||
local types = require('cmp.types')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local api = require('cmp.utils.api')
|
||||
|
||||
local DEFAULT_HEIGHT = 10 -- @see https://github.com/vim/vim/blob/master/src/popupmenu.c#L45
|
||||
|
||||
---@class cmp.CustomEntriesView
|
||||
---@field private entries_win cmp.Window
|
||||
---@field private offset integer
|
||||
---@field private active boolean
|
||||
---@field private entries cmp.Entry[]
|
||||
---@field private column_width any
|
||||
---@field public event cmp.Event
|
||||
local custom_entries_view = {}
|
||||
|
||||
custom_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.custom_entries_view')
|
||||
|
||||
custom_entries_view.new = function()
|
||||
local self = setmetatable({}, { __index = custom_entries_view })
|
||||
|
||||
self.entries_win = window.new()
|
||||
self.entries_win:option('conceallevel', 2)
|
||||
self.entries_win:option('concealcursor', 'n')
|
||||
self.entries_win:option('cursorlineopt', 'line')
|
||||
self.entries_win:option('foldenable', false)
|
||||
self.entries_win:option('wrap', false)
|
||||
-- This is done so that strdisplaywidth calculations for lines in the
|
||||
-- custom_entries_view window exactly match with what is really displayed,
|
||||
-- see comment in cmp.Entry.get_view. Setting tabstop to 1 makes all tabs be
|
||||
-- always rendered one column wide, which removes the unpredictability coming
|
||||
-- from variable width of the tab character.
|
||||
self.entries_win:buffer_option('tabstop', 1)
|
||||
self.entries_win:buffer_option('filetype', 'cmp_menu')
|
||||
self.entries_win:buffer_option('buftype', 'nofile')
|
||||
self.event = event.new()
|
||||
self.offset = -1
|
||||
self.active = false
|
||||
self.entries = {}
|
||||
self.bottom_up = false
|
||||
|
||||
autocmd.subscribe(
|
||||
'CompleteChanged',
|
||||
vim.schedule_wrap(function()
|
||||
if self:visible() and vim.fn.pumvisible() == 1 then
|
||||
self:close()
|
||||
end
|
||||
end)
|
||||
)
|
||||
|
||||
vim.api.nvim_set_decoration_provider(custom_entries_view.ns, {
|
||||
on_win = function(_, win, buf, top, bot)
|
||||
if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then
|
||||
return
|
||||
end
|
||||
|
||||
local fields = config.get().formatting.fields
|
||||
for i = top, bot do
|
||||
local e = self.entries[i + 1]
|
||||
if e then
|
||||
local v = e:get_view(self.offset, buf)
|
||||
local o = config.get().window.completion.side_padding
|
||||
local a = 0
|
||||
for _, field in ipairs(fields) do
|
||||
if field == types.cmp.ItemField.Abbr then
|
||||
a = o
|
||||
end
|
||||
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, o, {
|
||||
end_line = i,
|
||||
end_col = o + v[field].bytes,
|
||||
hl_group = v[field].hl_group,
|
||||
hl_mode = 'combine',
|
||||
ephemeral = true,
|
||||
})
|
||||
o = o + v[field].bytes + (self.column_width[field] - v[field].width) + 1
|
||||
end
|
||||
|
||||
for _, m in ipairs(e.matches or {}) do
|
||||
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, a + m.word_match_start - 1, {
|
||||
end_line = i,
|
||||
end_col = a + m.word_match_end,
|
||||
hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch',
|
||||
hl_mode = 'combine',
|
||||
ephemeral = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
custom_entries_view.ready = function()
|
||||
return vim.fn.pumvisible() == 0
|
||||
end
|
||||
|
||||
custom_entries_view.on_change = function(self)
|
||||
self.active = false
|
||||
end
|
||||
|
||||
custom_entries_view.is_direction_top_down = function(self)
|
||||
local c = config.get()
|
||||
if (c.view and c.view.entries and c.view.entries.selection_order) == 'top_down' then
|
||||
return true
|
||||
elseif c.view.entries == nil or c.view.entries.selection_order == nil then
|
||||
return true
|
||||
else
|
||||
return not self.bottom_up
|
||||
end
|
||||
end
|
||||
|
||||
custom_entries_view.open = function(self, offset, entries)
|
||||
local completion = config.get().window.completion
|
||||
assert(completion, 'config.get() must resolve window.completion with defaults')
|
||||
|
||||
self.offset = offset
|
||||
self.entries = {}
|
||||
self.column_width = { abbr = 0, kind = 0, menu = 0 }
|
||||
|
||||
local entries_buf = self.entries_win:get_buffer()
|
||||
local lines = {}
|
||||
local dedup = {}
|
||||
local preselect_index = 0
|
||||
for _, e in ipairs(entries) do
|
||||
local view = e:get_view(offset, entries_buf)
|
||||
if view.dup == 1 or not dedup[e.completion_item.label] then
|
||||
dedup[e.completion_item.label] = true
|
||||
self.column_width.abbr = math.max(self.column_width.abbr, view.abbr.width)
|
||||
self.column_width.kind = math.max(self.column_width.kind, view.kind.width)
|
||||
self.column_width.menu = math.max(self.column_width.menu, view.menu.width)
|
||||
table.insert(self.entries, e)
|
||||
table.insert(lines, ' ')
|
||||
if preselect_index == 0 and e.completion_item.preselect then
|
||||
preselect_index = #self.entries
|
||||
end
|
||||
end
|
||||
end
|
||||
if vim.bo[entries_buf].modifiable == false then
|
||||
vim.bo[entries_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines)
|
||||
vim.bo[entries_buf].modifiable = false
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines)
|
||||
end
|
||||
vim.api.nvim_buf_set_option(entries_buf, 'modified', false)
|
||||
|
||||
local width = 0
|
||||
width = width + 1
|
||||
width = width + self.column_width.abbr + (self.column_width.kind > 0 and 1 or 0)
|
||||
width = width + self.column_width.kind + (self.column_width.menu > 0 and 1 or 0)
|
||||
width = width + self.column_width.menu + 1
|
||||
|
||||
local height = vim.api.nvim_get_option_value('pumheight', {})
|
||||
height = height ~= 0 and height or #self.entries
|
||||
height = math.min(height, #self.entries)
|
||||
|
||||
local delta = 0
|
||||
if not config.get().view.entries.follow_cursor then
|
||||
local cursor_before_line = api.get_cursor_before_line()
|
||||
delta = vim.fn.strdisplaywidth(cursor_before_line:sub(self.offset))
|
||||
end
|
||||
local pos = api.get_screen_cursor()
|
||||
local row, col = pos[1], pos[2] - delta - 1
|
||||
|
||||
local border_info = window.get_border_info({ style = completion })
|
||||
local border_offset_row = border_info.top + border_info.bottom
|
||||
local border_offset_col = border_info.left + border_info.right
|
||||
if math.floor(vim.o.lines * 0.5) <= row + border_offset_row and vim.o.lines - row - border_offset_row <= math.min(DEFAULT_HEIGHT, height) then
|
||||
height = math.min(height, row - 1)
|
||||
row = row - height - border_offset_row - 1
|
||||
if row < 0 then
|
||||
height = height + row
|
||||
end
|
||||
end
|
||||
if math.floor(vim.o.columns * 0.5) <= col + border_offset_col and vim.o.columns - col - border_offset_col <= width then
|
||||
width = math.min(width, vim.o.columns - 1)
|
||||
col = vim.o.columns - width - border_offset_col - 1
|
||||
if col < 0 then
|
||||
width = width + col
|
||||
end
|
||||
end
|
||||
|
||||
if pos[1] > row then
|
||||
self.bottom_up = true
|
||||
else
|
||||
self.bottom_up = false
|
||||
end
|
||||
|
||||
if not self:is_direction_top_down() then
|
||||
local n = #self.entries
|
||||
for i = 1, math.floor(n / 2) do
|
||||
self.entries[i], self.entries[n - i + 1] = self.entries[n - i + 1], self.entries[i]
|
||||
end
|
||||
if preselect_index ~= 0 then
|
||||
preselect_index = #self.entries - preselect_index + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply window options (that might be changed) on the custom completion menu.
|
||||
self.entries_win:option('winblend', completion.winblend)
|
||||
self.entries_win:option('winhighlight', completion.winhighlight)
|
||||
self.entries_win:option('scrolloff', completion.scrolloff)
|
||||
self.entries_win:open({
|
||||
relative = 'editor',
|
||||
style = 'minimal',
|
||||
row = math.max(0, row),
|
||||
col = math.max(0, col + completion.col_offset),
|
||||
width = width,
|
||||
height = height,
|
||||
border = completion.border,
|
||||
zindex = completion.zindex or 1001,
|
||||
})
|
||||
|
||||
-- Don't set the cursor if the entries_win:open function fails
|
||||
-- due to the window's width or height being less than 1
|
||||
if self.entries_win.win == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Always set cursor when starting. It will be adjusted on the call to _select
|
||||
vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 0 })
|
||||
if preselect_index > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
|
||||
self:_select(preselect_index, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
||||
elseif not string.match(config.get().completion.completeopt, 'noselect') then
|
||||
if self:is_direction_top_down() then
|
||||
self:_select(1, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
||||
else
|
||||
self:_select(#self.entries, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
||||
end
|
||||
else
|
||||
if self:is_direction_top_down() then
|
||||
self:_select(0, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
||||
else
|
||||
self:_select(#self.entries + 1, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
custom_entries_view.close = function(self)
|
||||
self.prefix = nil
|
||||
self.offset = -1
|
||||
self.active = false
|
||||
self.entries = {}
|
||||
self.entries_win:close()
|
||||
self.bottom_up = false
|
||||
end
|
||||
|
||||
custom_entries_view.abort = function(self)
|
||||
if self.prefix then
|
||||
self:_insert(self.prefix)
|
||||
end
|
||||
feedkeys.call('', 'n', function()
|
||||
self:close()
|
||||
end)
|
||||
end
|
||||
|
||||
custom_entries_view.draw = function(self)
|
||||
local info = vim.fn.getwininfo(self.entries_win.win)[1]
|
||||
local topline = info.topline - 1
|
||||
local botline = info.topline + info.height - 1
|
||||
local texts = {}
|
||||
local fields = config.get().formatting.fields
|
||||
local entries_buf = self.entries_win:get_buffer()
|
||||
for i = topline, botline - 1 do
|
||||
local e = self.entries[i + 1]
|
||||
if e then
|
||||
local view = e:get_view(self.offset, entries_buf)
|
||||
local text = {}
|
||||
table.insert(text, string.rep(' ', config.get().window.completion.side_padding))
|
||||
for _, field in ipairs(fields) do
|
||||
table.insert(text, view[field].text)
|
||||
table.insert(text, string.rep(' ', 1 + self.column_width[field] - view[field].width))
|
||||
end
|
||||
table.insert(text, string.rep(' ', config.get().window.completion.side_padding))
|
||||
table.insert(texts, table.concat(text, ''))
|
||||
end
|
||||
end
|
||||
if vim.bo[entries_buf].modifiable == false then
|
||||
vim.bo[entries_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts)
|
||||
vim.bo[entries_buf].modifiable = false
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts)
|
||||
end
|
||||
vim.api.nvim_buf_set_option(entries_buf, 'modified', false)
|
||||
|
||||
if api.is_cmdline_mode() then
|
||||
vim.api.nvim_win_call(self.entries_win.win, function()
|
||||
misc.redraw()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
custom_entries_view.visible = function(self)
|
||||
return self.entries_win:visible()
|
||||
end
|
||||
|
||||
custom_entries_view.info = function(self)
|
||||
return self.entries_win:info()
|
||||
end
|
||||
|
||||
custom_entries_view.select_next_item = function(self, option)
|
||||
if self:visible() then
|
||||
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1]
|
||||
local is_top_down = self:is_direction_top_down()
|
||||
local last = #self.entries
|
||||
|
||||
if not self.entries_win:option('cursorline') then
|
||||
cursor = (is_top_down and 1) or last
|
||||
else
|
||||
if is_top_down then
|
||||
if cursor == last then
|
||||
cursor = 0
|
||||
else
|
||||
cursor = cursor + option.count
|
||||
if last < cursor then
|
||||
cursor = last
|
||||
end
|
||||
end
|
||||
else
|
||||
if cursor == 0 then
|
||||
cursor = last
|
||||
else
|
||||
cursor = cursor - option.count
|
||||
if cursor < 0 then
|
||||
cursor = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self:_select(cursor, {
|
||||
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
|
||||
active = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
custom_entries_view.select_prev_item = function(self, option)
|
||||
if self:visible() then
|
||||
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1]
|
||||
local is_top_down = self:is_direction_top_down()
|
||||
local last = #self.entries
|
||||
|
||||
if not self.entries_win:option('cursorline') then
|
||||
cursor = (is_top_down and last) or 1
|
||||
else
|
||||
if is_top_down then
|
||||
if cursor == 1 then
|
||||
cursor = 0
|
||||
else
|
||||
cursor = cursor - option.count
|
||||
if cursor < 0 then
|
||||
cursor = 1
|
||||
end
|
||||
end
|
||||
else
|
||||
if cursor == last then
|
||||
cursor = 0
|
||||
else
|
||||
cursor = cursor + option.count
|
||||
if last < cursor then
|
||||
cursor = last
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self:_select(cursor, {
|
||||
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
|
||||
active = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
custom_entries_view.get_offset = function(self)
|
||||
if self:visible() then
|
||||
return self.offset
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
custom_entries_view.get_entries = function(self)
|
||||
if self:visible() then
|
||||
return self.entries
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
custom_entries_view.get_first_entry = function(self)
|
||||
if self:visible() then
|
||||
return (self:is_direction_top_down() and self.entries[1]) or self.entries[#self.entries]
|
||||
end
|
||||
end
|
||||
|
||||
custom_entries_view.get_selected_entry = function(self)
|
||||
if self:visible() and self.entries_win:option('cursorline') then
|
||||
return self.entries[vim.api.nvim_win_get_cursor(self.entries_win.win)[1]]
|
||||
end
|
||||
end
|
||||
|
||||
custom_entries_view.get_active_entry = function(self)
|
||||
if self:visible() and self.active then
|
||||
return self:get_selected_entry()
|
||||
end
|
||||
end
|
||||
|
||||
custom_entries_view._select = function(self, cursor, option)
|
||||
local is_insert = (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert
|
||||
if is_insert and not self.active then
|
||||
self.prefix = string.sub(api.get_current_line(), self.offset, api.get_cursor()[2]) or ''
|
||||
end
|
||||
self.active = (0 < cursor and cursor <= #self.entries and option.active == true)
|
||||
|
||||
self.entries_win:option('cursorline', cursor > 0 and cursor <= #self.entries)
|
||||
vim.api.nvim_win_set_cursor(self.entries_win.win, {
|
||||
math.max(math.min(cursor, #self.entries), 1),
|
||||
0,
|
||||
})
|
||||
|
||||
if is_insert then
|
||||
self:_insert(self.entries[cursor] and self.entries[cursor]:get_vim_item(self.offset).word or self.prefix)
|
||||
end
|
||||
|
||||
self.entries_win:update()
|
||||
self:draw()
|
||||
self.event:emit('change')
|
||||
end
|
||||
|
||||
custom_entries_view._insert = setmetatable({
|
||||
pending = false,
|
||||
}, {
|
||||
__call = function(this, self, word)
|
||||
word = word or ''
|
||||
if api.is_cmdline_mode() then
|
||||
local cursor = api.get_cursor()
|
||||
-- setcmdline() added in v0.8.0
|
||||
if vim.fn.has('nvim-0.8') == 1 then
|
||||
local current_line = api.get_current_line()
|
||||
local before_line = current_line:sub(1, self.offset - 1)
|
||||
local after_line = current_line:sub(cursor[2] + 1)
|
||||
local pos = #before_line + #word + 1
|
||||
vim.fn.setcmdline(before_line .. word .. after_line, pos)
|
||||
vim.api.nvim_feedkeys(keymap.t('<Cmd>redraw<CR>'), 'ni', false)
|
||||
else
|
||||
vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true)
|
||||
end
|
||||
else
|
||||
if this.pending then
|
||||
return
|
||||
end
|
||||
this.pending = true
|
||||
|
||||
local release = require('cmp').suspend()
|
||||
feedkeys.call('', '', function()
|
||||
local cursor = api.get_cursor()
|
||||
local keys = {}
|
||||
table.insert(keys, keymap.indentkeys())
|
||||
table.insert(keys, keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])))
|
||||
table.insert(keys, word)
|
||||
table.insert(keys, keymap.indentkeys(vim.bo.indentkeys))
|
||||
feedkeys.call(
|
||||
table.concat(keys, ''),
|
||||
'int',
|
||||
vim.schedule_wrap(function()
|
||||
this.pending = false
|
||||
release()
|
||||
end)
|
||||
)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
return custom_entries_view
|
||||
@ -0,0 +1,151 @@
|
||||
local window = require('cmp.utils.window')
|
||||
local config = require('cmp.config')
|
||||
|
||||
---@class cmp.DocsView
|
||||
---@field public window cmp.Window
|
||||
local docs_view = {}
|
||||
|
||||
---Create new floating window module
|
||||
docs_view.new = function()
|
||||
local self = setmetatable({}, { __index = docs_view })
|
||||
self.entry = nil
|
||||
self.window = window.new()
|
||||
self.window:option('conceallevel', 2)
|
||||
self.window:option('concealcursor', 'n')
|
||||
self.window:option('foldenable', false)
|
||||
self.window:option('linebreak', true)
|
||||
self.window:option('scrolloff', 0)
|
||||
self.window:option('showbreak', 'NONE')
|
||||
self.window:option('wrap', true)
|
||||
self.window:buffer_option('filetype', 'cmp_docs')
|
||||
self.window:buffer_option('buftype', 'nofile')
|
||||
return self
|
||||
end
|
||||
|
||||
---Open documentation window
|
||||
---@param e cmp.Entry
|
||||
---@param view cmp.WindowStyle
|
||||
docs_view.open = function(self, e, view)
|
||||
local documentation = config.get().window.documentation
|
||||
if not documentation then
|
||||
return
|
||||
end
|
||||
|
||||
if not e or not view then
|
||||
return self:close()
|
||||
end
|
||||
|
||||
local border_info = window.get_border_info({ style = documentation })
|
||||
local right_space = vim.o.columns - (view.col + view.width) - 1
|
||||
local left_space = view.col - 1
|
||||
local max_width = math.max(left_space, right_space)
|
||||
if documentation.max_width > 0 then
|
||||
max_width = math.min(documentation.max_width, max_width)
|
||||
end
|
||||
|
||||
-- Update buffer content if needed.
|
||||
if not self.entry or e.id ~= self.entry.id then
|
||||
local documents = e:get_documentation()
|
||||
if #documents == 0 then
|
||||
return self:close()
|
||||
end
|
||||
|
||||
self.entry = e
|
||||
vim.api.nvim_buf_call(self.window:get_buffer(), function()
|
||||
vim.cmd([[syntax clear]])
|
||||
vim.api.nvim_buf_set_lines(self.window:get_buffer(), 0, -1, false, {})
|
||||
end)
|
||||
local opts = {
|
||||
max_width = max_width - border_info.horiz,
|
||||
}
|
||||
if documentation.max_height > 0 then
|
||||
opts.max_height = documentation.max_height
|
||||
end
|
||||
vim.lsp.util.stylize_markdown(self.window:get_buffer(), documents, opts)
|
||||
end
|
||||
|
||||
-- Set buffer as not modified, so it can be removed without errors
|
||||
vim.api.nvim_buf_set_option(self.window:get_buffer(), 'modified', false)
|
||||
|
||||
-- Calculate window size.
|
||||
local opts = {
|
||||
max_width = max_width - border_info.horiz,
|
||||
}
|
||||
if documentation.max_height > 0 then
|
||||
opts.max_height = documentation.max_height - border_info.vert
|
||||
end
|
||||
local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window:get_buffer(), 0, -1, false), opts)
|
||||
if width <= 0 or height <= 0 then
|
||||
return self:close()
|
||||
end
|
||||
|
||||
-- Calculate window position.
|
||||
local right_col = view.col + view.width
|
||||
local left_col = view.col - width - border_info.horiz
|
||||
local col, left
|
||||
if right_space >= width and left_space >= width then
|
||||
if right_space < left_space then
|
||||
col = left_col
|
||||
left = true
|
||||
else
|
||||
col = right_col
|
||||
end
|
||||
elseif right_space >= width then
|
||||
col = right_col
|
||||
elseif left_space >= width then
|
||||
col = left_col
|
||||
left = true
|
||||
else
|
||||
return self:close()
|
||||
end
|
||||
|
||||
-- Render window.
|
||||
self.window:option('winblend', documentation.winblend)
|
||||
self.window:option('winhighlight', documentation.winhighlight)
|
||||
local style = {
|
||||
relative = 'editor',
|
||||
style = 'minimal',
|
||||
width = width,
|
||||
height = height,
|
||||
row = view.row,
|
||||
col = col,
|
||||
border = documentation.border,
|
||||
zindex = documentation.zindex or 50,
|
||||
}
|
||||
self.window:open(style)
|
||||
|
||||
-- Correct left-col for scrollbar existence.
|
||||
if left then
|
||||
style.col = style.col - self.window:info().scrollbar_offset
|
||||
self.window:open(style)
|
||||
end
|
||||
end
|
||||
|
||||
---Close floating window
|
||||
docs_view.close = function(self)
|
||||
self.window:close()
|
||||
self.entry = nil
|
||||
end
|
||||
|
||||
docs_view.scroll = function(self, delta)
|
||||
if self:visible() then
|
||||
local info = vim.fn.getwininfo(self.window.win)[1] or {}
|
||||
local top = info.topline or 1
|
||||
top = top + delta
|
||||
top = math.max(top, 1)
|
||||
top = math.min(top, self.window:get_content_height() - info.height + 1)
|
||||
|
||||
vim.defer_fn(function()
|
||||
vim.api.nvim_buf_call(self.window:get_buffer(), function()
|
||||
vim.api.nvim_command('normal! ' .. top .. 'zt')
|
||||
self.window:update()
|
||||
end)
|
||||
end, 0)
|
||||
end
|
||||
end
|
||||
|
||||
docs_view.visible = function(self)
|
||||
return self.window:visible()
|
||||
end
|
||||
|
||||
return docs_view
|
||||
@ -0,0 +1,127 @@
|
||||
local config = require('cmp.config')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local snippet = require('cmp.utils.snippet')
|
||||
local str = require('cmp.utils.str')
|
||||
local api = require('cmp.utils.api')
|
||||
local types = require('cmp.types')
|
||||
|
||||
---@class cmp.GhostTextView
|
||||
---@field win number|nil
|
||||
---@field entry cmp.Entry|nil
|
||||
local ghost_text_view = {}
|
||||
|
||||
ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT')
|
||||
|
||||
local has_inline = (function()
|
||||
return (pcall(function()
|
||||
local id = vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, 0, 0, {
|
||||
virt_text = { { ' ', 'Comment' } },
|
||||
virt_text_pos = 'inline',
|
||||
hl_mode = 'combine',
|
||||
ephemeral = false,
|
||||
})
|
||||
vim.api.nvim_buf_del_extmark(0, ghost_text_view.ns, id)
|
||||
end))
|
||||
end)()
|
||||
|
||||
ghost_text_view.new = function()
|
||||
local self = setmetatable({}, { __index = ghost_text_view })
|
||||
self.win = nil
|
||||
self.entry = nil
|
||||
self.extmark_id = nil
|
||||
vim.api.nvim_set_decoration_provider(ghost_text_view.ns, {
|
||||
on_win = function(_, win)
|
||||
if self.extmark_id then
|
||||
vim.api.nvim_buf_del_extmark(self.extmark_buf, ghost_text_view.ns, self.extmark_id)
|
||||
self.extmark_id = nil
|
||||
end
|
||||
|
||||
if win ~= self.win then
|
||||
return false
|
||||
end
|
||||
|
||||
local c = config.get().experimental.ghost_text
|
||||
if not c then
|
||||
return
|
||||
end
|
||||
|
||||
if not self.entry then
|
||||
return
|
||||
end
|
||||
|
||||
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
if not has_inline then
|
||||
if string.sub(line, col + 1) ~= '' then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local text = self.text_gen(self, line, col)
|
||||
if #text > 0 then
|
||||
self.extmark_buf = vim.api.nvim_get_current_buf()
|
||||
self.extmark_id = vim.api.nvim_buf_set_extmark(self.extmark_buf, ghost_text_view.ns, row - 1, col, {
|
||||
right_gravity = true,
|
||||
virt_text = { { text, type(c) == 'table' and c.hl_group or 'Comment' } },
|
||||
virt_text_pos = has_inline and 'inline' or 'overlay',
|
||||
hl_mode = 'combine',
|
||||
ephemeral = false,
|
||||
})
|
||||
end
|
||||
end,
|
||||
})
|
||||
return self
|
||||
end
|
||||
|
||||
---Generate the ghost text
|
||||
--- This function calculates the bytes of the entry to display calculating the number
|
||||
--- of character differences instead of just byte difference.
|
||||
ghost_text_view.text_gen = function(self, line, cursor_col)
|
||||
local word = self.entry:get_insert_text()
|
||||
if self.entry:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
|
||||
word = tostring(snippet.parse(word))
|
||||
end
|
||||
word = str.oneline(word)
|
||||
local word_clen = vim.str_utfindex(word)
|
||||
local cword = string.sub(line, self.entry:get_offset(), cursor_col)
|
||||
local cword_clen = vim.str_utfindex(cword)
|
||||
-- Number of characters from entry text (word) to be displayed as ghost thext
|
||||
local nchars = word_clen - cword_clen
|
||||
-- Missing characters to complete the entry text
|
||||
local text
|
||||
if nchars > 0 then
|
||||
text = string.sub(word, vim.str_byteindex(word, word_clen - nchars) + 1)
|
||||
else
|
||||
text = ''
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
---Show ghost text
|
||||
---@param e cmp.Entry
|
||||
ghost_text_view.show = function(self, e)
|
||||
if not api.is_insert_mode() then
|
||||
return
|
||||
end
|
||||
local c = config.get().experimental.ghost_text
|
||||
if not c then
|
||||
return
|
||||
end
|
||||
local changed = e ~= self.entry
|
||||
self.win = vim.api.nvim_get_current_win()
|
||||
self.entry = e
|
||||
if changed then
|
||||
misc.redraw(true) -- force invoke decoration provider.
|
||||
end
|
||||
end
|
||||
|
||||
ghost_text_view.hide = function(self)
|
||||
if self.win and self.entry then
|
||||
self.win = nil
|
||||
self.entry = nil
|
||||
misc.redraw(true) -- force invoke decoration provider.
|
||||
end
|
||||
end
|
||||
|
||||
return ghost_text_view
|
||||
@ -0,0 +1,180 @@
|
||||
local event = require('cmp.utils.event')
|
||||
local autocmd = require('cmp.utils.autocmd')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local feedkeys = require('cmp.utils.feedkeys')
|
||||
local types = require('cmp.types')
|
||||
local config = require('cmp.config')
|
||||
local api = require('cmp.utils.api')
|
||||
|
||||
---@class cmp.NativeEntriesView
|
||||
---@field private offset integer
|
||||
---@field private items vim.CompletedItem
|
||||
---@field private entries cmp.Entry[]
|
||||
---@field private preselect_index integer
|
||||
---@field public event cmp.Event
|
||||
local native_entries_view = {}
|
||||
|
||||
native_entries_view.new = function()
|
||||
local self = setmetatable({}, { __index = native_entries_view })
|
||||
self.event = event.new()
|
||||
self.offset = -1
|
||||
self.items = {}
|
||||
self.entries = {}
|
||||
self.preselect_index = 0
|
||||
autocmd.subscribe('CompleteChanged', function()
|
||||
self.event:emit('change')
|
||||
end)
|
||||
return self
|
||||
end
|
||||
|
||||
native_entries_view.ready = function(_)
|
||||
if vim.fn.pumvisible() == 0 then
|
||||
return true
|
||||
end
|
||||
return vim.fn.complete_info({ 'mode' }).mode == 'eval'
|
||||
end
|
||||
|
||||
native_entries_view.on_change = function(self)
|
||||
if #self.entries > 0 and self.offset <= vim.api.nvim_win_get_cursor(0)[2] + 1 then
|
||||
local preselect_enabled = config.get().preselect == types.cmp.PreselectMode.Item
|
||||
|
||||
local completeopt = vim.o.completeopt
|
||||
if self.preselect_index == 1 and preselect_enabled then
|
||||
vim.o.completeopt = 'menu,menuone,noinsert'
|
||||
else
|
||||
vim.o.completeopt = config.get().completion.completeopt
|
||||
end
|
||||
vim.fn.complete(self.offset, self.items)
|
||||
vim.o.completeopt = completeopt
|
||||
|
||||
if self.preselect_index > 1 and preselect_enabled then
|
||||
self:preselect(self.preselect_index)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
native_entries_view.open = function(self, offset, entries)
|
||||
local dedup = {}
|
||||
local items = {}
|
||||
local dedup_entries = {}
|
||||
local preselect_index = 0
|
||||
for _, e in ipairs(entries) do
|
||||
local item = e:get_vim_item(offset)
|
||||
if item.dup == 1 or not dedup[item.abbr] then
|
||||
dedup[item.abbr] = true
|
||||
table.insert(items, item)
|
||||
table.insert(dedup_entries, e)
|
||||
if preselect_index == 0 and e.completion_item.preselect then
|
||||
preselect_index = #dedup_entries
|
||||
end
|
||||
end
|
||||
end
|
||||
self.offset = offset
|
||||
self.items = items
|
||||
self.entries = dedup_entries
|
||||
self.preselect_index = preselect_index
|
||||
self:on_change()
|
||||
end
|
||||
|
||||
native_entries_view.close = function(self)
|
||||
if api.is_insert_mode() and self:visible() then
|
||||
vim.api.nvim_select_popupmenu_item(-1, false, true, {})
|
||||
end
|
||||
self.offset = -1
|
||||
self.entries = {}
|
||||
self.items = {}
|
||||
self.preselect_index = 0
|
||||
end
|
||||
|
||||
native_entries_view.abort = function(_)
|
||||
if api.is_suitable_mode() then
|
||||
vim.api.nvim_select_popupmenu_item(-1, true, true, {})
|
||||
end
|
||||
end
|
||||
|
||||
native_entries_view.visible = function(_)
|
||||
return vim.fn.pumvisible() == 1
|
||||
end
|
||||
|
||||
native_entries_view.info = function(self)
|
||||
if self:visible() then
|
||||
local info = vim.fn.pum_getpos()
|
||||
return {
|
||||
width = info.width + (info.scrollbar and 1 or 0) + (info.col == 0 and 0 or 1),
|
||||
height = info.height,
|
||||
row = info.row,
|
||||
col = info.col == 0 and 0 or info.col - 1,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
native_entries_view.preselect = function(self, index)
|
||||
if self:visible() then
|
||||
if index <= #self.entries then
|
||||
vim.api.nvim_select_popupmenu_item(index - 1, false, false, {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
native_entries_view.select_next_item = function(self, option)
|
||||
local callback = function()
|
||||
self.event:emit('change')
|
||||
end
|
||||
if self:visible() then
|
||||
if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then
|
||||
feedkeys.call(keymap.t(string.rep('<C-n>', option.count)), 'n', callback)
|
||||
else
|
||||
feedkeys.call(keymap.t(string.rep('<Down>', option.count)), 'n', callback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
native_entries_view.select_prev_item = function(self, option)
|
||||
local callback = function()
|
||||
self.event:emit('change')
|
||||
end
|
||||
if self:visible() then
|
||||
if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then
|
||||
feedkeys.call(keymap.t(string.rep('<C-p>', option.count)), 'n', callback)
|
||||
else
|
||||
feedkeys.call(keymap.t(string.rep('<Up>', option.count)), 'n', callback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
native_entries_view.get_offset = function(self)
|
||||
if self:visible() then
|
||||
return self.offset
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
native_entries_view.get_entries = function(self)
|
||||
if self:visible() then
|
||||
return self.entries
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
native_entries_view.get_first_entry = function(self)
|
||||
if self:visible() then
|
||||
return self.entries[1]
|
||||
end
|
||||
end
|
||||
|
||||
native_entries_view.get_selected_entry = function(self)
|
||||
if self:visible() then
|
||||
local idx = vim.fn.complete_info({ 'selected' }).selected
|
||||
if idx > -1 then
|
||||
return self.entries[math.max(0, idx) + 1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
native_entries_view.get_active_entry = function(self)
|
||||
if self:visible() and (vim.v.completed_item or {}).word then
|
||||
return self:get_selected_entry()
|
||||
end
|
||||
end
|
||||
|
||||
return native_entries_view
|
||||
@ -0,0 +1,264 @@
|
||||
local event = require('cmp.utils.event')
|
||||
local autocmd = require('cmp.utils.autocmd')
|
||||
local feedkeys = require('cmp.utils.feedkeys')
|
||||
local config = require('cmp.config')
|
||||
local window = require('cmp.utils.window')
|
||||
local types = require('cmp.types')
|
||||
local keymap = require('cmp.utils.keymap')
|
||||
local misc = require('cmp.utils.misc')
|
||||
local api = require('cmp.utils.api')
|
||||
|
||||
---@class cmp.CustomEntriesView
|
||||
---@field private offset integer
|
||||
---@field private entries_win cmp.Window
|
||||
---@field private active boolean
|
||||
---@field private entries cmp.Entry[]
|
||||
---@field public event cmp.Event
|
||||
local wildmenu_entries_view = {}
|
||||
|
||||
wildmenu_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.statusline_entries_view')
|
||||
|
||||
wildmenu_entries_view.new = function()
|
||||
local self = setmetatable({}, { __index = wildmenu_entries_view })
|
||||
self.event = event.new()
|
||||
self.offset = -1
|
||||
self.active = false
|
||||
self.entries = {}
|
||||
self.offsets = {}
|
||||
self.selected_index = 0
|
||||
self.entries_win = window.new()
|
||||
|
||||
self.entries_win:option('conceallevel', 2)
|
||||
self.entries_win:option('concealcursor', 'n')
|
||||
self.entries_win:option('cursorlineopt', 'line')
|
||||
self.entries_win:option('foldenable', false)
|
||||
self.entries_win:option('wrap', false)
|
||||
self.entries_win:option('scrolloff', 0)
|
||||
self.entries_win:option('sidescrolloff', 0)
|
||||
self.entries_win:option('winhighlight', 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None')
|
||||
self.entries_win:buffer_option('tabstop', 1)
|
||||
|
||||
autocmd.subscribe(
|
||||
'CompleteChanged',
|
||||
vim.schedule_wrap(function()
|
||||
if self:visible() and vim.fn.pumvisible() == 1 then
|
||||
self:close()
|
||||
end
|
||||
end)
|
||||
)
|
||||
|
||||
vim.api.nvim_set_decoration_provider(wildmenu_entries_view.ns, {
|
||||
on_win = function(_, win, buf, _, _)
|
||||
if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then
|
||||
return
|
||||
end
|
||||
|
||||
for i, e in ipairs(self.entries) do
|
||||
if e then
|
||||
local view = e:get_view(self.offset, buf)
|
||||
vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i], {
|
||||
end_line = 0,
|
||||
end_col = self.offsets[i] + view.abbr.bytes,
|
||||
hl_group = view.abbr.hl_group,
|
||||
hl_mode = 'combine',
|
||||
ephemeral = true,
|
||||
})
|
||||
|
||||
if i == self.selected_index then
|
||||
vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i], {
|
||||
end_line = 0,
|
||||
end_col = self.offsets[i] + view.abbr.bytes,
|
||||
hl_group = 'PmenuSel',
|
||||
hl_mode = 'combine',
|
||||
ephemeral = true,
|
||||
})
|
||||
end
|
||||
|
||||
for _, m in ipairs(e.matches or {}) do
|
||||
vim.api.nvim_buf_set_extmark(buf, wildmenu_entries_view.ns, 0, self.offsets[i] + m.word_match_start - 1, {
|
||||
end_line = 0,
|
||||
end_col = self.offsets[i] + m.word_match_end,
|
||||
hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch',
|
||||
hl_mode = 'combine',
|
||||
ephemeral = true,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
return self
|
||||
end
|
||||
|
||||
wildmenu_entries_view.close = function(self)
|
||||
self.entries_win:close()
|
||||
end
|
||||
|
||||
wildmenu_entries_view.ready = function()
|
||||
return vim.fn.pumvisible() == 0
|
||||
end
|
||||
|
||||
wildmenu_entries_view.on_change = function(self)
|
||||
self.active = false
|
||||
end
|
||||
|
||||
wildmenu_entries_view.open = function(self, offset, entries)
|
||||
self.offset = offset
|
||||
self.entries = {}
|
||||
|
||||
-- Apply window options (that might be changed) on the custom completion menu.
|
||||
self.entries_win:option('winblend', vim.o.pumblend)
|
||||
|
||||
local dedup = {}
|
||||
local preselect = 0
|
||||
local i = 1
|
||||
for _, e in ipairs(entries) do
|
||||
local view = e:get_view(offset, 0)
|
||||
if view.dup == 1 or not dedup[e.completion_item.label] then
|
||||
dedup[e.completion_item.label] = true
|
||||
table.insert(self.entries, e)
|
||||
if preselect == 0 and e.completion_item.preselect then
|
||||
preselect = i
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
self.entries_win:open({
|
||||
relative = 'editor',
|
||||
style = 'minimal',
|
||||
row = vim.o.lines - 2,
|
||||
col = 0,
|
||||
width = vim.o.columns,
|
||||
height = 1,
|
||||
zindex = 1001,
|
||||
})
|
||||
self:draw()
|
||||
|
||||
if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
|
||||
self:_select(preselect, { behavior = types.cmp.SelectBehavior.Select })
|
||||
elseif not string.match(config.get().completion.completeopt, 'noselect') then
|
||||
self:_select(1, { behavior = types.cmp.SelectBehavior.Select })
|
||||
else
|
||||
self:_select(0, { behavior = types.cmp.SelectBehavior.Select })
|
||||
end
|
||||
end
|
||||
|
||||
wildmenu_entries_view.abort = function(self)
|
||||
feedkeys.call('', 'n', function()
|
||||
self:close()
|
||||
end)
|
||||
end
|
||||
|
||||
wildmenu_entries_view.draw = function(self)
|
||||
self.offsets = {}
|
||||
|
||||
local entries_buf = self.entries_win:get_buffer()
|
||||
local texts = {}
|
||||
local offset = 0
|
||||
for _, e in ipairs(self.entries) do
|
||||
local view = e:get_view(self.offset, entries_buf)
|
||||
table.insert(self.offsets, offset)
|
||||
table.insert(texts, view.abbr.text)
|
||||
offset = offset + view.abbr.bytes + #self:_get_separator()
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(entries_buf, 0, 1, false, { table.concat(texts, self:_get_separator()) })
|
||||
vim.api.nvim_buf_set_option(entries_buf, 'modified', false)
|
||||
|
||||
vim.api.nvim_win_call(0, function()
|
||||
misc.redraw()
|
||||
end)
|
||||
end
|
||||
|
||||
wildmenu_entries_view.visible = function(self)
|
||||
return self.entries_win:visible()
|
||||
end
|
||||
|
||||
wildmenu_entries_view.info = function(self)
|
||||
return self.entries_win:info()
|
||||
end
|
||||
|
||||
wildmenu_entries_view.select_next_item = function(self, option)
|
||||
if self:visible() then
|
||||
local cursor
|
||||
if self.selected_index == 0 or self.selected_index == #self.entries then
|
||||
cursor = option.count
|
||||
else
|
||||
cursor = self.selected_index + option.count
|
||||
end
|
||||
cursor = math.max(math.min(cursor, #self.entries), 0)
|
||||
self:_select(cursor, option)
|
||||
end
|
||||
end
|
||||
|
||||
wildmenu_entries_view.select_prev_item = function(self, option)
|
||||
if self:visible() then
|
||||
if self.selected_index == 0 or self.selected_index <= 1 then
|
||||
self:_select(#self.entries, option)
|
||||
else
|
||||
self:_select(math.max(self.selected_index - option.count, 0), option)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
wildmenu_entries_view.get_offset = function(self)
|
||||
if self:visible() then
|
||||
return self.offset
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
wildmenu_entries_view.get_entries = function(self)
|
||||
if self:visible() then
|
||||
return self.entries
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
wildmenu_entries_view.get_first_entry = function(self)
|
||||
if self:visible() then
|
||||
return self.entries[1]
|
||||
end
|
||||
end
|
||||
|
||||
wildmenu_entries_view.get_selected_entry = function(self)
|
||||
if self:visible() and self.active then
|
||||
return self.entries[self.selected_index]
|
||||
end
|
||||
end
|
||||
|
||||
wildmenu_entries_view.get_active_entry = function(self)
|
||||
if self:visible() and self.active then
|
||||
return self:get_selected_entry()
|
||||
end
|
||||
end
|
||||
|
||||
wildmenu_entries_view._select = function(self, selected_index, option)
|
||||
local is_next = self.selected_index < selected_index
|
||||
self.selected_index = selected_index
|
||||
self.active = (selected_index ~= 0)
|
||||
|
||||
if self.active then
|
||||
local e = self:get_active_entry()
|
||||
if option.behavior == types.cmp.SelectBehavior.Insert then
|
||||
local cursor = api.get_cursor()
|
||||
local word = e:get_vim_item(self.offset).word
|
||||
vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true)
|
||||
end
|
||||
vim.api.nvim_win_call(self.entries_win.win, function()
|
||||
local view = e:get_view(self.offset, self.entries_win:get_buffer())
|
||||
vim.api.nvim_win_set_cursor(0, { 1, self.offsets[selected_index] + (is_next and view.abbr.bytes or 0) })
|
||||
vim.cmd([[redraw!]]) -- Force refresh for vim.api.nvim_set_decoration_provider
|
||||
end)
|
||||
end
|
||||
|
||||
self.event:emit('change')
|
||||
end
|
||||
|
||||
wildmenu_entries_view._get_separator = function()
|
||||
local c = config.get()
|
||||
return (c and c.view and c.view.entries and c.view.entries.separator) or ' '
|
||||
end
|
||||
|
||||
return wildmenu_entries_view
|
||||
@ -0,0 +1,53 @@
|
||||
local misc = require('cmp.utils.misc')
|
||||
|
||||
local vim_source = {}
|
||||
|
||||
---@param id integer
|
||||
---@param args any[]
|
||||
vim_source.on_callback = function(id, args)
|
||||
if vim_source.to_callback.callbacks[id] then
|
||||
vim_source.to_callback.callbacks[id](unpack(args))
|
||||
end
|
||||
end
|
||||
|
||||
---@param callback function
|
||||
---@return integer
|
||||
vim_source.to_callback = setmetatable({
|
||||
callbacks = {},
|
||||
}, {
|
||||
__call = function(self, callback)
|
||||
local id = misc.id('cmp.vim_source.to_callback')
|
||||
self.callbacks[id] = function(...)
|
||||
callback(...)
|
||||
self.callbacks[id] = nil
|
||||
end
|
||||
return id
|
||||
end,
|
||||
})
|
||||
|
||||
---Convert to serializable args.
|
||||
---@param args any[]
|
||||
vim_source.to_args = function(args)
|
||||
for i, arg in ipairs(args) do
|
||||
if type(arg) == 'function' then
|
||||
args[i] = vim_source.to_callback(arg)
|
||||
end
|
||||
end
|
||||
return args
|
||||
end
|
||||
|
||||
---@param bridge_id integer
|
||||
---@param methods string[]
|
||||
vim_source.new = function(bridge_id, methods)
|
||||
local self = {}
|
||||
for _, method in ipairs(methods) do
|
||||
self[method] = (function(m)
|
||||
return function(_, ...)
|
||||
return vim.fn['cmp#_method'](bridge_id, m, vim_source.to_args({ ... }))
|
||||
end
|
||||
end)(method)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
return vim_source
|
||||
Reference in New Issue
Block a user