fix(dap.hover): remove tree and make nodes self-contained subtrees

This commit is contained in:
2025-09-30 20:40:56 +02:00
parent 39c1aac2a7
commit 968cf1cba5
4 changed files with 328 additions and 493 deletions
+164 -234
View File
@@ -1,39 +1,16 @@
local Content = require("ow.dap.hover.content")
local log = require("ow.log")
---@class ow.dap.hover.NodeInfo
---@field node ow.dap.hover.Node?
---@field line_number integer
---@field subtree_count integer
local NodeInfo = {}
NodeInfo.__index = NodeInfo
---@param node ow.dap.hover.Node?
---@param line_number integer
---@param subtree_count integer
---@return ow.dap.hover.NodeInfo
function NodeInfo.new(node, line_number, subtree_count)
return setmetatable({
node = node,
line_number = line_number,
subtree_count = subtree_count,
}, NodeInfo)
end
---@return boolean
function NodeInfo:is_valid()
return self.node ~= nil
end
---@class ow.dap.hover.Window
---@field NAMESPACE string
---@field NS_ID integer
---@field max_width? integer
---@field max_height? integer
---@field winid? integer
---@field bufnr? integer
---@field NS_ID integer
---@field augroup? integer
---@field tree ow.dap.hover.Tree?
---@field session dap.Session
---@field root ow.dap.hover.Node
local Window = {}
Window.__index = Window
@@ -42,8 +19,9 @@ Window.NS_ID = vim.api.nvim_create_namespace(Window.NAMESPACE)
local instance = nil
---@param session dap.Session
---@return ow.dap.hover.Window
function Window.get_instance()
function Window.get_instance(session)
if instance then
return instance
end
@@ -54,6 +32,8 @@ function Window.get_instance()
winid = nil,
bufnr = nil,
augroup = nil,
session = session,
root = nil,
}, Window)
return instance
@@ -73,7 +53,7 @@ function Window:close()
self.winid = nil
self.bufnr = nil
self.augroup = nil
self.tree = nil
self.root = nil
end
---@return integer
@@ -97,32 +77,173 @@ function Window:compute_height()
return math.min(self.max_height or text_height, text_height)
end
---@param content ow.dap.hover.Content
function Window:show(content)
---@param callback fun()
function Window:update_buffer(callback)
local prev_scrolloff = vim.wo[self.winid].scrolloff
vim.wo[self.winid].scrolloff = 0
vim.bo[self.bufnr].modifiable = true
callback()
vim.bo[self.bufnr].modifiable = false
vim.wo[self.winid].scrolloff = prev_scrolloff
end
---@async
---@param node ow.dap.hover.Node
---@param start_line integer 1-indexed line number
---@param line_count integer number of lines to replace
function Window:refresh_tree(node, start_line, line_count)
local content = Content.new()
node:format_into(content)
local lines = content:get_lines()
self:update_buffer(function()
vim.api.nvim_buf_set_lines(
self.bufnr,
start_line - 1,
start_line - 1 + line_count,
true,
lines
)
end)
content:apply_highlights(Window.NS_ID, self.bufnr, start_line - 1)
vim.api.nvim_win_set_config(self.winid, {
width = self:compute_width(),
})
vim.api.nvim_win_set_config(self.winid, {
height = self:compute_height(),
})
end
function Window:toggle_node()
coroutine.wrap(function()
local ok, err = xpcall(function()
local lnum = vim.api.nvim_win_get_cursor(self.winid)[1]
local node = self.root:at(lnum)
if not node or not node:is_expandable() then
return
end
if not node.is_expanded then
local success = node:load_children(self.session)
if not success then
return
end
end
local prev_size = node:size()
node.is_expanded = not node.is_expanded
self:refresh_tree(node, lnum, prev_size)
end, debug.traceback)
if not ok then
log.error("Expansion failed:\n%s", err)
end
end)()
end
function Window:collapse_parent()
coroutine.wrap(function()
local ok, err = xpcall(function()
if self:goto_parent() then
self:toggle_node()
end
end, debug.traceback)
if not ok then
log.error("Collapse failed:\n%s", err)
end
end)()
end
function Window:expand_all_at_cursor()
coroutine.wrap(function()
local ok, err = xpcall(function()
local lnum = vim.api.nvim_win_get_cursor(self.winid)[1]
local node = self.root:at(lnum)
if not node or not node:is_expandable() then
return
end
local prev_size = node:size()
node:expand_all(self.session)
self:refresh_tree(node, lnum, prev_size)
end, debug.traceback)
if not ok then
log.error("Expand all failed:\n%s", err)
end
end)()
end
function Window:collapse_all_at_cursor()
coroutine.wrap(function()
local ok, err = xpcall(function()
local lnum = vim.api.nvim_win_get_cursor(self.winid)[1]
local node = self.root:at(lnum)
if not node or not node:is_expandable() then
return
end
local prev_size = node:size()
node:collapse_all()
self:refresh_tree(node, lnum, prev_size)
end, debug.traceback)
if not ok then
log.error("Collapse all failed:\n%s", err)
end
end)()
end
---@return boolean success if parent line was found
function Window:goto_parent()
local lnum = vim.api.nvim_win_get_cursor(self.winid)[1]
local node = self.root:at(lnum)
if not node or not node.parent then
return false
end
local parent_line = self.root:index_of(node.parent)
if parent_line then
vim.api.nvim_win_set_cursor(self.winid, { parent_line, 0 })
return true
end
return false
end
function Window:yank_value()
local lnum = vim.api.nvim_win_get_cursor(self.winid)[1]
local node = self.root:at(lnum)
if not node then
return
end
vim.fn.setreg('"', node.item.value)
vim.fn.setreg("+", node.item.value)
end
---@async
---@param root ow.dap.hover.Node
function Window:show(root)
self.root = root
local prev_buf = vim.api.nvim_get_current_buf()
self.bufnr = vim.api.nvim_create_buf(false, true)
local lines = content:get_lines()
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, lines)
vim.bo[self.bufnr].modifiable = false
self.winid = vim.api.nvim_open_win(self.bufnr, false, {
relative = "cursor",
width = self:compute_width(),
width = 1,
height = 1,
row = 1,
col = 0,
border = "rounded",
style = "minimal",
hide = true,
})
vim.wo[self.winid].wrap = false
vim.api.nvim_win_set_config(self.winid, {
height = self:compute_height(),
hide = false,
})
content:apply_highlights(Window.NS_ID, self.bufnr, 0)
self:refresh_tree(self.root, 1, 1)
self.augroup =
vim.api.nvim_create_augroup(Window.NAMESPACE, { clear = true })
@@ -196,195 +317,4 @@ function Window:show(content)
end, { buffer = self.bufnr, nowait = true })
end
---@param callback fun()
function Window:update_buffer(callback)
local prev_scrolloff = vim.wo[self.winid].scrolloff
vim.wo[self.winid].scrolloff = 0
vim.bo[self.bufnr].modifiable = true
callback()
vim.bo[self.bufnr].modifiable = false
vim.wo[self.winid].scrolloff = prev_scrolloff
end
---@return ow.dap.hover.NodeInfo
function Window:get_current_node_info()
local lnum = vim.api.nvim_win_get_cursor(self.winid)[1]
local node = self.tree:get_node_at_line(lnum)
if not node then
return NodeInfo.new(nil, lnum, 0)
end
local subtree_count = self.tree:count_subtree_nodes(node)
return NodeInfo.new(node, lnum, subtree_count)
end
---@async
---@param node ow.dap.hover.Node
---@param start_line integer 1-indexed line number
---@param line_count integer number of lines to replace
function Window:refresh_subtree(node, start_line, line_count)
local content = Content.new()
self.tree:render_subtree(node, content)
local lines = content:get_lines()
self:update_buffer(function()
vim.api.nvim_buf_set_lines(
self.bufnr,
start_line - 1,
start_line - 1 + line_count,
true,
lines
)
end)
content:apply_highlights(Window.NS_ID, self.bufnr, start_line - 1)
vim.api.nvim_win_set_config(self.winid, {
width = self:compute_width(),
})
vim.api.nvim_win_set_config(self.winid, {
height = self:compute_height(),
})
end
function Window:toggle_node()
if not self.tree then
return
end
coroutine.wrap(function()
local ok, err = xpcall(function()
local info = self:get_current_node_info()
if not info:is_valid() or not info.node:is_expandable() then
return
end
local success = self.tree:toggle_node(info.node)
if not success then
return
end
self:refresh_subtree(
info.node,
info.line_number,
info.subtree_count
)
end, debug.traceback)
if not ok then
log.error("Expansion failed:\n%s", err)
end
end)()
end
function Window:collapse_parent()
if not self.tree then
return
end
coroutine.wrap(function()
local ok, err = xpcall(function()
if self:goto_parent() then
self:toggle_node()
end
end, debug.traceback)
if not ok then
log.error("Collapse failed:\n%s", err)
end
end)()
end
function Window:expand_all_at_cursor()
if not self.tree then
return
end
coroutine.wrap(function()
local ok, err = xpcall(function()
local info = self:get_current_node_info()
if not info:is_valid() or not info.node:is_expandable() then
return
end
local success = self.tree:expand_all_children(info.node)
if not success then
return
end
self:refresh_subtree(
info.node,
info.line_number,
info.subtree_count
)
end, debug.traceback)
if not ok then
log.error("Expand all failed:\n%s", err)
end
end)()
end
function Window:collapse_all_at_cursor()
if not self.tree then
return
end
coroutine.wrap(function()
local ok, err = xpcall(function()
local info = self:get_current_node_info()
if not info:is_valid() or not info.node:is_container() then
return
end
self.tree:collapse_all_children(info.node)
self:refresh_subtree(
info.node,
info.line_number,
info.subtree_count
)
end, debug.traceback)
if not ok then
log.error("Collapse all failed:\n%s", err)
end
end)()
end
---@return boolean success if parent line was found
function Window:goto_parent()
if not self.tree then
return false
end
local lnum = vim.api.nvim_win_get_cursor(self.winid)[1]
local node = self.tree:get_node_at_line(lnum)
if not node or not node.parent then
return false
end
local parent_line = self.tree:get_line_for_node(node.parent)
if parent_line then
vim.api.nvim_win_set_cursor(self.winid, { parent_line, 0 })
return true
end
return false
end
function Window:yank_value()
if not self.tree then
return
end
local info = self:get_current_node_info()
if not info:is_valid() then
return
end
vim.fn.setreg('"', info.node.item.value)
vim.fn.setreg("+", info.node.item.value)
end
return Window