1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-03 15:10:04 +08:00
SpaceVim/bundle/nui.nvim/lua/nui/popup/border.lua
2023-05-30 21:09:18 +08:00

636 lines
16 KiB
Lua
Vendored

local Object = require("nui.object")
local Line = require("nui.line")
local Text = require("nui.text")
local _ = require("nui.utils")._
local defaults = require("nui.utils").defaults
local is_type = require("nui.utils").is_type
local u = {
clear_namespace = _.clear_namespace,
}
local has_nvim_0_5_1 = vim.fn.has("nvim-0.5.1") == 1
local index_name = {
"top_left",
"top",
"top_right",
"right",
"bottom_right",
"bottom",
"bottom_left",
"left",
}
local function to_border_map(border)
-- fillup all 8 characters
local count = vim.tbl_count(border)
if count < 8 then
for i = count + 1, 8 do
local fallback_index = i % count
local char = border[fallback_index == 0 and count or fallback_index]
if is_type("table", char) then
char = char.content and Text(char) or vim.deepcopy(char)
end
border[i] = char
end
end
local named_border = {}
for index, name in ipairs(index_name) do
named_border[name] = border[index]
end
return named_border
end
local function to_border_list(named_border)
local border = {}
for index, name in ipairs(index_name) do
if is_type("nil", named_border[name]) then
error(string.format("missing named border: %s", name))
end
border[index] = named_border[name]
end
return border
end
---@param internal nui_popup_border_internal
local function normalize_border_char(internal)
if not internal.char or is_type("string", internal.char) then
return internal.char
end
if internal.type == "simple" then
for position, item in pairs(internal.char) do
if is_type("string", item) then
internal.char[position] = item
elseif item.content then
if item.extmark and item.extmark.hl_group then
internal.char[position] = { item:content(), item.extmark.hl_group }
else
internal.char[position] = item:content()
end
else
internal.char[position] = item
end
end
return internal.char
end
for position, item in pairs(internal.char) do
if is_type("string", item) then
internal.char[position] = Text(item, "FloatBorder")
elseif not item.content then
internal.char[position] = Text(item[1], item[2] or "FloatBorder")
elseif item.extmark then
item.extmark.hl_group = item.extmark.hl_group or "FloatBorder"
else
item.extmark = { hl_group = "FloatBorder" }
end
end
return internal.char
end
---@param text? nil | string | NuiText
local function normalize_border_text(text)
if not text then
return text
end
if is_type("string", text) then
return Text(text, "FloatTitle")
end
text.extmark = vim.tbl_deep_extend("keep", text.extmark or {}, {
hl_group = "FloatTitle",
})
return text
end
---@param internal nui_popup_border_internal
---@param popup_winhighlight? string
local function calculate_winhighlight(internal, popup_winhighlight)
if internal.type == "simple" then
return
end
local winhl = popup_winhighlight
-- @deprecated
if internal.highlight then
if not string.match(internal.highlight, ":") then
internal.highlight = "FloatBorder:" .. internal.highlight
end
winhl = internal.highlight
internal.highlight = nil
end
return winhl
end
---@return nui_popup_border_internal_padding|nil
local function parse_padding(padding)
if not padding then
return nil
end
if is_type("map", padding) then
return padding
end
local map = {}
map.top = defaults(padding[1], 0)
map.right = defaults(padding[2], map.top)
map.bottom = defaults(padding[3], map.top)
map.left = defaults(padding[4], map.right)
return map
end
---@param edge "'top'" | "'bottom'"
---@param text? nil | string | NuiText
---@param align? nil | "'left'" | "'center'" | "'right'"
---@return table NuiLine
local function calculate_buf_edge_line(internal, edge, text, align)
local char, size = internal.char, internal.size
local left_char = char[edge .. "_left"]
local mid_char = char[edge]
local right_char = char[edge .. "_right"]
if left_char:content() == "" then
left_char = Text(mid_char:content() == "" and char["left"] or mid_char)
end
if right_char:content() == "" then
right_char = Text(mid_char:content() == "" and char["right"] or mid_char)
end
local max_width = size.width - left_char:width() - right_char:width()
local content_text = Text(defaults(text, ""))
if mid_char:width() == 0 then
content_text:set(string.rep(" ", max_width))
else
content_text:set(_.truncate_text(content_text:content(), max_width))
end
local left_gap_width, right_gap_width = _.calculate_gap_width(
defaults(align, "center"),
max_width,
content_text:width()
)
local line = Line()
line:append(left_char)
if left_gap_width > 0 then
line:append(Text(mid_char):set(string.rep(mid_char:content(), left_gap_width)))
end
line:append(content_text)
if right_gap_width > 0 then
line:append(Text(mid_char):set(string.rep(mid_char:content(), right_gap_width)))
end
line:append(right_char)
return line
end
---@return nil | table[] # NuiLine[]
local function calculate_buf_lines(internal)
local char, size, text = internal.char, internal.size, defaults(internal.text, {})
if is_type("string", char) then
return nil
end
local left_char, right_char = char.left, char.right
local gap_length = size.width - left_char:width() - right_char:width()
local lines = {}
table.insert(lines, calculate_buf_edge_line(internal, "top", text.top, text.top_align))
for _ = 1, size.height - 2 do
table.insert(
lines,
Line({
Text(left_char),
Text(string.rep(" ", gap_length)),
Text(right_char),
})
)
end
table.insert(lines, calculate_buf_edge_line(internal, "bottom", text.bottom, text.bottom_align))
return lines
end
local styles = {
double = to_border_map({ "", "", "", "", "", "", "", "" }),
none = "none",
rounded = to_border_map({ "", "", "", "", "", "", "", "" }),
shadow = "shadow",
single = to_border_map({ "", "", "", "", "", "", "", "" }),
solid = to_border_map({ "", "", "", "", "", "", "", "" }),
}
---@param internal nui_popup_border_internal
---@return nui_popup_border_internal_size
local function calculate_size_delta(internal)
---@type nui_popup_border_internal_size
local delta = {
width = 0,
height = 0,
}
local char = internal.char
if is_type("map", char) then
if char.top ~= "" then
delta.height = delta.height + 1
end
if char.bottom ~= "" then
delta.height = delta.height + 1
end
if char.left ~= "" then
delta.width = delta.width + 1
end
if char.right ~= "" then
delta.width = delta.width + 1
end
end
local padding = internal.padding
if padding then
if padding.top then
delta.height = delta.height + padding.top
end
if padding.bottom then
delta.height = delta.height + padding.bottom
end
if padding.left then
delta.width = delta.width + padding.left
end
if padding.right then
delta.width = delta.width + padding.right
end
end
return delta
end
---@param border NuiPopupBorder
---@return nui_popup_border_internal_size
local function calculate_size(border)
---@type nui_popup_border_internal_size
local size = vim.deepcopy(border.popup._.size)
size.width = size.width + border._.size_delta.width
size.height = size.height + border._.size_delta.height
return size
end
---@param border NuiPopupBorder
---@return nui_popup_border_internal_position
local function calculate_position(border)
local position = vim.deepcopy(border.popup._.position)
position.col = position.col - math.floor(border._.size_delta.width / 2 + 0.5)
position.row = position.row - math.floor(border._.size_delta.height / 2 + 0.5)
return position
end
local function adjust_popup_win_config(border)
local internal = border._
if internal.type ~= "complex" then
return
end
local popup_position = {
row = 0,
col = 0,
}
local char = internal.char
if is_type("map", char) then
if char.top ~= "" then
popup_position.row = popup_position.row + 1
end
if char.left ~= "" then
popup_position.col = popup_position.col + 1
end
end
local padding = internal.padding
if padding then
if padding.top then
popup_position.row = popup_position.row + padding.top
end
if padding.left then
popup_position.col = popup_position.col + padding.left
end
end
local popup = border.popup
if not has_nvim_0_5_1 then
popup.win_config.row = internal.position.row + popup_position.row
popup.win_config.col = internal.position.col + popup_position.col
return
end
popup.win_config.relative = "win"
-- anchor to the border window instead
popup.win_config.anchor = "NW"
popup.win_config.win = border.winid
popup.win_config.bufpos = nil
popup.win_config.row = popup_position.row
popup.win_config.col = popup_position.col
end
--luacheck: push no max line length
---@alias nui_t_text_align "'left'" | "'center'" | "'right'"
---@alias nui_popup_border_internal_padding { top: number, right: number, bottom: number, left: number }
---@alias nui_popup_border_internal_position { row: number, col: number }
---@alias nui_popup_border_internal_size { width: number, height: number }
---@alias nui_popup_border_internal_text { top?: string|NuiText, top_align?: nui_t_text_align, bottom?: string|NuiText, bottom_align?: nui_t_text_align }
---@alias nui_popup_border_internal { type: "'simple'"|"'complex'", style: table, char: any, padding?: nui_popup_border_internal_padding, position: nui_popup_border_internal_position, size: nui_popup_border_internal_size, size_delta: nui_popup_border_internal_size, text: nui_popup_border_internal_text, lines?: table[], winhighlight?: string }
--luacheck: pop
---@class NuiPopupBorder
---@field bufnr integer
---@field private _ nui_popup_border_internal
---@field private popup NuiPopup
---@field win_config nui_popup_win_config
---@field winid number
local Border = Object("NuiPopupBorder")
---@param popup NuiPopup
function Border:init(popup, options)
self.popup = popup
self._ = {
type = "simple",
style = defaults(options.style, "none"),
-- @deprecated
highlight = options.highlight,
padding = parse_padding(options.padding),
text = options.text,
}
local internal = self._
if internal.text then
internal.text.top = normalize_border_text(internal.text.top)
internal.text.bottom = normalize_border_text(internal.text.bottom)
end
local style = internal.style
if is_type("list", style) then
internal.char = to_border_map(style)
elseif is_type("string", style) then
if not styles[style] then
error("invalid border style name")
end
internal.char = vim.deepcopy(styles[style])
else
internal.char = internal.style
end
local is_borderless = is_type("string", internal.char)
if is_borderless then
if internal.text then
error("text not supported for style:" .. internal.char)
end
end
if internal.text or internal.padding then
internal.type = "complex"
end
internal.winhighlight = calculate_winhighlight(internal, self.popup._.win_options.winhighlight)
internal.char = normalize_border_char(internal)
internal.size_delta = calculate_size_delta(internal)
if internal.type == "simple" then
return self
end
self.win_config = {
style = "minimal",
border = "none",
focusable = false,
zindex = self.popup.win_config.zindex - 1,
anchor = self.popup.win_config.anchor,
}
if type(internal.char) == "string" then
self.win_config.border = internal.char
end
end
function Border:_open_window()
if self.winid or not self.bufnr then
return
end
self.win_config.noautocmd = true
self.winid = vim.api.nvim_open_win(self.bufnr, false, self.win_config)
self.win_config.noautocmd = nil
assert(self.winid, "failed to create border window")
if self._.winhighlight then
vim.api.nvim_win_set_option(self.winid, "winhighlight", self._.winhighlight)
end
adjust_popup_win_config(self)
vim.api.nvim_command("redraw")
end
function Border:_close_window()
if not self.winid then
return
end
if vim.api.nvim_win_is_valid(self.winid) then
vim.api.nvim_win_close(self.winid, true)
end
self.winid = nil
end
function Border:mount()
local popup = self.popup
if not popup._.loading or popup._.mounted then
return
end
local internal = self._
if internal.type == "simple" then
return
end
self.bufnr = vim.api.nvim_create_buf(false, true)
assert(self.bufnr, "failed to create border buffer")
if internal.lines then
_.render_lines(internal.lines, self.bufnr, popup.ns_id, 1, #internal.lines)
end
self:_open_window()
end
function Border:unmount()
local popup = self.popup
if not popup._.loading or not popup._.mounted then
return
end
local internal = self._
if internal.type == "simple" then
return
end
if self.bufnr then
if vim.api.nvim_buf_is_valid(self.bufnr) then
u.clear_namespace(self.bufnr, self.popup.ns_id)
vim.api.nvim_buf_delete(self.bufnr, { force = true })
end
self.bufnr = nil
end
self:_close_window()
end
function Border:_relayout()
local internal = self._
if internal.type ~= "complex" then
return
end
local position = self.popup._.position
self.win_config.relative = position.relative
self.win_config.win = position.relative == "win" and position.win or nil
self.win_config.bufpos = position.bufpos
internal.size = calculate_size(self)
self.win_config.width = internal.size.width
self.win_config.height = internal.size.height
internal.position = calculate_position(self)
self.win_config.row = internal.position.row
self.win_config.col = internal.position.col
internal.lines = calculate_buf_lines(internal)
if self.winid then
vim.api.nvim_win_set_config(self.winid, self.win_config)
end
if self.bufnr then
if internal.lines then
_.render_lines(internal.lines, self.bufnr, self.popup.ns_id, 1, #internal.lines)
end
end
adjust_popup_win_config(self)
vim.api.nvim_command("redraw")
end
---@param edge "'top'" | "'bottom'"
---@param text? nil | string | table # string or NuiText
---@param align? nil | "'left'" | "'center'" | "'right'"
function Border:set_text(edge, text, align)
local internal = self._
if not internal.lines or not internal.text then
return
end
internal.text[edge] = normalize_border_text(text)
internal.text[edge .. "_align"] = defaults(align, internal.text[edge .. "_align"])
local line = calculate_buf_edge_line(internal, edge, internal.text[edge], internal.text[edge .. "_align"])
local linenr = edge == "top" and 1 or #internal.lines
internal.lines[linenr] = line
line:render(self.bufnr, self.popup.ns_id, linenr)
end
---@param highlight string highlight group
function Border:set_highlight(highlight)
local internal = self._
local winhighlight_data = _.parse_winhighlight(self.popup._.win_options.winhighlight)
winhighlight_data["FloatBorder"] = highlight
self.popup._.win_options.winhighlight = _.serialize_winhighlight(winhighlight_data)
if self.popup.winid then
vim.api.nvim_win_set_option(self.popup.winid, "winhighlight", self.popup._.win_options.winhighlight)
end
internal.winhighlight = calculate_winhighlight(internal, self.popup._.win_options.winhighlight)
if self.winid then
vim.api.nvim_win_set_option(self.winid, "winhighlight", internal.winhighlight)
end
end
function Border:get()
local internal = self._
if internal.type ~= "simple" then
return nil
end
if is_type("string", internal.char) then
return internal.char
end
if is_type("map", internal.char) then
return to_border_list(internal.char)
end
end
---@alias NuiPopupBorder.constructor fun(popup: NuiPopup, options: table): NuiPopupBorder
---@type NuiPopupBorder|NuiPopupBorder.constructor
local NuiPopupBorder = Border
return NuiPopupBorder