diff --git a/lua/cmp/entry.lua b/lua/cmp/entry.lua index 495e607..a4590cd 100644 --- a/lua/cmp/entry.lua +++ b/lua/cmp/entry.lua @@ -198,27 +198,34 @@ entry.is_deprecated = function(self) end ---Return view information. +---@param suggest_offset number +---@param entries_buf number The buffer this entry will be rendered into. ---@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) +entry.get_view = function(self, suggest_offset, entries_buf) local item = self:get_vim_item(suggest_offset) - return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0 }, function() + return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0, entries_buf }, 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' .. types.lsp.CompletionItemKind[self:get_kind()] - 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 + -- The result of vim.fn.strdisplaywidth depends on which buffer it was + -- called in because it reads the values of the option 'tabstop' when + -- rendering characters. + vim.api.nvim_buf_call(entries_buf, function() + view.abbr = {} + view.abbr.text = item.abbr or '' + view.abbr.bytes = #view.abbr.text + view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text) + view.abbr.hl_group = self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr' + view.kind = {} + view.kind.text = item.kind or '' + view.kind.bytes = #view.kind.text + view.kind.width = vim.fn.strdisplaywidth(view.kind.text) + view.kind.hl_group = 'CmpItemKind' .. types.lsp.CompletionItemKind[self:get_kind()] + view.menu = {} + view.menu.text = item.menu or '' + view.menu.bytes = #view.menu.text + view.menu.width = vim.fn.strdisplaywidth(view.menu.text) + view.menu.hl_group = 'CmpItemMenu' + view.dup = item.dup + end) return view end) end diff --git a/lua/cmp/utils/buffer.lua b/lua/cmp/utils/buffer.lua index f3d1c8e..b48a1b1 100644 --- a/lua/cmp/utils/buffer.lua +++ b/lua/cmp/utils/buffer.lua @@ -1,17 +1,30 @@ local buffer = {} -buffer.ensure = setmetatable({ - cache = {}, -}, { - __call = function(self, name) - if not (self.cache[name] and vim.api.nvim_buf_is_valid(self.cache[name])) then - local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile') - vim.api.nvim_buf_set_option(buf, 'bufhidden', 'hide') - self.cache[name] = buf - end - return self.cache[name] - end, -}) +buffer.cache = {} + +---@return number buf +buffer.get = function(name) + local buf = buffer.cache[name] + if buf and vim.api.nvim_buf_is_valid(buf) then + return buf + else + return nil + end +end + +---@return number buf +---@return boolean created_new +buffer.ensure = function(name) + local created_new = false + local buf = buffer.get(name) + if not buf then + created_new = true + buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile') + vim.api.nvim_buf_set_option(buf, 'bufhidden', 'hide') + buffer.cache[name] = buf + end + return buf, created_new +end return buffer diff --git a/lua/cmp/utils/window.lua b/lua/cmp/utils/window.lua index 98d8e17..fb083b6 100644 --- a/lua/cmp/utils/window.lua +++ b/lua/cmp/utils/window.lua @@ -18,6 +18,7 @@ local api = require('cmp.utils.api') ---@field public swin2 number|nil ---@field public style cmp.WindowStyle ---@field public opt table +---@field public buffer_opt table ---@field public cache cmp.Cache local window = {} @@ -32,6 +33,7 @@ window.new = function() self.style = {} self.cache = cache.new() self.opt = {} + self.buffer_opt = {} return self end @@ -54,6 +56,26 @@ window.option = function(self, key, value) end end +---Set buffer option. +---NOTE: If the buffer already visible, immediately applied to it. +---@param key string +---@param value any +window.buffer_option = function(self, key, value) + if vim.fn.exists('+' .. key) == 0 then + return + end + + if value == nil then + return self.buffer_opt[key] + end + + self.buffer_opt[key] = value + local existing_buf = buffer.get(self.name) + if existing_buf then + vim.api.nvim_buf_set_option(existing_buf, key, value) + end +end + ---Set style. ---@param style cmp.WindowStyle window.set_style = function(self, style) @@ -70,7 +92,13 @@ end ---Return buffer id. ---@return number window.get_buffer = function(self) - return buffer.ensure(self.name) + local buf, created_new = buffer.ensure(self.name) + if created_new then + for k, v in pairs(self.buffer_opt) do + vim.api.nvim_buf_set_option(buf, k, v) + end + end + return buf end ---Open window @@ -89,7 +117,7 @@ window.open = function(self, style) else local s = misc.copy(self.style) s.noautocmd = true - self.win = vim.api.nvim_open_win(buffer.ensure(self.name), false, s) + self.win = vim.api.nvim_open_win(self:get_buffer(), false, s) for k, v in pairs(self.opt) do vim.api.nvim_win_set_option(self.win, k, v) end @@ -251,9 +279,14 @@ window.get_content_height = function(self) vim.api.nvim_buf_get_changedtick(self:get_buffer()), }, function() local height = 0 - for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do - height = height + math.ceil(math.max(1, vim.str_utfindex(text)) / self.style.width) - end + local buf = self:get_buffer() + -- The result of vim.fn.strdisplaywidth depends on the buffer it was called + -- in (see comment in cmp.Entry.get_view). + vim.api.nvim_buf_call(buf, function() + for _, text in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do + height = height + math.ceil(math.max(1, vim.fn.strdisplaywidth(text)) / self.style.width) + end + end) return height end) end diff --git a/lua/cmp/view/custom_entries_view.lua b/lua/cmp/view/custom_entries_view.lua index 4469931..92cbe37 100644 --- a/lua/cmp/view/custom_entries_view.lua +++ b/lua/cmp/view/custom_entries_view.lua @@ -32,6 +32,12 @@ custom_entries_view.new = function() 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') + -- 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.event = event.new() self.offset = -1 self.active = false @@ -48,7 +54,7 @@ custom_entries_view.new = function() vim.api.nvim_set_decoration_provider(custom_entries_view.ns, { on_win = function(_, win, buf, top, bot) - if win ~= self.entries_win.win then + if win ~= self.entries_win.win or buf ~= self.entries_win:get_buffer() then return end @@ -56,7 +62,7 @@ custom_entries_view.new = function() for i = top, bot do local e = self.entries[i + 1] if e then - local v = e:get_view(self.offset) + local v = e:get_view(self.offset, buf) local o = SIDE_PADDING local a = 0 for _, field in ipairs(fields) do @@ -106,12 +112,13 @@ custom_entries_view.open = function(self, offset, entries) -- Apply window options (that might be changed) on the custom completion menu. self.entries_win:option('winblend', vim.o.pumblend) + local entries_buf = self.entries_win:get_buffer() local lines = {} local dedup = {} local preselect = 0 local i = 1 for _, e in ipairs(entries) do - local view = e:get_view(offset) + 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) @@ -125,7 +132,7 @@ custom_entries_view.open = function(self, offset, entries) i = i + 1 end end - vim.api.nvim_buf_set_lines(self.entries_win:get_buffer(), 0, -1, false, lines) + vim.api.nvim_buf_set_lines(entries_buf, 0, -1, false, lines) local width = 0 width = width + 1 @@ -193,10 +200,11 @@ custom_entries_view.draw = function(self) 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) + local view = e:get_view(self.offset, entries_buf) local text = {} table.insert(text, string.rep(' ', SIDE_PADDING)) for _, field in ipairs(fields) do @@ -207,7 +215,7 @@ custom_entries_view.draw = function(self) table.insert(texts, table.concat(text, '')) end end - vim.api.nvim_buf_set_lines(self.entries_win:get_buffer(), topline, botline, false, texts) + vim.api.nvim_buf_set_lines(entries_buf, topline, botline, false, texts) if api.is_cmdline_mode() then vim.api.nvim_win_call(self.entries_win.win, function()