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
+134
View File
@@ -1,3 +1,6 @@
local Item = require("ow.dap.item")
local log = require("ow.log")
---@class ow.dap.hover.Node
---@field lang string
---@field item ow.dap.Item
@@ -23,6 +26,104 @@ function Node.new(item, parent, lang)
}, Node)
end
---@return integer
function Node:size()
local count = 1
if self.is_expanded and self.children then
for _, child in ipairs(self.children) do
count = count + child:size()
end
end
return count
end
---@param n integer
---@return ow.dap.hover.Node?
function Node:at(n)
if n < 1 then
return nil
end
local current = 0
---@param node ow.dap.hover.Node
local function search(node)
current = current + 1
if current == n then
return node
end
if node.is_expanded and node.children then
for _, child in ipairs(node.children) do
local found = search(child)
if found then
return found
end
end
end
end
return search(self)
end
---@param target ow.dap.hover.Node? if nil, returns index of self
---@return integer?
function Node:index_of(target)
target = target or self
local current = 0
local function search(node)
current = current + 1
if node == target then
return current
end
if node.is_expanded and node.children then
for _, child in ipairs(node.children) do
local found = search(child)
if found then
return found
end
end
end
end
return search(self)
end
---@param session dap.Session
function Node:expand_all(session)
if not self:is_expandable() then
return true
end
if not self.is_expanded then
local success = self:load_children(session)
if not success then
return false
end
self.is_expanded = true
end
for _, child in ipairs(self.children) do
local success = child:expand_all(session)
if not success then
return false
end
end
return true
end
function Node:collapse_all()
self.is_expanded = false
for _, child in ipairs(self.children) do
child:collapse_all()
end
end
---@return boolean
function Node:is_container()
return self.item.variablesReference and self.item.variablesReference > 0
@@ -133,4 +234,37 @@ function Node:format_into(content)
content:add_with_treesitter(text, self.lang)
end
---@async
---@param session dap.Session
---@return boolean
function Node:load_children(session)
if not self:is_container() or #self.children > 0 then
return true -- Already loaded or not a container
end
local err, resp = session:request("variables", {
variablesReference = self.item.variablesReference,
})
if err then
log.warning("Failed to get variables for %s: %s", self.item.name, err)
end
if err or not resp or #resp.variables == 0 then
return false
end
for i, var in ipairs(resp.variables) do
local item = Item.from_var(var)
local child = Node.new(item, self, self.lang)
child.is_last_child = (i == #resp.variables)
if item.name:match("^%d+$") then
item.name = "[" .. item.name .. "]"
end
table.insert(self.children, child)
end
return true
end
return Node
-212
View File
@@ -1,212 +0,0 @@
local Content = require("ow.dap.hover.content")
local Item = require("ow.dap.item")
local Node = require("ow.dap.hover.node")
local log = require("ow.log")
---@class ow.dap.hover.Tree
---@field lang string
---@field session dap.Session
---@field root ow.dap.hover.Node?
local Tree = {}
Tree.__index = Tree
---@param session dap.Session
---@return ow.dap.hover.Tree
function Tree.new(session)
return setmetatable({
lang = session.filetype,
session = session,
root = nil,
}, Tree)
end
---@async
---@param item ow.dap.Item
function Tree:build(item)
self.root = Node.new(item, nil, self.lang)
if self.root:is_container() then
self:load_children(self.root)
self.root.is_expanded = true
end
end
---@async
---@param node ow.dap.hover.Node
---@return boolean success
function Tree:load_children(node)
if not node:is_container() or #node.children > 0 then
return true -- Already loaded or not a container
end
local err, resp = self.session:request("variables", {
variablesReference = node.item.variablesReference,
})
if err then
log.warning("Failed to get variables for %s: %s", node.item.name, err)
end
if err or not resp or #resp.variables == 0 then
return false
end
for i, var in ipairs(resp.variables) do
local item = Item.from_var(var)
local child = Node.new(item, node, self.lang)
child.is_last_child = (i == #resp.variables)
if item.name:match("^%d+$") then
item.name = "[" .. item.name .. "]"
end
table.insert(node.children, child)
end
return true
end
---@async
---@return ow.dap.hover.Content
function Tree:render()
if not self.root then
return Content.new()
end
local content = Content.new()
self:render_subtree(self.root, content)
return content
end
---@async
---@param node ow.dap.hover.Node
---@param content ow.dap.hover.Content
function Tree:render_subtree(node, content)
node:format_into(content)
if node.is_expanded then
for _, child in ipairs(node.children) do
content:newline()
self:render_subtree(child, content)
end
end
end
---@param node ow.dap.hover.Node
---@return integer
function Tree:count_subtree_nodes(node)
local count = 1
if node.is_expanded then
for _, child in ipairs(node.children) do
count = count + self:count_subtree_nodes(child)
end
end
return count
end
---@param target_line integer
---@return ow.dap.hover.Node?
function Tree:get_node_at_line(target_line)
local current_line = 0
---@param node ow.dap.hover.Node
local function search(node)
current_line = current_line + 1
if current_line == target_line then
return node
end
if node.is_expanded and node.children then
for _, child in ipairs(node.children) do
local found = search(child)
if found then
return found
end
end
end
end
if self.root then
return search(self.root)
end
end
---@param target_node ow.dap.hover.Node
---@return integer?
function Tree:get_line_for_node(target_node)
local current_line = 0
---@param node ow.dap.hover.Node
local function search(node)
current_line = current_line + 1
if node == target_node then
return current_line
end
if node.is_expanded and node.children then
for _, child in ipairs(node.children) do
local found = search(child)
if found then
return found
end
end
end
return nil
end
if self.root then
return search(self.root)
end
end
---@async
---@param node ow.dap.hover.Node
---@return boolean success
function Tree:toggle_node(node)
if not node.is_expanded then
local success = self:load_children(node)
if not success then
return false
end
end
node.is_expanded = not node.is_expanded
return true
end
---@async
---@param node ow.dap.hover.Node
---@return boolean success
function Tree:expand_all_children(node)
if not node:is_expandable() then
return true
end
if not node.is_expanded then
local success = self:load_children(node)
if not success then
return false
end
node.is_expanded = true
end
for _, child in ipairs(node.children) do
local success = self:expand_all_children(child)
if not success then
return false
end
end
return true
end
---@param node ow.dap.hover.Node
function Tree:collapse_all_children(node)
node.is_expanded = false
for _, child in ipairs(node.children) do
self:collapse_all_children(child)
end
end
return Tree
+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