--[[ Eldit (still being made) by LDDestroier wget https://raw.githubusercontent.com/LDDestroier/CC/master/eldit.lua TO DO: - MAJOR: Merge selections that intersect - MAJOR: Allow selecting with Shift + ArrowKeys - MAJOR: Fix deleting multiple selections (MUST delete selections from bottom to top) - MAJOR: Add syntax highlighting - Add more keyboard shortcuts - Add help menu - Eventually add simultaneous peer editing --]] local scr_x, scr_y = term.getSize() local argData = { ["-l"] = "number" } local eldit, config = {}, {} eldit.buffer = {{}} -- stores all text, organized like eldit.buffer[yPos][xPos] eldit.undoBuffer = {{{}}} -- stores buffers for undoing/redoing eldit.allowUndo = true -- whether or not to allow undoing/redoing eldit.maxUndo = 16 -- maximum size of the undo buffer eldit.undoPos = 1 -- current position in undo buffer eldit.undoDelay = 0.3 -- amount of time to wait after typing, before the buffer is put in the undo buffer eldit.clipboards = {} -- all clipboard entries eldit.selectedClipboard = 1 -- which clipboard to use eldit.scrollX = 0 -- horizontal scroll eldit.scrollY = 0 -- vertical scroll eldit.selections = {} -- all selected areas eldit.size = { x = 1, -- top left corner X y = 1, -- top left corner Y width = scr_x, -- horizontal size height = scr_y -- vertical size } -- made-up keys for easier use of both left and right modifier keys keys.shift = 127 keys.alt = 128 keys.ctrl = 129 config.showLineNumberIndicator = false config.showWhitespace = true config.showTrailingSpace = true config.findExtension = true -- minor optimizations, I think local concatTable = table.concat local sortTable = table.sort -- I'm never using regular argument parsing again, this function rules local interpretArgs = function(tInput, tArgs) local output = {} local errors = {} local usedEntries = {} for aName, aType in pairs(tArgs) do output[aName] = false for i = 1, #tInput do if not usedEntries[i] then if tInput[i] == aName and not output[aName] then if aType then usedEntries[i] = true if type(tInput[i+1]) == aType or type(tonumber(tInput[i+1])) == aType then usedEntries[i+1] = true if aType == "number" then output[aName] = tonumber(tInput[i+1]) else output[aName] = tInput[i+1] end else output[aName] = nil errors[1] = errors[1] and (errors[1] + 1) or 1 errors[aName] = "expected " .. aType .. ", got " .. type(tInput[i+1]) end else usedEntries[i] = true output[aName] = true end end end end end for i = 1, #tInput do if not usedEntries[i] then output[#output+1] = tInput[i] end end return output, errors end local argList = interpretArgs({...}, argData) eldit.filename = argList[1] and shell.resolve(argList[1]) if eldit.filename then if fs.isDir(eldit.filename) then error("Cannot edit a directory.", 0) end if config.findExtension then if not fs.exists(eldit.filename) then local m local d = fs.list(fs.getDir(eldit.filename)) for i = 1, #d do m = d[i]:match(fs.getName(eldit.filename) .. "%....$") if m then eldit.filename = fs.combine(fs.getDir(eldit.filename), m) break end end end end end eldit.cursors = {{ x = 1, y = math.max(1, argList["-l"] or 1), lastX = 1 }} local eClearLine = function(y) local cx, cy = term.getCursorPos() term.setCursorPos(eldit.size.x, y or cy) term.write((" "):rep(eldit.size.width)) term.setCursorPos(cx, cy) end local eClear = function() local cx, cy = term.getCursorPos() for y = eldit.size.y, eldit.size.y + eldit.size.height - 1 do term.setCursorPos(eldit.size.x, y) term.write((" "):rep(eldit.size.width)) end term.setCursorPos(cx, cy) end -- sorts all selections based on each of their (x,y) positions (top left first) local sortSelections = function() for id,sel in pairs(eldit.selections) do sortTable(sel, function(a,b) return (a.y * eldit.size.width) + a.x < (b.y * eldit.size.width) + b.x end) end end -- sorts all cursors based on (x,y) position (top left first) local sortCursors = function() sortTable(eldit.cursors, function(a,b) return (a.y * eldit.size.width) + a.x < (b.y * eldit.size.width) + b.x end) end local explode = function(div, str, replstr, includeDiv) if (div == '') then return false end local pos, arr = 0, {} for st, sp in function() return string.find(str, div, pos, false) end do table.insert(arr, string.sub(replstr or str, pos, st - 1 + (includeDiv and #div or 0))) pos = sp + 1 end table.insert(arr, string.sub(replstr or str, pos)) return arr end local readFile = function(path) if fs.exists(path) then local file = fs.open(path, "r") local contents = file.readAll() file.close() return contents else return nil end end local writeFile = function(path, contents) if fs.isReadOnly(path) or fs.isDir(path) then return false else local file = fs.open(path, "w") file.write(contents) file.close() return true end end local deepCopy deepCopy = function(tbl) local output = {} for k,v in pairs(tbl) do if type(v) == "table" then output[k] = deepCopy(v) else output[k] = v end end return output end local choice = function(input, breakKeys, returnNumber) local fpos = 0 repeat event, key = os.pullEvent("char") key = key or "" if type(breakKeys) == "table" then for a = 1, #breakKeys do if key == breakKeys[a] then return "" end end end fpos = string.find(input, key) until fpos return returnNumber and fpos or key end prompt = function(prebuffer, precy, maxY, _eldit) term.setCursorBlink(false) local keysDown = {} -- list of all keys being pressed local miceDown = {} -- list of all mouse buttons being pressed eldit = _eldit or eldit -- you can replace the "eldit" table if you want I guess maxY = maxY or math.huge -- limits amount of lines local defaultBarLife = 10 -- default amount of time bar msg will stay onscreen local barmsg = "Started Eldit." -- message displayed on bottom screen local barlife = defaultBarLife local lastMouse = {} -- last place you clicked onscreen local isSelecting = false -- whether or not you are selecting text if type(prebuffer) == "string" then -- enter a "prebuffer" (string or table) to set the contents for i = 1, #prebuffer do if prebuffer:sub(i,i) == "\n" then eldit.buffer[#eldit.buffer + 1] = {} else eldit.buffer[#eldit.buffer][#eldit.buffer[#eldit.buffer] + 1] = prebuffer:sub(i,i) end end elseif type(prebuffer) == "table" then eldit.buffer = prebuffer end eldit.undoBuffer[1] = { buffer = deepCopy(eldit.buffer), cursors = deepCopy(eldit.cursors), selections = deepCopy(eldit.selections) } local isCursorBlink = false -- blinks the background color on each cursor local isInsert = false -- will overwrite characters instead of appending them -- gets length of left line numbers, if enabled at all local getLineNoLen = function() if config.showLineNumberIndicator then return #tostring(#eldit.buffer) else return 0 end end -- list of all characters that will stop a CTRL+Backspace or CTRL+Delete or CTRL+Left/Right local interruptable = { [" "] = true, ["["] = true, ["]"] = true, ["{"] = true, ["}"] = true, ["("] = true, [")"] = true, ["|"] = true, ["/"] = true, ["\\"] = true, ["+"] = true, ["-"] = true, ["*"] = true, ["="] = true, ["."] = true, [","] = true } -- checks if (checkX, checkY) is between (x1, y1) and (x2, y2) in terms of selection (it's not a rectangular check) local checkWithinArea = function(checkX, checkY, x1, y1, x2, y2) if checkY == y1 then if y1 == y2 then return checkX >= x1 and checkX <= x2 else return checkX >= x1 end elseif checkY == y2 then if y1 == y2 then return checkX >= x1 and checkX <= x2 else return checkX <= x2 and checkX >= 1 end elseif checkY > y1 and checkY < y2 then return true else return false end end -- goes over every selection and checks if it is selected -- (x, y) = position on buffer local checkIfSelected = function(x, y) sortSelections() local fin if y >= 1 and y <= #eldit.buffer then if x >= 1 and x <= #eldit.buffer[y] + 1 then for id, sel in pairs(eldit.selections) do if checkWithinArea(x, y, sel[1].x, sel[1].y, sel[2].x, sel[2].y) then return id end end end end return false end -- goes over every cursor and checks if they are at (x, y) -- (x,y) = position on buffer local checkIfCursor = function(x, y) for id, cur in pairs(eldit.cursors) do if x == cur.x and y == cur.y then return id end end return false end -- returns character at (x, y) on the buffer local getChar = function(x, y) if eldit.buffer[y] then return eldit.buffer[y][x] else return nil end end -- all characters that count as whitespace local tab = { [" "] = true, ["\9"] = true } -- the big boi function, draws **EVERYTHING** local render = function() local cx, cy local lineNoLen = getLineNoLen() local isHighlighted = false local textPoses = {math.huge, -math.huge} -- used to identify space characters without text local screen = {{},{},{}} for y = 1, eldit.size.height - 1 do -- minus one because it reserves space for the bar screen[1][y] = {} screen[2][y] = {} screen[3][y] = {} cy = y + eldit.scrollY -- find text if eldit.buffer[cy] and (config.showWhitespace or config.showTrailingSpace) then textPoses = { #(concatTable(eldit.buffer[cy]):match("^ +") or "") + 1, #eldit.buffer[cy] - #(concatTable(eldit.buffer[y]):match(" +$") or "") } end if cy <= #eldit.buffer and lineNoLen > 0 then isHighlighted = false for id,cur in pairs(eldit.cursors) do if cy == cur.y then isHighlighted = true break end end if not isHighlighted then for id,sel in pairs(eldit.selections) do if cy >= sel[1].y and cy <= sel[2].y then isHighlighted = true break end end end if isHighlighted then term.setBackgroundColor(colors.gray) term.setTextColor(colors.white) else term.setBackgroundColor(colors.black) term.setTextColor(colors.lightGray) end term.setCursorPos(eldit.size.x, eldit.size.y + y - 1) term.write(cy .. (" "):rep(lineNoLen - #tostring(y))) end -- actually draw text local cChar, cTxt, cBg = " ", " ", " " term.setCursorPos(eldit.size.x + lineNoLen, eldit.size.y + y - 1) for x = lineNoLen + 1, eldit.size.width do cx = x + eldit.scrollX - lineNoLen if checkIfCursor(cx, cy) and isCursorBlink then if isInsert then cTxt, cBg = "8", "0" else cTxt, cBg = "f", "8" end else if checkIfSelected(cx, cy) then cBg = "b" else cBg = "f" end cTxt = "0" end if config.showWhitespace or config.showTrailingSpace then if textPoses[1] and textPoses[2] and eldit.buffer[cy] then if cx < textPoses[1] and eldit.buffer[cy][cx] then cTxt = "7" cChar = "|" elseif (cx > textPoses[2] and eldit.buffer[cy][cx]) then cTxt = "7" cChar = "-" else cChar = getChar(cx, cy) or " " end else cChar = getChar(cx, cy) or " " end else cChar = getChar(cx, cy) or " " end screen[1][y][x - lineNoLen] = cChar screen[2][y][x - lineNoLen] = cTxt screen[3][y][x - lineNoLen] = cBg end term.blit( concatTable(screen[1][y]), concatTable(screen[2][y]), concatTable(screen[3][y]) ) end term.setCursorPos(eldit.size.x, eldit.size.y + eldit.size.height - 1) term.setBackgroundColor(colors.black) eClearLine() if barlife > 0 then term.setTextColor(colors.yellow) term.write(barmsg) else term.setTextColor(colors.yellow) for id,cur in pairs(eldit.cursors) do term.write("(" .. cur.x .. "," .. cur.y .. ") ") end end end -- if all cursors are offscreen, will scroll so that at least one of them is onscreen local scrollToCursor = function() lineNoLen = getLineNoLen() local lowCur, highCur = eldit.cursors[1], eldit.cursors[1] local leftCur, rightCur = eldit.cursors[1], eldit.cursors[1] for id,cur in pairs(eldit.cursors) do if cur.y < lowCur.y then lowCur = cur elseif cur.y > highCur.y then highCur = cur end if cur.x < leftCur.x then leftCur = cur elseif cur.y > rightCur.x then rightCur = cur end end if lowCur.y - eldit.scrollY < 1 then eldit.scrollY = -1 + highCur.y elseif highCur.y - eldit.scrollY > -1 + eldit.size.height then eldit.scrollY = 1 + lowCur.y - eldit.size.height end if leftCur.x - eldit.scrollX < 1 then eldit.scrollX = -1 + rightCur.x elseif rightCur.x - eldit.scrollX > eldit.size.width - lineNoLen then eldit.scrollX = leftCur.x - (eldit.size.width - lineNoLen) end end -- gets the widest line length in all the buffer local getMaximumWidth = function() local maxX = 0 for y = 1, #eldit.buffer do maxX = math.max(maxX, #eldit.buffer[y]) end return maxX end -- scrolls the screen, and fixes it if it's set to some weird value local adjustScroll = function(modx, mody) modx, mody = modx or 0, mody or 0 local lineNoLen = getLineNoLen() if mody then eldit.scrollY = math.min( math.max( 0, eldit.scrollY + mody ), math.max( 0, 1 + #eldit.buffer - eldit.size.height ) ) end if modx then eldit.scrollX = math.min( math.max( 0, eldit.scrollX + modx ), math.max( 0, 1 + getMaximumWidth() - eldit.size.width - lineNoLen ) ) end end -- removes any cursors that share positions local removeRedundantCursors = function() local xes = {} for i = #eldit.cursors, 1, -1 do if xes[eldit.cursors[i].x] == eldit.cursors[i].y then table.remove(eldit.cursors, i) else xes[eldit.cursors[i].x] = eldit.cursors[i].y end end end -- deletes text at every cursor position, either forward or backward or neutral local deleteText = function(mode, direction, _cx, _cy) local xAdjList = {} local yAdj = 0 sortCursors() local rowBuff -- represents the buffer row at the current cursor's Y local startOnInterruptable for id,cur in pairs(eldit.cursors) do cx = _cx or cur.x - (xAdjList[_cy or cur.y] or 0) cy = _cy or cur.y - yAdj rowBuff = eldit.buffer[cy] or {} startOnInterruptable = interruptable[rowBuff[cx]] or (not rowBuff[cx]) if mode == "single" or (direction == "forward" and cx == #eldit.buffer[cy] or (direction == "backward" and cx == 1)) then if direction == "forward" then if cx < #eldit.buffer[cy] then xAdjList[cy] = (xAdjList[cy] or 0) + 1 table.remove(eldit.buffer[cy], cx) elseif cy < #eldit.buffer then for i = 1, #eldit.buffer[cy + 1] do table.insert(eldit.buffer[cy], eldit.buffer[cy + 1][i]) end table.remove(eldit.buffer, cy + 1) yAdj = yAdj + 1 end elseif direction == "backward" then if cx > 1 then cx = cx - 1 xAdjList[cy] = (xAdjList[cy] or 0) + 1 table.remove(eldit.buffer[cy], cx) elseif cy > 1 then cx = #eldit.buffer[cy - 1] + 1 for i = 1, #eldit.buffer[cy] do table.insert(eldit.buffer[cy - 1], eldit.buffer[cy][i]) end table.remove(eldit.buffer, cy) yAdj = yAdj + 1 cy = cy - 1 end else if cx >= 1 and cx <= #eldit.buffer[cy] then table.remove(eldit.buffer[cy], cx) elseif cx == #eldit.buffer[cy] + 1 and cy < #eldit.buffer then for i = 1, #eldit.buffer[cy + 1] do table.insert(eldit.buffer[cy], eldit.buffer[cy + 1][i]) end table.remove(eldit.buffer, cy + 1) yAdj = yAdj + 1 end end elseif mode == "word" then local pos = cx if direction == "forward" then while true do pos = pos + 1 if startOnInterruptable then if (not interruptable[rowBuff[pos]]) or (not rowBuff[pos]) then startOnInterruptable = false end else if interruptable[rowBuff[pos]] or (not rowBuff[pos]) then break end end if (pos + 1) < 0 or (pos + 1) > #rowBuff + 1 then break end end for i = pos, cx, -1 do xAdjList[cy] = (xAdjList[cy] or 0) + 1 table.remove(eldit.buffer[cy], i) end else while true do pos = pos - 1 if startOnInterruptable then if (not interruptable[rowBuff[pos]]) or (not rowBuff[pos]) then startOnInterruptable = false end else if interruptable[rowBuff[pos]] or (not rowBuff[pos]) then break end end if (pos - 1) < 0 or (pos - 1) > #rowBuff + 1 then break end end pos = math.max(1, pos) for i = cx - 1, pos, -1 do table.remove(eldit.buffer[cy], i) end cx = pos end elseif mode == "line" then -- like word but is only interrupted by newline if direction == "forward" then for i = cx, #eldit.buffer[cy] do eldit.buffer[cy][i] = nil end else for i = cx, 1, -1 do table.remove(eldit.buffer[cy], i) end end end if _cx then return yAdj else cur.x = cx cur.y = cy cur.lastX = cx end end removeRedundantCursors() if not isSelecting then scrollToCursor() end return yAdj end local indentLines = function(goBackward) sortSelections() local safeY = {} for id,sel in pairs(eldit.selections) do for y = sel[1].y, sel[2].y do if not safeY[y] then if goBackward then if eldit.buffer[y][1] == "\9" or eldit.buffer[y][1] == " " then table.remove(eldit.buffer[y], 1) if y == sel[1].y then sel[1].x = math.max(1, -1 + sel[1].x) elseif y == sel[2].y then sel[2].x = math.max(1, -1 + sel[2].x) end for idd,cur in pairs(eldit.cursors) do if cur.y == y and cur.x > 1 then cur.x = -1 + cur.x cur.lastX = cur.x end end end elseif eldit.buffer[y] then table.insert(eldit.buffer[y], 1, "\9") if y == sel[1].y then sel[1].x = 1 + sel[1].x elseif y == sel[2].y then sel[2].x = 1 + sel[2].x end for idd,cur in pairs(eldit.cursors) do if cur.y == y and cur.x < #eldit.buffer[y] then cur.x = 1 + cur.x cur.lastX = cur.x end end end end safeY[y] = true end end for id,cur in pairs(eldit.cursors) do if not safeY[cur.y] then if goBackward then if eldit.buffer[cur.y][1] == "\9" or eldit.buffer[cur.y][1] == " " then table.remove(eldit.buffer[cur.y], 1) cur.x = -1 + cur.x cur.lastX = cur.x end else table.insert(eldit.buffer[cur.y], 1, "\9") cur.x = 1 + cur.x cur.lastX = cur.x end end end end -- moves the cursor by (xmod, ymod), and fixes its position if it's set to an invalid one local adjustCursor = function(_xmod, _ymod, setLastX, mode, doNotDelSelections, adjustSelections, doNotTouchScroll) local step = (_xmod / math.abs(_xmod)) local rowBuff -- represents the buffer row at the current cursor's Y local startOnInterruptable local origCX, origCY for id,cur in pairs(eldit.cursors) do origCX, origCY = cur.x, cur.y rowBuff = eldit.buffer[cur.y] or {} startOnInterruptable = interruptable[rowBuff[cur.x + step]] if mode == "word" then xmod = step ymod = 0 while true do xmod = xmod + step if math.abs(xmod) > math.abs(step) then if startOnInterruptable then if (not interruptable[rowBuff[cur.x + xmod]]) or (not rowBuff[cur.x + xmod]) then startOnInterruptable = false end else if interruptable[rowBuff[cur.x + xmod]] or (not rowBuff[cur.x + xmod]) then break end end end if (cur.x + xmod + step) < 0 or (cur.x + xmod + step) > #rowBuff + 1 then break end end xmod = xmod - math.min(0, math.max(xmod, -1)) else xmod = _xmod ymod = _ymod end if mode == "flip" then if eldit.buffer[cur.y + ymod] then eldit.buffer[cur.y], eldit.buffer[cur.y + ymod] = eldit.buffer[cur.y + ymod], eldit.buffer[cur.y] end end cur.x = cur.x + xmod cur.y = math.max(1, math.min(cur.y + ymod, #eldit.buffer)) if xmod ~= 0 then repeat if cur.x < 1 and cur.y > 1 then cur.y = cur.y - 1 cur.x = cur.x + #eldit.buffer[cur.y] + 1 elseif cur.x > #eldit.buffer[cur.y] + 1 and cur.y < #eldit.buffer then -- cur.x = cur.x - #eldit.buffer[cur.y] - 1 cur.x = 1 cur.y = cur.y + 1 end until (cur.x >= 1 and cur.x <= #eldit.buffer[cur.y] + 1) or ((cur.y == 1 and xmod < 0) or (cur.y == #eldit.buffer and xmod > 0)) end cur.lastX = setLastX and cur.x or cur.lastX if cur.y < 1 then cur.y = math.max(1, math.min(cur.y, #eldit.buffer)) cur.x = 1 elseif cur.y > #eldit.buffer then cur.y = math.max(1, math.min(cur.y, #eldit.buffer)) cur.x = #eldit.buffer[cur.y] + 1 else cur.y = math.max(1, math.min(cur.y, #eldit.buffer)) cur.x = math.max(1, math.min(cur.x, #eldit.buffer[cur.y] + 1)) end if adjustSelections then for sid, sel in pairs(eldit.selections) do end end end removeRedundantCursors() if (not keysDown[keys.ctrl]) and not (xmod == 0 and ymod == 0) and not doNotDelSelections then eldit.selections = {} isSelecting = false end if (not isSelecting) and (not doNotTouchScroll) then scrollToCursor() end end -- deletes the parts of the buffer that are selected, then clears the selection list local deleteSelections = function() sortSelections() if #eldit.selections == 0 then return {}, {} end local xAdjusts = {} local yAdjusts = {} local xAdj = 0 local yAdj = 0 for id,sel in pairs(eldit.selections) do for y = sel[1].y, sel[2].y do xAdj = 0 if eldit.buffer[y] then xAdjusts[y] = xAdjusts[y] or {} if checkWithinArea(#eldit.buffer[y] + 1, y, sel[1].x, sel[1].y, sel[2].x, sel[2].y) then yAdj = yAdj + 1 end yAdjusts[y + 1] = math.min(yAdjusts[y + 1] or math.huge, yAdj) for x = 2, #eldit.buffer[y] do xAdjusts[y][x] = math.min(xAdjusts[y][x] or math.huge, xAdj) if checkWithinArea(x, y, sel[1].x, sel[1].y, sel[2].x, sel[2].y) then xAdj = xAdj + 1 end end end end end for id,sel in pairs(eldit.selections) do for y = sel[2].y, sel[1].y, -1 do if eldit.buffer[y] then for x = #eldit.buffer[y] + 1, 1, -1 do if checkWithinArea(x, y, sel[1].x, sel[1].y, sel[2].x, sel[2].y) then if x == #eldit.buffer[y] + 1 then if eldit.buffer[y + 1] then for i = 1, #eldit.buffer[y + 1] do table.insert(eldit.buffer[y], eldit.buffer[y + 1][i]) end table.remove(eldit.buffer, y + 1) end else deleteText("single", nil, x, y) end end end end end end eldit.selections = {} adjustCursor(0, 0, true) return xAdjusts, yAdjusts end -- puts text at every cursor position local placeText = function(text, cursorList) local xAdjusts, yAdjusts = deleteSelections() removeRedundantCursors() sortCursors() local xAdjList = {} for id,cur in pairs(cursorList or eldit.cursors) do cur.y = cur.y - (yAdjusts[cur.y] or 0) cur.x = cur.x - ((xAdjusts[cur.y] or {})[cur.x] or 0) + (xAdjList[cur.y] or 0) for i = 1, #text do if isInsert then if cur.x == #eldit.buffer[cur.y] + 1 then for i = 1, #eldit.buffer[cur.y + 1] do table.insert(eldit.buffer[cur.y], eldit.buffer[cur.y + 1][i]) end table.remove(eldit.buffer, cur.y + 1) end eldit.buffer[cur.y][cur.x + i - 1] = text:sub(i,i) else table.insert(eldit.buffer[cur.y], cur.x, text:sub(i,i)) if #xAdjusts + #yAdjusts == 0 then xAdjList[cur.y] = (xAdjList[cur.y] or 0) + 1 end end cur.x = cur.x + 1 end cur.lastX = cur.x end if not isSelecting then scrollToCursor() end end -- adds a new line to the buffer at every cursor position local makeNewLine = function(cursorList) for id,cur in pairs(cursorList or eldit.cursors) do table.insert(eldit.buffer, cur.y + 1, {}) for i = cur.x, #eldit.buffer[cur.y] do if i > cur.x or not isInsert then table.insert(eldit.buffer[cur.y + 1], eldit.buffer[cur.y][i]) end eldit.buffer[cur.y][i] = nil end cur.x = 1 cur.y = cur.y + 1 end if not isSelecting then scrollToCursor() end end local compareBuffers compareBuffers = function(left, right) for k,v in pairs(left) do if type(v) == "table" then if not compareBuffers(v, right[k]) then return false end elseif right then if left[k] ~= right[k] then return false end else return false end end return true end -- simulate key inputs, for pre-entering text into read() local simType = function(text) for i = 1, #text do os.queueEvent("key", keys[text:sub(i,i)]) os.queueEvent("char", text:sub(i,i)) end end -- saves to file, duhh local saveFile = function(preSaveAs) keysDown, miceDown = {}, {} local compiled = "" for y = 1, #eldit.buffer do compiled = compiled .. concatTable(eldit.buffer[y]) if y < #eldit.buffer then compiled = compiled .. "\n" end end if preSaveAs or (not eldit.filename) then local newName, cx, cy = "" if type(preSaveAs) == "string" then simType(preSaveAs) end repeat render() term.setCursorPos(eldit.size.y, eldit.size.y + eldit.size.height - 1) eClearLine() term.setTextColor(colors.yellow) term.write("Save as: ") term.setTextColor(colors.white) cx, cy = term.getCursorPos() term.setCursorPos(cx, cy) newName = read() if tab[newName:sub(-1,-1)] then render() term.setCursorPos(eldit.size.y, eldit.size.y + eldit.size.height - 1) term.write("Path cannot have trailing space!") sleep(0.5) simType(newName) elseif fs.exists(newName) and newName ~= "" then render() term.setCursorPos(eldit.size.y, eldit.size.y + eldit.size.height - 1) eClearLine() if fs.isDir(newName) then term.write("Cannot overwrite a directory!") sleep(0.5) simType(newName) else term.write("Overwrite? (Y/N)") if choice("yn", nil, false) == "n" then barmsg = "Cancelled save." barlife = defaultBarLife return end end end until ( (not fs.isDir(newName) or newName == "") and (#newName:gsub(" ", "") > 0 or newName == "") and not tab[newName:sub(-1,-1)] ) if newName == "" then barmsg = "Cancelled save." barlife = defaultBarLife return else eldit.filename = newName end end writeFile(eldit.filename, compiled) barmsg = "Saved to '" .. eldit.filename .. "'." barlife = defaultBarLife end local evt local tID = os.startTimer(0.5) -- timer for cursor blinking local bartID = os.startTimer(0.1) -- timer for bar message to go away local undotID -- timer for when the buffer is put in the undo buffer local doRender = true -- if true, renders -- converts numerical key events to usable numbers local numToKey = { -- number bar [2] = 1, [3] = 2, [4] = 3, [5] = 4, [6] = 5, [7] = 6, [8] = 7, [9] = 8, [10] = 9, [11] = 0, -- number pad [79] = 1, [80] = 2, [81] = 3, [75] = 4, [76] = 5, [77] = 6, [71] = 7, [72] = 8, [73] = 9, [82] = 0, } local startedSelecting = false -- here we go my man scrollToCursor() while true do evt = {os.pullEvent()} repeat if evt[1] == "timer" then if evt[2] == tID then if isCursorBlink then tID = os.startTimer(0.4) else tID = os.startTimer(0.3) end isCursorBlink = not isCursorBlink doRender = true elseif evt[2] == bartID then bartID = os.startTimer(0.1) barlife = math.max(0, barlife - 1) elseif evt[2] == undotID then if not compareBuffers(eldit.buffer, eldit.undoBuffer[#eldit.undoBuffer].buffer or {}) then if #eldit.undoBuffer >= eldit.maxUndo then repeat table.remove(eldit.undoBuffer, 1) until #eldit.undoBuffer < eldit.maxUndo end if eldit.undoPos < #eldit.undoBuffer then repeat table.remove(eldit.undoBuffer, 0) until eldit.undoPos == #eldit.undoBuffer end eldit.undoPos = math.min(eldit.undoPos + 1, eldit.maxUndo) table.insert(eldit.undoBuffer, { buffer = deepCopy(eldit.buffer), cursors = deepCopy(eldit.cursors), selections = deepCopy(eldit.selections), }) end end elseif (evt[1] == "char" and not keysDown[keys.ctrl]) then placeText(evt[2]) if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end doRender = true elseif evt[1] == "paste" then if keysDown[keys.shift] then local cb = eldit.clipboards[eldit.selectedClipboard] local cbb = {} if cb then deleteSelections() sortCursors() for i = 1, math.max(#cb, #eldit.cursors) do cbb[i] = cb[(i % #cb) + 1] end for i = 1, #cbb do if eldit.cursors[i] then for y = 1, #cbb[i] do placeText(concatTable(cbb[i][y]), {eldit.cursors[i]}) if y < #cbb[i] then makeNewLine({eldit.cursors[i]}) end end else makeNewLine({eldit.cursors[#eldit.cursors]}) for y = 1, #cbb[i] do placeText(concatTable(cbb[i][y]), {eldit.cursors[#eldit.cursors]}) if y < #cbb[i] then makeNewLine({eldit.cursors[#eldit.cursors]}) end end end end barmsg = "Pasted from clipboard " .. eldit.selectedClipboard .. "." if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end else barmsg = "Clipboard " .. eldit.selectedClipboard .. " is empty." end barlife = defaultBarLife else placeText(evt[2]) if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end end doRender = true elseif evt[1] == "key" then keysDown[evt[2]] = true keysDown[keys.shift] = keysDown[keys.leftShift] or keysDown[keys.rightShift] keysDown[keys.alt] = keysDown[keys.leftAlt] or keysDown[keys.rightAlt] keysDown[keys.ctrl] = keysDown[keys.leftCtrl] or keysDown[keys.rightCtrl] -- KEYBOARD SHORTCUTS if keysDown[keys.ctrl] then if keysDown[keys.shift] then if evt[2] == keys.c or evt[2] == keys.x then doRender = true if #eldit.selections == 0 then barmsg = "No selections have been made." barlife = defaultBarLife else eldit.clipboards[eldit.selectedClipboard] = {} local cb = eldit.clipboards[eldit.selectedClipboard] sortSelections() local id, selY for y = 1, #eldit.buffer do for x = 1, #eldit.buffer[y] + 1 do id = checkIfSelected(x, y) if id then selY = y - eldit.selections[id][1].y + 1 cb[id] = cb[id] or {} cb[id][selY] = cb[id][selY] or {} table.insert(cb[id][selY], eldit.buffer[y][x]) end end end if evt[2] == keys.x then deleteSelections() barmsg = "Cut to clipboard " .. eldit.selectedClipboard .. "." barlife = defaultBarLife if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end else barmsg = "Copied to clipboard " .. eldit.selectedClipboard .. "." barlife = defaultBarLife end end elseif evt[2] == keys.z then if eldit.undoPos < #eldit.undoBuffer then eldit.undoPos = math.min(#eldit.undoBuffer, eldit.maxUndo, eldit.undoPos + 1) eldit.selections = deepCopy(eldit.undoBuffer[eldit.undoPos].selections) eldit.cursors = deepCopy(eldit.undoBuffer[eldit.undoPos].cursors) eldit.buffer = deepCopy(eldit.undoBuffer[eldit.undoPos].buffer) adjustCursor(0, 0, true) barmsg = "Redone. (" .. eldit.undoPos .. "/" .. #eldit.undoBuffer .. ")" barlife = defaultBarLife else barmsg = "Reached top of undo buffer. (" .. eldit.undoPos .. "/" .. #eldit.undoBuffer .. ")" barlife = defaultBarLife end doRender = true elseif evt[2] == keys.s then saveFile(eldit.filename) tID = os.startTimer(0.4) bartID = os.startTimer(0.1) doRender = true end -- In-editor pasting is done with the "paste" event! else if numToKey[evt[2]] then -- if that's a number then eldit.selectedClipboard = numToKey[evt[2]] barmsg = "Selected clipboard " .. eldit.selectedClipboard if eldit.clipboards[eldit.selectedClipboard] then barmsg = barmsg .. ": " .. table.concat(eldit.clipboards[eldit.selectedClipboard][1][1]) else barmsg = barmsg .. "." end barlife = defaultBarLife doRender = true elseif evt[2] == keys.rightBracket then indentLines(false) doRender, isCursorBlink = true, true elseif evt[2] == keys.leftBracket then indentLines(true) doRender, isCursorBlink = true, true elseif evt[2] == keys.backspace then if #eldit.selections > 0 then deleteSelections() else deleteText("word", "backward") end if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end doRender, isCursorBlink = true, false elseif evt[2] == keys.delete then if #eldit.selections > 0 then deleteSelections() else deleteText("word", "forward") end if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end doRender, isCursorBlink = true, false elseif evt[2] == keys.q then return "exit" elseif evt[2] == keys.s then saveFile(false) tID = os.startTimer(0.4) bartID = os.startTimer(0.1) doRender = true elseif evt[2] == keys.a then eldit.selections = {{ { x = 1, y = 1 },{ x = #eldit.buffer[#eldit.buffer], y = #eldit.buffer } }} doRender = true elseif evt[2] == keys.z then if eldit.undoPos > 1 then eldit.undoPos = math.max(1, eldit.undoPos - 1) eldit.selections = deepCopy(eldit.undoBuffer[eldit.undoPos].selections) eldit.cursors = deepCopy(eldit.undoBuffer[eldit.undoPos].cursors) eldit.buffer = deepCopy(eldit.undoBuffer[eldit.undoPos].buffer) adjustCursor(0, 0, true) barmsg = "Undone. (" .. eldit.undoPos .. "/" .. #eldit.undoBuffer .. ")" barlife = defaultBarLife else barmsg = "Reached back of undo buffer." barlife = defaultBarLife end doRender = true elseif evt[2] == keys.left then adjustCursor(-1, 0, true, "word", false, keysDown[keys.shift]) doRender, isCursorBlink = true, true eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors elseif evt[2] == keys.right then adjustCursor(1, 0, true, "word", false, keysDown[keys.shift]) doRender, isCursorBlink = true, true eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors elseif evt[2] == keys.up then adjustCursor(0, -1, false, "flip") doRender, isCursorBlink = true, true if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors elseif evt[2] == keys.down then adjustCursor(0, 1, false, "flip") doRender, isCursorBlink = true, true if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors end end else if evt[2] == keys.tab then if keysDown[keys.shift] then indentLines(true) elseif #eldit.selections > 0 then indentLines(false) else placeText("\9") end doRender = true if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end elseif evt[2] == keys.insert then isInsert = not isInsert doRender, isCursorBlink = true, true elseif evt[2] == keys.enter then deleteSelections() makeNewLine() doRender, isCursorBlink = true, true if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end elseif evt[2] == keys.home then eldit.cursors = {{ x = 1, y = eldit.cursors[1].y, lastX = 1 }} scrollToCursor() doRender, isCursorBlink = true, true elseif evt[2] == keys["end"] then eldit.cursors = {{ x = 1 + #eldit.buffer[eldit.cursors[1].y], y = eldit.cursors[1].y, lastX = 1 + #eldit.buffer[eldit.cursors[1].y] }} scrollToCursor() doRender, isCursorBlink = true, true elseif evt[2] == keys.pageUp then adjustScroll(0, -eldit.size.height) if isSelecting then os.queueEvent("mouse_drag", 1, (miceDown[1] or miceDown[2]).x, (miceDown[1] or miceDown[2]).y) end doRender = true elseif evt[2] == keys.pageDown then adjustScroll(0, eldit.size.height) if isSelecting then os.queueEvent("mouse_drag", 1, (miceDown[1] or miceDown[2]).x, (miceDown[1] or miceDown[2]).y) end doRender = true elseif evt[2] == keys.backspace then if #eldit.selections > 0 then deleteSelections() else deleteText("single", "backward") end doRender, isCursorBlink = true, false if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end elseif evt[2] == keys.delete then if #eldit.selections > 0 then deleteSelections() else deleteText("single", "forward") end doRender, isCursorBlink = true, false if eldit.allowUndo then undotID = os.startTimer(eldit.undoDelay) end elseif evt[2] == keys.left then adjustCursor(-1, 0, true, nil, false, keysDown[keys.shift]) eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors doRender, isCursorBlink = true, true elseif evt[2] == keys.right then adjustCursor(1, 0, true, nil, false, keysDown[keys.shift]) eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors doRender, isCursorBlink = true, true elseif evt[2] == keys.up then adjustCursor(0, -1, false, nil, false, keysDown[keys.shift]) eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors doRender, isCursorBlink = true, true elseif evt[2] == keys.down then adjustCursor(0, 1, false, nil, false, keysDown[keys.shift]) doRender, isCursorBlink = true, true eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors end end elseif evt[1] == "key_up" then keysDown[evt[2]] = nil keysDown[keys.shift] = keysDown[keys.leftShift] or keysDown[keys.rightShift] keysDown[keys.alt] = keysDown[keys.leftAlt] or keysDown[keys.rightAlt] keysDown[keys.ctrl] = keysDown[keys.leftCtrl] or keysDown[keys.rightCtrl] elseif evt[1] == "mouse_click" then local lineNoLen = getLineNoLen() startedSelecting = false miceDown[evt[2]] = {x = evt[3], y = evt[4]} if evt[4] == -1 + eldit.size.y + eldit.size.height then else if keysDown[keys.ctrl] and ( not checkIfSelected( math.min( evt[3] + eldit.scrollX - lineNoLen, #eldit.buffer[evt[4] + eldit.scrollY] + 1 ), evt[4] + eldit.scrollY ) ) then table.insert(eldit.cursors, { x = evt[3] + eldit.scrollX - lineNoLen, y = evt[4] + eldit.scrollY, lastX = evt[3] + eldit.scrollX - lineNoLen }) startedSelecting = true else eldit.cursors = {{ x = evt[3] + eldit.scrollX - lineNoLen, y = evt[4] + eldit.scrollY, lastX = evt[3] + eldit.scrollX - lineNoLen }} eldit.selections = {} end lastMouse = { x = evt[3], y = evt[4], scrollX = eldit.scrollX, scrollY = eldit.scrollY, lineNoLen = lineNoLen, ctrl = keysDown[keys.ctrl], curID = #eldit.cursors, } sortSelections() adjustCursor(0, 0, true, nil, nil, nil, startedSelecting) eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors end doRender = true elseif evt[1] == "mouse_drag" then if evt[4] == -1 + eldit.size.y + eldit.size.height then else local lineNoLen = getLineNoLen() local lastMX, lastMY if miceDown[evt[2]] then lastMX, lastMY = miceDown[evt[2]].x, miceDown[evt[2]].y else lastMX, lastMY = evt[3], evt[4] end miceDown[evt[2]] = {x = evt[3], y = evt[4]} if lastMouse.x and lastMouse.y and lastMouse.curID then local adjMY = lastMouse.y + lastMouse.scrollY local adjMX = math.min(lastMouse.x + lastMouse.scrollX, #(eldit.buffer[adjMY] or "") + 1) local adjEY = evt[4] + eldit.scrollY local adjEX = math.min(evt[3] + eldit.scrollX, #(eldit.buffer[adjEY] or "") + 1) local selID local cSelID = checkIfSelected(adjMX, adjMY) if (lastMouse.ctrl and not isSelecting) or #eldit.selections == 0 then selID = cSelID or (1 + #eldit.selections) else selID = #eldit.selections end if cSelID and not (eldit.selections[cSelID][1].x == adjMX and eldit.selections[cSelID][1].y == adjMY) then for id,cur in pairs(eldit.cursors) do if cur.x == eldit.selections[cSelID][1].x and cur.y == eldit.selections[cSelID][1].y then table.remove(eldit.cursors, id) break end end eldit.selections[cSelID][1] = { x = math.min(adjMX, #(eldit.buffer[adjMY] or "") + lineNoLen) - lineNoLen, y = adjMY } end eldit.selections[selID] = { { x = math.min(adjMX, #(eldit.buffer[adjMY] or "") + lineNoLen) - lineNoLen, y = adjMY }, { x = math.min(adjEX, #(eldit.buffer[adjEY] or "") + lineNoLen) - lineNoLen, y = adjEY } } sortSelections() eldit.cursors[lastMouse.curID] = { x = eldit.selections[selID][2].x, y = eldit.selections[selID][2].y, lastX = eldit.selections[selID][1].x } isSelecting = true adjustCursor(0, 0) eldit.undoBuffer[eldit.undoPos].selections = eldit.selections eldit.undoBuffer[eldit.undoPos].cursors = eldit.cursors end doRender = true end elseif evt[1] == "mouse_up" then miceDown[evt[2]] = nil isSelecting = false sortSelections() elseif evt[1] == "mouse_scroll" then if keysDown[keys.alt] then adjustScroll(((keysDown[keys.ctrl] and not (isSelecting or startedSelecting)) and eldit.size.width or 1) * evt[2], 0) else adjustScroll(0, ((keysDown[keys.ctrl] and not (isSelecting or startedSelecting)) and eldit.size.height or 1) * evt[2]) end if isSelecting then os.queueEvent("mouse_drag", 1, evt[3], evt[4]) end doRender = true end if doRender then if not (evt[1] == "mouse_scroll" and isSelecting) then render() doRender = false end end until true end end local contents = eldit.filename and readFile(eldit.filename) or nil local result = {prompt(contents)} if result[1] == "exit" then term.setBackgroundColor(colors.black) term.scroll(1) term.setCursorPos(1, scr_y) end