-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe -- -- SPDX-License-Identifier: LicenseRef-CCPL -- Get file to edit local tArgs = { ... } if #tArgs == 0 then local programName = "edit" print("Usage: " .. programName .. " ") return end -- Error checking local sPath = tArgs[1] local bReadOnly = false if fs.exists(sPath) and fs.isDir(sPath) then print("Cannot edit a directory.") return end local x, y = 1, 1 local w, h = term.getSize() local scrollX, scrollY = 0, 0 local tLines, tLineLexStates = {}, {} local bRunning = true -- colors local isColor = true local highlightColor, keywordColor, textColor, bgColor, errorColor if isColor then bgColor = colors.black textColor = colors.white highlightColor = colors.yellow keywordColor = colors.yellow errorColor = colors.red else bgColor = colors.black textColor = colors.white highlightColor = colors.white keywordColor = colors.white errorColor = colors.white end -- Menus local menu = require "cc.internal.menu" local current_menu local menu_items = {} if not bReadOnly then table.insert(menu_items, "Save") end --[[if shell.openTab then table.insert(menu_items, "Run") end if peripheral.find("printer") then table.insert(menu_items, "Print") end]] table.insert(menu_items, "Run") table.insert(menu_items, "Exit") local status_ok, status_text local function set_status(text, ok) status_ok = ok ~= false status_text = text end if bReadOnly then set_status("File is read only", false) elseif fs.getFreeSpace(sPath) < 1024 then set_status("Disk is low on space", false) else local message message = "Press Ctrl to access menu" if #message > w - 5 then message = "Press Ctrl for menu" end set_status(message) end local function load(_sPath) tLines = {} if fs.exists(_sPath) then bReadOnly = fs.isReadOnly(_sPath) local file = fs.open(_sPath, "r") local sLine = file:readLine() while sLine do table.insert(tLines, sLine) table.insert(tLineLexStates, false) sLine = file:readLine() end file:close() end if #tLines == 0 then table.insert(tLines, "") table.insert(tLineLexStates, false) end end local function save(_sPath, fWrite) -- Create intervening folder --[[local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len()) if not fs.exists(sDir) then fs.makeDir(sDir) end]] -- Save local file, fileerr local function innerSave() file, fileerr = fs.open(_sPath, "w") if file then if file then fWrite(file) end else error("Failed to open " .. _sPath) end end local ok, err = pcall(innerSave) if file then file:close() end return ok, err, fileerr end local tokens = require "cc.internal.syntax.parser".tokens local lex_one = require "cc.internal.syntax.lexer".lex_one local token_colors = { [tokens.STRING] = colors.red, [tokens.COMMENT] = colors.green, [tokens.NUMBER] = colors.magenta, -- Keywords [tokens.AND] = keywordColor, [tokens.BREAK] = keywordColor, [tokens.DO] = keywordColor, [tokens.ELSE] = keywordColor, [tokens.ELSEIF] = keywordColor, [tokens.END] = keywordColor, [tokens.FALSE] = keywordColor, [tokens.FOR] = keywordColor, [tokens.FUNCTION] = keywordColor, [tokens.GOTO] = keywordColor, [tokens.IF] = keywordColor, [tokens.IN] = keywordColor, [tokens.LOCAL] = keywordColor, [tokens.NIL] = keywordColor, [tokens.NOT] = keywordColor, [tokens.OR] = keywordColor, [tokens.REPEAT] = keywordColor, [tokens.RETURN] = keywordColor, [tokens.THEN] = keywordColor, [tokens.TRUE] = keywordColor, [tokens.UNTIL] = keywordColor, [tokens.WHILE] = keywordColor, } -- Fill in the remaining tokens. for _, token in pairs(tokens) do if not token_colors[token] then token_colors[token] = textColor end end local lex_context = { line = function() end, report = function() end } local tCompletions local nCompletion local tCompleteEnv = _ENV local function complete(sLine) --[[if settings.get("edit.autocomplete") then local nStartPos = string.find(sLine, "[a-zA-Z0-9_%.:]+$") if nStartPos then sLine = string.sub(sLine, nStartPos) end if #sLine > 0 then return textutils.complete(sLine, tCompleteEnv) end end return nil]] end local function recomplete() local sLine = tLines[y] if not bReadOnly and x == #sLine + 1 then tCompletions = complete(sLine) if tCompletions and #tCompletions > 0 then nCompletion = 1 else nCompletion = nil end else tCompletions = nil nCompletion = nil end end local function writeCompletion(sLine) if nCompletion then local sCompletion = tCompletions[nCompletion] term.setTextColor(colors.white) term.setBackgroundColor(colors.grey) term.write(sCompletion) term.setTextColor(textColor) term.setBackgroundColor(bgColor) end end --- Check if two values are equal. If both values are lists, then the contents will be -- checked for equality, to a depth of 1. -- -- @param x The first value. -- @param x The second value. -- @treturn boolean Whether the values are equal. local function shallowEqual(x, y) if x == y then return true end if type(x) ~= "table" or type(y) ~= "table" then return false end if #x ~= #y then return false end for i = 1, #x do if x[i] ~= y[i] then return false end end return true end local function redrawLines(line, endLine) term.setCursorBlink(false) if not endLine then endLine = line end local color = term.getTextColor() -- Highlight all lines between line and endLine, highlighting further lines if their -- lexer state has changed and aborting at the end of the screen. local changed = false while (changed or line <= endLine) and line - scrollY < h do term.setCursorPos(1 - scrollX, line - scrollY) term.clearLine() local contents = tLines[line] if not contents then break end -- Lex our first token, either taking our continuation state (if present) or -- the default lexer. local pos, token, _, finish, continuation = 1 local lex_state = tLineLexStates[line] if lex_state then token, finish, _, continuation = lex_state[1](lex_context, contents, table.unpack(lex_state, 2)) else token, _, finish, _, continuation = lex_one(lex_context, contents, 1) end while token do -- start at scrollX if finish >= scrollX+1 then -- Print out that token local new_color = token_colors[token] if new_color ~= color then term.setTextColor(new_color) color = new_color end -- limit printed line to screen if pos < scrollX+1 then pos = scrollX+1 end local cap = finish < scrollX+w and finish or scrollX+w term.write(contents:sub(pos, cap)) end pos = finish + 1 -- end if we're past the screen width if pos > scrollX+w then break end -- If we have a continuation, then we've reached the end of the line. Abort. if continuation then break end -- Otherwise lex another token and continue. token, _, finish, _, continuation = lex_one(lex_context, contents, pos) end -- Print the rest of the line. We don't strictly speaking need this, as it will -- only ever contain whitespace. term.write(contents:sub(pos)) if line == y and x == #contents + 1 then writeCompletion() color = term.getTextColor() end line = line + 1 -- Update the lext state of the next line. If that has changed, then -- re-highlight it too. We store the continuation as nil rather than -- false, to ensure we use the array part of the table. if continuation == nil then continuation = false end if tLineLexStates[line] ~= nil and not shallowEqual(tLineLexStates[line], continuation) then tLineLexStates[line] = continuation or false changed = true else changed = false end end term.setTextColor(colors.white) term.setCursorPos(x - scrollX, y - scrollY) end local function redrawText() redrawLines(scrollY + 1, scrollY + h - 1) end local function redrawMenu() term.setCursorBlink(false) -- Clear line term.setCursorPos(1, h) term.clearLine() term.setCursorPos(1, h) if current_menu then -- Draw menu menu.draw(current_menu) else -- Draw status term.setTextColor(status_ok and highlightColor or errorColor) term.write(status_text) term.setTextColor(textColor) -- Draw line numbers term.setCursorPos(w - #("Ln " .. y) + 1, h) term.setTextColor(highlightColor) term.write("Ln ") term.setTextColor(textColor) term.write(y) end -- Reset cursor term.setCursorPos(x - scrollX, y - scrollY) term.setCursorBlink(not current_menu) end local tMenuFuncs = { Save = function() if bReadOnly then set_status("Access denied", false) else local ok, _, fileerr = save(sPath, function(file) for _, sLine in ipairs(tLines) do file:writeLine(sLine) end end) if ok then set_status("Saved to " .. sPath) else if fileerr then set_status("Error saving: " .. fileerr, false) else set_status("Error saving to " .. sPath, false) end end end redrawMenu() end, --[[Print = function() local printer = peripheral.find("printer") if not printer then set_status("No printer attached", false) return end local nPage = 0 local sName = fs.getName(sPath) if printer.getInkLevel() < 1 then set_status("Printer out of ink", false) return elseif printer.getPaperLevel() < 1 then set_status("Printer out of paper", false) return end local screenTerminal = term.current() local printerTerminal = { getCursorPos = printer.getCursorPos, setCursorPos = printer.setCursorPos, getSize = printer.getPageSize, write = printer.write, } printerTerminal.scroll = function() if nPage == 1 then printer.setPageTitle(sName .. " (page " .. nPage .. ")") end while not printer.newPage() do if printer.getInkLevel() < 1 then set_status("Printer out of ink, please refill", false) elseif printer.getPaperLevel() < 1 then set_status("Printer out of paper, please refill", false) else set_status("Printer output tray full, please empty", false) end term.redirect(screenTerminal) redrawMenu() term.redirect(printerTerminal) sleep(0.5) end nPage = nPage + 1 if nPage == 1 then printer.setPageTitle(sName) else printer.setPageTitle(sName .. " (page " .. nPage .. ")") end end local old_menu = current_menu current_menu = nil term.redirect(printerTerminal) local ok, error = pcall(function() term.scroll() for _, sLine in ipairs(tLines) do print(sLine) end end) term.redirect(screenTerminal) if not ok then print(error) end while not printer.endPage() do set_status("Printer output tray full, please empty") redrawMenu() sleep(0.5) end current_menu = old_menu if nPage > 1 then set_status("Printed " .. nPage .. " Pages") else set_status("Printed 1 Page") end redrawMenu() end,]] Exit = function() bRunning = false end, Run = function() local sTempPath = "~temp.lua" --[[if fs.exists(sTempPath) then set_status("Error saving to " .. sTempPath, false) return end]] local ok = save(sTempPath, function(file) for _, sLine in ipairs(tLines) do file:writeLine(sLine) end end) if ok then collectgarbage() local f, err = loadfile(sTempPath) if not f then set_status(tostring(err)) else term.clear() keys.flush() print("Free memory: "..sys.freeMemory()) set_status("Press Ctrl to access menu") local status, err = pcall(f) draw.enableBuffer(false) if not status then print(tostring(err)) end term.setCursorPos(1, h) term.write("Press any key...") keys.flush() keys.wait(false, true) end --fs.delete(sTempPath) else set_status("Error saving to " .. sTempPath, false) end collectgarbage() term.clear() redrawMenu() redrawText() end, } local function setCursor(newX, newY) local _, oldY = x, y x, y = newX, newY local screenX = x - scrollX local screenY = y - scrollY local bRedraw = false if screenX < 1 then scrollX = x - 1 screenX = 1 bRedraw = true elseif screenX > w then scrollX = x - w screenX = w bRedraw = true end if screenY < 1 then scrollY = y - 1 screenY = 1 bRedraw = true elseif screenY > h - 1 then scrollY = y - (h - 1) screenY = h - 1 bRedraw = true end recomplete() if bRedraw then redrawText() elseif y ~= oldY then redrawLines(math.min(y, oldY), math.max(y, oldY)) else redrawLines(y) end term.setCursorBlink(not current_menu); redrawMenu() end -- Actual program functionality begins load(sPath) term.setBackgroundColor(bgColor) term.clear() term.setCursorPos(x, y) recomplete() redrawText() redrawMenu() local function acceptCompletion() if nCompletion then -- Append the completion local sCompletion = tCompletions[nCompletion] tLines[y] = tLines[y] .. sCompletion setCursor(x + #sCompletion, y) end end local function handleMenuEvent(key) assert(current_menu) local result = menu.handle_event(current_menu, key) if result == false then current_menu = nil redrawMenu() elseif result ~= nil then tMenuFuncs[result]() current_menu = nil redrawMenu() end end -- Handle input while bRunning do term.setCursorBlink(not current_menu) state, modifiers, key = keys.wait() if state == keys.states.pressed or state == keys.states.longHold then if current_menu then handleMenuEvent(key) else if key == keys.up then if nCompletion then -- Cycle completions nCompletion = nCompletion - 1 if nCompletion < 1 then nCompletion = #tCompletions end redrawLines(y) elseif y > 1 then -- Move cursor up setCursor( math.min(x, #tLines[y - 1] + 1), y - 1 ) end elseif key == keys.down then if nCompletion then -- Cycle completions nCompletion = nCompletion + 1 if nCompletion > #tCompletions then nCompletion = 1 end redrawLines(y) elseif y < #tLines then -- Move cursor down setCursor( math.min(x, #tLines[y + 1] + 1), y + 1 ) end elseif key == keys.tab and not bReadOnly then if nCompletion and x == #tLines[y] + 1 then -- Accept autocomplete acceptCompletion() else -- Indent line local sLine = tLines[y] tLines[y] = string.sub(sLine, 1, x - 1) .. "\t" .. string.sub(sLine, x) setCursor(x + 1, y) end elseif key == keys.pageUp then -- Move up a page local newY if y - (h - 1) >= 1 then newY = y - (h - 1) else newY = 1 end setCursor( math.min(x, #tLines[newY] + 1), newY ) elseif key == keys.pageDown then -- Move down a page local newY if y + (h - 1) <= #tLines then newY = y + (h - 1) else newY = #tLines end local newX = math.min(x, #tLines[newY] + 1) setCursor(newX, newY) elseif key == keys.home then -- Move cursor to the beginning if x > 1 then setCursor(1, y) end elseif key == keys["end"] then -- Move cursor to the end local nLimit = #tLines[y] + 1 if x < nLimit then setCursor(nLimit, y) end elseif key == keys.left then if x > 1 then -- Move cursor left setCursor(x - 1, y) elseif x == 1 and y > 1 then setCursor(#tLines[y - 1] + 1, y - 1) end elseif key == keys.right then local nLimit = #tLines[y] + 1 if x < nLimit then -- Move cursor right setCursor(x + 1, y) elseif nCompletion and x == #tLines[y] + 1 then -- Accept autocomplete acceptCompletion() elseif x == nLimit and y < #tLines then -- Go to next line setCursor(1, y + 1) end elseif key == keys.delete and not bReadOnly then local nLimit = #tLines[y] + 1 if x < nLimit then local sLine = tLines[y] tLines[y] = string.sub(sLine, 1, x - 1) .. string.sub(sLine, x + 1) recomplete() redrawLines(y) elseif y < #tLines then tLines[y] = tLines[y] .. tLines[y + 1] table.remove(tLines, y + 1) table.remove(tLineLexStates, y + 1) recomplete() redrawText() end elseif key == keys.backspace and not bReadOnly then if x > 1 then -- Remove character local sLine = tLines[y] if x > 4 and string.sub(sLine, x - 4, x - 1) == " " and not string.sub(sLine, 1, x - 1):find("%S") then tLines[y] = string.sub(sLine, 1, x - 5) .. string.sub(sLine, x) setCursor(x - 4, y) else tLines[y] = string.sub(sLine, 1, x - 2) .. string.sub(sLine, x) setCursor(x - 1, y) end elseif y > 1 then -- Remove newline local sPrevLen = #tLines[y - 1] tLines[y - 1] = tLines[y - 1] .. tLines[y] table.remove(tLines, y) table.remove(tLineLexStates, y) setCursor(sPrevLen + 1, y - 1) redrawText() end elseif (key == keys.enter) and not bReadOnly then -- Newline local sLine = tLines[y] local _, spaces = string.find(sLine, "^[ ]+") if not spaces then spaces = 0 end tLines[y] = string.sub(sLine, 1, x - 1) table.insert(tLines, y + 1, string.rep(' ', spaces) .. string.sub(sLine, x)) table.insert(tLineLexStates, y + 1, false) setCursor(spaces + 1, y + 1) redrawText() elseif key == keys.control then current_menu = menu.create(menu_items) redrawMenu() else if keys.isPrintable(key) and not bReadOnly then -- Input text local sLine = tLines[y] tLines[y] = string.sub(sLine, 1, x - 1) .. key .. string.sub(sLine, x) setCursor(x + 1, y) end end end end --[[elseif event[1] == "paste" and not bReadOnly then -- Close menu if open if current_menu then current_menu = nil redrawMenu() end -- Input text local text = event[2] local sLine = tLines[y] tLines[y] = string.sub(sLine, 1, x - 1) .. text .. string.sub(sLine, x) setCursor(x + #text, y) elseif event[1] == "mouse_click" then local button, cx, cy = event[2], event[3], event[4] if current_menu then handleMenuEvent(event) else if button == 1 then -- Left click if cy < h then local newY = math.min(math.max(scrollY + cy, 1), #tLines) local newX = math.min(math.max(scrollX + cx, 1), #tLines[newY] + 1) setCursor(newX, newY) else current_menu = menu.create(menu_items) redrawMenu() end end end elseif event[1] == "mouse_scroll" then if not current_menu then local direction = event[2] if direction == -1 then -- Scroll up if scrollY > 0 then -- Move cursor up scrollY = scrollY - 1 redrawText() end elseif direction == 1 then -- Scroll down local nMaxScroll = #tLines - (h - 1) if scrollY < nMaxScroll then -- Move cursor down scrollY = scrollY + 1 redrawText() end end end elseif event[1] == "term_resize" then w, h = term.getSize() setCursor(x, y) redrawMenu() redrawText() end]] end -- Cleanup function unrequire(m) package.loaded[m] = nil _G[m] = nil end unrequire("cc.internal.syntax.errors") unrequire("cc.internal.syntax.lexer") unrequire("cc.internal.syntax.parser") unrequire("cc.internal.menu") unrequire("cc.pretty") unrequire("cc.expect") collectgarbage() term.clear() term.setCursorBlink(false) term.setCursorPos(1, 1)