From 80a563dc9ce279f137134eefe1c75eb2bfbd426f Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Fri, 26 Sep 2025 22:56:58 +0200 Subject: [PATCH] feat(dap.hover): update buffer incrementally on expand/collapse --- lua/ow/dap/hover.lua | 62 ++++++++++++++++++---------- lua/ow/dap/hover/content.lua | 7 ++-- lua/ow/dap/hover/tree.lua | 80 ++++++++++++++++++++++++------------ 3 files changed, 98 insertions(+), 51 deletions(-) diff --git a/lua/ow/dap/hover.lua b/lua/ow/dap/hover.lua index 0257543..af45206 100644 --- a/lua/ow/dap/hover.lua +++ b/lua/ow/dap/hover.lua @@ -1,4 +1,5 @@ -- DAP hover implementation with tree-based display +local Content = require("ow.dap.hover.content") local Item = require("ow.dap.item") local Tree = require("ow.dap.hover.tree") local log = require("ow.log") @@ -45,6 +46,7 @@ function Window.show(lines, content) local orig_buf = vim.api.nvim_get_current_buf() local buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_set_option_value("modifiable", false, { buf = buf }) -- Create window (initially hidden for size calculation) local win = vim.api.nvim_open_win(buf, false, { @@ -66,7 +68,7 @@ function Window.show(lines, content) }) -- Apply syntax highlighting - content:apply_highlights(Window.ns_id, buf) + content:apply_highlights(Window.ns_id, buf, 0) -- Store window reference Window.current_win = win @@ -94,6 +96,17 @@ function Window.show(lines, content) end, { buffer = buf, nowait = true }) end +---@param buf integer +---@param callback fun() +function Window.update_buffer(buf, callback) + local prev_scrolloff = vim.wo[Window.current_win].scrolloff + vim.wo[Window.current_win].scrolloff = 0 + vim.bo[buf].modifiable = true + callback() + vim.bo[buf].modifiable = false + vim.wo[Window.current_win].scrolloff = prev_scrolloff +end + ---Expand/collapse item at cursor position ---@param buf integer function Window.expand_at_cursor(buf) @@ -105,36 +118,40 @@ function Window.expand_at_cursor(buf) coroutine.wrap(function() local ok, err = xpcall(function() -- Toggle expansion - local cursor = vim.api.nvim_win_get_cursor(Window.current_win) - local success = Window.tree:toggle_at_line(cursor[1]) + local lnum = vim.api.nvim_win_get_cursor(Window.current_win)[1] + local node = Window.tree:get_node_at_line(lnum) + if not node or not node:is_container() then + return + end + + local prev_node_count = Window.tree:count_subtree_nodes(node) + + local success = Window.tree:toggle_node(node) if not success then return end - local content = Window.tree:render() + local content = Content.new() + Window.tree:render_subtree(node, content) local lines = content:get_lines() - -- TODO: possible to only update the affected lines? - -- Update buffer content - vim.api.nvim_set_option_value( - "scrolloff", - 0, - { win = Window.current_win } - ) - vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) - vim.api.nvim_set_option_value( - "scrolloff", - vim.o.scrolloff, - { win = Window.current_win } - ) + Window.update_buffer(buf, function() + vim.api.nvim_buf_set_lines( + buf, + lnum - 1, + lnum - 1 + prev_node_count, + true, + lines + ) + end) - -- Re-apply highlights - vim.api.nvim_buf_clear_namespace(buf, -1, 0, -1) -- Clear old highlights - content:apply_highlights(Window.ns_id, buf) + -- Apply highlights + content:apply_highlights(Window.ns_id, buf, lnum - 1) -- Adjust window size + local all_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, true) vim.api.nvim_win_set_config(Window.current_win, { - width = Window.compute_width(lines), + width = Window.compute_width(all_lines), }) local text_height = vim.api.nvim_win_text_height(Window.current_win, {}).all @@ -152,7 +169,7 @@ function Window.expand_at_cursor(buf) end)() end ----Main hover entry point (async) +---Main hover entry point ---@async ---@param expr string Expression to evaluate ---@param session dap.Session DAP session @@ -207,6 +224,7 @@ local function hover_eval( end ---Public hover function +---@async local function hover_async() -- Check if hover window is already open - focus it instead if Window.current_win and vim.api.nvim_win_is_valid(Window.current_win) then diff --git a/lua/ow/dap/hover/content.lua b/lua/ow/dap/hover/content.lua index 3121068..20358ca 100644 --- a/lua/ow/dap/hover/content.lua +++ b/lua/ow/dap/hover/content.lua @@ -125,14 +125,15 @@ end ---Apply highlights to a buffer ---@param ns_id integer ---@param buf integer Buffer handle -function Content:apply_highlights(ns_id, buf) +---@param row_offset integer +function Content:apply_highlights(ns_id, buf, row_offset) for _, highlight in ipairs(self.highlights) do vim.hl.range( buf, ns_id, highlight.group, - { highlight.start_row, highlight.start_col }, - { highlight.end_row, highlight.end_col } + { row_offset + highlight.start_row, highlight.start_col }, + { row_offset + highlight.end_row, highlight.end_col } ) end end diff --git a/lua/ow/dap/hover/tree.lua b/lua/ow/dap/hover/tree.lua index a2bdeb7..124218b 100644 --- a/lua/ow/dap/hover/tree.lua +++ b/lua/ow/dap/hover/tree.lua @@ -4,9 +4,7 @@ local Node = require("ow.dap.hover.node") ---@class ow.dap.hover.Tree ---@field session dap.Session ----@field root_node ow.dap.hover.Node? ----@field line_to_node table Map line numbers to nodes ----@field extmark_to_node table Map extmark IDs to nodes +---@field root ow.dap.hover.Node? local Tree = {} Tree.__index = Tree @@ -22,20 +20,19 @@ function Tree.new(session) }, Tree) end ----Build the tree from a DAP item (async) +---Build the tree from a DAP item ---@async ---@param item ow.dap.Item Root item to build tree from ----@return ow.dap.hover.Node function Tree:build(item) - local root = Node.new(item, nil) - self.root_node = root + self.root = Node.new(item, nil) - -- For now, start with everything collapsed - -- Later we can add logic to expand first level by default - return root + if self.root:is_container() then + self:load_children(self.root) + self.root.is_expanded = true + end end ----Load children for a node (async) +---Load children for a node ---@async ---@param node ow.dap.hover.Node ---@return boolean success Whether loading succeeded @@ -80,14 +77,13 @@ end ---@async ---@return ow.dap.hover.Content function Tree:render() - if not self.root_node then + if not self.root then return Content.new() end local content = Content.new() - self.line_to_node = {} - self:render_subtree(self.root_node, content) + self:render_subtree(self.root, content) return content end @@ -96,9 +92,6 @@ end ---@param node ow.dap.hover.Node ---@param content ow.dap.hover.Content function Tree:render_subtree(node, content) - -- Store line mapping - self.line_to_node[content:current_line()] = node - -- Format this node node:format_into(self.session, content) @@ -111,18 +104,53 @@ function Tree:render_subtree(node, content) end end ----Toggle expansion state of node at given line ----@async ----@param line_number integer ----@return boolean success Whether toggle succeeded -function Tree:toggle_at_line(line_number) - local node = self.line_to_node[line_number] - if not node or not node:is_container() then - return false +---@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 + +---Toggle expansion state of node at given line +---@async +---@param node ow.dap.hover.Node +---@return boolean success Whether toggle succeeded +function Tree:toggle_node(node) if not node.is_expanded then - -- Expanding: load children if needed local success = self:load_children(node) if not success then return false