Advertisement
joebodo

vil.lua

May 30th, 2014
735
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 27.45 KB | None | 0 0
  1. vos.loadApi('core.api')
  2. vos.loadApi('ui.api')
  3.  
  4. Logger.disable()
  5. local assert = vos.assert
  6.  
  7. local commandBuffer = ''
  8. local commandCount
  9. local x, y = 1, 1
  10. local scrollx, scrolly = 0, 0
  11. local lines = {}
  12. local clipboard = nil
  13. local autoClearStatus = false
  14. local fileInfo = {
  15.   modified = false,
  16. }
  17.  
  18. -- forward declarations
  19. local keyMappings = { }
  20. local keyMapping
  21. local keyFunctions
  22.  
  23. local function isAdvanced() return term.isColor and term.isColor() end
  24. local colorTheme = {
  25.   editorBackground = "black",
  26.   normal = {
  27.     lineNumbersText = "gray",
  28.   },
  29.   highlight = {
  30.     lineNumbersText = 'white',
  31.   },
  32.   textColor = "white",
  33.   conditional = "yellow",
  34.   constant = "red",
  35.   ["function"] = "magenta",
  36.   language = 'cyan',
  37.   ["table"] = 'lime',
  38.   string = "red",
  39.   comment = "lime"
  40. }
  41.  
  42. local standardTheme = {
  43.   editorBackground = "black",
  44.   editorLineHighlight = "black",
  45.   editorLineNumbers = "black",
  46.   editorLineNumbersHighlight = "white",
  47.   editorError = "black",
  48.   editorErrorHighlight = "black",
  49.  
  50.   textColor = "white",
  51.   conditional = "white",
  52.   constant = "white",
  53.   ["function"] = "white",
  54.   string = "white",
  55.   comment = "white"
  56. }
  57.  
  58. -- Load Theme
  59. local theme = standardTheme
  60. if isAdvanced() then
  61.   theme = colorTheme
  62. end
  63.  
  64. keywords = {
  65.   ["and"] = "conditional",
  66.   ["break"] = "conditional",
  67.   ["do"] = "conditional",
  68.   ["else"] = "conditional",
  69.   ["elseif"] = "conditional",
  70.   ["for"] = "conditional",
  71.   ["if"] = "conditional",
  72.   ["in"] = "conditional",
  73.   ["local"] = "conditional",
  74.   ["not"] = "conditional",
  75.   ["or"] = "conditional",
  76.   ["repeat"] = "conditional",
  77.   ["return"] = "conditional",
  78.   ["then"] = "conditional",
  79.   ["until"] = "conditional",
  80.   ["while"] = "conditional",
  81.  
  82. --[[
  83.   ['{'] = 'table',
  84.   ['}'] = 'table',
  85. --]]
  86.  
  87.   ["function"] = "language",
  88.   ["end"] = "language",
  89.   ["pairs"] = "language",
  90.   ["ipairs"] = "language",
  91.   ["rawset"] = "language",
  92.   ["rawget"] = "language",
  93.   ["print"] = "language",
  94.  
  95.   ["true"] = "constant",
  96.   ["false"] = "constant",
  97.   ["nil"] = "constant",
  98.  
  99.   ["write"] = "function",
  100.   ["sleep"] = "function",
  101.   ["loadstring"] = "function",
  102.   ["loadfile"] = "function",
  103.   ["dofile"] = "function",
  104.   ["setfenv"] = "function",
  105.   ["getfenv"] = "function",
  106. }
  107.  
  108. local standardsCompletions = {
  109.   "if%s+.+%s+then%s*$",
  110.   "for%s+.+%s+do%s*$",
  111.   "while%s+.+%s+do%s*$",
  112.   "repeat%s*$",
  113.   "function%s+[a-zA-Z_0-9]?\(.*\)%s*$",
  114.   "=%s*function%s*\(.*\)%s*$",
  115.   "else%s*$",
  116.   "elseif%s+.+%s+then%s*$"
  117. }
  118.  
  119. local liveCompletions = {
  120.   ["("] = ")",
  121.   ["{"] = "}",
  122.   ["["] = "]",
  123.   ["\""] = "\"",
  124.   ["'"] = "'",
  125. }
  126.  
  127. local function hackyRead()
  128.   -- stops read from scrolling screen
  129.   local oldPrint = _G.print
  130.   _G.print = function() end
  131.   local result = read()
  132.   _G.print = oldPrint
  133.   result = Util.trim(result)
  134.   if #result > 0 then
  135.     return result
  136.   end
  137. end
  138.  
  139. local editor = UI.Page({
  140.   settings = {
  141.     tabWidth = 2,
  142.   },
  143.   gutter = UI.Window({
  144.     width = 3,
  145.     height = UI.term.height - 1,
  146.   }),
  147.   editArea = UI.Window({
  148.     x = 4,
  149.     y = 1,
  150.     width = UI.term.width - 3,
  151.     height = UI.term.height - 1,
  152.     dirty = { },
  153.     focus = function() end,
  154.   }),
  155.   statusBar = UI.StatusBar({
  156.     backgroundColor = colors.black,
  157.     columns = {
  158.       { '', 'status', UI.term.width - 10 },
  159.       { '', 'cursor', 8 }
  160.     },
  161.   }),
  162. })
  163.  
  164. local gutter = editor.gutter
  165. local editArea = editor.editArea
  166. local statusBar = editor.statusBar
  167.  
  168. function editor:draw()
  169.   term.setCursorBlink(false)
  170.   term.setBackgroundColor(colors.black)
  171.  
  172.   editArea:draw()
  173.   statusBar:draw()
  174.  
  175.   term.setTextColor(colors[theme.textColor])
  176.   term.setCursorPos(x - scrollx + editArea.x - 1, y - scrolly)
  177.   term.setCursorBlink(true)
  178. end
  179.  
  180. function editor:flashScreen()
  181.   term.setBackgroundColor(colors.lightGray)
  182.   term.clear()
  183.   os.sleep(0)
  184.   editArea:markScreenDirty()
  185. end
  186.  
  187. --[[ STATUSBAR AREA ]]--
  188. function statusBar:showMessage(str, ...)
  189.   assert('statusBar:showMessage', {
  190.     { 'string', str },
  191.   })
  192.   self:setValue('status', str:format(...))
  193.   self:draw()
  194. end
  195.  
  196. function statusBar:clearMessage()
  197.   self:showMessage('')
  198. end
  199.  
  200. function statusBar:showError(status)
  201.   term.setCursorPos(1, statusBar.y)
  202.   term.setBackgroundColor(colors.red)
  203.   term.setTextColor(colors.white)
  204.   term.write(status)
  205.   term.setBackgroundColor(colors[theme.editorBackground])
  206. end
  207.  
  208. function statusBar:draw()
  209.   if commandBuffer or commandCount then
  210.     self:setValue('cursor', tostring(commandCount or 1) .. tostring(commandBuffer or ''))
  211.   else
  212.     self:setValue('cursor', string.format('%d,%d', y, x))
  213.   end
  214.   UI.StatusBar.draw(self)
  215. end
  216.  
  217. --[[ EDIT AREA ]]--
  218. function editArea:attemptToHighlight(line, regex, col)
  219.   local match = string.match(line, regex)
  220.   if match then
  221.     if type(col) == "number" then
  222.       term.setTextColor(col)
  223.     elseif type(col) == "function" then
  224.       term.setTextColor(col(match))
  225.     end
  226.     term.write(match)
  227.     term.setTextColor(colors[theme.textColor])
  228.     return line:sub(match:len() + 1, -1)
  229.   end
  230.   return nil
  231. end
  232.  
  233. function editArea:writeHighlighted(line)
  234.   while line:len() > 0 do  
  235.     line = self:attemptToHighlight(line, "^%-%-%[%[.-%]%]", colors[theme.comment]) or
  236.            self:attemptToHighlight(line, "^%-%-.*", colors[theme.comment]) or
  237.            self:attemptToHighlight(line, "^\".*[^\\]\"", colors[theme.string]) or
  238.            self:attemptToHighlight(line, "^\'.*[^\\]\'", colors[theme.string]) or
  239.            self:attemptToHighlight(line, "^%[%[.-%]%]", colors[theme.string]) or
  240.            self:attemptToHighlight(line, "^[%w_]+", function(match)
  241.              if keywords[match] then
  242.                return colors[theme[keywords[match]]]
  243.              end
  244.                return colors[theme.textColor]
  245.              end) or
  246.            self:attemptToHighlight(line, "^[^%w_]", colors[theme.textColor])
  247.   end
  248. end
  249.  
  250. function editArea:drawLine(ly)
  251.   if (ly - scrolly < 1) or (ly - scrolly > editArea.height) then
  252.     return
  253.   end
  254.  
  255.   term.setCursorPos(1, ly - scrolly)
  256.   if ly <= #lines then
  257.     local _colors = theme.normal
  258.     if ly == y then
  259.       _colors = theme.highlight
  260.     end
  261.  
  262.     term.setTextColor(colors[_colors.lineNumbersText])
  263.     local ln = string.rep(" ", gutter.width - 1 - tostring(ly):len()) .. tostring(ly)
  264.     term.write(ln .. ' ')
  265.  
  266.     local a = lines[ly]
  267.     local totalWidth = self.width
  268.     if scrollx > 0 then
  269.       totalWidth = self.width + scrollx
  270.       term.setCursorPos(1 - scrollx + self.x, ly - scrolly)
  271.     end
  272.     self:writeHighlighted(a)
  273.     if #a < totalWidth then
  274.       term.write(string.rep(' ', totalWidth - #a))
  275.     end
  276.   else
  277.     term.clearLine()
  278.   end
  279. end
  280.  
  281. function editArea:drawDirty()
  282.   local keys = Util.keys(self.dirty)
  283.   for _,lineNo in pairs(keys) do
  284.     self:drawLine(lineNo)
  285.     self.dirty[lineNo] = nil
  286.   end
  287. end
  288.  
  289. function editArea:markDirty(ls)
  290.   assert('editor:markDirty', {
  291.     { 'table', ls },
  292.   })
  293.   for _, ly in pairs(ls) do
  294.     self.dirty[ly] = true
  295.   end
  296. end
  297.  
  298. function editArea:markSubsequentDirty(lineNo)
  299.   for i = lineNo - scrolly, self.height do
  300.     self.dirty[i + scrolly] = true
  301.   end
  302. end
  303.  
  304. function editArea:markScreenDirty()
  305.   for i = 1, self.height do
  306.     self.dirty[scrolly + i] = true
  307.   end
  308.   if autoClearStatus then
  309.     statusBar:setValue('status', '')
  310.   end
  311. end
  312.  
  313. function editArea:draw()
  314.   local gutterWidth = math.max(tostring(#lines):len() + 1, 3)
  315.   if gutterWidth ~= gutter.width then
  316.     gutter.width = gutterWidth
  317.     editArea.width = editor.width - gutter.width
  318.     editArea.x = gutter.width + 1
  319.     self:markScreenDirty()
  320.   end
  321.   if not Util.empty(self.dirty) then
  322.     --local ds = 'ds: ' .. table.concat(Util.keys(self.dirty), ',')
  323.     --editor.statusBar:setValue('status', ds)
  324.     self:drawDirty()
  325.   end
  326. end
  327.  
  328. function editArea:cursorLoc(newX, newY)
  329.  
  330.   if newY > #lines then
  331.     newY = #lines
  332.   elseif newY < 1 then
  333.     newY = 1
  334.   end
  335.   if y ~= newY then
  336.     self:markDirty({ y, newY })
  337.   end
  338.   y = newY
  339.  
  340.   if newX < 1 then
  341.     newX = 1
  342.   elseif newX > #lines[y] + 1 then
  343.     newX = #lines[y] + 1
  344.   end
  345.   x = newX
  346.  
  347.   if y > #lines then
  348.     y = #lines
  349.   end
  350.  
  351.   local force = false
  352.   local sx, sy = x - scrollx, y - scrolly
  353.   local oscrollx, oscrolly = scrollx, scrolly
  354.  
  355.   if sx < 1 then
  356.     scrollx = x - 1
  357.     force = true
  358.   elseif sx > self.width then
  359.     scrollx = x - self.width
  360.     force = true
  361.   end
  362.   if sy < 1 then
  363.     scrolly = y - 1
  364.   elseif sy > self.height then
  365.     scrolly = y - self.height
  366.   end
  367.  
  368.   local scrolledy = scrolly - oscrolly
  369.   if (math.abs(scrolledy) >= self.height) then
  370.     force = true
  371.   end
  372.  
  373.   if force or scrolledy ~= 0 then
  374.     if autoClearStatus then
  375.       editor.statusBar:setValue('status', '')
  376.     end
  377.   end
  378.  
  379.   if force then
  380.     self:markScreenDirty()
  381.   elseif scrolledy ~= 0 then
  382.     term.scroll(scrolledy)
  383.     if scrolledy > 0 then
  384.       statusBar:clear()
  385. --      term.setCursorPos(1, statusBar.y)
  386. --      term.clearLine()
  387.       self:markSubsequentDirty(scrolly + self.height - scrolledy + 1)
  388.     else
  389.       for i = -1, scrolledy, -1 do
  390.         self:markDirty({ scrolly - i })
  391.       end
  392.     end
  393.   end
  394. end
  395.  
  396. function getFileInfo(path)
  397.   local abspath = "/" .. shell.resolve(path)
  398.  
  399.   local fi = {
  400.     abspath = abspath,
  401.     path = path,
  402.     name = fs.getName(path),
  403.     dir = fs.getDir(abspath),
  404.     isNew = not fs.exists(abspath),
  405.     isDir = fs.isDir(path),
  406.     dirExists = fs.exists(fs.getDir(abspath)),
  407.     modified = false,
  408.   }
  409.   if fi.isDir then
  410.     fi.isReadOnly = true
  411.   else
  412.     fi.isReadOnly = fs.isReadOnly(fi.abspath)
  413.   end
  414.  
  415.   return fi
  416. end
  417.  
  418. function resetCommandBuffer()
  419.   commandBuffer = nil
  420.   commandCount = nil
  421. end
  422.  
  423. function newFile()
  424.   local fi = {
  425.     isNew = true,
  426.     isDir = false,
  427.     dirExists = false,
  428.     modified = false,
  429.     isReadOnly = false,
  430.   }
  431. end
  432.  
  433. function load(path)
  434.  
  435.   fileInfo = getFileInfo(path)
  436.  
  437.   if fileInfo.isNew then
  438.     lines = { '' }
  439.   else
  440.     if fileInfo.isDir then
  441.       lines = fs.list(fileInfo.abspath)
  442.     else
  443.       lines = Util.readLines(fileInfo.abspath) or { '' }
  444.     end
  445.   end
  446.  
  447.   multishell.setTitle(TAB_ID, fileInfo.name)
  448.   editArea:markScreenDirty()
  449.  
  450.   local status = fileInfo.path
  451.   if fileInfo.isNew then
  452.     if not fileInfo.dirExists then
  453.       statusBar:showMessage('"%s" [New DIRECTORY]', path)
  454.     else
  455.       statusBar:showMessage('"%s" [New File]', path)
  456.     end
  457.   elseif fileInfo.isDir then
  458.     statusBar:showMessage('"%s" is a directory')
  459.   elseif fileInfo.isReadOnly then
  460.     statusBar:showMessage('"%s" [readonly] %dL, %dC',
  461.           path, #lines, fs.getSize(fileInfo.abspath))
  462.   else
  463.     statusBar:showMessage('"%s" %dL, %dC',
  464.           path, #lines, fs.getSize(fileInfo.abspath))
  465.   end
  466. end
  467.  
  468. local function saveFile(path)
  469.   local dir = path:sub(1, path:len() - fs.getName(path):len())
  470.   if not fs.exists(dir) then
  471.     fs.makeDir(dir)
  472.   end
  473.  
  474.   if fs.isDir(path) then
  475.     return false, string.format('"%s" is a directory', path)
  476.   end
  477.  
  478.   if fs.isReadOnly(path) then
  479.     return false, string.format('"%s" is read only', path)
  480.   end
  481.  
  482.   if not Util.writeLines(path, lines) then
  483.     return false, string.format('Error saving "%s"', path)
  484.   end
  485.  
  486.   return true
  487. end
  488.  
  489. keyFunctions = {
  490.   commandMode = function()
  491.     statusBar:clearMessage()
  492.     autoClearStatus = true
  493.     keyMapping = keyMappings['vi-command']
  494.   end,
  495.   insertMode = function(event)
  496.     statusBar:showMessage('-- INSERT --')
  497.     autoClearStatus = false
  498.     keyMapping = keyMappings['vi-insert']
  499.     if event.key == 'a' then
  500.       keyFunctions.right()
  501.     elseif event.key == 'A' then
  502.       keyFunctions.lineEnd()
  503.     elseif event.key == 'o' then
  504.       keyFunctions.appendLine()
  505.     elseif event.key == 'O' then
  506.       keyFunctions.insertLine()
  507.     end
  508.   end,
  509.   up = function(event)
  510.     if y > 1 then
  511.       editArea:cursorLoc(x, y - (event.count or 1))
  512.       return true
  513.     end
  514.   end,
  515.   down = function(event)
  516.     if y < #lines then
  517.       editArea:cursorLoc(x, y + (event.count or 1))
  518.       return true
  519.     end
  520.   end,
  521.   left = function(event)
  522.     if x > 1 then
  523.       editArea:cursorLoc(x - (event.count or 1), y)
  524.       return true
  525.     end
  526.   end,
  527.   right = function(event)
  528.     if x < lines[y]:len() + 1 then
  529.       editArea:cursorLoc(x + (event.count or 1), y)
  530.       return true
  531.     end
  532.   end,
  533.   leftWrap = function()
  534.     if x > 1 then
  535.       editArea:cursorLoc(x - 1, y)
  536.       return true
  537.     end
  538.     if y > 1 then
  539.       editArea:cursorLoc(#lines[y - 1] + 1, y - 1)
  540.       return true
  541.     end
  542.   end,
  543.   rightWrap = function()
  544.     if x < lines[y]:len() + 1 then
  545.       editArea:cursorLoc(x + 1, y)
  546.       return true
  547.     end
  548.     if y < #lines then
  549.       editArea:cursorLoc(1, y + 1)
  550.       return true
  551.     end
  552.   end,
  553.   home = function()
  554.     editArea:cursorLoc(1, y)
  555.     return true
  556.   end,
  557.   lineEnd = function()
  558.     editArea:cursorLoc(lines[y]:len() + 1, y)
  559.     return true
  560.   end,
  561.   gotoLine = function(event)
  562.     if not event.count then
  563.       editArea:cursorLoc(1, #lines)
  564.       return true
  565.     end
  566.     if event.count > #lines then
  567.       return false
  568.     end
  569.     editArea:cursorLoc(1, event.count)
  570.     return true
  571.   end,
  572.   pageUp = function()
  573.     if scrolly > 0 then
  574.       editArea:cursorLoc(x, math.min(math.max(y - editArea.height, 1), #lines))
  575.       return true
  576.     end
  577.   end,
  578.   pageDown = function()
  579.     editArea:cursorLoc(x, math.min(math.max(y + editArea.height, 1), #lines))
  580.   end,
  581.   quit = function(event)
  582.     if fileInfo.modified and event.key ~= ':q!' then
  583.       return false, 'No write since last change (add ! to override)'
  584.     end
  585.     Event.exitPullEvents()
  586.     return #event.key
  587.   end,
  588.   abort = function()
  589.     Event.exitPullEvents()
  590.   end,
  591.   status = function()
  592.     local path = fileInfo.path or '[No Name]'
  593.     if #lines == 0 then
  594.       statusBar:showMessage('"%s" -- No lines in buffer --', path)
  595.     else
  596.       statusBar:showMessage('"%s" %d lines --%d%%--',
  597.            fileInfo.path, #lines, math.floor(y/#lines*100))
  598.     end
  599.     return true
  600.   end,
  601.   save = function()
  602.     saveFile(fileInfo.abspath)
  603.     statusBar:showMessage('"%s" %dL, %dC written',
  604.            fileInfo.path, #lines, fs.getSize(fileInfo.abspath))
  605.   end,
  606.   cutLine = function()
  607.     clipboard = lines[y]
  608.     table.remove(lines, y)
  609.     editArea:markSubsequentDirty(y)
  610.     editArea:cursorLoc(1, y)
  611.     fileInfo.modified = true
  612.   end,
  613.   copyLines = function(event)
  614.     clipboard = { }
  615.     local count = math.min(event.count or 1, #lines - y)
  616.     for i = 1, event.count do
  617.       table.insert(clipboard,  lines[y + i - 1])
  618.     end
  619.     statusBar:showMessage('%d lines yanked', count)
  620.   end,
  621.   appendLine = function()
  622.     table.insert(lines, y + 1, '')
  623.     editArea:markSubsequentDirty(y + 1)
  624.     editArea:cursorLoc(1, y + 1)
  625.     fileInfo.modified = true
  626.   end,
  627.   insertLine = function()
  628.     table.insert(lines, y, '')
  629.     editArea:markSubsequentDirty(y)
  630.     editArea:cursorLoc(1, y)
  631.     fileInfo.modified = true
  632.   end,
  633.   enter = function()
  634.     local f = nil
  635.     local oldy = y
  636.     for _, v in pairs(standardsCompletions) do
  637.       if lines[y]:find(v) then f = v end
  638.     end
  639.  
  640.     local _, spaces = lines[y]:find("^[ ]+")
  641.     if not spaces then spaces = 0 end
  642.     if f then
  643.       table.insert(lines, y + 1, string.rep(" ", spaces + 2))
  644.       if not f:find("else", 1, true) and not f:find("elseif", 1, true) then
  645.         table.insert(lines, y + 2, string.rep(" ", spaces) ..
  646.           (f:find("repeat", 1, true) and "until " or f:find("{", 1, true) and "}" or "end"))
  647.       end
  648.       editArea:cursorLoc(spaces + 3, y + 1)
  649.     else
  650.       local oldLine = lines[y]
  651.       lines[y] = lines[y]:sub(1, x - 1)
  652.       table.insert(lines, y + 1, string.rep(" ", spaces) .. oldLine:sub(x, -1))
  653.       editArea:cursorLoc(spaces + 1, y + 1)
  654.     end
  655.     editArea:markSubsequentDirty(oldy)
  656.     fileInfo.modified = true
  657.   end,
  658.   backspace = function()
  659.     if x > 1 then
  660.       local f = false
  661.       for k, v in pairs(liveCompletions) do
  662.         if lines[y]:sub(x - 1, x - 1) == k then f = true end
  663.       end
  664.       lines[y] = lines[y]:sub(1, x - 2) .. lines[y]:sub(x + (f and 1 or 0), -1)
  665.       editArea:markDirty({ y })
  666.       editArea:cursorLoc(x - 1, y)
  667.       fileInfo.modified = true
  668.     elseif y > 1 then
  669.       local prevLen = lines[y - 1]:len() + 1
  670.       lines[y - 1] = lines[y - 1] .. lines[y]
  671.       table.remove(lines, y)
  672.       editArea:markSubsequentDirty(y - 1)
  673.       editArea:cursorLoc(prevLen, y - 1)
  674.       fileInfo.modified = true
  675.     end
  676.   end,
  677.   delete = function()
  678.     if x < lines[y]:len() + 1 then
  679.       lines[y] = lines[y]:sub(1, x - 1) .. lines[y]:sub(x + 1)
  680.       editArea:markDirty({ y })
  681. --      editArea:cursorLoc(x, y)
  682.       fileInfo.modified = true
  683.     elseif y < #lines then
  684.       lines[y] = lines[y] .. lines[y + 1]
  685.       table.remove(lines, y + 1)
  686.       editArea:markSubsequentDirty(y)
  687. --      editArea:cursorLoc(x, y)
  688.       fileInfo.modified = true
  689.     end
  690.   end,
  691.   deleteToEnd = function()
  692.     lines[y] = lines[y]:sub(1, x - 1)
  693.     editArea:markDirty({ y })
  694.     editArea:cursorLoc(#lines[y], y)
  695.     fileInfo.modified = true
  696.   end,
  697.   tab = function()
  698.     lines[y] = string.rep(" ", editor.settings.tabWidth) .. lines[y]
  699.     editArea:markDirty({ y })
  700.     editArea:cursorLoc(x + 2, y)
  701.     fileInfo.modified = true
  702.   end,
  703.   insertCharacter = function(event)
  704.     local key = event.key
  705.     local shouldIgnore = false
  706.     for k, v in pairs(liveCompletions) do
  707.       if key == v and lines[y]:find(k, 1, true) and lines[y]:sub(x, x) == v then
  708.         shouldIgnore = true
  709.       end
  710.     end
  711.     local addOne = false
  712.     if not shouldIgnore then
  713.       for k, v in pairs(liveCompletions) do
  714.         if key == k and lines[y]:sub(x, x) ~= k then
  715.           key = key .. v
  716.           addOne = true
  717.         end
  718.       end
  719.       lines[y] = lines[y]:sub(1, x - 1) .. key .. lines[y]:sub(x, -1)
  720.     end
  721.  
  722.     editArea:markDirty({ y })
  723.     editArea:cursorLoc(x + (addOne and 1 or key:len()), y)
  724.     fileInfo.modified = true
  725.   end,
  726.   pasteLine = function()
  727.     if clipboard then
  728.       table.insert(lines, y, clipboard)
  729.       editArea:markSubsequentDirty(y)
  730.       statusBar:showMessage('1 more line')
  731.       fileInfo.modified = true
  732.     end
  733.   end,
  734.   nextWord = function()
  735.     local str = lines[y]
  736.     local result = { str:find("(%w+)", x) }
  737.     if #result > 0 and result[1] == x then
  738.       result = { str:find("(%w+)", result[2] + 1) }
  739.     end
  740.     if #result > 0 then
  741.       if #result > 0 then
  742.         editArea:cursorLoc(result[1], y)
  743.       end
  744.     elseif #result == 0 and y < #lines then
  745.       local nx
  746.       result = { lines[y + 1]:find("(%w+)", 1) }
  747.       if #result == 0 then
  748.         nx = 1
  749.       else
  750.         nx = result[1]
  751.       end
  752.       editArea:cursorLoc(nx, y + 1)
  753.     end
  754.   end,
  755.   deleteLines = function(event)
  756.     clipboard = { }
  757.     for i = 1, (event.count or 1) do
  758.       table.insert(clipboard, lines[y])
  759.       table.remove(lines, y)
  760.     end
  761.     if #lines == 0 then
  762.       table.insert(lines, '')
  763.     end
  764.     editArea:cursorLoc(1, y)
  765.     editArea:markSubsequentDirty(y)
  766.     fileInfo.modified = true
  767.   end,
  768.   pasteLinesBefore = function()
  769.     if clipboard and type(clipboard) == 'table' then
  770.       for i = 1, #clipboard do
  771.         table.insert(lines, y + i - 1, clipboard[i])
  772.       end
  773.       editArea:cursorLoc(1, y)
  774.       editArea:markSubsequentDirty(y)
  775.       fileInfo.modified = true
  776.     end
  777.   end,
  778.   searchPrompt = function()
  779.     term.setCursorPos(1, statusBar.y)
  780.     term.write('/')
  781.     hackyRead()
  782.   end,
  783.   nextLine = function()
  784.     if y < #lines then
  785.       editArea:cursorLoc(1, y + 1)
  786.     end
  787.   end,
  788.   write = function(event)
  789.     local fi = fileInfo
  790.     if event.args then
  791.       fi = getFileInfo(event.args)
  792.     end
  793.     if fi.isReadOnly then
  794.       return false, "'readonly' option is set"
  795.     end
  796.  
  797.     if event.args and fs.exists(fi.path) and not event.key:find(1, '!', true) then
  798.       return false, 'File exists (add ! to override)'
  799.     end
  800.     local success, msg = saveFile(fi.path)
  801.     if not success then
  802.       return false, msg
  803.     end
  804.     fi.modified = false
  805.     statusBar:showMessage('"%s" %dL, %dC written',
  806.           filename, #lines, fs.getSize(filename))
  807.     if event.key:sub(2, 1) == '!' then
  808.       return 2
  809.     end
  810.     return 1
  811.   end,
  812.   colonPrompt = function(event)
  813.     term.setCursorPos(1, statusBar.y)
  814.     term.clearLine()
  815.     term.write(':')
  816.     local cmd = hackyRead()
  817.     statusBar:clearMessage()
  818.     if cmd then
  819.       local words = Util.toWords(cmd)
  820.       local cmd = words[1]
  821.       local index = 1
  822.       repeat
  823.         local subcmd = ':' .. cmd:sub(index, 1)
  824.         if not keyMapping[subcmd] then
  825.           statusBar:showError('Invalid command')
  826.           break
  827.         end
  828.         event.key = ':' .. cmd:sub(index)
  829.         event.args = words[2]
  830.         local inc, msg = keyMapping[subcmd](event)
  831.         if not inc then
  832.           statusBar:showError(msg)
  833.           break
  834.         end
  835.         index = index + inc
  836.       until index > #cmd
  837.     end
  838.   end,
  839.   refresh = function()
  840.     editArea:markScreenDirty()
  841.   end,
  842.   insertTranslate = function(event)
  843.     if #event.key == 1 then
  844.       return keyFunctions.insertCharacter
  845.     end
  846.     return keyMapping[event.key]
  847.   end,
  848.   commandTranslate = function(event)
  849.  
  850.     local filterKeys = {
  851.       [ 'leftCtrl'  ] = true,
  852.       [ 'leftAlt'   ] = true,
  853.       [ 'leftShift' ] = true,
  854.       [ ' '         ] = true,
  855.     }
  856.     if filterKeys[event.key] then
  857.       return function() return true end
  858.     end
  859.  
  860.     if commandBuffer and #event.key > 1 then
  861.       return resetCommandBuffer
  862.     end
  863.  
  864.     if #event.key == 1 then
  865.       local ch = tonumber(event.key)
  866.       if ch then
  867.         if commandCount then
  868.           commandCount = commandCount * 10 + ch
  869.         else
  870.           commandCount = ch
  871.         end
  872.         commandBuffer = nil
  873.         return
  874.       end
  875.  
  876.       if not commandBuffer then
  877.         commandBuffer = event.key
  878.       else
  879.         commandBuffer = commandBuffer .. event.key
  880.       end
  881.       event.key = commandBuffer
  882.     end
  883.  
  884.     local fn = keyMapping[event.key]
  885.     if fn then
  886.       event.count = commandCount
  887.       commandBuffer = nil
  888.       commandCount = nil
  889.       return fn
  890.     end
  891.  
  892.     for k,_ in pairs(keyMapping) do
  893.       if k:find('^' .. event.key) then
  894.         return
  895.       end
  896.     end
  897.  
  898.     --statusBar:showMessage(tostring(event.key))
  899.     --autoClearStatus = false
  900.     return resetCommandBuffer
  901.   end,
  902. }
  903.  
  904. local commonKeyMapping = {
  905.   [ 'up'        ] = keyFunctions.up,
  906.   [ 'down'      ] = keyFunctions.down,
  907.   [ 'left'      ] = keyFunctions.left,
  908.   [ 'right'     ] = keyFunctions.right,
  909.   [ 'home'      ] = keyFunctions.home,
  910.   [ 'end'       ] = keyFunctions.lineEnd,
  911.   [ 'pageUp'    ] = keyFunctions.pageUp,
  912.   [ 'pageDown'  ] = keyFunctions.pageDown,
  913.   [ 'control-t' ] = keyFunctions.abort,
  914. }
  915.  
  916. local colonCommands = {
  917.     [ 'w'         ] = keyFunctions.write,
  918.     [ 'r'         ] = keyFunctions.notImplemented,
  919.     [ 'e'         ] = keyFunctions.notImplemented,
  920.     [ 'vi'        ] = keyFunctions.notImplemented,
  921.  
  922.     [ '!!'        ] = keyFunctions.notImplemented,
  923.     [ '^!(%S)'    ] = keyFunctions.run,
  924.     [ 'sh'        ] = keyFunctions.notImplemented,
  925.  
  926.     [ 'set'       ] = keyFunctions.notImplemented,
  927.  
  928.     [ 'q'         ] = keyFunctions.quit,
  929. }
  930.  
  931. local keyMappingsSource = {
  932.   [ 'vi-command'  ] = {
  933.  
  934.     [ 'translate' ] = keyFunctions.commandTranslate,
  935.  
  936.     -- insert
  937.     [ 'i'         ] = keyFunctions.insertMode,
  938.     [ 'I'         ] = keyFunctions.notImplemented,
  939.     [ 'a'         ] = keyFunctions.insertMode,
  940.     [ 'A'         ] = keyFunctions.insertMode,
  941.     [ 'o'         ] = keyFunctions.insertMode,
  942.     [ 'O'         ] = keyFunctions.insertMode,
  943.     [ 'r'         ] = keyFunctions.notImplemented,
  944.     [ 'R'         ] = keyFunctions.notImplemented,
  945.  
  946.     -- motion
  947.     [ 'h'         ] = keyFunctions.left,
  948.     [ 'j'         ] = keyFunctions.down,
  949.     [ 'k'         ] = keyFunctions.up,
  950.     [ 'l'         ] = keyFunctions.right,
  951.     [ 'w'         ] = keyFunctions.nextWord,
  952.     [ 'b'         ] = keyFunctions.notImplemented,
  953.     [ '^'         ] = keyFunctions.notImplemented,
  954.     [ 'space'     ] = keyFunctions.rightWrap,
  955.     [ '0'         ] = keyFunctions.home,
  956.     [ '$'         ] = keyFunctions.lineEnd,
  957.     [ 'G'         ] = keyFunctions.gotoLine,
  958.     [ 'control-f' ] = keyFunctions.pageDown,
  959.     [ 'control-b' ] = keyFunctions.pageUp,
  960.     [ 'backspace' ] = keyFunctions.leftWrap,
  961.  
  962.     -- deleting
  963.     [ 'x'         ] = keyFunctions.delete,
  964.     [ 'X'         ] = keyFunctions.notImplemented,
  965.     [ 'D'         ] = keyFunctions.deleteToEnd,
  966.     [ 'dd'        ] = keyFunctions.deleteLines,
  967.     [ 'dw'        ] = keyFunctions.notImplemented,
  968.  
  969.     -- status
  970.     [ 'control-g' ] = keyFunctions.status,
  971.  
  972.     -- paste
  973.     [ 'p'         ] = keyFunctions.pasteLinesAfter,
  974.     [ 'P'         ] = keyFunctions.pasteLinesBefore,
  975.     [ 'yy'        ] = keyFunctions.copyLines,
  976.  
  977.     -- changing text
  978.     [ 'C'         ] = keyFunctions.notImplemented,
  979.     [ 'cc'        ] = keyFunctions.notImplemented,
  980.     [ 'cw'        ] = keyFunctions.notImplemented,
  981.  
  982.     -- searching
  983.     [ 'n'         ] = keyFunctions.notImplemented,
  984.     [ 'N'         ] = keyFunctions.notImplemented,
  985.  
  986.     [ '/'         ] = keyFunctions.searchPrompt,
  987.     [ 'enter'     ] = keyFunctions.nextLine,
  988.     [ ':'         ] = keyFunctions.colonPrompt,
  989.  
  990.     [ 'ZZ'        ] = keyFunctions.notImplemented,
  991.     [ 'control-l' ] = keyFunctions.refresh,
  992.   },
  993.   [ 'vi-insert'   ] = {
  994.     [ 'leftAlt'   ] = keyFunctions.commandMode,
  995.     [ 'enter'     ] = keyFunctions.enter,
  996.     [ 'backspace' ] = keyFunctions.backspace,
  997.     [ 'delete'    ] = keyFunctions.delete,
  998.     [ 'tab'       ] = keyFunctions.tab,
  999.     [ 'default'   ] = keyFunctions.default,
  1000.     [ 'translate' ] = keyFunctions.insertTranslate,
  1001.   },
  1002.   [ 'fullscreen'  ] = {
  1003.     [ 'control-g' ] = keyFunctions.status,
  1004.     [ 'control-s' ] = keyFunctions.save,
  1005.     [ 'control-x' ] = keyFunctions.cutLine,
  1006.     [ 'control-c' ] = keyFunctions.copyLines,
  1007.     [ 'contol-v'  ] = keyFunctions.pasteLine,
  1008.     [ 'right'     ] = keyFunctions.rightWrap,
  1009.     [ 'left'      ] = keyFunctions.leftWrap,
  1010.     [ 'enter'     ] = keyFunctions.enter,
  1011.     [ 'backspace' ] = keyFunctions.backspace,
  1012.     [ 'delete'    ] = keyFunctions.delete,
  1013.     [ 'tab'       ] = keyFunctions.tab,
  1014.     [ 'default'   ] = keyFunctions.default,
  1015.   },
  1016. }
  1017.  
  1018. for k,v in pairs(keyMappingsSource) do
  1019.   keyMappings[k] = Util.shallowCopy(commonKeyMapping)
  1020.   Util.merge(keyMappings[k], v)
  1021. end
  1022.  
  1023. keyMapping = keyMappings['vi-command']
  1024.  
  1025. function editArea:eventHandler(event)
  1026.  
  1027.   if event.type == 'paste' then
  1028.     event.type = 'key'
  1029.     event.key = 'contol-v'
  1030.   end
  1031.  
  1032.   if event.type == 'key' then
  1033.     local fn = keyMapping.translate(event)
  1034.     if fn then
  1035.       if not fn(event) then
  1036.         editor:flashScreen()
  1037.       end
  1038.     end
  1039.   elseif event.type == "mouse_click" then
  1040.     editArea:cursorLoc(scrollx + event.x, scrolly + event.y)
  1041.   else
  1042.     return false
  1043.   end
  1044.  
  1045.   editor:draw()
  1046.   return true
  1047. end
  1048.  
  1049. local function main(arguments)
  1050.  
  1051.   if #arguments > 0 then
  1052.     load(arguments[1])
  1053.   else
  1054.     newFile()
  1055.   end
  1056.  
  1057.   term.setBackgroundColor(colors[theme.editorBackground])
  1058.   UI.pager:setPage(editor)
  1059.  
  1060. --  autoClearStatus = true
  1061.  
  1062.   Event.pullEvents()
  1063. end
  1064.  
  1065. local args = {...}
  1066. local _, err = pcall(function()
  1067.   main(args)
  1068. end)
  1069.  
  1070. term.setTextColor(colors.white)
  1071. if not err then
  1072.   term.clear()
  1073.   term.setCursorPos(1, 1)
  1074. end
  1075.  
  1076. -- Catch errors
  1077. if err and not err:find("Terminated") then
  1078.   print(err)
  1079. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement