Ele: Extendable Lua Editor
Ele is an (In Development) Extendable Lua Editor written to be easy
to understand, extend and modify. It is the primary editor and shell for the
Civstack project, and also the core framework for the devleopment of
Civstack's text-based learning games.
To install: follow civ.html#install, ele will be installed by default.
Architecture
Ele is architected using the MVI (model-view-intent) architecture, also
known as the "React architecture" from the web library of the same
name.
,_____________________________________________
| intent(): keyboard, timer, executor, etc |
`~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
/\ || Data + events
|| Data + scheduled \/
,__________________ Data + scheduled ,____________________________
| view(): paint | <================ | model(): keybind, actions |
`~~~~~~~~~~~~~~~~~~' `~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
In practice, this is accomplished in four coroutines spawned
by ele.lua's main function:
- A coroutine that listens to stdin for vt100 key sequences
and sends event (plain-old-table values) to the keys
channel.
- A coroutine that listens to key actions and converts them
to events (again, plain-old-tables) based on the
Mod ele.bindings
,
which are sent on an events channel. This also handles
chords correctly, see the Mod ele.bindings
documentation.
- A coroutine that listens for event tables, looks up their
action in
Mod ele.actions
and runs the appropriate action.
- A coroutine that "draws" the current display once per "frame"
This is done by recursing the tree from Editor.root down,
having them write the relevant text to a vt100.Term object,
which gets flushed to the display at the end.
This roughly implements the MVI architecture because ALL actions
are performed sequentially based on the ordering in the events
channel.
Actions or plugins have the option to spawn their own coroutine. However,
this behavior should be extremely rare, and reserved mostly for things that
really can happen concurrently with no user feedback needed, such as saving a
file, finding file lints, or updating syntax highlighting. Most real-world
editor operations can block the user while they happen, and if they can't
then they should consider not being included as an editor operation. Some
exceptions such as searching for patterns in a recursive tree should be
spawned as a coroutine but be cancelled if the user modifies the buffer
that the results are being written to in any way.
Adding Bindings
Adding simple bindings is easy. Simply insert the space-separated chord
of keys you want to go to your binding to one of ele's default modes, or
create your own.
Mod ele.bindings
's
command,
insert and/or
system
entries are where you will find the default modes. For instance,
the following will insert the expected text at the cursor position
from command mode:
local B = require'ele.bindings'
B.command['y y'] = {action='insert', 'Why oh why did I bind this?\n'},
To write your own action you must:
- Add a callable to
Mod ele.actions
which implements the intended behavior. The
signature is: fn(Editor, event, Channel[event]). You may
modify the Editor in the appropriate way for your action. You are
recommended to handle event.times to do your action multiple times (if
that makes sense for your action). Note that you are free to throw errors --
any errors will be caught and logged.
- Add a binding to an editor mode. The binding can either be a plain table,
which is the event that will be generated, or a fn(KeySt) -> ev? callable.
Using the callable API is slightly complex but allows for complex key
interactions where you build-up a command with a chaing (aka "chord") of
multiple key inputs. Refer to the
Mod ele.bindings
documentation.
Editor API
ele.Editor is the main object most custom actions or Ele scripts will
interact with. It has several fields, but the fields and methods that most
folks will care about are:
- edit: this is the current edit buffer and is typically where the
user wants to insert or otherwise interact with text. It is
typically a
Record Edit
instance, though plugins may
eventually allow other types to be used.
- view this is the "root" view, which contains a tree who's
leaves are the visible Edit views.
- ext a plain table that extensions can set Edit-local values
too. Very useful for plugins.
- :buffer(idOrPath) --> Buffer this will get or create a buffer.
- :focus(idOrPath) --> Edit focus on a buffer, replacing
the current one.
Most plugins will simply get edit and then insert/remove/search its buf
data using its APIs and/or change its l,c (line,column) values. They are
free to use any lua API to do so, but should avoid large amounts
of work as much as possible.
Usage:
ele path/to/file.txt
The ele commandline editor.
Types: VSplit HSplit
Functions
A container with windows split vertically (i.e. tall windows)
Fields:
Methods
A container with windows split horizontally (i.e. wide windows)
Fields:
Methods
Types: KeySt KeyBindings
Functions
The state of the keyboard input (chord).
Some bindings are a simple action to perform, whereas callable bindings
can update the KeySt to affect future ones, such as decimals causing
later actions to be repeated a
num of times.
Fields:
- chord
list of keys which led to this binding, i.e. {'space', 'a'}
- event
table to use when returning (emitting) an event.
- next
the binding which will be used for the next key
- keep
if true the above fields will be preserved in next call
Methods
- fn:check(ele) -> errstring?
Check the current Key State.
A map of key -> binding.
The name and doc can be provided for the user.
Other "fields" must be valid chords. They will be automatically
split (by whitespace) to create sub-KeyBindings as-needed.
The value must be one of:
- KeyBindings instance to explicitly create chorded bindings.
- plain event table to fire off a simple event
- callable event(ev, keySt) for more complex bindings.
Fields:
- name
the name of the group for documentation
- doc
documentation to display to the user
Methods
- fn getBinding(name)
Create a parser spec record. These have the fields kind and name
and must define the parse method.
Types: Edit
Fields:
Methods
Types: nav
Functions
- fn keyinput(ed, ev, evsend)
- fn merge(ed, ev)
- fn chain(ed, ev, evsend)
- fn move(ed, ev)
- fn getMove(ed, ev) -> l1,c1, l2,c2
Get the start/end of a move without changing
the position.
- fn insert(ed, ev, evsend)
- fn insertTab(ed, ev)
- fn backspace(ed, ev)
- fn autoIndent(ed, ev)
- fn yank(ed, ev)
yank movement action.
- fn paste(ed, ev)
Paste from an index (default=1) from the yank stream.
The index goes from last -> first, so 1 is the most recent yank.
- fn remove(ed, ev)
- fn searchBuf(ed, ev)
- fn path(ed, ev, evsend)
- fn edit(ed, ev)
Do something with the edit view, in this order:
- save=true: save the current edit view.
- focus: focus the buffer, typically 'b#named'.
- clear: clear the current edit view.
- fn buf(ed, ev)
used for the overlay.
- fn window(ed, ev)
Window operations like split and close
Functions
- fn getFocus(line)
- fn getEntry(line) -> (indent, kind, entry)
- fn findParent(b, l) -> linenum, line
Find the parent of current path entry
if isFocus the entry will be the focus (and ind will be 0)
- fn findFocus(b, l) -> linenum, line
Find the focus path line num (i.e. the starting directory)
- fn findEnd(b, l) -> linenum
Find the last line of the focus's entities (or itself).
invariant: line l is an entry or focus.
- fn findView(b, l) -> (fln, eln, fline)
Find the view (focusLineNum, endLineNum, focusLine)
- fn getPath(b, l,c) -> string
Walk up the parents, getting the full path.
If not an entry, try to find the path from the column.
- fn findEntryEnd(b, l) -> linenum
- fn backFocus(b, l)
- fn backEntry(b, l) -> ln
Go backwards on the entry, returning the new line
For focus, this will go back one component.
For entry, this will collapse parent (and move to it).
- fn expandEntry(b, l, ls) -> numEntries
- fn doBack(b, l, times)
- fn doExpand(b, l, times, ls)
- fn goPath(ed, create)
go to path at l,c. If op=='create' then create the path
- fn doEntry(ed, op, times, ls)
perform the entry operation
Fields:
- s
- mode
current editor mode
- modes
keyboard bindings per mode (see: bindings.lua)
- actions
actions which events can trigger (see: actions.lua)
- resources
resources to close when shutting down
- buffers
- bufferId
- namedBuffers
- overlay
the overlay buffer
- edit
the current edit buffer. Also in namedBuffers.overlay
- view
the root view
- display
display/terminal to write+paint text
- run =true
set to false to stop the app
- ext
table for extensions to store data
- search
search pattern for searchBuf, etc
- lastEvent
the last event executed.
- yank
a deque of removed text. See yankMax.
- error
error handler (ds.log.logfmt sig)
- warn
warn handler
- newDat =function() instance
function to create new buffer
- redraw
set to true to force a redraw
Methods
- fn:getEditor()
- fn:init()
- fn:getBuffer(v) -> Buffer?
Get an existing buffer if it exists.
Else return false if the buffer is path-like and should be
created, else nil.
- fn:buffer(idOrPath) -> Buffer
- fn:namedBuffer(name, path)
Get or create a named buffer (NOT a path).
- fn:open(path) -> edit
- fn:draw()
- fn:handleStandard(ev)
Handle standard event fields.
Currently this only handles the mode field.
- fn:replace(from, to) -> from
Replace the view/edit from with to.
Since Editor supports only self.view this means
it must be that value.
- fn:remove(v) -> v
Remove a view and remove self as it's container.
This does NOT close the view.
- fn:focusFirst(c)
Focus the first edit view in container c (default self.view)
- fn:focus(b) -> Edit
Replace the current edit view with the new self:buffer(b).
Return the new edit view being focused.
- fn:close()
Fields:
Methods
Functions