code / lua-debug-tui

Lines1.5K Lua1.5K Markdown33
(1.0K lines)
1 C = require "curses"
2 re = require 're'
3 line_matcher = re.compile('lines<-{| line ("\n" line)* |} line<-{[^\n]*}')
4 local ldb
5 AUTO = {} -- Singleton
6 PARENT = {} -- Singleton
8 -- TODO: add support for stepping debugger
9 -- TODO: add vim-style search functionality via "/"
11 _error = error
12 _assert = assert
14 -- Return the callstack index of the code that actually caused an error and the max index
15 callstack_range = ->
16 min, max = 0, -1
17 for i=1,999 do
18 info = debug.getinfo(i, 'f')
19 if not info
20 min = i-1
21 break
22 if info.func == ldb.run_debugger
23 min = i+2
24 break
25 for i=min,999
26 info = debug.getinfo(i, 'f')
27 if not info or info.func == ldb.guard
28 max = i-3
29 break
30 return min, max
33 wrap_text = (text, width)->
34 lines = {}
35 for line in *line_matcher\match(text)
36 while #line > width
37 table.insert(lines, line\sub(1,width))
38 line = line\sub(width+1,-1)
39 if #line == 0
40 line = nil
41 if line
42 table.insert(lines, line)
43 return lines
46 local Color
47 do
48 color_index = 0
49 existing = {}
50 make_color = (fg=-1, bg=-1)->
51 key = "#{fg},#{bg}"
52 unless existing[key]
53 color_index += 1
54 C.init_pair(color_index, fg, bg)
55 existing[key] = C.color_pair(color_index)
56 return existing[key]
57 color_lang = re.compile[[
58 x <- {|
59 {:attrs: {| {attr} (" " {attr})* |} :}
60 / (
61 ({:bg: "on " {color} :} / ({:fg: color :} (" on " {:bg: color :})?))
62 {:attrs: {| (" " {attr})* |} :})
63 |}
64 attr <- "blink" / "bold" / "dim" / "invis" / "normal" / "protect" / "reverse" / "standout" / "underline" / "altcharset"
65 color <- "black" / "blue" / "cyan" / "green" / "magenta" / "red" / "white" / "yellow" / "default"
66 ]]
67 C.COLOR_DEFAULT = -1
68 Color = (s="default")->
69 t = _assert(color_lang\match(s), "Invalid color: #{s}")
70 if t.fg then t.fg = C["COLOR_"..t.fg\upper!]
71 if t.bg then t.bg = C["COLOR_"..t.bg\upper!]
72 c = make_color(t.fg, t.bg)
73 for a in *t.attrs
74 c |= C["A_"..a\upper!]
75 return c
78 class Pad
79 new: (@label,@y,@x,height,width,...)=>
80 @scroll_y, @scroll_x = 1, 1
81 @selected = nil
83 @columns = {}
84 @column_widths = {}
85 @active_frame = Color("yellow bold")
86 @inactive_frame = Color("blue dim")
87 @colors = {}
88 for i=1,select('#',...)-1,2
89 col = select(i, ...)
90 table.insert(@columns, col)
91 w = 0
92 for chunk in *col do w = math.max(w, #chunk)
93 table.insert(@column_widths, w)
94 color_fn = select(i+1,...) or ((i)=>Color())
95 _assert(type(color_fn) == 'function', "Invalid color function type: #{type color_fn}")
96 table.insert(@colors, color_fn)
98 @configure_size height, width
99 @_frame = C.newwin(@height, @width, @y, @x)
100 @_frame\immedok(true)
101 @_pad = C.newpad(@_height, @_width)
102 @_pad\scrollok(true)
103 @set_active false
104 @chstrs = {}
105 for i=1,#@columns[1]
106 @chstrs[i] = C.new_chstr(@_width)
107 @setup_chstr(i)
108 @dirty = true
110 configure_size: (@height, @width)=>
111 @_height = math.max(#@columns[1], 1)
112 if @height == AUTO
113 @height = @_height + 2
115 @_width = #@columns-1
116 for i,col in ipairs(@columns)
117 col_width = 0
118 for chunk in *col do col_width = math.max(col_width, #chunk)
119 @_width += col_width
120 @_width = math.max(@_width, 1)
121 if @width == AUTO
122 @width = @_width + 2
124 setup_chstr: (i)=>
125 chstr = _assert(@chstrs[i], "Failed to find chstrs[#{i}]")
126 x = 0
127 for c=1,#@columns
128 attr = @colors[c](@, i)
129 chunk = @columns[c][i]
130 chstr\set_str(x, chunk, attr)
131 x += #chunk
132 if #chunk < @column_widths[c]
133 chstr\set_str(x, " ", attr, @column_widths[c]-#chunk)
134 x += @column_widths[c]-#chunk
135 if c < #@columns
136 chstr\set_ch(x, C.ACS_VLINE, Color("black bold"))
137 x += 1
138 @_pad\mvaddchstr(i-1,0,chstr)
139 @dirty = true
141 set_active: (active)=>
142 return if active == @active
143 @active = active
144 @_frame\attrset(active and @active_frame or @inactive_frame)
145 @dirty = true
147 select: (i)=>
148 if #@columns[1] == 0 then i = nil
149 if i == @selected then return @selected
150 old_y, old_x = @scroll_y, @scroll_x
151 if i != nil
152 i = math.max(1, math.min(#@columns[1], i))
154 old_selected,@selected = @selected,i
156 if old_selected
157 @setup_chstr(old_selected)
159 if @selected
160 @setup_chstr(@selected)
162 scrolloff = 3
163 if @selected > @scroll_y + (@height-2) - scrolloff
164 @scroll_y = @selected - (@height-2) + scrolloff
165 elseif @selected < @scroll_y + scrolloff
166 @scroll_y = @selected - scrolloff
167 @scroll_y = math.max(1, math.min(@_height, @scroll_y))
169 if @scroll_y == old_y
170 w = math.min(@width-2,@_width)
171 if old_selected and @scroll_y <= old_selected and old_selected <= @scroll_y + @height-2
172 @_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)
173 if @selected and @scroll_y <= @selected and @selected <= @scroll_y + @height-2
174 @_pad\pnoutrefresh(@selected-1,@scroll_x-1,@y+1+(@selected-@scroll_y),@x+1,@y+1+(@selected-@scroll_y)+1,@x+w)
175 else
176 @dirty = true
178 if @on_select then @on_select(@selected)
179 return @selected
181 scroll: (dy,dx)=>
182 old_y, old_x = @scroll_y, @scroll_x
183 if @selected != nil
184 @select(@selected + (dy or 0))
185 else
186 @scroll_y = math.max(1, math.min(@_height-(@height-2-1), @scroll_y+(dy or 0)))
187 @scroll_x = math.max(1, math.min(@_width-(@width-2-1), @scroll_x+(dx or 0)))
188 if @scroll_y != old_y or @scroll_x != old_x
189 @dirty = true
191 refresh: (force=false)=>
192 return if not force and not @dirty
193 @_frame\border(C.ACS_VLINE, C.ACS_VLINE,
194 C.ACS_HLINE, C.ACS_HLINE,
195 C.ACS_ULCORNER, C.ACS_URCORNER,
196 C.ACS_LLCORNER, C.ACS_LRCORNER)
197 if @label
198 @_frame\mvaddstr(0, math.floor((@width-#@label-2)/2), " #{@label} ")
199 @_frame\refresh!
200 --@_frame\prefresh(0,0,@y,@x,@y+@height-1,@x+@width-1)
201 h,w = math.min(@height-2,@_height),math.min(@width-2,@_width)
202 @_pad\pnoutrefresh(@scroll_y-1,@scroll_x-1,@y+1,@x+1,@y+h,@x+w)
203 @dirty = false
205 keypress: (c)=>
206 switch c
207 when C.KEY_DOWN, C.KEY_SR, ("j")\byte!
208 @scroll(1,0)
209 when ('J')\byte!
210 @scroll(10,0)
212 when C.KEY_UP, C.KEY_SF, ("k")\byte!
213 @scroll(-1,0)
214 when ('K')\byte!
215 @scroll(-10,0)
217 when C.KEY_RIGHT, ("l")\byte!
218 @scroll(0,1)
219 when ("L")\byte!
220 @scroll(0,10)
222 when C.KEY_LEFT, ("h")\byte!
223 @scroll(0,-1)
224 when ("H")\byte!
225 @scroll(0,-10)
227 erase: =>
228 @dirty = true
229 @_frame\erase!
230 @_frame\refresh!
232 __gc: =>
233 @_frame\close!
234 @_pad\close!
236 class NumberedPad extends Pad
237 new: (@label,@y,@x,height,width,...)=>
238 col1 = select(1, ...)
239 fmt = "%#{#tostring(#col1)}d"
240 line_nums = [fmt\format(i) for i=1,#col1]
241 cols = {line_nums, ((i)=> i == @selected and Color() or Color("yellow")), ...}
242 super @label, @y, @x, height, width, unpack(cols)
245 expansions = {}
246 TOP_LOCATION, KEY, VALUE = {}, {}, {}
247 locations = {}
248 Location = (old_loc, kind, key)->
249 if old_loc == nil
250 return TOP_LOCATION
251 unless locations[old_loc]
252 locations[old_loc] = {}
253 unless locations[old_loc][kind]
254 locations[old_loc][kind] = {}
255 unless locations[old_loc][kind][key]
256 locations[old_loc][kind][key] = {:old_loc, :kind, :key}
257 return locations[old_loc][kind][key]
259 expand = (kind, key, location)->
260 expansions[Location(location, kind, key)] = true
261 collapse = (kind, key, location)->
262 expansions[Location(location, kind, key)] = nil
263 is_key_expanded = (location, key)->
264 expansions[Location(location, KEY, key)]
265 is_value_expanded = (location, key)->
266 expansions[Location(location, VALUE, key)]
268 TYPE_COLORS = setmetatable({}, {__index: 0})
270 colored_repr = (x, width, depth=2)->
271 depth -= 1
272 x_type = type(x)
273 if x_type == 'table' then
274 if next(x) == nil
275 return {"{}", TYPE_COLORS.table}
276 if depth == 0
277 return {"{", TYPE_COLORS.table, "...", Color('white'), "}", TYPE_COLORS.table}
278 ret = {"{", TYPE_COLORS.table}
279 i = 1
280 for k, v in pairs(x)
281 if k == i
282 for s in *colored_repr(x[i], width, depth) do ret[#ret+1] = s
283 i = i + 1
284 else
285 for s in *colored_repr(k, width, depth) do ret[#ret+1] = s
286 ret[#ret+1] = ' = '
287 ret[#ret+1] = Color('white')
288 for s in *colored_repr(v, width, depth) do ret[#ret+1] = s
289 ret[#ret+1] = ', '
290 ret[#ret+1] = Color('white')
291 if #ret > 2
292 ret[#ret] = nil
293 ret[#ret] = nil
294 len = 0
295 for i=1,#ret-1,2 do len += #ret[i]
296 for i=#ret-1,3,-2
297 if len <= width-1
298 break
299 if ret[i+2]
300 ret[i+2], ret[i+3] = nil, nil
301 ret[i] = '...'
302 ret[i+1] = Color('white')
303 ret[#ret+1] = '}'
304 ret[#ret+1] = TYPE_COLORS.table
305 return ret
306 elseif x_type == 'string'
307 ret = {(x\match('^[^\t\r\v\b\a\n]*')), TYPE_COLORS.string}
308 for escape, line in x\gmatch('([\t\r\v\b\a\n])([^\t\r\v\b\a\n]*)')
309 ret[#ret+1] = '\\'..({['\t']:'t',['\r']:'r',['\v']:'v',['\b']:'b',['\a']:'a',['\n']:'n'})[escape]
310 ret[#ret+1] = Color('white on black')
311 ret[#ret+1] = line
312 ret[#ret+1] = TYPE_COLORS.string
313 len = 0
314 for i=1,#ret-1,2 do len += #ret[i]
315 for i=#ret-1,1,-2
316 if len <= width then break
317 if ret[i+2]
318 ret[i+2], ret[i+3] = nil, nil
319 len -= #ret[i]
320 if len <= width
321 ret[i] = ret[i]\sub(1, width-len-3)
322 ret[i+2] = '...'
323 ret[i+3] = Color('blue')
324 break
325 return ret
326 else
327 ok,s = pcall(tostring,x)
328 if not ok
329 return {"tostring error: "..s, Color("red")}
330 return if #s > width
331 {s\sub(1,width-3), TYPE_COLORS[type(x)], '...', Color('blue')}
332 else
333 {s, TYPE_COLORS[type(x)]}
335 make_lines = (location, x, width)->
336 -- Return a list of {location=location, text1, color1, text2, color2, ...}
337 switch type(x)
338 when 'string'
339 lines = {}
340 for line in *line_matcher\match(x)
341 wrapped = wrap_text(line, width-1)
342 for i,subline in ipairs(wrapped)
343 _line = {location:location}
344 if i > 1
345 table.insert(_line, C.ACS_BULLET)
346 table.insert(_line, Color('black bold altcharset'))
347 table.insert(_line, subline)
348 table.insert(_line, Color('blue on black'))
349 table.insert(lines, _line)
350 if #lines == 0
351 table.insert lines, {:location, "''", Color('blue')}
352 return lines
353 when 'table'
354 prepend = (line, ...)->
355 for i=1,select('#', ...)
356 table.insert(line, i, (select(i, ...)))
357 lines = {}
358 for k,v in pairs(x)
359 if is_key_expanded(location, k) and is_value_expanded(location, k)
360 table.insert lines, {
361 location:Location(location,KEY,k), 'key', Color('green bold'),
362 '/', Color!, 'value', Color('blue bold'), ':', Color('white')}
363 key_lines = make_lines(Location(location, KEY, k), k, width-1)
364 for i,key_line in ipairs(key_lines)
365 if i == 1
366 prepend(key_line, ' ', Color!, C.ACS_DIAMOND, Color('green bold'), ' ', Color!)
367 else
368 prepend(key_line, ' ', Color!)
369 table.insert(lines, key_line)
370 value_lines = make_lines(Location(location, VALUE, k), v, width-2)
371 for i,value_line in ipairs(value_lines)
372 if i == 1
373 prepend(value_line, ' ', Color!, C.ACS_DIAMOND, Color('blue bold'), ' ', Color!)
374 else
375 prepend(value_line, ' ', Color!)
376 table.insert(lines, value_line)
377 elseif is_value_expanded(location, k)
378 k_str = colored_repr(k,width-1)
379 table.insert lines, {
380 location:Location(location, KEY, k),
381 '-', Color('red'), unpack(k_str)
384 v_lines = make_lines(Location(location, VALUE, k), v, width-1)
385 prepend(v_lines[1], ' ', Color!)
386 for i=2,#v_lines
387 prepend(v_lines[i], ' ', Color!)
388 for v_line in *v_lines do table.insert(lines, v_line)
389 elseif is_key_expanded(location, k)
390 k_lines = make_lines(Location(location, KEY, k), k, width-4)
391 for i=1,#k_lines
392 prepend(k_lines[i], ' ', Color!)
393 for k_line in *k_lines do table.insert(lines, k_line)
395 v_str = colored_repr(v,width-2)
396 table.insert(lines, {location:Location(location, VALUE, k), ' ', Color!, unpack(v_str)})
397 else
398 k_space = math.floor((width-4)/3)
399 k_str = colored_repr(k,k_space)
400 v_space = (width-4)-#k_str
401 v_str = colored_repr(v,v_space)
402 line = {
403 location:Location(location, VALUE, k),
404 '+', Color('green'),
405 unpack(k_str)
407 table.insert line, ' = '
408 table.insert line, Color('white')
409 for s in *v_str do table.insert(line, s)
410 table.insert(lines, line)
411 if #lines == 0
412 table.insert lines, {:location, '{}', TYPE_COLORS.table}
413 return lines
414 else
415 if getmetatable(x) and getmetatable(x).__pairs
416 lines = make_lines(location, {k,v for k,v in pairs(x)}, width)
417 if getmetatable(x).__tostring
418 s_lines = {}
419 ok, s = pcall(tostring, x)
420 if not ok then s = "tostring error: "..s
421 for line in *line_matcher\match(s)
422 wrapped = wrap_text(line, width)
423 for i,subline in ipairs(wrapped)
424 table.insert(s_lines, {:location, subline, ok and Color('yellow') or Color('red')})
425 for i=1,#s_lines
426 table.insert(lines, i, s_lines[i])
427 return lines
428 str = tostring(x)
429 if #str > width
430 str = str\sub(1,width-3)..'...'
431 return {{:location, str, TYPE_COLORS[type(x)]}}
434 class DataViewer extends Pad
435 new: (@data,@label,@y,@x,height,width)=>
436 @scroll_y, @scroll_x = 1, 1
437 @selected = nil
439 @active_frame = Color("yellow bold")
440 @inactive_frame = Color("blue dim")
442 @full_refresh = ->
443 old_location = @selected and @chstr_locations and @chstr_locations[@selected]
444 @chstrs, @chstr_locations = {}, {}
445 W = width-3
446 lines = make_lines(TOP_LOCATION, @data, W)
447 for i,line in ipairs(lines)
448 chstr = C.new_chstr(W)
449 if i == @selected
450 chstr\set_ch(0, C.ACS_RARROW, Color('yellow bold'))
451 else
452 chstr\set_str(0, ' ', Color('yellow bold'))
453 offset = 1
454 for j=1,#line-1,2
455 chunk, attrs = line[j], line[j+1]
456 if type(chunk) == 'number'
457 chstr\set_ch(offset, chunk, attrs)
458 offset += 1
459 else
460 chstr\set_str(offset, chunk, attrs)
461 offset += #chunk
462 if offset < W
463 chstr\set_str(offset, ' ', attrs, W-offset)
464 table.insert @chstrs, chstr
465 table.insert @chstr_locations, line.location
467 @_height, @_width = #@chstrs, @width-2
468 @_pad\resize(@_height, @_width)
469 for i,chstr in ipairs(@chstrs)
470 @_pad\mvaddchstr(i-1,0,chstr)
471 @dirty = true
472 if old_location
473 for i,loc in ipairs(@chstr_locations)
474 if loc == old_location
475 @select(i)
476 break
478 @height, @width = height, width
479 @_frame = C.newwin(@height, @width, @y, @x)
480 @_frame\immedok(true)
481 @_pad = C.newpad(@height-2, @width-2)
482 @_pad\scrollok(true)
483 @set_active false
485 @full_refresh!
486 @select 1
488 setup_chstr:(i)=>
490 configure_size: (@height, @width)=>
491 @_height, @_width = #@chstrs, @width-2
493 select:(i)=>
494 if #@chstrs == 0 then i = nil
495 if i == @selected then return @selected
496 old_y, old_x = @scroll_y, @scroll_x
497 if i != nil
498 i = math.max(1, math.min(#@chstrs, i))
500 old_selected,@selected = @selected,i
502 if old_selected and @chstrs[old_selected]
503 @chstrs[old_selected]\set_str(0, ' ', Color('yellow bold'))
504 @_pad\mvaddchstr(old_selected-1,0,@chstrs[old_selected])
506 if @selected
507 @chstrs[@selected]\set_ch(0, C.ACS_RARROW, Color('yellow bold'))
508 @_pad\mvaddchstr(@selected-1,0,@chstrs[@selected])
510 scrolloff = 3
511 if @selected > @scroll_y + (@height-2) - scrolloff
512 @scroll_y = @selected - (@height-2) + scrolloff
513 elseif @selected < @scroll_y + scrolloff
514 @scroll_y = @selected - scrolloff
515 @scroll_y = math.max(1, math.min(@_height, @scroll_y))
517 if @scroll_y == old_y
518 w = math.min(@width-2,@_width)
519 if old_selected and @scroll_y <= old_selected and old_selected <= @scroll_y + @height-2
520 @_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)
521 if @selected and @scroll_y <= @selected and @selected <= @scroll_y + @height-2
522 @_pad\pnoutrefresh(@selected-1,@scroll_x-1,@y+1+(@selected-@scroll_y),@x+1,@y+1+(@selected-@scroll_y)+1,@x+w)
523 else
524 @dirty = true
526 if @on_select then @on_select(@selected)
527 return @selected
529 keypress: (c)=>
530 switch c
531 when C.KEY_DOWN, C.KEY_SR, ("j")\byte!
532 @scroll(1,0)
533 when ('J')\byte!
534 @scroll(10,0)
536 when C.KEY_UP, C.KEY_SF, ("k")\byte!
537 @scroll(-1,0)
538 when ('K')\byte!
539 @scroll(-10,0)
541 when C.KEY_RIGHT, ("l")\byte!
542 expansions[@chstr_locations[@selected]] = true
543 @full_refresh!
544 when ("L")\byte!
545 expansions[@chstr_locations[@selected]] = true
546 @full_refresh!
548 when C.KEY_LEFT, ("h")\byte!
549 loc = @chstr_locations[@selected]
550 if expansions[loc] == nil
551 loc = Location(loc.old_loc, (loc.kind == KEY and VALUE or KEY), loc.key)
552 while loc and expansions[loc] == nil
553 loc = loc.old_loc
554 if loc
555 expansions[loc] = nil
556 @full_refresh!
557 if loc and @chstr_locations[@selected] != loc
558 for i,chstr_loc in ipairs(@chstr_locations)
559 if chstr_loc == loc
560 @select(i)
561 break
562 elseif not loc
563 @select(1)
565 when ("H")\byte!
566 loc = @chstr_locations[@selected]
567 if expansions[loc] == nil
568 loc = Location(loc.old_loc, (loc.kind == KEY and VALUE or KEY), loc.key)
569 while loc and expansions[loc] == nil
570 loc = loc.old_loc
571 if loc
572 expansions[loc] = nil
573 @full_refresh!
574 if loc and @chstr_locations[@selected] != loc
575 for i,chstr_loc in ipairs(@chstr_locations)
576 if chstr_loc == loc
577 @select(i)
578 break
579 elseif not loc
580 @select(1)
582 ok, to_lua = pcall -> require('moonscript.base').to_lua
583 if not ok then to_lua = -> nil
584 file_cache = setmetatable({}, {__index:(filename)=>
585 file = io.open(filename)
586 if not file then return nil
587 contents = file\read("a")\sub(1,-2) -- drop the trailing "\n" that Lua adds for some reason
588 @[filename] = contents
589 return contents
591 line_tables = setmetatable({}, {__index:(filename)=>
592 file = file_cache[filename]
593 if not file
594 return nil
595 ok, line_table = to_lua(file)
596 if ok
597 @[filename] = line_table
598 return line_table
602 -- Cleanup curses and print the error to stdout like regular
603 err_hand = (err)->
604 C.endwin!
605 print "Error in debugger."
606 print(debug.traceback(err, 2))
607 os.exit(2)
609 ldb = {
610 run_debugger: (err_msg)->
611 local select_pad
612 err_msg or= ''
613 if type(err_msg) != 'string' then err_msg = tostring(err_msg)
614 stdscr = C.initscr!
615 SCREEN_H, SCREEN_W = stdscr\getmaxyx!
616 C.cbreak!
617 C.echo(false)
618 C.nl(false)
619 C.curs_set(0)
620 C.start_color!
621 C.use_default_colors!
622 with TYPE_COLORS
623 .string = Color('blue on black')
624 .number = Color('magenta')
625 .boolean = Color('cyan')
626 .nil = Color('cyan')
627 .table = Color('yellow')
628 .function = Color('green')
629 .userdata = Color('cyan bold')
630 .thread = Color('blue')
632 do -- Fullscreen flash
633 stdscr\wbkgd(Color"yellow on red bold")
634 stdscr\clear!
635 stdscr\refresh!
636 lines = wrap_text("ERROR!\n \n "..err_msg.."\n \npress any key...", math.floor(SCREEN_W-2))
637 max_line = 0
638 for line in *lines do max_line = math.max(max_line, #line)
639 for i, line in ipairs(lines)
640 if i == 1 or i == #lines
641 stdscr\mvaddstr(math.floor(SCREEN_H/2 - #lines/2)+i, math.floor((SCREEN_W-#line)/2), line)
642 else
643 stdscr\mvaddstr(math.floor(SCREEN_H/2 - #lines/2)+i, math.floor((SCREEN_W-max_line)/2), line)
644 stdscr\refresh!
645 C.doupdate!
646 stdscr\getch!
648 stdscr\keypad!
649 stdscr\wbkgd(Color!)
650 stdscr\clear!
651 stdscr\refresh!
653 pads = {}
655 do -- Err pad
656 err_msg_lines = wrap_text(err_msg, SCREEN_W - 4)
657 for i,line in ipairs(err_msg_lines)
658 err_msg_lines[i] = (" ")\rep(2)..line
659 height = math.min(#err_msg_lines+2, 7)
660 pads.err = Pad("(E)rror Message", 0,0,height,SCREEN_W, err_msg_lines, (i)=> Color("red bold"))
662 err_lines = {}
663 stack_sources = {}
664 stack_locations = {}
665 -- Map from stack index -> expr -> value
666 watch_exprs = setmetatable({}, {__index:(k)=>
667 t = {}
668 self[k] = t
669 return t
671 do -- Stack pad
672 stack_names = {}
673 max_filename, max_fn_name = 0, 0
674 stack_min, stack_max = callstack_range!
675 for i=stack_min,stack_max
676 info = debug.getinfo(i)
677 if not info then break
678 fn_name = info.name
679 unless fn_name
680 fn_name = if info.istailcall then "<tail call>"
681 else "<anonymous>"
683 table.insert(stack_names, fn_name)
684 line = if info.short_src
685 line_table = line_tables[info.short_src]
686 if line_table
687 char = line_table[info.currentline]
688 line_num = 1
689 file = file_cache[info.short_src] or info.source
690 for _ in file\sub(1,char)\gmatch("\n") do line_num += 1
691 "#{info.short_src}:#{line_num}"
692 else
693 info.short_src..":"..info.currentline
694 else
695 "???"
696 err_lines[line] = true
697 table.insert(stack_locations, line)
698 table.insert(stack_sources, info.source)
699 max_filename = math.max(max_filename, #line)
700 max_fn_name = math.max(max_fn_name, #fn_name)
701 max_fn_name, max_filename = 0, 0
702 for i=1,#stack_names do
703 max_fn_name = math.max(max_fn_name, #stack_names[i])
704 max_filename = math.max(max_filename, #stack_locations[i])
706 stack_h = math.floor(SCREEN_H*.6)--math.max(#stack_names+2, math.floor(2/3*SCREEN_H))
707 stack_w = math.min(max_fn_name + 3 + max_filename, math.floor(1/3*SCREEN_W))
708 pads.stack = Pad "(C)allstack",pads.err.height,SCREEN_W-stack_w,stack_h,stack_w,
709 stack_names, ((i)=> (i == @selected) and Color("black on green") or Color("green bold")),
710 stack_locations, ((i)=> (i == @selected) and Color("black on cyan") or Color("cyan bold"))
712 show_src = (filename, line_no, file_contents=nil)->
713 if pads.src
714 if pads.src.filename == filename
715 pads.src\select(line_no)
716 pads.src.colors[2] = (i)=>
717 return if i == line_no and i == @selected then Color("yellow on red bold")
718 elseif i == line_no then Color("yellow on red")
719 elseif err_lines["#{filename}:#{i}"] == true then Color("red on black bold")
720 elseif i == @selected then Color("reverse")
721 else Color()
722 for line,_ in pairs(err_lines)
723 _filename, i = line\match("([^:]*):(%d*).*")
724 if _filename == filename and tonumber(i)
725 pads.src\setup_chstr(tonumber(i))
726 pads.src\select(line_no)
727 return
728 else
729 pads.src\erase!
730 file_contents = file_contents or file_cache[filename]
731 if file_contents
732 src_lines = line_matcher\match(file_contents)
733 pads.src = NumberedPad "(S)ource Code", pads.err.height,0,
734 pads.stack.height,pads.stack.x, src_lines, (i)=>
735 return if i == line_no and i == @selected then Color("yellow on red bold")
736 elseif i == line_no then Color("yellow on red")
737 elseif err_lines["#{filename}:#{i}"] == true then Color("red on black bold")
738 elseif i == @selected then Color("reverse")
739 else Color()
740 pads.src\select(line_no)
741 else
742 lines = {}
743 for i=1,math.floor(pads.stack.height/2)-1 do table.insert(lines, "")
744 s = "<no source code found>"
745 s = (" ")\rep(math.floor((pads.stack.x-2-#s)/2))..s
746 table.insert(lines, s)
747 pads.src = Pad "(S)ource Code", pads.err.height,0,pads.stack.height,pads.stack.x,lines, ->Color("red")
748 pads.src.filename = filename
750 local stack_env
751 show_vars = (stack_index)->
752 if pads.vars
753 pads.vars\erase!
754 if pads.data
755 pads.data\erase!
756 callstack_min, _ = callstack_range!
757 var_names, values = {}, {}
758 stack_env = setmetatable({}, {__index:_G})
759 for loc=1,999
760 name, value = debug.getlocal(callstack_min+stack_index-1, loc)
761 if name == nil then break
762 table.insert(var_names, tostring(name))
763 table.insert(values, value)
764 stack_env[name] = value
765 num_locals = #var_names
766 info = debug.getinfo(callstack_min+stack_index-1,"uf")
767 for upval=1,info.nups
768 name,value = debug.getupvalue(info.func, upval)
769 if name == "_ENV" then continue
770 table.insert(var_names, tostring(name))
771 table.insert(values, value)
772 stack_env[name] = value
773 for watch in *watch_exprs[stack_index]
774 continue if stack_env[watch.expr] != nil
775 table.insert(var_names, watch.expr)
776 table.insert(values, watch.value)
777 stack_env[watch.expr] = watch.value
779 var_y = pads.stack.y + pads.stack.height
780 var_x = 0
781 --height = math.min(2+#var_names, SCREEN_H-pads.err.height-pads.stack.height)
782 height = SCREEN_H-(pads.err.height+pads.stack.height)
783 pads.vars = Pad "(V)ars", var_y,var_x,height,AUTO,var_names, (i)=>
784 color = if i <= num_locals then Color()
785 elseif i <= num_locals + info.nups - 1 then Color("blue")
786 else Color("green")
787 if i == @selected then color += C.A_REVERSE
788 return color
790 pads.vars.keypress = (key)=>
791 if key == ('l')\byte! or key == C.KEY_RIGHT
792 select_pad(pads.data)
793 else Pad.keypress(self, key)
795 pads.vars.on_select = (var_index)=>
796 if var_index == nil then return
797 value_x = pads.vars.x+pads.vars.width
798 value_w = SCREEN_W-(value_x)
799 value = stack_env[var_names[var_index]]--values[var_index]
800 type_str = tostring(type(value))
801 -- Show single value:
802 pads.data = DataViewer value, "(D)ata [#{type_str}]", var_y,value_x,pads.vars.height,value_w
803 pads.data.keypress = (key)=>
804 if (key == ('h')\byte! or key == C.KEY_LEFT) and @selected == 1
805 select_pad(pads.vars)
806 else DataViewer.keypress(self, key)
807 collectgarbage()
808 collectgarbage()
810 pads.vars\select(1)
812 pads.stack.on_select = (stack_index)=>
813 filename,line_no = pads.stack.columns[2][stack_index]\match("^(.*):(%d*)$")
814 --filename, line_no = pads.stack.lines[stack_index]\match("[^|]*| ([^:]*):(%d*).*")
815 line_no = tonumber(line_no)
816 show_src(filename, line_no, filename and file_cache[filename] or stack_sources[stack_index])
817 show_vars(stack_index)
819 pads.stack\select(1)
821 selected_pad = nil
822 select_pad = (pad)->
823 if selected_pad != pad
824 if selected_pad
825 selected_pad\set_active(false)
826 selected_pad\refresh!
827 selected_pad = pad
828 if selected_pad
829 selected_pad\set_active(true)
830 selected_pad\refresh!
832 select_pad(pads.stack)
834 while true
835 for _,p in pairs(pads)
836 p\refresh!
837 s = " press 'q' to quit "
838 stdscr\mvaddstr(math.floor(SCREEN_H - 1), math.floor((SCREEN_W-#s)), s)
839 --C.doupdate!
840 c = stdscr\getch!
841 switch c
842 when (':')\byte!, ('>')\byte!, ('?')\byte!
843 C.echo(true)
844 print_nil = false
845 local user_input
846 code = ''
847 if c == ('?')\byte!
848 stdscr\mvaddstr(SCREEN_H-1, 0, "? "..(' ')\rep(SCREEN_W-1))
849 stdscr\move(SCREEN_H-1, 2)
850 user_input = stdscr\getstr!
851 code = 'return '..user_input
852 print_nil = true
853 elseif c == (':')\byte! or c == ('>')\byte!
854 numlines = 1
855 stdscr\mvaddstr(SCREEN_H-1, 0, "> "..(' ')\rep(SCREEN_W-1))
856 stdscr\move(SCREEN_H-1, 2)
857 while true
858 line = stdscr\getstr!
859 if line == '' then break
860 code ..= line..'\n'
861 numlines += 1
862 stdscr\mvaddstr(SCREEN_H-numlines, 0, "> "..((' ')\rep(SCREEN_W)..'\n')\rep(numlines))
863 stdscr\mvaddstr(SCREEN_H-numlines, 2, code)
864 stdscr\mvaddstr(SCREEN_H-1, 0, (' ')\rep(SCREEN_W))
865 stdscr\move(SCREEN_H-1, 0)
866 C.echo(false)
867 output = ""
868 if not stack_env
869 stack_env = setmetatable({}, {__index:_G})
870 stack_env.print = (...)->
871 for i=1,select('#',...)
872 if i > 1 then output ..= '\t'
873 output ..= tostring(select(i, ...))
874 output ..= "\n"
876 for _,p in pairs(pads)
877 p\refresh(true)
879 run_fn, err_msg = load(code, 'user input', 't', stack_env)
880 if not run_fn
881 stdscr\attrset(Color('red bold'))
882 stdscr\addstr(err_msg)
883 stdscr\attrset(Color!)
884 else
885 ok, ret = pcall(run_fn)
886 if not ok
887 stdscr\attrset(Color('red bold'))
888 stdscr\addstr(ret)
889 stdscr\attrset(Color!)
890 elseif ret != nil or print_nil
891 value_bits = {'= ', Color('yellow'), unpack(colored_repr(ret, SCREEN_W-2, 4))}
892 numlines = 1
893 for i=1,#value_bits-1,2
894 for nl in value_bits[i]\gmatch('\n') do numlines += 1
895 for nl in output\gmatch('\n') do numlines += 1
896 y, x = SCREEN_H-numlines, 0
897 if output != ""
898 stdscr\mvaddstr(SCREEN_H-numlines, 0, output)
899 for nl in output\gmatch('\n') do y += 1
900 for i=1,#value_bits-1,2
901 stdscr\attrset(value_bits[i+1])
902 first_line = value_bits[i]\match('^[^\n]*')
903 stdscr\mvaddstr(y, x, first_line)
904 x += #first_line
905 for line in value_bits[i]\gmatch('\n([^\n]*)')
906 stdscr\mvaddstr(y,x,(' ')\rep(SCREEN_W-x))
907 y += 1
908 x = 0
909 stdscr\mvaddstr(y, x, line)
910 stdscr\attrset(Color!)
911 stdscr\mvaddstr(y,x,(' ')\rep(SCREEN_W-x))
913 if c == ("?")\byte! and ret != nil
914 replacing = false
915 watch_index = nil
916 watches = watch_exprs[pads.stack.selected]
917 for i,w in ipairs watches
918 if w.expr == user_input
919 w.value = ret
920 watch_index = i
921 break
922 unless watch_index
923 table.insert watches, {expr:user_input, value:ret}
924 watch_index = #watches
925 show_vars(pads.stack.selected)
926 --pads.vars\select(#pads.vars.columns[1] - #watches + watch_index)
927 for i,s in ipairs(pads.vars.columns[1])
928 if s == user_input
929 pads.vars\select(i)
930 break
931 select_pad(pads.data)
932 else
933 numlines = 0
934 for nl in output\gmatch('\n') do numlines += 1
935 stdscr\mvaddstr(SCREEN_H-numlines, 0, output)
938 when ('o')\byte!
939 file = stack_locations[pads.stack.selected]
940 filename,line_no = file\match("([^:]*):(.*)")
941 line_no = tostring(pads.src.selected)
942 -- Launch system editor and then redraw everything
943 C.endwin!
944 os.execute((os.getenv("EDITOR") or "nano").." +"..line_no.." "..filename)
945 stdscr = C.initscr!
946 C.cbreak!
947 C.echo(false)
948 C.nl(false)
949 C.curs_set(0)
950 C.start_color!
951 C.use_default_colors!
952 stdscr\clear!
953 stdscr\refresh!
954 for _,pad in pairs(pads) do pad\refresh(true)
956 when C.KEY_RESIZE
957 SCREEN_H, SCREEN_W = stdscr\getmaxyx!
958 stdscr\clear!
959 stdscr\refresh!
960 for _,pad in pairs(pads) do pad\refresh(true)
961 C.doupdate!
963 when ('q')\byte!, ("Q")\byte!
964 pads = {}
965 C.endwin!
966 return
968 when ('c')\byte!
969 select_pad(pads.stack) -- (C)allstack
971 when ('s')\byte!
972 select_pad(pads.src) -- (S)ource Code
974 when ('v')\byte!
975 select_pad(pads.vars) -- (V)ars
977 when ('d')\byte!
978 select_pad(pads.data) -- (D)ata
980 when ('e')\byte!
981 select_pad(pads.err) -- (E)rror
983 when C.KEY_DC, C.KEY_DL, C.KEY_BACKSPACE
984 if selected_pad == pads.vars
985 watches = watch_exprs[pads.stack.selected]
986 expr = pads.vars.columns[1][pads.vars.selected]
987 for i,w in ipairs watches
988 if w.expr == expr
989 table.remove(watches, i)
990 show_vars(pads.stack.selected)
991 select_pad(pads.vars)
992 break
994 else
995 if selected_pad
996 selected_pad\keypress(c)
998 C.endwin!
1000 guard: (fn, ...)->
1001 handler = (err_msg)->
1002 print(debug.traceback(err_msg, 2))
1003 xpcall(ldb.run_debugger, err_hand, err_msg)
1004 return xpcall(fn, handler, ...)
1006 breakpoint: ->
1007 return xpcall(ldb.run_debugger, err_hand, "Breakpoint triggered!")
1009 hijack: ->
1010 export error, assert
1011 error = (err_msg)->
1012 print(debug.traceback(err_msg, 2))
1013 xpcall(ldb.run_debugger, err_hand, err_msg)
1014 os.exit(2)
1016 assert = (condition, err_msg)->
1017 if not condition
1018 err_msg or= 'Assertion failed!'
1019 print(debug.traceback(err_msg, 2))
1020 xpcall(ldb.run_debugger, err_hand, err_msg)
1021 os.exit(2)
1022 return condition
1025 return ldb