| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- -- editor is a floating pane of editable code that accepts keyboard input
- local m
- m = {} -- stores functions under keys and also all editors in a list
- m.__index = m
- m.active = nil -- reference to the editor that receives text input
- m.font = lovr.graphics.newFont('ubuntu-mono.ttf', 20)
- m.font:setPixelDensity(1)
- m.font_width = m.font:getWidth(' ')
- m.font_height = m.font:getHeight()
- local buffer = require'buffer'
- local keymapping = {
- buffer = {
- ['up'] = 'moveUp',
- ['down'] = 'moveDown',
- ['volume_down'] = 'moveLeft',
- ['volume_up'] = 'moveRight',
- ['left'] = 'moveLeft',
- ['ctrl+up'] = 'moveJumpUp',
- ['ctrl+down'] = 'moveJumpDown',
- ['ctrl+left'] = 'moveJumpLeft',
- ['ctrl+right'] = 'moveJumpRight',
- ['right'] = 'moveRight',
- ['home'] = 'moveHome',
- ['end'] = 'moveEnd',
- ['pageup'] = 'movePageUp',
- ['pagedown'] = 'movePageDown',
- ['ctrl+home'] = 'moveJumpHome',
- ['ctrl+end'] = 'moveJumpEnd',
- ['shift+up'] = 'selectUp',
- ['alt+shift+up'] = 'selectJumpUp',
- ['shift+down'] = 'selectDown',
- ['alt+shift+down'] = 'selectJumpDown',
- ['shift+left'] = 'selectLeft',
- ['ctrl+shift+left'] = 'selectJumpLeft',
- ['ctrl+shift+right'] = 'selectJumpRight',
- ['shift+right'] = 'selectRight',
- ['shift+home'] = 'selectHome',
- ['shift+end'] = 'selectEnd',
- ['shift+pageup'] = 'selectPageUp',
- ['shift+pagedown'] = 'selectPageDown',
- ['tab'] = 'insertTab',
- ['return'] = 'breakLine',
- ['enter'] = 'breakLine',
- ['delete'] = 'deleteRight',
- ['backspace'] = 'deleteLeft',
- ['ctrl+backspace'] = 'deleteWord',
- ['ctrl+x'] = 'cutText',
- ['ctrl+c'] = 'copyText',
- ['ctrl+v'] = 'pasteText',
- },
- macros = {
- ['ctrl+o'] = function(self) self:listFiles() end,
- ['ctrl+s'] = function(self) self:saveFile(self.path) end,
- ['ctrl+h'] = function(self) m.new(120, 60):openHelp() end,
- ['ctrl+shift+enter'] = function(self) self:execLine() end,
- ['ctrl+shift+return'] = function(self) self:execLine() end,
- ['ctrl+shift+home'] = function(self) self:center() end,
- },
- }
- local palette = { -- mariana color palette
- background = 0x1f212b, --editor background
- cursorline = 0x4c5863, --cursor background
- caret = 0xeda550, --cursor
- whitespace = 0x111111, --spaces, newlines, tabs, and carriage returns
- comment = 0xa6acb9, --either multi-line or single-line comments
- string_start = 0x5fb4b4, --starts and ends of a string. There will be no non-string tokens between these two.
- string_end = 0x5fb4b4,
- string = 0x99c794, --part of a string that isn't an escape
- escape = 0xc695c6, --a string escape, like \n, only found inside strings
- keyword = 0xc695c6, --keywords. Like "while", "end", "do", etc
- value = 0xec5f66, --special values. Only true, false, and nil
- ident = 0x6699cc, --identifier. Variables, function names, etc
- number = 0xf9ae58, --numbers, including both base 10 (and scientific notation) and hexadecimal
- symbol = 0xa6acb9, --symbols, like brackets, parenthesis, ., .., etc
- vararg = 0xec5f66, --...
- operator = 0xf97b58, --operators, like +, -, %, =, ==, >=, <=, ~=, etc
- label_start = 0x5fb4b4, --the starts and ends of labels. Always equal to '::'. Between them there can only be whitespace and label tokens.
- label_end = 0x5fb4b4,
- label = 0xc695c6, --basically an ident between a label_start and label_end.
- unidentified = 0xec5f66, --anything that isn't one of the above tokens. Consider them errors. Invalid escapes are also unidentified.
- selection = 0x4c5863,
- active_bar = 0xf97b58, -- editor sidebar
- disabled_bar = 0x4c5863,
- }
- local function drawRectangle(pass, col, row, columns, tokenType)
- local color = palette[tokenType] or tonumber(tokenType)
- pass:setColor(color)
- -- rectangle in text-coordinates
- local width = columns * m.font_width
- local height = m.font_height
- local x = col * m.font_width
- local y = -row * m.font_height
- pass:plane(x + width / 2, y - height / 2, -2, width, height)
- end
- local function drawToken(pass, text, col, row, tokenType)
- local color = palette[tokenType] or 0xFFFFFF
- -- in-editor preview of colors in hex format 0xRRGGBBAA
- if tokenType == 'number' and text:match('0x%x+') then
- drawRectangle(pass, col, row, 2, text)
- end
- pass:setColor(color)
- pass:setFont(m.font)
- local x = col * m.font_width
- local y = -row * m.font_height
- pass:text(text, x,y,0, 1, 0, 1,0,0, 0, 'left','top')
- end
- function m.new(cols, rows, switchToProjectFn)
- local self = setmetatable({}, m)
- cols, rows = math.floor(cols), math.floor(rows)
- self.width = 1
- self.height = 1
- self.texture_size = 1024
- self.transform = lovr.math.newMat4(0, 1, -1, 1,1,1, 0, 0,1,0)
- self.ortho = lovr.math.newMat4()
- self.font:setPixelDensity(1)
- self.path = ''
- self.is_dirty = true
- self.switchToProject = switchToProjectFn or function() end
- self.buffer = buffer.new(cols, rows, drawToken, drawRectangle)
- self.pass = lovr.graphics.newPass()
- self:resize(cols, rows)
- self:center()
- table.insert(m, self)
- m.active = self
- return self
- end
- function m:resize(cols, rows)
- self.buffer.cols = cols
- self.buffer.rows = rows
- local status_lines = 1
- local texture_width = self.buffer.cols * m.font_width
- local texture_height = (self.buffer.rows + status_lines) * m.font_height
- self.width = texture_width / 1000
- self.height = texture_height / 1000
- self.ortho:orthographic(0, texture_width, 0, -texture_height, -10, 10)
- self.texture = lovr.graphics.newTexture(texture_width, texture_height, {mipmaps=false})
- self.pass:setCanvas(self.texture)
- self:refresh()
- end
- function m:setText(content)
- self.buffer:setText(content)
- end
- function m:center()
- local x,y,z, angle, ax,ay,az = 0,0,0, 0, 1,0,0
- if lovr.headset then
- x,y,z, angle, ax,ay,az = lovr.headset.getPose('head')
- end
- local headTransform = mat4(x,y,z, angle, ax,ay,az)
- local headPosition = vec3(headTransform)
- local panePosition = vec3(headTransform:mul(0,0,-1.0))
- self.transform:target(panePosition, headPosition)
- end
- function m:openFile(filename_line)
- -- file path can optionally include line number to jump to, like 'main.lua:50'
- local filename, linenumber = filename_line:match('([^:]+):([%d]+)')
- filename = filename or filename_line
- local content = ' '
- if lovr.filesystem.isFile(filename) then
- content = lovr.filesystem.read(filename)
- end
- print('file open', filename, 'size', #content)
- self.buffer:setText(content)
- local coreFile = lovr.filesystem.getRealDirectory(filename) == lovr.filesystem.getSource()
- self.buffer:setName((coreFile and 'core! ' or '').. filename)
- self.path = filename
- self.buffer:jumpToLine(1, 0)
- self:refresh()
- end
- function m:removeFile(file_path)
- if lovr.filesystem.isFile(file_path) then
- print('deleting', file_path)
- lovr.filesystem.remove(file_path)
- else
- local err = file_path .. ' does not exist'
- print('! ' .. err)
- error(err)
- end
- end
- function m:openHelp(path)
- self:listFiles('help')
- end
- function m:listFiles(path)
- if not path then -- try to use directory path of currently open file
- path = m.active and m.active.path:sub(1, (m.active.path:find('/[^/]+$') or 1) - 1) or ''
- end
- self.buffer:setText('')
- self.buffer:setName('FILES')
- self.path = ''
- --determine parent dir
- local previous = ''
- local parent = ''
- for subpath in path:gmatch('[^/]+/') do
- previous = subpath
- parent = parent .. previous
- end
- self.buffer:insertString('-- directory listing\n')
- if path ~= '' then
- parent = parent:sub(1, #parent - 1)
- self.buffer:insertString(string.format('self:listFiles(\'%s\') -- parent dir\n', parent))
- end
- --list directory items, first subdirectories then files
- local last_line = 2
- local files = {}
- local prefix = path == '' and '.' or path
- for _,filename in ipairs(lovr.filesystem.getDirectoryItems(path)) do
- local fullpath = prefix .. '/' .. filename
- if lovr.filesystem.isDirectory(fullpath) then
- self.buffer:insertString(string.format('self:listFiles(\'%s\')\n', fullpath))
- last_line = last_line + 1
- else
- table.insert(files, fullpath)
- end
- end
- if path == '' then -- root dir should only list directories and project-switching
- self.buffer:insertString('\n-- run project\n')
- for _,projname in ipairs(lovr.filesystem.getDirectoryItems('projects')) do
- self.buffer:insertString(string.format("self.switchToProject('%s')\n", projname))
- end
- else -- non-root dir shall list source/asset files and helpful commands
- for _, fullpath in ipairs(files) do
- self.buffer:insertString(string.format('self:openFile(\'%s\')\n', fullpath))
- end
- self.buffer:insertString('\n-- useful commands\n')
- self.buffer:insertString('self:openFile(\''..path..'/'..'new_source.lua\') -- new file\n')
- self.buffer:insertString('self:removeFile(\''..path..'/'..'unwanted.ext\') -- remove file\n')
- end
- self.buffer:insertString('\nctrl+shift+enter confirm selection')
- self.buffer:jumpToLine(last_line, 0)
- self:refresh()
- end
- function m:saveFile(filename)
- local bytes
- filename = filename or self.path
- bytes = lovr.filesystem.write(filename, self.buffer:getText())
- self.path = filename
- self.buffer:setName(filename)
- print('file save', filename, 'size', bytes)
- return bytes
- end
- function m.storeSession(name)
- local bytes
- local scriptList = {}
- table.insert(scriptList, 'return {')
- for _, editor in ipairs(m) do
- if editor.path then
- table.insert(scriptList, ' {')
- table.insert(scriptList, string.format(" path = '%s:%d',", tostring(editor.path), editor.buffer.cursor.y))
- table.insert(scriptList, string.format(' pose = {%s}', table.concat({editor.transform:unpack()}, ', ')))
- table.insert(scriptList, ' },\n')
- end
- end
- table.insert(scriptList, '}\n')
- local content = table.concat(scriptList, '\n')
- bytes = lovr.filesystem.write(name .. '.lua', content)
- end
- function m.restoreSession(name)
- -- close editors, discarding potentially unsaved changes!
- for i = #m, 1, -1 do
- table.remove(m, i)
- end
- -- load in the stored session
- m.active = nil
- package.loaded[name] = nil
- local ok, session = pcall(require, name)
- if ok then
- for _, e in ipairs(session) do
- local editor
- editor = m.new(1, 1)
- editor:openFile(e.path)
- editor.transform:set(unpack(e.pose))
- end
- else
- print(session)
- end
- end
- function m:draw(pass)
- -- oriented towards -z so that mat4.target() works as expected
- pass:push()
- pass:transform(self.transform)
- -- background and side handle
- pass:setColor(palette.background)
- local margin = 0.02
- pass:plane(0,0, margin / 4, self.width + margin, self.height + margin, math.pi, 0,1,0)
- pass:setColor(self == m.active and palette.active_bar or palette.disabled_bar)
- local thickness = 0.02
- local handleX = self.width/2 + thickness / 2 + margin
- local handleY = 0
- pass:box(handleX, handleY, 0, thickness, self.height * 0.8, thickness)
- -- code rendered from texture
- pass:setColor(1,1,1)
- pass:setMaterial(self.texture)
- pass:plane(0,0,0, -self.width, self.height)
- pass:setMaterial()
- pass:pop()
- end
- function m:drawToTexture()
- if not self.texture then
- self.texture = lovr.graphics.newTexture(self.texture_size, self.texture_size, {mipmaps=false})
- self.pass:setCanvas(self.texture)
- end
- self.pass:reset()
- self.pass:setDepthWrite(false)
- self.pass:setViewPose(1, mat4())
- self.pass:setProjection(1, self.ortho)
- self.pass:setColor(palette.background)
- self.pass:fill()
- self.buffer:drawCode(self.pass)
- return self.pass
- end
- function m:refresh()
- self.is_dirty = true
- end
- -- key handling
- function m.keypressed(k)
- if m.active then
- -- execute buffer-mapped action for keypress
- if keymapping.buffer[k] then
- m.active.buffer[keymapping.buffer[k]](m.active.buffer)
- end
- -- execute macros
- if keymapping.macros[k] then
- print('executing macro for', k)
- keymapping.macros[k](m.active)
- end
- m.active:refresh()
- end
- if k == 'ctrl+tab' then -- select next editor
- local lastEditor = m[#m]
- for _, editor in ipairs(m) do
- if editor == m.active then break end
- lastEditor = editor
- end
- m.active = lastEditor
- elseif k == 'ctrl+w' then -- close current editor
- local lastEditor
- for i = #m, 1, -1 do
- if m[i] == m.active then
- table.remove(m, i)
- else
- lastEditor = lastEditor or m[i]
- end
- end
- m.active = lastEditor
- elseif k == 'ctrl+shift+s' then
- m.storeSession('indeck-session')
- elseif k == 'ctrl+shift+l' then
- m.restoreSession('indeck-session')
- end
- end
- function m.textinput(k)
- if m.active then
- m.active.buffer:insertCharacter(k)
- m.active:refresh()
- end
- end
- -- code execution environment
- function m:execLine()
- local line = self.buffer:getCursorLine()
- local commentPos = line:find("%s+%-%-")
- if commentPos then
- line = line:sub(1, commentPos - 1)
- end
- local success, result = self:execUnsafely(line)
- if success then
- self.buffer.statusLine = 'ok' .. ' > ' .. (result or "")
- else
- print('! line exec error:', result)
- self.buffer.statusLine = 'fail' .. ' > ' .. (result or "")
- end
- end
- function m:execUnsafely(code)
- local userCode, err = loadstring(code)
- if not userCode then
- print('code error:', err)
- return false, err
- end
- -- set up current scope environment for user code execution
- local environment = {self=self, print=print, require=require, lovr=lovr}
- setfenv(userCode, environment)
- -- timber!
- return pcall(userCode)
- end
- return m
|