Files
nvim-cmp/lua/cmp/utils/window.lua
hankertrix 53d80d4d0c fix: small windows no longer constantly throw errors (#1824)
The entries_win:open function will fail when either the width or
the height of the window is less than 1, which will result in the
nvim_win_set_cursor being passed a nil value for self.entries_win.win
instead of a number, which causes an error to constantly appear when
the window is small. Hence, a guard clause to check for whether
self.entries_win.win is nil is added to stop the error from occurring.

Also fixed the rarer 'height' must be a positive Integer error caused
by the window.update function, within the clause to draw the scrollbar
background. This error happens on small windows when pressing and
holding down the backspace key to delete a lot of characters at once.
The nvim_open_win function call to create the scrollbar
background throws the error when self.style.height is 0. Hence, an
additional check is added alongside the info.scrollable check which also
skips drawing the scrollbar thumb as it is not needed when the height is
0 and will result in a weird scrollbar thumb floating some distance
away from the text when holding down backspace to delete a lot of
characters.
2024-03-22 03:02:15 +09:00

319 lines
9.1 KiB
Lua

local misc = require('cmp.utils.misc')
local opt = require('cmp.utils.options')
local buffer = require('cmp.utils.buffer')
local api = require('cmp.utils.api')
local config = require('cmp.config')
---@class cmp.WindowStyle
---@field public relative string
---@field public row integer
---@field public col integer
---@field public width integer|float
---@field public height integer|float
---@field public border string|string[]|nil
---@field public zindex integer|nil
---@class cmp.Window
---@field public name string
---@field public win integer|nil
---@field public thumb_win integer|nil
---@field public sbar_win integer|nil
---@field public style cmp.WindowStyle
---@field public opt table<string, any>
---@field public buffer_opt table<string, any>
local window = {}
---new
---@return cmp.Window
window.new = function()
local self = setmetatable({}, { __index = window })
self.name = misc.id('cmp.utils.window.new')
self.win = nil
self.sbar_win = nil
self.thumb_win = nil
self.style = {}
self.opt = {}
self.buffer_opt = {}
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 vim.fn.exists('+' .. key) == 0 then
return
end
if value == nil then
return self.opt[key]
end
self.opt[key] = value
if self:visible() then
opt.win_set_option(self.win, 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
opt.buf_set_option(existing_buf, key, value)
end
end
---Set style.
---@param style cmp.WindowStyle
window.set_style = function(self, style)
self.style = style
local info = self:info()
if vim.o.lines and vim.o.lines <= info.row + info.height + 1 then
self.style.height = vim.o.lines - info.row - info.border_info.vert - 1
end
self.style.zindex = self.style.zindex or 1
--- GUI clients are allowed to return fractional bounds, but we need integer
--- bounds to open the window
self.style.width = math.ceil(self.style.width)
self.style.height = math.ceil(self.style.height)
end
---Return buffer id.
---@return integer
window.get_buffer = function(self)
local buf, created_new = buffer.ensure(self.name)
if created_new then
for k, v in pairs(self.buffer_opt) do
opt.buf_set_option(buf, k, v)
end
end
return buf
end
---Open window
---@param style cmp.WindowStyle
window.open = function(self, style)
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:get_buffer(), false, s)
for k, v in pairs(self.opt) do
opt.win_set_option(self.win, k, v)
end
end
self:update()
end
---Update
window.update = function(self)
local info = self:info()
if info.scrollable and self.style.height > 0 then
-- Draw the background of the scrollbar
if not info.border_info.visible then
local style = {
relative = 'editor',
style = 'minimal',
width = 1,
height = self.style.height,
row = info.row,
col = info.col + info.width - info.scrollbar_offset, -- info.col was already contained the scrollbar offset.
zindex = (self.style.zindex and (self.style.zindex + 1) or 1),
}
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
vim.api.nvim_win_set_config(self.sbar_win, style)
else
style.noautocmd = true
self.sbar_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'sbar_buf'), false, style)
opt.win_set_option(self.sbar_win, 'winhighlight', 'EndOfBuffer:PmenuSbar,NormalFloat:PmenuSbar')
end
end
-- Draw the scrollbar thumb
local thumb_height = math.floor(info.inner_height * (info.inner_height / self:get_content_height()) + 0.5)
local thumb_offset = math.floor(info.inner_height * (vim.fn.getwininfo(self.win)[1].topline / self:get_content_height()))
local style = {
relative = 'editor',
style = 'minimal',
width = 1,
height = math.max(1, thumb_height),
row = info.row + thumb_offset + (info.border_info.visible and info.border_info.top or 0),
col = info.col + info.width - 1, -- info.col was already added scrollbar offset.
zindex = (self.style.zindex and (self.style.zindex + 2) or 2),
}
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
vim.api.nvim_win_set_config(self.thumb_win, style)
else
style.noautocmd = true
self.thumb_win = vim.api.nvim_open_win(buffer.ensure(self.name .. 'thumb_buf'), false, style)
opt.win_set_option(self.thumb_win, 'winhighlight', 'EndOfBuffer:PmenuThumb,NormalFloat:PmenuThumb')
end
else
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
vim.api.nvim_win_hide(self.sbar_win)
self.sbar_win = nil
end
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
vim.api.nvim_win_hide(self.thumb_win)
self.thumb_win = nil
end
end
-- In cmdline, vim does not redraw automatically.
if api.is_cmdline_mode() then
vim.api.nvim_win_call(self.win, function()
misc.redraw()
end)
end
end
---Close window
window.close = function(self)
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_hide(self.win)
self.win = nil
end
if self.sbar_win and vim.api.nvim_win_is_valid(self.sbar_win) then
vim.api.nvim_win_hide(self.sbar_win)
self.sbar_win = nil
end
if self.thumb_win and vim.api.nvim_win_is_valid(self.thumb_win) then
vim.api.nvim_win_hide(self.thumb_win)
self.thumb_win = nil
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 win info.
window.info = function(self)
local border_info = self:get_border_info()
local scrollbar = config.get().window.completion.scrollbar
local info = {
row = self.style.row,
col = self.style.col,
width = self.style.width + border_info.left + border_info.right,
height = self.style.height + border_info.top + border_info.bottom,
inner_width = self.style.width,
inner_height = self.style.height,
border_info = border_info,
scrollable = false,
scrollbar_offset = 0,
}
if self:get_content_height() > info.inner_height and scrollbar then
info.scrollable = true
if not border_info.visible then
info.scrollbar_offset = 1
info.width = info.width + 1
end
end
return info
end
---Return border information.
---@return { top: integer, left: integer, right: integer, bottom: integer, vert: integer, horiz: integer, visible: boolean }
window.get_border_info = function(self)
local border = self.style.border
if not border or border == 'none' then
return {
top = 0,
left = 0,
right = 0,
bottom = 0,
vert = 0,
horiz = 0,
visible = false,
}
end
if type(border) == 'string' then
if border == 'shadow' then
return {
top = 0,
left = 0,
right = 1,
bottom = 1,
vert = 1,
horiz = 1,
visible = false,
}
end
return {
top = 1,
left = 1,
right = 1,
bottom = 1,
vert = 2,
horiz = 2,
visible = true,
}
end
local new_border = {}
while #new_border <= 8 do
for _, b in ipairs(border) do
table.insert(new_border, type(b) == 'string' and b or b[1])
end
end
local info = {}
info.top = new_border[2] == '' and 0 or 1
info.right = new_border[4] == '' and 0 or 1
info.bottom = new_border[6] == '' and 0 or 1
info.left = new_border[8] == '' and 0 or 1
info.vert = info.top + info.bottom
info.horiz = info.left + info.right
info.visible = not (vim.tbl_contains({ '', ' ' }, new_border[2]) and vim.tbl_contains({ '', ' ' }, new_border[4]) and vim.tbl_contains({ '', ' ' }, new_border[6]) and vim.tbl_contains({ '', ' ' }, new_border[8]))
return info
end
---Get scroll height.
---NOTE: The result of vim.fn.strdisplaywidth depends on the buffer it was called in (see comment in cmp.Entry.get_view).
---@return integer
window.get_content_height = function(self)
if not self:option('wrap') then
return vim.api.nvim_buf_line_count(self:get_buffer())
end
local height = 0
vim.api.nvim_buf_call(self:get_buffer(), function()
for _, text in ipairs(vim.api.nvim_buf_get_lines(self:get_buffer(), 0, -1, false)) do
height = height + math.max(1, math.ceil(vim.fn.strdisplaywidth(text) / self.style.width))
end
end)
return height
end
return window