editors.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. -- editor is a floating pane of editable code that accepts keyboard input
  2. local m
  3. m = {} -- this table stores functions under keys and also all editors in a list
  4. m.__index = m
  5. m.active = nil -- the one editor which receives text input
  6. local buffer = require'buffer'
  7. local panes = require'pane'
  8. local keymapping = {
  9. buffer = {
  10. ['up'] = 'moveUp',
  11. ['down'] = 'moveDown',
  12. ['volume_down'] = 'moveLeft',
  13. ['volume_up'] = 'moveRight',
  14. ['left'] = 'moveLeft',
  15. ['ctrl+up'] = 'moveJumpUp',
  16. ['ctrl+down'] = 'moveJumpDown',
  17. ['ctrl+left'] = 'moveJumpLeft',
  18. ['ctrl+right'] = 'moveJumpRight',
  19. ['right'] = 'moveRight',
  20. ['home'] = 'moveHome',
  21. ['end'] = 'moveEnd',
  22. ['pageup'] = 'movePageUp',
  23. ['pagedown'] = 'movePageDown',
  24. ['ctrl+home'] = 'moveJumpHome',
  25. ['ctrl+end'] = 'moveJumpEnd',
  26. ['shift+up'] = 'selectUp',
  27. ['alt+shift+up'] = 'selectJumpUp',
  28. ['shift+down'] = 'selectDown',
  29. ['alt+shift+down'] = 'selectJumpDown',
  30. ['shift+left'] = 'selectLeft',
  31. ['ctrl+shift+left'] = 'selectJumpLeft',
  32. ['ctrl+shift+right'] = 'selectJumpRight',
  33. ['shift+right'] = 'selectRight',
  34. ['shift+home'] = 'selectHome',
  35. ['shift+end'] = 'selectEnd',
  36. ['shift+pageup'] = 'selectPageUp',
  37. ['shift+pagedown'] = 'selectPageDown',
  38. ['tab'] = 'insertTab',
  39. ['return'] = 'breakLine',
  40. ['enter'] = 'breakLine',
  41. ['delete'] = 'deleteRight',
  42. ['backspace'] = 'deleteLeft',
  43. ['ctrl+backspace'] = 'deleteWord',
  44. ['ctrl+x'] = 'cutText',
  45. ['ctrl+c'] = 'copyText',
  46. ['ctrl+v'] = 'pasteText',
  47. },
  48. macros = {
  49. ['ctrl+o'] = function(self) self:listFiles('') end,
  50. ['ctrl+s'] = function(self) self:saveFile(self.path) end,
  51. ['ctrl+h'] = function(self) m.new(1, 1):listFiles('lovr-api') end,
  52. ['f5'] = function(self) lovr.event.push('restart') end,
  53. ['f10'] = function(self) self:setFullscreen(not self.fullscreen) end,
  54. ['ctrl+shift+enter'] = function(self) self:execLine() end,
  55. ['ctrl+shift+return'] = function(self) self:execLine() end,
  56. ['alt+l'] = function(self) self.buffer:insertString('lovr.graphics.') end,
  57. ['ctrl+space'] = function(self) self.pane:center() end,
  58. ['shift+space'] = function(self) self.buffer:insertString(' ') end,
  59. ['ctrl+shift+p'] = function(self) m.profile() end,
  60. },
  61. }
  62. local highlighting =
  63. { -- taken from base16-woodland
  64. background = 0x231e18, --editor background
  65. cursorline = 0x433b2f, --cursor background
  66. caret = 0xc6bcb1, --cursor
  67. whitespace = 0x111111, --spaces, newlines, tabs, and carriage returns
  68. comment = 0x9d8b70, --either multi-line or single-line comments
  69. string_start = 0x9d8b70, --starts and ends of a string. There will be no non-string tokens between these two.
  70. string_end = 0x9d8b70,
  71. string = 0xb7ba53, --part of a string that isn't an escape
  72. escape = 0x6eb958, --a string escape, like \n, only found inside strings
  73. keyword = 0xb690e2, --keywords. Like "while", "end", "do", etc
  74. value = 0xca7f32, --special values. Only true, false, and nil
  75. ident = 0xd35c5c, --identifier. Variables, function names, etc
  76. number = 0xca7f32, --numbers, including both base 10 (and scientific notation) and hexadecimal
  77. symbol = 0xc6bcb1, --symbols, like brackets, parenthesis, ., .., etc
  78. vararg = 0xca7f32, --...
  79. operator = 0xcabcb1, --operators, like +, -, %, =, ==, >=, <=, ~=, etc
  80. label_start = 0x9d8b70, --the starts and ends of labels. Always equal to '::'. Between them there can only be whitespace and label tokens.
  81. label_end = 0x9d8b70,
  82. label = 0xc6bcb1, --basically an ident between a label_start and label_end.
  83. unidentified = 0xd35c5c, --anything that isn't one of the above tokens. Consider them errors. Invalid escapes are also unidentified.
  84. selection = 0x353937,
  85. }
  86. function m.new(width, height)
  87. local self = setmetatable({}, m)
  88. self.pane = panes.new(width, height)
  89. self.cols = math.floor(width * self.pane.canvasSize / self.pane.fontWidth)
  90. self.rows = math.floor(height * self.pane.canvasSize / self.pane.fontHeight) - 1
  91. self.buffer = buffer.new(self.cols, self.rows,
  92. function(text, col, row, tokenType) -- draw single token
  93. local color = highlighting[tokenType] or 0xFFFFFF
  94. -- in-editor preview of colors in hex format 0xRRGGBBAA
  95. if tokenType == 'number' and text:match('0x%x+') then
  96. lovr.graphics.setColor(tonumber(text))
  97. self.pane:drawTextRectangle(col, row, 1)
  98. end
  99. lovr.graphics.setColor(color)
  100. self.pane:drawText(text, col, row)
  101. end,
  102. function (col, row, width, tokenType) --draw rectangle
  103. local color = highlighting[tokenType] or 0xFFFFFF
  104. lovr.graphics.setColor(color)
  105. self.pane:drawTextRectangle(col, row, width)
  106. end)
  107. self.pane:center()
  108. table.insert(m, self)
  109. m.active = self
  110. return self
  111. end
  112. function m:close()
  113. for i,editor in ipairs(m) do
  114. if self == editor then
  115. table.remove(m, i)
  116. return
  117. end
  118. end
  119. end
  120. function m:openFile(filename_line)
  121. -- file path can optionally include line number to jump to, like 'main.lua:50'
  122. local filename, linenumber = filename_line:match('([^:]+):([%d]+)')
  123. filename = filename or filename_line
  124. linenumber = linenumber or 1
  125. if not lovr.filesystem.isFile(filename) then
  126. return false, "no such file"
  127. end
  128. local content = lovr.filesystem.read(filename)
  129. print('file open', filename, 'size', #content)
  130. self.buffer:setText(content)
  131. local coreFile = lovr.filesystem.getRealDirectory(filename) == lovr.filesystem.getSource()
  132. self.buffer:setName((coreFile and 'core! ' or '').. filename)
  133. self.path = filename
  134. self.buffer:jumpToLine(linenumber)
  135. self:refresh()
  136. end
  137. function m:listFiles(path)
  138. self.buffer:setText('')
  139. self.buffer:setName('FILES')
  140. self.path = ''
  141. --determine parent dir
  142. local previous = ''
  143. local parent = ''
  144. for subpath in path:gmatch('[^/]+/') do
  145. previous = subpath
  146. parent = parent .. previous
  147. end
  148. parent = parent:sub(1, #parent - 1)
  149. self.buffer:insertString(string.format('self:listFiles(\'%s\')\n', parent))
  150. --list directory items, first subdirectories then files
  151. local files = {}
  152. for _,filename in ipairs(lovr.filesystem.getDirectoryItems(path)) do
  153. local fullpath = path .. '/' .. filename
  154. if lovr.filesystem.isDirectory(fullpath) then
  155. self.buffer:insertString(string.format('self:listFiles(\'%s\')\n', fullpath))
  156. else
  157. table.insert(files, fullpath)
  158. end
  159. end
  160. for _, fullpath in ipairs(files) do
  161. self.buffer:insertString(string.format('self:openFile(\'%s\')\n', fullpath))
  162. end
  163. self.buffer:insertString('\nctrl+shift+enter confirm selection')
  164. self.buffer:moveUp() self.buffer:moveUp()
  165. self:refresh()
  166. end
  167. function m:saveFile(filename)
  168. local bytes
  169. filename = filename or self.path
  170. bytes = lovr.filesystem.write(filename, self.buffer:getText())
  171. self.path = filename
  172. self.buffer:setName(filename)
  173. print('file save', filename, 'size', bytes)
  174. return bytes
  175. end
  176. function m.storeSession(name)
  177. local bytes
  178. local scriptList = {}
  179. table.insert(scriptList, 'return {')
  180. for _, editor in ipairs(m) do
  181. if editor.path then
  182. table.insert(scriptList, ' {')
  183. table.insert(scriptList, string.format(" path = '%s:%d',", tostring(editor.path), editor.buffer.cursor.y))
  184. table.insert(scriptList, string.format(' pose = {%s}', table.concat({editor.pane.transform:unpack()}, ', ')))
  185. table.insert(scriptList, ' },\n')
  186. end
  187. end
  188. table.insert(scriptList, '}\n')
  189. local content = table.concat(scriptList, '\n')
  190. bytes = lovr.filesystem.write(name .. '.lua', content)
  191. end
  192. function m.restoreSession(name)
  193. package.loaded[name] = nil
  194. local ok, session = pcall(require, name)
  195. if ok then
  196. for i, e in ipairs(session) do
  197. editor = m.new(1, 1)
  198. editor:openFile(e.path)
  199. editor.pane.transform:set(unpack(e.pose))
  200. end
  201. else
  202. print(session)
  203. end
  204. end
  205. function m:draw()
  206. if not self.fullscreen then
  207. self.pane:draw(self == m.active)
  208. else
  209. lovr.graphics.clear(highlighting.background)
  210. lovr.graphics.push()
  211. lovr.graphics.translate(-50,100,-100)
  212. lovr.graphics.scale(0.1)
  213. self.buffer:drawCode()
  214. lovr.graphics.pop()
  215. end
  216. end
  217. function m:refresh()
  218. if not self.fullscreen then
  219. self.pane:drawCanvas(function()
  220. lovr.graphics.clear(highlighting.background)
  221. self.buffer:drawCode()
  222. end)
  223. end
  224. end
  225. function m:setFullscreen(isFullscreen)
  226. self.fullscreen = isFullscreen
  227. if self.fullscreen then
  228. --lovr.graphics.setDepthTest('gequal', false)
  229. self.buffer.cols = 100
  230. self.buffer.rows = 100
  231. else
  232. --lovr.graphics.setDepthTest('lequal', true)
  233. self.buffer.cols, self.buffer.rows = self.cols, self.rows
  234. self.buffer:updateView()
  235. end
  236. end
  237. -- key handling
  238. function m.keypressed(k)
  239. if m.active then
  240. -- execute buffer-mapped action for keypress
  241. if keymapping.buffer[k] then
  242. m.active.buffer[keymapping.buffer[k]](m.active.buffer)
  243. end
  244. -- execute macros
  245. if keymapping.macros[k] then
  246. print('executing macro for', k)
  247. keymapping.macros[k](m.active)
  248. end
  249. m.active:refresh()
  250. end
  251. if k == 'ctrl+p' then -- spawn new editor
  252. m.active = m.new(1, 1)
  253. m.active:listFiles('')
  254. elseif k == 'ctrl+tab' then -- select next editor
  255. local lastEditor = m[#m]
  256. for i, editor in ipairs(m) do
  257. if editor == m.active then break end
  258. lastEditor = editor
  259. end
  260. m.active = lastEditor
  261. elseif k == 'ctrl+w' then -- close current editor
  262. local lastEditor
  263. for i, editor in ipairs(m) do
  264. if editor == m.active then
  265. table.remove(m, i)
  266. else
  267. lastEditor = editor
  268. end
  269. end
  270. m.active = lastEditor
  271. elseif k == 'ctrl+shift+s' then -- store session
  272. m.storeSession('saved-session')
  273. elseif k == 'ctrl+shift+l' then -- store session
  274. m.restoreSession('saved-session')
  275. end
  276. end
  277. function m.textinput(k)
  278. if m.active then
  279. m.active.buffer:insertCharacter(k)
  280. m.active:refresh()
  281. end
  282. end
  283. function m.profile()
  284. local profiler = require('profiler')
  285. profiler.start('p=-s')
  286. local editor = m.new(0.5, 0.8)
  287. editor.buffer:setName('performance')
  288. editor.buffer:setText(' Profiling the execution \n')
  289. local drawTime, drawStats
  290. local t = lovr.timer.getTime()
  291. while lovr.timer.getTime() < t + 1 do
  292. lovr.graphics.tick('profile')
  293. -- execute the reduced LOVR main loop
  294. local dt = lovr.timer.step()
  295. lovr.headset.update(dt)
  296. lovr.audio.update()
  297. lovr.update(dt)
  298. lovr.graphics.origin()
  299. lovr.headset.renderTo(lovr.draw)
  300. drawStats = lovr.graphics.getStats() -- stats are cleared on start of frame
  301. lovr.mirror()
  302. lovr.graphics.present()
  303. lovr.math.drain()
  304. -- end of LOVR main loop
  305. drawTime = lovr.graphics.tock('profile')
  306. end
  307. local report = profiler.stop()
  308. editor.buffer:setText('')
  309. editor.buffer:insertString('-- LOVR statistics --\n')
  310. for stat, value in pairs(drawStats) do
  311. editor.buffer:insertString(string.format('%s: %d\n', stat, value))
  312. end
  313. m.active.buffer:insertString(string.format('averagedelta: %2.1f ms\n', lovr.timer.getAverageDelta() * 1000))
  314. m.active.buffer:insertString(string.format('drawingtime: %2.1f ms\n', drawTime * 1000))
  315. editor.buffer:insertString(string.format('framespersecond: %d\n', lovr.timer.getFPS()))
  316. editor.buffer:insertString('-- LOVR graphics features --\n')
  317. for k, v in pairs(lovr.graphics.getFeatures()) do
  318. editor.buffer:insertString(k .. ': ' .. tostring(v) .. '\n')
  319. end
  320. editor.buffer:insertString(string.format('\n-- LuaJIT profiler report --\n'))
  321. editor.buffer:insertString(report)
  322. editor.buffer:moveJumpEnd()
  323. profiler = nil
  324. end
  325. -- code execution environment
  326. function m:execLine()
  327. local line = self.buffer:getCursorLine()
  328. local lineNum = self.buffer.cursor.y
  329. local commentPos = line:find("%s+%-%-")
  330. if commentPos then
  331. line = line:sub(1, commentPos - 1)
  332. end
  333. local success, result = self:execUnsafely(line)
  334. self.buffer.statusLine = (success and 'ok' or 'fail') .. ' > ' .. (result or "")
  335. end
  336. function m:execUnsafely(code)
  337. local userCode, err = loadstring(code)
  338. local result = ""
  339. if not userCode then
  340. print('code error:', err)
  341. return false, err
  342. end
  343. -- set up current scope environment for user code execution
  344. local environment = {self=self, print=print, require=require, lovr=lovr}
  345. setfenv(userCode, environment)
  346. -- timber!
  347. return pcall(userCode)
  348. end
  349. return m