diff options
| author | venomade <venomade@venomade.com> | 2026-05-21 20:34:45 +0100 |
|---|---|---|
| committer | venomade <venomade@venomade.com> | 2026-05-21 20:34:45 +0100 |
| commit | 637409d951e9dd1a2c29cd424bd41ff8c14b6d88 (patch) | |
| tree | 2d41be117f6a9f62562c7b54f06a1b1780c62a3b /lua_src | |
Initial Commit main
Diffstat (limited to 'lua_src')
| -rw-r--r-- | lua_src/main.lua | 48 | ||||
| -rw-r--r-- | lua_src/sild/ansi.lua | 41 | ||||
| -rw-r--r-- | lua_src/sild/commands.lua | 278 | ||||
| -rw-r--r-- | lua_src/sild/config.lua | 173 | ||||
| -rw-r--r-- | lua_src/sild/debugger.lua | 151 | ||||
| -rw-r--r-- | lua_src/sild/eval.lua | 71 | ||||
| -rw-r--r-- | lua_src/sild/highlight.lua | 84 | ||||
| -rw-r--r-- | lua_src/sild/parse.lua | 44 | ||||
| -rw-r--r-- | lua_src/sild/session.lua | 11 | ||||
| -rw-r--r-- | lua_src/sild/ui.lua | 109 |
10 files changed, 1010 insertions, 0 deletions
diff --git a/lua_src/main.lua b/lua_src/main.lua new file mode 100644 index 0000000..776f2a4 --- /dev/null +++ b/lua_src/main.lua @@ -0,0 +1,48 @@ +local config = require("sild.config") +local parse = require("sild.parse") +local session = require("sild.session") +local debugger = require("sild.debugger") +local commands = require("sild.commands") + +local cli = config.parse_cli(arg) + +if not cli.script_path then + config.print_help(SILD_VERSION); os.exit(1) +end + +local home = os.getenv("HOME") or os.getenv("USERPROFILE") or "" +local xdg_config_home = os.getenv("XDG_CONFIG_HOME") or (home .. "/.config") +config.parse_file(cli.config_file or (xdg_config_home .. "/sild/sild.cfg")) + +local chunk, load_err = loadfile(cli.script_path) +if not chunk then + local ansi = require("sild.ansi") + io.stderr:write(ansi.color_error( + "ERROR: cannot load '" .. cli.script_path .. "': " .. tostring(load_err) + ) .. "\n") + os.exit(1) +end + +local script_src_key = "@" .. cli.script_path +-- TODO: Look for annotations in require'd files +local breakpoints_list, actions_list = parse.annotations(cli.script_path) +session.breakpoints[script_src_key] = breakpoints_list +for _, ln in ipairs(cli.breaks) do + session.breakpoints[script_src_key][ln] = true +end +session.actions[script_src_key] = actions_list + +local real_arg = {} +for i, v in ipairs(cli.script_args) do real_arg[i] = v end +real_arg[0] = cli.script_path +arg = real_arg + +local ansi = require("sild.ansi") +print(ansi.color_prompt("sild " .. SILD_VERSION) .. " " .. ansi.color_bold(cli.script_path)) +print(ansi.color_dim("n=step c=continue ?=help q=quit") .. " " .. + -- TODO: use XDG_CONFIG_HOME here + ansi.color_dim("config: " .. (cli.config_file or (os.getenv("HOME") or "~") .. "/.config/sild/sild.cfg")) .. "\n") + +local has_immediate_bp = next(session.breakpoints[script_src_key]) ~= nil +debugger.install_hook(has_immediate_bp, commands.run) +chunk() diff --git a/lua_src/sild/ansi.lua b/lua_src/sild/ansi.lua new file mode 100644 index 0000000..8ee99df --- /dev/null +++ b/lua_src/sild/ansi.lua @@ -0,0 +1,41 @@ +local config = require("sild.config").config + +local M = {} + +function M.color(code, s) + if not code or code == "" then return s end + return "\027[" .. code .. "m" .. s .. "\027[0m" +end + +function M.ansi_len(s) + return #s:gsub("\027%[[%d;]*m", "") +end + +-- Pad/Truncate ANSI String +function M.fit(s, n) + s = s or "" + local vlen = M.ansi_len(s) + if vlen > n then + return s:gsub("\027%[[%d;]*m", ""):sub(1, n) + end + return s .. string.rep(" ", n - vlen) +end + +function M.color_keyword(s) return M.color(config.color_keyword, s) end +function M.color_string(s) return M.color(config.color_string, s) end +function M.color_number(s) return M.color(config.color_number, s) end +function M.color_operator(s) return M.color(config.color_operator, s) end +function M.color_comment(s) return M.color(config.color_comment, s) end +function M.color_current(s) return M.color(config.color_current, s) end +function M.color_breakpoint (s) return M.color(config.color_breakpoint, s) end +function M.color_dim(s) return M.color(config.color_dim, s) end +function M.color_header(s) return M.color(config.color_header, s) end +function M.color_linenumber(s) return M.color(config.color_linenumber, s) end +function M.color_watch(s) return M.color(config.color_watch, s) end +function M.color_value(s) return M.color(config.color_value, s) end +function M.color_info(s) return M.color(config.color_info, s) end +function M.color_error(s) return M.color(config.color_error, s) end +function M.color_prompt(s) return M.color(config.color_prompt, s) end +function M.color_bold(s) return M.color("1", s) end + +return M diff --git a/lua_src/sild/commands.lua b/lua_src/sild/commands.lua new file mode 100644 index 0000000..98e74fd --- /dev/null +++ b/lua_src/sild/commands.lua @@ -0,0 +1,278 @@ +local ansi = require("sild.ansi") +local eval = require("sild.eval") +local ui = require("sild.ui") +local session = require("sild.session") +local debugger = require("sild.debugger") +local highlight = require("sild.highlight") + +local M = {} + +local function print_help() + local function row(cmd, desc) + print(" " .. ansi.color_value(ansi.fit(cmd, 28)) .. " " .. desc) + end + -- TODO: Fit to screen width + print() + print(ansi.color_bold("-- Navigation ------------------")) + row("n / <enter>", "step to next line") + row("c", "continue freely (honours breakpoints)") + row("c <line>", "continue until line (one-shot)") + row("finish", "run until current function returns") + print(ansi.color_bold("-- Inspect ---------------------")) + row("vars", "list locals in scope with types") + row("eval <expr>", "evaluate a Lua expression") + row("dump <tbl> [depth]", "dump table recursively (default depth 3)") + row("bt", "stack traceback") + row("calls", "show function call counts") + print(ansi.color_bold("-- Modify ----------------------")) + row("set <name> <expr>", "set a local variable by name") + print(ansi.color_bold("-- Watches ---------------------")) + row("watch <expr>", "watch expression, printed each step") + row("unwatch <expr>", "remove a watch") + row("watches", "list active watches") + print(ansi.color_bold("-- Breakpoints -----------------")) + row("bp <line>", "set breakpoint at line") + row("bp <line> if <expr>", "conditional breakpoint") + row("bpdel <line>", "remove breakpoint at line") + row("bplist", "list all breakpoints") + print(ansi.color_bold("-- Other -----------------------")) + row("q", "quit sild") + row("? / h / help", "show this help") + print() +end + +local function fire_actions(src_key, line) + local acts = session.actions[src_key] and session.actions[src_key][line] + + if not acts then return end + session.actions[src_key][line] = nil + + for _, act in ipairs(acts) do + if act.kind == "watch" then + local already = false + for _, w in ipairs(session.watches) do + if w == act.expr then + already = true; break + end + end + if not already then + session.watches[#session.watches + 1] = act.expr + print(ansi.color_info("auto-watch: ") .. act.expr) + end + elseif act.kind == "unwatch" then + for i, w in ipairs(session.watches) do + if w == act.expr then + table.remove(session.watches, i); break + end + end + end + end +end + +local function print_watches(env) + if #session.watches == 0 then return end + + for _, expr in ipairs(session.watches) do + local v, ok, err = eval.eval(expr, env) + local vs = ok and ansi.color_value(tostring(v)) + or ansi.color_error("err: " .. tostring(err)) + io.write(ansi.color_watch("watch ") .. expr .. " = " .. vs .. "\n") + end + io.write("\n") +end + +local function normalise(input) + return (input == "next" or input == "step") and "n" + or (input == "quit" or input == "exit") and "q" + or input == "continue" and "c" + or input == "backtrace" and "bt" + or input == "break" and "bp" + or input:match("^break%s+%d+") and input:gsub("^break%s*", "bp ", 1) + or input +end + +local function handle_navigation(input, src_key) + if input == "n" or input == "" then + return false + elseif input == "c" then + debugger.install_hook(true, M.run) + return false + elseif input:match("^c%s+(%d+)$") then + local target = tonumber(input:match("^c%s+(%d+)$")) + session.continue_until = { source = src_key, line = target } + print(ansi.color_info("-> running until line " .. target)) + debugger.install_hook(true, M.run) + return false + elseif input == "finish" then + debugger.finish(M.run) + return false + end + + return nil +end + +local function handle_breakpoints(input, src_key) + if input:match("^bp%s+(%d+)$") then + local n = tonumber(input:match("^bp%s+(%d+)$")) + session.breakpoints[src_key] = session.breakpoints[src_key] or {} + session.breakpoints[src_key][n] = true + print(ansi.color_info("breakpoint set at line " .. n)) + elseif input:match("^bp%s+(%d+)%s+if%s+(.+)$") then + local n, cond = input:match("^bp%s+(%d+)%s+if%s+(.+)$") + session.breakpoints[src_key] = session.breakpoints[src_key] or {} + session.breakpoints[src_key][tonumber(n)] = cond + print(ansi.color_info("conditional breakpoint at line " .. n)) + elseif input:match("^bpdel%s+(%d+)$") then + local n = tonumber(input:match("^bpdel%s+(%d+)$")) + if session.breakpoints[src_key] then + session.breakpoints[src_key][n] = nil + end + print(ansi.color_dim("breakpoint removed at line " .. n)) + elseif input == "bplist" then + local any = false + for sk, tbl in pairs(session.breakpoints) do + for ln, cond in pairs(tbl) do + any = true + local cs = cond == true and "" or ansi.color_dim(" if " .. tostring(cond)) + print(" " .. ansi.color_header(sk) .. ":" .. ansi.color_linenumber(tostring(ln)) .. cs) + end + end + if not any then print(ansi.color_dim("no breakpoints set")) end + else + return false + end + + return true +end + +local function handle_inspect(input, locals_snapshot, env) + if input == "vars" then + if #locals_snapshot == 0 then + print(ansi.color_dim("no locals in scope")) + else + for _, v in ipairs(locals_snapshot) do + local vs + if type(v.value) == "string" then + vs = ansi.color_string('"' .. tostring(v.value) .. '"') + elseif type(v.value) == "boolean" then + vs = ansi.color_keyword(tostring(v.value)) + else + vs = ansi.color_value(tostring(v.value)) + end + print(" " .. ansi.color_header(v.name) .. " = " .. vs .. + " " .. ansi.color_dim("(" .. type(v.value) .. ")")) + end + end + elseif input:match("^eval%s+(.+)$") then + local expr = input:match("^eval%s+(.+)$") + local v, ok, err = eval.eval(expr, env) + if ok then + print(ansi.color_value(tostring(v))) + else + print(ansi.color_error("error: " .. tostring(err))) + end + elseif input:match("^dump%s+") then + local rest = input:match("^dump%s+(.+)$") + local expr, ds = rest:match("^(.-)%s+(%d+)$") + if not expr then expr = rest end + eval.do_dump(expr, env, tonumber(ds) or 3) + elseif input == "bt" then + print(debug.traceback("Stack trace:", 2)) + elseif input == "calls" then + local any = false + for fn, n in pairs(session.calls) do + any = true + print(" " .. ansi.color_header(fn) .. " called " .. ansi.color_value(tostring(n)) .. "x") + end + if not any then print(ansi.color_dim("no call data yet")) end + else + return false + end + + return true +end + +local function handle_watches(input) + if input:match("^watch%s+(.+)$") then + local expr = input:match("^watch%s+(.+)$") + local already = false + for _, w in ipairs(session.watches) do + if w == expr then + already = true; break + end + end + if already then + print(ansi.color_dim("already watching: " .. expr)) + else + session.watches[#session.watches + 1] = expr + print(ansi.color_watch("watching: ") .. expr) + end + elseif input:match("^unwatch%s+(.+)$") then + local expr = input:match("^unwatch%s+(.+)$") + local removed = false + for i, w in ipairs(session.watches) do + if w == expr then + table.remove(session.watches, i); removed = true; break + end + end + print(removed and ansi.color_dim("removed watch: " .. expr) + or ansi.color_error("not watching: " .. expr)) + elseif input == "watches" then + if #session.watches == 0 then + print(ansi.color_dim("no active watches")) + else + for _, w in ipairs(session.watches) do print(" " .. ansi.color_watch(w)) end + end + else + return false + end + + return true +end + +local function input_loop(src_key, locals_snapshot, env) + while true do + io.write("(sild) ") + io.flush() + local raw = io.read() + if raw == nil then os.exit(0) end + local input = normalise(raw:match("^%s*(.-)%s*$")) + + local nav = handle_navigation(input, src_key) + if nav == false then return end + + if handle_breakpoints(input, src_key) then + elseif handle_inspect(input, locals_snapshot, env) then + elseif handle_watches(input) then + elseif input:match("^set%s+(%a[%w_]*)%s+(.+)$") then + local name, expr = input:match("^set%s+(%a[%w_]*)%s+(.+)$") + debugger.write(locals_snapshot, env, name, expr) + elseif input == "q" then + os.exit(0) + elseif input == "?" or input == "h" or input == "help" then + print_help() + else + print(ansi.color_dim("unknown command - ? for help")) + end + end +end + +function M.run(info, locals_snapshot) + local src = info.short_src + local line = info.currentline + local src_key = info.source + local path = src_key:sub(1, 1) == "@" and src_key:sub(2) or nil + local text = path and (highlight.read_file(path)[line] or ""):gsub("%s+$", "") or "" + + fire_actions(src_key, line) + + session.history[#session.history + 1] = { src = src, line = line, text = text } + ui.draw(info) + + local env = debugger.build_env(locals_snapshot) + print_watches(env) + + input_loop(src_key, locals_snapshot, env) +end + +return M diff --git a/lua_src/sild/config.lua b/lua_src/sild/config.lua new file mode 100644 index 0000000..0091bce --- /dev/null +++ b/lua_src/sild/config.lua @@ -0,0 +1,173 @@ +local M = {} + +-- not in table means faster local lookup +local config = { + -- layout + auto_size = true, + code_lines = 15, + code_inner = 56, + hist_width = 40, + -- colours (ANSI Codes) + color_keyword = "1;34", + color_string = "32", + color_number = "35", + color_operator = "33", + color_comment = "2", + color_current = "33", + color_breakpoint = "31", + color_dim = "2", + color_header = "36", + color_linenumber = "33", + color_watch = "36", + color_value = "33", + color_info = "36", + color_error = "31", + color_prompt = "33", +} +M.config = config + +local config_warning_str = "WARNING: config line " + +-- TODO: Replace all this if-elseing with tables of handlers +function M.parse_file(path) + local f = io.open(path, "r") + if not f then return end + local line_number = 0 + for line in f:lines() do + line_number = line_number + 1 + line = line:gsub("#.*$", ""):match("^%s*(.-)%s*$") + if line ~= "" then + local key, val = line:match("^([%a_][%w_]*)%s*=%s*(.+)$") + if not key then + io.stderr:write(config_warning_str .. line_number .. ": malformed entry\n") + elseif config[key] == nil then + io.stderr:write(config_warning_str .. line_number .. ": unknown key '" .. key .. "'\n") + else + local t = type(config[key]) + if t == "number" then + local n = tonumber(val) + if n then + config[key] = n + else + io.stderr:write(config_warning_str .. line_number .. ": '" .. key .. "' expects a number\n") + end + elseif t == "boolean" then + if val == "true" then + config[key] = true + elseif val == "false" then + config[key] = false + else + io.stderr:write(config_warning_str .. line_number .. ": '" .. key .. "' expects true or false\n") + end + else + val = val:match('^"(.*)"$') or val:match("^'(.*)'$") or val + config[key] = val + end + end + end + end + f:close() +end + +function M.parse_cli(arg) + local cli = { breaks = {}, config_file = nil, script_path = nil, script_args = {} } + local i = 1 + while i <= #arg do + local a = arg[i] + if a == "-h" or a == "--help" then + M.print_help() + os.exit(0) + elseif a == "-v" or a == "--version" then + print("sild " .. SILD_VERSION) + os.exit(0) + elseif a == "-b" or a == "--break" then + i = i + 1 + local n = tonumber(arg[i]) + if not n then + io.stderr:write("ERROR: --break requires a line number\n") + os.exit(1) + end + cli.breaks[#cli.breaks + 1] = math.floor(n) + elseif a == "-c" or a == "--config" then + i = i + 1 + cli.config_file = arg[i] + if not cli.config_file then + io.stderr:write("ERROR: --config requires a filename\n") + os.exit(1) + end + elseif a == "-l" or a == "--lines" then + i = i + 1 + local n = tonumber(arg[i]) + if not n then + io.stderr:write("ERROR: --lines requires a number\n") + os.exit(1) + end + config.code_lines = math.floor(n) + config.auto_size = false + elseif a == "-C" or a == "--columns" then + i = i + 1 + local n = tonumber(arg[i]) + if not n then + io.stderr:write("ERROR: --columns requires a number\n") + os.exit(1) + end + config.code_inner = math.floor(n) + config.auto_size = false + elseif a == "-W" or a == "--width" then + i = i + 1 + local n = tonumber(arg[i]) + if not n then + io.stderr:write("ERROR: --width requires a number\n") + os.exit(1) + end + config.hist_width = math.floor(n) + config.auto_size = false + elseif a:sub(1, 1) == "-" then + io.stderr:write("ERROR: unknown option '" .. a .. "' (try --help)\n") + os.exit(1) + else + cli.script_path = a + for j = i + 1, #arg do + cli.script_args[#cli.script_args + 1] = arg[j] + end + break + end + i = i + 1 + end + return cli +end + +local function row(flags, desc) + print(string.format(" %-28s %s", flags, desc)) +end +local function annotations(tag, desc) + print(string.format(" %-30s %s", tag, desc)) +end + +function M.print_help(version) + -- Placed here to avoid circular require + local ansi = require("sild.ansi") + print(ansi.color_bold("sild " .. (version or "?") .. " – The Small Interactive Lua Debugger")) + print() + print("Usage: sild [options] <script.lua> [script args...]") + print() + print("Options:") + row("-b, --break <line>", "set a breakpoint at <line> in the target script") + row("-c, --config <file>", "load config from <file> instead of default") + row("-l, --lines <n>", "number of source lines shown in code view (disables auto_size)") + row("-C, --columns <n>", "inner width of the code box in characters (disables auto_size)") + row("-W, --width <n>", "history column width in characters (disables auto_size)") + row("-h, --help", "show this help and exit") + row("-v, --version", "show version and exit") + print() + print("Inline annotations (place in your script, act on the following line):") + annotations("--- BREAK", "unconditional breakpoint") + annotations("--- BREAK if <expr>", "conditional breakpoint") + annotations("--- WATCH <expr>", "auto-register a watch expression") + annotations("--- UNWATCH <expr>", "remove a watch expression") + print() + print("Default config location: ~/.config/sild/sild.cfg") + print() +end + +return M diff --git a/lua_src/sild/debugger.lua b/lua_src/sild/debugger.lua new file mode 100644 index 0000000..54b3682 --- /dev/null +++ b/lua_src/sild/debugger.lua @@ -0,0 +1,151 @@ +local session = require("sild.session") +local ansi = require("sild.ansi") + +local M = {} + +local function is_internal(source) + return source:sub(1, 7) == "@sild." +end + +local pending_writes = {} + +function M.build_env(locals_snapshot) + local env = setmetatable({}, { __index = _G }) + for _, v in ipairs(locals_snapshot) do + env[v.name] = v.value + end + return env +end + +function M.write(locals_snapshot, env, name, expr) + local eval = require("sild.eval") + local newval, ok, err = eval.eval(expr, env) + if not ok then + print(ansi.color_error("eval error: " .. tostring(err))) + return + end + local found = false + for _, v in ipairs(locals_snapshot) do + if v.name == name then + found = true; break + end + end + if not found then + print(ansi.color_error("no local '" .. name .. "' in scope")) + return + end + env[name] = newval + pending_writes[#pending_writes + 1] = { name = name, value = newval } + local vs = type(newval) == "string" + and ansi.color_string('"' .. newval .. '"') + or ansi.color_value(tostring(newval)) + print(ansi.color_header(name) .. " <- " .. vs) +end + +local function snapshot() + local locals, seen = {}, {} + local i = 1 + while true do + local name, val = debug.getlocal(3, i) + if not name then break end + if name:sub(1, 1) ~= "(" and not seen[name] then + locals[#locals + 1] = { name = name, value = val, idx = i } + seen[name] = true + end + i = i + 1 + end + return locals +end + +function M.install_hook(bp_only, run_commands) + debug.sethook(function(event) + if event == "call" then + local fi = debug.getinfo(2, "Sn") + if not fi or is_internal(fi.source) then return end + local fn = fi.name or "?" + session.calls[fn] = (session.calls[fn] or 0) + 1 + return + end + + if event ~= "line" then return end + + local info = debug.getinfo(2, "Sl") + if not info or is_internal(info.source) then return end + + if #pending_writes > 0 then + local i = 1 + while true do + local n = debug.getlocal(2, i) + if not n then break end + for _, pw in ipairs(pending_writes) do + if pw.name == n then debug.setlocal(2, i, pw.value) end + end + i = i + 1 + end + pending_writes = {} + end + + if session.continue_until then + if info.source == session.continue_until.source then + if info.currentline == session.continue_until.line then + session.continue_until = nil + M.install_hook(false, run_commands) + run_commands(info, snapshot()) + end + end + return + end + + local bps = session.breakpoints[info.source] + if bps then + local cond = bps[info.currentline] + if cond then + local fire = true + if type(cond) == "string" then + local snap = snapshot() + local ev = require("sild.eval") + local env = M.build_env(snap) + local v, ok = ev.eval(cond, env) + + fire = ok and (v ~= false and v ~= nil) + + if fire then + print(ansi.color_prompt("\n*** breakpoint at line " .. info.currentline .. " ***")) + M.install_hook(false, run_commands) + run_commands(info, snap) + return + end + elseif cond == true then + print(ansi.color_prompt("\n*** breakpoint at line " .. info.currentline .. " ***")) + M.install_hook(false, run_commands) + run_commands(info, snapshot()) + return + end + end + end + + if not bp_only then + run_commands(info, snapshot()) + end + end, "cl") +end + +function M.finish(run_commands) + debug.sethook(function(event) + if event ~= "return" then return end + + local return_info = debug.getinfo(2, "S") + if return_info and is_internal(return_info.source) then return end + + debug.sethook(function(next_event) + if next_event ~= "line" then return end + + local caller_info = debug.getinfo(2, "Sl") + if caller_info and not is_internal(caller_info.source) then + M.install_hook(false, run_commands) + end + end, "l") + end, "r") +end + +return M diff --git a/lua_src/sild/eval.lua b/lua_src/sild/eval.lua new file mode 100644 index 0000000..618d3cb --- /dev/null +++ b/lua_src/sild/eval.lua @@ -0,0 +1,71 @@ +local ansi = require("sild.ansi") + +local M = {} + +function M.eval(expr, env) + -- Use 'return' for expressions like 'x + 1' + local chunk, err = load("return " .. expr, "=eval", "t", env or _G) + if not chunk then + -- Raw evaluate for expressions like 'x = 1' + chunk, err = load(expr, "=eval", "t", env or _G) + end + if not chunk then return nil, false, err end + local ok, result = pcall(chunk) + if ok then + return result, true, nil + else + return nil, false, tostring(result) + end +end + +local function dump_table(tbl, indent, depth, max_depth) + local pad = string.rep(" ", indent) + local keys = {} + + for key in pairs(tbl) do + keys[#keys + 1] = key + end + + -- Table is sorted alphanumerically to make output deterministic + table.sort(keys, function(a, b) + if type(a) == "number" and type(b) == "number" then + return a < b + end + return tostring(a) < tostring(b) + end) + + for _, key in ipairs(keys) do + local value = tbl[key] + local key_str = ansi.color_header(tostring(key)) + if type(value) == "table" and depth < max_depth then + print(pad .. key_str .. " = {") + dump_table(value, indent + 1, depth + 1, max_depth) + print(pad .. "}") + else + local value_str + if type(value) == "string" then + value_str = ansi.color_string('"' .. value .. '"') + elseif type(value) == "boolean" then + value_str = ansi.color_keyword(tostring(value)) + else + value_str = ansi.color_value(tostring(value)) + end + print(pad .. key_str .. " = " .. value_str .. " " .. ansi.color_dim("(" .. type(value) .. ")")) + end + end +end + +function M.do_dump(expr, env, max_depth) + local tbl, ok, err = M.eval(expr, env) + if not ok then + print(ansi.color_error("eval error: " .. tostring(err))); return + end + if type(tbl) ~= "table" then + print(ansi.color_error(expr .. " is not a table")); return + end + print(ansi.color_header(expr) .. " = {") + dump_table(tbl, 1, 1, max_depth) + print("}") +end + +return M diff --git a/lua_src/sild/highlight.lua b/lua_src/sild/highlight.lua new file mode 100644 index 0000000..3f8cbb0 --- /dev/null +++ b/lua_src/sild/highlight.lua @@ -0,0 +1,84 @@ +local ansi = require("sild.ansi") + +local M = {} + +local file_cache = {} + +function M.read_file(path) + if file_cache[path] then return file_cache[path] end + local lines = {} + local f = io.open(path, "r") + if f then + for l in f:lines() do lines[#lines + 1] = l end + f:close() + else + lines[1] = "-- FILE NOT AVAILABLE: " .. path + end + file_cache[path] = lines + return lines +end + +local KEYWORDS = {} +for _, k in ipairs({ + "and", + "break", + "do", + "else", + "elseif", + "end", + "false", + "for", + "function", + "goto", + "if", + "in", + "local", + "nil", + "not", + "or", + "repeat", + "return", + "then", + "true", + "until", + "while", +}) do + -- Table placement is irrelevant so use k,v boolean checking + KEYWORDS[k] = true +end + +function M.highlight(line) + local out = {} + local i = 1 + + while i <= #line do + local char = line:sub(i, i) + -- Comment Matching + if line:sub(i, i + 1) == "--" then + out[#out + 1] = ansi.color_comment(line:sub(i)) + break + -- String Matching + elseif char == '"' or char == "'" then + local j = line:find(char, i + 1, true) + out[#out + 1] = ansi.color_string(line:sub(i, j or #line)) + i = (j or #line) + 1 + -- Number Matching + elseif char:match("%d") or (char == "." and line:sub(i + 1, i + 1):match("%d")) then + local str = line:match("^[%d%.]+", i) + out[#out + 1] = ansi.color_number(str); i = i + #str + -- Keyword Matching + elseif char:match("[%a_]") then + local str = line:match("^[%w_]+", i) + out[#out + 1] = KEYWORDS[str] and ansi.color_keyword(str) or str + i = i + #str + -- Operator Matching + elseif char:match("[%+%-%*/%%#%^&|~<>=]") then + out[#out + 1] = ansi.color_operator(char); i = i + 1 + else + out[#out + 1] = char; i = i + 1 + end + end + return table.concat(out) +end + +return M diff --git a/lua_src/sild/parse.lua b/lua_src/sild/parse.lua new file mode 100644 index 0000000..1757658 --- /dev/null +++ b/lua_src/sild/parse.lua @@ -0,0 +1,44 @@ +local highlight = require("sild.highlight") + +local M = {} + +function M.annotations(path) + local breakpoints, actions = {}, {} + -- Use highlight's read_file for caching purposes + local lines = highlight.read_file(path) + + local function add_action(line_number, action_table) + actions[line_number] = actions[line_number] or {} + actions[line_number][#actions[line_number] + 1] = action_table + end + + for i, line in ipairs(lines) do + local next_line = i + 1 + + local stripped = line:match("^%s*(.-)%s*$") + + if stripped:sub(1, 3) == "---" then + local rest = stripped:sub(4):match("^%s*(.-)%s*$") + + if rest:sub(1, 8) == "BREAK if" then + breakpoints[next_line] = rest:sub(9):match("^%s*(.-)%s*$") + elseif rest == "BREAK" then + breakpoints[next_line] = true + elseif rest:sub(1, 5) == "WATCH" then + local condition = rest:sub(6):match("^%s*(.-)%s*$") + if condition ~= "" then + add_action(next_line, { type = "watch", condition = condition }) + end + elseif rest:sub(1, 7) == "UNWATCH" then + local condition = rest:sub(8):match("^%s*(.-)%s*$") + if condition ~= "" then + add_action(next_line, { type = "unwatch", condition = condition }) + end + end + end + end + + return breakpoints, actions +end + +return M diff --git a/lua_src/sild/session.lua b/lua_src/sild/session.lua new file mode 100644 index 0000000..258116c --- /dev/null +++ b/lua_src/sild/session.lua @@ -0,0 +1,11 @@ +local M = {} + +-- Global Session States +M.history = {} +M.watches = {} +M.breakpoints = {} +M.actions = {} +M.continue_until = nil +M.calls = {} + +return M diff --git a/lua_src/sild/ui.lua b/lua_src/sild/ui.lua new file mode 100644 index 0000000..2c8df39 --- /dev/null +++ b/lua_src/sild/ui.lua @@ -0,0 +1,109 @@ +local ansi = require("sild.ansi") +local highlight = require("sild.highlight") +local session = require("sild.session") +local config = require("sild.config").config + +local M = {} + +-- TODO: Make part of config +local SPLIT_POSITION = 0.35 + +do + if config.auto_size then + local f = io.popen("stty size 2>/dev/null") + if f then + local out = f:read("*l") + f:close() + if out then + local rows, cols = out:match("^(%d+)%s+(%d+)$") + rows, cols = tonumber(rows), tonumber(cols) + if rows and cols then + config.code_lines = math.max(5, rows - 8) + config.hist_width = math.max(20, math.floor(cols * SPLIT_POSITION)) + config.code_inner = math.max(20, cols - config.hist_width - 4) + end + end + end + end +end + +local function layout() + return config.code_lines, config.code_inner, config.hist_width, + math.floor(config.code_lines / 2) +end + +function M.fmt_header(src, line) + return ansi.color_header("[") .. src .. ansi.color_header(":") .. + ansi.color_linenumber(tostring(line)) .. ansi.color_header("]") +end + +function M.draw(info) + io.write("\027[2J\027[H") + + local CODE_LINES, CODE_INNER, HIST_WIDTH, HALF = layout() + local cur = info and info.currentline or 0 + local path = info and info.source:sub(1, 1) == "@" and info.source:sub(2) or nil + local src_key = info and info.source + + local code_rows = {} + if path then + local all = highlight.read_file(path) + local last = math.min(#all, cur + HALF) + local first = math.max(1, math.min(cur - HALF, last - CODE_LINES + 1)) + + for i = first, last do + local is_cur = (i == cur) + local has_bp = session.breakpoints[src_key] and session.breakpoints[src_key][i] + local num_plain = string.format("%4d", i) + local avail = CODE_INNER - 8 + local raw = (all[i] or ""):gsub("%s+$", "") + local code_plain = raw:sub(1, avail) + local plain_len = 4 + 1 + 2 + 1 + #code_plain + + local num_col = has_bp and ansi.color_breakpoint(num_plain) or ansi.color_dim(num_plain) + local mark_col = is_cur and ansi.color_current(">>") or ansi.color_dim(" ") + local code_col = highlight.highlight(code_plain) + if is_cur then code_col = ansi.color_bold(code_col) end + + code_rows[#code_rows + 1] = { + colored = num_col .. " " .. mark_col .. " " .. code_col, + plain_len = plain_len, + } + end + end + while #code_rows < CODE_LINES do + code_rows[#code_rows + 1] = { colored = "", plain_len = 0 } + end + + local entries_visible = math.floor(CODE_LINES / 2) + local start = math.max(1, #session.history - entries_visible + 1) + + local hist_lines = {} + for i = start, #session.history do + local h = session.history[i] + local trimmed = h.text:match("^%s*(.-)%s*$") + hist_lines[#hist_lines + 1] = M.fmt_header(h.src, h.line) + hist_lines[#hist_lines + 1] = " " .. ansi.color_dim(">") .. " " .. trimmed + end + while #hist_lines < CODE_LINES do + hist_lines[#hist_lines + 1] = "" + end + + local top = "╭" .. string.rep("─", CODE_INNER) .. "╮" + local bot = "╰" .. string.rep("─", CODE_INNER) .. "╯" + + local pad = string.rep(" ", HIST_WIDTH + 2) + io.write(pad .. top .. "\n") + for i = 1, CODE_LINES do + local hl = hist_lines[i] or "" + local cr = code_rows[i] + local spaces = math.max(0, CODE_INNER - cr.plain_len) + io.write( + ansi.fit(hl, HIST_WIDTH) .. " │" .. + cr.colored .. string.rep(" ", spaces) .. "│\n" + ) + end + io.write(pad .. bot .. "\n\n") +end + +return M |
