Ele: Extendable Lua Editor
Ele is an (**In Development**)
Extendable
Lua
Editor.
Ele's primary goals are:
- Implemented in a minimal amount of understandable code. It is the main editor
for Civboot
- Enjoyable and extendable for developers to fit their workflow
- undo/redo, syntax highlighting, plugins, user-configurable bindings
(supports vim/emacs style), etc.
- Implements a lua shell (zsh competitor)
- Can handle any size text file
- Cross platform (currently vt100-only protocol, which should run anywhere)
Non-goals of Ele are:
- Focusing on performance against the other goals. It should be fast enough
to be an editor and should not choke on large files -- otherwise performance
is not important.
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 ele.bindings,
which are sent on an events channel. This also handles
chords correctly, see the ele.bindings documentation.
- A coroutine that listens for event tables, looks up their
action in 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. 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 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 ele.bindings documentation.