Files
PicoCalc/Bin/PicoCalc SD/cc/edit.lua
2026-03-01 15:25:55 +08:00

815 lines
24 KiB
Lua

-- 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 .. " <path>")
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)