sc3-rom-dump/rom/programs/edit.lua

878 lines
25 KiB
Lua

-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
--
-- SPDX-License-Identifier: LicenseRef-CCPL
-- Get file to edit
local tArgs = { ... }
if #tArgs == 0 then
local programName = arg[0] or fs.getName(shell.getRunningProgram())
print("Usage: " .. programName .. " <path>")
return
end
-- Error checking
local sPath = shell.resolve(tArgs[1])
local bReadOnly = fs.isReadOnly(sPath)
if fs.exists(sPath) and fs.isDir(sPath) then
print("Cannot edit a directory.")
return
end
-- Create .lua files by default
if not fs.exists(sPath) and not string.find(sPath, "%.") then
local sExtension = settings.get("edit.default_extension")
if sExtension ~= "" and type(sExtension) == "string" then
sPath = sPath .. "." .. sExtension
end
end
local x, y = 1, 1
local w, h = term.getSize()
local scrollX, scrollY = 0, 0
local tLines = {}
local bRunning = true
-- Colours
local highlightColour, keywordColour, commentColour, textColour, bgColour, stringColour, errorColour
if term.isColour() then
bgColour = colours.black
textColour = colours.white
highlightColour = colours.yellow
keywordColour = colours.yellow
commentColour = colours.green
stringColour = colours.red
errorColour = colours.red
else
bgColour = colours.black
textColour = colours.white
highlightColour = colours.white
keywordColour = colours.white
commentColour = colours.white
stringColour = colours.white
errorColour = colours.white
end
local runHandler = [[multishell.setTitle(multishell.getCurrent(), %q)
local current = term.current()
local contents, name = %q, %q
local fn, err = load(contents, name, nil, _ENV)
if fn then
local exception = require "cc.internal.exception"
local ok, err, co = exception.try(fn, ...)
term.redirect(current)
term.setTextColor(term.isColour() and colours.yellow or colours.white)
term.setBackgroundColor(colours.black)
term.setCursorBlink(false)
if not ok then
printError(err)
exception.report(err, co, { [name] = contents })
end
else
local parser = require "cc.internal.syntax"
if parser.parse_program(contents) then printError(err) end
end
local message = "Press any key to continue."
if ok then message = "Program finished. " .. message end
local _, y = term.getCursorPos()
local w, h = term.getSize()
local wrapped = require("cc.strings").wrap(message, w)
local start_y = h - #wrapped + 1
if y >= start_y then term.scroll(y - start_y + 1) end
for i = 1, #wrapped do
term.setCursorPos(1, start_y + i - 1)
term.write(wrapped[i])
end
os.pullEvent('key')
]]
-- Menus
local bMenu = false
local nMenuItem = 1
local tMenuItems = {}
if not bReadOnly then
table.insert(tMenuItems, "Save")
end
if shell.openTab then
table.insert(tMenuItems, "Run")
end
if peripheral.find("printer") then
table.insert(tMenuItems, "Print")
end
table.insert(tMenuItems, "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
if term.isColour() then
message = "Press Ctrl or click here to access menu"
else
message = "Press Ctrl to access menu"
end
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
local file = io.open(_sPath, "r")
local sLine = file:read()
while sLine do
table.insert(tLines, sLine)
sLine = file:read()
end
file:close()
end
if #tLines == 0 then
table.insert(tLines, "")
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 tKeywords = {
["and"] = true,
["break"] = true,
["do"] = true,
["else"] = true,
["elseif"] = true,
["end"] = true,
["false"] = true,
["for"] = true,
["function"] = true,
["if"] = true,
["in"] = true,
["local"] = true,
["nil"] = true,
["not"] = true,
["or"] = true,
["repeat"] = true,
["return"] = true,
["then"] = true,
["true"] = true,
["until"] = true,
["while"] = true,
}
local function tryWrite(sLine, regex, colour)
local match = string.match(sLine, regex)
if match then
if type(colour) == "number" then
term.setTextColour(colour)
else
term.setTextColour(colour(match))
end
term.write(match)
term.setTextColour(textColour)
return string.sub(sLine, #match + 1)
end
return nil
end
local function writeHighlighted(sLine)
while #sLine > 0 do
sLine =
tryWrite(sLine, "^%-%-%[%[.-%]%]", commentColour) or
tryWrite(sLine, "^%-%-.*", commentColour) or
tryWrite(sLine, "^\"\"", stringColour) or
tryWrite(sLine, "^\".-[^\\]\"", stringColour) or
tryWrite(sLine, "^\'\'", stringColour) or
tryWrite(sLine, "^\'.-[^\\]\'", stringColour) or
tryWrite(sLine, "^%[%[.-%]%]", stringColour) or
tryWrite(sLine, "^[%w_]+", function(match)
if tKeywords[match] then
return keywordColour
end
return textColour
end) or
tryWrite(sLine, "^[^%w_]", textColour)
end
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 bMenu and 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(colours.white)
term.setBackgroundColor(colours.grey)
term.write(sCompletion)
term.setTextColor(textColour)
term.setBackgroundColor(bgColour)
end
end
local function redrawText()
local cursorX, cursorY = x, y
for y = 1, h - 1 do
term.setCursorPos(1 - scrollX, y)
term.clearLine()
local sLine = tLines[y + scrollY]
if sLine ~= nil then
writeHighlighted(sLine)
if cursorY == y and cursorX == #sLine + 1 then
writeCompletion()
end
end
end
term.setCursorPos(x - scrollX, y - scrollY)
end
local function redrawLine(_nY)
local sLine = tLines[_nY]
if sLine then
term.setCursorPos(1 - scrollX, _nY - scrollY)
term.clearLine()
writeHighlighted(sLine)
if _nY == y and x == #sLine + 1 then
writeCompletion()
end
term.setCursorPos(x - scrollX, _nY - scrollY)
end
end
local function redrawMenu()
-- Clear line
term.setCursorPos(1, h)
term.clearLine()
-- Draw line numbers
term.setCursorPos(w - #("Ln " .. y) + 1, h)
term.setTextColour(highlightColour)
term.write("Ln ")
term.setTextColour(textColour)
term.write(y)
term.setCursorPos(1, h)
if bMenu then
-- Draw menu
term.setTextColour(textColour)
for nItem, sItem in pairs(tMenuItems) do
if nItem == nMenuItem then
term.setTextColour(highlightColour)
term.write("[")
term.setTextColour(textColour)
term.write(sItem)
term.setTextColour(highlightColour)
term.write("]")
term.setTextColour(textColour)
else
term.write(" " .. sItem .. " ")
end
end
else
-- Draw status
term.setTextColour(status_ok and highlightColour or errorColour)
term.write(status_text)
term.setTextColour(textColour)
end
-- Reset cursor
term.setCursorPos(x - scrollX, y - scrollY)
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.write(sLine .. "\n")
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
bMenu = false
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
bMenu = true
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 sTitle = fs.getName(sPath)
if sTitle:sub(-4) == ".lua" then
sTitle = sTitle:sub(1, -5)
end
local sTempPath = bReadOnly and ".temp." .. sTitle or fs.combine(fs.getDir(sPath), ".temp." .. sTitle)
if fs.exists(sTempPath) then
set_status("Error saving to " .. sTempPath, false)
return
end
local ok = save(sTempPath, function(file)
file.write(runHandler:format(sTitle, table.concat(tLines, "\n"), "@/" .. sPath))
end)
if ok then
local nTask = shell.openTab("/" .. sTempPath)
if nTask then
shell.switchTab(nTask)
else
set_status("Error starting Task", false)
end
fs.delete(sTempPath)
else
set_status("Error saving to " .. sTempPath, false)
end
redrawMenu()
end,
}
local function doMenuItem(_n)
tMenuFuncs[tMenuItems[_n]]()
if bMenu then
bMenu = false
term.setCursorBlink(true)
end
redrawMenu()
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
redrawLine(oldY)
redrawLine(y)
else
redrawLine(y)
end
term.setCursorPos(screenX, screenY)
redrawMenu()
end
-- Actual program functionality begins
load(sPath)
term.setBackgroundColour(bgColour)
term.clear()
term.setCursorPos(x, y)
term.setCursorBlink(true)
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
-- Handle input
while bRunning do
local sEvent, param, param2, param3 = os.pullEvent()
if sEvent == "key" then
if param == keys.up then
-- Up
if not bMenu then
if nCompletion then
-- Cycle completions
nCompletion = nCompletion - 1
if nCompletion < 1 then
nCompletion = #tCompletions
end
redrawLine(y)
elseif y > 1 then
-- Move cursor up
setCursor(
math.min(x, #tLines[y - 1] + 1),
y - 1
)
end
end
elseif param == keys.down then
-- Down
if not bMenu then
-- Move cursor down
if nCompletion then
-- Cycle completions
nCompletion = nCompletion + 1
if nCompletion > #tCompletions then
nCompletion = 1
end
redrawLine(y)
elseif y < #tLines then
-- Move cursor down
setCursor(
math.min(x, #tLines[y + 1] + 1),
y + 1
)
end
end
elseif param == keys.tab then
-- Tab
if not bMenu 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) .. " " .. string.sub(sLine, x)
setCursor(x + 4, y)
end
end
elseif param == keys.pageUp then
-- Page Up
if not bMenu 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
)
end
elseif param == keys.pageDown then
-- Page Down
if not bMenu 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)
end
elseif param == keys.home then
-- Home
if not bMenu then
-- Move cursor to the beginning
if x > 1 then
setCursor(1, y)
end
end
elseif param == keys["end"] then
-- End
if not bMenu then
-- Move cursor to the end
local nLimit = #tLines[y] + 1
if x < nLimit then
setCursor(nLimit, y)
end
end
elseif param == keys.left then
-- Left
if not bMenu 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
else
-- Move menu left
nMenuItem = nMenuItem - 1
if nMenuItem < 1 then
nMenuItem = #tMenuItems
end
redrawMenu()
end
elseif param == keys.right then
-- Right
if not bMenu 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
else
-- Move menu right
nMenuItem = nMenuItem + 1
if nMenuItem > #tMenuItems then
nMenuItem = 1
end
redrawMenu()
end
elseif param == keys.delete then
-- Delete
if not bMenu 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()
redrawLine(y)
elseif y < #tLines then
tLines[y] = tLines[y] .. tLines[y + 1]
table.remove(tLines, y + 1)
recomplete()
redrawText()
end
end
elseif param == keys.backspace then
-- Backspace
if not bMenu 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)
setCursor(sPrevLen + 1, y - 1)
redrawText()
end
end
elseif param == keys.enter or param == keys.numPadEnter then
-- Enter/Numpad Enter
if not bMenu 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))
setCursor(spaces + 1, y + 1)
redrawText()
elseif bMenu then
-- Menu selection
doMenuItem(nMenuItem)
end
elseif param == keys.leftCtrl or param == keys.rightCtrl then
-- Menu toggle
bMenu = not bMenu
if bMenu then
term.setCursorBlink(false)
else
term.setCursorBlink(true)
end
redrawMenu()
elseif param == keys.rightAlt then
if bMenu then
bMenu = false
term.setCursorBlink(true)
redrawMenu()
end
end
elseif sEvent == "char" then
if not bMenu and not bReadOnly then
-- Input text
local sLine = tLines[y]
tLines[y] = string.sub(sLine, 1, x - 1) .. param .. string.sub(sLine, x)
setCursor(x + 1, y)
elseif bMenu then
-- Select menu items
for n, sMenuItem in ipairs(tMenuItems) do
if string.lower(string.sub(sMenuItem, 1, 1)) == string.lower(param) then
doMenuItem(n)
break
end
end
end
elseif sEvent == "paste" then
if not bReadOnly then
-- Close menu if open
if bMenu then
bMenu = false
term.setCursorBlink(true)
redrawMenu()
end
-- Input text
local sLine = tLines[y]
tLines[y] = string.sub(sLine, 1, x - 1) .. param .. string.sub(sLine, x)
setCursor(x + #param , y)
end
elseif sEvent == "mouse_click" then
local cx, cy = param2, param3
if not bMenu then
if param == 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
bMenu = true
redrawMenu()
end
end
else
if cy == h then
local nMenuPosEnd = 1
local nMenuPosStart = 1
for n, sMenuItem in ipairs(tMenuItems) do
nMenuPosEnd = nMenuPosEnd + #sMenuItem + 1
if cx > nMenuPosStart and cx < nMenuPosEnd then
doMenuItem(n)
end
nMenuPosEnd = nMenuPosEnd + 1
nMenuPosStart = nMenuPosEnd
end
else
bMenu = false
term.setCursorBlink(true)
redrawMenu()
end
end
elseif sEvent == "mouse_scroll" then
if not bMenu then
if param == -1 then
-- Scroll up
if scrollY > 0 then
-- Move cursor up
scrollY = scrollY - 1
redrawText()
end
elseif param == 1 then
-- Scroll down
local nMaxScroll = #tLines - (h - 1)
if scrollY < nMaxScroll then
-- Move cursor down
scrollY = scrollY + 1
redrawText()
end
end
end
elseif sEvent == "term_resize" then
w, h = term.getSize()
setCursor(x, y)
redrawMenu()
redrawText()
end
end
-- Cleanup
term.clear()
term.setCursorBlink(false)
term.setCursorPos(1, 1)