feat: remove ow directory, keep ow in type annotations only

This commit is contained in:
2025-10-05 00:30:22 +02:00
parent a1ff822efb
commit 8b17ef2b6b
39 changed files with 103 additions and 112 deletions
+130
View File
@@ -0,0 +1,130 @@
local Item = require("dap.item")
local Node = require("dap.hover.node")
local Window = require("dap.hover.window")
local log = require("log")
---@async
local function hover_async()
local dap = require("dap")
local session = dap.session()
if not session then
return
end
local win = Window.get_instance(session)
if win.winid and vim.api.nvim_win_is_valid(win.winid) then
vim.api.nvim_set_current_win(win.winid)
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("<ESC>", true, false, true),
"n",
false
)
else
expr = vim.fn.expand("<cexpr>")
end
if expr == "" then
return
end
local thread_id
do
local err, resp = session:request("threads", nil)
if err then
log.warning("Failed to get threads: %s", err)
end
if err or not resp or #resp.threads == 0 then
return
end
thread_id = resp.threads[1].id
end
local frame_id
do
local err, resp =
session:request("stackTrace", { threadId = thread_id })
if err then
log.warning("Failed to get stack trace: %s", err)
end
if err or not resp or #resp.stackFrames == 0 then
return
end
frame_id = resp.stackFrames[1].id
end
local request = {
expression = expr,
frameId = frame_id,
context = "hover",
line = line_nr,
column = col_nr,
source = {
path = current_file,
},
}
local err, resp = session:request("evaluate", request)
if err then
log.warning("Failed to evaluate '%s': %s", expr, err)
end
if err or not resp then
return
end
local item = Item.new(expr, resp.type, resp.result, resp.variablesReference)
local root = Node.new(item, nil, session.filetype)
root:load_children(session)
win:show(root)
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
return hover
+130
View File
@@ -0,0 +1,130 @@
---@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
---@field start_row integer 0-indexed
---@field start_col integer 0-indexed
---@field end_row integer 0-indexed
---@field end_col integer 0-indexed
---@class ow.dap.hover.Content
---@field text string
---@field highlights ow.dap.hover.Highlight[]
---@field _current_row integer
---@field _current_col integer
local Content = {}
Content.__index = Content
---@return ow.dap.hover.Content
function Content.new()
return setmetatable({
text = "",
highlights = {},
_current_row = 0,
_current_col = 0,
}, Content)
end
---@return integer
function Content:current_line()
return self._current_row + 1
end
---@param text string May not contain line breaks
---@param highlight_group? string
function Content:add(text, highlight_group)
local start_row = self._current_row
local start_col = self._current_col
local end_row = self._current_row
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_row = start_row,
start_col = start_col,
end_row = end_row,
end_col = end_col,
})
end
end
---@param text string May not contain line breaks
---@param lang string
function Content:add_with_treesitter(text, lang)
local start_row = self._current_row
local start_col = self._current_col
self:add(text)
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
for id, node in query:iter_captures(tree:root(), text, 0, -1) do
local capture_name = query.captures[id]
local start_row_rel, start_col_rel, end_row_rel, end_col_rel =
node:range()
local abs_start_row = start_row + start_row_rel
local abs_end_row = start_row + end_row_rel
local abs_start_col = start_col + start_col_rel
local abs_end_col = start_col + end_col_rel
table.insert(self.highlights, {
group = "@" .. capture_name,
start_row = abs_start_row,
start_col = abs_start_col,
end_row = abs_end_row,
end_col = abs_end_col,
})
end
end
function Content:newline()
self:add("\n")
self._current_col = 0
self._current_row = self._current_row + 1
end
---@return string[]
function Content:get_lines()
local text = self.text:gsub("%s+$", "")
return vim.split(text, "\n")
end
---@param ns_id integer
---@param buf integer
---@param row_offset integer 0-indexed
function Content:apply_highlights(ns_id, buf, row_offset)
for _, highlight in ipairs(self.highlights) do
vim.hl.range(
buf,
ns_id,
highlight.group,
{ row_offset + highlight.start_row, highlight.start_col },
{ row_offset + highlight.end_row, highlight.end_col }
)
end
end
return Content
+281
View File
@@ -0,0 +1,281 @@
local Item = require("dap.item")
local log = require("log")
---@class ow.dap.hover.Node
---@field lang string
---@field item ow.dap.Item
---@field parent ow.dap.hover.Node?
---@field children ow.dap.hover.Node[]
---@field is_expanded boolean
---@field is_last_child boolean
local Node = {}
Node.__index = Node
---@param item ow.dap.Item
---@param parent ow.dap.hover.Node?
---@param lang string
---@return ow.dap.hover.Node
function Node.new(item, parent, lang)
return setmetatable({
lang = lang,
item = item,
parent = parent,
children = {},
is_expanded = false,
is_last_child = false,
}, 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
or false
end
---@return boolean
function Node:is_c_lang()
return self.lang == "c" or self.lang == "cpp"
end
---@return boolean
function Node:is_c_pointer()
return self:is_c_lang()
and 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
return ""
end
local 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
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:is_c_lang() and self.item.name:match("^%[?%d+%]?$") ~= nil
end
---@return string
function Node:format_c()
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.item.value == "" then
return string.format("%s %s", self.item.type, self.item.name)
else
return string.format(
"%s %s = %s",
self.item.type,
self.item.name,
self.item.value
)
end
end
---@return boolean
function Node:is_expandable()
return self:is_container() and not self:is_c_null_pointer()
end
---@param content ow.dap.hover.Content
function Node:format_into(content)
local tree_prefix = self:get_tree_prefix()
if tree_prefix ~= "" then
content:add(tree_prefix, "DapHoverPrefix")
end
local text
if self:is_c_lang() then
text = self:format_c()
elseif self.lang == "python" then
text = string.format(
"%s: %s = %s",
self.item.name,
self.item.type,
self.item.value
)
else
error(string.format("Formatting for %s not implemented", self.lang))
end
content:add_with_treesitter(text, self.lang)
if self:is_expandable() then
if self.is_expanded then
for _, child in ipairs(self.children) do
content:newline()
child:format_into(content)
end
else
content:add(" ...", "DapHoverExpandMarker")
end
end
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
+336
View File
@@ -0,0 +1,336 @@
local Content = require("dap.hover.content")
local log = require("log")
---@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 augroup? integer
---@field session dap.Session
---@field root ow.dap.hover.Node
local Window = {}
Window.__index = Window
Window.NAMESPACE = "dap.hover.Window"
Window.NS_ID = vim.api.nvim_create_namespace(Window.NAMESPACE)
local function setup_highlights()
vim.api.nvim_set_hl(0, "DapHoverPrefix", {
link = "@comment",
})
vim.api.nvim_set_hl(0, "DapHoverExpandMarker", {
link = "@comment",
})
end
setup_highlights()
vim.api.nvim_create_autocmd("ColorScheme", {
callback = setup_highlights,
})
local instance = nil
---@param session dap.Session
---@return ow.dap.hover.Window
function Window.get_instance(session)
if instance then
return instance
end
instance = setmetatable({
max_width = nil,
max_height = nil,
winid = nil,
bufnr = nil,
augroup = nil,
session = session,
root = nil,
}, Window)
return instance
end
function Window:destroy()
if self.winid and vim.api.nvim_win_is_valid(self.winid) then
vim.api.nvim_win_close(self.winid, true)
end
if self.augroup then
vim.api.nvim_del_augroup_by_id(self.augroup)
end
instance = nil
end
---@return integer
function Window:compute_width()
local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, true)
local max_width = 1
for _, line in ipairs(lines) do
local line_width = vim.api.nvim_strwidth(line)
if self.max_width and line_width >= self.max_width then
max_width = self.max_width
break
end
max_width = math.max(max_width, line_width)
end
return max_width
end
---@return integer
function Window:compute_height()
local text_height = vim.api.nvim_win_text_height(self.winid, {}).all
return math.min(self.max_height or text_height, text_height)
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
---@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)
self.winid = vim.api.nvim_open_win(self.bufnr, false, {
relative = "cursor",
width = 1,
height = 1,
row = 1,
col = 0,
border = "rounded",
style = "minimal",
})
vim.wo[self.winid].wrap = false
self:refresh_tree(self.root, 1, 1)
self.augroup =
vim.api.nvim_create_augroup(Window.NAMESPACE, { clear = true })
vim.api.nvim_create_autocmd({ "CursorMoved", "InsertEnter" }, {
group = self.augroup,
buffer = prev_buf,
once = true,
callback = function()
self:destroy()
end,
})
vim.api.nvim_create_autocmd("BufEnter", {
group = self.augroup,
callback = function(arg)
if arg.buf ~= self.bufnr then
self:destroy()
return true
end
end,
})
vim.api.nvim_create_autocmd("WinClosed", {
group = self.augroup,
once = true,
pattern = tostring(self.winid),
callback = function()
self:destroy()
end,
})
-- Toggle expand/collapse
vim.keymap.set("n", "<CR>", function()
self:toggle_node()
end, { buffer = self.bufnr, nowait = true })
vim.keymap.set("n", "<Tab>", function()
self:toggle_node()
end, { buffer = self.bufnr, nowait = true })
-- Collapse
vim.keymap.set("n", "<S-Tab>", function()
self:collapse_parent()
end, { buffer = self.bufnr, nowait = true })
vim.keymap.set("n", "<BS>", 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:destroy()
end, { buffer = self.bufnr, nowait = true })
vim.keymap.set("n", "<Esc>", function()
self:destroy()
end, { buffer = self.bufnr, nowait = true })
-- Yank operations
vim.keymap.set("n", "y", function()
self:yank_value()
end, { buffer = self.bufnr, nowait = true })
end
return Window
+29
View File
@@ -0,0 +1,29 @@
---@class ow.dap.Item
---@field name string
---@field type string
---@field value string
---@field variablesReference? number
local Item = {}
Item.__index = Item
---@param name string
---@param type string
---@param value string
---@param variablesReference? number
---@return ow.dap.Item
function Item.new(name, type, value, variablesReference)
return setmetatable({
name = name,
type = type,
value = value,
variablesReference = variablesReference,
}, Item)
end
---@param var dap.Variable
---@return ow.dap.Item
function Item.from_var(var)
return Item.new(var.name, var.type, var.value, var.variablesReference)
end
return Item