diff --git a/lua/ow/dap/hover/node.lua b/lua/ow/dap/hover/node.lua index 1e93ca7..d2c976c 100644 --- a/lua/ow/dap/hover/node.lua +++ b/lua/ow/dap/hover/node.lua @@ -26,6 +26,20 @@ function Node:is_container() or false end +---@return boolean +function Node:is_c_pointer() + return self:is_container() + and self.item.type:match( + "%*%s*[const%s]*[volatile%s]*[restrict%s]*$" + ) + ~= nil +end + +---@return boolean +function Node:is_c_null_pointer() + return self:is_c_pointer() and self.item.value:match("^0[xX]0*$") ~= nil +end + ---@return string function Node:get_tree_prefix() if not self.parent then @@ -60,11 +74,7 @@ end ---@return boolean function Node:is_c_pointer_child() - return self.parent - and self.parent.item.type:match( - "%*%s*[const%s]*[volatile%s]*[restrict%s]*$" - ) ~= nil - or false + return self.parent ~= nil and self.parent:is_c_pointer() end ---@return string @@ -90,6 +100,39 @@ function Node:format_c() ) end +---@return string +function Node:get_full_expression() + local parts = {} + local current = self + + while current do + table.insert(parts, 1, current.item.name) + current = current.parent + end + + if #parts <= 1 then + return parts[1] or "" + end + + local expr = parts[1] + for i = 2, #parts do + local part = parts[i] + if part:match("^%[.*%]$") then + expr = expr .. part + elseif part:match("^%*") then + expr = "(" .. expr .. ")" .. part + else + if expr:match("%*$") then + expr = expr .. part + else + expr = expr .. "." .. part + end + end + end + + return expr +end + ---@param session dap.Session ---@param content ow.dap.hover.Content function Node:format_into(session, content) diff --git a/lua/ow/dap/hover/tree.lua b/lua/ow/dap/hover/tree.lua index d4beb3a..6ea37a5 100644 --- a/lua/ow/dap/hover/tree.lua +++ b/lua/ow/dap/hover/tree.lua @@ -130,6 +130,34 @@ function Tree:get_node_at_line(target_line) 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 @@ -145,4 +173,45 @@ function Tree:toggle_node(node) return true end +---@async +---@param node ow.dap.hover.Node +---@return boolean success +function Tree:expand_all_children(node) + if not node:is_container() then + return true + end + + if + (self.session.filetype == "c" or self.session.filetype == "cpp") + and node:is_c_null_pointer() + 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 diff --git a/lua/ow/dap/hover/window.lua b/lua/ow/dap/hover/window.lua index a46e5c0..2689ad2 100644 --- a/lua/ow/dap/hover/window.lua +++ b/lua/ow/dap/hover/window.lua @@ -1,6 +1,30 @@ 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 max_width? integer @@ -121,12 +145,58 @@ function Window:show(content) end, }) + -- Toggle expand/collapse vim.keymap.set("n", "", function() - self:expand_at_cursor() + self:toggle_node() end, { buffer = self.bufnr, nowait = true }) vim.keymap.set("n", "", function() - self:expand_at_cursor() + self:toggle_node() + end, { buffer = self.bufnr, nowait = true }) + + vim.keymap.set("n", "", function() + self:toggle_node() + end, { buffer = self.bufnr, nowait = true }) + + -- Collapse + vim.keymap.set("n", "", function() + self:collapse_parent() + end, { buffer = self.bufnr, nowait = true }) + + vim.keymap.set("n", "", function() + self:collapse_parent() + end, { buffer = self.bufnr, nowait = true }) + + -- Tree operations + vim.keymap.set("n", "E", function() + self:expand_all_at_cursor() + end, { buffer = self.bufnr, nowait = true }) + + vim.keymap.set("n", "C", function() + self:collapse_all_at_cursor() + end, { buffer = self.bufnr, nowait = true }) + + -- Navigation + vim.keymap.set("n", "p", function() + self:goto_parent() + end, { buffer = self.bufnr, nowait = true }) + + -- Quick actions + vim.keymap.set("n", "q", function() + self:close() + end, { buffer = self.bufnr, nowait = true }) + + vim.keymap.set("n", "", function() + self:close() + end, { buffer = self.bufnr, nowait = true }) + + -- Yank operations + vim.keymap.set("n", "y", function() + self:yank_value() + end, { buffer = self.bufnr, nowait = true }) + + vim.keymap.set("n", "Y", function() + self:yank_expression() end, { buffer = self.bufnr, nowait = true }) end @@ -140,48 +210,70 @@ function Window:update_buffer(callback) vim.wo[self.winid].scrolloff = prev_scrolloff end -function Window:expand_at_cursor() +---@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 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:is_container() then + local info = self:get_current_node_info() + if not info:is_valid() or not info.node:is_container() then return end - local prev_node_count = self.tree:count_subtree_nodes(node) - - local success = self.tree:toggle_node(node) + local success = self.tree:toggle_node(info.node) if not success then return end - 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, - lnum - 1, - lnum - 1 + prev_node_count, - true, - lines - ) - end) - - content:apply_highlights(Window.NS_ID, self.bufnr, lnum - 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(), - }) + self:refresh_subtree( + info.node, + info.line_number, + info.subtree_count + ) end, debug.traceback) if not ok then @@ -190,4 +282,128 @@ function Window:expand_at_cursor() 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_container() 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 + +function Window:yank_expression() + if not self.tree then + return + end + + local info = self:get_current_node_info() + if not info:is_valid() then + return + end + + local expr = info.node:get_full_expression() + vim.fn.setreg('"', expr) + vim.fn.setreg("+", expr) +end + return Window