Use floating window for completion menus (#224)

* WIP

* WIP

* Fix #226

* Insert text

* Emulate vim native

* テキトウ

* Tekito

* Move scrollbar impl

* aaa

* Ignore unexpected event

* fix

* fix scroll

* Refactor (conflict...)

* Fix bug

* Positive integer

* Refactor a bit

* Fix for pumheight=0

* fx

* Improve matching highlight

* Improve colorscheme handling

* fmt

* Add cmp.visible

* Fix pum pos

* ABBR_MARGIN

* Fix cel calculation

* up

* refactor

* fix

* a

* a

* compat

* Remove current completion state

* Fix ghost text

* Add feature toggle

* highlight customization

* Update

* Add breaking change announcement

* Add README.md

* Remove unused function

* extmark ephemeral ghost text

* Support native comp

* Fix docs  pos

* a

* Remove if native menu visible

* theme async

* Improvement idea: option to disables insert on select item (#240)

* use ghost text instead of insertion on prev/next item

* add disables_insert_on_selection option

* move disable_insert_on_select option as argumet on

* update README

* use an enum behavior to disable insert on select

* Adopt contribution

* Preselect

* Improve

* Change configuration option

* a

* Improve

* Improve

* Implement proper <C-e> behavior to native/custom

* Support <C-c> maybe

* Improve docs view

* Improve

* Avoid syntax leak

* TODO: refactor

* Fix

* Revert win pos

* fmt

* ghost text remaining

* Don't use italic by default

* bottom

* dedup by label

* Ignore events

* up

* Hacky native view partial support

* up

* perf

* improve

* more cache

* fmt

* Fix format option

* fmt

* recheck

* Fix

* Improve

* Improve

* compat

* implement redraw

* improve

* up

* fmt/lint

* immediate ghost text

* source timeout

* up

* Support multibyte

* disable highlight

* up

* improve

* fmt

* fmt

* fix

* fix

* up

* up

* Use screenpos

* Add undojoin check

* Fix height

* matcher bug

* Fix dot-repeat

* Remove undojoin

* macro

* Support dot-repeat

* MacroSafe

* Default item count is 200

* fmt

Co-authored-by: Eric Puentes <eric.puentes@mercadolibre.com.co>
This commit is contained in:
hrsh7th
2021-10-08 18:27:33 +09:00
committed by GitHub
parent 5bed2dc9f3
commit ada9ddeff7
31 changed files with 1802 additions and 718 deletions

View File

@@ -3,6 +3,11 @@
A completion engine plugin for neovim written in Lua. A completion engine plugin for neovim written in Lua.
Completion sources are installed from external repositories and "sourced". Completion sources are installed from external repositories and "sourced".
Readme!
====================
nvim-cmp's breaking change are [here](https://github.com/hrsh7th/nvim-cmp/issues/231).
Status Status
==================== ====================
@@ -67,7 +72,7 @@ lua <<EOF
snippet = { snippet = {
expand = function(args) expand = function(args)
-- For `vsnip` user. -- For `vsnip` user.
vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` user. vim.fn["vsnip#anonymous"](args.body)
-- For `luasnip` user. -- For `luasnip` user.
-- require('luasnip').lsp_expand(args.body) -- require('luasnip').lsp_expand(args.body)
@@ -125,8 +130,8 @@ If you want to remove an option, you can set it to `false` instead.
Built in helper `cmd.mappings` are: Built in helper `cmd.mappings` are:
- *cmp.mapping.select_prev_item()* - *cmp.mapping.select_prev_item({ cmp.SelectBehavior.{Insert,Select} } })*
- *cmp.mapping.select_next_item()* - *cmp.mapping.select_next_item({ cmp.SelectBehavior.{Insert,Select} })*
- *cmp.mapping.scroll_docs(number)* - *cmp.mapping.scroll_docs(number)*
- *cmp.mapping.complete()* - *cmp.mapping.complete()*
- *cmp.mapping.close()* - *cmp.mapping.close()*
@@ -137,6 +142,10 @@ You can configure `nvim-cmp` to use these `cmd.mappings` like this:
```lua ```lua
mapping = { mapping = {
['<C-n>'] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Insert }),
['<C-p>'] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Insert }),
['<Down>'] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Select }),
['<Up>'] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Select }),
['<C-d>'] = cmp.mapping.scroll_docs(-4), ['<C-d>'] = cmp.mapping.scroll_docs(-4),
['<C-f>'] = cmp.mapping.scroll_docs(4), ['<C-f>'] = cmp.mapping.scroll_docs(4),
['<C-Space>'] = cmp.mapping.complete(), ['<C-Space>'] = cmp.mapping.complete(),
@@ -344,14 +353,6 @@ The documentation window's max width.
The documentation window's max height. The documentation window's max height.
#### formatting.deprecated (type: boolean)
Specify deprecated candidate should be marked as deprecated or not.
This option is useful but disabled by default because sometimes, this option can break your terminal appearance.
Default: `false`
#### formatting.format (type: fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem) #### formatting.format (type: fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem)
A function to customize completion menu. A function to customize completion menu.
@@ -375,13 +376,18 @@ cmp.setup {
A callback function called when the item is confirmed. A callback function called when the item is confirmed.
#### experimental.ghost_text (type: boolean) #### experimental.native_menu (type: boolean)
Use vim's native completion menu instead of custom floating menu.
Default: `false`
#### experimental.ghost_text (type: cmp.GhostTextConfig | false)
Specify whether to display ghost text. Specify whether to display ghost text.
Default: `false` Default: `false`
Commands Commands
==================== ====================
@@ -396,6 +402,34 @@ Autocmds
Invoke after nvim-cmp setup. Invoke after nvim-cmp setup.
Highlights
====================
※ The following highlights are used only when enabling `experimental.cusom_menu = true`.
#### `CmpItemAbbr`
The abbr field.
#### `CmpItemAbbrDeprecated`
The deprecated item's abbr field.
#### `CmpItemAbbrMatch`
The matched characters highlight.
#### `CmpItemAbbrMatchFuzzy`
The fuzzy matched characters highlight.
#### `CmpItemKind`
The kind field.
#### `CmpItemMenu`
The menu field.
Programatic API Programatic API
==================== ====================
@@ -418,11 +452,11 @@ Close current completion menu.
Close current completion menu and restore current line (similar to native `<C-e>` behavior). Close current completion menu and restore current line (similar to native `<C-e>` behavior).
#### `cmp.select_next_item()` #### `cmp.select_next_item({ cmp.SelectBehavior.{Insert,Select} })`
Select next completion item if possible. Select next completion item if possible.
#### `cmp.select_prev_item()` #### `cmp.select_prev_item({ cmp.SelectBehavior.{Insert,Select} })`
Select prev completion item if possible. Select prev completion item if possible.

View File

@@ -49,7 +49,7 @@ endfunction
function! s:ensure(expr) abort function! s:ensure(expr) abort
if !bufexists(a:expr) if !bufexists(a:expr)
if type(a:expr) == type(0) if type(a:expr) == type(0)
throw printf('VS.Vim.Buffer: `%s` is not valid expr.', l:bufnr) throw printf('VS.Vim.Buffer: `%s` is not valid expr.', a:expr)
endif endif
badd `=a:expr` badd `=a:expr`
endif endif

View File

@@ -1,4 +1,5 @@
local compare = require('cmp.config.compare') local compare = require('cmp.config.compare')
local mapping = require('cmp.config.mapping')
local types = require('cmp.types') local types = require('cmp.types')
local WIDE_HEIGHT = 40 local WIDE_HEIGHT = 40
@@ -46,28 +47,58 @@ return function()
sorting = { sorting = {
priority_weight = 2, priority_weight = 2,
comparators = { comparators = {
compare.offset, function(e1, e2)
compare.exact, local diff
compare.score, diff = compare.offset(e1, e2)
compare.kind, if diff ~= nil then
compare.sort_text, return diff
compare.length, end
compare.order, diff = compare.exact(e1, e2)
if diff ~= nil then
return diff
end
diff = compare.score(e1, e2)
if diff ~= nil then
return diff
end
diff = compare.kind(e1, e2)
if diff ~= nil then
return diff
end
diff = compare.sort_text(e1, e2)
if diff ~= nil then
return diff
end
diff = compare.length(e1, e2)
if diff ~= nil then
return diff
end
return compare.order(e1, e2)
end,
}, },
}, },
event = {}, event = {},
mapping = {}, mapping = {
['<Down>'] = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Select }),
['<Up>'] = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Select }),
['<C-n>'] = mapping.select_next_item({ behavior = types.cmp.SelectBehavior.Insert }),
['<C-p>'] = mapping.select_prev_item({ behavior = types.cmp.SelectBehavior.Insert }),
['<C-c>'] = function(fallback)
require('cmp').close()
fallback()
end,
},
formatting = { formatting = {
deprecated = false,
format = function(_, vim_item) format = function(_, vim_item)
return vim_item return vim_item
end, end,
}, },
experimental = { experimental = {
native_menu = false,
ghost_text = false, ghost_text = false,
}, },

View File

@@ -1,5 +1,3 @@
local misc = require('cmp.utils.misc')
local mapping = setmetatable({}, { local mapping = setmetatable({}, {
__call = function(_, invoke, modes) __call = function(_, invoke, modes)
return { return {
@@ -10,6 +8,7 @@ local mapping = setmetatable({}, {
} }
end, end,
}) })
---Invoke completion ---Invoke completion
mapping.complete = function() mapping.complete = function()
return function(fallback) return function(fallback)
@@ -45,27 +44,24 @@ mapping.scroll_docs = function(delta)
end end
end end
end end
mapping.scroll = misc.deprecated(mapping.scroll_docs, '`cmp.mapping.scroll` is deprecated. Please change it to `cmp.mapping.scroll_docs` instead.')
---Select next completion item. ---Select next completion item.
mapping.select_next_item = function() mapping.select_next_item = function(option)
return function(fallback) return function(fallback)
if not require('cmp').select_next_item() then if not require('cmp').select_next_item(option) then
fallback() fallback()
end end
end end
end end
mapping.next_item = misc.deprecated(mapping.select_next_item, '`cmp.mapping.next_item` is deprecated. Please change it to `cmp.mapping.select_next_item` instead.')
---Select prev completion item. ---Select prev completion item.
mapping.select_prev_item = function() mapping.select_prev_item = function(option)
return function(fallback) return function(fallback)
if not require('cmp').select_prev_item() then if not require('cmp').select_prev_item(option) then
fallback() fallback()
end end
end end
end end
mapping.prev_item = misc.deprecated(mapping.select_prev_item, '`cmp.mapping.prev_item` is deprecated. Please change it to `cmp.mapping.select_prev_item` instead.')
---Confirm selection ---Confirm selection
mapping.confirm = function(option) mapping.confirm = function(option)

View File

@@ -8,8 +8,6 @@ local cache = require('cmp.utils.cache')
---@field public cache cmp.Cache ---@field public cache cmp.Cache
---@field public prev_context cmp.Context ---@field public prev_context cmp.Context
---@field public option cmp.ContextOption ---@field public option cmp.ContextOption
---@field public pumvisible boolean
---@field public pumselect boolean
---@field public filetype string ---@field public filetype string
---@field public time number ---@field public time number
---@field public mode string ---@field public mode string
@@ -41,13 +39,10 @@ context.new = function(prev_context, option)
option = option or {} option = option or {}
local self = setmetatable({}, { __index = context }) local self = setmetatable({}, { __index = context })
local completeinfo = vim.fn.complete_info({ 'selected', 'mode', 'pum_visible' })
self.id = misc.id('context') self.id = misc.id('context')
self.cache = cache.new() self.cache = cache.new()
self.prev_context = prev_context or context.empty() self.prev_context = prev_context or context.empty()
self.option = option or { reason = types.cmp.ContextReason.None } self.option = option or { reason = types.cmp.ContextReason.None }
self.pumvisible = completeinfo.pum_visible ~= 0
self.pumselect = completeinfo.selected ~= -1
self.filetype = vim.api.nvim_buf_get_option(0, 'filetype') self.filetype = vim.api.nvim_buf_get_option(0, 'filetype')
self.time = vim.loop.now() self.time = vim.loop.now()
self.mode = vim.api.nvim_get_mode().mode self.mode = vim.api.nvim_get_mode().mode
@@ -110,13 +105,6 @@ end
context.changed = function(self, ctx) context.changed = function(self, ctx)
local curr = self local curr = self
if self.pumvisible then
local completed_item = vim.v.completed_item or {}
if completed_item.word then
return false
end
end
if curr.bufnr ~= ctx.bufnr then if curr.bufnr ~= ctx.bufnr then
return true return true
end end

View File

@@ -1,137 +1,92 @@
local debug = require('cmp.utils.debug') local debug = require('cmp.utils.debug')
local char = require('cmp.utils.char') local char = require('cmp.utils.char')
local str = require('cmp.utils.str')
local pattern = require('cmp.utils.pattern') local pattern = require('cmp.utils.pattern')
local async = require('cmp.utils.async') local async = require('cmp.utils.async')
local keymap = require('cmp.utils.keymap') local keymap = require('cmp.utils.keymap')
local context = require('cmp.context') local context = require('cmp.context')
local source = require('cmp.source') local source = require('cmp.source')
local menu = require('cmp.menu') local view = require('cmp.view')
local misc = require('cmp.utils.misc') local misc = require('cmp.utils.misc')
local config = require('cmp.config') local config = require('cmp.config')
local types = require('cmp.types') local types = require('cmp.types')
local SOURCE_TIMEOUT = 500
local THROTTLE_TIME = 120
local DEBOUNCE_TIME = 20
---@class cmp.Core ---@class cmp.Core
---@field public suspending boolean
---@field public view cmp.View
---@field public sources cmp.Source[]
---@field public sources_by_name table<string, cmp.Source>
---@field public context cmp.Context
local core = {} local core = {}
core.SOURCE_TIMEOUT = 500 core.new = function()
core.THROTTLE_TIME = 80 local self = setmetatable({}, { __index = core })
self.suspending = false
---Suspending state. self.sources = {}
core.suspending = false self.sources_by_name = {}
self.context = context.new()
core.GHOST_TEXT_NS = vim.api.nvim_create_namespace('cmp:GHOST_TEXT') self.view = view.new()
self.view.event:on('keymap', function(...)
---@type cmp.Menu self:on_keymap(...)
core.menu = menu.new({ end)
on_select = function(e) return self
for _, c in ipairs(config.get().confirmation.get_commit_characters(e:get_commit_characters())) do
keymap.listen('i', c, core.on_keymap)
end
core.ghost_text(e)
end,
})
---Show ghost text if possible
---@param e cmp.Entry
core.ghost_text = function(e)
vim.api.nvim_buf_clear_namespace(0, core.GHOST_TEXT_NS, 0, -1)
local c = config.get().experimental.ghost_text
if not c then
return
end
if not e then
return
end
local ctx = context.new()
if ctx.cursor_after_line ~= '' then
return
end
local diff = ctx.cursor.col - e:get_offset()
local text = e:get_insert_text()
if e.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
text = vim.lsp.util.parse_snippet(text)
end
text = string.sub(str.oneline(text), diff + 1)
if #text > 0 then
vim.api.nvim_buf_set_extmark(ctx.bufnr, core.GHOST_TEXT_NS, ctx.cursor.row - 1, ctx.cursor.col - 1, {
right_gravity = false,
virt_text = { { text, c.hl_group or 'Comment' } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
priority = 1,
})
end
end end
---@type table<number, cmp.Source>
core.sources = {}
---@type table<string, cmp.Source[]>
core.sources_by_name = {}
---@type cmp.Context
core.context = context.new()
---Register source ---Register source
---@param s cmp.Source ---@param s cmp.Source
core.register_source = function(s) core.register_source = function(self, s)
core.sources[s.id] = s self.sources[s.id] = s
if not core.sources_by_name[s.name] then if not self.sources_by_name[s.name] then
core.sources_by_name[s.name] = {} self.sources_by_name[s.name] = {}
end
table.insert(core.sources_by_name[s.name], s)
if misc.is_insert_mode() then
core.complete(core.get_context({ reason = types.cmp.ContextReason.Auto }))
end end
table.insert(self.sources_by_name[s.name], s)
end end
---Unregister source ---Unregister source
---@param source_id string ---@param source_id string
core.unregister_source = function(source_id) core.unregister_source = function(self, source_id)
local name = core.sources[source_id].name local name = self.sources[source_id].name
core.sources_by_name[name] = vim.tbl_filter(function(s) self.sources_by_name[name] = vim.tbl_filter(function(s)
return s.id ~= source_id return s.id ~= source_id
end, core.sources_by_name[name]) end, self.sources_by_name[name])
core.sources[source_id] = nil self.sources[source_id] = nil
end end
---Get new context ---Get new context
---@param option cmp.ContextOption ---@param option cmp.ContextOption
---@return cmp.Context ---@return cmp.Context
core.get_context = function(option) core.get_context = function(self, option)
local prev = core.context:clone() local prev = self.context:clone()
prev.prev_context = nil prev.prev_context = nil
local ctx = context.new(prev, option) local ctx = context.new(prev, option)
core.set_context(ctx) self:set_context(ctx)
return core.context return self.context
end end
---Set new context ---Set new context
---@param ctx cmp.Context ---@param ctx cmp.Context
core.set_context = function(ctx) core.set_context = function(self, ctx)
core.context = ctx self.context = ctx
end end
---Suspend completion ---Suspend completion
core.suspend = function() core.suspend = function(self)
core.suspending = true self.suspending = true
return function() return function()
core.suspending = false self.suspending = false
end end
end end
---Get sources that sorted by priority ---Get sources that sorted by priority
---@param statuses cmp.SourceStatus[] ---@param statuses cmp.SourceStatus[]
---@return cmp.Source[] ---@return cmp.Source[]
core.get_sources = function(statuses) core.get_sources = function(self, statuses)
local sources = {} local sources = {}
for _, c in pairs(config.get().sources) do for _, c in pairs(config.get().sources) do
for _, s in ipairs(core.sources_by_name[c.name] or {}) do for _, s in ipairs(self.sources_by_name[c.name] or {}) do
if not statuses or vim.tbl_contains(statuses, s.status) then if not statuses or vim.tbl_contains(statuses, s.status) then
if s:is_available() then if s:is_available() then
table.insert(sources, s) table.insert(sources, s)
@@ -143,7 +98,7 @@ core.get_sources = function(statuses)
end end
---Keypress handler ---Keypress handler
core.on_keymap = function(keys, fallback) core.on_keymap = function(self, keys, fallback)
for key, action in pairs(config.get().mapping) do for key, action in pairs(config.get().mapping) do
if keymap.equals(key, keys) then if keymap.equals(key, keys) then
if type(action) == 'function' then if type(action) == 'function' then
@@ -157,18 +112,18 @@ core.on_keymap = function(keys, fallback)
--Commit character. NOTE: This has a lot of cmp specific implementation to make more user-friendly. --Commit character. NOTE: This has a lot of cmp specific implementation to make more user-friendly.
local chars = keymap.t(keys) local chars = keymap.t(keys)
local e = core.menu:get_selected_entry() 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 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)) local is_printable = char.is_printable(string.byte(chars, 1))
core.confirm(e, { self:confirm(e, {
behavior = is_printable and 'insert' or 'replace', behavior = is_printable and 'insert' or 'replace',
}, function() }, function()
local ctx = core.get_context() local ctx = self:get_context()
local word = e:get_word() local word = e:get_word()
if string.sub(ctx.cursor_before_line, -#word, ctx.cursor.col - 1) == word and is_printable then if string.sub(ctx.cursor_before_line, -#word, ctx.cursor.col - 1) == word and is_printable then
fallback() fallback()
else else
core.reset() self:reset()
end end
end) end)
return return
@@ -178,7 +133,7 @@ core.on_keymap = function(keys, fallback)
end end
---Prepare completion ---Prepare completion
core.prepare = function() core.prepare = function(self)
for keys, action in pairs(config.get().mapping) do for keys, action in pairs(config.get().mapping) do
if type(action) == 'function' then if type(action) == 'function' then
action = { action = {
@@ -187,36 +142,37 @@ core.prepare = function()
} }
end end
for _, mode in ipairs(action.modes) do for _, mode in ipairs(action.modes) do
keymap.listen(mode, keys, core.on_keymap) keymap.listen(mode, keys, function(...)
self:on_keymap(...)
end)
end end
end end
end end
---Check auto-completion ---Check auto-completion
core.on_change = function(event) core.on_change = function(self, event)
if core.suspending then 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 return
end end
core.autoindent(event, function() self:autoindent(event, function()
local ctx = core.get_context({ reason = types.cmp.ContextReason.Auto }) local ctx = self:get_context({ reason = types.cmp.ContextReason.Auto })
-- Skip autocompletion when the item is selected manually.
if ctx.pumvisible and not vim.tbl_isempty(vim.v.completed_item) then
return
end
debug.log(('ctx: `%s`'):format(ctx.cursor_before_line)) debug.log(('ctx: `%s`'):format(ctx.cursor_before_line))
if ctx:changed(ctx.prev_context) then if ctx:changed(ctx.prev_context) then
self.view:redraw()
debug.log('changed') debug.log('changed')
core.menu:restore(ctx)
core.ghost_text(core.menu:get_first_entry())
if vim.tbl_contains(config.get().completion.autocomplete or {}, event) then if vim.tbl_contains(config.get().completion.autocomplete or {}, event) then
core.complete(ctx) self:complete(ctx)
else else
core.filter.timeout = core.THROTTLE_TIME self.filter.timeout = THROTTLE_TIME
core.filter() self:filter()
end end
else else
debug.log('unchanged') debug.log('unchanged')
@@ -227,25 +183,28 @@ end
---Check autoindent ---Check autoindent
---@param event cmp.TriggerEvent ---@param event cmp.TriggerEvent
---@param callback function ---@param callback function
core.autoindent = function(event, callback) core.autoindent = function(self, event, callback)
if event == types.cmp.TriggerEvent.TextChanged then if event == types.cmp.TriggerEvent.TextChanged then
local cursor_before_line = misc.get_cursor_before_line() local cursor_before_line = misc.get_cursor_before_line()
local prefix = pattern.matchstr('[^[:blank:]]\\+$', cursor_before_line) local prefix = pattern.matchstr('[^[:blank:]]\\+$', cursor_before_line)
if prefix then if prefix then
for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do for _, key in ipairs(vim.split(vim.bo.indentkeys, ',')) do
if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then if vim.tbl_contains({ '=' .. prefix, '0=' .. prefix }, key) then
return vim.schedule(function() local release = self:suspend()
vim.schedule(function()
if cursor_before_line == misc.get_cursor_before_line() then if cursor_before_line == misc.get_cursor_before_line() then
local indentkeys = vim.bo.indentkeys local indentkeys = vim.bo.indentkeys
vim.bo.indentkeys = indentkeys .. ',!^F' vim.bo.indentkeys = indentkeys .. ',!^F'
keymap.feedkeys(keymap.t('<C-f>'), 'n', function() keymap.feedkeys(keymap.t('<C-f>'), 'n', function()
vim.bo.indentkeys = indentkeys vim.bo.indentkeys = indentkeys
release()
callback() callback()
end) end)
else else
callback() callback()
end end
end) end)
return
end end
end end
end end
@@ -255,62 +214,71 @@ end
---Invoke completion ---Invoke completion
---@param ctx cmp.Context ---@param ctx cmp.Context
core.complete = function(ctx) core.complete = function(self, ctx)
if not misc.is_insert_mode() then if not misc.is_insert_mode() then
return return
end end
self:set_context(ctx)
core.set_context(ctx) for _, s in ipairs(self:get_sources({ source.SourceStatus.WAITING, source.SourceStatus.COMPLETED })) do
s:complete(
local callback = function() ctx,
(function(src)
local callback
callback = function()
local new = context.new(ctx) local new = context.new(ctx)
if new:changed(new.prev_context) and ctx == core.context then if new:changed(new.prev_context) and ctx == self.context then
core.complete(new) src:complete(new, callback)
else else
core.filter.timeout = core.THROTTLE_TIME self.filter.stop()
core.filter() self.filter.timeout = DEBOUNCE_TIME
self:filter()
end end
end end
for _, s in ipairs(core.get_sources({ source.SourceStatus.WAITING, source.SourceStatus.COMPLETED })) do return callback
s:complete(ctx, callback) end)(s)
)
end end
core.filter.timeout = ctx.pumvisible and core.THROTTLE_TIME or 0 self.filter.timeout = THROTTLE_TIME
core.filter() self:filter()
end end
---Update completion menu ---Update completion menu
core.filter = async.throttle(function() core.filter = async.throttle(
vim.schedule_wrap(function(self)
if not misc.is_insert_mode() then if not misc.is_insert_mode() then
return return
end end
local ctx = core.get_context() local ctx = self:get_context()
-- To wait for processing source for that's timeout. -- To wait for processing source for that's timeout.
local sources = {} local sources = {}
for _, s in ipairs(core.get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do for _, s in ipairs(self:get_sources({ source.SourceStatus.FETCHING, source.SourceStatus.COMPLETED })) do
local time = core.SOURCE_TIMEOUT - s:get_fetching_time() local time = SOURCE_TIMEOUT - s:get_fetching_time()
if not s.incomplete and time > 0 then if not s.incomplete and time > 0 then
if #sources == 0 then if #sources == 0 then
core.filter.stop() self.filter.stop()
core.filter.timeout = time + 1 self.filter.timeout = time + 1
core.filter() self:filter()
return return
end end
break break
end end
table.insert(sources, s) table.insert(sources, s)
end end
self.filter.timeout = THROTTLE_TIME
core.menu:update(ctx, sources) self.view:open(ctx, sources)
core.ghost_text(core.menu:get_first_entry()) end),
end, core.THROTTLE_TIME) THROTTLE_TIME
)
---Confirm completion. ---Confirm completion.
---@param e cmp.Entry ---@param e cmp.Entry
---@param option cmp.ConfirmOption ---@param option cmp.ConfirmOption
---@param callback function ---@param callback function
core.confirm = function(e, option, callback) core.confirm = function(self, e, option, callback)
if not (e and not e.confirmed) then if not (e and not e.confirmed) then
return return
end end
@@ -318,8 +286,11 @@ core.confirm = function(e, option, callback)
debug.log('entry.confirm', e:get_completion_item()) debug.log('entry.confirm', e:get_completion_item())
local suspending = core.suspend() local release = self:suspend()
local ctx = core.get_context() local ctx = self:get_context()
-- Close menus.
self.view:close()
-- Simulate `<C-y>` behavior. -- Simulate `<C-y>` behavior.
local confirm = {} local confirm = {}
@@ -398,7 +369,7 @@ core.confirm = function(e, option, callback)
}) })
end end
e:execute(vim.schedule_wrap(function() e:execute(vim.schedule_wrap(function()
suspending() release()
if config.get().event.on_confirm_done then if config.get().event.on_confirm_done then
config.get().event.on_confirm_done(e) config.get().event.on_confirm_done(e)
@@ -413,14 +384,11 @@ core.confirm = function(e, option, callback)
end end
---Reset current completion state ---Reset current completion state
core.reset = function() core.reset = function(self)
for _, s in pairs(core.sources) do for _, s in pairs(self.sources) do
s:reset() s:reset()
end end
core.menu:reset() self:get_context() -- To prevent new event
core.get_context() -- To prevent new event
core.ghost_text(nil)
end end
return core return core

View File

@@ -10,6 +10,7 @@ local types = require('cmp.types')
---@field public cache cmp.Cache ---@field public cache cmp.Cache
---@field public score number ---@field public score number
---@field public exact boolean ---@field public exact boolean
---@field public matches table
---@field public context cmp.Context ---@field public context cmp.Context
---@field public source cmp.Source ---@field public source cmp.Source
---@field public source_offset number ---@field public source_offset number
@@ -32,6 +33,8 @@ entry.new = function(ctx, source, completion_item)
self.id = misc.id('entry') self.id = misc.id('entry')
self.cache = cache.new() self.cache = cache.new()
self.score = 0 self.score = 0
self.exact = false
self.matches = {}
self.context = ctx self.context = ctx
self.source = source self.source = source
self.source_offset = source.request_offset self.source_offset = source.request_offset
@@ -56,7 +59,7 @@ entry.get_offset = function(self)
local c = misc.to_vimindex(self.context.cursor_line, range.start.character) local c = misc.to_vimindex(self.context.cursor_line, range.start.character)
for idx = c, self.source_offset do for idx = c, self.source_offset do
if not char.is_white(string.byte(self.context.cursor_line, idx)) then if not char.is_white(string.byte(self.context.cursor_line, idx)) then
offset = math.min(offset, idx) offset = idx
break break
end end
end end
@@ -103,8 +106,8 @@ entry.get_word = function(self)
local word local word
if misc.safe(self.completion_item.textEdit) then if misc.safe(self.completion_item.textEdit) then
word = str.trim(self.completion_item.textEdit.newText) word = str.trim(self.completion_item.textEdit.newText)
local _, after = self:get_overwrite() local overwrite = self:get_overwrite()
if 0 < after or self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then if 0 < overwrite[2] or self.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = str.get_word(word, string.byte(self.context.cursor_after_line, 1)) word = str.get_word(word, string.byte(self.context.cursor_after_line, 1))
end end
elseif misc.safe(self.completion_item.insertText) then elseif misc.safe(self.completion_item.insertText) then
@@ -129,9 +132,9 @@ entry.get_overwrite = function(self)
local e = misc.to_vimindex(self.context.cursor_line, r['end'].character) local e = misc.to_vimindex(self.context.cursor_line, r['end'].character)
local before = self.context.cursor.col - s local before = self.context.cursor.col - s
local after = e - self.context.cursor.col local after = e - self.context.cursor.col
return before, after return { before, after }
end end
return 0, 0 return { 0, 0 }
end) end)
end end
@@ -185,6 +188,38 @@ entry.get_insert_text = function(self)
end) end)
end end
---Return the item is deprecated or not.
---@return boolean
entry.is_deprecated = function(self)
return self.completion_item.deprecated or vim.tbl_contains(self.completion_item.tags or {}, types.lsp.CompletionItemTag.Deprecated)
end
---Return view information.
---@return { abbr: { text: string, bytes: number, width: number, hl_group: string }, kind: { text: string, bytes: number, width: number, hl_group: string }, menu: { text: string, bytes: number, width: number, hl_group: string } }
entry.get_view = function(self, suggest_offset)
local item = self:get_vim_item(suggest_offset)
return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0 }, function()
local view = {}
view.abbr = {}
view.abbr.text = item.abbr or ''
view.abbr.bytes = #view.abbr.text
view.abbr.width = vim.str_utfindex(view.abbr.text)
view.abbr.hl_group = 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.str_utfindex(view.kind.text)
view.kind.hl_group = 'CmpItemKind'
view.menu = {}
view.menu.text = item.menu or ''
view.menu.bytes = #view.menu.text
view.menu.width = vim.str_utfindex(view.menu.text)
view.menu.hl_group = 'CmpItemMenu'
view.dup = item.dup
return view
end)
end
---Make vim.CompletedItem ---Make vim.CompletedItem
---@param suggest_offset number ---@param suggest_offset number
---@return vim.CompletedItem ---@return vim.CompletedItem
@@ -192,7 +227,7 @@ entry.get_vim_item = function(self, suggest_offset)
return self.cache:ensure({ 'get_vim_item', suggest_offset, self.resolved_completion_item and 1 or 0 }, function() return self.cache:ensure({ 'get_vim_item', suggest_offset, self.resolved_completion_item and 1 or 0 }, function()
local completion_item = self:get_completion_item() local completion_item = self:get_completion_item()
local word = self:get_word() local word = self:get_word()
local abbr = str.trim(completion_item.label) local abbr = str.oneline(str.trim(completion_item.label))
-- ~ indicator -- ~ indicator
if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then
@@ -204,13 +239,6 @@ entry.get_vim_item = function(self, suggest_offset)
end end
end end
-- deprecated
if config.get().formatting.deprecated then
if completion_item.deprecated or vim.tbl_contains(completion_item.tags or {}, types.lsp.CompletionItemTag.Deprecated) then
abbr = str.strikethrough(abbr)
end
end
-- append delta text -- append delta text
if suggest_offset < self:get_offset() then if suggest_offset < self:get_offset() then
word = string.sub(self.context.cursor_before_line, suggest_offset, self:get_offset() - 1) .. word word = string.sub(self.context.cursor_before_line, suggest_offset, self:get_offset() - 1) .. word
@@ -246,9 +274,12 @@ entry.get_vim_item = function(self, suggest_offset)
if config.get().formatting.format then if config.get().formatting.format then
vim_item = config.get().formatting.format(self, vim_item) vim_item = config.get().formatting.format(self, vim_item)
end 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.equal = 1
vim_item.empty = 1 vim_item.empty = 1
vim_item.user_data = ('cmp:%s'):format(self.id)
return vim_item return vim_item
end) end)

View File

@@ -1,138 +0,0 @@
local async = require('cmp.utils.async')
local config = require('cmp.config')
---@class cmp.Float
---@field public entry cmp.Entry|nil
---@field public buf number|nil
---@field public win number|nil
local float = {}
---Create new floating window module
float.new = function()
local self = setmetatable({}, { __index = float })
self.entry = nil
self.win = nil
self.buf = nil
return self
end
---Show floating window
---@param e cmp.Entry
float.show = function(self, e)
float.close.stop()
local documentation = config.get().documentation
if not documentation then
return
end
local pum = vim.fn.pum_getpos() or {}
if not pum.col then
return self:close()
end
local right_space = vim.o.columns - (pum.col + pum.width + (pum.scrollbar and 1 or 0)) - 1
local left_space = pum.col - 1
local maxwidth = math.min(documentation.maxwidth, math.max(left_space, right_space))
-- update buffer content if needed.
if not self.entry or e.id ~= self.entry.id then
local documents = e:get_documentation()
if #documents == 0 then
return self:close()
end
self.entry = e
self.buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(self.buf, 'bufhidden', 'wipe')
vim.lsp.util.stylize_markdown(self.buf, documents, {
max_width = maxwidth,
max_height = documentation.maxheight,
})
end
local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.buf, 0, -1, false), {
max_width = maxwidth,
max_height = documentation.maxheight,
})
if width <= 0 or height <= 0 then
return self:close()
end
local right_col = pum.col + pum.width + (pum.scrollbar and 1 or 0)
local left_col = pum.col - width - 3 -- TODO: Why is this needed -3?
local col
if right_space >= width and left_space >= width then
if right_space < left_space then
col = left_col
else
col = right_col
end
elseif right_space >= width then
col = right_col
elseif left_space >= width then
col = left_col
else
return self:close()
end
local style = {
relative = 'editor',
style = 'minimal',
width = width,
height = height,
row = pum.row,
col = col,
border = documentation.border,
}
if self.win and vim.api.nvim_win_is_valid(self.win) then
vim.api.nvim_win_set_buf(self.win, self.buf)
vim.api.nvim_win_set_config(self.win, style)
else
self.win = vim.api.nvim_open_win(self.buf, false, style)
vim.api.nvim_win_set_option(self.win, 'conceallevel', 2)
vim.api.nvim_win_set_option(self.win, 'concealcursor', 'n')
vim.api.nvim_win_set_option(self.win, 'winhighlight', config.get().documentation.winhighlight)
vim.api.nvim_win_set_option(self.win, 'foldenable', false)
vim.api.nvim_win_set_option(self.win, 'wrap', true)
vim.api.nvim_win_set_option(self.win, 'scrolloff', 0)
end
end
---Close floating window
float.close = async.throttle(
vim.schedule_wrap(function(self)
if self:is_visible() then
vim.api.nvim_win_close(self.win, true)
end
self.entry = nil
self.buf = nil
self.win = nil
end),
20
)
float.scroll = function(self, delta)
if self:is_visible() then
local info = vim.fn.getwininfo(self.win)[1] or {}
local buf = vim.api.nvim_win_get_buf(self.win)
local top = info.topline or 1
top = top + delta
top = math.max(top, 1)
top = math.min(top, vim.api.nvim_buf_line_count(buf) - info.height + 1)
vim.defer_fn(function()
vim.api.nvim_buf_call(buf, function()
vim.api.nvim_command('normal! ' .. top .. 'zt')
end)
end, 0)
end
end
float.is_visible = function(self)
return self.win and vim.api.nvim_win_is_valid(self.win)
end
return float

View File

@@ -1,11 +1,13 @@
local core = require('cmp.core') local core = require('cmp.core')
local keymap = require('cmp.utils.keymap')
local source = require('cmp.source') local source = require('cmp.source')
local config = require('cmp.config') local config = require('cmp.config')
local autocmd = require('cmp.utils.autocmd') local autocmd = require('cmp.utils.autocmd')
local keymap = require('cmp.utils.keymap')
local cmp = {} local cmp = {}
cmp.core = core.new()
---Expose types ---Expose types
for k, v in pairs(require('cmp.types.cmp')) do for k, v in pairs(require('cmp.types.cmp')) do
cmp[k] = v cmp[k] = v
@@ -26,27 +28,38 @@ cmp.mapping = require('cmp.config.mapping')
---@return number ---@return number
cmp.register_source = function(name, s) cmp.register_source = function(name, s)
local src = source.new(name, s) local src = source.new(name, s)
core.register_source(src) cmp.core:register_source(src)
return src.id return src.id
end end
---Unregister completion source ---Unregister completion source
---@param id number ---@param id number
cmp.unregister_source = function(id) cmp.unregister_source = function(id)
core.unregister_source(id) cmp.core:unregister_source(id)
end end
---Invoke completion manually ---Invoke completion manually
cmp.complete = function() cmp.complete = function()
core.complete(core.get_context({ reason = cmp.ContextReason.Manual })) cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.Manual }))
return true return true
end end
---Return view is visible or not.
cmp.visible = function()
return cmp.core.view:visible() or vim.fn.pumvisible() == 1
end
---Close current completion ---Close current completion
cmp.close = function() cmp.close = function()
if vim.fn.pumvisible() == 1 then if cmp.core.view:visible() then
core.reset() local release = cmp.core:suspend()
keymap.feedkeys(keymap.t('<C-e>'), 'n') cmp.core.view:close()
cmp.core:reset()
vim.schedule(release)
return true
elseif vim.fn.pumvisible() == 1 then
vim.fn.complete(1, {})
cmp.core:reset()
return true return true
else else
return false return false
@@ -55,10 +68,13 @@ end
---Abort current completion ---Abort current completion
cmp.abort = function() cmp.abort = function()
if vim.fn.pumvisible() == 1 then if cmp.core.view:visible() then
keymap.feedkeys(keymap.t('<C-e>'), 'n', function() local release = cmp.core:suspend()
core.reset() cmp.core.view:abort()
end) vim.schedule(release)
return true
elseif vim.fn.pumvisible() == 1 then
vim.api.nvim_select_popupmenu_item(-1, true, true, {})
return true return true
else else
return false return false
@@ -66,9 +82,12 @@ cmp.abort = function()
end end
---Select next item if possible ---Select next item if possible
cmp.select_next_item = function() cmp.select_next_item = function(option)
if vim.fn.pumvisible() == 1 then option = option or {}
vim.api.nvim_feedkeys(keymap.t('<C-n>'), 'n', true) if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:select_next_item(option)
vim.schedule(release)
return true return true
else else
return false return false
@@ -76,9 +95,12 @@ cmp.select_next_item = function()
end end
---Select prev item if possible ---Select prev item if possible
cmp.select_prev_item = function() cmp.select_prev_item = function(option)
if vim.fn.pumvisible() == 1 then option = option or {}
vim.api.nvim_feedkeys(keymap.t('<C-p>'), 'n', true) if cmp.core.view:visible() then
local release = cmp.core:suspend()
cmp.core.view:select_prev_item(option)
vim.schedule(release)
return true return true
else else
return false return false
@@ -87,8 +109,8 @@ end
---Scrolling documentation window if possible ---Scrolling documentation window if possible
cmp.scroll_docs = function(delta) cmp.scroll_docs = function(delta)
if core.menu.float:is_visible() then if cmp.core.view:visible() then
core.menu.float:scroll(delta) cmp.core.view:scroll_docs(delta)
return true return true
else else
return false return false
@@ -98,15 +120,20 @@ end
---Confirm completion ---Confirm completion
cmp.confirm = function(option) cmp.confirm = function(option)
option = option or {} option = option or {}
local e = core.menu:get_selected_entry() or (option.select and core.menu:get_first_entry() or nil)
local e = cmp.core.view:get_selected_entry() or (option.select and cmp.core.view:get_first_entry() or nil)
if e then if e then
core.confirm(e, { cmp.core:confirm(e, {
behavior = option.behavior, behavior = option.behavior,
}, function() }, function()
core.complete(core.get_context({ reason = cmp.ContextReason.TriggerOnly })) cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly }))
end) end)
return true return true
else else
if vim.fn.complete_info({ 'selected' }).selected ~= -1 then
keymap.feedkeys(keymap.t('<C-y>'), 'n')
return true
end
return false return false
end end
end end
@@ -121,7 +148,7 @@ cmp.status = function()
kinds.installed = {} kinds.installed = {}
kinds.invalid = {} kinds.invalid = {}
local names = {} local names = {}
for _, s in pairs(core.sources) do for _, s in pairs(cmp.core.sources) do
names[s.name] = true names[s.name] = true
if config.get_source_config(s.name) then if config.get_source_config(s.name) then
@@ -192,20 +219,21 @@ autocmd.subscribe('InsertEnter', function()
-- Avoid unexpected mode detection (mode() function will returns `normal mode` on the InsertEnter event.) -- Avoid unexpected mode detection (mode() function will returns `normal mode` on the InsertEnter event.)
vim.schedule(function() vim.schedule(function()
if config.enabled() then if config.enabled() then
core.prepare() cmp.core:prepare()
core.on_change('InsertEnter') cmp.core:on_change('InsertEnter')
end end
end) end)
end) end)
autocmd.subscribe('TextChanged', function() autocmd.subscribe('TextChanged', function()
if config.enabled() then if config.enabled() then
core.on_change('TextChanged') cmp.core:on_change('TextChanged')
end end
end) end)
autocmd.subscribe('InsertLeave', function() autocmd.subscribe('InsertLeave', function()
core.reset() cmp.core:reset()
cmp.core.view:close()
end) end)
return cmp return cmp

View File

@@ -1,9 +1,8 @@
local char = require('cmp.utils.char') local char = require('cmp.utils.char')
local str = require('cmp.utils.str')
local matcher = {} local matcher = {}
matcher.WORD_BOUNDALY_ORDER_FACTOR = 5 matcher.WORD_BOUNDALY_ORDER_FACTOR = 10
matcher.PREFIX_FACTOR = 8 matcher.PREFIX_FACTOR = 8
matcher.NOT_FUZZY_FACTOR = 6 matcher.NOT_FUZZY_FACTOR = 6
@@ -78,12 +77,12 @@ end
matcher.match = function(input, word, words) matcher.match = function(input, word, words)
-- Empty input -- Empty input
if #input == 0 then if #input == 0 then
return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR return matcher.PREFIX_FACTOR + matcher.NOT_FUZZY_FACTOR, {}
end end
-- Ignore if input is long than word -- Ignore if input is long than word
if #input > #word then if #input > #word then
return 0 return 0, {}
end end
--- Gather matched regions --- Gather matched regions
@@ -107,17 +106,27 @@ matcher.match = function(input, word, words)
end end
if #matches == 0 then if #matches == 0 then
return 0 return 0, {}
end end
matcher.debug(word, matches)
-- Add prefix bonus -- Add prefix bonus
local prefix = false local prefix = false
if matches[1].input_match_start == 1 and matches[1].word_match_start == 1 then if matches[1].input_match_start == 1 and matches[1].word_match_start == 1 then
prefix = true prefix = true
else else
for _, w in ipairs(words or {}) do for _, w in ipairs(words or {}) do
if str.has_prefix(w, string.sub(input, matches[1].input_match_start, matches[1].input_match_end)) then
prefix = true prefix = true
local o = 1
for i = matches[1].input_match_start, matches[1].input_match_end do
if not char.match(string.byte(w, o), string.byte(input, i)) then
prefix = false
break
end
o = o + 1
end
if prefix then
break break
end end
end end
@@ -125,7 +134,7 @@ matcher.match = function(input, word, words)
-- Compute prefix match score -- Compute prefix match score
local score = prefix and matcher.PREFIX_FACTOR or 0 local score = prefix and matcher.PREFIX_FACTOR or 0
local boundary_fixer = prefix and matches[1].index - 1 or 0 local offset = prefix and matches[1].index - 1 or 0
local idx = 1 local idx = 1
for _, m in ipairs(matches) do for _, m in ipairs(matches) do
local s = 0 local s = 0
@@ -135,20 +144,21 @@ matcher.match = function(input, word, words)
end end
idx = idx + 1 idx = idx + 1
if s > 0 then if s > 0 then
s = s * (m.strict_match and 1.2 or 1) s = s * (1 + m.strict_ratio)
score = score + (s * (1 + math.max(0, matcher.WORD_BOUNDALY_ORDER_FACTOR - (m.index - boundary_fixer)) / matcher.WORD_BOUNDALY_ORDER_FACTOR)) s = s * (1 + math.max(0, matcher.WORD_BOUNDALY_ORDER_FACTOR - (m.index - offset)) / matcher.WORD_BOUNDALY_ORDER_FACTOR)
score = score + s
end end
end end
-- Check remaining input as fuzzy -- Check remaining input as fuzzy
if matches[#matches].input_match_end < #input then if matches[#matches].input_match_end < #input then
if matcher.fuzzy(input, word, matches) then if prefix and matcher.fuzzy(input, word, matches) then
return score return score, matches
end end
return 0 return 0, {}
end end
return score + matcher.NOT_FUZZY_FACTOR return score + matcher.NOT_FUZZY_FACTOR, matches
end end
--- fuzzy --- fuzzy
@@ -178,16 +188,37 @@ matcher.fuzzy = function(input, word, matches)
local matched = false local matched = false
local word_offset = 0 local word_offset = 0
local word_index = last_match.word_match_end + 1 local word_index = last_match.word_match_end + 1
local input_match_start = -1
local input_match_end = -1
local word_match_start = -1
local strict_count = 0
local match_count = 0
while word_offset + word_index <= #word and input_index <= #input do while word_offset + word_index <= #word and input_index <= #input do
if char.match(string.byte(word, word_index + word_offset), string.byte(input, input_index)) then 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 matched = true
input_index = input_index + 1 input_index = input_index + 1
strict_count = strict_count + (c1 == c2 and 1 or 0)
match_count = match_count + 1
elseif matched then elseif matched then
input_index = last_input_index input_index = last_input_index
input_match_end = input_index - 1
end end
word_offset = word_offset + 1 word_offset = word_offset + 1
end end
if input_index > #input then if input_index > #input then
table.insert(matches, {
input_match_start = input_match_start,
input_match_end = input_match_end,
word_match_start = word_match_start,
word_match_end = word_index + word_offset - 1,
strict_ratio = strict_count / match_count,
fuzzy = true,
})
return true return true
end end
return false return false
@@ -208,10 +239,11 @@ matcher.find_match_region = function(input, input_start_index, input_end_index,
return nil return nil
end end
local strict_match_count = 0
local input_match_start = -1 local input_match_start = -1
local input_index = input_end_index local input_index = input_end_index
local word_offset = 0 local word_offset = 0
local strict_count = 0
local match_count = 0
while input_index <= #input and word_index + word_offset <= #word do while input_index <= #input and word_index + word_offset <= #word do
local c1 = string.byte(input, input_index) local c1 = string.byte(input, input_index)
local c2 = string.byte(word, word_index + word_offset) local c2 = string.byte(word, word_index + word_offset)
@@ -221,11 +253,8 @@ matcher.find_match_region = function(input, input_start_index, input_end_index,
input_match_start = input_index input_match_start = input_index
end end
-- Increase strict_match_count strict_count = strict_count + (c1 == c2 and 1 or 0)
if c1 == c2 then match_count = match_count + 1
strict_match_count = strict_match_count + 1
end
word_offset = word_offset + 1 word_offset = word_offset + 1
else else
-- Match end (partial region) -- Match end (partial region)
@@ -235,7 +264,8 @@ matcher.find_match_region = function(input, input_start_index, input_end_index,
input_match_end = input_index - 1, input_match_end = input_index - 1,
word_match_start = word_index, word_match_start = word_index,
word_match_end = word_index + word_offset - 1, word_match_end = word_index + word_offset - 1,
strict_match = strict_match_count == input_index - input_match_start, strict_ratio = strict_count / match_count,
fuzzy = false,
} }
else else
return nil return nil
@@ -251,7 +281,8 @@ matcher.find_match_region = function(input, input_start_index, input_end_index,
input_match_end = input_index - 1, input_match_end = input_index - 1,
word_match_start = word_index, word_match_start = word_index,
word_match_end = word_index + word_offset - 1, word_match_end = word_index + word_offset - 1,
strict_match = strict_match_count == input_index - input_match_start, strict_ratio = strict_count / match_count,
fuzzy = false,
} }
end end

View File

@@ -23,12 +23,21 @@ describe('matcher', function()
assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list')) assert.is.truthy(matcher.match('my_', 'my_awesome_variable') > matcher.match('my_', 'completion_matching_strategy_list'))
assert.is.truthy(matcher.match('luacon', 'lua_context') > matcher.match('luacon', 'LuaContext')) assert.is.truthy(matcher.match('luacon', 'lua_context') > matcher.match('luacon', 'LuaContext'))
assert.is.truthy(matcher.match('call', 'calc') == 0) assert.is.truthy(matcher.match('call', 'calc') == 0)
assert.is.truthy(matcher.match('vi', 'void#') >= 1)
assert.is.truthy(matcher.match('vo', 'void#') >= 1)
assert.is.truthy(matcher.match('usela', 'useLayoutEffect') > matcher.match('usela', 'useDataLayer'))
assert.is.truthy(matcher.match('true', 'v:true', { 'true' }) == matcher.match('true', 'true'))
assert.is.truthy(matcher.match('g', 'get', { 'get' }) > matcher.match('g', 'dein#get', { 'dein#get' }))
end) end)
it('debug', function() it('debug', function()
matcher.debug = function(...) matcher.debug = function(...)
print(vim.inspect({ ... })) print(vim.inspect({ ... }))
end end
-- print('score', matcher.match('vsnipnextjump', 'vsnip-jump-next')) -- print(vim.inspect({
-- a = matcher.match('true', 'v:true', { 'true' }),
-- b = matcher.match('true', 'true'),
-- }))
end) end)
end) end)

View File

@@ -1,232 +0,0 @@
local debug = require('cmp.utils.debug')
local types = require('cmp.types')
local async = require('cmp.utils.async')
local float = require('cmp.float')
local config = require('cmp.config')
local autocmd = require('cmp.utils.autocmd')
---@class cmp.MenuOption
---@field on_select fun(e: cmp.Entry)
---@class cmp.Menu
---@field public float cmp.Float
---@field public cache cmp.Cache
---@field public offset number
---@field public on_select fun(e: cmp.Entry)
---@field public items vim.CompletedItem[]
---@field public entries cmp.Entry[]
---@field public deduped_entries cmp.Entry[]
---@field public selected_entry cmp.Entry|nil
---@field public context cmp.Context
---@field public resolve_dedup fun(callback: function)
local menu = {}
---Create menu
---@param opts cmp.MenuOption
---@return cmp.Menu
menu.new = function(opts)
local self = setmetatable({}, { __index = menu })
self.float = float.new()
self.resolve_dedup = async.dedup()
self.on_select = opts.on_select or function() end
self:reset()
autocmd.subscribe('CompleteChanged', function()
local e = self:get_selected_entry()
if e then
self:select(e)
else
self:unselect()
end
end)
return self
end
---Close menu
menu.close = function(self)
vim.schedule(function()
debug.log('menu.close', vim.fn.pumvisible())
if vim.fn.pumvisible() == 1 then
-- TODO: Is it safe to call...?
local line = vim.api.nvim_win_get_cursor(0)[1]
vim.fn.complete(#vim.api.nvim_buf_get_lines(0, line - 1, line, true)[1] + 1, {})
end
self:unselect()
end)
end
---Reset menu
menu.reset = function(self)
self.offset = nil
self.items = {}
self.entries = {}
self.deduped_entries = {}
self.context = nil
self.preselect = 0
self:close()
end
---Update menu
---@param ctx cmp.Context
---@param sources cmp.Source[]
---@return cmp.Menu
menu.update = function(self, ctx, sources)
local entries = {}
-- check the source triggered by character
local has_triggered_by_symbol_source = false
for _, s in ipairs(sources) do
if #s:get_entries(ctx) > 0 then
if s.is_triggered_by_symbol then
has_triggered_by_symbol_source = true
break
end
end
end
-- create filtered entries.
local offset = ctx.cursor.col
for i, s in ipairs(sources) do
if s.offset <= offset then
if not has_triggered_by_symbol_source or s.is_triggered_by_symbol then
-- source order priority bonus.
local priority = s:get_config().priority or ((#sources - (i - 1)) * config.get().sorting.priority_weight)
for _, e in ipairs(s:get_entries(ctx)) do
e.score = e.score + priority
table.insert(entries, e)
offset = math.min(offset, e:get_offset())
end
end
end
end
-- sort.
table.sort(entries, function(e1, e2)
for _, fn in ipairs(config.get().sorting.comparators) do
local diff = fn(e1, e2)
if diff ~= nil then
return diff
end
end
end)
-- create vim items.
local items = {}
local deduped_entries = {}
local deduped_words = {}
local preselect = 0
for _, e in ipairs(entries) do
local item = e:get_vim_item(offset)
if item.dup == 1 or not deduped_words[item.word] then
deduped_words[item.word] = true
-- We have done deduplication already, no need to force Vim to repeat it.
item.dup = 1
table.insert(items, item)
table.insert(deduped_entries, e)
if preselect == 0 and e.completion_item.preselect and config.get().preselect ~= types.cmp.PreselectMode.None then
preselect = #deduped_entries
end
end
end
-- save recent pum state.
self.offset = offset
self.items = items
self.entries = entries
self.deduped_entries = deduped_entries
self.preselect = preselect
self.context = ctx
self:show()
end
---Restore previous menu
---@param ctx cmp.Context
menu.restore = function(self, ctx)
if not ctx.pumvisible then
if #self.items > 0 then
if self.offset <= ctx.cursor.col then
debug.log('menu/restore')
self:show()
end
end
end
end
---Show completion item
menu.show = function(self)
if #self.deduped_entries == 0 then
self:close()
return
end
debug.log('menu.show', #self.deduped_entries)
local completeopt = vim.o.completeopt
if self.preselect == 1 then
vim.opt.completeopt = { 'menuone', 'noinsert' }
else
vim.opt.completeopt = config.get().completion.completeopt
end
vim.fn.complete(self.offset, self.items)
if self.preselect > 1 then
vim.api.nvim_select_popupmenu_item(self.preselect - 1, false, false, {})
end
vim.opt.completeopt = completeopt
end
---Select current item
---@param e cmp.Entry
menu.select = function(self, e)
-- Documentation (always invoke to follow to the pum position)
e:resolve(self.resolve_dedup(vim.schedule_wrap(function()
if self:get_selected_entry() == e then
self.float:show(e)
end
end)))
self.on_select(e)
end
---Select current item
menu.unselect = function(self)
self.float:close()
end
---Geta current active entry
---@return cmp.Entry|nil
menu.get_active_entry = function(self)
if vim.fn.pumvisible() == 0 or not (vim.v.completed_item or {}).user_data then
return nil
end
return self:get_selected_entry()
end
---Get current selected entry
---@return cmp.Entry|nil
menu.get_selected_entry = function(self)
if not self:is_valid_mode() then
return nil
end
local selected = vim.fn.complete_info({ 'selected' }).selected
if selected == -1 then
return nil
end
return self.deduped_entries[math.max(selected, 0) + 1]
end
---Get first entry
---@param self cmp.Entry|nil
menu.get_first_entry = function(self)
if not self:is_valid_mode() then
return nil
end
return self.deduped_entries[1]
end
---Return the completion menu is visible or not.
---@return boolean
menu.is_valid_mode = function()
return vim.fn.complete_info({ 'mode' }).mode == 'eval'
end
return menu

View File

@@ -104,10 +104,12 @@ source.get_entries = function(self, ctx)
if not inputs[o] then if not inputs[o] then
inputs[o] = string.sub(ctx.cursor_before_line, o) inputs[o] = string.sub(ctx.cursor_before_line, o)
end end
e.score = matcher.match(inputs[o], e:get_filter_text(), { e:get_word() }) local score, matches = matcher.match(inputs[o], e:get_filter_text(), { e:get_word() })
e.score = score
e.exact = false e.exact = false
e.matches = matches
if e.score >= 1 then if e.score >= 1 then
e.exact = vim.tbl_contains({ e:get_filter_text(), e:get_word() }, inputs[o]) e.exact = e:get_filter_text() == inputs[o] or e:get_word() == inputs[o]
table.insert(entries, e) table.insert(entries, e)
end end
end end
@@ -115,7 +117,7 @@ source.get_entries = function(self, ctx)
return entries return entries
end) end)
local max_item_count = self:get_config().max_item_count local max_item_count = self:get_config().max_item_count or 200
local limited_entries = {} local limited_entries = {}
for _, e in ipairs(entries) do for _, e in ipairs(entries) do
table.insert(limited_entries, e) table.insert(limited_entries, e)
@@ -306,6 +308,7 @@ source.complete = function(self, ctx, callback)
option = self:get_config().opts, option = self:get_config().opts,
completion_context = completion_context, completion_context = completion_context,
}, },
async.timeout(
self.complete_dedup(vim.schedule_wrap(function(response) self.complete_dedup(vim.schedule_wrap(function(response)
if #((response or {}).items or response or {}) > 0 then if #((response or {}).items or response or {}) > 0 then
debug.log(self:get_debug_name(), 'retrieve', #(response.items or response)) debug.log(self:get_debug_name(), 'retrieve', #(response.items or response))
@@ -336,7 +339,9 @@ source.complete = function(self, ctx, callback)
self.status = prev_status self.status = prev_status
end end
callback() callback()
end)) end)),
2000
)
) )
return true return true
end end

View File

@@ -5,6 +5,11 @@ cmp.ConfirmBehavior = {}
cmp.ConfirmBehavior.Insert = 'insert' cmp.ConfirmBehavior.Insert = 'insert'
cmp.ConfirmBehavior.Replace = 'replace' cmp.ConfirmBehavior.Replace = 'replace'
---@alias cmp.SelectBehavior "'insert'" | "'select'"
cmp.SelectBehavior = {}
cmp.SelectBehavior.Insert = 'insert'
cmp.SelectBehavior.Select = 'select'
---@alias cmp.ContextReason "'auto'" | "'manual'" | "'none'" ---@alias cmp.ContextReason "'auto'" | "'manual'" | "'none'"
cmp.ContextReason = {} cmp.ContextReason = {}
cmp.ContextReason.Auto = 'auto' cmp.ContextReason.Auto = 'auto'
@@ -28,6 +33,9 @@ cmp.PreselectMode.None = 'none'
---@class cmp.ConfirmOption ---@class cmp.ConfirmOption
---@field public behavior cmp.ConfirmBehavior ---@field public behavior cmp.ConfirmBehavior
---@class cmp.SelectOption
---@field public behavior cmp.SelectBehavior
---@class cmp.SnippetExpansionParams ---@class cmp.SnippetExpansionParams
---@field public body string ---@field public body string
---@field public insert_text_mode number ---@field public insert_text_mode number
@@ -82,7 +90,6 @@ cmp.PreselectMode.None = 'none'
---@field public comparators function[] ---@field public comparators function[]
---@class cmp.FormattingConfig ---@class cmp.FormattingConfig
---@field public deprecated boolean
---@field public format fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem ---@field public format fun(entry: cmp.Entry, vim_item: vim.CompletedItem): vim.CompletedItem
---@class cmp.SnippetConfig ---@class cmp.SnippetConfig
@@ -92,6 +99,7 @@ cmp.PreselectMode.None = 'none'
---@field on_confirm_done function(e: cmp.Entry) ---@field on_confirm_done function(e: cmp.Entry)
---@class cmp.ExperimentalConfig ---@class cmp.ExperimentalConfig
---@field public native_menu boolean
---@field public ghost_text cmp.GhostTextConfig|"false" ---@field public ghost_text cmp.GhostTextConfig|"false"
---@class cmp.GhostTextConfig ---@class cmp.GhostTextConfig

View File

@@ -26,19 +26,39 @@ async.throttle = function(fn, timeout)
end end
timer:stop() timer:stop()
local delta = math.max(0, self.timeout - (vim.loop.now() - time)) local delta = math.max(1, self.timeout - (vim.loop.now() - time))
timer:start( timer:start(delta, 0, function()
delta,
0,
vim.schedule_wrap(function()
time = nil time = nil
fn(unpack(args)) fn(unpack(args))
end) end)
)
end, end,
}) })
end end
---Timeout callback function
---@param fn function
---@param timeout number
---@return function
async.timeout = function(fn, timeout)
local timer
local done = false
local callback = function(...)
if not done then
done = true
timer:stop()
timer:close()
fn(...)
end
end
timer = vim.loop.new_timer()
timer:start(timeout, 0, function()
callback()
end)
return callback
end
---@alias cmp.AsyncDedup fun(callback: function): function
---Create deduplicated callback ---Create deduplicated callback
---@return function ---@return function
async.dedup = function() async.dedup = function()

33
lua/cmp/utils/binary.lua Normal file
View File

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

View File

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

View File

@@ -14,7 +14,7 @@ end
cache.get = function(self, key) cache.get = function(self, key)
key = self:key(key) key = self:key(key)
if self.entries[key] ~= nil then if self.entries[key] ~= nil then
return unpack(self.entries[key]) return self.entries[key]
end end
return nil return nil
end end
@@ -22,9 +22,9 @@ end
---Set cache value explicitly ---Set cache value explicitly
---@param key string ---@param key string
---@vararg any ---@vararg any
cache.set = function(self, key, ...) cache.set = function(self, key, value)
key = self:key(key) key = self:key(key)
self.entries[key] = { ... } self.entries[key] = value
end end
---Ensure value by callback ---Ensure value by callback
@@ -33,9 +33,11 @@ end
cache.ensure = function(self, key, callback) cache.ensure = function(self, key, callback)
local value = self:get(key) local value = self:get(key)
if value == nil then if value == nil then
self:set(key, callback()) local v = callback()
self:set(key, v)
return v
end end
return self:get(key) return value
end end
---Clear all cache entries ---Clear all cache entries

51
lua/cmp/utils/event.lua Normal file
View File

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

View File

@@ -0,0 +1,46 @@
local highlight = {}
highlight.keys = {
'gui',
'guifg',
'guibg',
'cterm',
'ctermfg',
'ctermbg',
}
highlight.inherit = function(name, source, override)
local cmd = ('highlight! default %s'):format(name)
for _, key in ipairs(highlight.keys) do
if override[key] then
cmd = cmd .. (' %s=%s'):format(key, override[key])
else
local v = highlight.get(source, key)
v = v == '' and 'NONE' or v
cmd = cmd .. (' %s=%s'):format(key, v)
end
end
vim.cmd(cmd)
end
highlight.get = function(source, key)
if key == 'gui' or key == 'cterm' then
local ui = {}
for _, k in ipairs({ 'bold', 'italic', 'reverse', 'inverse', 'standout', 'underline', 'undercurl', 'strikethrough' }) do
if vim.fn.synIDattr(vim.fn.hlID(source), k, key) == 1 then
table.insert(ui, k)
end
end
return table.concat(ui, ',')
elseif key == 'guifg' then
return vim.fn.synIDattr(vim.fn.hlID(source), 'fg#', 'gui')
elseif key == 'guibg' then
return vim.fn.synIDattr(vim.fn.hlID(source), 'bg#', 'gui')
elseif key == 'ctermfg' then
return vim.fn.synIDattr(vim.fn.hlID(source), 'fg', 'term')
elseif key == 'ctermbg' then
return vim.fn.synIDattr(vim.fn.hlID(source), 'bg', 'term')
end
end
return highlight

View File

@@ -149,8 +149,12 @@ misc.set(_G, { 'cmp', 'utils', 'keymap', 'listen', 'run' }, function(mode, keys)
local bufnr = vim.api.nvim_get_current_buf() local bufnr = vim.api.nvim_get_current_buf()
local fallback = keymap.listen.cache:get({ mode, bufnr, keys }).fallback local fallback = keymap.listen.cache:get({ mode, bufnr, keys }).fallback
local callback = keymap.listen.cache:get({ mode, bufnr, keys }).callback local callback = keymap.listen.cache:get({ mode, bufnr, keys }).callback
local done = false
callback(keys, function() callback(keys, function()
if not done then
done = true
keymap.feedkeys(keymap.t(fallback), 'i') keymap.feedkeys(keymap.t(fallback), 'i')
end
end) end)
return keymap.t('<Ignore>') return keymap.t('<Ignore>')
end) end)

View File

@@ -35,7 +35,7 @@ end
---@return T ---@return T
misc.merge = function(v1, v2) misc.merge = function(v1, v2)
local merge1 = type(v1) == 'table' and (not vim.tbl_islist(v1) or vim.tbl_isempty(v1)) local merge1 = type(v1) == 'table' and (not vim.tbl_islist(v1) or vim.tbl_isempty(v1))
local merge2 = type(v2) == 'table' and (not vim.tbl_islist(v1) or vim.tbl_isempty(v1)) local merge2 = type(v2) == 'table' and (not vim.tbl_islist(v2) or vim.tbl_isempty(v2))
if merge1 and merge2 then if merge1 and merge2 then
local new_tbl = {} local new_tbl = {}
for k, v in pairs(v2) do for k, v in pairs(v2) do

View File

@@ -15,6 +15,8 @@ INVALID_CHARS[string.byte('\t')] = true
INVALID_CHARS[string.byte('\n')] = true INVALID_CHARS[string.byte('\n')] = true
INVALID_CHARS[string.byte('\r')] = true INVALID_CHARS[string.byte('\r')] = true
local NR_BYTE = string.byte('\n')
local PAIR_CHARS = {} local PAIR_CHARS = {}
PAIR_CHARS[string.byte('[')] = string.byte(']') PAIR_CHARS[string.byte('[')] = string.byte(']')
PAIR_CHARS[string.byte('(')] = string.byte(')') PAIR_CHARS[string.byte('(')] = string.byte(')')
@@ -72,24 +74,6 @@ str.strikethrough = function(text)
return buffer return buffer
end end
---omit
---@param text string
---@param width number
---@return string
str.omit = function(text, width)
if width == 0 then
return ''
end
if not text then
text = ''
end
if #text > width then
return string.sub(text, 1, width + 1) .. '...'
end
return text
end
---trim ---trim
---@param text string ---@param text string
---@return string ---@return string
@@ -136,21 +120,12 @@ str.get_word = function(text, stop_char)
return text return text
end end
---Get character length.
---@param text string
---@param s number
---@param e number
---@return number
str.chars = function(text, s, e)
return vim.fn.strchars(string.sub(text, s, e))
end
---Oneline ---Oneline
---@param text string ---@param text string
---@return string ---@return string
str.oneline = function(text) str.oneline = function(text)
for i = 1, #text do for i = 1, #text do
if string.byte(text, i) == string.byte('\n', 1) then if string.byte(text, i) == NR_BYTE then
return string.sub(text, 1, i - 1) return string.sub(text, 1, i - 1)
end end
end end

256
lua/cmp/utils/window.lua Normal file
View File

@@ -0,0 +1,256 @@
local cache = require('cmp.utils.cache')
local misc = require('cmp.utils.misc')
---@class cmp.WindowStyle
---@field public relative string
---@field public row number
---@field public col number
---@field public width number
---@field public height number
---@field public zindex number|nil
---@class cmp.Window
---@field public buf number
---@field public win number|nil
---@field public sbuf1 number
---@field public swin1 number|nil
---@field public sbuf2 number
---@field public swin2 number|nil
---@field public style cmp.WindowStyle
---@field public opt table<string, any>
---@field public cache cmp.Cache
local window = {}
---new
---@return cmp.Window
window.new = function()
local self = setmetatable({}, { __index = window })
self.buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(self.buf, 'undolevels', -1)
vim.api.nvim_buf_set_option(self.buf, 'buftype', 'nofile')
self.win = nil
self.style = {}
self.sbuf1 = vim.api.nvim_create_buf(false, true)
self.swin1 = nil
self.sbuf2 = vim.api.nvim_create_buf(false, true)
self.swin2 = nil
self.cache = cache.new()
self.opt = {}
self.id = 0
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 value == nil then
return self.opt[key]
end
self.opt[key] = value
if self:visible() then
vim.api.nvim_win_set_option(self.win, key, value)
end
end
---Set style.
---@param style cmp.WindowStyle
window.set_style = function(self, style)
if vim.o.columns and vim.o.columns <= style.col + style.width then
style.width = vim.o.columns - style.col - 1
end
if vim.o.lines and vim.o.lines <= style.row + style.height then
style.height = vim.o.lines - style.row - 1
end
self.style = style
self.style.zindex = self.style.zindex or 1
end
---Open window
---@param style cmp.WindowStyle
window.open = function(self, style)
self.id = self.id + 1
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.buf, false, s)
for k, v in pairs(self.opt) do
vim.api.nvim_win_set_option(self.win, k, v)
end
end
self:update()
end
---Update
window.update = function(self)
if self:has_scrollbar() then
local total = self:get_content_height()
local info = self:info()
local bar_height = math.ceil(info.height * (info.height / total))
local bar_offset = math.min(info.height - bar_height, math.floor(info.height * (vim.fn.getwininfo(self.win)[1].topline / total)))
local style1 = {}
style1.relative = 'editor'
style1.style = 'minimal'
style1.width = 1
style1.height = info.height
style1.row = info.row
style1.col = info.col + info.width - (info.has_scrollbar and 1 or 0)
style1.zindex = (self.style.zindex and (self.style.zindex + 1) or 1)
if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then
vim.api.nvim_win_set_config(self.swin1, style1)
else
style1.noautocmd = true
self.swin1 = vim.api.nvim_open_win(self.sbuf1, false, style1)
vim.api.nvim_win_set_option(self.swin1, 'winhighlight', 'Normal:PmenuSbar,NormalNC:PmenuSbar,NormalFloat:PmenuSbar')
end
local style2 = {}
style2.relative = 'editor'
style2.style = 'minimal'
style2.width = 1
style2.height = bar_height
style2.row = info.row + bar_offset
style2.col = info.col + info.width - (info.has_scrollbar and 1 or 0)
style2.zindex = (self.style.zindex and (self.style.zindex + 2) or 2)
if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then
vim.api.nvim_win_set_config(self.swin2, style2)
else
style2.noautocmd = true
self.swin2 = vim.api.nvim_open_win(self.sbuf2, false, style2)
vim.api.nvim_win_set_option(self.swin2, 'winhighlight', 'Normal:PmenuThumb,NormalNC:PmenuThumb,NormalFloat:PmenuThumb')
end
else
if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then
vim.api.nvim_win_close(self.swin1, false)
self.swin1 = nil
end
if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then
vim.api.nvim_win_close(self.swin2, false)
self.swin2 = nil
end
end
end
---Close window
window.close = function(self)
local id = self.id
vim.schedule(function()
if id == self.id then
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_close(self.win, true)
self.win = nil
end
if self.swin1 and vim.api.nvim_win_is_valid(self.swin1) then
vim.api.nvim_win_close(self.swin1, false)
self.swin1 = nil
end
if self.swin2 and vim.api.nvim_win_is_valid(self.swin2) then
vim.api.nvim_win_close(self.swin2, false)
self.swin2 = nil
end
end
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 the scrollbar will shown or not.
window.has_scrollbar = function(self)
return (self.style.height or 0) < self:get_content_height()
end
---Return win info.
window.info = function(self)
local border_width = self:get_border_width()
local has_scrollbar = self:has_scrollbar()
return {
row = self.style.row,
col = self.style.col,
width = self.style.width + border_width + (has_scrollbar and 1 or 0),
height = self.style.height,
border_width = border_width,
has_scrollbar = has_scrollbar,
}
end
---Get border width
---@return number
window.get_border_width = function(self)
local border = self.style.border
if type(border) == 'table' then
local new_border = {}
while #new_border < 8 do
for _, b in ipairs(border) do
table.insert(new_border, b)
end
end
border = new_border
end
local w = 0
if border then
if type(border) == 'string' then
if border == 'single' then
w = 2
elseif border == 'solid' then
w = 2
elseif border == 'double' then
w = 2
elseif border == 'rounded' then
w = 2
elseif border == 'shadow' then
w = 1
end
elseif type(border) == 'table' then
local b4 = type(border[4]) == 'table' and border[4][1] or border[4]
if #b4 > 0 then
w = w + 1
end
local b8 = type(border[8]) == 'table' and border[8][1] or border[8]
if #b8 > 0 then
w = w + 1
end
end
end
return w
end
---Get scroll height.
---@return number
window.get_content_height = function(self)
if not self:option('wrap') then
return vim.api.nvim_buf_line_count(self.buf)
end
return self.cache:ensure({
'get_content_height',
self.style.width,
self.buf,
vim.api.nvim_buf_get_changedtick(self.buf),
}, function()
local height = 0
for _, text in ipairs(vim.api.nvim_buf_get_lines(self.buf, 0, -1, false)) do
height = height + math.ceil(math.max(1, vim.fn.strdisplaywidth(text)) / self.style.width)
end
return height
end)
end
return window

206
lua/cmp/view.lua Normal file
View File

@@ -0,0 +1,206 @@
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 native_entries_view = require('cmp.view.native_entries_view')
local ghost_text_view = require('cmp.view.ghost_text_view')
---@class cmp.View
---@field public event cmp.Event
---@field private resolve_dedup cmp.AsyncDedup
---@field private native_entries_view cmp.NativeEntriesView
---@field private custom_entries_view cmp.CustomEntriesView
---@field private change_dedup cmp.AsyncDedup
---@field private docs_view cmp.DocsView
---@field private ghost_text_view cmp.GhostTextView
local view = {}
---Create menu
view.new = function()
local self = setmetatable({}, { __index = view })
self.resolve_dedup = async.dedup()
self.custom_entries_view = custom_entries_view.new()
self.native_entries_view = native_entries_view.new()
self.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
---Redraw menu.
view.redraw = function(self)
self:_get_entries_view():redraw()
end
---Open menu
---@param ctx cmp.Context
---@param sources cmp.Source[]
view.open = function(self, ctx, sources)
local entries = {}
-- check the source triggered by character
local has_triggered_by_symbol_source = false
for _, s in ipairs(sources) do
if #s:get_entries(ctx) > 0 then
if s.is_triggered_by_symbol then
has_triggered_by_symbol_source = true
break
end
end
end
-- create filtered entries.
local offset = ctx.cursor.col
for i, s in ipairs(sources) do
if s.offset <= offset then
if not has_triggered_by_symbol_source or s.is_triggered_by_symbol then
-- source order priority bonus.
local priority = s:get_config().priority or ((#sources - (i - 1)) * config.get().sorting.priority_weight)
for _, e in ipairs(s:get_entries(ctx)) do
e.score = e.score + priority
table.insert(entries, e)
offset = math.min(offset, e:get_offset())
end
end
end
end
-- sort.
local comparetors = config.get().sorting.comparators
table.sort(entries, function(e1, e2)
for _, fn in ipairs(comparetors) do
local diff = fn(e1, e2)
if diff ~= nil then
return diff
end
end
end)
-- open
if #entries > 0 then
self:_get_entries_view():open(offset, entries)
else
self:close()
end
end
---Close menu
view.close = function(self)
self:_get_entries_view():close()
self.docs_view:close()
self.ghost_text_view:hide()
end
---Abort menu
view.abort = function(self)
self:_get_entries_view():abort()
self.docs_view:close()
self.ghost_text_view:hide()
end
---Return the view is visible or not.
---@return boolean
view.visible = function(self)
return self:_get_entries_view():visible()
end
---Scroll documentation window if possible.
---@param delta number
view.scroll_docs = function(self, delta)
self.docs_view:scroll(delta)
end
---Select prev menu item.
---@param option cmp.SelectOption
view.select_next_item = function(self, option)
self:_get_entries_view():select_next_item(option)
end
---Select prev menu item.
---@param option cmp.SelectOption
view.select_prev_item = function(self, option)
self:_get_entries_view():select_prev_item(option)
end
---Get 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)
local c = config.get()
self.native_entries_view.event:clear()
self.custom_entries_view.event:clear()
if c.experimental.native_menu then
self.native_entries_view.event:on('change', function()
self:on_entry_change()
end)
return self.native_entries_view
else
self.custom_entries_view.event:on('change', function()
self:on_entry_change()
end)
return self.custom_entries_view
end
end
---On entry change
view.on_entry_change = async.throttle(
vim.schedule_wrap(function(self)
if not self:visible() then
return
end
local e = self:get_selected_entry()
if e then
for _, c in ipairs(config.get().confirmation.get_commit_characters(e:get_commit_characters())) do
keymap.listen('i', c, function(...)
self.event:emit('keymap', ...)
end)
end
e:resolve(vim.schedule_wrap(self.resolve_dedup(function()
if not self:visible() then
return
end
self.docs_view:open(e, self:_get_entries_view():info())
end)))
else
self.docs_view:close()
end
e = e or self:get_first_entry()
if e then
self.ghost_text_view:show(e)
else
self.ghost_text_view:hide()
end
end),
20
)
return view

View File

@@ -0,0 +1,295 @@
local event = require('cmp.utils.event')
local autocmd = require('cmp.utils.autocmd')
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')
---@class cmp.CustomEntriesView
---@field private entries_win cmp.Window
---@field private offset number
---@field private entries cmp.Entry[]
---@field private column_bytes any
---@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('foldenable', false)
self.entries_win:option('wrap', false)
self.entries_win:option('scrolloff', 0)
self.entries_win:option('winhighlight', 'Normal:Pmenu,FloatBorder:Pmenu,CursorLine:PmenuSel,Search:None')
self.event = event.new()
self.offset = -1
self.entries = {}
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 then
return
end
for i = top, bot do
local e = self.entries[i + 1]
if e then
local v = e:get_view(self.offset)
local o = 1
for _, key in ipairs({ 'abbr', 'kind', 'menu' }) do
if self.column_bytes[key] > 0 then
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, o, {
end_line = i,
end_col = o + v[key].bytes,
hl_group = v[key].hl_group,
hl_mode = 'combine',
ephemeral = true,
})
o = o + self.column_bytes[key] + 1
end
end
for _, m in ipairs(e.matches or {}) do
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, m.word_match_start, {
end_line = i,
end_col = m.word_match_end + 1,
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.redraw = function()
-- noop
end
custom_entries_view.open = function(self, offset, entries)
self.offset = offset
self.entries = {}
self.column_bytes = { abbr = 0, kind = 0, menu = 0 }
self.column_width = { abbr = 0, kind = 0, menu = 0 }
local lines = {}
local dedup = {}
local preselect = 0
local i = 1
for _, e in ipairs(entries) do
local view = e:get_view(offset)
if view.dup == 1 or not dedup[e.completion_item.label] then
dedup[e.completion_item.label] = true
self.column_bytes.abbr = math.max(self.column_bytes.abbr, view.abbr.bytes)
self.column_bytes.kind = math.max(self.column_bytes.kind, view.kind.bytes)
self.column_bytes.menu = math.max(self.column_bytes.menu, view.menu.bytes)
self.column_width.abbr = math.max(self.column_width.abbr, view.abbr.width)
self.column_width.kind = math.max(self.column_width.kind, view.kind.width)
self.column_width.menu = math.max(self.column_width.menu, view.menu.width)
table.insert(self.entries, e)
table.insert(lines, ' ')
if preselect == 0 and e.completion_item.preselect then
preselect = i
end
i = i + 1
end
end
vim.api.nvim_buf_set_lines(self.entries_win.buf, 0, -1, false, lines)
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 cursor = vim.api.nvim_win_get_cursor(0)
local row = vim.fn.screenpos('.', cursor[1], cursor[2] + 1).row
local height = vim.api.nvim_get_option('pumheight')
height = height == 0 and #self.entries or height
height = math.min(height, #self.entries)
if (vim.o.lines - row) <= 8 and row - 8 > 0 then
height = math.min(height, row - 1)
row = row - height - 1
else
height = math.min(height, vim.o.lines - row)
end
if width < 1 or height < 1 then
return
end
local delta = cursor[2] + 1 - self.offset
self.entries_win:option('cursorline', false)
self.entries_win:open({
relative = 'editor',
style = 'minimal',
row = row,
col = vim.fn.screencol() - 1 - delta - 1,
width = width,
height = height,
zindex = 1001,
})
vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 1 })
if preselect > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
self:preselect(preselect)
elseif string.match(config.get().completion.completeopt, 'noinsert') then
self:preselect(1)
else
self:draw()
end
self.event:emit('change')
end
custom_entries_view.close = function(self)
self.offset = -1
self.entries = {}
self.entries_win:close()
end
custom_entries_view.abort = function(self)
if self.prefix then
self:_insert(self.prefix)
end
self:close()
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 = {}
for i = topline, botline - 1 do
local view = self.entries[i + 1]:get_view(self.offset)
local text = {}
table.insert(text, ' ')
table.insert(text, view.abbr.text)
table.insert(text, string.rep(' ', 1 + self.column_width.abbr - view.abbr.width))
table.insert(text, view.kind.text)
table.insert(text, string.rep(' ', 1 + self.column_width.kind - view.kind.width))
table.insert(text, view.menu.text)
table.insert(text, string.rep(' ', 1 + self.column_width.menu - view.menu.width))
table.insert(text, ' ')
table.insert(texts, table.concat(text, ''))
end
vim.api.nvim_buf_set_lines(self.entries_win.buf, topline, botline, false, texts)
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.preselect = function(self, index)
if self:visible() then
if index <= #self.entries then
self.entries_win:option('cursorline', true)
vim.api.nvim_win_set_cursor(self.entries_win.win, { index, 1 })
self.entries_win:update()
self:draw()
end
end
end
custom_entries_view.select_next_item = function(self, option)
if self.entries_win:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1] + 1
if not self.entries_win:option('cursorline') then
cursor = 1
elseif #self.entries < cursor then
cursor = 0
end
self:_select(cursor, option)
end
end
custom_entries_view.select_prev_item = function(self, option)
if self.entries_win:visible() then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)[1] - 1
if not self.entries_win:option('cursorline') then
cursor = #self.entries
end
self:_select(cursor, option)
end
end
custom_entries_view.get_first_entry = function(self)
if self.entries_win:visible() then
return self.entries[1]
end
end
custom_entries_view.get_selected_entry = function(self)
if self.entries_win: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.entries_win:visible() and self.entries_win:option('cursorline') then
local cursor = vim.api.nvim_win_get_cursor(self.entries_win.win)
if cursor[2] == 0 then
return self:get_selected_entry()
end
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 then
if vim.api.nvim_win_get_cursor(self.entries_win.win)[2] == 1 then
self.prefix = string.sub(vim.api.nvim_get_current_line(), self.offset, vim.api.nvim_win_get_cursor(0)[2]) or ''
end
end
self.entries_win:option('cursorline', cursor > 0)
vim.api.nvim_win_set_cursor(self.entries_win.win, { math.max(cursor, 1), is_insert and 0 or 1 })
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 = function(self, word)
vim.api.nvim_buf_set_keymap(0, 'i', '<Plug>(cmp.view.custom_entries_view._insert.remove)', ('v:lua.cmp.view.custom_entries_view._insert.remove(%s)'):format(self.offset), {
expr = true,
noremap = true,
})
keymap.feedkeys(keymap.t('<Plug>(cmp.view.custom_entries_view._insert.remove)'), 't')
keymap.feedkeys(word, 'nt')
end
misc.set(_G, { 'cmp', 'view', 'custom_entries_view', '_insert', 'remove' }, function(offset)
local cursor = vim.api.nvim_win_get_cursor(0)
local length = vim.str_utfindex(string.sub(vim.api.nvim_get_current_line(), offset, cursor[2]))
return keymap.t(string.rep('<C-g>U<Left><Del>', length))
end)
return custom_entries_view

126
lua/cmp/view/docs_view.lua Normal file
View File

@@ -0,0 +1,126 @@
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('scrolloff', 0)
self.window:option('wrap', true)
return self
end
---Open documentation window
---@param e cmp.Entry
---@param view cmp.WindowStyle
docs_view.open = function(self, e, view)
local documentation = config.get().documentation
if not documentation then
return
end
if not e or not view then
return self:close()
end
local right_space = vim.o.columns - (view.col + view.width) - 2
local left_space = view.col - 2
local maxwidth = math.min(documentation.maxwidth, math.max(left_space, right_space))
-- update buffer content if needed.
if not self.entry or e.id ~= self.entry.id then
local documents = e:get_documentation()
if #documents == 0 then
return self:close()
end
self.entry = e
vim.api.nvim_buf_call(self.window.buf, function()
vim.cmd([[syntax clear]])
end)
vim.lsp.util.stylize_markdown(self.window.buf, documents, {
max_width = maxwidth,
max_height = documentation.maxheight,
})
end
local width, height = vim.lsp.util._make_floating_popup_size(vim.api.nvim_buf_get_lines(self.window.buf, 0, -1, false), {
max_width = maxwidth,
max_height = documentation.maxheight,
})
if width <= 0 or height <= 0 then
return self:close()
end
local right_col = view.col + view.width
local left_col = view.col - width - 2
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
self.window:option('winhighlight', documentation.winhighlight)
self.window:set_style({
relative = 'editor',
style = 'minimal',
width = width,
height = height,
row = view.row,
col = col,
border = documentation.border,
})
if left and self.window:has_scrollbar() then
self.window.style.col = self.window.style.col - 1
end
self.window:open()
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.buf, function()
vim.api.nvim_command('normal! ' .. top .. 'zt')
self.window:update()
end)
end, 0)
end
end
docs_view.visible = function(self)
return self.window:visible()
end
return docs_view

View File

@@ -0,0 +1,72 @@
local config = require('cmp.config')
local str = require('cmp.utils.str')
local types = require('cmp.types')
---@class cmp.GhostTextView
local ghost_text_view = {}
ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT')
ghost_text_view.new = function()
local self = setmetatable({}, { __index = ghost_text_view })
self.win = nil
self.entry = nil
vim.api.nvim_set_decoration_provider(ghost_text_view.ns, {
on_win = function(_, win)
return win == self.win
end,
on_line = function(_)
local c = config.get().experimental.ghost_text
if not c then
return
end
if not self.entry then
return
end
local cursor = vim.api.nvim_win_get_cursor(0)
if string.sub(vim.api.nvim_get_current_line(), cursor[2] + 1) ~= '' then
return
end
local diff = 1 + cursor[2] - self.entry:get_offset()
local text = self.entry:get_insert_text()
if self.entry.completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
text = vim.lsp.util.parse_snippet(text)
end
text = string.sub(str.oneline(text), diff + 1)
if #text > 0 then
vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, cursor[1] - 1, cursor[2], {
right_gravity = false,
virt_text = { { text, c.hl_group or 'Comment' } },
virt_text_pos = 'overlay',
hl_mode = 'combine',
ephemeral = true,
})
end
end,
})
return self
end
---Show ghost text
---@param e cmp.Entry
ghost_text_view.show = function(self, e)
local changed = e ~= self.entry
self.win = vim.api.nvim_get_current_win()
self.entry = e
if changed then
vim.cmd([[redraw!]]) -- 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
vim.cmd([[redraw!]]) -- force invoke decoration provider.
end
end
return ghost_text_view

View File

@@ -0,0 +1,152 @@
local event = require('cmp.utils.event')
local autocmd = require('cmp.utils.autocmd')
local keymap = require('cmp.utils.keymap')
local types = require('cmp.types')
local config = require('cmp.config')
---@class cmp.NativeEntriesView
---@field private offset number
---@field private items vim.CompletedItem
---@field private entries cmp.Entry[]
---@field private preselect number
---@field public event cmp.Event
local native_entries_view = {}
native_entries_view.new = function()
local self = setmetatable({}, { __index = native_entries_view })
self.event = event.new()
self.offset = -1
self.items = {}
self.entries = {}
self.preselect = 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.redraw = function(self)
if #self.entries > 0 and self.offset <= vim.api.nvim_win_get_cursor(0)[2] then
local completeopt = vim.o.completeopt
vim.o.completeopt = self.preselect == 1 and 'menu,menuone,noinsert' or config.get().completion.completeopt
vim.fn.complete(self.offset, self.items)
vim.o.completeopt = completeopt
if self.preselect > 1 and config.get().preselect == types.cmp.PreselectMode.Item then
self:preselect(self.preselect)
end
end
end
native_entries_view.open = function(self, offset, entries)
local dedup = {}
local items = {}
local dedup_entries = {}
local preselect = 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 == 0 and e.completion_item.preselect then
preselect = #dedup_entries
end
end
end
self.offset = offset
self.items = items
self.entries = dedup_entries
self.preselect = preselect
self:redraw()
end
native_entries_view.close = function(self)
if string.sub(vim.api.nvim_get_mode().mode, 1, 1) == 'i' then
vim.fn.complete(1, {})
end
self.offset = -1
self.entries = {}
self.items = {}
self.preselect = 0
end
native_entries_view.abort = function(_)
if string.sub(vim.api.nvim_get_mode().mode, 1, 1) == 'i' 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),
height = info.height,
row = info.row,
col = info.col,
}
end
end
native_entries_view.preselect = function(self, index)
if self:visible() then
if index <= #self.entries then
vim.api.nvim_select_popupmenu_item(index - 1, false, false, {})
end
end
end
native_entries_view.select_next_item = function(self, option)
if self:visible() then
if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then
keymap.feedkeys(keymap.t('<C-n>'), 'n')
else
keymap.feedkeys(keymap.t('<Down>'), 'n')
end
end
end
native_entries_view.select_prev_item = function(self, option)
if self:visible() then
if (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert then
keymap.feedkeys(keymap.t('<C-p>'), 'n')
else
keymap.feedkeys(keymap.t('<Up>'), 'n')
end
end
end
native_entries_view.get_first_entry = function(self)
if self:visible() then
return self.entries[1]
end
end
native_entries_view.get_selected_entry = function(self)
if self:visible() then
local idx = vim.fn.complete_info({ 'selected' }).selected
if idx > -1 then
return self.entries[math.max(0, idx) + 1]
end
end
end
native_entries_view.get_active_entry = function(self)
if self:visible() and (vim.v.completed_item or {}).word then
return self:get_selected_entry()
end
end
return native_entries_view

View File

@@ -5,7 +5,9 @@ local vim_source = {}
---@param id number ---@param id number
---@param args any[] ---@param args any[]
vim_source.on_callback = function(id, args) vim_source.on_callback = function(id, args)
return vim_source.to_callback.callbacks[id](unpack(args)) if vim_source.to_callback.callbacks[id] then
vim_source.to_callback.callbacks[id](unpack(args))
end
end end
---@param callback function ---@param callback function
@@ -34,7 +36,7 @@ vim_source.to_args = function(args)
return args return args
end end
---@param id number ---@param bridge_id number
---@param methods string[] ---@param methods string[]
vim_source.new = function(bridge_id, methods) vim_source.new = function(bridge_id, methods)
local self = {} local self = {}

View File

@@ -4,6 +4,7 @@ end
vim.g.loaded_cmp = true vim.g.loaded_cmp = true
local misc = require('cmp.utils.misc') local misc = require('cmp.utils.misc')
local highlight = require('cmp.utils.highlight')
-- TODO: https://github.com/neovim/neovim/pull/14661 -- TODO: https://github.com/neovim/neovim/pull/14661
vim.cmd [[ vim.cmd [[
@@ -11,12 +12,68 @@ vim.cmd [[
autocmd! autocmd!
autocmd InsertEnter * lua require'cmp.utils.autocmd'.emit('InsertEnter') autocmd InsertEnter * lua require'cmp.utils.autocmd'.emit('InsertEnter')
autocmd InsertLeave * lua require'cmp.utils.autocmd'.emit('InsertLeave') autocmd InsertLeave * lua require'cmp.utils.autocmd'.emit('InsertLeave')
autocmd TextChangedI,TextChangedP * lua require'cmp.utils.autocmd'.emit('TextChanged') autocmd CursorMovedI,TextChangedI,TextChangedP * lua require'cmp.utils.autocmd'.emit('TextChanged')
autocmd CompleteChanged * lua require'cmp.utils.autocmd'.emit('CompleteChanged') autocmd CompleteChanged * lua require'cmp.utils.autocmd'.emit('CompleteChanged')
autocmd CompleteDone * lua require'cmp.utils.autocmd'.emit('CompleteDone') autocmd CompleteDone * lua require'cmp.utils.autocmd'.emit('CompleteDone')
autocmd ColorScheme * call v:lua.cmp.plugin.colorscheme()
augroup END augroup END
]] ]]
misc.set(_G, { 'cmp', 'plugin', 'colorscheme' }, function()
highlight.inherit('CmpItemAbbrDefault', 'Comment', {
guibg = 'NONE',
ctermbg = 'NONE',
})
highlight.inherit('CmpItemAbbrDeprecatedDefault', 'Comment', {
gui = 'NONE',
guibg = 'NONE',
ctermbg = 'NONE',
})
highlight.inherit('CmpItemAbbrMatchDefault', 'Normal', {
gui = 'bold',
guibg = 'NONE',
ctermbg = 'NONE',
})
highlight.inherit('CmpItemAbbrMatchFuzzyDefault', 'Normal', {
gui = 'NONE',
guibg = 'NONE',
ctermbg = 'NONE',
})
highlight.inherit('CmpItemKindDefault', 'Special', {
guibg = 'NONE',
ctermbg = 'NONE',
})
highlight.inherit('CmpItemMenuDefault', 'NonText', {
guibg = 'NONE',
ctermbg = 'NONE',
})
end)
_G.cmp.plugin.colorscheme()
if vim.fn.hlexists('CmpItemAbbr') ~= 1 then
vim.cmd [[highlight! default link CmpItemAbbr CmpItemAbbrDefault]]
end
if vim.fn.hlexists('CmpItemAbbrDeprecated') ~= 1 then
vim.cmd [[highlight! default link CmpItemAbbrDeprecated CmpItemAbbrDeprecatedDefault]]
end
if vim.fn.hlexists('CmpItemAbbrMatch') ~= 1 then
vim.cmd [[highlight! default link CmpItemAbbrMatch CmpItemAbbrMatchDefault]]
end
if vim.fn.hlexists('CmpItemAbbrMatchFuzzy') ~= 1 then
vim.cmd [[highlight! default link CmpItemAbbrMatchFuzzy CmpItemAbbrMatchFuzzyDefault]]
end
if vim.fn.hlexists('CmpItemKind') ~= 1 then
vim.cmd [[highlight! default link CmpItemKind CmpItemKindDefault]]
end
if vim.fn.hlexists('CmpItemMenu') ~= 1 then
vim.cmd [[highlight! default link CmpItemMenu CmpItemMenuDefault]]
end
vim.cmd [[command! CmpStatus lua require('cmp').status()]] vim.cmd [[command! CmpStatus lua require('cmp').status()]]
vim.cmd [[doautocmd <nomodeline> User cmp#ready]] vim.cmd [[doautocmd <nomodeline> User cmp#ready]]