repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
neovim/lua/std/dbg/format.lua
Raw---Debug event format module.
---Resolves {0} and {N} and {N.key...} placeholders against wrapped varargs.
---
---## Model
---
---All varargs are wrapped into `wrapped = {...}`. Every placeholder indexes into this.
---
---### Syntax: {0}, {N}, or {N.key.sub...}
--- {0} -> vim.inspect(all data)
--- {N} -> wrapped[N]
--- {N.key} -> wrapped[N].key
--- {N.1.key} -> wrapped[N][1].key (numeric into string)
--- {N.["key"]} -> wrapped[N]["key"] (bracket = always string key)
--- {N.["a.b"]} -> wrapped[N]["a.b"] (key containing dots)
--- Mixed bare and bracket: {N.a.["x.y"].z} works as expected
---
---### Grammar (key segments after {N.})
--- key_segment := bare_key | '[' quoted_string ']'
--- bare_key := [^.[]+ (alphanumeric, underscores, etc.)
--- quoted_string := '"' [^"]* '"' | "'" [^']* "'"
--- Separator '.' between segments. No whitespace between segments.
---
---### Examples
--- dbg('{0}', {a=1, b='bee'}) -> '{ a = 1, b = "bee" }'
--- dbg('{1}', 'hello') -> 'hello'
--- dbg('{2}', 'a', 'b') -> 'b'
--- dbg('{1.name}', {name='hi'}) -> 'hi'
--- dbg('{1.1}', {{'nested'}}) -> 'nested'
--- dbg('{1.1.name}', {{name='nested'}}) -> 'nested'
--- dbg('{2.1.jan.name}', {'x', {jan={name='jan'}}}) -> 'jan'
--- dbg('{2.1.jan.age}', {'x', {age=39, {jan={name='jan',age=11}}}}) -> '11'
--- dbg('{1.["a.b"]}', { {['a.b'] = 'dots'} }) -> 'dots'
--- dbg('{1.["1"]}', { {['1'] = 'str', [1] = 'int'} }) -> 'str' (string key wins)
--- dbg('{1.["function"]}', { {['function'] = 'kw'} }) -> 'kw'
--- dbg('{1.["hello world"]}', { {['hello world'] = 'hw'} }) -> 'hw'
--- dbg('{1.[""]}', { {[''] = 'empty'} }) -> 'empty'
---
---### Rules
--- - {0} returns vim.inspect of all data; cannot be followed by keys
--- - First key must be numeric (0 or the index into wrapped)
--- - Subsequent bare keys are strings unless they parse as integers
--- - Bracket keys (["key"]) are ALWAYS string keys — never parsed as numbers
--- - This means {1.["2"]} accesses string key "2", while {1.2} accesses integer key 2
--- - Indices out of range produce an error
--- - Any nil result (missing index or missing chained key) produces an error
--- - Table values are rendered via vim.inspect
---
---### Error Suggestions
--- - Index off by 1-3: suggests correct index (e.g., {3} with 2 items suggests {2})
--- - Wrong key name: suggests closest key using Levenshtein distance (threshold 2)
---
---Parses a key expression into segments.
---Grammar:
--- expression := key_segment ('.' key_segment)*
--- key_segment := bare_key | '[' quoted_string ']'
--- bare_key := [^.[]+
--- quoted_string := '"' [^"]* '"' | "'" [^']* "'"
---Returns array of { key = string|number, is_string = boolean }.
---Bracket keys are always strings; bare keys are strings unless numeric.
---@param inner string
---@return table
local function parse_keys(inner)
local keys = {}
local pos = 1
while pos <= #inner do
if inner:sub(pos, pos) == '.' then
pos = pos + 1
end
if pos > #inner then break end
local c = inner:sub(pos, pos)
if c == '[' then
pos = pos + 1
if pos > #inner then
error('parse error: unclosed bracket in {' .. inner .. '}')
end
local quote = inner:sub(pos, pos)
if quote ~= '"' and quote ~= "'" then
error('parse error: expected quote in {' .. inner .. '}')
end
pos = pos + 1
local key_start = pos
local quote_end = inner:find(quote, pos, true)
if not quote_end then
error('parse error: unclosed bracket in {' .. inner .. '}')
end
local key = inner:sub(key_start, quote_end - 1)
pos = quote_end + 1
if pos > #inner or inner:sub(pos, pos) ~= ']' then
error('parse error: expected ] in {' .. inner .. '}')
end
pos = pos + 1
keys[#keys + 1] = { key = key, is_string = true }
else
local key_start = pos
while pos <= #inner and inner:sub(pos, pos) ~= '.' and inner:sub(pos, pos) ~= '[' do
pos = pos + 1
end
local key = inner:sub(key_start, pos - 1)
if key == '' then
error('parse error: empty key in {' .. inner .. '}')
end
keys[#keys + 1] = { key = tonumber(key) or key, is_string = false }
end
end
return keys
end
---Computes Levenshtein edit distance between two strings.
---Uses Wagner-Fischer dynamic programming algorithm.
---@param a string first string
---@param b string second string
---@return number edit distance (0 = identical)
local function levenshtein(a, b)
local m, n = #a, #b
if m == 0 then return n end
if n == 0 then return m end
local prev = {}
local curr = {}
for j = 0, n do
prev[j] = j
end
for i = 1, m do
curr[0] = i
for j = 1, n do
local cost = a:sub(i, i) == b:sub(j, j) and 0 or 1
curr[j] = math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost)
end
prev, curr = curr, prev
end
return prev[n]
end
---Suggests a similar key from a table based on Levenshtein distance.
---Only suggests keys within threshold distance (<= 2).
---@param tbl table the table to search
---@param wrong_key any the key that was not found
---@return string? suggested key or nil if none close enough
local function suggest_key(tbl, wrong_key)
if type(wrong_key) ~= 'string' then return nil end
local best_key, best_dist = nil, math.huge
for k in pairs(tbl) do
if type(k) == 'string' then
local d = levenshtein(k, wrong_key)
if d < best_dist then
best_key, best_dist = k, d
end
end
end
if best_dist <= 2 then return best_key end
return nil
end
---@param format string format string with placeholders
---@param data table varargs list packed as {...}
---@return string formatted message
---@see vim.inspect
local function format_message(format, data)
vim.validate('format', format, 'string')
vim.validate('data', data, 'table')
local data_len = #data
local result = {}
local pos = 1
while pos <= #format do
local startbrace, endbrace = format:find('%b{}', pos)
if not startbrace then
table.insert(result, format:sub(pos))
break
end
if startbrace > pos then
table.insert(result, format:sub(pos, startbrace - 1))
end
local match = format:sub(startbrace, endbrace)
local inner = match:match('^{%s*(.-)%s*}$')
local replacement
if not inner or inner == '' then
error('Empty {} is not allowed. Use {N} or {N.key...} syntax.')
else
local keys = parse_keys(inner)
local first_key_entry = keys[1]
local first_key = first_key_entry.key
if first_key_entry.is_string or type(first_key) ~= 'number' then
error(
'First key in {' .. inner .. '} must be a numeric index (like {1} or {1.key}). '
.. 'Named keys like {' .. inner .. '} are not supported. Use {1.' .. inner .. '} instead.'
)
end
local idx = first_key
if idx == 0 then
if #keys > 1 then
error('{0} cannot be followed by keys like {0.key}. Use {0} alone or {N.key} instead.')
end
if vim.tbl_count(data) == 0 then
error('{0} cannot be used on empty data.')
end
replacement = vim.inspect(data)
elseif idx > data_len or idx < 1 then
local suggestion = nil
if idx > data_len and idx <= data_len + 3 then
suggestion = ' (did you mean {' .. (idx - 1) .. '}?)'
end
error(
'Index ' .. idx .. ' is out of range (have ' .. data_len .. ' items)' .. (suggestion or '')
)
else
local current = data[idx]
local failed_key = nil
local parent_table = nil
for i = 2, #keys do
if current == nil then break end
local k = keys[i]
local access_key = k.is_string and k.key or tonumber(k.key) or k.key
if type(current) == 'table' then
local key_exists = false
for ek in pairs(current) do
if ek == access_key then key_exists = true break end
end
if not key_exists then
failed_key = access_key
parent_table = current
end
end
current = current[access_key]
end
if current == nil then
local msg = 'Key not found while resolving {' .. inner .. '}'
if failed_key and parent_table then
local suggestion = suggest_key(parent_table, failed_key)
if suggestion then
msg = msg .. " (did you mean '" .. suggestion .. "'?)"
end
end
error(msg)
end
replacement = type(current) == 'table' and vim.inspect(current) or tostring(current)
end
end
table.insert(result, replacement)
pos = endbrace + 1
end
return table.concat(result)
end
return setmetatable(
{ levenshtein = levenshtein },
{ __call = function(_, ...) return format_message(...) end }
)