C = require "curses" re = require 're' line_matcher = re.compile('lines<-{| line ("\n" line)* |} line<-{[^\n]*}') local ldb AUTO = {} -- Singleton PARENT = {} -- Singleton -- TODO: add support for stepping debugger -- TODO: add vim-style search functionality via "/" _error = error _assert = assert -- Return the callstack index of the code that actually caused an error and the max index callstack_range = -> min, max = 0, -1 for i=1,999 do info = debug.getinfo(i, 'f') if not info min = i-1 break if info.func == ldb.run_debugger min = i+2 break for i=min,999 info = debug.getinfo(i, 'f') if not info or info.func == ldb.guard max = i-3 break return min, max wrap_text = (text, width)-> lines = {} for line in *line_matcher\match(text) while #line > width table.insert(lines, line\sub(1,width)) line = line\sub(width+1,-1) if #line == 0 line = nil if line table.insert(lines, line) return lines local Color do color_index = 0 existing = {} make_color = (fg=-1, bg=-1)-> key = "#{fg},#{bg}" unless existing[key] color_index += 1 C.init_pair(color_index, fg, bg) existing[key] = C.color_pair(color_index) return existing[key] color_lang = re.compile[[ x <- {| {:attrs: {| {attr} (" " {attr})* |} :} / ( ({:bg: "on " {color} :} / ({:fg: color :} (" on " {:bg: color :})?)) {:attrs: {| (" " {attr})* |} :}) |} attr <- "blink" / "bold" / "dim" / "invis" / "normal" / "protect" / "reverse" / "standout" / "underline" / "altcharset" color <- "black" / "blue" / "cyan" / "green" / "magenta" / "red" / "white" / "yellow" / "default" ]] C.COLOR_DEFAULT = -1 Color = (s="default")-> t = _assert(color_lang\match(s), "Invalid color: #{s}") if t.fg then t.fg = C["COLOR_"..t.fg\upper!] if t.bg then t.bg = C["COLOR_"..t.bg\upper!] c = make_color(t.fg, t.bg) for a in *t.attrs c |= C["A_"..a\upper!] return c class Pad new: (@label,@y,@x,height,width,...)=> @scroll_y, @scroll_x = 1, 1 @selected = nil @columns = {} @column_widths = {} @active_frame = Color("yellow bold") @inactive_frame = Color("blue dim") @colors = {} for i=1,select('#',...)-1,2 col = select(i, ...) table.insert(@columns, col) w = 0 for chunk in *col do w = math.max(w, #chunk) table.insert(@column_widths, w) color_fn = select(i+1,...) or ((i)=>Color()) _assert(type(color_fn) == 'function', "Invalid color function type: #{type color_fn}") table.insert(@colors, color_fn) @configure_size height, width @_frame = C.newwin(@height, @width, @y, @x) @_frame\immedok(true) @_pad = C.newpad(@_height, @_width) @_pad\scrollok(true) @set_active false @chstrs = {} for i=1,#@columns[1] @chstrs[i] = C.new_chstr(@_width) @setup_chstr(i) @dirty = true configure_size: (@height, @width)=> @_height = math.max(#@columns[1], 1) if @height == AUTO @height = @_height + 2 @_width = #@columns-1 for i,col in ipairs(@columns) col_width = 0 for chunk in *col do col_width = math.max(col_width, #chunk) @_width += col_width @_width = math.max(@_width, 1) if @width == AUTO @width = @_width + 2 setup_chstr: (i)=> chstr = _assert(@chstrs[i], "Failed to find chstrs[#{i}]") x = 0 for c=1,#@columns attr = @colors[c](@, i) chunk = @columns[c][i] chstr\set_str(x, chunk, attr) x += #chunk if #chunk < @column_widths[c] chstr\set_str(x, " ", attr, @column_widths[c]-#chunk) x += @column_widths[c]-#chunk if c < #@columns chstr\set_ch(x, C.ACS_VLINE, Color("black bold")) x += 1 @_pad\mvaddchstr(i-1,0,chstr) @dirty = true set_active: (active)=> return if active == @active @active = active @_frame\attrset(active and @active_frame or @inactive_frame) @dirty = true select: (i)=> if #@columns[1] == 0 then i = nil if i == @selected then return @selected old_y, old_x = @scroll_y, @scroll_x if i != nil i = math.max(1, math.min(#@columns[1], i)) old_selected,@selected = @selected,i if old_selected @setup_chstr(old_selected) if @selected @setup_chstr(@selected) scrolloff = 3 if @selected > @scroll_y + (@height-2) - scrolloff @scroll_y = @selected - (@height-2) + scrolloff elseif @selected < @scroll_y + scrolloff @scroll_y = @selected - scrolloff @scroll_y = math.max(1, math.min(@_height, @scroll_y)) if @scroll_y == old_y w = math.min(@width-2,@_width) if old_selected and @scroll_y <= old_selected and old_selected <= @scroll_y + @height-2 @_pad\pnoutrefresh(old_selected-1,@scroll_x-1,@y+1+(old_selected-@scroll_y),@x+1,@y+1+(old_selected-@scroll_y)+1,@x+w) if @selected and @scroll_y <= @selected and @selected <= @scroll_y + @height-2 @_pad\pnoutrefresh(@selected-1,@scroll_x-1,@y+1+(@selected-@scroll_y),@x+1,@y+1+(@selected-@scroll_y)+1,@x+w) else @dirty = true if @on_select then @on_select(@selected) return @selected scroll: (dy,dx)=> old_y, old_x = @scroll_y, @scroll_x if @selected != nil @select(@selected + (dy or 0)) else @scroll_y = math.max(1, math.min(@_height-(@height-2-1), @scroll_y+(dy or 0))) @scroll_x = math.max(1, math.min(@_width-(@width-2-1), @scroll_x+(dx or 0))) if @scroll_y != old_y or @scroll_x != old_x @dirty = true refresh: (force=false)=> return if not force and not @dirty @_frame\border(C.ACS_VLINE, C.ACS_VLINE, C.ACS_HLINE, C.ACS_HLINE, C.ACS_ULCORNER, C.ACS_URCORNER, C.ACS_LLCORNER, C.ACS_LRCORNER) if @label @_frame\mvaddstr(0, math.floor((@width-#@label-2)/2), " #{@label} ") @_frame\refresh! --@_frame\prefresh(0,0,@y,@x,@y+@height-1,@x+@width-1) h,w = math.min(@height-2,@_height),math.min(@width-2,@_width) @_pad\pnoutrefresh(@scroll_y-1,@scroll_x-1,@y+1,@x+1,@y+h,@x+w) @dirty = false keypress: (c)=> switch c when C.KEY_DOWN, C.KEY_SR, ("j")\byte! @scroll(1,0) when ('J')\byte! @scroll(10,0) when C.KEY_UP, C.KEY_SF, ("k")\byte! @scroll(-1,0) when ('K')\byte! @scroll(-10,0) when C.KEY_RIGHT, ("l")\byte! @scroll(0,1) when ("L")\byte! @scroll(0,10) when C.KEY_LEFT, ("h")\byte! @scroll(0,-1) when ("H")\byte! @scroll(0,-10) erase: => @dirty = true @_frame\erase! @_frame\refresh! __gc: => @_frame\close! @_pad\close! class NumberedPad extends Pad new: (@label,@y,@x,height,width,...)=> col1 = select(1, ...) fmt = "%#{#tostring(#col1)}d" line_nums = [fmt\format(i) for i=1,#col1] cols = {line_nums, ((i)=> i == @selected and Color() or Color("yellow")), ...} super @label, @y, @x, height, width, unpack(cols) expansions = {} TOP_LOCATION, KEY, VALUE = {}, {}, {} locations = {} Location = (old_loc, kind, key)-> if old_loc == nil return TOP_LOCATION unless locations[old_loc] locations[old_loc] = {} unless locations[old_loc][kind] locations[old_loc][kind] = {} unless locations[old_loc][kind][key] locations[old_loc][kind][key] = {:old_loc, :kind, :key} return locations[old_loc][kind][key] expand = (kind, key, location)-> expansions[Location(location, kind, key)] = true collapse = (kind, key, location)-> expansions[Location(location, kind, key)] = nil is_key_expanded = (location, key)-> expansions[Location(location, KEY, key)] is_value_expanded = (location, key)-> expansions[Location(location, VALUE, key)] TYPE_COLORS = setmetatable({}, {__index: 0}) colored_repr = (x, width, depth=2)-> depth -= 1 x_type = type(x) if x_type == 'table' then if next(x) == nil return {"{}", TYPE_COLORS.table} if depth == 0 return {"{", TYPE_COLORS.table, "...", Color('white'), "}", TYPE_COLORS.table} ret = {"{", TYPE_COLORS.table} i = 1 for k, v in pairs(x) if k == i for s in *colored_repr(x[i], width, depth) do ret[#ret+1] = s i = i + 1 else for s in *colored_repr(k, width, depth) do ret[#ret+1] = s ret[#ret+1] = ' = ' ret[#ret+1] = Color('white') for s in *colored_repr(v, width, depth) do ret[#ret+1] = s ret[#ret+1] = ', ' ret[#ret+1] = Color('white') if #ret > 2 ret[#ret] = nil ret[#ret] = nil len = 0 for i=1,#ret-1,2 do len += #ret[i] for i=#ret-1,3,-2 if len <= width-1 break if ret[i+2] ret[i+2], ret[i+3] = nil, nil ret[i] = '...' ret[i+1] = Color('white') ret[#ret+1] = '}' ret[#ret+1] = TYPE_COLORS.table return ret elseif x_type == 'string' ret = {(x\match('^[^\t\r\v\b\a\n]*')), TYPE_COLORS.string} for escape, line in x\gmatch('([\t\r\v\b\a\n])([^\t\r\v\b\a\n]*)') ret[#ret+1] = '\\'..({['\t']:'t',['\r']:'r',['\v']:'v',['\b']:'b',['\a']:'a',['\n']:'n'})[escape] ret[#ret+1] = Color('white on black') ret[#ret+1] = line ret[#ret+1] = TYPE_COLORS.string len = 0 for i=1,#ret-1,2 do len += #ret[i] for i=#ret-1,1,-2 if len <= width then break if ret[i+2] ret[i+2], ret[i+3] = nil, nil len -= #ret[i] if len <= width ret[i] = ret[i]\sub(1, width-len-3) ret[i+2] = '...' ret[i+3] = Color('blue') break return ret else ok,s = pcall(tostring,x) if not ok return {"tostring error: "..s, Color("red")} return if #s > width {s\sub(1,width-3), TYPE_COLORS[type(x)], '...', Color('blue')} else {s, TYPE_COLORS[type(x)]} make_lines = (location, x, width)-> -- Return a list of {location=location, text1, color1, text2, color2, ...} switch type(x) when 'string' lines = {} for line in *line_matcher\match(x) wrapped = wrap_text(line, width-1) for i,subline in ipairs(wrapped) _line = {location:location} if i > 1 table.insert(_line, C.ACS_BULLET) table.insert(_line, Color('black bold altcharset')) table.insert(_line, subline) table.insert(_line, Color('blue on black')) table.insert(lines, _line) if #lines == 0 table.insert lines, {:location, "''", Color('blue')} return lines when 'table' prepend = (line, ...)-> for i=1,select('#', ...) table.insert(line, i, (select(i, ...))) lines = {} for k,v in pairs(x) if is_key_expanded(location, k) and is_value_expanded(location, k) table.insert lines, { location:Location(location,KEY,k), 'key', Color('green bold'), '/', Color!, 'value', Color('blue bold'), ':', Color('white')} key_lines = make_lines(Location(location, KEY, k), k, width-1) for i,key_line in ipairs(key_lines) if i == 1 prepend(key_line, ' ', Color!, C.ACS_DIAMOND, Color('green bold'), ' ', Color!) else prepend(key_line, ' ', Color!) table.insert(lines, key_line) value_lines = make_lines(Location(location, VALUE, k), v, width-2) for i,value_line in ipairs(value_lines) if i == 1 prepend(value_line, ' ', Color!, C.ACS_DIAMOND, Color('blue bold'), ' ', Color!) else prepend(value_line, ' ', Color!) table.insert(lines, value_line) elseif is_value_expanded(location, k) k_str = colored_repr(k,width-1) table.insert lines, { location:Location(location, KEY, k), '-', Color('red'), unpack(k_str) } v_lines = make_lines(Location(location, VALUE, k), v, width-1) prepend(v_lines[1], ' ', Color!) for i=2,#v_lines prepend(v_lines[i], ' ', Color!) for v_line in *v_lines do table.insert(lines, v_line) elseif is_key_expanded(location, k) k_lines = make_lines(Location(location, KEY, k), k, width-4) for i=1,#k_lines prepend(k_lines[i], ' ', Color!) for k_line in *k_lines do table.insert(lines, k_line) v_str = colored_repr(v,width-2) table.insert(lines, {location:Location(location, VALUE, k), ' ', Color!, unpack(v_str)}) else k_space = math.floor((width-4)/3) k_str = colored_repr(k,k_space) v_space = (width-4)-#k_str v_str = colored_repr(v,v_space) line = { location:Location(location, VALUE, k), '+', Color('green'), unpack(k_str) } table.insert line, ' = ' table.insert line, Color('white') for s in *v_str do table.insert(line, s) table.insert(lines, line) if #lines == 0 table.insert lines, {:location, '{}', TYPE_COLORS.table} return lines else if getmetatable(x) and getmetatable(x).__pairs lines = make_lines(location, {k,v for k,v in pairs(x)}, width) if getmetatable(x).__tostring s_lines = {} ok, s = pcall(tostring, x) if not ok then s = "tostring error: "..s for line in *line_matcher\match(s) wrapped = wrap_text(line, width) for i,subline in ipairs(wrapped) table.insert(s_lines, {:location, subline, ok and Color('yellow') or Color('red')}) for i=1,#s_lines table.insert(lines, i, s_lines[i]) return lines str = tostring(x) if #str > width str = str\sub(1,width-3)..'...' return {{:location, str, TYPE_COLORS[type(x)]}} class DataViewer extends Pad new: (@data,@label,@y,@x,height,width)=> @scroll_y, @scroll_x = 1, 1 @selected = nil @active_frame = Color("yellow bold") @inactive_frame = Color("blue dim") @full_refresh = -> old_location = @selected and @chstr_locations and @chstr_locations[@selected] @chstrs, @chstr_locations = {}, {} W = width-3 lines = make_lines(TOP_LOCATION, @data, W) for i,line in ipairs(lines) chstr = C.new_chstr(W) if i == @selected chstr\set_ch(0, C.ACS_RARROW, Color('yellow bold')) else chstr\set_str(0, ' ', Color('yellow bold')) offset = 1 for j=1,#line-1,2 chunk, attrs = line[j], line[j+1] if type(chunk) == 'number' chstr\set_ch(offset, chunk, attrs) offset += 1 else chstr\set_str(offset, chunk, attrs) offset += #chunk if offset < W chstr\set_str(offset, ' ', attrs, W-offset) table.insert @chstrs, chstr table.insert @chstr_locations, line.location @_height, @_width = #@chstrs, @width-2 @_pad\resize(@_height, @_width) for i,chstr in ipairs(@chstrs) @_pad\mvaddchstr(i-1,0,chstr) @dirty = true if old_location for i,loc in ipairs(@chstr_locations) if loc == old_location @select(i) break @height, @width = height, width @_frame = C.newwin(@height, @width, @y, @x) @_frame\immedok(true) @_pad = C.newpad(@height-2, @width-2) @_pad\scrollok(true) @set_active false @full_refresh! @select 1 setup_chstr:(i)=> configure_size: (@height, @width)=> @_height, @_width = #@chstrs, @width-2 select:(i)=> if #@chstrs == 0 then i = nil if i == @selected then return @selected old_y, old_x = @scroll_y, @scroll_x if i != nil i = math.max(1, math.min(#@chstrs, i)) old_selected,@selected = @selected,i if old_selected and @chstrs[old_selected] @chstrs[old_selected]\set_str(0, ' ', Color('yellow bold')) @_pad\mvaddchstr(old_selected-1,0,@chstrs[old_selected]) if @selected @chstrs[@selected]\set_ch(0, C.ACS_RARROW, Color('yellow bold')) @_pad\mvaddchstr(@selected-1,0,@chstrs[@selected]) scrolloff = 3 if @selected > @scroll_y + (@height-2) - scrolloff @scroll_y = @selected - (@height-2) + scrolloff elseif @selected < @scroll_y + scrolloff @scroll_y = @selected - scrolloff @scroll_y = math.max(1, math.min(@_height, @scroll_y)) if @scroll_y == old_y w = math.min(@width-2,@_width) if old_selected and @scroll_y <= old_selected and old_selected <= @scroll_y + @height-2 @_pad\pnoutrefresh(old_selected-1,@scroll_x-1,@y+1+(old_selected-@scroll_y),@x+1,@y+1+(old_selected-@scroll_y)+1,@x+w) if @selected and @scroll_y <= @selected and @selected <= @scroll_y + @height-2 @_pad\pnoutrefresh(@selected-1,@scroll_x-1,@y+1+(@selected-@scroll_y),@x+1,@y+1+(@selected-@scroll_y)+1,@x+w) else @dirty = true if @on_select then @on_select(@selected) return @selected keypress: (c)=> switch c when C.KEY_DOWN, C.KEY_SR, ("j")\byte! @scroll(1,0) when ('J')\byte! @scroll(10,0) when C.KEY_UP, C.KEY_SF, ("k")\byte! @scroll(-1,0) when ('K')\byte! @scroll(-10,0) when C.KEY_RIGHT, ("l")\byte! expansions[@chstr_locations[@selected]] = true @full_refresh! when ("L")\byte! expansions[@chstr_locations[@selected]] = true @full_refresh! when C.KEY_LEFT, ("h")\byte! loc = @chstr_locations[@selected] if expansions[loc] == nil loc = Location(loc.old_loc, (loc.kind == KEY and VALUE or KEY), loc.key) while loc and expansions[loc] == nil loc = loc.old_loc if loc expansions[loc] = nil @full_refresh! if loc and @chstr_locations[@selected] != loc for i,chstr_loc in ipairs(@chstr_locations) if chstr_loc == loc @select(i) break elseif not loc @select(1) when ("H")\byte! loc = @chstr_locations[@selected] if expansions[loc] == nil loc = Location(loc.old_loc, (loc.kind == KEY and VALUE or KEY), loc.key) while loc and expansions[loc] == nil loc = loc.old_loc if loc expansions[loc] = nil @full_refresh! if loc and @chstr_locations[@selected] != loc for i,chstr_loc in ipairs(@chstr_locations) if chstr_loc == loc @select(i) break elseif not loc @select(1) ok, to_lua = pcall -> require('moonscript.base').to_lua if not ok then to_lua = -> nil file_cache = setmetatable({}, {__index:(filename)=> file = io.open(filename) if not file then return nil contents = file\read("a")\sub(1,-2) -- drop the trailing "\n" that Lua adds for some reason @[filename] = contents return contents }) line_tables = setmetatable({}, {__index:(filename)=> file = file_cache[filename] if not file return nil ok, line_table = to_lua(file) if ok @[filename] = line_table return line_table }) -- Cleanup curses and print the error to stdout like regular err_hand = (err)-> C.endwin! print "Error in debugger." print(debug.traceback(err, 2)) os.exit(2) ldb = { run_debugger: (err_msg)-> local select_pad err_msg or= '' if type(err_msg) != 'string' then err_msg = tostring(err_msg) stdscr = C.initscr! SCREEN_H, SCREEN_W = stdscr\getmaxyx! C.cbreak! C.echo(false) C.nl(false) C.curs_set(0) C.start_color! C.use_default_colors! with TYPE_COLORS .string = Color('blue on black') .number = Color('magenta') .boolean = Color('cyan') .nil = Color('cyan') .table = Color('yellow') .function = Color('green') .userdata = Color('cyan bold') .thread = Color('blue') do -- Fullscreen flash stdscr\wbkgd(Color"yellow on red bold") stdscr\clear! stdscr\refresh! lines = wrap_text("ERROR!\n \n "..err_msg.."\n \npress any key...", math.floor(SCREEN_W-2)) max_line = 0 for line in *lines do max_line = math.max(max_line, #line) for i, line in ipairs(lines) if i == 1 or i == #lines stdscr\mvaddstr(math.floor(SCREEN_H/2 - #lines/2)+i, math.floor((SCREEN_W-#line)/2), line) else stdscr\mvaddstr(math.floor(SCREEN_H/2 - #lines/2)+i, math.floor((SCREEN_W-max_line)/2), line) stdscr\refresh! C.doupdate! stdscr\getch! stdscr\keypad! stdscr\wbkgd(Color!) stdscr\clear! stdscr\refresh! pads = {} do -- Err pad err_msg_lines = wrap_text(err_msg, SCREEN_W - 4) for i,line in ipairs(err_msg_lines) err_msg_lines[i] = (" ")\rep(2)..line height = math.min(#err_msg_lines+2, 7) pads.err = Pad("(E)rror Message", 0,0,height,SCREEN_W, err_msg_lines, (i)=> Color("red bold")) err_lines = {} stack_sources = {} stack_locations = {} -- Map from stack index -> expr -> value watch_exprs = setmetatable({}, {__index:(k)=> t = {} self[k] = t return t }) do -- Stack pad stack_names = {} max_filename, max_fn_name = 0, 0 stack_min, stack_max = callstack_range! for i=stack_min,stack_max info = debug.getinfo(i) if not info then break fn_name = info.name unless fn_name fn_name = if info.istailcall then "" else "" table.insert(stack_names, fn_name) line = if info.short_src line_table = line_tables[info.short_src] if line_table char = line_table[info.currentline] line_num = 1 file = file_cache[info.short_src] or info.source for _ in file\sub(1,char)\gmatch("\n") do line_num += 1 "#{info.short_src}:#{line_num}" else info.short_src..":"..info.currentline else "???" err_lines[line] = true table.insert(stack_locations, line) table.insert(stack_sources, info.source) max_filename = math.max(max_filename, #line) max_fn_name = math.max(max_fn_name, #fn_name) max_fn_name, max_filename = 0, 0 for i=1,#stack_names do max_fn_name = math.max(max_fn_name, #stack_names[i]) max_filename = math.max(max_filename, #stack_locations[i]) stack_h = math.floor(SCREEN_H*.6)--math.max(#stack_names+2, math.floor(2/3*SCREEN_H)) stack_w = math.min(max_fn_name + 3 + max_filename, math.floor(1/3*SCREEN_W)) pads.stack = Pad "(C)allstack",pads.err.height,SCREEN_W-stack_w,stack_h,stack_w, stack_names, ((i)=> (i == @selected) and Color("black on green") or Color("green bold")), stack_locations, ((i)=> (i == @selected) and Color("black on cyan") or Color("cyan bold")) show_src = (filename, line_no, file_contents=nil)-> if pads.src if pads.src.filename == filename pads.src\select(line_no) pads.src.colors[2] = (i)=> return if i == line_no and i == @selected then Color("yellow on red bold") elseif i == line_no then Color("yellow on red") elseif err_lines["#{filename}:#{i}"] == true then Color("red on black bold") elseif i == @selected then Color("reverse") else Color() for line,_ in pairs(err_lines) _filename, i = line\match("([^:]*):(%d*).*") if _filename == filename and tonumber(i) pads.src\setup_chstr(tonumber(i)) pads.src\select(line_no) return else pads.src\erase! file_contents = file_contents or file_cache[filename] if file_contents src_lines = line_matcher\match(file_contents) pads.src = NumberedPad "(S)ource Code", pads.err.height,0, pads.stack.height,pads.stack.x, src_lines, (i)=> return if i == line_no and i == @selected then Color("yellow on red bold") elseif i == line_no then Color("yellow on red") elseif err_lines["#{filename}:#{i}"] == true then Color("red on black bold") elseif i == @selected then Color("reverse") else Color() pads.src\select(line_no) else lines = {} for i=1,math.floor(pads.stack.height/2)-1 do table.insert(lines, "") s = "" s = (" ")\rep(math.floor((pads.stack.x-2-#s)/2))..s table.insert(lines, s) pads.src = Pad "(S)ource Code", pads.err.height,0,pads.stack.height,pads.stack.x,lines, ->Color("red") pads.src.filename = filename local stack_env show_vars = (stack_index)-> if pads.vars pads.vars\erase! if pads.data pads.data\erase! callstack_min, _ = callstack_range! var_names, values = {}, {} stack_env = setmetatable({}, {__index:_G}) for loc=1,999 name, value = debug.getlocal(callstack_min+stack_index-1, loc) if name == nil then break table.insert(var_names, tostring(name)) table.insert(values, value) stack_env[name] = value num_locals = #var_names info = debug.getinfo(callstack_min+stack_index-1,"uf") for upval=1,info.nups name,value = debug.getupvalue(info.func, upval) if name == "_ENV" then continue table.insert(var_names, tostring(name)) table.insert(values, value) stack_env[name] = value for watch in *watch_exprs[stack_index] continue if stack_env[watch.expr] != nil table.insert(var_names, watch.expr) table.insert(values, watch.value) stack_env[watch.expr] = watch.value var_y = pads.stack.y + pads.stack.height var_x = 0 --height = math.min(2+#var_names, SCREEN_H-pads.err.height-pads.stack.height) height = SCREEN_H-(pads.err.height+pads.stack.height) pads.vars = Pad "(V)ars", var_y,var_x,height,AUTO,var_names, (i)=> color = if i <= num_locals then Color() elseif i <= num_locals + info.nups - 1 then Color("blue") else Color("green") if i == @selected then color += C.A_REVERSE return color pads.vars.keypress = (key)=> if key == ('l')\byte! or key == C.KEY_RIGHT select_pad(pads.data) else Pad.keypress(self, key) pads.vars.on_select = (var_index)=> if var_index == nil then return value_x = pads.vars.x+pads.vars.width value_w = SCREEN_W-(value_x) value = stack_env[var_names[var_index]]--values[var_index] type_str = tostring(type(value)) -- Show single value: pads.data = DataViewer value, "(D)ata [#{type_str}]", var_y,value_x,pads.vars.height,value_w pads.data.keypress = (key)=> if (key == ('h')\byte! or key == C.KEY_LEFT) and @selected == 1 select_pad(pads.vars) else DataViewer.keypress(self, key) collectgarbage() collectgarbage() pads.vars\select(1) pads.stack.on_select = (stack_index)=> filename,line_no = pads.stack.columns[2][stack_index]\match("^(.*):(%d*)$") --filename, line_no = pads.stack.lines[stack_index]\match("[^|]*| ([^:]*):(%d*).*") line_no = tonumber(line_no) show_src(filename, line_no, filename and file_cache[filename] or stack_sources[stack_index]) show_vars(stack_index) pads.stack\select(1) selected_pad = nil select_pad = (pad)-> if selected_pad != pad if selected_pad selected_pad\set_active(false) selected_pad\refresh! selected_pad = pad if selected_pad selected_pad\set_active(true) selected_pad\refresh! select_pad(pads.stack) while true for _,p in pairs(pads) p\refresh! s = " press 'q' to quit " stdscr\mvaddstr(math.floor(SCREEN_H - 1), math.floor((SCREEN_W-#s)), s) --C.doupdate! c = stdscr\getch! switch c when (':')\byte!, ('>')\byte!, ('?')\byte! C.echo(true) print_nil = false local user_input code = '' if c == ('?')\byte! stdscr\mvaddstr(SCREEN_H-1, 0, "? "..(' ')\rep(SCREEN_W-1)) stdscr\move(SCREEN_H-1, 2) user_input = stdscr\getstr! code = 'return '..user_input print_nil = true elseif c == (':')\byte! or c == ('>')\byte! numlines = 1 stdscr\mvaddstr(SCREEN_H-1, 0, "> "..(' ')\rep(SCREEN_W-1)) stdscr\move(SCREEN_H-1, 2) while true line = stdscr\getstr! if line == '' then break code ..= line..'\n' numlines += 1 stdscr\mvaddstr(SCREEN_H-numlines, 0, "> "..((' ')\rep(SCREEN_W)..'\n')\rep(numlines)) stdscr\mvaddstr(SCREEN_H-numlines, 2, code) stdscr\mvaddstr(SCREEN_H-1, 0, (' ')\rep(SCREEN_W)) stdscr\move(SCREEN_H-1, 0) C.echo(false) output = "" if not stack_env stack_env = setmetatable({}, {__index:_G}) stack_env.print = (...)-> for i=1,select('#',...) if i > 1 then output ..= '\t' output ..= tostring(select(i, ...)) output ..= "\n" for _,p in pairs(pads) p\refresh(true) run_fn, err_msg = load(code, 'user input', 't', stack_env) if not run_fn stdscr\attrset(Color('red bold')) stdscr\addstr(err_msg) stdscr\attrset(Color!) else ok, ret = pcall(run_fn) if not ok stdscr\attrset(Color('red bold')) stdscr\addstr(ret) stdscr\attrset(Color!) elseif ret != nil or print_nil value_bits = {'= ', Color('yellow'), unpack(colored_repr(ret, SCREEN_W-2, 4))} numlines = 1 for i=1,#value_bits-1,2 for nl in value_bits[i]\gmatch('\n') do numlines += 1 for nl in output\gmatch('\n') do numlines += 1 y, x = SCREEN_H-numlines, 0 if output != "" stdscr\mvaddstr(SCREEN_H-numlines, 0, output) for nl in output\gmatch('\n') do y += 1 for i=1,#value_bits-1,2 stdscr\attrset(value_bits[i+1]) first_line = value_bits[i]\match('^[^\n]*') stdscr\mvaddstr(y, x, first_line) x += #first_line for line in value_bits[i]\gmatch('\n([^\n]*)') stdscr\mvaddstr(y,x,(' ')\rep(SCREEN_W-x)) y += 1 x = 0 stdscr\mvaddstr(y, x, line) stdscr\attrset(Color!) stdscr\mvaddstr(y,x,(' ')\rep(SCREEN_W-x)) if c == ("?")\byte! and ret != nil replacing = false watch_index = nil watches = watch_exprs[pads.stack.selected] for i,w in ipairs watches if w.expr == user_input w.value = ret watch_index = i break unless watch_index table.insert watches, {expr:user_input, value:ret} watch_index = #watches show_vars(pads.stack.selected) --pads.vars\select(#pads.vars.columns[1] - #watches + watch_index) for i,s in ipairs(pads.vars.columns[1]) if s == user_input pads.vars\select(i) break select_pad(pads.data) else numlines = 0 for nl in output\gmatch('\n') do numlines += 1 stdscr\mvaddstr(SCREEN_H-numlines, 0, output) when ('o')\byte! file = stack_locations[pads.stack.selected] filename,line_no = file\match("([^:]*):(.*)") line_no = tostring(pads.src.selected) -- Launch system editor and then redraw everything C.endwin! os.execute((os.getenv("EDITOR") or "nano").." +"..line_no.." "..filename) stdscr = C.initscr! C.cbreak! C.echo(false) C.nl(false) C.curs_set(0) C.start_color! C.use_default_colors! stdscr\clear! stdscr\refresh! for _,pad in pairs(pads) do pad\refresh(true) when C.KEY_RESIZE SCREEN_H, SCREEN_W = stdscr\getmaxyx! stdscr\clear! stdscr\refresh! for _,pad in pairs(pads) do pad\refresh(true) C.doupdate! when ('q')\byte!, ("Q")\byte! pads = {} C.endwin! return when ('c')\byte! select_pad(pads.stack) -- (C)allstack when ('s')\byte! select_pad(pads.src) -- (S)ource Code when ('v')\byte! select_pad(pads.vars) -- (V)ars when ('d')\byte! select_pad(pads.data) -- (D)ata when ('e')\byte! select_pad(pads.err) -- (E)rror when C.KEY_DC, C.KEY_DL, C.KEY_BACKSPACE if selected_pad == pads.vars watches = watch_exprs[pads.stack.selected] expr = pads.vars.columns[1][pads.vars.selected] for i,w in ipairs watches if w.expr == expr table.remove(watches, i) show_vars(pads.stack.selected) select_pad(pads.vars) break else if selected_pad selected_pad\keypress(c) C.endwin! guard: (fn, ...)-> handler = (err_msg)-> print(debug.traceback(err_msg, 2)) xpcall(ldb.run_debugger, err_hand, err_msg) return xpcall(fn, handler, ...) breakpoint: -> return xpcall(ldb.run_debugger, err_hand, "Breakpoint triggered!") hijack: -> export error, assert error = (err_msg)-> print(debug.traceback(err_msg, 2)) xpcall(ldb.run_debugger, err_hand, err_msg) os.exit(2) assert = (condition, err_msg)-> if not condition err_msg or= 'Assertion failed!' print(debug.traceback(err_msg, 2)) xpcall(ldb.run_debugger, err_hand, err_msg) os.exit(2) return condition } return ldb