feat(dap.hover): update buffer incrementally on expand/collapse

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