initial code commit
parent
de2769b9fa
commit
a90b90e711
58
src/Acorn.jl
58
src/Acorn.jl
|
@ -1,5 +1,61 @@
|
|||
module Acorn
|
||||
|
||||
# package code goes here
|
||||
include("terminal.jl")
|
||||
|
||||
# Configuration submodule
|
||||
include("EditorConfig.jl")
|
||||
using .EditorConfig
|
||||
|
||||
include("row.jl")
|
||||
include("cmds/Command.jl")
|
||||
include("editor.jl")
|
||||
|
||||
|
||||
# Load commands
|
||||
include("cmds/open.jl")
|
||||
include("cmds/save.jl")
|
||||
include("cmds/quit.jl")
|
||||
include("cmds/find.jl")
|
||||
include("cmds/help.jl")
|
||||
include("cmds/bind.jl")
|
||||
include("cmds/set.jl")
|
||||
include("cmds/echo.jl")
|
||||
|
||||
function acorn(filename::String, rel=true)
|
||||
ed = Editor()
|
||||
|
||||
rel && (filename = abspath(filename))
|
||||
|
||||
editorOpen(ed, filename)
|
||||
|
||||
setStatusMessage(ed, "HELP: Ctrl-Q: quit | Ctrl-S: save | Ctrl-F: find")
|
||||
|
||||
Base.Terminals.raw!(ed.term, true)
|
||||
|
||||
try
|
||||
while !ed.quit
|
||||
refreshScreen(ed)
|
||||
processKeypress(ed)
|
||||
end
|
||||
catch ex
|
||||
editorQuit(ed, force=true)
|
||||
throw(ex)
|
||||
end
|
||||
|
||||
Base.Terminals.raw!(ed.term, false)
|
||||
|
||||
return nothing
|
||||
end
|
||||
|
||||
function acorn()
|
||||
# If a file is given, open it
|
||||
if length(ARGS) > 0
|
||||
filename = ARGS[1]
|
||||
acorn(filename, rel=false)
|
||||
end
|
||||
end
|
||||
|
||||
export
|
||||
acorn
|
||||
|
||||
end # module
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
module EditorConfig
|
||||
|
||||
|
||||
###############
|
||||
## Parameter ##
|
||||
###############
|
||||
|
||||
type Parameter{T}
|
||||
value::T
|
||||
validate::Union{Function, Void}
|
||||
desc::String # Used when calling help <param name>
|
||||
end
|
||||
|
||||
validate(p::Parameter) = p.validate == nothing ? true : p.validate(p.value)
|
||||
|
||||
function set(p::Parameter, x)
|
||||
old_val = p.value
|
||||
|
||||
# Correct type?
|
||||
try
|
||||
p.value = x
|
||||
catch Exception
|
||||
p.value = old_val
|
||||
throw(ArgumentError("Invalid parameter assignment: $sym, $x"))
|
||||
end
|
||||
|
||||
# Valid?
|
||||
if !validate(p)
|
||||
p.value = old_val
|
||||
throw(ArgumentError("Invalid parameter assignmnt: $sym, $x"))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
############
|
||||
## CONFIG ##
|
||||
############
|
||||
|
||||
const CONFIG = Dict{Symbol, Parameter}()
|
||||
|
||||
|
||||
|
||||
function configSet(sym::Symbol, x)
|
||||
# Check if parameter exists
|
||||
if !(sym in keys(CONFIG))
|
||||
throw(ArgumentError("No parameter named $sym"))
|
||||
end
|
||||
|
||||
p = CONFIG[sym]
|
||||
set(p, x)
|
||||
|
||||
CONFIG[sym] = p
|
||||
end
|
||||
|
||||
configGet(sym::Symbol) = CONFIG[sym].value
|
||||
configDesc(sym::Symbol) = CONFIG[sym].desc
|
||||
configIsParam(sym::Symbol) = sym in keys(CONFIG)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
##################
|
||||
## KEY BINDINGS ##
|
||||
##################
|
||||
|
||||
|
||||
const KEY_BINDINGS = Dict{UInt32, String}()
|
||||
|
||||
function setKeyBinding(c::Char, s::String)
|
||||
KEY_BINDINGS[UInt32(c) & 0x1f] = s
|
||||
end
|
||||
|
||||
function getKeyBinding(c::Char)
|
||||
get(KEY_BINDINGS, UInt32(c) & 0x1f, "")
|
||||
end
|
||||
|
||||
isKeyBound(c::Char) = (UInt32(c) & 0x1f) in keys(KEY_BINDINGS)
|
||||
|
||||
|
||||
|
||||
|
||||
########################
|
||||
## DEFAULT PARAMETERS ##
|
||||
########################
|
||||
|
||||
CONFIG[:tab_stop] = Parameter{Int}(4, n-> n > 0 && n <= 16, "visual size of a tab in number of spaces")
|
||||
CONFIG[:expandtab] = Parameter{Bool}(false, nothing, "if true, use spaces instead of tabs when pressing <tab>")
|
||||
|
||||
|
||||
##########################
|
||||
## DEFAULT KEY BINDINGS ##
|
||||
##########################
|
||||
|
||||
setKeyBinding('s', "save")
|
||||
setKeyBinding('o', "open")
|
||||
setKeyBinding('f', "find")
|
||||
setKeyBinding('q', "quit")
|
||||
|
||||
|
||||
# # In juliarc:
|
||||
# # using Acorn
|
||||
#
|
||||
#
|
||||
# ## KEY_BINDINGS
|
||||
# #Acorn.addKeyBinding('f', "find")
|
||||
# Acorn.addKeyBinding('o', "open")
|
||||
# Acorn.addKeyBinding('s', "save")
|
||||
#
|
||||
# """
|
||||
# bind f find
|
||||
# bind o open
|
||||
# bind s save
|
||||
# set tab_stop 4
|
||||
# set expandtab true
|
||||
# """
|
||||
|
||||
# TODO:
|
||||
# hard/soft tabs
|
||||
# show hidden characters
|
||||
|
||||
export
|
||||
configGet,
|
||||
configSet,
|
||||
configIsParam,
|
||||
configDesc,
|
||||
setKeyBinding,
|
||||
getKeyBinding,
|
||||
isKeyBound
|
||||
|
||||
end #module
|
Binary file not shown.
|
@ -0,0 +1,11 @@
|
|||
type Command
|
||||
name::Symbol
|
||||
help::String
|
||||
cmd::Function
|
||||
end
|
||||
|
||||
const COMMANDS = Dict{Symbol, Command}()
|
||||
|
||||
function addCommand(name::Symbol, cmd::Function; help::String="")
|
||||
COMMANDS[name] = Command(name, help, cmd)
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
function commandBind(ed::Editor, args::String)
|
||||
arg_arr = strip.(split(args, ' ', limit=2))
|
||||
|
||||
if length(arg_arr) != 2
|
||||
setStatusMessage(ed, "bind: command requires two arguments")
|
||||
return
|
||||
elseif !( length(arg_arr[1]) == 1 && isalpha(arg_arr[1][1]) )
|
||||
setStatusMessage(ed, "bind: first arg must be a letter")
|
||||
return
|
||||
# elseif !Base.isidentifier(arg_arr[2])
|
||||
# setStatusMessage(ed, "bind: command $(arg_arr[2]) is invalid")
|
||||
# return
|
||||
end
|
||||
|
||||
key = lowercase(arg_arr[1][1])
|
||||
|
||||
setKeyBinding(key, join(arg_arr[2]))
|
||||
end
|
||||
|
||||
addCommand(:bind, commandBind,
|
||||
help="bind <C> <CMD>: bind ctrl-<C> to command <CMD>")
|
|
@ -0,0 +1,6 @@
|
|||
function commandEcho(ed::Editor, args::String)
|
||||
setStatusMessage(ed, args)
|
||||
end
|
||||
|
||||
addCommand(:echo, commandEcho,
|
||||
help="echo <msg>: set the status message to <msg>")
|
|
@ -0,0 +1,86 @@
|
|||
|
||||
function initFind(ed::Editor)
|
||||
ed.params[:find] = Dict{Symbol, Any}()
|
||||
|
||||
# 1 indexed, 0 none
|
||||
ed.params[:find][:last_match] = 0
|
||||
ed.params[:find][:direction] = 1
|
||||
end
|
||||
|
||||
|
||||
function findCallback(ed::Editor, query::String, key::Char)
|
||||
# If the params have not been created, init them
|
||||
if !(:find in keys(ed.params))
|
||||
initFind(ed)
|
||||
end
|
||||
|
||||
last_match = ed.params[:find][:last_match]
|
||||
direction = ed.params[:find][:direction]
|
||||
|
||||
if key == '\r' || key == '\x1b'
|
||||
last_match = 0
|
||||
direction = 1
|
||||
return
|
||||
elseif key == ARROW_RIGHT || key == ARROW_DOWN
|
||||
direction = 1
|
||||
elseif key == ARROW_LEFT || key == ARROW_UP
|
||||
direction = -1
|
||||
else
|
||||
last_match = 0
|
||||
direction = 1
|
||||
end
|
||||
|
||||
last_match == 0 && (direction = 1)
|
||||
current = last_match
|
||||
for i = 1:length(ed.rows)
|
||||
current += direction
|
||||
|
||||
# Bounds check
|
||||
if current == 0
|
||||
# At begenning of document? Go to end
|
||||
current = length(ed.rows)
|
||||
elseif current == length(ed.rows)+1
|
||||
# At end of doc? Got to start
|
||||
current = 1
|
||||
end
|
||||
|
||||
row = ed.rows[current]
|
||||
loc = search(row.chars, query)
|
||||
if loc != 0:-1
|
||||
last_match = current
|
||||
ed.csr.y = current
|
||||
ed.csr.x = first(loc)#charX(row, first(loc))
|
||||
ed.rowoff = length(ed.rows)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# Update params
|
||||
ed.params[:find][:last_match] = last_match
|
||||
ed.params[:find][:direction] = direction
|
||||
|
||||
end
|
||||
|
||||
function editorFind(ed::Editor, str::String)
|
||||
saved_cx, saved_cy = ed.csr.x, ed.csr.y
|
||||
saved_coloff = ed.coloff
|
||||
saved_rowoff = ed.rowoff
|
||||
|
||||
findCallback(ed, str, ' ')
|
||||
query = editorPrompt(ed, "Search (arrow keys for next/prev): ",
|
||||
callback=findCallback,
|
||||
buf=str,
|
||||
showcursor=false)
|
||||
|
||||
# If the user cancels the search, restore view
|
||||
if query == ""
|
||||
ed.csr.x = saved_cx
|
||||
ed.csr.y = saved_cy
|
||||
ed.coloff = saved_coloff
|
||||
ed.rowoff = saved_rowoff
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
addCommand(:find, editorFind,
|
||||
help="type to find, <enter> to get cursor, arrow keys to next/prev")
|
|
@ -0,0 +1,31 @@
|
|||
function commandHelp(ed::Editor, args::String)
|
||||
if args == ""
|
||||
setStatusMessage(ed, "Type help <command> for command specific help")
|
||||
return
|
||||
end
|
||||
|
||||
if !Base.isidentifier(args)
|
||||
setStatusMessage(ed, "help: '$args' is not a valid command name")
|
||||
return
|
||||
end
|
||||
|
||||
sym = Symbol(args)
|
||||
|
||||
# Try getting help information from commands and params
|
||||
helptext = ""
|
||||
|
||||
if configIsParam(sym)
|
||||
helptext = configDesc(sym)
|
||||
elseif sym in keys(COMMANDS)
|
||||
helptext = COMMANDS[sym].help
|
||||
end
|
||||
|
||||
if helptext == ""
|
||||
setStatusMessage(ed, "No help documents for '$args'")
|
||||
else
|
||||
setStatusMessage(ed, helptext)
|
||||
end
|
||||
end
|
||||
|
||||
addCommand(:help, commandHelp,
|
||||
help="Type help <command> for command specific help")
|
|
@ -0,0 +1,10 @@
|
|||
function commandOpen(ed::Editor, args::String)
|
||||
if args == ""
|
||||
editorOpen(ed)
|
||||
else
|
||||
editorOpen(ed, args)
|
||||
end
|
||||
end
|
||||
|
||||
addCommand(:open, commandOpen,
|
||||
help="open: open a file")
|
|
@ -0,0 +1,6 @@
|
|||
function commandQuit(ed::Editor, args::String)
|
||||
editorQuit(ed, force=strip(args) == "!")
|
||||
end
|
||||
|
||||
addCommand(:quit, commandQuit,
|
||||
help="quit [!]: quit acorn. run 'quit !' to force quit")
|
|
@ -0,0 +1,6 @@
|
|||
function commandSave(ed::Editor, args::String)
|
||||
editorSave(ed, args)
|
||||
end
|
||||
|
||||
addCommand(:save, commandSave,
|
||||
help="save: save the file")
|
|
@ -0,0 +1,36 @@
|
|||
function commandSet(ed::Editor, args::String)
|
||||
arg_arr = strip.(split(args, ' '))
|
||||
|
||||
# Initial checks
|
||||
if length(arg_arr) != 2
|
||||
setStatusMessage(ed, "set: command requires two arguments")
|
||||
return
|
||||
elseif !Base.isidentifier(arg_arr[1])
|
||||
setStatusMessage(ed, "set: $(arg_arr[1]) is not a valid command name")
|
||||
return
|
||||
end
|
||||
|
||||
sym = Symbol(arg_arr[1])
|
||||
|
||||
# Check if it is a valid parameter
|
||||
if !configIsParam(sym)
|
||||
setStatusMessage(ed, "set: '$sym' is not a valid parameter name")
|
||||
return
|
||||
end
|
||||
|
||||
# Attempt to assign the parameter
|
||||
try
|
||||
val = parse(arg_arr[2])
|
||||
configSet(sym, val)
|
||||
catch Exception
|
||||
setStatusMessage(ed, "set: invalid argument for $sym '$(arg_arr[2])'")
|
||||
end
|
||||
|
||||
for row in ed.rows
|
||||
update!(row)
|
||||
end
|
||||
refreshScreen(ed)
|
||||
end
|
||||
|
||||
addCommand(:set, commandSet,
|
||||
help="set <param> <value>: set the given parameter to a value")
|
|
@ -0,0 +1,536 @@
|
|||
# """ Clear the screen, print an error message, and kill the program """
|
||||
# function die(msg)
|
||||
# write(STDOUT, "\x1b[2J")
|
||||
# write(STDOUT, "\x1b[H")
|
||||
# error(msg)
|
||||
# end
|
||||
|
||||
|
||||
##########
|
||||
# CURSOR #
|
||||
##########
|
||||
|
||||
mutable struct Cursor
|
||||
x::Int
|
||||
y::Int
|
||||
rx::Int
|
||||
end
|
||||
|
||||
|
||||
##########
|
||||
# EDITOR #
|
||||
##########
|
||||
|
||||
mutable struct Editor
|
||||
rowoff::Int
|
||||
coloff::Int
|
||||
width::Int
|
||||
height::Int
|
||||
filename::String
|
||||
statusmsg::String
|
||||
status_time::Float64
|
||||
dirty::Bool
|
||||
quit::Bool
|
||||
|
||||
csr::Cursor
|
||||
rows::Rows
|
||||
term::Base.Terminals.TTYTerminal
|
||||
|
||||
params::Dict{Symbol, Dict{Symbol, Any}}
|
||||
end
|
||||
|
||||
function Editor()
|
||||
rowoff = 0
|
||||
coloff = 0
|
||||
width = 0
|
||||
height = 0
|
||||
filename = ""
|
||||
statusmsg = ""
|
||||
status_time = 0
|
||||
dirty = false
|
||||
quit = false
|
||||
|
||||
csr = Cursor(1,1,1)
|
||||
rows = Rows()
|
||||
term = Base.Terminals.TTYTerminal(get(ENV, "TERM", @static is_windows() ? "" : "dumb"), STDIN, STDOUT, STDERR)
|
||||
|
||||
params = Dict{Symbol, Dict{Symbol, Any}}()
|
||||
|
||||
Editor(rowoff, coloff, width, height, filename,
|
||||
statusmsg, status_time, dirty, quit,
|
||||
csr, rows, term, params)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
###################
|
||||
# FILE OPERATIONS #
|
||||
###################
|
||||
|
||||
function expand(filename::String)
|
||||
# If the path is something like "~/Desktop/..." expand it
|
||||
if length(filename) > 1 && filename[1] == '~'
|
||||
filename = expanduser(filename)
|
||||
end
|
||||
|
||||
return filename
|
||||
end
|
||||
|
||||
""" Open a file and read the lines into the ed.rows array """
|
||||
function editorOpen(ed::Editor, filename::String)
|
||||
try
|
||||
filename = expand(filename)
|
||||
|
||||
# If no file exists, create it
|
||||
!isfile(filename) && open(filename, "w") do f end
|
||||
|
||||
open(filename, "r") do file
|
||||
clearRows(ed.rows)
|
||||
for line in eachline(file)
|
||||
appendRow(ed.rows, line)
|
||||
end
|
||||
ed.filename = filename
|
||||
ed.csr.x = 1
|
||||
ed.csr.y = 1
|
||||
ed.dirty = false
|
||||
end
|
||||
catch Exception e
|
||||
setStatusMessage(ed, "Cannot open file $filename")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function editorOpen(ed::Editor)
|
||||
if ed.dirty
|
||||
confirm = editorPrompt(ed, "There are unsaved changes. Open another file? [y/n]: ")
|
||||
if confirm != "y"
|
||||
setStatusMessage(ed, "Open aborted")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
filename = editorPrompt(ed, "Open file: ")
|
||||
filename = expand(filename)
|
||||
|
||||
if filename != ""
|
||||
editorOpen(ed, filename)
|
||||
else
|
||||
setStatusMessage(ed, "Open aborted")
|
||||
end
|
||||
end
|
||||
|
||||
function editorSave(ed::Editor)
|
||||
editorSave(ed, "")
|
||||
end
|
||||
|
||||
""" Save the contents of the file buffer to disc """
|
||||
function editorSave(ed::Editor, path::String)
|
||||
prev_filename = ed.filename
|
||||
|
||||
try
|
||||
if path == ""
|
||||
if ed.filename == ""
|
||||
ed.filename = editorPrompt(ed, "Save as: ")
|
||||
if ed.filename == ""
|
||||
setStatusMessage(ed, "Save aborted")
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
ed.filename = expand(path)
|
||||
end
|
||||
|
||||
open(ed.filename, "w") do f
|
||||
write(f, rowsToString(ed.rows))
|
||||
end
|
||||
setStatusMessage(ed, "File saved: $(ed.filename)")
|
||||
ed.dirty = false
|
||||
catch Exception
|
||||
# There was an error saving, restore original filename
|
||||
ed.filename = prev_filename
|
||||
setStatusMessage(ed, "Unable to save: $(ed.filename)")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
##########
|
||||
# VISUAL #
|
||||
##########
|
||||
|
||||
""" Given an arrow input, move the cursor """
|
||||
function moveCursor(ed::Editor, key::UInt32)
|
||||
if key == ARROW_LEFT
|
||||
if ed.csr.x > 1
|
||||
ed.csr.x -= 1
|
||||
elseif ed.csr.y > 1
|
||||
# At start of line, move to end of prev line
|
||||
ed.csr.y -= 1
|
||||
ed.csr.x = 1+length(ed.rows[ed.csr.y].chars)
|
||||
end
|
||||
|
||||
elseif key == ARROW_RIGHT
|
||||
onrow = ed.csr.y <= length(ed.rows)
|
||||
if onrow && ed.csr.x <= length(ed.rows[ed.csr.y].chars)
|
||||
ed.csr.x += 1
|
||||
elseif ed.csr.y < length(ed.rows) && ed.csr.x == 1 + length(ed.rows[ed.csr.y].chars)
|
||||
# At end of line, move to next line
|
||||
ed.csr.y += 1
|
||||
ed.csr.x = 1
|
||||
end
|
||||
|
||||
elseif key == ARROW_UP
|
||||
ed.csr.y > 1 && (ed.csr.y -= 1)
|
||||
|
||||
elseif key == ARROW_DOWN
|
||||
ed.csr.y < length(ed.rows) && (ed.csr.y += 1)
|
||||
end
|
||||
|
||||
# Snap to end of line if we are further out
|
||||
rowlen = ed.csr.y < length(ed.rows)+1 ? length(ed.rows[ed.csr.y].chars)+1 : 1
|
||||
ed.csr.x > rowlen && (ed.csr.x = rowlen)
|
||||
end
|
||||
|
||||
moveCursor(ed, key::Key) = moveCursor(ed, UInt32(key))
|
||||
|
||||
|
||||
|
||||
""" Scroll the screen based on the cursor position """
|
||||
function scroll(ed::Editor)
|
||||
ed.csr.rx = 1
|
||||
if ed.csr.y <= length(ed.rows)
|
||||
ed.csr.rx = renderX(ed.rows[ed.csr.y], ed.csr.x)
|
||||
end
|
||||
|
||||
# Vertical scrolling
|
||||
if ed.csr.y < ed.rowoff+1
|
||||
ed.rowoff = ed.csr.y-1
|
||||
end
|
||||
if ed.csr.y >= ed.rowoff+1 + ed.height
|
||||
ed.rowoff = ed.csr.y - ed.height
|
||||
end
|
||||
|
||||
# Horizontal scrolling
|
||||
if ed.csr.rx < ed.coloff+1
|
||||
ed.coloff = ed.csr.rx
|
||||
end
|
||||
if ed.csr.rx >= ed.coloff+1 + ed.width
|
||||
ed.coloff = ed.csr.rx - ed.width
|
||||
end
|
||||
end
|
||||
|
||||
function drawRows(ed::Editor, buf::IOBuffer)
|
||||
for y = 1:ed.height
|
||||
filerow = y + ed.rowoff
|
||||
y != 1 && write(buf, "\r\n")
|
||||
|
||||
write(buf, "\x1b[K") # Clear line
|
||||
if filerow > length(ed.rows)
|
||||
if y == div(ed.height, 3) && ed.width > 40 && length(ed.rows) == 0
|
||||
msg = "Acorn Editor"
|
||||
padding = div(ed.width - length(msg), 2)
|
||||
if padding > 0
|
||||
write(buf, "~")
|
||||
padding -= 1
|
||||
end
|
||||
while (padding -= 1) > 0
|
||||
write(buf, " ")
|
||||
end
|
||||
write(buf, msg)
|
||||
else
|
||||
write(buf, "~");
|
||||
end
|
||||
else
|
||||
len = length(ed.rows[filerow].render) - ed.coloff
|
||||
len = clamp(len, 0, ed.width)
|
||||
write(buf, ed.rows[filerow].render[1+ed.coloff : ed.coloff + len])
|
||||
end
|
||||
end
|
||||
# Write a newline to prepare for status bar
|
||||
write(buf, "\r\n");
|
||||
end
|
||||
|
||||
function drawStatusBar(ed::Editor, buf::IOBuffer)
|
||||
write(buf, "\x1b[7m") # invert colors
|
||||
col = 1
|
||||
|
||||
# left padding
|
||||
write(buf, ' ')
|
||||
col += 1
|
||||
|
||||
# filename
|
||||
filestatus = string(ed.filename, ed.dirty ? " *" : "")
|
||||
|
||||
for i = 1:min(div(ed.width,2), length(filestatus))
|
||||
write(buf, filestatus[i])
|
||||
col += 1
|
||||
end
|
||||
|
||||
linenum = string(ed.csr.y)
|
||||
|
||||
while col < ed.width - length(linenum)
|
||||
write(buf, ' ')
|
||||
col += 1
|
||||
end
|
||||
|
||||
write(buf, linenum, ' ')
|
||||
|
||||
write(buf, "\x1b[m") # uninvert colors
|
||||
|
||||
# make line for message bar
|
||||
write(buf, "\r\n")
|
||||
end
|
||||
|
||||
function drawStatusMessage(ed::Editor, buf::IOBuffer)
|
||||
write(buf, "\x1b[K")
|
||||
if time() - ed.status_time < 5.0
|
||||
write(buf, ed.statusmsg[1:min(ed.width, length(ed.statusmsg))])
|
||||
end
|
||||
end
|
||||
|
||||
function setStatusMessage(ed::Editor, msg::String)
|
||||
ed.statusmsg = msg
|
||||
ed.status_time = time()
|
||||
end
|
||||
|
||||
function refreshScreen(ed::Editor)
|
||||
|
||||
# Update terminal size
|
||||
ed.height = Base.Terminals.height(ed.term) - 2 # status + msg bar = 2
|
||||
ed.width = Base.Terminals.width(ed.term)
|
||||
|
||||
scroll(ed)
|
||||
|
||||
buf = IOBuffer()
|
||||
|
||||
write(buf, "\x1b[?25l") # ?25l: Hide cursor
|
||||
write(buf, "\x1b[H") # H: Move cursor to top left
|
||||
|
||||
drawRows(ed, buf)
|
||||
drawStatusBar(ed, buf)
|
||||
drawStatusMessage(ed, buf)
|
||||
|
||||
@printf(buf, "\x1b[%d;%dH", ed.csr.y-ed.rowoff,
|
||||
ed.csr.rx-ed.coloff)
|
||||
|
||||
write(buf, "\x1b[?25h") # ?25h: Show cursor
|
||||
|
||||
write(STDOUT, String(take!(buf)))
|
||||
end
|
||||
|
||||
function editorPrompt(ed::Editor, prompt::String;
|
||||
callback=nothing,
|
||||
buf::String="",
|
||||
showcursor::Bool=true) ::String
|
||||
while true
|
||||
statusmsg = string(prompt, buf)
|
||||
setStatusMessage(ed, string(prompt, buf))
|
||||
refreshScreen(ed)
|
||||
|
||||
if showcursor
|
||||
# Position the cursor at the end of the line
|
||||
@printf(STDOUT, "\x1b[%d;%dH", 999, length(statusmsg)+1)
|
||||
end
|
||||
|
||||
c = Char(readKey())
|
||||
|
||||
if c == '\x1b'
|
||||
setStatusMessage(ed, "")
|
||||
callback != nothing && callback(ed, buf, c)
|
||||
return ""
|
||||
elseif c == '\r'
|
||||
if length(buf) != 0
|
||||
setStatusMessage(ed, "")
|
||||
callback != nothing && callback(ed, buf, c)
|
||||
return buf
|
||||
end
|
||||
elseif UInt32(c) == BACKSPACE && length(buf) > 0
|
||||
buf = buf[1:end-1]
|
||||
elseif !iscntrl(c) && UInt32(c) < 128
|
||||
buf = string(buf, c)
|
||||
end
|
||||
|
||||
callback != nothing && callback(ed, buf, c)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
##############
|
||||
## Keyboard ##
|
||||
##############
|
||||
|
||||
|
||||
function processKeypress(ed::Editor)
|
||||
c = readKey();
|
||||
|
||||
if c == ctrl_key('p')
|
||||
runCommand(ed)
|
||||
elseif (c == ARROW_LEFT
|
||||
|| c == ARROW_UP
|
||||
|| c == ARROW_RIGHT
|
||||
|| c == ARROW_DOWN)
|
||||
moveCursor(ed, c)
|
||||
elseif c == PAGE_UP || c == PAGE_DOWN
|
||||
lines = ed.height
|
||||
while (lines-=1) > 0
|
||||
moveCursor(ed, c == PAGE_UP ? ARROW_UP : ARROW_DOWN)
|
||||
end
|
||||
elseif c == HOME_KEY
|
||||
ed.csr.x = 0
|
||||
elseif c == END_KEY
|
||||
ed.csr.y < length(ed.rows) && (ed.csr.x = length(ed.rows[ed.csr.y].chars))
|
||||
elseif c == UInt32('\r')
|
||||
editorInsertNewline(ed)
|
||||
elseif c == BACKSPACE
|
||||
editorDelChar(ed)
|
||||
elseif c == DEL_KEY
|
||||
moveCursor(ed, ARROW_RIGHT)
|
||||
editorDelChar(ed)
|
||||
elseif c == ctrl_key('l')
|
||||
# Refresh screen
|
||||
return
|
||||
elseif c == UInt32('\x1b')
|
||||
return
|
||||
elseif iscntrl(Char(c)) && isKeyBound(Char(c))
|
||||
runCommand(ed, getKeyBinding(Char(c)))
|
||||
elseif c == UInt32('\t')
|
||||
editorInsertTab(ed)
|
||||
elseif !iscntrl(Char(c))
|
||||
editorInsertChar(ed, c)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
#####################
|
||||
# Editor Operations #
|
||||
#####################
|
||||
|
||||
function editorInsertChar(ed::Editor, c::UInt32)
|
||||
# The cursor is able to move beyond the last row
|
||||
ed.csr.y == length(ed.rows)+1 && appendRow(ed.rows, "")
|
||||
insertChar!(ed.rows[ed.csr.y], ed.csr.x, Char(c))
|
||||
ed.csr.x += 1
|
||||
ed.dirty = true
|
||||
end
|
||||
|
||||
function editorInsertTab(ed::Editor)
|
||||
# The cursor is able to move beyond the last row
|
||||
ed.csr.y == length(ed.rows)+1 && appendRow(ed.rows, "")
|
||||
|
||||
# Insert character(s) into the row data
|
||||
mv_fwd = insertTab!(ed.rows[ed.csr.y], ed.csr.x)
|
||||
|
||||
ed.csr.x += mv_fwd
|
||||
|
||||
ed.dirty = true
|
||||
end
|
||||
|
||||
|
||||
function editorDelChar(ed::Editor)
|
||||
ed.csr.y == length(ed.rows)+1 && return
|
||||
ed.csr.x == 1 && ed.csr.y == 1 && return
|
||||
|
||||
if ed.csr.x > 1
|
||||
deleteChar!(ed.rows[ed.csr.y], ed.csr.x -1)
|
||||
ed.csr.x -= 1
|
||||
ed.dirty = true
|
||||
else
|
||||
# Move cursor to end of prev line
|
||||
ed.csr.x = 1+length(ed.rows[ed.csr.y-1].chars)
|
||||
appendRowString(ed.rows[ed.csr.y-1], ed.rows[ed.csr.y].chars)
|
||||
editorDelRow(ed, ed.csr.y)
|
||||
ed.csr.y -= 1
|
||||
end
|
||||
end
|
||||
|
||||
function editorInsertRow(ed::Editor, i::Int, str::String)
|
||||
row = Row(str)
|
||||
update!(row)
|
||||
insert!(ed.rows, i, row)
|
||||
end
|
||||
|
||||
function editorInsertNewline(ed::Editor)
|
||||
if ed.csr.x == 1
|
||||
editorInsertRow(ed, ed.csr.y, "")
|
||||
else
|
||||
row = ed.rows[ed.csr.y]
|
||||
before = row.chars[1:ed.csr.x-1]
|
||||
after = row.chars[ed.csr.x:end]
|
||||
editorInsertRow(ed, ed.csr.y + 1, after)
|
||||
row.chars = before
|
||||
update!(row)
|
||||
end
|
||||
ed.csr.y += 1
|
||||
ed.csr.x = 1
|
||||
end
|
||||
|
||||
function editorDelRow(ed::Editor, i::Int)
|
||||
i < 1 || i > length(ed.rows) && return
|
||||
deleteat!(ed.rows, i)
|
||||
ed.dirty = true
|
||||
end
|
||||
|
||||
function editorQuit(ed::Editor; force::Bool=false)
|
||||
if !force && ed.dirty
|
||||
setStatusMessage(ed,
|
||||
"File has unsaved changes. Save changes of use 'quit !' to quit anyway")
|
||||
else
|
||||
write(STDOUT, "\x1b[2J")
|
||||
write(STDOUT, "\x1b[H")
|
||||
ed.quit = true
|
||||
!isinteractive() && exit(0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
############
|
||||
# COMMANDS #
|
||||
############
|
||||
|
||||
|
||||
function runCommand(ed::Editor)
|
||||
cmd = editorPrompt(ed, "> ")
|
||||
runCommand(ed, strip(cmd))
|
||||
end
|
||||
|
||||
function runCommand(ed::Editor, command_str::String)
|
||||
cmd_arr = split(command_str, ' ', limit=2)
|
||||
|
||||
# Get the command
|
||||
cmd = strip(cmd_arr[1])
|
||||
|
||||
# Blank, do nothing
|
||||
cmd == "" && return
|
||||
|
||||
# Command must be a valid identifier
|
||||
if !Base.isidentifier(cmd)
|
||||
setStatusMessage(ed, "'$sym' is not a valid command name")
|
||||
return
|
||||
end
|
||||
|
||||
cmd_sym = Symbol(cmd)
|
||||
|
||||
# Get arguments if there are any
|
||||
args = ""
|
||||
if length(cmd_arr) > 1
|
||||
args = cmd_arr[2]
|
||||
end
|
||||
|
||||
# If the command exists, run it
|
||||
if cmd_sym in keys(COMMANDS)
|
||||
# join(args): convert Substring to String
|
||||
runCommand(COMMANDS[cmd_sym], ed, join(args))
|
||||
else
|
||||
setStatusMessage(ed, "'$sym' is not a valid command")
|
||||
end
|
||||
end
|
||||
|
||||
function runCommand(c::Command, ed::Editor, args::String)
|
||||
c.cmd(ed, args)
|
||||
end
|
|
@ -0,0 +1,160 @@
|
|||
#############
|
||||
# ROW #
|
||||
#############
|
||||
|
||||
""" Holds file true data and render data """
|
||||
mutable struct Row
|
||||
# file data, a row of text
|
||||
chars::String
|
||||
# a row of text as rendered on screen
|
||||
render::String
|
||||
end
|
||||
|
||||
Row(s::String) = Row(s, "")
|
||||
|
||||
""" Update the `render` string using the `chars` string """
|
||||
function update!(row::Row)
|
||||
# Count the number of tabs
|
||||
tabs = 0
|
||||
for c in row.chars
|
||||
c == '\t' && (tabs += 1)
|
||||
end
|
||||
|
||||
# Allocate an array of characters
|
||||
updated = Array{Char, 1}(length(row.chars) + tabs*(configGet(:tab_stop)-1))
|
||||
|
||||
# copy the characters into the updated array
|
||||
idx = 1
|
||||
for i in 1:length(row.chars)
|
||||
|
||||
# replace tabs with spaces
|
||||
if row.chars[i] == '\t'
|
||||
updated[idx] = ' '
|
||||
idx += 1
|
||||
while idx % configGet(:tab_stop) != 0;
|
||||
updated[idx] = ' ';
|
||||
idx += 1
|
||||
end
|
||||
else
|
||||
updated[idx] = row.chars[i]
|
||||
idx += 1
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
row.render = join(updated[1:idx-1])
|
||||
end
|
||||
|
||||
"""Convert a cursor position in the document to a rendered cursor position"""
|
||||
function renderX(row::Row, cx::Int)
|
||||
rx = 1
|
||||
for i in 1:cx-1
|
||||
# If there is a tab, move the cursor forward to the tab stop
|
||||
if row.chars[i] == '\t'
|
||||
rx += (configGet(:tab_stop) - 1) - (rx % configGet(:tab_stop))
|
||||
end
|
||||
rx += 1
|
||||
end
|
||||
rx
|
||||
end
|
||||
|
||||
|
||||
"""opposite of renderX: Convert a render position to a chars position"""
|
||||
function charX(row::Row, rx::Int)
|
||||
cur_rx = 1
|
||||
for cx = 1:length(row.chars)
|
||||
if row.chars[cx] == '\t'
|
||||
cur_rx += (configGet(:tab_stop) - 1) - (cur_rx % configGet(:tab_stop))
|
||||
end
|
||||
cur_rx += 1
|
||||
|
||||
cur_rx > rx && return cx
|
||||
end
|
||||
cx
|
||||
end
|
||||
|
||||
|
||||
""" Insert a char into a string """
|
||||
function insert(s::String, i::Int, c::Union{Char,String})
|
||||
if s == ""
|
||||
string(c)
|
||||
else
|
||||
string(s[1:i-1], c, s[i:end])
|
||||
end
|
||||
end
|
||||
|
||||
""" Delete char from string """
|
||||
function delete(s::String, i::Int)
|
||||
i < 1 || i > length(s) && return s
|
||||
string(s[1:i-1], s[i+1:end])
|
||||
end
|
||||
|
||||
""" Insert a char into the row at a given location """
|
||||
function insertChar!(row::Row, i::Int, c::Char)
|
||||
row.chars = insert(row.chars, i, c)
|
||||
update!(row)
|
||||
end
|
||||
|
||||
""" Insert a tab into the row at a given location. Return the number of chars inserted """
|
||||
function insertTab!(row::Row, i::Int)
|
||||
num_chars = 1
|
||||
t = '\t'
|
||||
|
||||
# If we are using spaces, move ahead more
|
||||
if configGet(:expandtab)
|
||||
num_chars = configGet(:tab_stop) - (i % configGet(:tab_stop))
|
||||
t = repeat(" ", num_chars)
|
||||
end
|
||||
|
||||
row.chars = insert(row.chars, i, t)
|
||||
update!(row)
|
||||
|
||||
# Used for positioning the cursor
|
||||
return num_chars
|
||||
end
|
||||
|
||||
|
||||
""" Delete char from row """
|
||||
function deleteChar!(row::Row, i::Int)
|
||||
row.chars = delete(row.chars, i)
|
||||
update!(row)
|
||||
end
|
||||
|
||||
"""
|
||||
Add a row to the end of the document
|
||||
initialize the row with the given string
|
||||
"""
|
||||
function appendRowString(row::Row, str::String)
|
||||
row.chars = string(row.chars, str)
|
||||
update!(row)
|
||||
end
|
||||
|
||||
|
||||
|
||||
########
|
||||
# ROWS #
|
||||
########
|
||||
|
||||
""" type alias for Array{Row, 1} """
|
||||
Rows = Array{Row, 1}
|
||||
|
||||
""" delete all rows from a Rows """
|
||||
function clearRows(rows::Rows)
|
||||
while length(rows) > 0
|
||||
pop!(rows)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
""" Add a row to the end of the document """
|
||||
function appendRow(rows::Rows, s::String)
|
||||
row = Row(s)
|
||||
update!(row)
|
||||
push!(rows, row)
|
||||
end
|
||||
|
||||
|
||||
""" Convert all ROW data to a single string """
|
||||
function rowsToString(rows::Rows)
|
||||
join(map(row -> row.chars, rows), '\n')
|
||||
end
|
|
@ -0,0 +1,127 @@
|
|||
import Base.==
|
||||
|
||||
@enum(Key,
|
||||
BACKSPACE = 127,
|
||||
ARROW_LEFT = 1000,
|
||||
ARROW_RIGHT,
|
||||
ARROW_UP,
|
||||
ARROW_DOWN,
|
||||
DEL_KEY,
|
||||
HOME_KEY,
|
||||
END_KEY,
|
||||
PAGE_UP,
|
||||
PAGE_DOWN,
|
||||
S_ARROW_UP,
|
||||
S_ARROW_DOWN,
|
||||
S_ARROW_LEFT,
|
||||
S_ARROW_RIGHT,
|
||||
C_ARROW_UP,
|
||||
C_ARROW_DOWN,
|
||||
C_ARROW_LEFT,
|
||||
C_ARROW_RIGHT)
|
||||
|
||||
==(c::UInt32, k::Key) = c == UInt32(k)
|
||||
==(k::Key, c::UInt32) = c == UInt32(k)
|
||||
==(c::Char, k::Key) = UInt32(c) == UInt32(k)
|
||||
==(k::Key, c::Char) = UInt32(c) == UInt32(k)
|
||||
|
||||
ctrl_key(c::Char)::UInt32 = UInt32(c) & 0x1f
|
||||
|
||||
readNextChar() = Char(read(STDIN,1)[1])
|
||||
|
||||
function readKey() ::UInt32
|
||||
c = readNextChar()
|
||||
|
||||
# Escape characters
|
||||
if c == '\x1b'
|
||||
STDIN.buffer.size < 3 && return '\x1b'
|
||||
esc_a = readNextChar()
|
||||
esc_b = readNextChar()
|
||||
|
||||
if esc_a == '['
|
||||
if esc_b >= '0' && esc_b <= '9'
|
||||
STDIN.buffer.size < 4 && return '\x1b'
|
||||
esc_c = readNextChar()
|
||||
|
||||
if esc_c == '~'
|
||||
if esc_b == '1'
|
||||
return HOME_KEY
|
||||
elseif esc_b == '4'
|
||||
return END_KEY
|
||||
elseif esc_b == '3'
|
||||
return DEL_KEY
|
||||
elseif esc_b == '5'
|
||||
return PAGE_UP
|
||||
elseif esc_b == '6'
|
||||
return PAGE_DOWN
|
||||
elseif esc_b == '7'
|
||||
return HOME_KEY
|
||||
elseif esc_b == '8'
|
||||
return END_KEY
|
||||
else
|
||||
return '\x1b'
|
||||
end
|
||||
elseif esc_c == ';'
|
||||
STDIN.buffer.size < 6 && return '\x1b'
|
||||
esc_d = readNextChar()
|
||||
esc_e = readNextChar()
|
||||
|
||||
if esc_d == '2'
|
||||
# shift + arrorw
|
||||
if esc_e == 'A'
|
||||
return S_ARROW_UP
|
||||
elseif esc_e == 'B'
|
||||
return S_ARROW_DOWN
|
||||
elseif esc_e == 'C'
|
||||
return S_ARROW_RIGHT
|
||||
elseif esc_e == 'D'
|
||||
return S_ARROW_LEFT
|
||||
else
|
||||
return '\x1b'
|
||||
end
|
||||
elseif esc_d == '5'
|
||||
# Ctrl + arrow
|
||||
if esc_e == 'A'
|
||||
return C_ARROW_UP
|
||||
elseif esc_e == 'B'
|
||||
return C_ARROW_DOWN
|
||||
elseif esc_e == 'C'
|
||||
return C_ARROW_RIGHT
|
||||
elseif esc_e == 'D'
|
||||
return C_ARROW_LEFT
|
||||
else
|
||||
return '\x1b'
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
# Arrow keys
|
||||
if esc_b == 'A'
|
||||
return ARROW_UP
|
||||
elseif esc_b == 'B'
|
||||
return ARROW_DOWN
|
||||
elseif esc_b == 'C'
|
||||
return ARROW_RIGHT
|
||||
elseif esc_b == 'D'
|
||||
return ARROW_LEFT
|
||||
elseif esc_b == 'H'
|
||||
return HOME_KEY
|
||||
elseif esc_b == 'F'
|
||||
return END_KEY
|
||||
else
|
||||
return '\x1b'
|
||||
end
|
||||
end
|
||||
elseif esc_a == 'O'
|
||||
if esc_a == 'H'
|
||||
return HOME_KEY
|
||||
elseif esc_a == 'F'
|
||||
return END_KEY
|
||||
end
|
||||
end
|
||||
|
||||
return '\x1b'
|
||||
else
|
||||
return c;
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue