mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 10:49:08 +00:00
136 lines
4.1 KiB
Lua
136 lines
4.1 KiB
Lua
---@class snacks.debug
|
|
---@overload fun(...)
|
|
local M = setmetatable({}, {
|
|
__call = function(t, ...)
|
|
return t.inspect(...)
|
|
end,
|
|
})
|
|
|
|
local uv = vim.uv or vim.loop
|
|
|
|
-- Show a notification with a pretty printed dump of the object(s)
|
|
-- with lua treesitter highlighting and the location of the caller
|
|
function M.inspect(...)
|
|
local len = select("#", ...) ---@type number
|
|
local obj = { ... } ---@type unknown[]
|
|
local caller = debug.getinfo(1, "S")
|
|
for level = 2, 10 do
|
|
local info = debug.getinfo(level, "S")
|
|
if
|
|
info
|
|
and info.source ~= caller.source
|
|
and info.what == "Lua"
|
|
and info.source ~= "lua"
|
|
and info.source ~= "@" .. vim.env.MYVIMRC
|
|
then
|
|
caller = info
|
|
break
|
|
end
|
|
end
|
|
local title = "Debug: " .. vim.fn.fnamemodify(caller.source:sub(2), ":~:.") .. ":" .. caller.linedefined
|
|
Snacks.notify.warn(vim.inspect(len == 1 and obj[1] or len > 0 and obj or nil), { title = title, ft = "lua" })
|
|
end
|
|
|
|
-- Show a notification with a pretty backtrace
|
|
function M.backtrace()
|
|
local trace = {}
|
|
for level = 2, 20 do
|
|
local info = debug.getinfo(level, "Sln")
|
|
if info and info.what == "Lua" and info.source ~= "lua" then
|
|
local line = "- `" .. vim.fn.fnamemodify(info.source:sub(2), ":p:~:.") .. "`:" .. info.currentline
|
|
if info.name then
|
|
line = line .. " _in_ **" .. info.name .. "**"
|
|
end
|
|
table.insert(trace, line)
|
|
end
|
|
end
|
|
Snacks.notify.warn(#trace > 0 and (table.concat(trace, "\n")) or "", { title = "Backtrace" })
|
|
end
|
|
|
|
-- Very simple function to profile a lua function.
|
|
-- * **flush**: set to `true` to use `jit.flush` in every iteration.
|
|
-- * **count**: defaults to 100
|
|
---@param fn fun()
|
|
---@param opts? {count?: number, flush?: boolean}
|
|
function M.profile(fn, opts)
|
|
opts = vim.tbl_extend("force", { count = 100, flush = true }, opts or {})
|
|
local start = uv.hrtime()
|
|
for _ = 1, opts.count, 1 do
|
|
if opts.flush then
|
|
jit.flush(fn, true)
|
|
end
|
|
fn()
|
|
end
|
|
Snacks.notify(((uv.hrtime() - start) / 1e6 / opts.count) .. "ms")
|
|
end
|
|
|
|
-- Log a message to the file `./debug.log`.
|
|
-- - a timestamp will be added to every message.
|
|
-- - accepts multiple arguments and pretty prints them.
|
|
-- - if the argument is not a string, it will be printed using `vim.inspect`.
|
|
-- - if the message is smaller than 120 characters, it will be printed on a single line.
|
|
--
|
|
-- ```lua
|
|
-- Snacks.debug.log("Hello", { foo = "bar" }, 42)
|
|
-- -- 2024-11-08 08:56:52 Hello { foo = "bar" } 42
|
|
-- ```
|
|
function M.log(...)
|
|
local file = "./debug.log"
|
|
local fd = io.open(file, "a+")
|
|
if not fd then
|
|
error(("Could not open file %s for writing"):format(file))
|
|
end
|
|
local c = select("#", ...)
|
|
local parts = {} ---@type string[]
|
|
for i = 1, c do
|
|
local v = select(i, ...)
|
|
parts[i] = type(v) == "string" and v or vim.inspect(v)
|
|
end
|
|
local msg = table.concat(parts, " ")
|
|
msg = #msg < 120 and msg:gsub("%s+", " ") or msg
|
|
fd:write(os.date("%Y-%m-%d %H:%M:%S ") .. msg)
|
|
fd:write("\n")
|
|
fd:close()
|
|
end
|
|
|
|
---@alias snacks.debug.Trace {name: string, time: number, [number]:snacks.debug.Trace}
|
|
|
|
---@type snacks.debug.Trace[]
|
|
M._traces = { { name = "snacks" } }
|
|
|
|
---@param name string?
|
|
function M.trace(name)
|
|
if name then
|
|
local entry = { name = name, time = uv.hrtime() } ---@type snacks.debug.Trace
|
|
table.insert(M._traces[#M._traces], entry)
|
|
table.insert(M._traces, entry)
|
|
return entry
|
|
else
|
|
local entry = assert(table.remove(M._traces), "trace not ended?") ---@type snacks.debug.Trace
|
|
entry.time = uv.hrtime() - entry.time
|
|
return entry
|
|
end
|
|
end
|
|
|
|
---@param opts? {min?: number}
|
|
function M.stats(opts)
|
|
opts = opts or {}
|
|
local lines = {}
|
|
---@param stat snacks.debug.Trace
|
|
---@param level number
|
|
local function collect(stat, level)
|
|
for _, entry in ipairs(stat) do
|
|
local ms = math.floor(entry.time / 1e4) / 1e2
|
|
if ms >= (opts.min or 0) then
|
|
table.insert(lines, ("%s- `%s`: **%.2f**ms"):format((" "):rep(level), entry.name, entry.time / 1e6))
|
|
if entry[1] then
|
|
collect(entry, level + 1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
collect(M._traces[1], 0)
|
|
Snacks.notify.warn(lines, { title = "Traces" })
|
|
end
|
|
|
|
return M
|