initial code commit

master
Nick Paul 2017-08-03 16:41:59 -04:00
parent de2769b9fa
commit a90b90e711
15 changed files with 1226 additions and 1 deletions

View File

@ -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

133
src/EditorConfig.jl Normal file
View File

@ -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

BIN
src/cmds/.quit.jl.swp Normal file

Binary file not shown.

11
src/cmds/Command.jl Normal file
View File

@ -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

21
src/cmds/bind.jl Normal file
View File

@ -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>")

6
src/cmds/echo.jl Normal file
View File

@ -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>")

86
src/cmds/find.jl Normal file
View File

@ -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")

31
src/cmds/help.jl Normal file
View File

@ -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")

10
src/cmds/open.jl Normal file
View File

@ -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")

6
src/cmds/quit.jl Normal file
View 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")

6
src/cmds/save.jl Normal file
View File

@ -0,0 +1,6 @@
function commandSave(ed::Editor, args::String)
editorSave(ed, args)
end
addCommand(:save, commandSave,
help="save: save the file")

36
src/cmds/set.jl Normal file
View 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")

536
src/editor.jl Normal file
View File

@ -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

160
src/row.jl Normal file
View File

@ -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

127
src/terminal.jl Normal file
View File

@ -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