diff --git a/lua/ow/dap/hover.lua b/lua/ow/dap/hover.lua new file mode 100644 index 0000000..8c0d773 --- /dev/null +++ b/lua/ow/dap/hover.lua @@ -0,0 +1,314 @@ +-- DAP hover implementation with tree-based display +local Item = require("ow.dap.item") +local Tree = require("ow.dap.hover.tree") +local log = require("ow.log") + +---@class ow.dap.hover.Window +---@field current_win? integer Currently active hover window ID +---@field ns_id integer Namespace for extmarks +---@field tree ow.dap.hover.Tree? Current tree formatter +local Window = {} + +Window.MAX_WIDTH = nil +Window.MAX_HEIGHT = nil +Window.ns_id = vim.api.nvim_create_namespace("ow.dap.hover") + +---Close any existing hover window +function Window.close() + if Window.current_win and vim.api.nvim_win_is_valid(Window.current_win) then + vim.api.nvim_win_close(Window.current_win, true) + end + Window.current_win = nil + Window.tree = nil +end + +---@param lines string[] +---@return integer +function Window.compute_width(lines) + local max_width = 1 + for _, line in ipairs(lines) do + if Window.MAX_WIDTH and #line >= Window.MAX_WIDTH then + max_width = Window.MAX_WIDTH + break + end + max_width = math.max(max_width, #line) + end + + return max_width +end + +---Create and display hover window with tree content +---@param session dap.Session +---@param item ow.dap.Item +---@param lines string[] +---@param content ow.dap.hover.Content +function Window.show(session, item, lines, content) + -- Create buffer + 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) + + -- Create window (initially hidden for size calculation) + local win = vim.api.nvim_open_win(buf, false, { + relative = "cursor", + width = Window.compute_width(lines), + height = 1, + row = 1, + col = 0, + border = "rounded", + style = "minimal", + hide = true, + }) + + -- Calculate and apply final height + local text_height = vim.api.nvim_win_text_height(win, {}).all + vim.api.nvim_win_set_config(win, { + height = math.min(Window.MAX_HEIGHT or text_height, text_height), + hide = false, + }) + + -- Apply syntax highlighting + content:apply_highlights(Window.ns_id, buf) + + -- Store window reference + Window.current_win = win + + -- Set up auto-close behavior + vim.api.nvim_create_autocmd({ "CursorMoved", "InsertEnter" }, { + buffer = orig_buf, + once = true, + callback = Window.close, + }) + + vim.api.nvim_create_autocmd("WinLeave", { + buffer = buf, + once = true, + callback = Window.close, + }) + + -- Set up expansion keymaps + vim.keymap.set("n", "", function() + Window.expand_at_cursor(buf) + end, { buffer = buf, nowait = true }) + + vim.keymap.set("n", "", function() + Window.expand_at_cursor(buf) + end, { buffer = buf, nowait = true }) +end + +---Expand/collapse item at cursor position +---@param buf integer +function Window.expand_at_cursor(buf) + if not Window.tree then + return + end + + -- Re-render the tree + 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] - 1) + if not success then + return + end + + local content = Window.tree:render() + 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 } + ) + + -- Re-apply highlights + vim.api.nvim_buf_clear_namespace(buf, -1, 0, -1) -- Clear old highlights + content:apply_highlights(Window.ns_id, buf) + + -- Adjust window size + vim.api.nvim_win_set_config(Window.current_win, { + width = Window.compute_width(lines), + }) + local text_height = + vim.api.nvim_win_text_height(Window.current_win, {}).all + vim.api.nvim_win_set_config(Window.current_win, { + height = math.min( + Window.MAX_HEIGHT or text_height, + text_height + ), + }) + end, debug.traceback) + + if not ok then + log.error("Expansion failed:\n%s", err) + end + end)() +end + +---Main hover entry point (async) +---@async +---@param expr string Expression to evaluate +---@param session dap.Session DAP session +---@param frame_id number Current frame ID +---@param line_nr integer Line number for context +---@param col_nr integer Column number for context +---@param current_file string Current file path +local function hover_eval( + expr, + session, + frame_id, + line_nr, + col_nr, + current_file +) + -- Close existing hover window + Window.close() + + -- Evaluate expression + local eval_request = { + expression = expr, + frameId = frame_id, + context = "hover", + line = line_nr, + column = col_nr, + source = { + path = current_file, + }, + } + + local err, resp = session:request("evaluate", eval_request) + if err or not resp then + log.warning("Failed to evaluate '%s': %s", expr, err) + return + end + + -- Create item and tree formatter + local item = + Item.new(expr, resp.type, resp.result, resp.variablesReference, 0) + local tree = Tree.new(session) + + -- Build and render tree + tree:build(item) + local content = tree:render() + local lines = content:get_lines() + + -- Store formatter for expansion + Window.tree = tree + + -- Show hover window + Window.show(session, item, lines, content) +end + +---Public hover function +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 + vim.api.nvim_set_current_win(Window.current_win) + return + end + + local dap = require("dap") + local session = dap.session() + if not session then + return + end + + local capabilities = session.capabilities or {} + local supports_hover = capabilities.supportsEvaluateForHovers + if not supports_hover then + log.warning("Hover is not supported by this adapter") + return + end + + local cursor_pos = vim.api.nvim_win_get_cursor(0) + local line_nr = cursor_pos[1] -- nvim-dap sets linesStartAt1=true + local col_nr = cursor_pos[2] + 1 -- nvim-dap sets columnsStartAt1=true + local current_file = vim.api.nvim_buf_get_name(0) + + -- Get expression under cursor + local expr + local mode = vim.api.nvim_get_mode() + if mode.mode == "v" then + -- Visual mode selection + local start_pos = vim.fn.getpos("v") + local end_pos = vim.fn.getpos(".") + + local start_row, start_col = start_pos[2], start_pos[3] + local end_row, end_col = end_pos[2], end_pos[3] + + if start_row == end_row and end_col < start_col then + start_col, end_col = end_col, start_col + elseif end_row < start_row then + start_row, end_row = end_row, start_row + start_col, end_col = end_col, start_col + end + + local lines = vim.api.nvim_buf_get_text( + 0, + start_row - 1, + start_col - 1, + end_row - 1, + end_col, + {} + ) + expr = table.concat(lines, "\n") + + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes("", true, false, true), + "n", + false + ) + else + expr = vim.fn.expand("") + end + + if expr == "" then + return + end + + -- Get thread and frame information + local thread_id + do + local err, resp = session:request("threads", nil) + if err or not resp or #resp.threads == 0 then + log.warning("Failed to get threads: %s", err) + return + end + thread_id = resp.threads[1].id + end + + local frame_id + do + local err, resp = + session:request("stackTrace", { threadId = thread_id }) + if err or not resp or #resp.stackFrames == 0 then + log.warning("Failed to get stack trace: %s", err) + return + end + frame_id = resp.stackFrames[1].id + end + + -- Evaluate and display hover + hover_eval(expr, session, frame_id, line_nr, col_nr, current_file) +end + +---Wrapped hover function with error handling +local function hover() + coroutine.wrap(function() + local ok, err = xpcall(hover_async, debug.traceback) + if not ok then + log.error("Hover failed:\n%s", err) + end + end)() +end + +return hover diff --git a/lua/ow/dap/hover/content.lua b/lua/ow/dap/hover/content.lua new file mode 100644 index 0000000..292ae4f --- /dev/null +++ b/lua/ow/dap/hover/content.lua @@ -0,0 +1,146 @@ +-- Highlighted text content builder for DAP tree display +local log = require("ow.log") +---@class ow.dap.hover.content.Capture +---@field start_col integer +---@field end_col integer +---@field text string +---@field group string +---@field priority integer + +---@class ow.dap.hover.Highlight +---@field group string Highlight group name +---@field start_col integer Start column (0-indexed) +---@field end_col integer End column (0-indexed) + +---@class ow.dap.hover.Content +---@field text string The complete text content +---@field highlights ow.dap.hover.Highlight[] List of highlights to apply +---@field _current_col integer Current column position (for building) +local Content = {} +Content.__index = Content + +---Create new highlighted content +---@return ow.dap.hover.Content +function Content.new() + return setmetatable({ + text = "", + highlights = {}, + _current_col = 0, + }, Content) +end + +---Add text with optional highlighting +---@param text string Text to add +---@param highlight_group? string Optional highlight group +function Content:add(text, highlight_group) + local start_col = self._current_col + local end_col = start_col + #text + + self.text = self.text .. text + self._current_col = end_col + + if highlight_group then + table.insert(self.highlights, { + group = highlight_group, + start_col = start_col, + end_col = end_col, + }) + end +end + +---Add text with tree-sitter syntax highlighting +---@param text string The text to highlight +---@param lang string Language for tree-sitter +function Content:add_with_treesitter(text, lang) + local start_col = self._current_col + + -- First, just add the text normally + self:add(text) + + -- Then apply tree-sitter highlights on top + local ok, parser = pcall(vim.treesitter.get_string_parser, text, lang) + if not ok or not parser then + return + end + + local tree = parser:parse()[1] + if not tree then + return + end + + local query = vim.treesitter.query.get(lang, "highlights") + if not query then + return + end + + -- Add highlights for all captures (overlapping is fine) + for id, node in query:iter_captures(tree:root(), text, 0, -1) do + local capture_name = query.captures[id] + local start_row, start_col_rel, end_row, end_col_rel = node:range() + + -- TODO: keep track of text as lines instead, so we can handle multiline + -- highlights + if start_row == end_row then -- Single line only + -- Convert to absolute column positions + local abs_start_col = start_col + start_col_rel + local abs_end_col = start_col + end_col_rel + + -- Add the highlight + table.insert(self.highlights, { + group = "@" .. capture_name, + start_col = abs_start_col, + end_col = abs_end_col, + }) + end + end +end + +---Add a newline and reset column tracking +function Content:newline() + self:add("\n") + self._current_col = 0 +end + +---Get the lines as a table +---@return string[] +function Content:get_lines() + return vim.split(self.text, "\n", { trimempty = true }) +end + +---Apply highlights to a buffer +---@param ns_id integer +---@param buf integer Buffer handle +---@param line_offset? integer Line offset to apply highlights at (default 0) +function Content:apply_highlights(ns_id, buf, line_offset) + line_offset = line_offset or 0 + local lines = self:get_lines() + local current_line = 0 + local line_start_col = 0 + + for _, highlight in ipairs(self.highlights) do + -- Find which line this highlight belongs to + while + current_line < #lines - 1 + and highlight.start_col + >= line_start_col + #lines[current_line + 1] + 1 + do + line_start_col = line_start_col + #lines[current_line + 1] + 1 -- +1 for newline + current_line = current_line + 1 + end + + -- Calculate column positions relative to line start + local start_col = highlight.start_col - line_start_col + local end_col = highlight.end_col - line_start_col + + -- Apply highlight + vim.hl.range( + buf, + ns_id, + highlight.group, + { line_offset + current_line, start_col }, + { line_offset + current_line, end_col } + ) + end +end + +return Content diff --git a/lua/ow/dap/hover/node.lua b/lua/ow/dap/hover/node.lua new file mode 100644 index 0000000..e3ca745 --- /dev/null +++ b/lua/ow/dap/hover/node.lua @@ -0,0 +1,141 @@ +-- Tree node representation for DAP variables +local Content = require("ow.dap.hover.content") +local log = require("ow.log") + +---@class ow.dap.hover.Node +---@field item ow.dap.Item The DAP item this node represents +---@field parent ow.dap.hover.Node? Parent node +---@field children ow.dap.hover.Node[] Child nodes +---@field is_expanded boolean Whether this node is expanded +---@field line_number integer Buffer line number where this node is displayed +---@field is_last_child boolean Whether this is the last child of its parent +local Node = {} +Node.__index = Node + +---Create a new tree node +---@param item ow.dap.Item +---@param parent ow.dap.hover.Node? +---@return ow.dap.hover.Node +function Node.new(item, parent) + return setmetatable({ + item = item, + parent = parent, + children = {}, + is_expanded = false, + line_number = -1, + is_last_child = false, + }, Node) +end + +---Check if this node represents a container (struct/array) +---@return boolean +function Node:is_container() + return self.item.variablesReference and self.item.variablesReference > 0 + or false +end + +---Get the tree prefix for this node (├─, └─, │, etc.) +---@return string +function Node:get_tree_prefix() + if not self.parent then + return "" -- Root node has no prefix + end + + local prefix = "" + + -- Walk up the tree to build the prefix + local node = self.parent + while node and node.parent do + if node.is_last_child then + prefix = " " .. prefix + else + prefix = "│ " .. prefix + end + node = node.parent + end + + -- Add the final branch character + if self.is_last_child then + prefix = prefix .. "└─ " + else + prefix = prefix .. "├─ " + end + + return prefix +end + +---@return boolean +function Node:is_c_array_element() + return self.item.name:match("^%[?%d+%]?$") ~= nil +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 +end + +---@return string +function Node:format_c_expression() + if self:is_c_array_element() then + return string.format( + "%s = (%s) %s", + self.item.name, + self.item.type, + self.item.value + ) + end + + if self:is_c_pointer_child() then + return string.format("*%s = %s", self.parent.item.name, self.item.value) + end + + return string.format( + "%s %s = %s", + self.item.type, + self.item.name, + self.item.value + ) +end + +---Format this node as highlighted content +---@param session dap.Session DAP session for making requests +---@return ow.dap.hover.Content +function Node:format(session) + local content = Content.new() + + -- Add expansion marker for containers + if self:is_container() then + local marker = self.is_expanded and "-" or "+" + content:add(marker .. " ", "@comment") + else + content:add(" ") + end + + -- Add tree prefix + local tree_prefix = self:get_tree_prefix() + if tree_prefix ~= "" then + content:add(tree_prefix, "@comment") + end + + local stmt + if session.filetype == "c" or session.filetype == "cpp" then + stmt = self:format_c_expression() + else + error( + string.format("Formatting for %s not implemented", session.filetype) + ) + end + content:add_with_treesitter(stmt, session.filetype) + + if self.item.value == "" then + content:add("...", "@comment") + end + + return content +end + +return Node diff --git a/lua/ow/dap/hover/tree.lua b/lua/ow/dap/hover/tree.lua new file mode 100644 index 0000000..47f9f1c --- /dev/null +++ b/lua/ow/dap/hover/tree.lua @@ -0,0 +1,156 @@ +-- Tree-based DAP variable formatter with expand/collapse support +local Content = require("ow.dap.hover.content") +local Node = require("ow.dap.hover.node") +local log = require("ow.log") + +---@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 +local Tree = {} +Tree.__index = Tree + +---Create a new tree formatter +---@param session dap.Session +---@return ow.dap.hover.Tree +function Tree.new(session) + return setmetatable({ + session = session, + root_node = nil, + line_to_node = {}, + extmark_to_node = {}, + }, Tree) +end + +---Build the tree from a DAP item (async) +---@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 + + -- For now, start with everything collapsed + -- Later we can add logic to expand first level by default + return root +end + +---Load children for a node (async) +---@async +---@param node ow.dap.hover.Node +---@return boolean success Whether loading succeeded +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 or not resp or #resp.variables == 0 then + return false + end + + -- Create child nodes + for i, var in ipairs(resp.variables) do + local child_item = { + name = var.name, + type = var.type, + value = var.value, + variablesReference = var.variablesReference, + depth = node.item.depth + 1, + } + + local child = Node.new(child_item, node) + child.is_last_child = (i == #resp.variables) + + -- Format array indices properly + if child_item.name:match("^%d+$") then + child_item.name = "[" .. child_item.name .. "]" + end + + table.insert(node.children, child) + end + + return true +end + +---Render the tree to highlighted content +---@async +---@return ow.dap.hover.Content +function Tree:render() + if not self.root_node then + return Content.new() + end + + local content = Content.new() + self.line_to_node = {} + + self:render_node(self.root_node, content, 0) + return content +end + +---Render a single node and its expanded children +---@async +---@param node ow.dap.hover.Node +---@param content ow.dap.hover.Content +---@param line_number integer Current line number +---@return integer new_line_number Updated line number after rendering +function Tree:render_node(node, content, line_number) + -- Store line mapping + node.line_number = line_number + self.line_to_node[line_number] = node + + -- Format this node + local node_content = node:format(self.session) + content.text = content.text .. node_content.text + + -- Copy highlights, adjusting for current position + local text_offset = #content.text - #node_content.text + for _, highlight in ipairs(node_content.highlights) do + table.insert(content.highlights, { + group = highlight.group, + start_col = highlight.start_col + text_offset, + end_col = highlight.end_col + text_offset, + }) + end + + content:newline() + line_number = line_number + 1 + + -- Render expanded children + if node.is_expanded then + for _, child in ipairs(node.children) do + line_number = self:render_node(child, content, line_number) + end + end + + return line_number +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 + end + + if not node.is_expanded then + -- Expanding: load children if needed + local success = self:load_children(node) + if not success then + return false + end + end + + -- Toggle state + node.is_expanded = not node.is_expanded + return true +end + +return Tree diff --git a/lua/ow/dap/item.lua b/lua/ow/dap/item.lua new file mode 100644 index 0000000..b1dc97e --- /dev/null +++ b/lua/ow/dap/item.lua @@ -0,0 +1,43 @@ +-- DAP variable item representation + +---@class ow.dap.Item +---@field name string +---@field type string +---@field value string +---@field variablesReference? number +---@field depth integer +local Item = {} +Item.__index = Item + +---Create a new item +---@param name string +---@param type string +---@param value string +---@param variablesReference? number +---@param depth integer +---@return ow.dap.Item +function Item.new(name, type, value, variablesReference, depth) + return setmetatable({ + name = name, + type = type, + value = value, + variablesReference = variablesReference, + depth = depth, + }, Item) +end + +---Create item from DAP variable +---@param var dap.Variable +---@param depth integer +---@return ow.dap.Item +function Item.from_var(var, depth) + return Item.new( + var.name, + var.type, + var.value, + var.variablesReference, + depth + ) +end + +return Item diff --git a/lua/ow/plugins/nvim-dap.lua b/lua/ow/plugins/nvim-dap.lua index f65dd2e..48dcfc5 100644 --- a/lua/ow/plugins/nvim-dap.lua +++ b/lua/ow/plugins/nvim-dap.lua @@ -1,515 +1,6 @@ -- https://github.com/mfussenegger/nvim-dap local log = require("ow.log") - ----@class Item ----@field name string ----@field type string ----@field value string ----@field variablesReference? number ----@field depth integer -local Item = {} -Item.__index = Item - ----@param name string ----@param type string ----@param value string ----@param variablesReference? number ----@param depth integer ----@return Item -function Item.new(name, type, value, variablesReference, depth) - return setmetatable({ - name = name, - type = type, - value = value, - variablesReference = variablesReference, - depth = depth, - }, Item) -end - ----@param var dap.Variable ----@param depth integer ----@return Item -function Item.from_var(var, depth) - return Item.new( - var.name, - var.type, - var.value, - var.variablesReference, - depth - ) -end - ----@class FormatResult ----@field success boolean ----@field error? dap.ErrorResponse ----@field value? string -local FormatResult = {} -FormatResult.__index = FormatResult - ----@param error? dap.ErrorResponse ----@param value? string ----@return FormatResult -function FormatResult.new(error, value) - return setmetatable({ - success = not error and true or false, - error = error, - value = value, - }, FormatResult) -end - ----@class BaseFormatter ----@field session dap.Session ----@field MAX_DEPTH integer ----@field INDENT string -local BaseFormatter = {} -BaseFormatter.__index = BaseFormatter - -BaseFormatter.MAX_DEPTH = 2 -BaseFormatter.INDENT = " " - ----@return BaseFormatter -function BaseFormatter:new(session) - return setmetatable({ session = session }, self) -end - ----@param item Item ----@return FormatResult ----@diagnostic disable-next-line: unused-local -function BaseFormatter:format(item) - return FormatResult.new(nil, item.value) -end - ----@param item Item ----@return boolean -function BaseFormatter:is_container(item) - return item.variablesReference and item.variablesReference > 0 or false -end - ----@param level integer ----@return string -function BaseFormatter:make_indent(level) - return string.rep(self.INDENT, level) -end - ----@class CFormatter: BaseFormatter -local CFormatter = {} -CFormatter.__index = CFormatter -setmetatable(CFormatter, BaseFormatter) - -CFormatter.MAX_ARR_ELEMENTS = 10 -CFormatter.ARRAY_ELEM_PFX = "" -CFormatter.STRUCT_FIELD_PFX = "." -CFormatter.PLACEHOLDER = "..." - ----@param item Item ----@return boolean -function CFormatter:is_pointer(item) - return item.type:match("%*%s*[const%s]*[volatile%s]*[restrict%s]*$") ~= nil -end - ----@param item Item ----@return boolean -function CFormatter:is_null_pointer(item) - return self:is_pointer(item) and item.value:match("^0x0+$") -end - ----@async ----@param item Item ----@return FormatResult -function CFormatter:format_pointer(item) - if self:is_null_pointer(item) then - return FormatResult.new(nil, "nullptr") - elseif not item.value:match("^0x%x+$") then - -- Value contains more than just a pointer address - -- (e.g., "0x12345 \"hello\"" for char*) - -- Remove the leading address to show just the meaningful content - return FormatResult.new(nil, item.value:gsub("^0x%x+%s*", "")) - elseif - item.depth == CFormatter.MAX_DEPTH - or not self:is_container(item) - then - return FormatResult.new(nil, item.value) - end - - local err, resp = self.session:request("variables", { - variablesReference = item.variablesReference, - }) - if err or not resp then - return FormatResult.new(err) - end - - if #resp.variables == 0 then - return FormatResult.new(nil, item.value) - elseif #resp.variables == 1 then - local var = resp.variables[1] - local inner = Item.from_var(var, item.depth) - return self:format_value(inner) - else - return self:format_container(item, resp.variables) - end -end - ----@async ----@param item Item ----@param vars dap.Variable[]? ----@return FormatResult -function CFormatter:format_container(item, vars) - if item.depth >= CFormatter.MAX_DEPTH then - return FormatResult.new( - nil, - vim.trim(item.value) ~= "" and item.value - or string.format("{%s}", CFormatter.PLACEHOLDER) - ) - end - - if not vars then - local err, resp = self.session:request("variables", { - variablesReference = item.variablesReference, - }) - if err or not resp then - return FormatResult.new(err) - end - - vars = resp.variables - end - - if #vars == 0 then - return FormatResult.new(nil, item.value) - end - - ---@type Item[] - local items = {} - for _, var in ipairs(vars) do - table.insert(items, Item.from_var(var, item.depth + 1)) - end - - local is_array = false - local pfx - if items[1].name:match("^%[?%d+%]?$") then - is_array = true - pfx = CFormatter.ARRAY_ELEM_PFX - else - pfx = CFormatter.STRUCT_FIELD_PFX - end - - local indent = self:make_indent(items[1].depth) - local content = "{\n" - - for i, inner in ipairs(items) do - if is_array and i > CFormatter.MAX_ARR_ELEMENTS then - content = content - .. string.format("%s%s\n", indent, CFormatter.PLACEHOLDER) - break - end - - if is_array and inner.name:match("^%d+$") then - inner.name = "[" .. inner.name .. "]" - end - - local res = self:format(inner) - if not res.success then - return res - end - - content = content .. string.format("%s%s%s,\n", indent, pfx, res.value) - end - - content = content .. self:make_indent(item.depth) .. "}" - return FormatResult.new(nil, content) -end - ----@async ----@param item Item ----@return FormatResult -function CFormatter:format_value(item) - if self:is_pointer(item) then - return self:format_pointer(item) - elseif self:is_container(item) then - return self:format_container(item) - else - return FormatResult.new(nil, item.value) - end -end - ----@async ----@param item Item ----@return FormatResult -function CFormatter:format(item) - local res = self:format_value(item) - if not res.success then - return res - end - - if self:is_container(item) and not self:is_null_pointer(item) then - local value = string.format( - "%s(%s) %s", - item.name ~= "" and string.format("%s = ", item.name) or "", - item.type, - res.value - ) - return FormatResult.new(nil, value) - else - local value = string.format("%s = %s", item.name, res.value) - return FormatResult.new(nil, value) - end -end - ----@class HoverState ----@field MAX_WIDTH integer ----@field MAX_HEIGHT integer ----@field current_win? integer ----@field session dap.Session ----@field frame_id number ----@field line_nr integer ----@field col_nr integer ----@field current_file string ----@field lines string[] ----@field depth integer -local HoverState = {} -HoverState.__index = HoverState - -HoverState.MAX_WIDTH = 80 -HoverState.MAX_HEIGHT = 20 - -function HoverState.new(session, frame_id, line_nr, col_nr, current_file) - return setmetatable({ - session = session, - frame_id = frame_id, - line_nr = line_nr, - col_nr = col_nr, - current_file = current_file, - lines = {}, - depth = 0, - }, HoverState) -end - -function HoverState.close() - if - HoverState.current_win - and vim.api.nvim_win_is_valid(HoverState.current_win) - then - vim.api.nvim_win_close(HoverState.current_win, true) - end - HoverState.current_win = nil -end - ----@param content string -function HoverState:render(content) - local lines = vim.split(content, "\n") - local filetype = self.session.filetype - - local orig_buf = vim.api.nvim_get_current_buf() - local hover_buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_lines(hover_buf, 0, -1, false, lines) - vim.api.nvim_set_option_value( - "filetype", - filetype or self.session.filetype, - { buf = hover_buf } - ) - - local max_width = 0 - for _, line in ipairs(lines) do - if #line >= HoverState.MAX_WIDTH then - max_width = HoverState.MAX_WIDTH - break - end - max_width = math.max(max_width, #line) - end - - if max_width == 0 then - return - end - - local win = vim.api.nvim_open_win(hover_buf, false, { - relative = "cursor", - width = max_width, - height = 1, - row = 1, - col = 0, - border = "rounded", - style = "minimal", - hide = true, - }) - - vim.api.nvim_win_set_config(win, { - height = math.min( - HoverState.MAX_HEIGHT, - vim.api.nvim_win_text_height(win, {}).all - ), - hide = false, - }) - - HoverState.current_win = win - - vim.api.nvim_create_autocmd({ "CursorMoved", "InsertEnter" }, { - buffer = orig_buf, - once = true, - callback = HoverState.close, - }) - - vim.api.nvim_create_autocmd("WinLeave", { - buffer = hover_buf, - once = true, - callback = HoverState.close, - }) -end - ----@return BaseFormatter -function HoverState:get_formatter() - local filetype = self.session.filetype - if filetype == "c" or filetype == "cpp" then - return CFormatter:new(self.session) - else - return BaseFormatter:new(self.session) - end -end - ----@async ----@param expr string ----@return dap.ErrorResponse?, string? -function HoverState:eval(expr) - local request = { - expression = expr, - frameId = self.frame_id, - context = "hover", - line = self.line_nr, - column = self.col_nr, - source = { - path = self.current_file, - }, - } - - local eval - do - local err, resp = self.session:request("evaluate", request) - if err or not resp then - return err - end - - eval = resp - end - - local fmt = self:get_formatter() - local res = fmt:format( - Item.new(expr, eval.type, eval.result, eval.variablesReference, 0) - ) - if not res.success then - return res.error - end - - return nil, res.value -end - ----@async -local function hover_async() - if - HoverState.current_win - and vim.api.nvim_win_is_valid(HoverState.current_win) - then - vim.api.nvim_set_current_win(HoverState.current_win) - return - end - - local dap = require("dap") - local session = dap.session() - if not session then - return - end - - local capabilities = session.capabilities or {} - local supports_hover = capabilities.supportsEvaluateForHovers - - if not supports_hover then - log.warning("Hover is not supported by this adapter") - return - end - - local cursor_pos = vim.api.nvim_win_get_cursor(0) - local line_nr = cursor_pos[1] -- nvim-dap sets linesStartAt1=true - local col_nr = cursor_pos[2] + 1 -- nvim-dap sets columnsStartAt1=true - local current_file = vim.api.nvim_buf_get_name(0) - - local expr - local mode = vim.api.nvim_get_mode() - if mode.mode == "v" then - local start_pos = vim.fn.getpos("v") - local end_pos = vim.fn.getpos(".") - - local start_row, start_col = start_pos[2], start_pos[3] - local end_row, end_col = end_pos[2], end_pos[3] - - if start_row == end_row and end_col < start_col then - start_col, end_col = end_col, start_col - elseif end_row < start_row then - start_row, end_row = end_row, start_row - start_col, end_col = end_col, start_col - end - - local lines = vim.api.nvim_buf_get_text( - 0, - start_row - 1, - start_col - 1, - end_row - 1, - end_col, - {} - ) - expr = table.concat(lines, "\n") - - vim.api.nvim_feedkeys( - vim.api.nvim_replace_termcodes("", true, false, true), - "n", - false - ) - else - expr = vim.fn.expand("") - end - - if expr == "" then - return - end - - local thread_id - do - local err, resp = session:request("threads", nil) - if err or not resp or #resp.threads == 0 then - log.warning("Failed to get threads: %s", err) - return - end - thread_id = resp.threads[1].id - end - - local frame_id - do - local err, resp = - session:request("stackTrace", { threadId = thread_id }) - if err or not resp or #resp.stackFrames == 0 then - log.warning("Failed to get stack trace: %s", err) - return - end - - frame_id = resp.stackFrames[1].id - end - - local state = - HoverState.new(session, frame_id, line_nr, col_nr, current_file) - - local err, resp = state:eval(expr) - if err or not resp then - log.warning("Failed to evaluate '%s': %s", expr, err) - return - end - - state:render(resp) -end - -local function hover() - coroutine.wrap(function() - local ok, err = xpcall(hover_async, debug.traceback) - if not ok then - log.error("Hover failed:\n%s", err) - end - end)() -end +local hover = require("ow.dap.hover") ---@type LazyPluginSpec return {