lua-debug-tui/ldt.moon

1026 lines
40 KiB
Plaintext

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 "<tail call>"
else "<anonymous>"
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 = "<no source code found>"
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