* perf: avoid creating closure in cache.ensure and drop some cached getters This mainly addresses the perf issue on large amount of calls to `entry.new`. Previously every `cache.ensure` calls in the code path of it creates an anonymous function, and it seems that luajit just could not inline it. Function creation is not expensive in luajit, but that overhead is noticeable if every `cache.ensure` call creates a function. The first improvemnt is to solidate the cache callback and attach it to the metatable of `entry`. This ensures that every created entry instance share the same cache callback and no new functions will be frequently created, reduces the ram usage and GC overhead. To improve it further, some frequently accessed fields of entry like `completion_item` and `offset` is refactored to use simple table access instead of getter pattern. The current cached getter is implemented using `cache.ensure`, which introduces two more levels of function calls on each access: `cache.key` and `cache.get`. The overhead is okay if but noticeable if entries amount is quite large: you need to call 4 functions on a simple `completion_item` field access for each item. All of the changes done in the commit is just constant time optimization. But the different is huge if tested with LS providing large amount of entries like tailwindcss. * perf: delay fuzzy match on displayed vim item `entry.get_vim_item` is a very heavy call, especially when user do complex stuff on item formatting. Delay its call to window displaying to let `performance.max_view_entries` applied to it. * remove unneeded fill_defaults * update gha --------- Co-authored-by: hrsh7th <629908+hrsh7th@users.noreply.github.com>
489 lines
15 KiB
Lua
489 lines
15 KiB
Lua
local event = require('cmp.utils.event')
|
|
local autocmd = require('cmp.utils.autocmd')
|
|
local feedkeys = require('cmp.utils.feedkeys')
|
|
local window = require('cmp.utils.window')
|
|
local config = require('cmp.config')
|
|
local types = require('cmp.types')
|
|
local keymap = require('cmp.utils.keymap')
|
|
local misc = require('cmp.utils.misc')
|
|
local api = require('cmp.utils.api')
|
|
|
|
local DEFAULT_HEIGHT = 10 -- @see https://github.com/vim/vim/blob/master/src/popupmenu.c#L45
|
|
|
|
---@class cmp.CustomEntriesView
|
|
---@field private entries_win cmp.Window
|
|
---@field private offset integer
|
|
---@field private active boolean
|
|
---@field private entries cmp.Entry[]
|
|
---@field private column_width any
|
|
---@field public event cmp.Event
|
|
local custom_entries_view = {}
|
|
|
|
custom_entries_view.ns = vim.api.nvim_create_namespace('cmp.view.custom_entries_view')
|
|
|
|
custom_entries_view.new = function()
|
|
local self = setmetatable({}, { __index = custom_entries_view })
|
|
|
|
self.entries_win = window.new()
|
|
self.entries_win:option('conceallevel', 2)
|
|
self.entries_win:option('concealcursor', 'n')
|
|
self.entries_win:option('cursorlineopt', 'line')
|
|
self.entries_win:option('foldenable', false)
|
|
self.entries_win:option('wrap', false)
|
|
-- This is done so that strdisplaywidth calculations for lines in the
|
|
-- custom_entries_view window exactly match with what is really displayed,
|
|
-- see comment in cmp.Entry.get_view. Setting tabstop to 1 makes all tabs be
|
|
-- always rendered one column wide, which removes the unpredictability coming
|
|
-- from variable width of the tab character.
|
|
self.entries_win:buffer_option('tabstop', 1)
|
|
self.entries_win:buffer_option('filetype', 'cmp_menu')
|
|
self.entries_win:buffer_option('buftype', 'nofile')
|
|
self.event = event.new()
|
|
self.offset = -1
|
|
self.active = false
|
|
self.entries = {}
|
|
self.bottom_up = false
|
|
|
|
autocmd.subscribe(
|
|
'CompleteChanged',
|
|
vim.schedule_wrap(function()
|
|
if self:visible() and vim.fn.pumvisible() == 1 then
|
|
self:close()
|
|
end
|
|
end)
|
|
)
|
|
|
|
vim.api.nvim_set_decoration_provider(custom_entries_view.ns, {
|
|
on_win = function(_, win, buf, top, bot)
|
|
if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then
|
|
return
|
|
end
|
|
|
|
local fields = config.get().formatting.fields
|
|
for i = top, bot do
|
|
local e = self.entries[i + 1]
|
|
if e then
|
|
local v = e:get_view(self.offset, buf)
|
|
local o = config.get().window.completion.side_padding
|
|
local a = 0
|
|
for _, field in ipairs(fields) do
|
|
if field == types.cmp.ItemField.Abbr then
|
|
a = o
|
|
end
|
|
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, o, {
|
|
end_line = i,
|
|
end_col = o + v[field].bytes,
|
|
hl_group = v[field].hl_group,
|
|
hl_mode = 'combine',
|
|
ephemeral = true,
|
|
})
|
|
o = o + v[field].bytes + (self.column_width[field] - v[field].width) + 1
|
|
end
|
|
|
|
for _, m in ipairs(e:get_view_matches(v.abbr.text) or {}) do
|
|
vim.api.nvim_buf_set_extmark(buf, custom_entries_view.ns, i, a + m.word_match_start - 1, {
|
|
end_line = i,
|
|
end_col = a + m.word_match_end,
|
|
hl_group = m.fuzzy and 'CmpItemAbbrMatchFuzzy' or 'CmpItemAbbrMatch',
|
|
hl_mode = 'combine',
|
|
ephemeral = true,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
})
|
|
|
|
return self
|
|
end
|
|
|
|
custom_entries_view.ready = function()
|
|
return vim.fn.pumvisible() == 0
|
|
end
|
|
|
|
custom_entries_view.on_change = function(self)
|
|
self.active = false
|
|
end
|
|
|
|
custom_entries_view.is_direction_top_down = function(self)
|
|
local c = config.get()
|
|
if (c.view and c.view.entries and c.view.entries.selection_order) == 'top_down' then
|
|
return true
|
|
elseif c.view.entries == nil or c.view.entries.selection_order == nil then
|
|
return true
|
|
else
|
|
return not self.bottom_up
|
|
end
|
|
end
|
|
|
|
custom_entries_view.open = function(self, offset, entries)
|
|
local completion = config.get().window.completion
|
|
assert(completion, 'config.get() must resolve window.completion with defaults')
|
|
|
|
self.offset = offset
|
|
self.entries = {}
|
|
self.column_width = { abbr = 0, kind = 0, menu = 0 }
|
|
|
|
local entries_buf = self.entries_win:get_buffer()
|
|
local lines = {}
|
|
local dedup = {}
|
|
local preselect_index = 0
|
|
for _, e in ipairs(entries) do
|
|
local view = e:get_view(offset, entries_buf)
|
|
if view.dup == 1 or not dedup[e.completion_item.label] then
|
|
dedup[e.completion_item.label] = true
|
|
self.column_width.abbr = math.max(self.column_width.abbr, view.abbr.width)
|
|
self.column_width.kind = math.max(self.column_width.kind, view.kind.width)
|
|
self.column_width.menu = math.max(self.column_width.menu, view.menu.width)
|
|
table.insert(self.entries, e)
|
|
table.insert(lines, ' ')
|
|
if preselect_index == 0 and e.completion_item.preselect then
|
|
preselect_index = #self.entries
|
|
end
|
|
end
|
|
end
|
|
if vim.bo[entries_buf].modifiable == false then
|
|
vim.bo[entries_buf].modifiable = true
|
|
vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines)
|
|
vim.bo[entries_buf].modifiable = false
|
|
else
|
|
vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines)
|
|
end
|
|
vim.api.nvim_buf_set_option(entries_buf, 'modified', false)
|
|
|
|
local width = 0
|
|
width = width + 1
|
|
width = width + self.column_width.abbr + (self.column_width.kind > 0 and 1 or 0)
|
|
width = width + self.column_width.kind + (self.column_width.menu > 0 and 1 or 0)
|
|
width = width + self.column_width.menu + 1
|
|
|
|
local height = vim.api.nvim_get_option_value('pumheight', {})
|
|
height = height ~= 0 and height or #self.entries
|
|
height = math.min(height, #self.entries)
|
|
|
|
local delta = 0
|
|
if not config.get().view.entries.follow_cursor then
|
|
local cursor_before_line = api.get_cursor_before_line()
|
|
delta = vim.fn.strdisplaywidth(cursor_before_line:sub(self.offset))
|
|
end
|
|
local pos = api.get_screen_cursor()
|
|
local row, col = pos[1], pos[2] - delta - 1
|
|
|
|
local border_info = window.get_border_info({ style = completion })
|
|
local border_offset_row = border_info.top + border_info.bottom
|
|
local border_offset_col = border_info.left + border_info.right
|
|
if math.floor(vim.o.lines * 0.5) <= row + border_offset_row and vim.o.lines - row - border_offset_row <= math.min(DEFAULT_HEIGHT, height) then
|
|
height = math.min(height, row - 1)
|
|
row = row - height - border_offset_row - 1
|
|
if row < 0 then
|
|
height = height + row
|
|
end
|
|
end
|
|
if math.floor(vim.o.columns * 0.5) <= col + border_offset_col and vim.o.columns - col - border_offset_col <= width then
|
|
width = math.min(width, vim.o.columns - 1)
|
|
col = vim.o.columns - width - border_offset_col - 1
|
|
if col < 0 then
|
|
width = width + col
|
|
end
|
|
end
|
|
|
|
if pos[1] > row then
|
|
self.bottom_up = true
|
|
else
|
|
self.bottom_up = false
|
|
end
|
|
|
|
if not self:is_direction_top_down() then
|
|
local n = #self.entries
|
|
for i = 1, math.floor(n / 2) do
|
|
self.entries[i], self.entries[n - i + 1] = self.entries[n - i + 1], self.entries[i]
|
|
end
|
|
if preselect_index ~= 0 then
|
|
preselect_index = #self.entries - preselect_index + 1
|
|
end
|
|
end
|
|
|
|
-- Apply window options (that might be changed) on the custom completion menu.
|
|
self.entries_win:option('winblend', completion.winblend)
|
|
self.entries_win:option('winhighlight', completion.winhighlight)
|
|
self.entries_win:option('scrolloff', completion.scrolloff)
|
|
self.entries_win:open({
|
|
relative = 'editor',
|
|
style = 'minimal',
|
|
row = math.max(0, row),
|
|
col = math.max(0, col + completion.col_offset),
|
|
width = width,
|
|
height = height,
|
|
border = completion.border,
|
|
zindex = completion.zindex or 1001,
|
|
})
|
|
|
|
-- Don't set the cursor if the entries_win:open function fails
|
|
-- due to the window's width or height being less than 1
|
|
if self.entries_win.win == nil then
|
|
return
|
|
end
|
|
|
|
-- Always set cursor when starting. It will be adjusted on the call to _select
|
|
vim.api.nvim_win_set_cursor(self.entries_win.win, { 1, 0 })
|
|
if preselect_index > 0 and config.get().preselect == types.cmp.PreselectMode.Item then
|
|
self:_select(preselect_index, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
|
elseif not string.match(config.get().completion.completeopt, 'noselect') then
|
|
if self:is_direction_top_down() then
|
|
self:_select(1, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
|
else
|
|
self:_select(#self.entries, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
|
end
|
|
else
|
|
if self:is_direction_top_down() then
|
|
self:_select(0, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
|
else
|
|
self:_select(#self.entries + 1, { behavior = types.cmp.SelectBehavior.Select, active = false })
|
|
end
|
|
end
|
|
end
|
|
|
|
custom_entries_view.close = function(self)
|
|
self.prefix = nil
|
|
self.offset = -1
|
|
self.active = false
|
|
self.entries = {}
|
|
self.entries_win:close()
|
|
self.bottom_up = false
|
|
end
|
|
|
|
custom_entries_view.abort = function(self)
|
|
if self.prefix then
|
|
self:_insert(self.prefix)
|
|
end
|
|
feedkeys.call('', 'n', function()
|
|
self:close()
|
|
end)
|
|
end
|
|
|
|
custom_entries_view.draw = function(self)
|
|
local info = vim.fn.getwininfo(self.entries_win.win)[1]
|
|
local topline = info.topline - 1
|
|
local botline = info.topline + info.height - 1
|
|
local texts = {}
|
|
local fields = config.get().formatting.fields
|
|
local entries_buf = self.entries_win:get_buffer()
|
|
for i = topline, botline - 1 do
|
|
local e = self.entries[i + 1]
|
|
if e then
|
|
local view = e:get_view(self.offset, entries_buf)
|
|
local text = {}
|
|
table.insert(text, string.rep(' ', config.get().window.completion.side_padding))
|
|
for _, field in ipairs(fields) do
|
|
table.insert(text, view[field].text)
|
|
table.insert(text, string.rep(' ', 1 + self.column_width[field] - view[field].width))
|
|
end
|
|
table.insert(text, string.rep(' ', config.get().window.completion.side_padding))
|
|
table.insert(texts, table.concat(text, ''))
|
|
end
|
|
end
|
|
if vim.bo[entries_buf].modifiable == false then
|
|
vim.bo[entries_buf].modifiable = true
|
|
vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts)
|
|
vim.bo[entries_buf].modifiable = false
|
|
else
|
|
vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts)
|
|
end
|
|
vim.api.nvim_buf_set_option(entries_buf, 'modified', false)
|
|
|
|
if api.is_cmdline_mode() then
|
|
vim.api.nvim_win_call(self.entries_win.win, function()
|
|
misc.redraw()
|
|
end)
|
|
end
|
|
end
|
|
|
|
custom_entries_view.visible = function(self)
|
|
return self.entries_win:visible()
|
|
end
|
|
|
|
custom_entries_view.info = function(self)
|
|
return self.entries_win:info()
|
|
end
|
|
|
|
custom_entries_view.get_selected_index = function(self)
|
|
if self:visible() and self.entries_win:option('cursorline') then
|
|
return vim.api.nvim_win_get_cursor(self.entries_win.win)[1]
|
|
end
|
|
end
|
|
|
|
custom_entries_view.select_next_item = function(self, option)
|
|
if self:visible() then
|
|
local cursor = self:get_selected_index()
|
|
local is_top_down = self:is_direction_top_down()
|
|
local last = #self.entries
|
|
|
|
if not self.entries_win:option('cursorline') then
|
|
cursor = (is_top_down and 1) or last
|
|
else
|
|
if is_top_down then
|
|
if cursor == last then
|
|
cursor = 0
|
|
else
|
|
cursor = cursor + option.count
|
|
if last < cursor then
|
|
cursor = last
|
|
end
|
|
end
|
|
else
|
|
if cursor == 0 then
|
|
cursor = last
|
|
else
|
|
cursor = cursor - option.count
|
|
if cursor < 0 then
|
|
cursor = 0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
self:_select(cursor, {
|
|
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
|
|
active = true,
|
|
})
|
|
end
|
|
end
|
|
|
|
custom_entries_view.select_prev_item = function(self, option)
|
|
if self:visible() then
|
|
local cursor = self:get_selected_index()
|
|
local is_top_down = self:is_direction_top_down()
|
|
local last = #self.entries
|
|
|
|
if not self.entries_win:option('cursorline') then
|
|
cursor = (is_top_down and last) or 1
|
|
else
|
|
if is_top_down then
|
|
if cursor == 1 then
|
|
cursor = 0
|
|
else
|
|
cursor = cursor - option.count
|
|
if cursor < 0 then
|
|
cursor = 1
|
|
end
|
|
end
|
|
else
|
|
if cursor == last then
|
|
cursor = 0
|
|
else
|
|
cursor = cursor + option.count
|
|
if last < cursor then
|
|
cursor = last
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
self:_select(cursor, {
|
|
behavior = option.behavior or types.cmp.SelectBehavior.Insert,
|
|
active = true,
|
|
})
|
|
end
|
|
end
|
|
|
|
custom_entries_view.get_offset = function(self)
|
|
if self:visible() then
|
|
return self.offset
|
|
end
|
|
return nil
|
|
end
|
|
|
|
custom_entries_view.get_entries = function(self)
|
|
if self:visible() then
|
|
return self.entries
|
|
end
|
|
return {}
|
|
end
|
|
|
|
custom_entries_view.get_first_entry = function(self)
|
|
if self:visible() then
|
|
return (self:is_direction_top_down() and self.entries[1]) or self.entries[#self.entries]
|
|
end
|
|
end
|
|
|
|
custom_entries_view.get_selected_entry = function(self)
|
|
if self:visible() and self.entries_win:option('cursorline') then
|
|
return self.entries[self:get_selected_index()]
|
|
end
|
|
end
|
|
|
|
custom_entries_view.get_active_entry = function(self)
|
|
if self:visible() and self.active then
|
|
return self:get_selected_entry()
|
|
end
|
|
end
|
|
|
|
custom_entries_view._select = function(self, cursor, option)
|
|
local is_insert = (option.behavior or types.cmp.SelectBehavior.Insert) == types.cmp.SelectBehavior.Insert
|
|
if is_insert and not self.active then
|
|
self.prefix = string.sub(api.get_current_line(), self.offset, api.get_cursor()[2]) or ''
|
|
end
|
|
self.active = (0 < cursor and cursor <= #self.entries and option.active == true)
|
|
|
|
self.entries_win:option('cursorline', cursor > 0 and cursor <= #self.entries)
|
|
vim.api.nvim_win_set_cursor(self.entries_win.win, {
|
|
math.max(math.min(cursor, #self.entries), 1),
|
|
0,
|
|
})
|
|
|
|
if is_insert then
|
|
self:_insert(self.entries[cursor] and self.entries[cursor]:get_vim_item(self.offset).word or self.prefix)
|
|
end
|
|
|
|
self.entries_win:update()
|
|
self:draw()
|
|
self.event:emit('change')
|
|
end
|
|
|
|
custom_entries_view._insert = setmetatable({
|
|
pending = false,
|
|
}, {
|
|
__call = function(this, self, word)
|
|
word = word or ''
|
|
if api.is_cmdline_mode() then
|
|
local cursor = api.get_cursor()
|
|
-- setcmdline() added in v0.8.0
|
|
if vim.fn.has('nvim-0.8') == 1 then
|
|
local current_line = api.get_current_line()
|
|
local before_line = current_line:sub(1, self.offset - 1)
|
|
local after_line = current_line:sub(cursor[2] + 1)
|
|
local pos = #before_line + #word + 1
|
|
vim.fn.setcmdline(before_line .. word .. after_line, pos)
|
|
vim.api.nvim_feedkeys(keymap.t('<Cmd>redraw<CR>'), 'ni', false)
|
|
else
|
|
vim.api.nvim_feedkeys(keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])) .. word, 'int', true)
|
|
end
|
|
else
|
|
if this.pending then
|
|
return
|
|
end
|
|
this.pending = true
|
|
|
|
local release = require('cmp').suspend()
|
|
feedkeys.call('', '', function()
|
|
local cursor = api.get_cursor()
|
|
local keys = {}
|
|
table.insert(keys, keymap.indentkeys())
|
|
table.insert(keys, keymap.backspace(string.sub(api.get_current_line(), self.offset, cursor[2])))
|
|
table.insert(keys, word)
|
|
table.insert(keys, keymap.indentkeys(vim.bo.indentkeys))
|
|
feedkeys.call(
|
|
table.concat(keys, ''),
|
|
'int',
|
|
vim.schedule_wrap(function()
|
|
this.pending = false
|
|
release()
|
|
end)
|
|
)
|
|
end)
|
|
end
|
|
end,
|
|
})
|
|
|
|
return custom_entries_view
|