-
Notifications
You must be signed in to change notification settings - Fork 126
Lite
Lite is a full-featured extensible text editor is written in around 4.4kloc of Lua and 1kloc of C. It's built with a slim core and a flexible API so that users can extend it and add features by writing their own Lua code. Like most programs that embed Lua, it's easy to extend using Fennel too!
First we want to install the Fennel plugin for Lite so we can at least
get syntax highlighting. Clone the
lite-plugins repo and copy
plugins/language_fennel.lua
to the data/plugins
directory. This
isn't strictly necessary, but it sure makes things a lot nicer.
One of the main shortcomings of Lite is that it doesn't ship with any
facility for reloading your config. Let's see how tricky this would be
to add using Fennel! We start by opening up the data/user/init.lua
file which is loaded by Lite but is intended to include user config.
We can put just the minimum amount of code here needed to bootstrap
the real config in a separate init.fnl
file after copying
fennel.lua
into the data/plugins
directory.
local fennel = require("plugins.fennel")
table.insert(package.loaders or package.searchers, fennel.searcher)
require("data.user.config")
Now we can put all the actual config in data/user/config.fnl
:
(local command (require :core.command))
(local fennel (require :plugins.fennel))
(command.add nil {:user:reinit #(fennel.dofile "data/user/config.fnl")})
The nil
here means that the commands we're adding are always
available. We could instead provide a predicate function to determine
whether the command is applicable in a given context. Unfortunately
Lite doesn't really have a concept of first-class modes; predicates
are the only way to group commands together, and they're not as useful
as a table would be. Oh well.
Anyway now we can access the "User: Reinit" command by pressing
ctrl-shift-p
. (Unfortunately we have to quit and launch Lite again
still at this point before the command is available.) Let's try it!
data/core/command.lua:19: command already exists: user:reinit
Oh dear. It appears Lite really doesn't like reloading, does it. Well, it's all just Lua; let's take a look at the code where that error comes from:
function command.add(predicate, map)
predicate = predicate or always_true
if type(predicate) == "string" then
predicate = require(predicate)
end
if type(predicate) == "table" then
local class = predicate
predicate = function() return core.active_view:is(class) end
end
for name, fn in pairs(map) do
assert(not command.map[name], "command already exists: " .. name)
command.map[name] = { predicate = predicate, perform = fn }
end
end
That assert
near the end is the thing that's causing the
trouble. Apparently it wants to make sure the command being added
doesn't already exist! Well that's quite silly; it completely defeats
the purpose of reloading. But we can work around it from our Fennel
code since the table containing the commands is exposed to us:
(local command (require :core.command))
(local fennel (require :fennel))
(tset command.map :user:reinit k nil)
(command.add nil {:user:reinit #(fennel.dofile "data/user/config.fnl")})
Now that's more like it! But it's still quite limited; it only lets us reload a single file. What we really want is a REPL, a Read Eval Print Loop. Unfortunately a REPL doesn't really fit well into Lite's model of how buffers and files work, but ... could we settle for a REP instead? A command which reads, evaluates, and prints, well that's a decent approximation. Let's give it a try.
We're going to use the power of coroutines to take Fennel's standard
REPL and tie it into our Lite REP command. Let's create a coroutine
that wraps the fennel.repl
function:
(local repl (coroutine.create fennel.repl))
Once we have our coroutine, we can start it by using
coroutine.resume
and passing in the options we'll use to tie into
Lite. (The arguments to the first call to coroutine.resume
are the
same arguments we would have passed to fennel.repl
directly if we
had run it outside a coroutine.) Three things are needed in the
options table: a readChunk
function to give us strings of input (in
a normal repl this is basically just io.read
), onValues
to display
normal values and onError
to display errors.
Lite provides the core.log
and core.error
functions for these
latter two which just display the string to the bottom message
area. Our readChunk
function is just coroutine.yield
which we'll
explain soon:
(local {: log : error &as core} (require :core))
(coroutine.resume repl {:readChunk coroutine.yield
:onValues #(log (table.concat $...))
:onError #(error (table.concat $...))})
So what exactly do we have here? Well, the standard Fennel repl (the
same thing you get when you run fennel
in your shell) is now running
inside a coroutine such that it gets its input from coroutine.yield
and writes its output to Lite's log. We started up the repl, and once
it got going, it went to read some input. Since its input function is
coroutine.yield
, that meant that it immediately yielded back to the
original calling code without doing anything. However, just because it
yielded doesn't mean it returned. We can go back to the point at
which it yielded and give it some data so that it can continue.
So if we were to simply run something like this, it would get evaluated by the repl exactly as if we had typed it in during a repl session in the shell:
(coroutine.resume repl "{:math (+ 1 2 (* 3 4))}")
But obviously we don't want canned input, we want to accept input from the user. In order to do that we need to tie into another part of Lite's UI:
(fn handle [input]
(coroutine.resume repl (.. input "\n")))
(fn rep []
(core.command_view:enter :eval handle))
(tset command.map :user:rep nil)
(command.add nil {:user:rep rep})
The core.command_view:enter
function opens up the message area in
the bottom for text input. It takes a prompt string and a handler
function which is given the input as its argument. Then we add it as a
command so it can be invoked using ctrl-shift-p
.
Give it a try! It should show you the results of any expression in the message area.
At this point we no longer need the original user:reinit
command. That's because the repl has built-in reloading
capabilities--this isn't just a barebones repl; this is the same
standard repl that ships with Fennel, and that means it has access to
commands like ,reload user.data.config
which will allow us to do
reloads for any module in the whole system, not just the initial config.
That's just scratching the surface of what you can do when extending Lite with Fennel; there's plenty more you can do.