Files
nvim-cmp/lua/cmp/view/ghost_text_view.lua
yioneko 1a1d7ecb73 perf: improve for source providing huge list of items (#1980)
* 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>
2024-10-20 13:28:20 +09:00

133 lines
3.8 KiB
Lua

local config = require('cmp.config')
local misc = require('cmp.utils.misc')
local snippet = require('cmp.utils.snippet')
-- local str = require('cmp.utils.str')
local api = require('cmp.utils.api')
local types = require('cmp.types')
---@class cmp.GhostTextView
---@field win number|nil
---@field entry cmp.Entry|nil
local ghost_text_view = {}
ghost_text_view.ns = vim.api.nvim_create_namespace('cmp:GHOST_TEXT')
local has_inline = (function()
return (pcall(function()
local id = vim.api.nvim_buf_set_extmark(0, ghost_text_view.ns, 0, 0, {
virt_text = { { ' ', 'Comment' } },
virt_text_pos = 'inline',
hl_mode = 'combine',
ephemeral = false,
})
vim.api.nvim_buf_del_extmark(0, ghost_text_view.ns, id)
end))
end)()
ghost_text_view.new = function()
local self = setmetatable({}, { __index = ghost_text_view })
self.win = nil
self.entry = nil
self.extmark_id = nil
vim.api.nvim_set_decoration_provider(ghost_text_view.ns, {
on_win = function(_, win)
if self.extmark_id then
vim.api.nvim_buf_del_extmark(self.extmark_buf, ghost_text_view.ns, self.extmark_id)
self.extmark_id = nil
end
if win ~= self.win then
return false
end
local c = config.get().experimental.ghost_text
if not c then
return
end
if not self.entry then
return
end
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local line = vim.api.nvim_get_current_line()
if not has_inline then
if string.sub(line, col + 1) ~= '' then
return
end
end
local text = self.text_gen(self, line, col)
if #text > 0 then
local virt_lines = {}
for _, l in ipairs(vim.fn.split(text, '\n')) do
table.insert(virt_lines, { { l, type(c) == 'table' and c.hl_group or 'Comment' } })
end
local first_line = table.remove(virt_lines, 1)
self.extmark_buf = vim.api.nvim_get_current_buf()
self.extmark_id = vim.api.nvim_buf_set_extmark(self.extmark_buf, ghost_text_view.ns, row - 1, col, {
right_gravity = true,
virt_text = first_line,
virt_text_pos = has_inline and 'inline' or 'overlay',
virt_lines = virt_lines,
hl_mode = 'combine',
ephemeral = false,
})
end
end,
})
return self
end
---Generate the ghost text
--- This function calculates the bytes of the entry to display calculating the number
--- of character differences instead of just byte difference.
ghost_text_view.text_gen = function(self, line, cursor_col)
local word = self.entry:get_insert_text()
if self.entry:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
word = tostring(snippet.parse(word))
end
local word_clen = vim.str_utfindex(word)
local cword = string.sub(line, self.entry.offset, cursor_col)
local cword_clen = vim.str_utfindex(cword)
-- Number of characters from entry text (word) to be displayed as ghost thext
local nchars = word_clen - cword_clen
-- Missing characters to complete the entry text
local text
if nchars > 0 then
text = string.sub(word, vim.str_byteindex(word, word_clen - nchars) + 1)
else
text = ''
end
return text
end
---Show ghost text
---@param e cmp.Entry
ghost_text_view.show = function(self, e)
if not api.is_insert_mode() then
return
end
local c = config.get().experimental.ghost_text
if not c then
return
end
local changed = e ~= self.entry
self.win = vim.api.nvim_get_current_win()
self.entry = e
if changed then
misc.redraw(true) -- force invoke decoration provider.
end
end
ghost_text_view.hide = function(self)
if self.win and self.entry then
self.win = nil
self.entry = nil
misc.redraw(true) -- force invoke decoration provider.
end
end
return ghost_text_view