mirror of
https://github.com/kepler155c/opus
synced 2024-11-05 00:16:16 +00:00
1271 lines
28 KiB
Lua
1271 lines
28 KiB
Lua
-- Get file to edit
|
|
local tArgs = { ... }
|
|
if #tArgs == 0 then
|
|
error( "Usage: edit <path>" )
|
|
end
|
|
|
|
-- Error checking
|
|
local sPath = shell.resolve( tArgs[1] )
|
|
local bReadOnly = fs.isReadOnly( sPath )
|
|
if fs.exists( sPath ) and fs.isDir( sPath ) then
|
|
error( "Cannot edit a directory." )
|
|
end
|
|
|
|
if multishell then
|
|
multishell.setTitle(multishell.getCurrent(), sPath)
|
|
end
|
|
|
|
local x, y = 1, 1
|
|
local w, h = term.getSize()
|
|
local scrollX = 0
|
|
local scrollY = 0
|
|
local lastPos = { x = 1, y = 1 }
|
|
local tLines = { }
|
|
local bRunning = true
|
|
local sStatus = ""
|
|
local isError
|
|
local fileInfo
|
|
|
|
local dirty = { y = 1, ey = h }
|
|
local mark = { anchor, active, continue }
|
|
local keyboard
|
|
local searchPattern
|
|
local undo = { chain = { }, pointer = 0 }
|
|
local complete = { }
|
|
|
|
if not clipboard then
|
|
_G.clipboard = { internal, data }
|
|
clipboard.shim = true
|
|
|
|
function clipboard.setData(data)
|
|
clipboard.data = data
|
|
if data then
|
|
clipboard.useInternal(true)
|
|
end
|
|
end
|
|
|
|
function clipboard.getText()
|
|
if clipboard.data then
|
|
return Util.tostring(clipboard.data)
|
|
end
|
|
end
|
|
|
|
function clipboard.isInternal()
|
|
return clipboard.internal
|
|
end
|
|
|
|
function clipboard.useInternal(mode)
|
|
if mode ~= clipboard.mode then
|
|
clipboard.internal = mode
|
|
end
|
|
end
|
|
end
|
|
|
|
local color = {
|
|
textColor = '0',
|
|
keywordColor = '4',
|
|
commentColor = 'd',
|
|
stringColor = 'e',
|
|
bgColor = colors.black,
|
|
highlightColor = colors.orange,
|
|
cursorColor = colors.lime,
|
|
errorBackground = colors.red,
|
|
}
|
|
|
|
if not term.isColor() then
|
|
color = {
|
|
textColor = '0',
|
|
keywordColor = '8',
|
|
commentColor = '8',
|
|
stringColor = '8',
|
|
bgColor = colors.black,
|
|
highlightColor = colors.lightGray,
|
|
cursorColor = colors.white,
|
|
errorBackground = colors.gray,
|
|
}
|
|
end
|
|
|
|
local keyMapping = {
|
|
-- movement
|
|
up = 'up',
|
|
down = 'down',
|
|
left = 'left',
|
|
right = 'right',
|
|
pageUp = 'pageUp',
|
|
[ 'control-b' ] = 'pageUp',
|
|
pageDown = 'pageDown',
|
|
-- [ 'control-f' ] = 'pageDown',
|
|
home = 'home',
|
|
[ 'end' ] = 'toend',
|
|
[ 'control-home' ] = 'top',
|
|
[ 'control-end' ] = 'bottom',
|
|
[ 'control-right' ] = 'word',
|
|
[ 'control-left' ] = 'backword',
|
|
[ 'scrollUp' ] = 'scroll_up',
|
|
[ 'control-up' ] = 'scroll_up',
|
|
[ 'scrollDown' ] = 'scroll_down',
|
|
[ 'control-down' ] = 'scroll_down',
|
|
[ 'mouse_click' ] = 'goto',
|
|
[ 'control-l' ] = 'goto_line',
|
|
|
|
-- marking
|
|
[ 'shift-up' ] = 'mark_up',
|
|
[ 'shift-down' ] = 'mark_down',
|
|
[ 'shift-left' ] = 'mark_left',
|
|
[ 'shift-right' ] = 'mark_right',
|
|
[ 'mouse_drag' ] = 'mark_to',
|
|
[ 'shift-mouse_click' ] = 'mark_to',
|
|
[ 'control-a' ] = 'mark_all',
|
|
[ 'control-shift-right' ] = 'mark_word',
|
|
[ 'control-shift-left' ] = 'mark_backword',
|
|
[ 'shift-end' ] = 'mark_end',
|
|
[ 'shift-home' ] = 'mark_home',
|
|
|
|
-- editing
|
|
delete = 'delete',
|
|
backspace = 'backspace',
|
|
enter = 'enter',
|
|
char = 'char',
|
|
paste = 'paste',
|
|
tab = 'tab',
|
|
[ 'control-z' ] = 'undo',
|
|
[ 'control-space' ] = 'autocomplete',
|
|
|
|
-- copy/paste
|
|
[ 'control-x' ] = 'cut',
|
|
[ 'control-c' ] = 'copy',
|
|
[ 'control-v' ] = 'paste',
|
|
[ 'control-t' ] = 'toggle_clipboard',
|
|
|
|
-- file
|
|
[ 'control-s' ] = 'save',
|
|
[ 'control-q' ] = 'exit',
|
|
[ 'control-enter' ] = 'run',
|
|
|
|
-- search
|
|
[ 'control-f' ] = 'find_prompt',
|
|
[ 'control-slash' ] = 'find_prompt',
|
|
[ 'control-n' ] = 'find_next',
|
|
|
|
-- misc
|
|
[ 'control-g' ] = 'status',
|
|
[ 'control-r' ] = 'refresh',
|
|
[ 'leftCtrl' ] = 'menu',
|
|
}
|
|
|
|
local messages = {
|
|
menu = '^s: save, ^q: quit, ^enter: run',
|
|
wrapped = 'search hit BOTTOM, continuing at TOP',
|
|
}
|
|
if w < 32 then
|
|
messages = {
|
|
menu = '^s = save, ^q = quit',
|
|
wrapped = 'search wrapped',
|
|
}
|
|
end
|
|
|
|
local function getFileInfo(path)
|
|
local abspath = shell.resolve(path)
|
|
|
|
local fi = {
|
|
abspath = abspath,
|
|
path = path,
|
|
isNew = not fs.exists(abspath),
|
|
dirExists = fs.exists(fs.getDir(abspath)),
|
|
modified = false,
|
|
}
|
|
if fi.isDir then
|
|
fi.isReadOnly = true
|
|
else
|
|
fi.isReadOnly = fs.isReadOnly(fi.abspath)
|
|
end
|
|
|
|
return fi
|
|
end
|
|
|
|
local function setStatus(pattern, ...)
|
|
sStatus = string.format(pattern, ...)
|
|
end
|
|
|
|
local function setError(pattern, ...)
|
|
setStatus(pattern, ...)
|
|
isError = true
|
|
end
|
|
|
|
local function load(path)
|
|
tLines = {}
|
|
if fs.exists(path) then
|
|
local file = io.open(path, "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
|
|
|
|
fileInfo = getFileInfo(tArgs[1])
|
|
|
|
local name = fileInfo.path
|
|
if w < 32 then
|
|
name = fs.getName(fileInfo.path)
|
|
end
|
|
if fileInfo.isNew then
|
|
if not fileInfo.dirExists then
|
|
setStatus('"%s" [New DIRECTORY]', name)
|
|
else
|
|
setStatus('"%s" [New File]', name)
|
|
end
|
|
elseif fileInfo.isReadOnly then
|
|
setStatus('"%s" [readonly] %dL, %dC',
|
|
name, #tLines, fs.getSize(fileInfo.abspath))
|
|
else
|
|
setStatus('"%s" %dL, %dC',
|
|
name, #tLines, fs.getSize(fileInfo.abspath))
|
|
end
|
|
end
|
|
|
|
local function save( _sPath )
|
|
-- 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 = nil
|
|
local function innerSave()
|
|
file = fs.open( _sPath, "w" )
|
|
if file then
|
|
for n, sLine in ipairs( tLines ) do
|
|
file.write(sLine .. "\n")
|
|
end
|
|
else
|
|
error( "Failed to open ".._sPath )
|
|
end
|
|
end
|
|
|
|
local ok, err = pcall( innerSave )
|
|
if file then
|
|
file.close()
|
|
end
|
|
return ok, err
|
|
end
|
|
|
|
local function split(str, pattern)
|
|
pattern = pattern or "(.-)\n"
|
|
local t = {}
|
|
local function helper(line) table.insert(t, line) return "" end
|
|
helper((str:gsub(pattern, helper)))
|
|
return t
|
|
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 writeHighlighted(sLine, ny)
|
|
local buffer = {
|
|
fg = '',
|
|
text = '',
|
|
}
|
|
|
|
local function tryWrite(sLine, regex, fgcolor)
|
|
local match = sLine:match(regex)
|
|
if match then
|
|
local fg
|
|
if type(fgcolor) == "string" then
|
|
fg = fgcolor
|
|
else
|
|
fg = fgcolor(match)
|
|
end
|
|
buffer.text = buffer.text .. match
|
|
buffer.fg = buffer.fg .. string.rep(fg, #match)
|
|
return sLine:sub(#match + 1)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
while #sLine > 0 do
|
|
sLine =
|
|
tryWrite(sLine, "^%-%-%[%[.-%]%]", color.commentColor ) or
|
|
tryWrite(sLine, "^%-%-.*", color.commentColor ) or
|
|
tryWrite(sLine, "^\".-[^\\]\"", color.stringColor ) or
|
|
tryWrite(sLine, "^\'.-[^\\]\'", color.stringColor ) or
|
|
tryWrite(sLine, "^%[%[.-%]%]", color.stringColor ) or
|
|
tryWrite(sLine, "^[%w_]+", function(match)
|
|
if tKeywords[match] then
|
|
return color.keywordColor
|
|
end
|
|
return color.textColor
|
|
end) or
|
|
tryWrite(sLine, "^[^%w_]", color.textColor)
|
|
end
|
|
|
|
buffer.fg = buffer.fg .. '7'
|
|
buffer.text = buffer.text .. '.'
|
|
|
|
if mark.active and ny >= mark.y and ny <= mark.ey then
|
|
local sx = 1
|
|
if ny == mark.y then
|
|
sx = mark.x
|
|
end
|
|
local ex = #buffer.text
|
|
if ny == mark.ey then
|
|
ex = mark.ex
|
|
end
|
|
buffer.bg = string.rep('f', sx - 1) ..
|
|
string.rep('7', ex - sx) ..
|
|
string.rep('f', #buffer.text - ex + 1)
|
|
|
|
else
|
|
buffer.bg = string.rep('f', #buffer.text)
|
|
end
|
|
|
|
term.blit(buffer.text, buffer.fg, buffer.bg)
|
|
end
|
|
|
|
local function redraw()
|
|
if dirty.y > 0 then
|
|
term.setBackgroundColor(color.bgColor)
|
|
for dy = 1, h do
|
|
|
|
local sLine = tLines[dy + scrollY]
|
|
if sLine ~= nil then
|
|
if dy + scrollY >= dirty.y and dy + scrollY <= dirty.ey then
|
|
term.setCursorPos(1 - scrollX, dy)
|
|
term.clearLine()
|
|
writeHighlighted(sLine, dy + scrollY)
|
|
end
|
|
else
|
|
term.setCursorPos(1 - scrollX, dy)
|
|
term.clearLine()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Draw status
|
|
if #sStatus > 0 then
|
|
if isError then
|
|
term.setTextColor(colors.white)
|
|
term.setBackgroundColor(color.errorBackground)
|
|
else
|
|
term.setTextColor(color.highlightColor)
|
|
term.setBackgroundColor(colors.gray)
|
|
end
|
|
term.setCursorPos(1, h)
|
|
term.clearLine()
|
|
term.write(string.format(' %s ', sStatus))
|
|
end
|
|
|
|
if not (w < 32 and #sStatus > 0) then
|
|
local clipboardIndicator = 'S'
|
|
if clipboard.isInternal() then
|
|
clipboardIndicator = 'I'
|
|
end
|
|
|
|
local modifiedIndicator = ' '
|
|
if undo.chain[1] then
|
|
modifiedIndicator = '*'
|
|
end
|
|
|
|
local str = string.format(' %d:%d %s%s',
|
|
y, x, clipboardIndicator, modifiedIndicator)
|
|
term.setTextColor(color.highlightColor)
|
|
term.setBackgroundColor(colors.gray)
|
|
term.setCursorPos(w - #str + 1, h)
|
|
term.write(str)
|
|
end
|
|
|
|
term.setTextColor(color.cursorColor)
|
|
term.setCursorPos(x - scrollX, y - scrollY)
|
|
|
|
dirty.y, dirty.ey = 0, 0
|
|
if #sStatus > 0 then
|
|
sStatus = ''
|
|
dirty.y = scrollY + h
|
|
dirty.ey = dirty.y
|
|
end
|
|
isError = false
|
|
end
|
|
|
|
local function nextWord(line, cx)
|
|
local result = { line:find("(%w+)", cx) }
|
|
if #result > 1 and result[2] > cx then
|
|
return result[2] + 1
|
|
elseif #result > 0 and result[1] == cx then
|
|
result = { line:find("(%w+)", result[2] + 1) }
|
|
if #result > 0 then
|
|
return result[1]
|
|
end
|
|
end
|
|
end
|
|
|
|
local function hacky_read()
|
|
local _oldSetCursorPos = term.setCursorPos
|
|
local _oldGetCursorPos = term.getCursorPos
|
|
|
|
term.setCursorPos = function(x, y)
|
|
return _oldSetCursorPos(x, h)
|
|
end
|
|
term.getCursorPos = function()
|
|
local x, y = _oldGetCursorPos()
|
|
return x, 1
|
|
end
|
|
|
|
local s, m = pcall(function() return read() end)
|
|
term.setCursorPos = _oldSetCursorPos
|
|
term.getCursorPos = _oldGetCursorPos
|
|
if s then
|
|
return m
|
|
end
|
|
if m == 'Terminated' then
|
|
bRunning = false
|
|
end
|
|
return ''
|
|
end
|
|
|
|
local actions
|
|
local __actions = {
|
|
|
|
input = function(prompt)
|
|
term.setTextColor(color.highlightColor)
|
|
term.setBackgroundColor(colors.gray)
|
|
term.setCursorPos(1, h)
|
|
term.clearLine()
|
|
term.write(prompt)
|
|
local str = hacky_read()
|
|
term.setCursorBlink(true)
|
|
keyboard.shift, keyboard.control = false, false
|
|
term.setCursorPos(x - scrollX, y - scrollY)
|
|
actions.dirty_line(scrollY + h)
|
|
return str
|
|
end,
|
|
|
|
undo = function()
|
|
local last = table.remove(undo.chain)
|
|
if last then
|
|
undo.active = true
|
|
actions[last.action](unpack(last.args))
|
|
undo.active = false
|
|
else
|
|
setStatus('Already at oldest change')
|
|
end
|
|
end,
|
|
|
|
addUndo = function(entry)
|
|
local last = undo.chain[#undo.chain]
|
|
if last and last.action == entry.action then
|
|
--[[
|
|
debug('---')
|
|
debug(last)
|
|
debug(last.args)
|
|
debug(entry)
|
|
debug(entry.args)
|
|
]]--
|
|
if last.action == 'deleteText' then
|
|
if last.args[3] == entry.args[1] and
|
|
last.args[4] == entry.args[2] then
|
|
last.args = {
|
|
last.args[1], last.args[2], entry.args[3], entry.args[4],
|
|
last.args[5] .. entry.args[5]
|
|
}
|
|
else
|
|
table.insert(undo.chain, entry)
|
|
end
|
|
else
|
|
-- insertText (need to finish)
|
|
table.insert(undo.chain, entry)
|
|
end
|
|
else
|
|
table.insert(undo.chain, entry)
|
|
end
|
|
end,
|
|
|
|
autocomplete = function()
|
|
if keyboard.lastAction ~= 'autocomplete' or not complete.results then
|
|
local sLine = tLines[y]:sub(1, x - 1)
|
|
local nStartPos = sLine:find("[a-zA-Z0-9_%.]+$")
|
|
if nStartPos then
|
|
sLine = sLine:sub(nStartPos)
|
|
end
|
|
if #sLine > 0 then
|
|
complete.results = textutils.complete(sLine)
|
|
else
|
|
complete.results = { }
|
|
end
|
|
complete.index = 0
|
|
complete.x = x
|
|
end
|
|
|
|
if #complete.results == 0 then
|
|
setError('No completions available')
|
|
|
|
elseif #complete.results == 1 then
|
|
actions.insertText(x, y, complete.results[1])
|
|
complete.results = nil
|
|
|
|
elseif #complete.results > 1 then
|
|
local prefix = complete.results[1]
|
|
for n = 1, #complete.results do
|
|
local result = complete.results[n]
|
|
while #prefix > 0 do
|
|
if result:find(prefix, 1, true) == 1 then
|
|
break
|
|
end
|
|
prefix = prefix:sub(1, #prefix - 1)
|
|
end
|
|
end
|
|
if #prefix > 0 then
|
|
actions.insertText(x, y, prefix)
|
|
complete.results = nil
|
|
else
|
|
if complete.index > 0 then
|
|
actions.deleteText(complete.x, y, complete.x + #complete.results[complete.index], y)
|
|
end
|
|
complete.index = complete.index + 1
|
|
if complete.index > #complete.results then
|
|
complete.index = 1
|
|
end
|
|
actions.insertText(complete.x, y, complete.results[complete.index])
|
|
end
|
|
end
|
|
end,
|
|
|
|
refresh = function()
|
|
actions.dirty_all()
|
|
mark.continue = mark.active
|
|
setStatus('refreshed')
|
|
end,
|
|
|
|
menu = function()
|
|
setStatus(messages.menu)
|
|
mark.continue = mark.active
|
|
end,
|
|
|
|
goto_line = function()
|
|
local lineNo = tonumber(actions.input('Line: '))
|
|
if lineNo then
|
|
actions.goto(1, lineNo)
|
|
else
|
|
setStatus('Invalid line number')
|
|
end
|
|
end,
|
|
|
|
find = function(pattern, sx)
|
|
local nLines = #tLines
|
|
for i = 1, nLines + 1 do
|
|
local ny = y + i - 1
|
|
if ny > nLines then
|
|
ny = ny - nLines
|
|
end
|
|
local nx = tLines[ny]:lower():find(pattern, sx)
|
|
if nx then
|
|
if ny < y or ny == y and nx <= x then
|
|
setStatus(messages.wrapped)
|
|
end
|
|
actions.goto(nx, ny)
|
|
actions.mark_to(nx + #pattern, ny)
|
|
actions.goto(nx, ny)
|
|
return
|
|
end
|
|
sx = 1
|
|
end
|
|
setError('Pattern not found')
|
|
end,
|
|
|
|
find_next = function()
|
|
if searchPattern then
|
|
actions.unmark()
|
|
actions.find(searchPattern, x + 1)
|
|
end
|
|
end,
|
|
|
|
find_prompt = function()
|
|
local text = actions.input('/')
|
|
if #text > 0 then
|
|
searchPattern = text:lower()
|
|
if searchPattern then
|
|
actions.unmark()
|
|
actions.find(searchPattern, x)
|
|
end
|
|
end
|
|
end,
|
|
|
|
save = function()
|
|
if bReadOnly then
|
|
setError("Access denied")
|
|
else
|
|
local ok, err = save(sPath)
|
|
if ok then
|
|
setStatus('"%s" %dL, %dC written',
|
|
fileInfo.path, #tLines, fs.getSize(fileInfo.abspath))
|
|
else
|
|
setError("Error saving to %s", sPath)
|
|
end
|
|
end
|
|
end,
|
|
|
|
exit = function()
|
|
bRunning = false
|
|
end,
|
|
|
|
run = function()
|
|
local sTempPath = "/.temp"
|
|
local ok, err = save(sTempPath)
|
|
if ok then
|
|
local nTask = shell.openTab(sTempPath)
|
|
if nTask then
|
|
shell.switchTab(nTask)
|
|
else
|
|
setError("Error starting Task")
|
|
end
|
|
os.sleep(0)
|
|
fs.delete(sTempPath)
|
|
else
|
|
setError("Error saving to %s", sTempPath)
|
|
end
|
|
end,
|
|
|
|
status = function()
|
|
local modified = ''
|
|
if undo.chain[1] then
|
|
modified = '[Modified] '
|
|
end
|
|
setStatus('"%s" %s%d lines --%d%%--',
|
|
fileInfo.path, modified, #tLines,
|
|
math.floor((y - 1) / (#tLines - 1) * 100))
|
|
end,
|
|
|
|
dirty_line = function(dy)
|
|
if dirty.y == 0 then
|
|
dirty.y = dy
|
|
dirty.ey = dy
|
|
else
|
|
dirty.y = math.min(dirty.y, dy)
|
|
dirty.ey = math.max(dirty.ey, dy)
|
|
end
|
|
end,
|
|
|
|
dirty_range = function(dy, dey)
|
|
actions.dirty_line(dy)
|
|
actions.dirty_line(dey or #tLines)
|
|
end,
|
|
|
|
dirty = function()
|
|
actions.dirty_line(y)
|
|
end,
|
|
|
|
dirty_all = function()
|
|
actions.dirty_line(1)
|
|
actions.dirty_line(#tLines)
|
|
end,
|
|
|
|
mark_begin = function()
|
|
actions.dirty()
|
|
if not mark.active then
|
|
mark.active = true
|
|
mark.anchor = { x = x, y = y }
|
|
end
|
|
end,
|
|
|
|
mark_finish = function()
|
|
if y == mark.anchor.y then
|
|
if x == mark.anchor.x then
|
|
mark.active = false
|
|
else
|
|
mark.x = math.min(mark.anchor.x, x)
|
|
mark.y = y
|
|
mark.ex = math.max(mark.anchor.x, x)
|
|
mark.ey = y
|
|
end
|
|
elseif y < mark.anchor.y then
|
|
mark.x = x
|
|
mark.y = y
|
|
mark.ex = mark.anchor.x
|
|
mark.ey = mark.anchor.y
|
|
else
|
|
mark.x = mark.anchor.x
|
|
mark.y = mark.anchor.y
|
|
mark.ex = x
|
|
mark.ey = y
|
|
end
|
|
actions.dirty()
|
|
mark.continue = mark.active
|
|
end,
|
|
|
|
unmark = function()
|
|
if mark.active then
|
|
actions.dirty_range(mark.y, mark.ey)
|
|
mark.active = false
|
|
end
|
|
end,
|
|
|
|
mark_to = function(nx, ny)
|
|
actions.mark_begin()
|
|
actions.goto(nx, ny)
|
|
actions.mark_finish()
|
|
end,
|
|
|
|
mark_up = function()
|
|
actions.mark_begin()
|
|
actions.up()
|
|
actions.mark_finish()
|
|
end,
|
|
|
|
mark_right = function()
|
|
actions.mark_begin()
|
|
actions.right()
|
|
actions.mark_finish()
|
|
end,
|
|
|
|
mark_down = function()
|
|
actions.mark_begin()
|
|
actions.down()
|
|
actions.mark_finish()
|
|
end,
|
|
|
|
mark_left = function()
|
|
actions.mark_begin()
|
|
actions.left()
|
|
actions.mark_finish()
|
|
end,
|
|
|
|
mark_word = function()
|
|
actions.mark_begin()
|
|
actions.word()
|
|
actions.mark_finish()
|
|
end,
|
|
|
|
mark_backword = function()
|
|
actions.mark_begin()
|
|
actions.backword()
|
|
actions.mark_finish()
|
|
end,
|
|
|
|
mark_home = function()
|
|
actions.mark_begin()
|
|
actions.home()
|
|
actions.mark_finish()
|
|
end,
|
|
|
|
mark_end = function()
|
|
actions.mark_begin()
|
|
actions.toend()
|
|
actions.mark_finish()
|
|
end,
|
|
|
|
mark_all = function()
|
|
mark.anchor = { x = 1, y = 1 }
|
|
mark.active = true
|
|
mark.continue = true
|
|
mark.x = 1
|
|
mark.y = 1
|
|
mark.ey = #tLines
|
|
mark.ex = #tLines[mark.ey] + 1
|
|
actions.dirty_all()
|
|
end,
|
|
|
|
setCursor = function(newX, newY)
|
|
local oldX, oldY = lastPos.x, lastPos.y
|
|
|
|
lastPos.x = x
|
|
lastPos.y = y
|
|
|
|
local screenX = x - scrollX
|
|
local screenY = y - scrollY
|
|
|
|
if screenX < 1 then
|
|
scrollX = x - 1
|
|
screenX = 1
|
|
actions.dirty_all()
|
|
elseif screenX > w then
|
|
scrollX = x - w
|
|
screenX = w
|
|
actions.dirty_all()
|
|
end
|
|
|
|
if screenY < 1 then
|
|
scrollY = y - 1
|
|
screenY = 1
|
|
actions.dirty_all()
|
|
elseif screenY > h - 1 then
|
|
scrollY = y - (h - 1)
|
|
screenY = h - 1
|
|
actions.dirty_all()
|
|
end
|
|
end,
|
|
|
|
top = function()
|
|
actions.goto(1, 1)
|
|
end,
|
|
|
|
bottom = function()
|
|
y = #tLines
|
|
x = #tLines[y] + 1
|
|
end,
|
|
|
|
up = function()
|
|
if y > 1 then
|
|
x = math.min(x, #tLines[y - 1] + 1)
|
|
y = y - 1
|
|
end
|
|
end,
|
|
|
|
down = function()
|
|
if y < #tLines then
|
|
x = math.min(x, #tLines[y + 1] + 1)
|
|
y = y + 1
|
|
end
|
|
end,
|
|
|
|
tab = function()
|
|
if mark.active then
|
|
actions.delete()
|
|
end
|
|
actions.insertText(x, y, ' ')
|
|
end,
|
|
|
|
pageUp = function()
|
|
actions.goto(x, y - (h - 1))
|
|
end,
|
|
|
|
pageDown = function()
|
|
actions.goto(x, y + (h - 1))
|
|
end,
|
|
|
|
home = function()
|
|
x = 1
|
|
end,
|
|
|
|
toend = function()
|
|
x = #tLines[y] + 1
|
|
end,
|
|
|
|
left = function()
|
|
if x > 1 then
|
|
x = x - 1
|
|
elseif y > 1 then
|
|
x = #tLines[y - 1] + 1
|
|
y = y - 1
|
|
else
|
|
return false
|
|
end
|
|
return true
|
|
end,
|
|
|
|
right = function()
|
|
if x < #tLines[y] + 1 then
|
|
x = x + 1
|
|
elseif y < #tLines then
|
|
x = 1
|
|
y = y + 1
|
|
end
|
|
end,
|
|
|
|
word = function()
|
|
local nx = nextWord(tLines[y], x)
|
|
if nx then
|
|
x = nx
|
|
elseif x < #tLines[y] + 1 then
|
|
x = #tLines[y] + 1
|
|
elseif y < #tLines then
|
|
x = 1
|
|
y = y + 1
|
|
end
|
|
end,
|
|
|
|
backword = function()
|
|
if x == 1 then
|
|
actions.left()
|
|
else
|
|
local sLine = tLines[y]
|
|
local lx = 1
|
|
while true do
|
|
local nx = nextWord(sLine, lx)
|
|
if not nx or nx >= x then
|
|
break
|
|
end
|
|
lx = nx
|
|
end
|
|
if not lx then
|
|
x = 1
|
|
else
|
|
x = lx
|
|
end
|
|
end
|
|
end,
|
|
|
|
insertText = function(sx, sy, text)
|
|
x = sx
|
|
y = sy
|
|
local sLine = tLines[y]
|
|
|
|
if not text:find('\n') then
|
|
tLines[y] = sLine:sub(1, x - 1) .. text .. sLine:sub(x)
|
|
actions.dirty_line(y)
|
|
x = x + #text
|
|
else
|
|
local lines = split(text)
|
|
local remainder = sLine:sub(x)
|
|
tLines[y] = sLine:sub(1, x - 1) .. lines[1]
|
|
actions.dirty_range(y, #tLines + #lines)
|
|
x = x + #lines[1]
|
|
for k = 2, #lines do
|
|
y = y + 1
|
|
table.insert(tLines, y, lines[k])
|
|
x = #lines[k] + 1
|
|
end
|
|
tLines[y] = tLines[y]:sub(1, x) .. remainder
|
|
end
|
|
|
|
if not undo.active then
|
|
actions.addUndo(
|
|
{ action = 'deleteText', args = { sx, sy, x, y, text } })
|
|
end
|
|
end,
|
|
|
|
deleteText = function(sx, sy, ex, ey)
|
|
x = sx
|
|
y = sy
|
|
|
|
if not undo.active then
|
|
local text = actions.copyText(sx, sy, ex, ey)
|
|
actions.addUndo(
|
|
{ action = 'insertText', args = { sx, sy, text } })
|
|
end
|
|
|
|
local front = tLines[sy]:sub(1, sx - 1)
|
|
local back = tLines[ey]:sub(ex, #tLines[ey])
|
|
for k = 2, ey - sy + 1 do
|
|
table.remove(tLines, y + 1)
|
|
end
|
|
tLines[y] = front .. back
|
|
if sy ~= ey then
|
|
actions.dirty_range(y)
|
|
else
|
|
actions.dirty()
|
|
end
|
|
end,
|
|
|
|
copyText = function(csx, csy, cex, cey)
|
|
local count = 0
|
|
local lines = { }
|
|
|
|
for y = csy, cey do
|
|
local line = tLines[y]
|
|
if line then
|
|
local x = 1
|
|
local ex = #line
|
|
if y == csy then
|
|
x = csx
|
|
end
|
|
if y == cey then
|
|
ex = cex - 1
|
|
end
|
|
local str = line:sub(x, ex)
|
|
count = count + #str
|
|
table.insert(lines, str)
|
|
end
|
|
end
|
|
return table.concat(lines, '\n'), count
|
|
end,
|
|
|
|
delete = function()
|
|
if mark.active then
|
|
actions.deleteText(mark.x, mark.y, mark.ex, mark.ey)
|
|
else
|
|
local nLimit = #tLines[y] + 1
|
|
if x < nLimit then
|
|
actions.deleteText(x, y, x + 1, y)
|
|
elseif y < #tLines then
|
|
actions.deleteText(x, y, 1, y + 1)
|
|
end
|
|
end
|
|
end,
|
|
|
|
backspace = function()
|
|
if mark.active then
|
|
actions.delete()
|
|
elseif actions.left() then
|
|
actions.delete()
|
|
end
|
|
end,
|
|
|
|
enter = function()
|
|
local sLine = tLines[y]
|
|
local _,spaces = sLine:find("^[ ]+")
|
|
if not spaces then
|
|
spaces = 0
|
|
end
|
|
spaces = math.min(spaces, x - 1)
|
|
if mark.active then
|
|
actions.delete()
|
|
end
|
|
actions.insertText(x, y, '\n' .. string.rep(' ', spaces))
|
|
end,
|
|
|
|
char = function(ch)
|
|
if mark.active then
|
|
actions.delete()
|
|
end
|
|
actions.insertText(x, y, ch)
|
|
end,
|
|
|
|
toggle_clipboard = function()
|
|
if clipboard.shim then
|
|
clipboard.setInternal(not clipboard.internal)
|
|
end
|
|
if clipboard.isInternal() then
|
|
setStatus('Using internal clipboard')
|
|
else
|
|
setStatus('Using system clipboard')
|
|
end
|
|
end,
|
|
|
|
copy_marked = function()
|
|
local text, size =
|
|
actions.copyText(mark.x, mark.y, mark.ex, mark.ey)
|
|
clipboard.setData(text)
|
|
setStatus('%d chars copied', size)
|
|
clipboard.useInternal(true)
|
|
end,
|
|
|
|
cut = function()
|
|
if mark.active then
|
|
actions.copy_marked()
|
|
actions.delete()
|
|
end
|
|
end,
|
|
|
|
copy = function()
|
|
if mark.active then
|
|
actions.copy_marked()
|
|
mark.continue = true
|
|
end
|
|
end,
|
|
|
|
paste = function(text)
|
|
if mark.active then
|
|
actions.delete()
|
|
end
|
|
if clipboard.isInternal() then
|
|
text = clipboard.getText()
|
|
end
|
|
if text then
|
|
actions.insertText(x, y, text)
|
|
setStatus('%d chars added', #text)
|
|
else
|
|
setStatus('Clipboard empty')
|
|
end
|
|
end,
|
|
|
|
goto = function(cx, cy)
|
|
y = math.min(math.max(cy, 1), #tLines)
|
|
x = math.min(math.max(cx, 1), #tLines[y] + 1)
|
|
end,
|
|
|
|
scroll_up = function()
|
|
if scrollY > 0 then
|
|
scrollY = scrollY - 1
|
|
actions.dirty_all()
|
|
end
|
|
mark.continue = mark.active
|
|
end,
|
|
|
|
scroll_down = function()
|
|
local nMaxScroll = #tLines - (h-1)
|
|
if scrollY < nMaxScroll then
|
|
scrollY = scrollY + 1
|
|
actions.dirty_all()
|
|
end
|
|
mark.continue = mark.active
|
|
end,
|
|
}
|
|
|
|
actions = __actions
|
|
|
|
-- Actual program functionality begins
|
|
load(sPath)
|
|
|
|
term.setCursorBlink(true)
|
|
redraw()
|
|
|
|
if not keyboard then
|
|
keyboard = { control, shift, combo }
|
|
|
|
function keyboard:translate(event, code)
|
|
if event == 'key' then
|
|
local ch = keys.getName(code)
|
|
if ch then
|
|
|
|
if code == keys.leftCtrl or code == keys.rightCtrl then
|
|
self.control = true
|
|
self.combo = false
|
|
return
|
|
end
|
|
|
|
if code == keys.leftShift or code == keys.rightShift then
|
|
self.shift = true
|
|
self.combo = false
|
|
return
|
|
end
|
|
|
|
if self.shift then
|
|
if #ch > 1 then
|
|
ch = 'shift-' .. ch
|
|
elseif self.control then
|
|
-- will create control-X
|
|
-- better than shift-control-x
|
|
ch = ch:upper()
|
|
end
|
|
self.combo = true
|
|
end
|
|
|
|
if self.control then
|
|
ch = 'control-' .. ch
|
|
self.combo = true
|
|
-- even return numbers such as
|
|
-- control-seven
|
|
return ch
|
|
end
|
|
|
|
-- filter out characters that will be processed in
|
|
-- the subsequent char event
|
|
if ch and #ch > 1 and (code < 2 or code > 11) then
|
|
return ch
|
|
end
|
|
end
|
|
|
|
elseif event == 'key_up' then
|
|
|
|
if code == keys.leftCtrl or code == keys.rightCtrl then
|
|
self.control = false
|
|
elseif code == keys.leftShift or code == keys.rightShift then
|
|
self.shift = false
|
|
else
|
|
return
|
|
end
|
|
|
|
-- only send through the shift / control event if it wasn't
|
|
-- used in combination with another event
|
|
if not self.combo then
|
|
return keys.getName(code)
|
|
end
|
|
|
|
elseif event == 'char' then
|
|
if not self.control then
|
|
self.combo = true
|
|
return event
|
|
end
|
|
|
|
elseif event == 'mouse_click' then
|
|
|
|
local buttons = { 'mouse_click', 'mouse_rightclick', 'mouse_doubleclick' }
|
|
|
|
self.combo = true
|
|
if self.shift then
|
|
return 'shift-' .. buttons[code]
|
|
end
|
|
return buttons[code]
|
|
|
|
elseif event == "mouse_scroll" then
|
|
local directions = {
|
|
[ -1 ] = 'scrollUp',
|
|
[ 1 ] = 'scrollDown'
|
|
}
|
|
return directions[code]
|
|
|
|
elseif event == 'paste' then
|
|
self.combo = true
|
|
return event
|
|
|
|
elseif event == 'mouse_drag' then
|
|
return event
|
|
end
|
|
end
|
|
end
|
|
|
|
while bRunning do
|
|
local sEvent, param, param2, param3 = os.pullEventRaw()
|
|
local action
|
|
|
|
if sEvent == 'terminate' then
|
|
action = 'exit'
|
|
elseif sEvent == "mouse_click" or sEvent == 'mouse_drag' then
|
|
if param3 < h or sEvent == 'mouse_drag' then
|
|
local ch = keyboard:translate(sEvent, param)
|
|
if ch then
|
|
action = keyMapping[ch]
|
|
param = param2 + scrollX
|
|
param2 = param3 + scrollY
|
|
end
|
|
end
|
|
else
|
|
local ch = keyboard:translate(sEvent, param)
|
|
if ch then
|
|
action = keyMapping[ch]
|
|
end
|
|
end
|
|
|
|
if action then
|
|
if not actions[action] then
|
|
error('Invaid action: ' .. action)
|
|
end
|
|
|
|
local wasMarking = mark.continue
|
|
mark.continue = false
|
|
|
|
actions[action](param, param2)
|
|
if action ~= 'menu' then
|
|
keyboard.lastAction = action
|
|
end
|
|
|
|
if x ~= lastPos.x or y ~= lastPos.y then
|
|
actions.setCursor()
|
|
end
|
|
if not mark.continue and wasMarking then
|
|
actions.unmark()
|
|
end
|
|
|
|
redraw()
|
|
|
|
elseif sEvent == "term_resize" then
|
|
w,h = term.getSize()
|
|
actions.setCursor(x, y)
|
|
actions.dirty_all()
|
|
redraw()
|
|
end
|
|
end
|
|
|
|
-- Cleanup
|
|
term.setBackgroundColor(colors.black)
|
|
term.setTextColor(colors.white)
|
|
term.clear()
|
|
term.setCursorBlink(false)
|
|
term.setCursorPos(1, 1)
|