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:
64
README.md
64
README.md
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
252
lua/cmp/core.lua
252
lua/cmp/core.lua
@@ -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
|
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
|
|
||||||
|
|
||||||
---@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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
232
lua/cmp/menu.lua
232
lua/cmp/menu.lua
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
33
lua/cmp/utils/binary.lua
Normal 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
|
||||||
28
lua/cmp/utils/binary_spec.lua
Normal file
28
lua/cmp/utils/binary_spec.lua
Normal 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)
|
||||||
@@ -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
51
lua/cmp/utils/event.lua
Normal 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
|
||||||
46
lua/cmp/utils/highlight.lua
Normal file
46
lua/cmp/utils/highlight.lua
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
256
lua/cmp/utils/window.lua
Normal 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
206
lua/cmp/view.lua
Normal 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
|
||||||
295
lua/cmp/view/custom_entries_view.lua
Normal file
295
lua/cmp/view/custom_entries_view.lua
Normal 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
126
lua/cmp/view/docs_view.lua
Normal 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
|
||||||
72
lua/cmp/view/ghost_text_view.lua
Normal file
72
lua/cmp/view/ghost_text_view.lua
Normal 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
|
||||||
152
lua/cmp/view/native_entries_view.lua
Normal file
152
lua/cmp/view/native_entries_view.lua
Normal 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
|
||||||
@@ -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 = {}
|
||||||
|
|||||||
@@ -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]]
|
||||||
|
|||||||
Reference in New Issue
Block a user