diff --git a/src/Acorn.jl b/src/Acorn.jl index e25e2c3..dfc4906 100644 --- a/src/Acorn.jl +++ b/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 diff --git a/src/EditorConfig.jl b/src/EditorConfig.jl new file mode 100644 index 0000000..13d847f --- /dev/null +++ b/src/EditorConfig.jl @@ -0,0 +1,133 @@ +module EditorConfig + + +############### +## Parameter ## +############### + +type Parameter{T} + value::T + validate::Union{Function, Void} + desc::String # Used when calling help +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 ") + + +########################## +## 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 diff --git a/src/cmds/.quit.jl.swp b/src/cmds/.quit.jl.swp new file mode 100644 index 0000000..add5738 Binary files /dev/null and b/src/cmds/.quit.jl.swp differ diff --git a/src/cmds/Command.jl b/src/cmds/Command.jl new file mode 100644 index 0000000..652f029 --- /dev/null +++ b/src/cmds/Command.jl @@ -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 diff --git a/src/cmds/bind.jl b/src/cmds/bind.jl new file mode 100644 index 0000000..69bf321 --- /dev/null +++ b/src/cmds/bind.jl @@ -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 : bind ctrl- to command ") diff --git a/src/cmds/echo.jl b/src/cmds/echo.jl new file mode 100644 index 0000000..ec5efe8 --- /dev/null +++ b/src/cmds/echo.jl @@ -0,0 +1,6 @@ +function commandEcho(ed::Editor, args::String) + setStatusMessage(ed, args) +end + +addCommand(:echo, commandEcho, + help="echo : set the status message to ") diff --git a/src/cmds/find.jl b/src/cmds/find.jl new file mode 100644 index 0000000..a076a2e --- /dev/null +++ b/src/cmds/find.jl @@ -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, to get cursor, arrow keys to next/prev") diff --git a/src/cmds/help.jl b/src/cmds/help.jl new file mode 100644 index 0000000..94245eb --- /dev/null +++ b/src/cmds/help.jl @@ -0,0 +1,31 @@ +function commandHelp(ed::Editor, args::String) + if args == "" + setStatusMessage(ed, "Type help 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 for command specific help") diff --git a/src/cmds/open.jl b/src/cmds/open.jl new file mode 100644 index 0000000..69295c4 --- /dev/null +++ b/src/cmds/open.jl @@ -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") diff --git a/src/cmds/quit.jl b/src/cmds/quit.jl new file mode 100644 index 0000000..ed6f638 --- /dev/null +++ b/src/cmds/quit.jl @@ -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") diff --git a/src/cmds/save.jl b/src/cmds/save.jl new file mode 100644 index 0000000..e6059ef --- /dev/null +++ b/src/cmds/save.jl @@ -0,0 +1,6 @@ +function commandSave(ed::Editor, args::String) + editorSave(ed, args) +end + +addCommand(:save, commandSave, + help="save: save the file") diff --git a/src/cmds/set.jl b/src/cmds/set.jl new file mode 100644 index 0000000..b9bfa13 --- /dev/null +++ b/src/cmds/set.jl @@ -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 : set the given parameter to a value") diff --git a/src/editor.jl b/src/editor.jl new file mode 100644 index 0000000..3914fac --- /dev/null +++ b/src/editor.jl @@ -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 diff --git a/src/row.jl b/src/row.jl new file mode 100644 index 0000000..58b2610 --- /dev/null +++ b/src/row.jl @@ -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 diff --git a/src/terminal.jl b/src/terminal.jl new file mode 100644 index 0000000..1c1f3c9 --- /dev/null +++ b/src/terminal.jl @@ -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