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