log_browser.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. -- environment for immutable logs
  2. -- optionally reads extensions for rendering some types from the source codebase that generated them
  3. --
  4. -- We won't care too much about long, wrapped lines. If they lines get too
  5. -- long to manage, you need a better, graphical rendering for them. Load
  6. -- functions to render them into the log_render namespace.
  7. function source.initialize_log_browser_side()
  8. Log_browser_state = edit.initialize_state(Margin_top, Editor_state.right + Margin_right + Margin_left, (Editor_state.right+Margin_right)*2, Editor_state.font, Editor_state.font_height, Editor_state.line_height)
  9. Log_browser_state.filename = 'log'
  10. load_from_disk(Log_browser_state) -- TODO: pay no attention to Fold
  11. log_browser.parse(Log_browser_state)
  12. Text.redraw_all(Log_browser_state)
  13. Log_browser_state.screen_top1 = {line=1, pos=1}
  14. Log_browser_state.cursor1 = {line=1, pos=1}
  15. end
  16. Section_stack = {}
  17. Section_border_color = {r=0.7, g=0.7, b=0.7}
  18. Cursor_line_background_color = {r=0.7, g=0.7, b=0, a=0.1}
  19. Section_border_padding_horizontal = 30 -- TODO: adjust this based on font height (because we draw text vertically along the borders
  20. Section_border_padding_vertical = 15 -- TODO: adjust this based on font height
  21. log_browser = {}
  22. function log_browser.parse(State)
  23. for _,line in ipairs(State.lines) do
  24. if line.data ~= '' then
  25. local rest
  26. line.filename, line.line_number, rest = line.data:match('%[string "([^:]*)"%]:([^:]*):%s*(.*)')
  27. if line.filename == nil then
  28. line.filename, line.line_number, rest = line.data:match('([^:]*):([^:]*):%s*(.*)')
  29. end
  30. if rest then
  31. line.data = rest
  32. end
  33. line.line_number = tonumber(line.line_number)
  34. if line.data:sub(1,1) == '{' then
  35. local data = json.decode(line.data)
  36. if log_render[data.name] then
  37. line.data = data
  38. end
  39. line.section_stack = table.shallowcopy(Section_stack)
  40. elseif line.data:match('%[ u250c') then
  41. line.section_stack = table.shallowcopy(Section_stack) -- as it is at the beginning
  42. local section_name = line.data:match('u250c%s*(.*)')
  43. table.insert(Section_stack, {name=section_name})
  44. line.section_begin = true
  45. line.section_name = section_name
  46. line.data = nil
  47. elseif line.data:match('%] u2518') then
  48. local section_name = line.data:match('] u2518%s*(.*)')
  49. if array.find(Section_stack, function(x) return x.name == section_name end) then
  50. while table.remove(Section_stack).name ~= section_name do
  51. --
  52. end
  53. line.section_end = true
  54. line.section_name = section_name
  55. line.data = nil
  56. end
  57. line.section_stack = table.shallowcopy(Section_stack)
  58. else
  59. -- string
  60. line.section_stack = table.shallowcopy(Section_stack)
  61. end
  62. else
  63. line.section_stack = {}
  64. end
  65. end
  66. end
  67. function table.shallowcopy(x)
  68. return {unpack(x)}
  69. end
  70. function log_browser.draw(State, hide_cursor)
  71. assert(#State.lines == #State.line_cache, ('line_cache is out of date; %d elements when it should be %d'):format(#State.line_cache, #State.lines))
  72. local mouse_line_index = log_browser.line_index(State, App.mouse_x(), App.mouse_y())
  73. local y = State.top
  74. for line_index = State.screen_top1.line,#State.lines do
  75. App.color(Text_color)
  76. local line = State.lines[line_index]
  77. if y + State.line_height > App.screen.height then break end
  78. local height = State.line_height
  79. if should_show(line) then
  80. local xleft = render_stack_left_margin(State, line_index, line, y)
  81. local xright = render_stack_right_margin(State, line_index, line, y)
  82. if line.section_name then
  83. App.color(Section_border_color)
  84. if line.section_begin then
  85. local sectiony = y+Section_border_padding_vertical
  86. love.graphics.line(xleft,sectiony, xleft,y+State.line_height)
  87. love.graphics.line(xright,sectiony, xright,y+State.line_height)
  88. love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)
  89. love.graphics.print(line.section_name, xleft+50,y)
  90. love.graphics.line(xleft+50+State.font:getWidth(line.section_name)+2,sectiony, xright,sectiony)
  91. else assert(line.section_end, "log line has a section name, but it's neither the start nor end of a section")
  92. local sectiony = y+State.line_height-Section_border_padding_vertical
  93. love.graphics.line(xleft,y, xleft,sectiony)
  94. love.graphics.line(xright,y, xright,sectiony)
  95. love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)
  96. love.graphics.print(line.section_name, xleft+50,y)
  97. love.graphics.line(xleft+50+State.font:getWidth(line.section_name)+2,sectiony, xright,sectiony)
  98. end
  99. else
  100. if type(line.data) == 'string' then
  101. local old_left, old_right = State.left,State.right
  102. State.left,State.right = xleft,xright
  103. Text.draw(State, line_index, y, --[[startpos]] 1, hide_cursor)
  104. State.left,State.right = old_left,old_right
  105. else
  106. height = log_render[line.data.name](line.data, xleft, y, xright-xleft)
  107. end
  108. end
  109. if App.mouse_x() > Log_browser_state.left and line_index == mouse_line_index then
  110. App.color(Cursor_line_background_color)
  111. love.graphics.rectangle('fill', xleft,y, xright-xleft, height)
  112. end
  113. y = y + height
  114. end
  115. end
  116. end
  117. function render_stack_left_margin(State, line_index, line, y)
  118. if line.section_stack == nil then
  119. -- assertion message
  120. for k,v in pairs(line) do
  121. print(k)
  122. end
  123. end
  124. App.color(Section_border_color)
  125. for i=1,#line.section_stack do
  126. local x = State.left + (i-1)*Section_border_padding_horizontal
  127. love.graphics.line(x,y, x,y+log_browser.height(State, line_index))
  128. if y < 30 then
  129. love.graphics.print(line.section_stack[i].name, x+State.font_height+5, y+5, --[[vertically]] math.pi/2)
  130. end
  131. if y > App.screen.height-log_browser.height(State, line_index) then
  132. love.graphics.print(line.section_stack[i].name, x+State.font_height+5, App.screen.height-State.font:getWidth(line.section_stack[i].name)-5, --[[vertically]] math.pi/2)
  133. end
  134. end
  135. return log_browser.left_margin(State, line)
  136. end
  137. function render_stack_right_margin(State, line_index, line, y)
  138. App.color(Section_border_color)
  139. for i=1,#line.section_stack do
  140. local x = State.right - (i-1)*Section_border_padding_horizontal
  141. love.graphics.line(x,y, x,y+log_browser.height(State, line_index))
  142. if y < 30 then
  143. love.graphics.print(line.section_stack[i].name, x, y+5, --[[vertically]] math.pi/2)
  144. end
  145. if y > App.screen.height-log_browser.height(State, line_index) then
  146. love.graphics.print(line.section_stack[i].name, x, App.screen.height-State.font:getWidth(line.section_stack[i].name)-5, --[[vertically]] math.pi/2)
  147. end
  148. end
  149. return log_browser.right_margin(State, line)
  150. end
  151. function should_show(line)
  152. -- Show a line if every single section it's in is expanded.
  153. for i=1,#line.section_stack do
  154. local section = line.section_stack[i]
  155. if not section.expanded then
  156. return false
  157. end
  158. end
  159. return true
  160. end
  161. function log_browser.left_margin(State, line)
  162. return State.left + #line.section_stack*Section_border_padding_horizontal
  163. end
  164. function log_browser.right_margin(State, line)
  165. return State.right - #line.section_stack*Section_border_padding_horizontal
  166. end
  167. function log_browser.update(State, dt)
  168. end
  169. function log_browser.quit(State)
  170. end
  171. function log_browser.mouse_press(State, x,y, mouse_button)
  172. local line_index = log_browser.line_index(State, x,y)
  173. if line_index == nil then
  174. -- below lower margin
  175. return
  176. end
  177. -- leave some space to click without focusing
  178. local line = State.lines[line_index]
  179. local xleft = log_browser.left_margin(State, line)
  180. local xright = log_browser.right_margin(State, line)
  181. if x < xleft or x > xright then
  182. return
  183. end
  184. -- if it's a section begin/end and the section is collapsed, expand it
  185. -- TODO: how to collapse?
  186. if line.section_begin or line.section_end then
  187. -- HACK: get section reference from next/previous line
  188. local new_section
  189. if line.section_begin then
  190. if line_index < #State.lines then
  191. local next_section_stack = State.lines[line_index+1].section_stack
  192. if next_section_stack then
  193. new_section = next_section_stack[#next_section_stack]
  194. end
  195. end
  196. elseif line.section_end then
  197. if line_index > 1 then
  198. local previous_section_stack = State.lines[line_index-1].section_stack
  199. if previous_section_stack then
  200. new_section = previous_section_stack[#previous_section_stack]
  201. end
  202. end
  203. end
  204. if new_section and new_section.expanded == nil then
  205. new_section.expanded = true
  206. return
  207. end
  208. end
  209. -- open appropriate file in source side
  210. if line.filename ~= Editor_state.filename then
  211. source.switch_to_file(line.filename)
  212. end
  213. -- set cursor
  214. Editor_state.cursor1 = {line=line.line_number, pos=1}
  215. -- make sure it's visible
  216. -- TODO: handle extremely long lines
  217. Editor_state.screen_top1.line = math.max(0, Editor_state.cursor1.line-5)
  218. -- show cursor
  219. Focus = 'edit'
  220. end
  221. function log_browser.line_index(State, mx,my)
  222. -- duplicate some logic from log_browser.draw
  223. local y = State.top
  224. for line_index = State.screen_top1.line,#State.lines do
  225. local line = State.lines[line_index]
  226. if should_show(line) then
  227. y = y + log_browser.height(State, line_index)
  228. if my < y then
  229. return line_index
  230. end
  231. if y > App.screen.height then break end
  232. end
  233. end
  234. end
  235. function log_browser.mouse_release(State, x,y, mouse_button)
  236. end
  237. function log_browser.mouse_wheel_move(State, dx,dy)
  238. if dy > 0 then
  239. for i=1,math.floor(dy) do
  240. log_browser.up(State)
  241. end
  242. elseif dy < 0 then
  243. for i=1,math.floor(-dy) do
  244. log_browser.down(State)
  245. end
  246. end
  247. end
  248. function log_browser.text_input(State, t)
  249. end
  250. function log_browser.keychord_press(State, chord, key)
  251. -- move
  252. if chord == 'up' then
  253. log_browser.up(State)
  254. elseif chord == 'down' then
  255. log_browser.down(State)
  256. elseif chord == 'pageup' then
  257. local y = 0
  258. while State.screen_top1.line > 1 and y < App.screen.height - 100 do
  259. State.screen_top1.line = State.screen_top1.line - 1
  260. if should_show(State.lines[State.screen_top1.line]) then
  261. y = y + log_browser.height(State, State.screen_top1.line)
  262. end
  263. end
  264. elseif chord == 'pagedown' then
  265. local y = 0
  266. while State.screen_top1.line < #State.lines and y < App.screen.height - 100 do
  267. if should_show(State.lines[State.screen_top1.line]) then
  268. y = y + log_browser.height(State, State.screen_top1.line)
  269. end
  270. State.screen_top1.line = State.screen_top1.line + 1
  271. end
  272. end
  273. end
  274. function log_browser.up(State)
  275. while State.screen_top1.line > 1 do
  276. State.screen_top1.line = State.screen_top1.line-1
  277. if should_show(State.lines[State.screen_top1.line]) then
  278. break
  279. end
  280. end
  281. end
  282. function log_browser.down(State)
  283. while State.screen_top1.line < #State.lines do
  284. State.screen_top1.line = State.screen_top1.line+1
  285. if should_show(State.lines[State.screen_top1.line]) then
  286. break
  287. end
  288. end
  289. end
  290. function log_browser.height(State, line_index)
  291. local line = State.lines[line_index]
  292. if line.data == nil then
  293. -- section header
  294. return State.line_height
  295. elseif type(line.data) == 'string' then
  296. return State.line_height
  297. else
  298. if line.height == nil then
  299. --? print('nil line height! rendering off screen to calculate')
  300. line.height = log_render[line.data.name](line.data, State.left, App.screen.height, State.right-State.left)
  301. end
  302. return line.height
  303. end
  304. end
  305. function log_browser.key_release(State, key, scancode)
  306. end