- Include some common functions, macros or prelude in all files.
- Write a plugin in fennel.
- Compile into the
lua/
directory. - Use a
.hotpot.lua
file. - Write
init.lua
asinit.fnl
. - Write an config
ftplugin
. - View compiled lua output.
You may set the hotpot.setup({preprocessing = ...})
option to a function that
receives the source code to be compiled and returns it with any alterations.
The function receives the following arguments:
- The fennel source code to be compiled, as a string.
- A table containing:
path
: the path of the file being compiled,modname
: the name of the module being compiled andmacro?
: a boolean indicating whether the file is being compiled as a macro or not.
The function must return the source code to compile.
Note that in some contexts, path
and modname
may be nil, such as when
running fennel code via the API. It's recommended you check the path strictly.
Ex.
(fn [src {: path : modname : macro?}]
(if (and path modname (path:match "config/nvim"))
(let [head (-> (table.concat ["(import-macros {: defmodule} :my.macros)"
"(defmodule %s"] :\n)
(string.format modname))
tail ")"]
(.. head src tail))
;; remember to return the source in other cases
(values src)))
Assuming that you wish to distribute your plugin without requiring a Fennel
loader as a dependency, you can use a .hotpot.lua
file
to build your plugin.
Most plugins will only need to enable the default build
and clean
values,
which will automatically convert all fnl/
files to lua/
except when they
have "macro" in their filename.
Be aware that any settings passed to setup()
are not inherited in the
project directory and must respecified in the .hotpot.lua
file.
-- ~/projects/plugin.nvim/.hotpot.lua
return {
build = true,
-- clean = true
}
By default, Fennels compiler won't show an error if you try to reference unknown symbols. You may prefer to enforce a known list so your builds get "hard errors":
local allowed_globals = {}
for key, _ in pairs(_G) do
table.insert(allowed_globals, key)
end
return {
compiler = {
modules = {
allowedGlobals = allowed_globals
}
},
-- ...
}
For more details, see Using .hotpot.lua
.
See both using a .hotpot.lua
file and writing a
plugin for automated processing, or :h hotpot.api.make
for manual control.
Hotpot can optionally be configured per-project/directory by a .hotpot.lua
file.
This adds support for:
- Defining fennel to lua build rules via glob patterns.
- Automatically building and cleaning lua targets on save.
- Alternative compiler settings per project than those passed to
setup()
.
.hotpot.lua
wraps functionality exposed by
hotpot.api.make
, you can also manually invoke the
automake system by calling hotpot.api.make.auto.build
.
.hotpot.lua
is intended for plugin developers, but you can apply the same practices to your main Neovim config if you want to generatelua/
files or others such ascolors/
etc. See also how to write an init.fnl.
The user:
- Must name the file
.hotpot.lua
. - Must place
.hotpot.lua
in the same directory as thefnl/
directory. .hotpot.lua
must return a lua table, don't forget thereturn
keyword!
The presence of a valid .hotpot.lua
file -- even an empty table -- will
override all hotpot settings passed to setup()
back to their default values,
for fennel files in that directory.
-- ~/projects/my-plugin.nvim/.hotpot.lua
return {
-- build = ...
-- clean = ...
-- compiler = { ... }
}
Note: for performance reasons, the file is lua instead of fennel. You can use
the auto-build feature to compile .hotpot.fnl
to .hotpot.lua
by adding a
{".hotpot.fnl", true}
value to the build
list.
Specify auto-build instructions for the .hotpot.lua
directory. When present,
hotpot will build all fennel files in a project when a fennel file is saved.
See also :h hotpot.api.make.build
for more
details and the underlying API.
See also :h hotpot.api.make.auto.build
to manually
invoke the build rules without opening and saving a file.
Supported values are:
build = false
or build = nil
or the key is omitted
Disable auto-building.
build = true
Enable auto-building with a default value that should be applicable to most
usage. The default value skips any file with macro
in its name and compiles
all other files under fnl/
to lua/
, eg:
{{"fnl/**/*macro*.fnl", false},
{"fnl/**/*.fnl", true}}
build = {{glob_pattern, boolean_or_function}, ...}
or build = {{options}, {glob_pattern, boolean_or_function}, ...}
Each glob pattern is expanded in order and if the boolean value is true, the file will be compiled, if false, the file will be ignored.
If given a function, the absolute path is passed to the function, the function
should return the desired lua path as a string (the extension will be
automatically converted from .fnl
to .lua
), or false.
If a file matches multiple glob patterns, only the first value will be used. This allows earlier, more specific matches to ignore files before broader matches specify compiled files.
Glob patterns that begin with fnl/
are automatically compiled to to lua/
,
other patterns are compiled in place or should be constructing explicitly by a
function.
The first element in the list may also specify options to pass to
hotpot.api.make.build.
such as verbose
or dryrun
. See :h :hotpot.api.make.build
for more details.
Ex.
build = {
{verbose = true, atomic = true},
{"fnl/**/*macro*.fnl", false}, -- dont compile macro files
{"fnl/**/*-test.fnl", false}, -- dont compile x-test.fnl files
{"fnl/**/*.fnl", true}, -- compile all other fnl files, automatically becomes lua/**/*.lua
{"colors/*.fnl", true}, -- compiles colors/x.fnl to colors/x.lua
{"colors/*.fnl", function(path) return string.gsub(path, "fnl$", "lua") end} -- same as above
}
Specify auto-clean instructions for the .hotpot.lua
directory. When present
Hotpot will remove any unknown files matching the given glob pattern after it
runs an auto-build.
Auto-clean will only run after an auto-build. Auto-clean will not run if the
dryrun = true
option was given to build
, or if atomic = true
was given
and compilation errors occurred.
Supported values are:
clean = false
or clean = nil
or the key is omitted
Disable auto-cleaning.
clean = true
Enable auto-cleaning with a default value that should be applicable to most
usage. The default value removes all files from lua/
, eg:
{{"lua/**/*.lua", true}}
clean = {{glob_pattern, boolean}, ...}
Each glob pattern is expanded in order and if the boolean value is true, the file will be marked for removal if it is unrecognised (eg: not created by hotpot during the build step). If the values is false, the file will be retained.
clean = {
{"lua/lib/**/*.lua", false}, -- dont remove lib files
{"lua/**/*.lua", true}, -- remove anything else
}
The compiler key supports the same values as the compiler
options you may
pass to setup()
. See :h hotpot-setup
.
compiler = {
modules = { ... },
macros = { ... },
preprocessor = function ... end
}
Using .hotpot.lua
You can configure a .hotpot.lua
file to build init.fnl
, see using
.hotpot.lua
for more information.
You should be aware that the presence of a .hotpot.lua
file will disable any
compiler
options you pass to setup, so you should instead move them into the
.hotpot.lua
file.
-- ~/.config/nvim/.hotpot.lua
return {
build = {
{verbose = true},
-- This will only compile init.fnl, all other fnl/ files will behave as normal.
{"init.fnl", true},
-- Or you could enable other patterns too,
-- {"colors/*.fnl", true},
-- {"fnl/**/*.fnl", true},
}
}
Using autocommands and hotpot.api.make
We can use a combination of the Make API and autocommands to write our main
init.lua
in Fennel and automatically compile it to loadable lua on save.
;; ~/.config/nvim/init.fnl
(let [hotpot (require :hotpot)
setup hotpot.setup
build hotpot.api.make.build
uv vim.loop]
;; do some configuration stuff
(setup {:compiler {:modules {:correlate true}
:macros {:env :_COMPILER
:compilerEnv _G
:allowedGlobals false}}})
(fn rebuild-on-save [{: buf}]
(let [{: build} (require :hotpot.api.make)
au-config {:buffer buf
:callback #(build (vim.fn.stdpath :config)
{:verbose true :atomic true
;; Enforce hard errors when unknown symbols are encountered.
:compiler {:modules {:allowedGlobals (icollect [n _ (pairs _G)] n)}}}
[["init.fnl" true]])}]
(vim.api.nvim_create_autocmd :BufWritePost au-config)))
;; watch file opens, attach builder if we open the config
(vim.api.nvim_create_autocmd :BufRead
{:pattern (-> (.. (vim.fn.stdpath :config) :/init.fnl)
;; call realpath if you have some symlink setup
;; (vim.loop.fs_realpath)
(vim.fs.normalize))
:callback rebuild-on-save}))
(require :the-rest-of-my-config)
Finally, we have to manually run this code once to generate the new init.lua
:
- Open
init.fnl
- Run
:Fnlfile %
to execute the current file and enable the autocommand.- Note, this will also run any code that is executed by
(require :the-rest-of-my-config)
. - You could also use
:Fnl
with the appropriate code selected.
- Note, this will also run any code that is executed by
- Run
:e
to re-open the buffer and attach the autocommand. - Save the file with
:w
to run the autocommand.- This will overwrite your existing
init.lua
!
- This will overwrite your existing
- Open
init.lua
to confirm it contains your fennel, compiled into lua. - Start neovim in a new terminal to confirm the config loading is functioning without any errors.
Put your code in ~/.config/nvim/ftplugin
as you would any lua ftplugin.
Ex.
;;~/.config/nvim/ftplugin/fennel.fnl
(print (vim.fn.expand :<afile>)) ;; print name of fennel file
(vim.opt.formatoptions:append :j)
ftplugins are put in the cache, irrespective of any colocation setting. This is to avoid any module precedence issues.
!! The Reflect API is experimental and its shape may change, particularly around
accepting ranges instead of requiring a visual selection and some API terms
such as what a session
is. !!
!! Do NOT run dangerous code (like (system "rm -rf /")
inside an evaluation
block! You could cause massive damage to your system! !!
!! Some plugins (Parinfer) can be quite destructive to the buffer and can cause marks to be lost or damaged. In this event you can just reselect your range. !!
Reflect API acts similarly to a REPL environment but instead of entering statements in a conversational manner, you mark sections of your code and the API will "reflect" the result to you and update itself as you change your code.
The basic usage of the API is:
-
Get an output buffer pass it to
attach-output
. Asession-id
is returned. -
Visually select a region of code and call
attach-input session-id <buf>
where buf is probably0
for current buffer.
Note that windowing is not mentioned. The Reflect API leaves general window management to the user as they can best decide how they wish to structure their editor - with floating windows, splits above, below, etc. The Reflect API also does not provide any default bindings.
The following is an example binding setup that will open a new window and connect the output and inputs with one binding. It tracks the session and only allows one per-editor session. This code is written verbosely for education and could be condensed.
;; Open session and attach input in one step.
;; Note the complexity here is mostly due to nvim not having an api to create a
;; split window, so we must shuffle some code to create a buf, pair input and output
;; then put that buf inside a window.
(local reflect-session {:id nil :mode :compile})
(fn new-or-attach-reflect []
(let [reflect (require :hotpot.api.reflect)
with-session-id (if reflect-session.id
(fn [f]
;; session id already exists, so we can just pass
;; it to whatever needs it
(f reflect-session.id))
(fn [f]
;; session id does not exist, so we need to create
;; an output buffer first then we can pass the
;; session id on, and finally hook up the output
;; buffer to a window
(let [buf (api.nvim_create_buf true true)
id (reflect.attach-output buf)]
(set reflect-session.id id)
(f id)
;; create window, which will forcibly assume focus, swap the buffer
;; to our output buffer and setup an autocommand to drop the session id
;; when the session window is closed.
(vim.schedule #(do
(api.nvim_command "botright vnew")
(api.nvim_win_set_buf (api.nvim_get_current_win) buf)
(api.nvim_create_autocmd :BufWipeout
{:buffer buf
:once true
:callback #(set reflect-session.id nil)}))))))]
;; we want to set the session mode to our current mode, and attach the
;; input buffer once we have a session id
(with-session-id (fn [session-id]
;; we manually set the mode each time so it is persisted if we close the session.
;; By default `reflect` will use compile mode.
(reflect.set-mode session-id reflect-session.mode)
(reflect.attach-input session-id 0)))))
(vim.keymap.set :v :hr new-or-attach-reflect)
(fn swap-reflect-mode []
(let [reflect (require :hotpot.api.reflect)]
;; only makes sense to do this when we have a session active
(when reflect-session.id
;; swap held mode
(if (= reflect-session.mode :compile)
(set reflect-session.mode :eval)
(set reflect-session.mode :compile))
;; tell session to use new mode
(reflect.set-mode reflect-session.id reflect-session.mode))))
(vim.keymap.set :n :hx swap-reflect-mode)
See :h hotpot.api
a complete listing.
Note: The API modules can be lazy-accessed from hotpot
and hotpot.api
(let [hotpot (require :hotpot)
eval hotpot.api.eval]
(eval.eval-selection))
(let [api (require :hotpot.api)
compile api.compile]
(compile.compile-buffer 0))
Eval & Compile
Evaluate or compile the v
selection, or the entire buffer.
(fn pecho [ok? ...]
"nvim_echo vargs, as DiagnosticHint or DiagnosticError depending on ok?"
(let [{: nvim_echo} vim.api
{: view} (require :fennel)
hl (if ok? :DiagnosticHint :DiagnosticError)
list [...]
output []]
;; TODO: this can be fcollect in fennel 1.2.0)
(for [i 1 (select :# ...)]
(table.insert output (-> (. list i)
(#(match (type $1)
:table (view $1)
_ (tostring $1)))
(.. "\n"))))
(nvim_echo (icollect [_ l (ipairs output)] [l hl]) true {})))
(vim.keymap.set :n :heb
#(let [{: eval-buffer} (require :hotpot.api.eval)]
(pecho (eval-buffer 0)))
{:desc "Evaluate entire buffer"})
(vim.keymap.set :v :hes
#(let [{: eval-selection} (require :hotpot.api.eval)]
(pecho (eval-selection)))
{:desc "Evaluate selection"})
(vim.keymap.set :n :hcb
#(let [{: compile-buffer} (require :hotpot.api.compile)]
(pecho (compile-buffer 0)))
{:desc "Compile entire buffer"})
(vim.keymap.set :v :hcs
#(let [{: compile-selection} (require :hotpot.api.compile)]
(pecho (compile-selection)))
{:desc "Compile selection"})
Cache operations
Open the cache directory with Telescope searcher:
(let [{: find_files} (require :telescope.builtin)
{: cache-prefix} (require :hotpot.api.cache)]
(find_files {:cwd (cache-prefix)
:hidden true}))
See also :h hotpot.api.cache
.
Commands to run snippets of Fennel, similar to Neovim's :lua
et al commands.
:[range]Fnl {expression} -> evaluate range in buffer OR expression
:[range]Fnldo {expression} -> evaluate expression for each line in range
:Fnlfile {file} -> evaluate file
:source {file} -> alias to :Fnlsource
, must be called as:source my-file.fnl
or:source %
and the given file must be a descendent of afnl
directory. Will attempt to recompile, recache and reload the given file.
Hotpot expects the user to specify most maps themselves via the API functions.
It does provide one <Plug>
mapping for operator-pending eval.
map <Plug> ghe <Plug>(hotpot-operator-eval)
gheip -> evaluate fennel code in paragraph
Fennel compiles macros in a restricted environment called a sandbox. In this
environment, common lua tables such as os
, or in Neovim, vim
are
unavailable.
Note: this restriction applies to code executed in the macro, not code generated by the macro.
As an example, imagine we want a function that prints the time of compilation, we may write something like this:
;; ts-fn.fnl
(macro fn-with-ts [name args body]
(let [now (os.date :%s)]
`(fn ,name ,args
(do
(print "code generated at" ,now)
,body))))
(fn-with-ts my-func [x]
(print (* x x)))
If we try to build this with the Fennel CLI, we get the following error, because
os
is unavailable:
$ fennel -c ts-fn.fnl
Compile error in ts-fn.fnl:2:13
unknown identifier in strict mode: os
(let [now (os.date :%s)]
* Try looking to see if there's a typo.
* Try using the _G table instead, eg. _G.os if you really want a global.
* Try moving this code to somewhere that os is in scope.
* Try binding os as a local in the scope of this code.
We can disable the compiler sandbox with --no-compiler-sandbox
, which will
allow us to compile our code:
$ fennel --no-compiler-sandbox -c ts-fn.fnl
local function my_func(x)
print("code generated at", "1665501877")
return print((x * x))
end
return my_func
With this understanding, we can adjust the macro compilation options we provide
to hotpot.setup
, and then we can use the function inside Neovim:
-- ...
macros = {
env = "_COMPILER",
compilerEnv = _G,
allowGlobals = false,
}
-- ...
For more information on available options, see Fennels own documentation.
Fennel supports user provided compiler plugins and Hotpot does too. For more information on compiler plugins, see Fennels own documentation.
Plugins are specified for both modules
and macros
and may be provided as a
table (ie. as described by Fennels documentation) or a module name as a string.
When your plugin requires access to the compiler environment or is
uncomfortable to write in lua (which may be the language your using to define
setup
's options), specifying the plugin as a string lets you do that.
Compiler plugins are extremely powerful and can let you add new language constructs to Fennel or modify existing ones but be aware of the impact you might have on portability and clarity.
Below are two identical plugins which add 1 to every (+)
call (so (+ 1 1)
becomes (+ 1 1 1)
.
;; .config/nvim/fnl/off_by_one.fnl
(fn call [ast scope ...]
(match ast
[[:+]] (table.insert ast 1))
(values nil))
{:name :add_one_module
:call call
:versions [:1.2.1]}
off_by_one = {
name = "add_one_table",
call = function (ast, scope)
if ast[1][1] == "+" then
table.insert(ast, 1)
end
return nil
end,
versions = {"1.2.1"}
}
require("hotpot").setup({
compiler = {
modules = {
plugins = {
"off_by_one",
off_by_one,
}
},
-- you may also define for macros
-- macros = {
-- plugins = {...},
-- },
}
})
F͙̖͍͇̤ͣ̅ͯ̕Ō̝̦͎̣̲͖̬̬̌́R̖̮͈ͭ͊̾̈́͘B̢̮̖̊ͧ̃Į̳̘͇̣͖̔͋D̈̑̅͏̟͓̮̰̼̪͈Ď̡̲̠͇͍͓̔E̥̠̱ͫ̋̈̽͢Ņ̹̠̱̮̖̖̝ͣͯ̌ ̠̰̲̗̝̂͞K̶̩̲̖̦̯͕̜̱̃͆ͯ̾Ṉ͔̠̩̗̅̓̈́͢Ǫ̻̳̜̅W̰̩̰̬ͣ͗̕L̽ͦ̂͑҉͇̠E̫͎̝͖͕̰ͣ͡D̖͎͇̔̂ͬ͡G͇͚̩̱̮̹̈́͠E̱̖̯̫̬̫̞͒ͧ͜
;; plugin.fnl
;; must define as function that returns a list
(fn map-seq-fn [seq f]
`(icollect [_# v# (ipairs ,seq)] (,f v#)))
(fn call [ast scope ...]
(match ast
;; match against symbol and capture arguments
[[:map-seq] & other]
;; written as do for comment clarity
(do
;; expand our macro as compiler would do, passing in capture arguments
(local macro-ast (map-seq-fn (unpack other)))
;; now expand that ast again (this expands icollect etc, *other* macros)
(local true-ast (macroexpand macro-ast))
;; change ast to match macro ast, note that we must
;; **modifiy** the ast, not return a new one, as we're
;; actually modifying the ast back in the compiler call-site.
(each [i ex-ast (ipairs true-ast)]
(tset ast i ex-ast))))
;; nil to continue other plugins
(values nil))
{:name :magic-map-seq
:call call
:versions [:1.2.1]}
;; file.fnl
(map-seq [1 2 3] #(print $)) ;; works by magic