editors.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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. ['ctrl+z'] = function(self) self.buffer:undo() end,
  53. ['f5'] = function(self) lovr.event.push('restart') end,
  54. ['f10'] = function(self) self:setFullscreen(not self.fullscreen) end,
  55. ['ctrl+shift+enter'] = function(self) self:execLine() end,
  56. ['ctrl+shift+return'] = function(self) self:execLine() end,
  57. ['alt+l'] = function(self) self.buffer:insertString('lovr.graphics.') end,
  58. ['ctrl+shift+home'] = function(self) self.pane:center() 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.path = ''
  89. self.pane = panes.new(width, height)
  90. self.cols = math.floor(width * self.pane.canvasSize / self.pane.fontWidth)
  91. self.rows = math.floor(height * self.pane.canvasSize / self.pane.fontHeight) - 1
  92. self.buffer = buffer.new(self.cols, self.rows,
  93. function(text, col, row, tokenType) -- draw single token
  94. local color = highlighting[tokenType] or 0xFFFFFF
  95. -- in-editor preview of colors in hex format 0xRRGGBBAA
  96. if tokenType == 'number' and text:match('0x%x+') then
  97. lovr.graphics.setColor(tonumber(text))
  98. self.pane:drawTextRectangle(col, row, 1)
  99. end
  100. lovr.graphics.setColor(color)
  101. self.pane:drawText(text, col, row)
  102. end,
  103. function (col, row, width, tokenType) --draw rectangle
  104. local color = highlighting[tokenType] or 0xFFFFFF
  105. lovr.graphics.setColor(color)
  106. self.pane:drawTextRectangle(col, row, width)
  107. end, '', 15)
  108. self.pane:center()
  109. table.insert(m, self)
  110. m.active = self
  111. return self
  112. end
  113. function m:close()
  114. for i,editor in ipairs(m) do
  115. if self == editor then
  116. table.remove(m, i)
  117. return
  118. end
  119. end
  120. end
  121. function m:openFile(filename_line)
  122. -- file path can optionally include line number to jump to, like 'main.lua:50'
  123. local filename, linenumber = filename_line:match('([^:]+):([%d]+)')
  124. filename = filename or filename_line
  125. linenumber = linenumber or 1
  126. if not lovr.filesystem.isFile(filename) then
  127. return false, "no such file"
  128. end
  129. local content = lovr.filesystem.read(filename)
  130. print('file open', filename, 'size', #content)
  131. self.buffer:setText(content)
  132. local coreFile = lovr.filesystem.getRealDirectory(filename) == lovr.filesystem.getSource()
  133. self.buffer:setName((coreFile and 'core! ' or '').. filename)
  134. self.path = filename
  135. self.buffer:jumpToLine(linenumber)
  136. self:refresh()
  137. end
  138. function m:listFiles(path)
  139. if not path then -- try to use directory path of currently open file
  140. path = m.active and m.active.path:sub(1, (m.active.path:find('/[^/]+$') or 1) - 1) or ''
  141. end
  142. self.buffer:setText('')
  143. self.buffer:setName('FILES')
  144. self.path = ''
  145. --determine parent dir
  146. local previous = ''
  147. local parent = ''
  148. for subpath in path:gmatch('[^/]+/') do
  149. previous = subpath
  150. parent = parent .. previous
  151. end
  152. parent = parent:sub(1, #parent - 1)
  153. self.buffer:insertString(string.format('self:listFiles(\'%s\')\n', parent))
  154. --list directory items, first subdirectories then files
  155. local files = {}
  156. for _,filename in ipairs(lovr.filesystem.getDirectoryItems(path)) do
  157. local fullpath = path .. '/' .. filename
  158. if lovr.filesystem.isDirectory(fullpath) then
  159. self.buffer:insertString(string.format('self:listFiles(\'%s\')\n', fullpath))
  160. else
  161. table.insert(files, fullpath)
  162. end
  163. end
  164. for _, fullpath in ipairs(files) do
  165. self.buffer:insertString(string.format('self:openFile(\'%s\')\n', fullpath))
  166. end
  167. self.buffer:insertString('\nctrl+shift+enter confirm selection')
  168. self.buffer:moveUp() self.buffer:moveUp()
  169. self:refresh()
  170. end
  171. function m:saveFile(filename)
  172. local bytes
  173. filename = filename or self.path
  174. bytes = lovr.filesystem.write(filename, self.buffer:getText())
  175. self.path = filename
  176. self.buffer:setName(filename)
  177. print('file save', filename, 'size', bytes)
  178. return bytes
  179. end
  180. function m.storeSession(name)
  181. local bytes
  182. local scriptList = {}
  183. table.insert(scriptList, 'return {')
  184. for _, editor in ipairs(m) do
  185. if editor.path then
  186. table.insert(scriptList, ' {')
  187. table.insert(scriptList, string.format(" path = '%s:%d',", tostring(editor.path), editor.buffer.cursor.y))
  188. table.insert(scriptList, string.format(' pose = {%s}', table.concat({editor.pane.transform:unpack()}, ', ')))
  189. table.insert(scriptList, ' },\n')
  190. end
  191. end
  192. table.insert(scriptList, '}\n')
  193. local content = table.concat(scriptList, '\n')
  194. bytes = lovr.filesystem.write(name .. '.lua', content)
  195. end
  196. function m.restoreSession(name)
  197. for _, editor in ipairs(m) do
  198. editor:close() -- discarding potentially unsaved changes!
  199. end
  200. package.loaded[name] = nil
  201. local ok, session = pcall(require, name)
  202. if ok then
  203. for i, e in ipairs(session) do
  204. editor = m.new(1, 1)
  205. editor:openFile(e.path)
  206. editor.pane.transform:set(unpack(e.pose))
  207. end
  208. else
  209. print(session)
  210. end
  211. end
  212. function m:draw()
  213. if not self.fullscreen then
  214. self.pane:draw(self == m.active)
  215. else
  216. lovr.graphics.clear(highlighting.background)
  217. lovr.graphics.push()
  218. lovr.graphics.translate(-50,100,-100)
  219. lovr.graphics.scale(0.1)
  220. self.buffer:drawCode()
  221. lovr.graphics.pop()
  222. end
  223. end
  224. function m:refresh()
  225. if not self.fullscreen then
  226. self.pane:drawCanvas(function()
  227. lovr.graphics.clear(highlighting.background)
  228. self.buffer:drawCode()
  229. end)
  230. end
  231. end
  232. function m:setFullscreen(isFullscreen)
  233. self.fullscreen = isFullscreen
  234. if self.fullscreen then
  235. --lovr.graphics.setDepthTest('gequal', false)
  236. self.buffer.cols = 100
  237. self.buffer.rows = 100
  238. else
  239. --lovr.graphics.setDepthTest('lequal', true)
  240. self.buffer.cols, self.buffer.rows = self.cols, self.rows
  241. self.buffer:updateView()
  242. end
  243. end
  244. -- key handling
  245. function m.keypressed(k)
  246. if m.active then
  247. -- execute buffer-mapped action for keypress
  248. if keymapping.buffer[k] then
  249. m.active.buffer[keymapping.buffer[k]](m.active.buffer)
  250. end
  251. -- execute macros
  252. if keymapping.macros[k] then
  253. print('executing macro for', k)
  254. keymapping.macros[k](m.active)
  255. end
  256. m.active:refresh()
  257. end
  258. if k == 'ctrl+p' then -- spawn new editor
  259. m.active = m.new(1, 1)
  260. m.active:listFiles()
  261. elseif k == 'ctrl+tab' then -- select next editor
  262. local lastEditor = m[#m]
  263. for i, editor in ipairs(m) do
  264. if editor == m.active then break end
  265. lastEditor = editor
  266. end
  267. m.active = lastEditor
  268. elseif k == 'ctrl+w' then -- close current editor
  269. local lastEditor
  270. for i, editor in ipairs(m) do
  271. if editor == m.active then
  272. table.remove(m, i)
  273. else
  274. lastEditor = editor
  275. end
  276. end
  277. m.active = lastEditor
  278. elseif k == 'ctrl+shift+s' then -- store session
  279. m.storeSession('saved-session')
  280. elseif k == 'ctrl+shift+l' then -- store session
  281. m.restoreSession('saved-session')
  282. end
  283. end
  284. function m.textinput(k)
  285. if m.active then
  286. m.active.buffer:insertCharacter(k)
  287. m.active:refresh()
  288. end
  289. end
  290. function m.profile()
  291. local profiler = require('profiler')
  292. profiler.start('p=-s')
  293. local editor = m.new(0.5, 0.8)
  294. editor.buffer:setName('performance')
  295. editor.buffer:setText(' Profiling the execution \n')
  296. local drawTime, drawStats
  297. local t = lovr.timer.getTime()
  298. while lovr.timer.getTime() < t + 1 do
  299. lovr.graphics.tick('profile')
  300. -- execute the reduced LOVR main loop
  301. local dt = lovr.timer.step()
  302. lovr.headset.update(dt)
  303. lovr.audio.update()
  304. lovr.update(dt)
  305. lovr.graphics.origin()
  306. lovr.headset.renderTo(lovr.draw)
  307. drawStats = lovr.graphics.getStats() -- stats are cleared on start of frame
  308. lovr.mirror()
  309. lovr.graphics.present()
  310. lovr.math.drain()
  311. -- end of LOVR main loop
  312. drawTime = lovr.graphics.tock('profile')
  313. end
  314. local report = profiler.stop()
  315. editor.buffer:setText('')
  316. editor.buffer:insertString('-- LOVR statistics --\n')
  317. for stat, value in pairs(drawStats) do
  318. editor.buffer:insertString(string.format('%s: %d\n', stat, value))
  319. end
  320. m.active.buffer:insertString(string.format('averagedelta: %2.1f ms\n', lovr.timer.getAverageDelta() * 1000))
  321. m.active.buffer:insertString(string.format('drawingtime: %2.1f ms\n', drawTime * 1000))
  322. editor.buffer:insertString(string.format('framespersecond: %d\n', lovr.timer.getFPS()))
  323. editor.buffer:insertString('-- LOVR graphics features --\n')
  324. for k, v in pairs(lovr.graphics.getFeatures()) do
  325. editor.buffer:insertString(k .. ': ' .. tostring(v) .. '\n')
  326. end
  327. editor.buffer:insertString(string.format('\n-- LuaJIT profiler report --\n'))
  328. editor.buffer:insertString(report)
  329. editor.buffer:moveJumpEnd()
  330. profiler = nil
  331. end
  332. -- code execution environment
  333. function m:execLine()
  334. local line = self.buffer:getCursorLine()
  335. local lineNum = self.buffer.cursor.y
  336. local commentPos = line:find("%s+%-%-")
  337. if commentPos then
  338. line = line:sub(1, commentPos - 1)
  339. end
  340. local success, result = self:execUnsafely(line)
  341. self.buffer.statusLine = (success and 'ok' or 'fail') .. ' > ' .. (result or "")
  342. end
  343. function m:execUnsafely(code)
  344. local userCode, err = loadstring(code)
  345. local result = ""
  346. if not userCode then
  347. print('code error:', err)
  348. return false, err
  349. end
  350. -- set up current scope environment for user code execution
  351. local environment = {self=self, print=print, require=require, lovr=lovr}
  352. setfenv(userCode, environment)
  353. -- timber!
  354. return pcall(userCode)
  355. end
  356. return m