- Notifications
You must be signed in to change notification settings - Fork0
Enhance your Neovim with Fennel
License
k13labs/nfnl
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Enhance yourNeovim experience throughFennel with zerooverhead. Write Fennel, run Lua, nfnl will not load unless you're activelymodifying your Neovim configuration or plugin source code(nfnl-plugin-example).
- Only loads when working in directories containing a
.nfnl.fnl
configurationfile. - Automatically compiles
*.fnl
files to*.lua
when you save your changes. - Can be used for your Neovim configuration orpluginswith no special configuration, it just works for both.
- Includes a Clojure inspiredstandard library (based onAniseed).
- Compiles your Fennel code and then steps out of the way leaving you with plainLua that doesn't require nfnl to load in the future.
- Displays compilation errors as you save your Fennel code to keep the feedbackloop as tight as possible.
First, you must create the configuration file at the root of your project orconfiguration, this can be blank if you wish to rely on the defaults for now.
echo"{}"> .nfnl.fnl
The first time you open a Fennel file under this directory you'll be prompted totrust this configuration file since it's Fennel code that's executed on yourbehalf. You can put any Fennel code you want in this file, just be sure toreturn a table of configuration at the end.
(print"Hello, World! From my nfnl configuration!"){:fennel-path"..."}
By default, writing to any Fennel file with Neovim under the directorycontaining.nfnl.fnl
will automatically compile it to Lua. If there arecompilations errors they'll be displayed usingvim.notify
and the Lua will notbe updated.
nfnl will refuse to overwrite any existing Lua at the destination if nfnl wasnot the one to compile it, this protects you from accidentally overwritingexisting Lua with compiled output. To bypass the warning you must delete or movethe Lua file residing at the destination yourself.
Now you may use the compiled Lua just like you would any other Lua files withNeovim. There's nothing special about it, please refer to the abundantdocumentation on the topic of Neovim configuration and plugins in Lua.
You must commit the Lua into your configuration or plugin so that it can beloaded by native Neovim Lua systems, with absolutely no knowledge of the Fennelit originated from.
nfnl is configured on a per directory basis using.nfnl.fnl
files which alsosignify that the plugin should operate on the files within this directory.Without the file the plugin is inert, meaning even if you don't lazy load it youwon't see any performance impact at startup time.
Any configuration you don't provide (an empty file or just{}
is absolutelyfine!) will default to these values that should work fine for most people.
{;; Enables verbose notifications from nfnl, including notifications about;; when it starts up and when it compiles successfully. Useful for debugging;; the plugin itself and checking that it's running when you expect it to.:verbosefalse;; Passed to fennel.compileString when your code is compiled.;; See https://fennel-lang.org/api for more information.:compiler-options {;; Disables ansi escape sequences in compiler output.:error-pinpointfalse};; Warning! In reality these paths are absolute and set to the root directory;; of your project (where your .nfnl.fnl file is). This means even if you;; open a .fnl file from outside your project's cwd the compiler will still;; find your macro files. If you use relative paths like I'm demonstrating here;; then macros will only work if your cwd is in the project you're working on.;; They also use OS specific path separators, what you see below is just an example really.;; I'm not including nfnl's directory from your runtimepath, but it would be there by default.;; See :rtp-patterns below for more information on including other plugins in your path.;; String to set the compiler's fennel.path to before compilation.:fennel-path"./?.fnl;./?/init.fnl;./fnl/?.fnl;./fnl/?/init.fnl";; String to set the compiler's fennel.macro-path to before compilation.:fennel-macro-path"./?.fnl;./?/init-macros.fnl;./?/init.fnl;./fnl/?.fnl;./fnl/?/init-macros.fnl;./fnl/?/init.fnl";; A list of glob patterns (autocmd pattern syntax) of files that;; should be compiled. This is used as configuration for the BufWritePost;; autocmd, so it'll only apply to buffers you're interested in.;; Will use backslashes on Windows.;; Defaults to compiling all .fnl files, you may want to limit it to your fnl/ directory.:source-file-patterns ["*.fnl""**/*.fnl"];; A function that is given the absolute path of a Fennel file and should return;; the equivalent Lua path, by default this will translate `fnl/foo/bar.fnl` to `lua/foo/bar.lua`.;; See the "Writing Lua elsewhere" tip below for an example function that writes to a sub directory.:fnl-path->lua-path (fn [fnl-path]...)}
As an example, if you only want to compile.fnl
files under thefnl/
directory of your Neovim configuration (so nothing in the root directory) youcould use this.nfnl.fnl
file instead.
{:source-file-patterns ["fnl/**/*.fnl"]}
And since this is a Fennel file that's executed within Neovim you can actuallyload nfnl's modules to access things like the default config values.
(localcore (require:nfnl.core))(localconfig (require:nfnl.config))(localdefault (config.default)){:source-file-patterns (core.concatdefault.source-file-patterns ["custom-dir/*.fnl"])}
config.default
accepts a table of arguments (docs) tochange how it builds the default configuration. You can call(config.default {...})
on the last line of your.nfnl.fnl
file in order toreturn a modified default configuration table. You also then have the option tocallconfig.default
, modify that table with extra values and then return that.
By providing a differentrtp-patterns
value (defaults to["/nfnl$"]
) we caninclude other plugins you have installed in your search paths when requiring Luamodules or macros.
;; Configuration that includes nfnl _and_ your-cool-plugin in the search paths.(localconfig (require:nfnl.config))(config.default {:rtp-patterns ["/nfnl$""/your-cool-plugin$"]});; Configuration that includes ALL of your installed plugins in your search paths.;; This might slow down compilation on some machines, so it's not the default.(localconfig (require:nfnl.config))(config.default {:rtp-patterns [".*"]});; Searching all of your plugins _and_ merging in some other custom configuration.(localcore (require:nfnl.core))(localconfig (require:nfnl.config))(core.merge (config.default {:rtp-patterns [".*"]}) {:source-file-patterns ["fnl/**/*.fnl"]})
Lazy will lazily load the plugin when you enter a Fennel file for thefirst time. There is no need to callrequire("nfnl").setup()
right now, it'scurrently a noop but it may be used eventually. Some plugin managers supportthis function and will call it automatically.
- Requires Neovim > v0.9.0.
- Add the dependency to your plugin manager.
- Add lazy loading rules on the Fennel filetype if you want.
nfnl ships with a standard library used mostly for it's internal implementation,but it can also be used by your own Neovim configuration or plugins. This isbased onAniseed's standard library but without the module systemthat prevented you from using it in standard, non-Neovim, Fennel projects.
Full API documentation powered byfenneldoc can be found in theapi directory.
The documentation is regenerated by executing./script/render-api-documentation
. One limitation of using this tool is thatall top level values in a module should really be functions, if we do workinside(local)
for example we'll end up incurring side effects atdocumentation rendering time that we may not want.
Fennel allows you to write inline macros with the(macro ...)
form but they'rerestricted to only being used in that one file. If you wish to have a macromodule shared by the rest of your codebase you need to mark that file as a macromodule by placing;; [nfnl-macro]
somewhere within the source code. The exactamount of;
and whitespace doesn't matter, you just need a comment with[nfnl-macro]
inside of it.
This marker does two things:
- Instructs the compiler not to attempt to compile this file since it wouldfail. You can't compile macro modules to Lua, they use features that can onlybe referred to at compile time and simply do not translate to Lua.
- Ensures that whenever the file is written to all other non-macro modules getrecompiled instead. Ensuring any inter-dependencies between your Fennel andyour macro modules stays in sync and you never end up having to find old Luathat was compiled with old versions of your macros.
For example, here's a simplified macro file from nfnl itself atfnl/nfnl/macros.fnl
.
;; [nfnl-macro];; .nfnl.fnl config so you don't need to prefix globals like _G.vim.*;; {:compiler-options {:compilerEnv _G}}(fntime [...] `(let [start# (vim.loop.hrtime)result# (do ,...)end# (vim.loop.hrtime)] (print (.."Elapsed time: " (/ (-end#start#)1000000)" msecs"))result#)){:time}
When writing to this file, no matching.lua
will be generatedbut all othersource files in the project will be re-compiled against the new version of themacro module.
This system does not currently use static analysis to work out which filesdepend on each other, instead we opt for the safe approach of recompilingeverything. This should still be fast enough for everyone's needs and avoids thehorrible subtle bugs that would come with trying to be clever with it.
By default, the Fennel compiler employs a compiler sandbox in your macromodules. This means you can't refer to any free global variables such avim
.You have to configure theFennel compiler API with the{:compiler-options {...}}
section of your.nfnl.fnl
file.
You can either prefix each of these globals with_G
like_G.vim.g.some_var
or you can turn off the relevant sandboxing rules. One approach is to set thecompiler environment to_G
instead of Fennel's sanitised environment. You cando that with the following.nfnl.fnl
file.
{:compiler-options {:compilerEnv_G}}
Currently only developed for and tested on Arch Linux, but this works fine onMacOS. You can see another example of creating a plugin and done on MacOS atthis blog post. I triedmy best to support Windows without actually testing it. So I've ensured it usesthe right path separators in all the places I can find.
If you try this out and it works on MacOS or Windows, please let me know so Ican add it here. If you run into issues, please report them with as much detailas possible.
Create a.ignore
file so your.lua
files don't show up inTelescope when paired withripgrep among many othertools that respect this file.
lua/**/*.lua
You can also add these known directories and files to things like yourNeo-tree configuration in order to completely hide them.
Create a.gitattributes
file to teach GitHub which of your files are generatedor vendored. This ensures your "languages" section on your repository pagereflects reality.
lua/**/*.lua linguist-generatedlua/nfnl/fennel.lua linguist-vendoredscript/fennel.lua linguist-vendored
I highly recommend looking into getting a good LSP setup forfennel-language-server
. I useAstroNvim since it bundles LSPconfigurations and Mason, a way to install dependencies, in one pre-configuredsystem. My configuration ishere in my dotfiles.
With the Fennel LSP running I get a little autocompletion alongside reallyuseful unused or undeclared symbol linting. It'll also pick up things likeunbalanced parenthesisbefore I try to compile the file.
The same can be done for Lua so you can also check the linting and staticanalysis of the compiled output in order to help debug some runtime issues.
I wrotenvim-local-fennel to solve this problem years agobut I now recommend combining nfnl with the built inexrc
option. Simply:set exrc
(see:help exrc
for more information), create a.nfnl.fnl
fileand then edit.nvim.fnl
.
This will write Lua to.nvim.lua
which will be executed whenever your Neovimenters this directory tree. Even if you uninstall nfnl the.lua
file willcontinue to work. Colleagues who also use Neovim but don't have nfnl installedcan also use the.nvim.lua
file provided they haveexrc
enabled (even ifthey can't edit the Fennel to compile new versions of the Lua).
This solution achieves the same goal as nvim-local-fennel with far less codeand built in options all Neovim users can lean on.
If you want to ship a plugin (nfnl-plugin-example) thatdepends on nfnl modules you'll need to embed it inside your project. You caneithercp -r lua/nfnl
intoyour-project/lua/nfnl
if you don't mind yourplugin's copy of nfnl colliding with other plugins or you can usescript/embed-library
to copy the files into a lower level directory and modifythem to isolate them for your plugin specifically.
cp -r nfnl/lua/nfnl my-plugin/lua/nfnl
Now your plugin can always use(require :nfnl.core)
and know it'll be around,but you might run into issues where another plugin author has done the same andis using an older version of nfnl that lacks some feature you require. Lua has aglobal module namespace, so collisions are quite easy to accidentally cause. Youmay use my embedding script (or your own) to avoid this though:
# There are more paths and options available, see the script source for more information.# This will write to $PROJECT/lua/$PROJECT/nfnl.SRC_DIR=nfnl/lua/nfnl PROJECT=my-plugin ./nfnl/script/embed-library
This will copy nfnl's Lua code into your project's directory under a namespaceddirectory unique to your project. It will then perform a find and replace on theLua code to scope the nfnl source to your plugin, avoiding conflicts with anyother copy of nfnl.
This script depends uponfd andsd, so make sure you install thosefirst! Alternatively you could modify or write your own script that works foryour OS with your available tools.
If you're not happy with the defaults of Lua being written beside your Fenneland still disagree withmy justifications for itthen you may want to override the:fnl-path->lua-path
function to perform in away you like. Since you get to define a function, how this behaves is entirelyup to you. Here's how you could write to a sub-directory rather than justlua
,just include this in your.nfnl.fnl
configuration file for your project.
(localconfig (require:nfnl.config))(localdefault (config.default)){:fnl-path->lua-path (fn [fnl-path] (let [rel-fnl-path (vim.fn.fnamemodifyfnl-path":.")] (default.fnl-path->lua-path (.."some-other-dir/"rel-fnl-path))))}
User commands are defined inside your Fennel buffers when nfnl configuration isdetected, they are just thin wrappers around the function found innfnl.api
which you can read about under the next header.
:NfnlFile [path]
path
defaults to%
Run the matching Lua file for this Fennel file from disk. Does not recompilethe Lua, you must use nfnl to compile your Fennel to Lua first. Callsnfnl.api/dofile under the hood.
:NfnlCompileAllFiles [path]
path
defaults to.
Executes (nfnl.api/compile-all-files) which will, you guessed it, compile allof your files.
Although you can require any internal nfnl Lua module and call it's functions(full index of internal modules and functions) there is a specificmodule,nfnl.api
(documentation), that is designed to behooked up to your own functions, mappings and autocmds.
The functions within are designed to "do the right thing" with little to noconfiguration. You shouldn't need them in normal use, but they may come inuseful when you need to fit nfnl into an interesting workflow or system.
As an example, here's how and why you'd use thecompile-all-files
functionfrom another Fennel file to, you guessed it, compile all of your files.
(localnfnl (require:nfnl.api));; Takes an optional directory as an argument, defaults to (vim.fn.getcwd).(nfnl.compile-all-files)
In the case where you're absolutely adamant that you need to.gitignore
yourcompiled Lua output, this can be used after yougit pull
to ensure everythingis compiled. However, I strongly advise committing your Lua for performanceand stability.
This project was designed around the principal of compiling early and then neverneeding to compile again unless you make changes. I thought long and hard aboutthe tradeoffs so you don't have to. These tools are here for when I'm wrong.
If you have nfnl installed in Neovim you should be able to just modify Fennelfiles and have them get recompiled automatically for you. So nfnl is compiledwith nfnl. This does however mean you can perform an Oopsie and break nfnl,rendering it useless to recompile itself with fixed code.
If you run into issues like this, you can executescript/bootstrap-dev
to runa file watching Fennel compiler andscript/bootstrap
to compile everythingwith a one off command. Both of these lean onscript/fennel.bb
which is asmart Fennel compiler wrapper written inBabashka. This wrapperrelies on the bundled Fennel compiler atscript/fennel.lua
, so it will ignoreany Fennel version installed at the system level on the CLI.
So you'll need the following to use the full development suite:
- A Lua runtime of some kind to execute
script/fennel.lua
. - Babashka to execute
script/fennel.bb
. - Entr if you want to use file watching with
script/bootstrap-dev
.
The bootstrap tools should only really ever be required during the initialdevelopment of this plugin or if something has gone catastrophically wrong andnfnl can no longer recompile itself. Normally having nfnl installed and editingthe.fnl
files should be enough.
Remember to rebuild the API documentation and run the tests when making changes.This workflow will be automated and streamlined eventually.
Tests are written underfnl/spec/nfnl/**/*_spec.fnl
. They're compiled into thelua/
directory by nfnl itself and executed byPlenary, you musthave this plugin installed in order to run the test suite within Neovim.
The project local.nfnl.fnl
defines the<localleader>pt
mapping which allowsyou to execute the test suite from within Neovim using Plenary.
To run the tests outside of your configuration you can run./script/setup-test-deps
(installs dependencies into this local directory) andthen./script/test
to execute the tests in a headless local Neovimconfiguration.
Find the fullUnlicense in theUNLICENSE
file, but here's asnippet.
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distributethis software, either in source code form or as a compiled binary, for anypurpose, commercial or non-commercial, and by any means.
lua/nfnl/fennel.lua
andscript/fennel.lua
are excluded from this licensing,they're downloaded from theFennel website and retains any licenseused by the original author. We vendor it within this tool to simplify the userexperience.
script/fenneldoc.lua
is also excluded since it's compiled from thefenneldoc repository.