mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-03 15:10:04 +08:00
636 lines
16 KiB
Lua
Vendored
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
|