1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-03 11:50:05 +08:00
SpaceVim/bundle/nui.nvim/lua/nui/layout/init.lua

502 lines
11 KiB
Lua
Raw Normal View History

2023-05-30 21:09:18 +08:00
local Object = require("nui.object")
local Popup = require("nui.popup")
local Split = require("nui.split")
local utils = require("nui.utils")
local layout_utils = require("nui.layout.utils")
local float_layout = require("nui.layout.float")
local split_layout = require("nui.layout.split")
local split_utils = require("nui.split.utils")
local autocmd = require("nui.utils.autocmd")
local _ = utils._
local defaults = utils.defaults
local is_type = utils.is_type
local u = {
get_next_id = _.get_next_id,
position = layout_utils.position,
size = layout_utils.size,
split = split_utils,
update_layout_config = layout_utils.update_layout_config,
}
-- GitHub Issue: https://github.com/neovim/neovim/issues/18925
local function apply_workaround_for_float_relative_position_issue_18925(layout)
local current_winid = vim.api.nvim_get_current_win()
vim.api.nvim_set_current_win(layout.winid)
vim.api.nvim_command("redraw!")
vim.api.nvim_set_current_win(current_winid)
end
local function merge_default_options(options)
options.relative = defaults(options.relative, "win")
return options
end
local function normalize_options(options)
options = _.normalize_layout_options(options)
return options
end
local function is_box(object)
return object and (object.box or object.component)
end
local function is_component(object)
return object and object.mount
end
local function is_component_mounted(component)
return is_type("number", component.winid)
end
local function get_layout_config_relative_to_component(component)
return {
relative = { type = "win", winid = component.winid },
position = { row = 0, col = 0 },
size = { width = "100%", height = "100%" },
}
end
---@param layout NuiLayout
---@param box table Layout.Box
local function wire_up_layout_components(layout, box)
for _, child in ipairs(box.box) do
if child.component then
autocmd.create({ "BufWipeout", "QuitPre" }, {
group = layout._.augroup.unmount,
buffer = child.component.bufnr,
callback = vim.schedule_wrap(function()
layout:unmount()
end),
}, child.component.bufnr)
autocmd.create("BufWinEnter", {
group = layout._.augroup.unmount,
buffer = child.component.bufnr,
callback = function()
local winid = child.component.winid
if layout._.type == "float" and not winid then
--[[
`BufWinEnter` does not contain window id and
it is fired before `nvim_open_win` returns
the window id.
--]]
winid = vim.fn.bufwinid(child.component.bufnr)
end
autocmd.create("WinClosed", {
group = layout._.augroup.hide,
pattern = tostring(winid),
callback = function()
layout:hide()
end,
}, child.component.bufnr)
end,
}, child.component.bufnr)
else
wire_up_layout_components(layout, child)
end
end
end
---@class NuiLayout
local Layout = Object("NuiLayout")
---@return '"float"'|'"split"' layout_type
local function get_layout_type(box)
for _, child in ipairs(box.box) do
if child.component and child.type then
return child.type
end
local type = get_layout_type(child)
if type then
return type
end
end
error("unexpected empty box")
end
function Layout:init(options, box)
local id = u.get_next_id()
box = Layout.Box(box)
local type = get_layout_type(box)
self._ = {
id = id,
type = type,
box = box,
loading = false,
mounted = false,
augroup = {
hide = string.format("%s_hide", id),
unmount = string.format("%s_unmount", id),
},
}
if type == "float" then
local container
if is_component(options) then
container = options
options = get_layout_config_relative_to_component(container)
else
options = merge_default_options(options)
options = normalize_options(options)
end
self._[type] = {
container = container,
layout = {},
win_enter = false,
win_config = {
focusable = false,
style = "minimal",
zindex = 49,
},
win_options = {
winblend = 100,
},
}
if not is_component(container) or is_component_mounted(container) then
self:update(options)
end
end
if type == "split" then
options = u.split.merge_default_options(options)
options = u.split.normalize_options(options)
self._[type] = {
layout = {},
position = options.position,
size = {},
win_config = {
pending_changes = {},
},
}
self:update(options)
end
end
function Layout:_process_layout()
local type = self._.type
if type == "float" then
local info = self._.float
apply_workaround_for_float_relative_position_issue_18925(self)
float_layout.process(self._.box, {
winid = self.winid,
container_size = info.size,
position = {
row = 0,
col = 0,
},
})
return
end
if type == "split" then
local info = self._.split
split_layout.process(self._.box, {
position = info.position,
relative = info.relative,
container_size = info.size,
container_fallback_size = info.container_info.size,
})
end
end
function Layout:_open_window()
if self._.type == "float" then
local info = self._.float
self.winid = vim.api.nvim_open_win(self.bufnr, info.win_enter, info.win_config)
assert(self.winid, "failed to create popup window")
_.set_win_options(self.winid, info.win_options)
end
end
function Layout:_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 Layout:mount()
if self._.loading or self._.mounted then
return
end
self._.loading = true
local type = self._.type
if type == "float" then
local info = self._.float
local container = info.container
if is_component(container) and not is_component_mounted(container) then
container:mount()
self:update(get_layout_config_relative_to_component(container))
end
if not self.bufnr then
self.bufnr = vim.api.nvim_create_buf(false, true)
assert(self.bufnr, "failed to create buffer")
end
self:_open_window()
end
self:_process_layout()
if type == "float" then
float_layout.mount_box(self._.box)
end
if type == "split" then
split_layout.mount_box(self._.box)
end
self._.loading = false
self._.mounted = true
end
function Layout:unmount()
if self._.loading or not self._.mounted then
return
end
pcall(autocmd.delete_group, self._.augroup.hide)
pcall(autocmd.delete_group, self._.augroup.unmount)
self._.loading = true
local type = self._.type
if type == "float" then
float_layout.unmount_box(self._.box)
if self.bufnr then
if vim.api.nvim_buf_is_valid(self.bufnr) then
vim.api.nvim_buf_delete(self.bufnr, { force = true })
end
self.bufnr = nil
end
self:_close_window()
end
if type == "split" then
split_layout.unmount_box(self._.box)
end
self._.loading = false
self._.mounted = false
end
function Layout:hide()
if self._.loading or not self._.mounted then
return
end
self._.loading = true
pcall(autocmd.delete_group, self._.augroup.hide)
local type = self._.type
if type == "float" then
float_layout.hide_box(self._.box)
self:_close_window()
end
if type == "split" then
split_layout.hide_box(self._.box)
end
self._.loading = false
end
function Layout:show()
if self._.loading or not self._.mounted then
return
end
self._.loading = true
autocmd.create_group(self._.augroup.hide, { clear = true })
local type = self._.type
if type == "float" then
self:_open_window()
end
self:_process_layout()
if type == "float" then
float_layout.show_box(self._.box)
end
if type == "split" then
split_layout.show_box(self._.box)
end
self._.loading = false
end
function Layout:update(config, box)
config = config or {}
if not box and is_box(config) or is_box(config[1]) then
box = config
config = {}
end
autocmd.create_group(self._.augroup.hide, { clear = true })
autocmd.create_group(self._.augroup.unmount, { clear = true })
local prev_box = self._.box
if box then
self._.box = Layout.Box(box)
self._.type = get_layout_type(self._.box)
end
if self._.type == "float" then
local info = self._.float
u.update_layout_config(info, config)
if self.winid then
vim.api.nvim_win_set_config(self.winid, info.win_config)
self:_process_layout()
float_layout.process_box_change(self._.box, prev_box)
end
wire_up_layout_components(self, self._.box)
end
if self._.type == "split" then
local info = self._.split
local relative_winid = info.relative and info.relative.win
local prev_winid = vim.api.nvim_get_current_win()
if relative_winid then
vim.api.nvim_set_current_win(relative_winid)
end
local curr_box = self._.box
if prev_box ~= curr_box then
self._.box = prev_box
self:hide()
self._.box = curr_box
end
u.split.update_layout_config(info, config)
if prev_box == curr_box then
self:_process_layout()
else
self:show()
end
if vim.api.nvim_win_is_valid(prev_winid) then
vim.api.nvim_set_current_win(prev_winid)
end
wire_up_layout_components(self, self._.box)
end
end
function Layout.Box(box, options)
options = options or {}
if is_box(box) then
return box
end
if box.mount then
local type
if box:is_instance_of(Popup) then
type = "float"
elseif box:is_instance_of(Split) then
type = "split"
end
if not type then
error("unsupported component")
end
return {
type = type,
component = box,
grow = options.grow,
size = options.size,
}
end
local dir = defaults(options.dir, "row")
-- normalize children size
for _, child in ipairs(box) do
if not child.grow and not child.size then
error("missing child.size")
end
if dir == "row" then
if not is_type("table", child.size) then
child.size = { width = child.size }
end
if not child.size.height then
child.size.height = "100%"
end
elseif dir == "col" then
if not is_type("table", child.size) then
child.size = { height = child.size }
end
if not child.size.width then
child.size.width = "100%"
end
end
end
return {
box = box,
dir = dir,
grow = options.grow,
size = options.size,
}
end
---@alias NuiLayout.constructor fun(options: table, box: table): NuiLayout
---@type NuiLayout|NuiLayout.constructor
local NuiLayout = Layout
return NuiLayout