Advertisement
Slamakans

Editor for OpenComputers with syntax highlighting

Dec 4th, 2017
946
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 29.89 KB | None | 0 0
  1. local fs = require("filesystem")
  2. local keyboard = require("keyboard")
  3. local shell = require("shell")
  4. local term = require("term") -- TODO use tty and cursor position instead of global area and gpu
  5. local text = require("text")
  6. local unicode = require("unicode")
  7. local computer = require("computer")
  8.  
  9. if not term.isAvailable() then
  10.   return
  11. end
  12.  
  13. local gpu = term.gpu()
  14.  
  15. local timers = {}
  16. local function timerStart(index)
  17.   timers[index] = computer.uptime()
  18. end
  19.  
  20. local function timerElapsed(index)
  21.   return computer.uptime() - timers[index]
  22. end
  23.  
  24. -- %tms for the time taken in milliseconds
  25. -- Used for profiling, you can remove this
  26. local function timerDraw(index, x, y, s)
  27.   local timeTakenSeconds = computer.uptime() - timers[index]
  28.   s = s:gsub("%%tms", timeTakenSeconds * 1000)
  29.        :gsub("%%t", timeTakenSeconds) .. (" "):rep(100)
  30.  
  31.   local fg, fgp = gpu.getForeground()
  32.   local bg, bgp = gpu.getBackground()
  33.  
  34.   gpu.setForeground(0xFFFFFF)
  35.   gpu.setBackground(0x000000)
  36.   gpu.set(x, y, s)
  37.   gpu.setForeground(fg, fgp)
  38.   gpu.setBackground(bg, bgp)
  39. end
  40.  
  41. local c_highlight_keyword = 0xF9275D
  42. local c_highlight_func_def_name = 0x99FF40
  43. local c_highlight_constant = 0xCC6DFF
  44. local c_highlight_comment = 0x75715E
  45. local c_highlight_string = 0xFFDB40
  46. local c_highlight_self = 0xFD9720
  47.  
  48. local max_time_to_determine_if_multiline_comment_in_seconds = 0.05
  49.  
  50. -- [[ Highlight helper functions ]]
  51.  
  52. local leadingOrTrailingString = nil
  53.  
  54. local function noLeadingOrTrailingStringProcess(self, x, y, w, h, lineNr, wholeStr, str, a, b, s)
  55.   local fg, fgp = gpu.getForeground()
  56.   gpu.setForeground(self.c)
  57.  
  58.   local init = 1
  59.   repeat
  60.     local aa, bb = wholeStr:find(self.p, init)
  61.     if aa and str:find(self.p) and not leadingOrTrailingString(self, wholeStr, str, aa, bb, s) then
  62.       gpu.set(x + aa - 1, y, str:sub(aa, bb))
  63.       str = str:gsub(self.p, (" "):rep(bb - aa + 1), 1)
  64.       init = bb + 1
  65.     elseif aa then
  66.       init = bb + 1
  67.     end
  68.   until not aa or not str:find(self.p)
  69.  
  70.   gpu.setForeground(fg, fgp)
  71.  
  72.   return str
  73. end
  74.  
  75. function leadingOrTrailingString(self, wholeStr, str, a, b, s)
  76.   if not a then return false end
  77.   return wholeStr:sub(a - #s, a - 1) == s or wholeStr:sub(b + 1, b + #s) == s
  78. end
  79.  
  80. local function isolated(self, wholeStr, str, a, b)
  81.   if not a or not b then return false end
  82.   local leading = wholeStr:sub(a - 1, a - 1)
  83.   local trailing = wholeStr:sub(b + 1, b + 1)
  84.  
  85.   --[[ untested, but should work better
  86.   return (leading:match("[ (=-+,~*%[/{]") or leading == "") and (trailing:match("[ )=-+,~*%]/}]") or trailing == "")
  87.   ]]
  88.   return (leading:match("[ (=-+]") or leading == "") and (trailing:match("[ )=-+]") or trailing == "")
  89. end
  90.  
  91. local highlights = {
  92.   -- comments
  93.   { p = "%-%-.*", c = c_highlight_comment },
  94.   -- escapes
  95.   { p = "\\[abfnrtv\\\"'%[%]]", c = c_highlight_constant },
  96.   -- strings
  97.   { p = '(["\'])(.*)%1', c = c_highlight_string, process = function(self, x, y, w, h, lineNr, wholeStr, str, a, b)
  98.     local match = str:sub(a, b)
  99.     local delimiter = match:sub(1, 1)
  100.  
  101.     local segments = { { a = 1 } }
  102.  
  103.     local i = 2
  104.  
  105.     while i <= #match do
  106.       local ch = match:sub(i, i)
  107.       if ch == delimiter then
  108.         segments[#segments].b = i
  109.         local ch2 = nil
  110.         repeat
  111.           i = i + 1
  112.           ch2 = match:sub(i, i)
  113.         until ch2 == delimiter or ch2 == ""
  114.         if ch2 == delimiter then
  115.           table.insert(segments, { a = i })
  116.         end
  117.       end
  118.  
  119.       i = i + 1
  120.     end
  121.  
  122.     local fg, fgp = gpu.getForeground()
  123.     gpu.setForeground(self.c)
  124.  
  125.     for _, seg in ipairs(segments) do
  126.       if seg.b then
  127.         local s = match:sub(seg.a, seg.b)
  128.         str = str:gsub(text.escapeMagic(s), (" "):rep(#s), 1)
  129.  
  130.         -- split so we don't draw spaces, so already (escape stuff for example) highlighted stuff still shows
  131.         local parts = { { a = 1 } }
  132.         i = 2
  133.         while i <= #s do
  134.           local ch = s:sub(i, i)
  135.           if ch == " " then
  136.             parts[#parts].b = i - 1
  137.             repeat
  138.               i = i + 1
  139.             until s:sub(i, i) ~= " "
  140.             if s:sub(i, i) ~= "" then
  141.               table.insert(parts, { a = i })
  142.             end
  143.           end
  144.  
  145.           i = i + 1
  146.         end
  147.  
  148.         for _,part in ipairs(parts) do
  149.           local pa, pb = part.a, part.b or #s
  150.           gpu.set(x + a - 1 + seg.a - 1 + pa - 1, y, s:sub(pa, pb))
  151.         end
  152.       end
  153.     end
  154.  
  155.     gpu.setForeground(fg, fgp)
  156.  
  157.     return str
  158.   end, specialFinishCondition = function(self, wholeStr, str, a, b)
  159.     return
  160.   end },
  161.   -- func def name
  162.   { p = "%sfunction (%w[%S]*)%([%w%s,%d]*%)", c = c_highlight_func_def_name, captures = true },
  163.   { p = "^function (%w[%S]*)%([%w%s,%d]*%)", c = c_highlight_func_def_name, captures = true },
  164.   -- constants
  165.   { p = "nil", c = c_highlight_constant },
  166.   { p = "false", c = c_highlight_constant },
  167.   { p = "true", c = c_highlight_constant },
  168.   { p = "%.%.%.", c = c_highlight_constant, process = function(self, x, y, w, h, lineNr, wholeStr, str, a, b)
  169.     return noLeadingOrTrailingStringProcess(self, x, y, w, h, lineNr, wholeStr, str, a, b, ".")
  170.   end, specialFinishCondition = function(self, wholeStr, str, a, b)
  171.     return leadingOrTrailingString(self, wholeStr, str, a, b, ".")
  172.   end },
  173.   -- keywords
  174.   { p = "local", c = c_highlight_keyword },
  175.   { p = "return", c = c_highlight_keyword },
  176.   { p = "if", c = c_highlight_keyword },
  177.   { p = "do", c = c_highlight_keyword },
  178.   { p = "then", c = c_highlight_keyword },
  179.   { p = "end", c = c_highlight_keyword },
  180.   { p = "while", c = c_highlight_keyword },
  181.   { p = "for", c = c_highlight_keyword },
  182.   { p = "repeat", c = c_highlight_keyword },
  183.   { p = "until", c = c_highlight_keyword },
  184.   { p = "function", c = c_highlight_keyword },
  185.   { p = "or", c = c_highlight_keyword },
  186.   { p = "and", c = c_highlight_keyword },
  187.   { p = "in", c = c_highlight_keyword },
  188.   { p = "else", c = c_highlight_keyword },
  189.   { p = "not", c = c_highlight_keyword },
  190.   { p = "elseif", c = c_highlight_keyword },
  191.   { p = "break", c = c_highlight_keyword },
  192.   { p = "%.%.", c = c_highlight_keyword, process = function(self, x, y, w, h, lineNr, wholeStr, str, a, b)
  193.     return noLeadingOrTrailingStringProcess(self, x, y, w, h, lineNr, wholeStr, str, a, b, ".")
  194.   end, specialFinishCondition = function(self, wholeStr, str, a, b)
  195.     return leadingOrTrailingString(self, wholeStr, str, a, b, ".")
  196.   end },
  197.   { p = "==", c = c_highlight_keyword },
  198.   { p = ">=", c = c_highlight_keyword },
  199.   { p = "<=", c = c_highlight_keyword },
  200.   { p = "<", c = c_highlight_keyword },
  201.   { p = ">", c = c_highlight_keyword },
  202.   { p = "=", c = c_highlight_keyword },
  203.   { p = "~=", c = c_highlight_keyword },
  204.   { p = "%*", c = c_highlight_keyword },
  205.   { p = "%+", c = c_highlight_keyword },
  206.   { p = "%-", c = c_highlight_keyword },
  207.   { p = "%^", c = c_highlight_keyword },
  208.   { p = "/", c = c_highlight_keyword },
  209.   { p = "%%", c = c_highlight_keyword },
  210.   -- self
  211.   { p = "self", c = c_highlight_self },
  212.   -- hex color preview
  213.   { p = "0x(%x%x%x%x%x%x)", c = nil, process = function(self, x, y, w, h, lineNr, wholeStr, str, a, b)
  214.     if isolated(self, wholeStr, str, a, b) then
  215.       local hex = tonumber(str:sub(a, b):match(self.p), 16)
  216.       local bg, bgp = gpu.getBackground()
  217.       local fg, fgp = gpu.getForeground()
  218.      
  219.       local c_r, c_g, c_b = hex >> 16 & 0xFF, hex >> 8 & 0xFF, hex & 0xFF
  220.       local perceivedLuminance = 0.299*c_r + 0.587*c_g + 0.114*c_b
  221.  
  222.       if perceivedLuminance >= 0.5 then
  223.         gpu.setForeground(0x000000)
  224.       else
  225.         gpu.setForeground(0xFFFFFF)
  226.       end
  227.       gpu.setBackground(hex)
  228.  
  229.       gpu.set(x + a - 1, y, str:sub(a, b))
  230.  
  231.       gpu.setForeground(fg, fgp)
  232.       gpu.setBackground(bg, bgp)
  233.  
  234.       str = str:gsub(self.p, (" "):rep(b - a + 1), 1)
  235.     end
  236.  
  237.     return str
  238.   end, specialFinishCondition = function(self, wholeStr, str, a, b)
  239.     return not isolated(self, wholeStr, str, a, b)
  240.   end }
  241. }
  242.  
  243. for i=1,#highlights do
  244.   if highlights[i].p:match("^%w+$") then
  245.     table.insert(highlights, { p = "^(" .. highlights[i].p .. ")[^%a%d_]", c = highlights[i].c, captures = true })
  246.     highlights[i].p = "%s(" .. highlights[i].p .. ")[^%a%d_]"
  247.     highlights[i].captures = true
  248.   end
  249. end
  250.  
  251. local args, options = shell.parse(...)
  252. if #args == 0 then
  253.   io.write("Usage: edit <filename>[:linenumber]")
  254.   io.write("Example: edit program.lua:53")
  255.   return
  256. end
  257.  
  258. local initial_line = tonumber(args[1]:match(":(%d+)"))
  259. local filename = shell.resolve((args[1]:gsub(":%d+", "")))
  260. local file_parentpath = fs.path(filename)
  261.  
  262. if fs.exists(file_parentpath) and not fs.isDirectory(file_parentpath) then
  263.   io.stderr:write(string.format("Not a directory: %s\n", file_parentpath))
  264.   return 1
  265. end
  266.  
  267. local readonly = options.r or fs.get(filename) == nil or fs.get(filename).isReadOnly()
  268.  
  269. if fs.isDirectory(filename) then
  270.   io.stderr:write("file is a directory\n")
  271.   return 1
  272. elseif not fs.exists(filename) and readonly then
  273.   io.stderr:write("file system is read only\n")
  274.   return 1
  275. end
  276.  
  277. local function loadConfig()
  278.   -- Try to load user settings.
  279.   local env = {}
  280.   local config = loadfile("/etc/edit.cfg", nil, env)
  281.   if config then
  282.     pcall(config)
  283.   end
  284.   -- Fill in defaults.
  285.   env.keybinds = env.keybinds or {
  286.     left = {{"left"}},
  287.     right = {{"right"}},
  288.     up = {{"up"}},
  289.     down = {{"down"}},
  290.     home = {{"home"}},
  291.     eol = {{"end"}},
  292.     pageUp = {{"pageUp"}},
  293.     pageDown = {{"pageDown"}},
  294.  
  295.     backspace = {{"back"}},
  296.     delete = {{"delete"}},
  297.     deleteLine = {{"control", "delete"}, {"shift", "delete"}},
  298.     newline = {{"enter"}},
  299.  
  300.     save = {{"control", "s"}},
  301.     close = {{"control", "w"}},
  302.     find = {{"control", "f"}},
  303.     findnext = {{"control", "g"}, {"control", "n"}, {"f3"}}
  304.   }
  305.   -- Generate config file if it didn't exist.
  306.   if not config then
  307.     local root = fs.get("/")
  308.     if root and not root.isReadOnly() then
  309.       fs.makeDirectory("/etc")
  310.       local f = io.open("/etc/edit.cfg", "w")
  311.       if f then
  312.         local serialization = require("serialization")
  313.         for k, v in pairs(env) do
  314.           f:write(k.."="..tostring(serialization.serialize(v, math.huge)).."\n")
  315.         end
  316.         f:close()
  317.       end
  318.     end
  319.   end
  320.   return env
  321. end
  322.  
  323. term.clear()
  324. term.setCursorBlink(true)
  325.  
  326. local running = true
  327. local buffer = {}
  328. local scrollX, scrollY = 0, 0
  329. local config = loadConfig()
  330.  
  331. local getKeyBindHandler -- forward declaration for refind()
  332.  
  333. local function helpStatusText()
  334.   local function prettifyKeybind(label, command)
  335.     local keybind = type(config.keybinds) == "table" and config.keybinds[command]
  336.     if type(keybind) ~= "table" or type(keybind[1]) ~= "table" then return "" end
  337.     local alt, control, shift, key
  338.     for _, value in ipairs(keybind[1]) do
  339.       if value == "alt" then alt = true
  340.       elseif value == "control" then control = true
  341.       elseif value == "shift" then shift = true
  342.       else key = value end
  343.     end
  344.     if not key then return "" end
  345.     return label .. ": [" ..
  346.            (control and "Ctrl+" or "") ..
  347.            (alt and "Alt+" or "") ..
  348.            (shift and "Shift+" or "") ..
  349.            unicode.upper(key) ..
  350.            "] "
  351.   end
  352.   return prettifyKeybind("Save", "save") ..
  353.          prettifyKeybind("Close", "close") ..
  354.          prettifyKeybind("Find", "find")
  355. end
  356.  
  357. --
  358.  
  359. local function setStatus(value)
  360.   local x, y, w, h = term.getGlobalArea()
  361.   value = unicode.wlen(value) > w - 10 and unicode.wtrunc(value, w - 9) or value
  362.   value = text.padRight(value, w - 10)
  363.   gpu.set(x, y + h - 1, value)
  364. end
  365.  
  366. local function getArea()
  367.   local x, y, w, h = term.getGlobalArea()
  368.   return x, y, w, h - 1
  369. end
  370.  
  371. local function removePrefix(line, length)
  372.   if length >= unicode.wlen(line) then
  373.     return ""
  374.   else
  375.     local prefix = unicode.wtrunc(line, length + 1)
  376.     local suffix = unicode.sub(line, unicode.len(prefix) + 1)
  377.     length = length - unicode.wlen(prefix)
  378.     if length > 0 then
  379.       suffix = (" "):rep(unicode.charWidth(suffix) - length) .. unicode.sub(suffix, 2)
  380.     end
  381.     return suffix
  382.   end
  383. end
  384.  
  385. local function lengthToChars(line, length)
  386.   if length > unicode.wlen(line) then
  387.     return unicode.len(line) + 1
  388.   else
  389.     local prefix = unicode.wtrunc(line, length)
  390.     return unicode.len(prefix) + 1
  391.   end
  392. end
  393.  
  394.  
  395. local function isWideAtPosition(line, x)
  396.   local index = lengthToChars(line, x)
  397.   if index > unicode.len(line) then
  398.     return false, false
  399.   end
  400.   local prefix = unicode.sub(line, 1, index)
  401.   local char = unicode.sub(line, index, index)
  402.   --isWide, isRight
  403.   return unicode.isWide(char), unicode.wlen(prefix) == x
  404. end
  405.  
  406. local function getMultiLineInfo(lineNr, str, timerIndex)
  407.   if lineNr <= 1 then return false end
  408.   if not timerIndex then
  409.     timerIndex = lineNr
  410.     timerStart(lineNr)
  411.   elseif timerElapsed(timerIndex) > max_time_to_determine_if_multiline_comment_in_seconds then
  412.     return false
  413.   end
  414.  
  415.   local line = str or buffer[lineNr] or ""
  416.   local prevLine = buffer[lineNr - 1] or ""
  417.  
  418.   if line:find("]]", 1, true) then
  419.     local prevMulti, first, last = getMultiLineInfo(lineNr - 1, nil, timerIndex)
  420.     local a, b = line:find("]]", 1, true)
  421.     return prevMulti and not last and a ~= nil, false, a ~= nil
  422.   end
  423.  
  424.   if line:find("--[[", 1, true) then --]]
  425.     return true, true, false
  426.   end
  427.  
  428.   local prevMulti, first, last = getMultiLineInfo(lineNr - 1, nil, timerIndex)
  429.   return prevMulti and not last, false, prevMulti and line:find("]]") ~= nil
  430. end
  431.  
  432. local function drawLine(x, y, w, h, lineNr, str)
  433.   local yLocal = lineNr - scrollY
  434.   if yLocal > 0 and yLocal <= h then
  435.     str = str or removePrefix(buffer[lineNr] or "", scrollX)
  436.     str = unicode.wlen(str) > w and unicode.wtrunc(str, w + 1) or str
  437.     str = text.padRight(str, w)
  438.  
  439.     local fg, fgp = gpu.getForeground()
  440.    
  441.     local multi, first, last = getMultiLineInfo(lineNr, str)
  442.     if multi then
  443.       gpu.setForeground(c_highlight_comment)
  444.       if first then
  445.         local ca, cb = str:find("%-%-%[%[.*")
  446.         local _, stop = str:find("]]", ca, true)
  447.         if stop then
  448.           cb = stop
  449.         end
  450.  
  451.         local comment = str:sub(ca, cb)
  452.         gpu.set(x + ca - 1, y - 1 + lineNr - scrollY, comment)
  453.         str = str:gsub(text.escapeMagic(comment), (" "):rep(#comment), 1)
  454.       elseif last then
  455.         local ca, cb = str:find("^.*%]%]")
  456.        
  457.         local comment = str:sub(ca, cb)
  458.         gpu.set(x + ca - 1, y - 1 + lineNr - scrollY, text.padRight(comment, w))
  459.         str = str:gsub(text.escapeMagic(comment), (" "):rep(#comment), 1)
  460.       else
  461.         gpu.set(x, y - 1 + lineNr - scrollY, str)
  462.       end
  463.       gpu.setForeground(fg, fgp)
  464.       return
  465.     end
  466.  
  467.     gpu.set(x, y - 1 + lineNr - scrollY, str)
  468.    
  469.     local wholeStr = str
  470.    
  471.     local cg, cgp = fg, fgp
  472.  
  473.     for _,v in ipairs(highlights) do
  474.       if v.process then
  475.         local a, b
  476.         repeat
  477.           a, b = str:find(v.p)
  478.           if a then
  479.             str = v:process(x, y - 1 + lineNr - scrollY, w, h, lineNr, wholeStr, str, a, b)
  480.           end
  481.         until not a or (v.specialFinishCondition and v:specialFinishCondition(wholeStr, str, a, b))
  482.       elseif v.captures then
  483.         repeat
  484.           local matches = { str:match(v.p) }
  485.           local match = matches[v.captureIndex or 1]
  486.  
  487.           if match then
  488.             if v.c ~= cg then
  489.               cg = v.c
  490.               gpu.setForeground(v.c)
  491.             end
  492.  
  493.             local a, b = str:find(v.p)
  494.            
  495.             a = str:find(match, a, true)
  496.             b = a + #match
  497.            
  498.             gpu.set(x + a - 1, y - 1 + lineNr - scrollY, match)
  499.             str = str:gsub(text.escapeMagic(match), (" "):rep(#match), 1)
  500.           end
  501.         until not match
  502.       else
  503.         repeat
  504.           local a, b = str:find(v.p)
  505.  
  506.           if a then
  507.             if v.c ~= cg then
  508.               cg = v.c
  509.               gpu.setForeground(v.c)
  510.             end
  511.  
  512.             local word = str:sub(a, b)
  513.             gpu.set(x + a - 1, y - 1 + lineNr - scrollY, word)
  514.             str = str:gsub(text.escapeMagic(word), (" "):rep(#word), 1)
  515.           end
  516.         until not a
  517.       end
  518.     end
  519.     gpu.setForeground(fg, fgp)
  520.   end
  521. end
  522.  
  523. local function getCursor()
  524.   local cx, cy = term.getCursor()
  525.   return cx + scrollX, cy + scrollY
  526. end
  527.  
  528. local function line()
  529.   local cbx, cby = getCursor()
  530.   return buffer[cby]
  531. end
  532.  
  533. local function getNormalizedCursor()
  534.   local cbx, cby = getCursor()
  535.   local wide, right = isWideAtPosition(buffer[cby], cbx)
  536.   if wide and right then
  537.     cbx = cbx - 1
  538.   end
  539.   return cbx, cby
  540. end
  541.  
  542. local function setCursor(nbx, nby)
  543.   local x, y, w, h = getArea()
  544.   nby = math.max(1, math.min(#buffer, nby))
  545.  
  546.   local ncy = nby - scrollY
  547.   if ncy > h then
  548.     term.setCursorBlink(false)
  549.     local sy = nby - h
  550.     local dy = math.abs(scrollY - sy)
  551.     scrollY = sy
  552.     if h > dy then
  553.       gpu.copy(x, y + dy, w, h - dy, 0, -dy)
  554.     end
  555.     for lineNr = nby - (math.min(dy, h) - 1), nby do
  556.       drawLine(x, y, w, h, lineNr)
  557.     end
  558.   elseif ncy < 1 then
  559.     term.setCursorBlink(false)
  560.     local sy = nby - 1
  561.     local dy = math.abs(scrollY - sy)
  562.     scrollY = sy
  563.     if h > dy then
  564.       gpu.copy(x, y, w, h - dy, 0, dy)
  565.     end
  566.     for lineNr = nby, nby + (math.min(dy, h) - 1) do
  567.       drawLine(x, y, w, h, lineNr)
  568.     end
  569.   end
  570.   term.setCursor(term.getCursor(), nby - scrollY)
  571.  
  572.   nbx = math.max(1, math.min(unicode.wlen(line()) + 1, nbx))
  573.   local wide, right = isWideAtPosition(line(), nbx)
  574.   local ncx = nbx - scrollX
  575.   if ncx > w or (ncx + 1 > w and wide and not right) then
  576.     term.setCursorBlink(false)
  577.     scrollX = nbx - w + ((wide and not right) and 1 or 0)
  578.     for lineNr = 1 + scrollY, math.min(h + scrollY, #buffer) do
  579.       drawLine(x, y, w, h, lineNr)
  580.     end
  581.   elseif ncx < 1 or (ncx - 1 < 1 and wide and right) then
  582.     term.setCursorBlink(false)
  583.     scrollX = nbx - 1 - ((wide and right) and 1 or 0)
  584.     for lineNr = 1 + scrollY, math.min(h + scrollY, #buffer) do
  585.       drawLine(x, y, w, h, lineNr)
  586.     end
  587.   end
  588.   term.setCursor(nbx - scrollX, nby - scrollY)
  589.   drawLine(x, y, w, h, nby)
  590.   --update with term lib
  591.   nbx, nby = getCursor()
  592.   gpu.set(x + w - 10, y + h, text.padLeft(string.format("%d,%d", nby, nbx), 10))
  593. end
  594.  
  595. local function highlight(bx, by, length, enabled)
  596.   local x, y, w, h = getArea()
  597.   local cx, cy = bx - scrollX, by - scrollY
  598.   cx = math.max(1, math.min(w, cx))
  599.   cy = math.max(1, math.min(h, cy))
  600.   length = math.max(1, math.min(w - cx, length))
  601.  
  602.   local fg, fgp = gpu.getForeground()
  603.   local bg, bgp = gpu.getBackground()
  604.   if enabled then
  605.     gpu.setForeground(bg, bgp)
  606.     gpu.setBackground(fg, fgp)
  607.   end
  608.   local indexFrom = lengthToChars(buffer[by], bx)
  609.   local value = unicode.sub(buffer[by], indexFrom)
  610.   if unicode.wlen(value) > length then
  611.     value = unicode.wtrunc(value, length + 1)
  612.   end
  613.   gpu.set(x - 1 + cx, y - 1 + cy, value)
  614.   if enabled then
  615.     gpu.setForeground(fg, fgp)
  616.     gpu.setBackground(bg, bgp)
  617.   end
  618. end
  619.  
  620. local function home()
  621.   local cbx, cby = getCursor()
  622.   setCursor(1, cby)
  623. end
  624.  
  625. local function ende()
  626.   local cbx, cby = getCursor()
  627.   setCursor(unicode.wlen(line()) + 1, cby)
  628. end
  629.  
  630. local function left()
  631.   local cbx, cby = getNormalizedCursor()
  632.   if cbx > 1 then
  633.     local wideTarget, rightTarget = isWideAtPosition(line(), cbx - 1)
  634.     if wideTarget and rightTarget then
  635.       setCursor(cbx - 2, cby)
  636.     else
  637.       setCursor(cbx - 1, cby)
  638.     end
  639.     return true -- for backspace
  640.   elseif cby > 1 then
  641.     setCursor(cbx, cby - 1)
  642.     ende()
  643.     return true -- again, for backspace
  644.   end
  645. end
  646.  
  647. local function right(n)
  648.   n = n or 1
  649.   local cbx, cby = getNormalizedCursor()
  650.   local be = unicode.wlen(line()) + 1
  651.   local wide, right = isWideAtPosition(line(), cbx + n)
  652.   if wide and right then
  653.     n = n + 1
  654.   end
  655.   if cbx + n <= be then
  656.     setCursor(cbx + n, cby)
  657.   elseif cby < #buffer then
  658.     setCursor(1, cby + 1)
  659.   end
  660. end
  661.  
  662. local function up(n)
  663.   n = n or 1
  664.   local cbx, cby = getCursor()
  665.   if cby > 1 then
  666.     local x, y, w, h = getArea()
  667.     drawLine(x, y, w, h, cby)
  668.     setCursor(cbx, cby - n)
  669.   end
  670. end
  671.  
  672. local function down(n)
  673.   n = n or 1
  674.   local cbx, cby = getCursor()
  675.   if cby < #buffer then
  676.     local x, y, w, h = getArea()
  677.     drawLine(x, y, w, h, cby)
  678.     setCursor(cbx, cby + n)
  679.   end
  680. end
  681.  
  682. local function delete(fullRow)
  683.   local cx, cy = term.getCursor()
  684.   local cbx, cby = getCursor()
  685.   local x, y, w, h = getArea()
  686.   local function deleteRow(row)
  687.     local content = table.remove(buffer, row)
  688.     local rcy = cy + (row - cby)
  689.     if rcy <= h then
  690.       gpu.copy(x, y + rcy, w, h - rcy, 0, -1)
  691.       drawLine(x, y, w, h, row + (h - rcy))
  692.     end
  693.     return content
  694.   end
  695.   if fullRow then
  696.     term.setCursorBlink(false)
  697.     if #buffer > 1 then
  698.       deleteRow(cby)
  699.     else
  700.       buffer[cby] = ""
  701.       gpu.fill(x, y - 1 + cy, w, 1, " ")
  702.     end
  703.     setCursor(1, cby)
  704.   elseif cbx <= unicode.wlen(line()) then
  705.     term.setCursorBlink(false)
  706.     local index = lengthToChars(line(), cbx)
  707.     buffer[cby] = unicode.sub(line(), 1, index - 1) ..
  708.                   unicode.sub(line(), index + 1)
  709.     drawLine(x, y, w, h, cby)
  710.   elseif cby < #buffer then
  711.     term.setCursorBlink(false)
  712.     local append = deleteRow(cby + 1)
  713.     buffer[cby] = buffer[cby] .. append
  714.     drawLine(x, y, w, h, cby)
  715.   else
  716.     return
  717.   end
  718.   setStatus(helpStatusText())
  719. end
  720.  
  721. local function insert(value)
  722.   if not value or unicode.len(value) < 1 then
  723.     return
  724.   end
  725.   term.setCursorBlink(false)
  726.   local cx, cy = term.getCursor()
  727.   local cbx, cby = getCursor()
  728.   local x, y, w, h = getArea()
  729.   local index = lengthToChars(line(), cbx)
  730.   buffer[cby] = unicode.sub(line(), 1, index - 1) ..
  731.                 value ..
  732.                 unicode.sub(line(), index)
  733.   drawLine(x, y, w, h, cby)
  734.   right(unicode.wlen(value))
  735.   setStatus(helpStatusText())
  736. end
  737.  
  738. local function enter()
  739.   term.setCursorBlink(false)
  740.   local cx, cy = term.getCursor()
  741.   local cbx, cby = getCursor()
  742.   local x, y, w, h = getArea()
  743.   local index = lengthToChars(line(), cbx)
  744.   table.insert(buffer, cby + 1, unicode.sub(buffer[cby], index))
  745.   buffer[cby] = unicode.sub(buffer[cby], 1, index - 1)
  746.   drawLine(x, y, w, h, cby)
  747.   if cy < h then
  748.     if cy < h - 1 then
  749.       gpu.copy(x, y + cy, w, h - (cy + 1), 0, 1)
  750.     end
  751.     drawLine(x, y, w, h, cby + 1)
  752.   end
  753.   setCursor(1, cby + 1)
  754.   setStatus(helpStatusText())
  755. end
  756.  
  757. local findText = ""
  758.  
  759. local function find()
  760.   local x, y, w, h = getArea()
  761.   local cx, cy = term.getCursor()
  762.   local cbx, cby = getCursor()
  763.   local ibx, iby = cbx, cby
  764.   while running do
  765.     if unicode.len(findText) > 0 then
  766.       local sx, sy
  767.       for syo = 1, #buffer do -- iterate lines with wraparound
  768.         sy = (iby + syo - 1 + #buffer - 1) % #buffer + 1
  769.         sx = string.find(buffer[sy], findText, syo == 1 and ibx or 1, true)
  770.         if sx and (sx >= ibx or syo > 1) then
  771.           break
  772.         end
  773.       end
  774.       if not sx then -- special case for single matches
  775.         sy = iby
  776.         sx = string.find(buffer[sy], findText, nil, true)
  777.       end
  778.       if sx then
  779.         sx = unicode.wlen(string.sub(buffer[sy], 1, sx - 1)) + 1
  780.         cbx, cby = sx, sy
  781.         setCursor(cbx, cby)
  782.         highlight(cbx, cby, unicode.wlen(findText), true)
  783.       end
  784.     end
  785.     term.setCursor(7 + unicode.wlen(findText), h + 1)
  786.     setStatus("Find: " .. findText)
  787.  
  788.     local _, address, char, code = term.pull("key_down")
  789.     if address == term.keyboard() then
  790.       local handler, name = getKeyBindHandler(code)
  791.       highlight(cbx, cby, unicode.wlen(findText), false)
  792.       if name == "newline" then
  793.         break
  794.       elseif name == "close" then
  795.         handler()
  796.       elseif name == "backspace" then
  797.         findText = unicode.sub(findText, 1, -2)
  798.       elseif name == "find" or name == "findnext" then
  799.         ibx = cbx + 1
  800.         iby = cby
  801.       elseif not keyboard.isControl(char) then
  802.         findText = findText .. unicode.char(char)
  803.       end
  804.     end
  805.   end
  806.   setCursor(cbx, cby)
  807.   setStatus(helpStatusText())
  808. end
  809.  
  810. -------------------------------------------------------------------------------
  811.  
  812. local keyBindHandlers = {
  813.   left = left,
  814.   right = right,
  815.   up = up,
  816.   down = down,
  817.   home = home,
  818.   eol = ende,
  819.   pageUp = function()
  820.     local x, y, w, h = getArea()
  821.     up(h - 1)
  822.   end,
  823.   pageDown = function()
  824.     local x, y, w, h = getArea()
  825.     down(h - 1)
  826.   end,
  827.  
  828.   backspace = function()
  829.     if not readonly and left() then
  830.       delete()
  831.     end
  832.   end,
  833.   delete = function()
  834.     if not readonly then
  835.       delete()
  836.     end
  837.   end,
  838.   deleteLine = function()
  839.     if not readonly then
  840.       delete(true)
  841.     end
  842.   end,
  843.   newline = function()
  844.     if not readonly then
  845.       enter()
  846.     end
  847.   end,
  848.  
  849.   save = function()
  850.     if readonly then return end
  851.     local new = not fs.exists(filename)
  852.     local backup
  853.     if not new then
  854.       backup = filename .. "~"
  855.       for i = 1, math.huge do
  856.         if not fs.exists(backup) then
  857.           break
  858.         end
  859.         backup = filename .. "~" .. i
  860.       end
  861.       fs.copy(filename, backup)
  862.     end
  863.     if not fs.exists(file_parentpath) then
  864.       fs.makeDirectory(file_parentpath)
  865.     end
  866.     local f, reason = io.open(filename, "w")
  867.     if f then
  868.       local chars, firstLine = 0, true
  869.       for _, line in ipairs(buffer) do
  870.         if not firstLine then
  871.           line = "\n" .. line
  872.         end
  873.         firstLine = false
  874.         f:write(line)
  875.         chars = chars + unicode.len(line)
  876.       end
  877.       f:close()
  878.       local format
  879.       if new then
  880.         format = [["%s" [New] %dL,%dC written]]
  881.       else
  882.         format = [["%s" %dL,%dC written]]
  883.       end
  884.       setStatus(string.format(format, fs.name(filename), #buffer, chars))
  885.     else
  886.       setStatus(reason)
  887.     end
  888.     if not new then
  889.       fs.remove(backup)
  890.     end
  891.   end,
  892.   close = function()
  893.     -- TODO ask to save if changed
  894.     running = false
  895.   end,
  896.   find = function()
  897.     findText = ""
  898.     find()
  899.   end,
  900.   findnext = find
  901. }
  902.  
  903. getKeyBindHandler = function(code)
  904.   if type(config.keybinds) ~= "table" then return end
  905.   -- Look for matches, prefer more 'precise' keybinds, e.g. prefer
  906.   -- ctrl+del over del.
  907.   local result, resultName, resultWeight = nil, nil, 0
  908.   for command, keybinds in pairs(config.keybinds) do
  909.     if type(keybinds) == "table" and keyBindHandlers[command] then
  910.       for _, keybind in ipairs(keybinds) do
  911.         if type(keybind) == "table" then
  912.           local alt, control, shift, key
  913.           for _, value in ipairs(keybind) do
  914.             if value == "alt" then alt = true
  915.             elseif value == "control" then control = true
  916.             elseif value == "shift" then shift = true
  917.             else key = value end
  918.           end
  919.           local keyboardAddress = term.keyboard()
  920.           if (not alt or keyboard.isAltDown(keyboardAddress)) and
  921.              (not control or keyboard.isControlDown(keyboardAddress)) and
  922.              (not shift or keyboard.isShiftDown(keyboardAddress)) and
  923.              code == keyboard.keys[key] and
  924.              #keybind > resultWeight
  925.           then
  926.             resultWeight = #keybind
  927.             resultName = command
  928.             result = keyBindHandlers[command]
  929.           end
  930.         end
  931.       end
  932.     end
  933.   end
  934.   return result, resultName
  935. end
  936.  
  937. -------------------------------------------------------------------------------
  938.  
  939. local function onKeyDown(char, code)
  940.   local handler = getKeyBindHandler(code)
  941.   if handler then
  942.     handler()
  943.   elseif readonly and code == keyboard.keys.q then
  944.     running = false
  945.   elseif not readonly then
  946.     if not keyboard.isControl(char) then
  947.       insert(unicode.char(char))
  948.     elseif unicode.char(char) == "\t" then
  949.       insert("  ")
  950.     end
  951.   end
  952. end
  953.  
  954. local function onClipboard(value)
  955.   value = value:gsub("\r\n", "\n")
  956.   local cbx, cby = getCursor()
  957.   local start = 1
  958.   local l = value:find("\n", 1, true)
  959.   if l then
  960.     repeat
  961.       local line = string.sub(value, start, l - 1)
  962.       line = text.detab(line, 2)
  963.       insert(line)
  964.       enter()
  965.       start = l + 1
  966.       l = value:find("\n", start, true)
  967.     until not l
  968.   end
  969.   insert(string.sub(value, start))
  970. end
  971.  
  972. local function onClick(x, y)
  973.   setCursor(x + scrollX, y + scrollY)
  974. end
  975.  
  976. local function onScroll(direction)
  977.   local cbx, cby = getCursor()
  978.   setCursor(cbx, cby - direction * 12)
  979. end
  980.  
  981. -------------------------------------------------------------------------------
  982.  
  983. do
  984.   local f = io.open(filename)
  985.   if f then
  986.     local x, y, w, h = getArea()
  987.     local chars = 0
  988.     for line in f:lines() do
  989.       table.insert(buffer, line)
  990.       chars = chars + unicode.len(line)
  991.       if #buffer <= h then
  992.         drawLine(x, y, w, h, #buffer)
  993.       end
  994.     end
  995.     f:close()
  996.     if #buffer == 0 then
  997.       table.insert(buffer, "")
  998.     end
  999.     local format
  1000.     if readonly then
  1001.       format = [["%s" [readonly] %dL,%dC]]
  1002.     else
  1003.       format = [["%s" %dL,%dC]]
  1004.     end
  1005.     setStatus(string.format(format, fs.name(filename), #buffer, chars))
  1006.   else
  1007.     table.insert(buffer, "")
  1008.     setStatus(string.format([["%s" [New File] ]], fs.name(filename)))
  1009.   end
  1010.  
  1011.   setCursor(1, initial_line or 1)
  1012. end
  1013.  
  1014. while running do
  1015.   local event, address, arg1, arg2, arg3 = term.pull()
  1016.   if address == term.keyboard() or address == term.screen() then
  1017.     local blink = true
  1018.     if event == "key_down" then
  1019.       onKeyDown(arg1, arg2)
  1020.     elseif event == "clipboard" and not readonly then
  1021.       onClipboard(arg1)
  1022.     elseif event == "touch" or event == "drag" then
  1023.       local x, y, w, h = getArea()
  1024.       arg1 = arg1 - x + 1
  1025.       arg2 = arg2 - y + 1
  1026.       if arg1 >= 1 and arg2 >= 1 and arg1 <= w and arg2 <= h then
  1027.         onClick(arg1, arg2)
  1028.       end
  1029.     elseif event == "scroll" then
  1030.       onScroll(arg3)
  1031.     else
  1032.       blink = false
  1033.     end
  1034.     if blink then
  1035.       term.setCursorBlink(true)
  1036.     end
  1037.   end
  1038. end
  1039.  
  1040. term.clear()
  1041. term.setCursorBlink(true)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement