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

485 lines
13 KiB
Lua
Vendored

local Object = require("nui.object")
local _ = require("nui.utils")._
local defaults = require("nui.utils").defaults
local is_type = require("nui.utils").is_type
local tree_util = require("nui.tree.util")
local u = {
clear_namespace = _.clear_namespace,
normalize_namespace_id = _.normalize_namespace_id,
}
---@param bufnr number
---@param linenr_range { [1]: integer, [2]: integer }
local function clear_buf_lines(bufnr, linenr_range)
local count = linenr_range[2] - linenr_range[1] + 1
if count < 1 then
return
end
local lines = {}
for i = 1, count do
lines[i] = ""
end
vim.api.nvim_buf_set_lines(bufnr, linenr_range[1] - 1, linenr_range[2], false, lines)
end
-- returns id of the first window that contains the buffer
---@param bufnr number
---@return number winid
local function get_winid(bufnr)
return vim.fn.win_findbuf(bufnr)[1]
end
---@param nodes NuiTreeNode[]
---@param parent_node? NuiTreeNode
---@param get_node_id nui_tree_get_node_id
---@return { by_id: table<string, NuiTreeNode>, root_ids: string[] }
local function initialize_nodes(nodes, parent_node, get_node_id)
local start_depth = parent_node and parent_node:get_depth() + 1 or 1
---@type table<string, NuiTreeNode>
local by_id = {}
---@type string[]
local root_ids = {}
---@param node NuiTreeNode
---@param depth number
local function initialize(node, depth)
node._depth = depth
node._id = get_node_id(node)
node._initialized = true
local node_id = node:get_id()
if by_id[node_id] then
error("duplicate node id" .. node_id)
end
by_id[node_id] = node
if depth == start_depth then
table.insert(root_ids, node_id)
end
if not node.__children or #node.__children == 0 then
return
end
if not node._child_ids then
node._child_ids = {}
end
for _, child_node in ipairs(node.__children) do
child_node._parent_id = node_id
initialize(child_node, depth + 1)
table.insert(node._child_ids, child_node:get_id())
end
node.__children = nil
end
for _, node in ipairs(nodes) do
node._parent_id = parent_node and parent_node:get_id() or nil
initialize(node, start_depth)
end
return {
by_id = by_id,
root_ids = root_ids,
}
end
---@class NuiTreeNode
local TreeNode = {
super = nil,
}
---@return string
function TreeNode:get_id()
return self._id
end
---@return number
function TreeNode:get_depth()
return self._depth
end
---@return string|nil
function TreeNode:get_parent_id()
return self._parent_id
end
---@return boolean
function TreeNode:has_children()
return #(self._child_ids or self.__children or {}) > 0
end
---@return string[]
function TreeNode:get_child_ids()
return self._child_ids or {}
end
---@return boolean
function TreeNode:is_expanded()
return self._is_expanded
end
---@return boolean is_updated
function TreeNode:expand()
if self:has_children() and not self:is_expanded() then
self._is_expanded = true
return true
end
return false
end
---@return boolean is_updated
function TreeNode:collapse()
if self:is_expanded() then
self._is_expanded = false
return true
end
return false
end
--luacheck: push no max line length
---@alias nui_tree_get_node_id fun(node: NuiTreeNode): string
---@alias nui_tree_prepare_node fun(node: NuiTreeNode, parent_node?: NuiTreeNode): nil | string | string[] | NuiLine | NuiLine[]
---@alias nui_tree_internal { buf_options: table<string,any>, win_options: table<string,any>, get_node_id: nui_tree_get_node_id, prepare_node: nui_tree_prepare_node, track_tree_linenr?: boolean }
--luacheck: pop
---@class NuiTree
---@field bufnr integer
---@field nodes { by_id: table<string,NuiTreeNode>, root_ids: string[] }
---@field ns_id integer
---@field private _ nui_tree_internal
---@field winid number # @deprecated
local Tree = Object("NuiTree")
function Tree:init(options)
---@deprecated
if options.winid then
if not vim.api.nvim_win_is_valid(options.winid) then
error("invalid winid " .. options.winid)
end
self.winid = options.winid
self.bufnr = vim.api.nvim_win_get_buf(self.winid)
end
if options.bufnr then
if not vim.api.nvim_buf_is_valid(options.bufnr) then
error("invalid bufnr " .. options.bufnr)
end
self.bufnr = options.bufnr
self.winid = nil
end
if not self.bufnr then
error("missing bufnr")
end
self.ns_id = u.normalize_namespace_id(options.ns_id)
self._ = {
buf_options = vim.tbl_extend("force", {
bufhidden = "hide",
buflisted = false,
buftype = "nofile",
modifiable = false,
readonly = true,
swapfile = false,
undolevels = 0,
}, defaults(options.buf_options, {})),
---@deprecated
win_options = vim.tbl_extend("force", {
foldcolumn = "0",
foldmethod = "manual",
wrap = false,
}, defaults(options.win_options, {})),
get_node_id = defaults(options.get_node_id, tree_util.default_get_node_id),
prepare_node = defaults(options.prepare_node, tree_util.default_prepare_node),
track_tree_linenr = nil,
}
_.set_buf_options(self.bufnr, self._.buf_options)
---@deprecated
if self.winid then
_.set_win_options(self.winid, self._.win_options)
end
self:set_nodes(defaults(options.nodes, {}))
end
---@generic D : table
---@param data D data table
---@param children NuiTreeNode[]
---@return NuiTreeNode|D
function Tree.Node(data, children)
---@type NuiTreeNode
local self = {
__children = children,
_initialized = false,
_is_expanded = false,
_child_ids = nil,
_parent_id = nil,
_depth = nil,
_id = nil,
}
self = setmetatable(vim.tbl_extend("keep", self, data), {
__index = TreeNode,
__name = "NuiTreeNode",
})
return self
end
---@param node_id_or_linenr? string | number
---@return NuiTreeNode|nil node
---@return number|nil linenr
function Tree:get_node(node_id_or_linenr)
if is_type("string", node_id_or_linenr) then
return self.nodes.by_id[node_id_or_linenr], unpack(self._content.linenr_by_node_id[node_id_or_linenr] or {})
end
local winid = get_winid(self.bufnr)
local linenr = node_id_or_linenr or vim.api.nvim_win_get_cursor(winid)[1]
local node_id = self._content.node_id_by_linenr[linenr]
return self.nodes.by_id[node_id], unpack(self._content.linenr_by_node_id[node_id] or {})
end
---@param parent_id? string parent node's id
---@return NuiTreeNode[] nodes
function Tree:get_nodes(parent_id)
local node_ids = {}
if parent_id then
local parent_node = self.nodes.by_id[parent_id]
if parent_node then
node_ids = parent_node._child_ids
end
else
node_ids = self.nodes.root_ids
end
return vim.tbl_map(function(id)
return self.nodes.by_id[id]
end, node_ids or {})
end
---@param nodes NuiTreeNode[]
---@param parent_node? NuiTreeNode
function Tree:_add_nodes(nodes, parent_node)
local new_nodes = initialize_nodes(nodes, parent_node, self._.get_node_id)
self.nodes.by_id = vim.tbl_extend("force", self.nodes.by_id, new_nodes.by_id)
if parent_node then
if not parent_node._child_ids then
parent_node._child_ids = {}
end
for _, id in ipairs(new_nodes.root_ids) do
table.insert(parent_node._child_ids, id)
end
else
for _, id in ipairs(new_nodes.root_ids) do
table.insert(self.nodes.root_ids, id)
end
end
end
---@param nodes NuiTreeNode[]
---@param parent_id? string parent node's id
function Tree:set_nodes(nodes, parent_id)
--luacheck: push no max line length
---@type { linenr: {[1]?:integer,[2]?:integer}, lines: string[]|NuiLine[], node_id_by_linenr: table<number,string>, linenr_by_node_id: table<string, {[1]:integer,[2]:integer}> }
self._content = { linenr = {}, lines = {}, node_id_by_linenr = {}, linenr_by_node_id = {} }
--luacheck: pop
if not parent_id then
self.nodes = { by_id = {}, root_ids = {} }
self:_add_nodes(nodes)
return
end
local parent_node = self.nodes.by_id[parent_id]
if not parent_node then
error("invalid parent_id " .. parent_id)
end
if parent_node._child_ids then
for _, node_id in ipairs(parent_node._child_ids) do
self.nodes.by_id[node_id] = nil
end
parent_node._child_ids = nil
end
self:_add_nodes(nodes, parent_node)
end
---@param node NuiTreeNode
---@param parent_id? string parent node's id
function Tree:add_node(node, parent_id)
local parent_node = self.nodes.by_id[parent_id]
if parent_id and not parent_node then
error("invalid parent_id " .. parent_id)
end
self:_add_nodes({ node }, parent_node)
end
local function remove_node(tree, node_id)
local node = tree.nodes.by_id[node_id]
if node:has_children() then
for _, child_id in ipairs(node._child_ids) do
-- We might want to store the nodes and return them with the node itself?
-- We should _really_ not be doing this recursively, but it will work for now
remove_node(tree, child_id)
end
end
tree.nodes.by_id[node_id] = nil
return node
end
---@param node_id string
---@return NuiTreeNode
function Tree:remove_node(node_id)
local node = remove_node(self, node_id)
local parent_id = node._parent_id
if parent_id then
local parent_node = self.nodes.by_id[parent_id]
parent_node._child_ids = vim.tbl_filter(function(id)
return id ~= node_id
end, parent_node._child_ids)
else
self.nodes.root_ids = vim.tbl_filter(function(id)
return id ~= node_id
end, self.nodes.root_ids)
end
return node
end
---@param linenr_start number start line number (1-indexed)
function Tree:_prepare_content(linenr_start)
self._content.lines = {}
self._content.node_id_by_linenr = {}
self._content.linenr_by_node_id = {}
local current_linenr = 1
local function prepare(node_id, parent_node)
local node = self.nodes.by_id[node_id]
if not node then
return
end
local lines = self._.prepare_node(node, parent_node)
if lines then
if not is_type("table", lines) or lines.content then
lines = { lines }
end
local linenr = {}
for _, line in ipairs(lines) do
self._content.lines[current_linenr] = line
self._content.node_id_by_linenr[current_linenr + linenr_start - 1] = node:get_id()
linenr[1] = linenr[1] or (current_linenr + linenr_start - 1)
linenr[2] = (current_linenr + linenr_start - 1)
current_linenr = current_linenr + 1
end
self._content.linenr_by_node_id[node:get_id()] = linenr
end
if not node:has_children() or not node:is_expanded() then
return
end
for _, child_node_id in ipairs(node:get_child_ids()) do
prepare(child_node_id, node)
end
end
for _, node_id in ipairs(self.nodes.root_ids) do
prepare(node_id)
end
self._content.linenr = { linenr_start, current_linenr - 1 + linenr_start - 1 }
end
---@param linenr_start? number start line number (1-indexed)
function Tree:render(linenr_start)
if is_type("nil", self._.track_tree_linenr) then
self._.track_tree_linenr = is_type("number", linenr_start)
end
linenr_start = linenr_start or self._content.linenr[1] or 1
local prev_linenr = { self._content.linenr[1], self._content.linenr[2] }
self:_prepare_content(linenr_start)
local next_linenr = { self._content.linenr[1], self._content.linenr[2] }
_.set_buf_options(self.bufnr, { modifiable = true, readonly = false })
local buf_lines = vim.tbl_map(function(line)
if is_type("string", line) then
return line
end
return line:content()
end, self._content.lines)
if self._.track_tree_linenr then
u.clear_namespace(self.bufnr, self.ns_id, prev_linenr[1], prev_linenr[2])
-- if linenr_start was shifted downwards, clear the
-- previously rendered buffer lines above the tree.
clear_buf_lines(self.bufnr, {
math.min(next_linenr[1], prev_linenr[1] or next_linenr[1]),
prev_linenr[1] and next_linenr[1] - 1 or 0,
})
-- for initial render, start inserting the tree in a single buffer line.
local content_linenr_range = {
next_linenr[1],
next_linenr[1],
}
-- for subsequent renders, replace the buffer lines from previous tree.
if prev_linenr[1] then
content_linenr_range[2] = prev_linenr[2] < next_linenr[2] and math.min(next_linenr[2], prev_linenr[2])
or math.max(next_linenr[2], prev_linenr[2])
end
vim.api.nvim_buf_set_lines(self.bufnr, content_linenr_range[1] - 1, content_linenr_range[2], false, buf_lines)
else
u.clear_namespace(self.bufnr, self.ns_id)
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, buf_lines)
end
for i, line in ipairs(self._content.lines) do
if not is_type("string", line) then
line:highlight(self.bufnr, self.ns_id, i + linenr_start - 1)
end
end
_.set_buf_options(self.bufnr, { modifiable = false, readonly = true })
end
---@alias NuiTree.constructor fun(options: table): NuiTree
---@type NuiTree|NuiTree.constructor
local NuiTree = Tree
return NuiTree