LoganDark

ShEdit

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