Ele: Extendable Lua Editor
Ele is an 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.
Learn Ele in Y Minutes
To learn ele (or vim for that matter) just install civstack and run:
ele cmd/ele/README.cxt
Pressing ^q ^q (cntrl+q twice) will exit ele from any mode.
You can move the cursor with the keys h j k l which are mapped to
h=left j=down k=up l=right (yes they are weird but also easy to access).
The top left will say mode:command, meaning you are in command mode.
In command mode, ele's key bindings are in a memonic "language" that is relatively
quick to learn but does take some effort. Stringing multiple keys together is
called a "chord", and the language of chords are typically of the form:
* amount: for instance 2.
* action/verb: for instance delete.
* noun/movement: for instance word.
So typing 2 d w will delete two words. Typing actions twice tends to trigger
a specialized action, for instance d d is delete line and 4 d d deletes
four lines. Capital letters are also typically special, e.g. D deletes from
the cursor to the end of the line.
You can press ? ? at any point to see what keys are available. You can
also type ? <key> to see what that key would have done in that mode.
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.
Arguments:
- run
lua module to call at start
Core ele types.
Types: BufState PaneState State BasePane VSplit HSplit EditLoc
Functions
Cached buffer state
Fields:
Cached window/pane state.
Fields:
- ty
the type to ds.wantpath, i.e. "ele.edit.Edit"
- dat
the data to pass to the ty
- chld
children
Methods
Editor state for caching/reloading the current
editor state.
Fields:
The base record for Edit/Game.
These should be implemented:
function M.BasePane:state() --> PaneState
function M.BasePane.fromState(T, ed, s) --> new self
Fields:
- id
- container
- actions
override of actions, especially keyinput
- hide =true
whether to hide cursor
- l =1
cursor line
- c =1
cursor col
- tl =-1
- tc =-1
- th =-1
- tw =-1
- closed =false
- modes
override specific keybindings for this pane
Methods
A container with windows split vertically (i.e. tall windows)
Fields:
Methods
A container with windows split horizontally (i.e. wide windows)
Fields:
Methods
The location the cursor was at, to be in a stack.
Fields:
Methods
- fn parse(T, str, defaultBuf)
parses l.c:b
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
- save
saved binding, only used for help
- 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
Types: Edit
Ele Edit View for viewing and editing text files in a pane.
Fields:
- id
- container
- actions
override of actions, especially keyinput
- hide =true
whether to hide cursor
- l =1
cursor line
- c =1
cursor col
- tl =-1
- tc =-1
- th =-1
- tw =-1
- closed =false
- modes
override specific keybindings for this pane
- box
whether visual mode is in "box" mode.
- ol
origin line
- oc
origin col
- vl =1
- vc =1
- buf
- yank
global yank deque
- locations
a deq of locations visited.
- lineStyle ="bar:line"
asciicolor style
Methods
- fn:close(ed)
- fn:drawCursor(ed)
- fn:save(ed)
- fn:copy()
- fn:get(l)
- fn:curLine(self.l)
- fn:colEnd()
- fn:lastLine()
- fn:offset(off)
- fn:selected(l2,c2) -> iter[l,c, l2,c2]
Get iterator of selection. If box is used, multiple distinct
lines will be returned from bottom -> top.
- fn:boundC(l,c)
- fn:boundLC(l, c)
- fn:boundCol(c, l)
- fn:viewCursor()
- fn:changeStart()
- fn:changeUpdate2()
- fn:append(msg)
- fn:insert(s, l,c)
Insert text at l,c or default self.(l,c)
Updates the cursor position only if l,c is nil or exactly matches
self.(l,c).
- fn:remove(...)
- fn:removeOff(off, l, c)
- fn:replace(s, ...)
- fn:clear()
Clear the buffer.
- fn:undo()
- fn:redo()
- fn:draw(ed, isRight)
- fn:barDims()
- fn:drawBars(d) -> botHeight, leftWidth
- fn:split(S) -> split
Split the edit by wrapping it and a copy into split type S.
Return the resulting split.
- fn:autoIndent()
- fn:path() -> path?
- fn:state()
- fn fromState(T, ed, s)
Types: nav
Functions
Functions
- fn getFocus(line)
- fn getBuffer(line)
- fn getEntry(line) -> (indent, kind, entry)
- fn getArgs(b)
Return the arguments for navigation at the top of the file.
- 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 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 findEnd(b, l) -> linenum, maxChildInd
find the last line of a focus or entry.
- fn backFocus(ed, b, l)
- fn backEntry(ed, 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(ed, b, l) -> numExpanded
- fn doBack(ed, b, l, times)
- fn doExpand(ed, b, l, times)
- fn goPath(ed, create)
go to path at l,c. If op=='create' then create the path
- fn doEntry(ed, op, times)
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
- pane
the currently active pane.
- 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
- listeners
list of functions to call for each successful event
- yank
a deque of removed text. See yankMax.
- bindings
set to bindings.lua
- error
error handler (ds.log.logfmt sig)
- warn
warn handler
- newDat =function() instance
function to create new buffer
- navLs =function() instance
ls used for nav
- redraw
set to true to force a redraw
Methods
- fn:getEditor()
- fn:init()
- fn:edit()
- fn:bufferName(b) -> string
- fn:currentLocation(e)
- fn:pushLocation(e) -> pushed
- 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.
Special buffers:
- b#tmp creates a new buffer pointing to a tmp file.
- fn:buffer(idOrPath) -> Buffer
- fn:namedBuffer(name, path)
Get or create a named buffer.
- 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(p) -> Edit
Replace the current edit view with the new self:buffer(b).
Return the new edit view being focused.
- fn:close()
- fn:state() -> ele.types.State
- fn:loadState(st) -> self
- fn:rmTmp()
Cleanup all temporary files. Used for tests.
Fields:
Methods
Ele game library.
Types: Game
A Game window which renders a list of sprites.
Fields:
- id
- container
- actions
override of actions, especially keyinput
- hide =true
whether to hide cursor
- l =1
cursor line
- c =1
cursor col
- tl =-1
- tc =-1
- th =-1
- tw =-1
- closed =false
- modes
override specific keybindings for this pane
- sprites
list of sprites to render.
sprites are written first -> last (last wins).
- mh
minimum height
- mw
minimum width
Methods
Functions