• Stars
    star
    223
  • Rank 178,391 (Top 4 %)
  • Language Fennel
  • License
    The Unlicense
  • Created over 1 year ago
  • Updated about 1 month ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Enhance your Neovim with Fennel

nfnl

Enhance your Neovim experience through Fennel with zero overhead. Write Fennel, run Lua, nfnl will not load unless you're actively modifying your Neovim configuration or plugin source code (nfnl-plugin-example).

  • Only loads when working in directories containing a .nfnl.fnl configuration file.
  • Automatically compiles *.fnl files to *.lua when you save your changes.
  • Can be used for your Neovim configuration or plugins with no special configuration, it just works for both.
  • Includes a Clojure inspired standard library (based on Aniseed).
  • Compiles your Fennel code and then steps out of the way leaving you with plain Lua that doesn't require nfnl to load in the future.
  • Displays compilation errors as you save your Fennel code to keep the feedback loop as tight as possible.

Usage

First, you must create the configuration file at the root of your project or configuration, 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 to trust this configuration file since it's Fennel code that's executed on your behalf. You can put any Fennel code you want in this file, just be sure to return 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 directory containing .nfnl.fnl will automatically compile it to Lua. If there are compilations errors they'll be displayed using vim.notify and the Lua will not be updated.

nfnl will refuse to overwrite any existing Lua at the destination if nfnl was not the one to compile it, this protects you from accidentally overwriting existing Lua with compiled output. To bypass the warning you must delete or move the Lua file residing at the destination yourself.

Now you may use the compiled Lua just like you would any other Lua files with Neovim. There's nothing special about it, please refer to the abundant documentation on the topic of Neovim configuration and plugins in Lua.

You must commit the Lua into your configuration or plugin so that it can be loaded by native Neovim Lua systems, with absolutely no knowledge of the Fennel it originated from.

Compiling all files

If you for whatever reason need to compile all of your files to Lua at once then you may do so by invoking the compile-all-files function like so.

require('nfnl')['compile-all-files']()

In the case where you're absolutely adamant that you need to .gitignore your compiled Lua output, this can be used after you git pull to ensure everything is compiled. I strongly advise committing your Lua for performance and stability reasons however.

This project was designed around the principal of compiling early and then never needing to compile again unless you make changes. I thought long and hard about the tradeoffs so you don't have to.

Configuration

nfnl is configured on a per directory basis using .nfnl.fnl files which also signify 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 you won't see any performance impact at startup time.

Any configuration you don't provide (an empty file or just {} is absolutely fine!) will default to these values that should work fine for most people.

{;; Passed to fennel.compileString when your code is compiled.
 ;; See https://fennel-lang.org/api for more information.
 :compiler-options {}

 ;; 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.

 ;; 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 the fnl/ directory of your Neovim configuration (so nothing in the root directory) you could 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 actually load nfnl's modules to access things like the default config values.

(local core (require :nfnl.core))
(local config (require :nfnl.config))
(local default (config.default))

{:source-file-patterns (core.concat default.source-file-patterns ["custom-dir/*.fnl"])}

Installation

  • Lazy: { "Olical/nfnl", ft = "fennel" }
  • Plug: Plug 'Olical/nfnl'
  • Packer: use "Olical/nfnl"

Lazy will lazily load the plugin when you enter a Fennel file for the first time. There is no need to call require("nfnl").setup() right now, it's currently a noop but it may be used eventually. Some plugin managers support this 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.

Standard library

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 is based on Aniseed's standard library but without the module system that prevented you from using it in standard, non-Neovim, Fennel projects.

Full API documentation powered by fenneldoc can be found in the api directory.

The documentation is regenerated by executing ./script/render-api-documentation. One limitation of using this tool is that all top level values in a module should really be functions, if we do work inside (local) for example we'll end up incurring side effects at documentation rendering time that we may not want.

Macros

Fennel allows you to write inline macros with the (macro ...) form but they're restricted to only being used in that one file. If you wish to have a macro module shared by the rest of your codebase you need to mark that file as a macro module by placing ;; [nfnl-macro] somewhere within the source code. The exact amount 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 would fail. You can't compile macro modules to Lua, they use features that can only be 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 get recompiled instead. Ensuring any inter-dependencies between your Fennel and your macro modules stays in sync and you never end up having to find old Lua that was compiled with old versions of your macros.

For example, here's a simplified macro file from nfnl itself at fnl/nfnl/macros.fnl.

;; [nfnl-macro]

(fn time [...]
  `(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 generated but all other source files in the project will be re-compiled against the new version of the macro module.

This system does not currently use static analysis to work out which files depend on each other, instead we opt for the safe approach of recompiling everything. This should still be fast enough for everyone's needs and avoids the horrible subtle bugs that would come with trying to be clever with it.

OS support

Currently only developed for and tested on Arch Linux, but this works fine on MacOS. You can see another example of creating a plugin and done on MacOS at this blog post. I tried my best to support Windows without actually testing it. So I've ensured it uses the 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 I can add it here. If you run into issues, please report them with as much detail as possible.

Tips

Ignoring compiled Lua

Create a .ignore file so your .lua files don't show up in Telescope when paired with ripgrep among many other tools that respect this file.

lua/**/*.lua

You can also add these known directories and files to things like your Neo-tree configuration in order to completely hide them.

GitHub language statistics

Create a .gitattributes file to teach GitHub which of your files are generated or vendored. This ensures your "languages" section on your repository page reflects reality.

lua/**/*.lua linguist-generated
lua/nfnl/fennel.lua linguist-vendored
script/fennel.lua linguist-vendored

LSP

I highly recommend looking into getting a good LSP setup for fennel-language-server. I use AstroNvim since it bundles LSP configurations and Mason, a way to install dependencies, in one pre-configured system. My configuration is here in my dotfiles.

With the Fennel LSP running I get a little autocompletion alongside really useful unused or undeclared symbol linting. It'll also pick up things like unbalanced parenthesis before I try to compile the file.

The same can be done for Lua so you can also check the linting and static analysis of the compiled output in order to help debug some runtime issues.

Directory local Neovim configuration in Fennel

I wrote nvim-local-fennel to solve this problem years ago but I now recommend combining nfnl with the built in exrc option. Simply :set exrc (see :help exrc for more information), create a .nfnl.fnl file and then edit .nvim.fnl.

This will write Lua to .nvim.lua which will be executed whenever your Neovim enters this directory tree. Even if you uninstall nfnl the .lua file will continue to work. Colleagues who also use Neovim but don't have nfnl installed can also use the .nvim.lua file provided they have exrc enabled (even if they 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 code and built in options all Neovim users can lean on.

Embedding nfnl inside your plugin

If you want to ship a plugin (nfnl-plugin-example) that depends on nfnl modules you'll need to embed it inside your project. You can either cp -r lua/nfnl into your-project/lua/nfnl if you don't mind your plugin's copy of nfnl colliding with other plugins or you can use script/embed-library to copy the files into a lower level directory and modify them 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 and is using an older version of nfnl that lacks some feature you require. Lua has a global module namespace, so collisions are quite easy to accidentally cause. You may 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 namespaced directory unique to your project. It will then perform a find and replace on the Lua code to scope the nfnl source to your plugin, avoiding conflicts with any other copy of nfnl.

This script depends upon fd and sd, so make sure you install those first! Alternatively you could modify or write your own script that works for your OS with your available tools.

Writing Lua elsewhere

If you're not happy with the defaults of Lua being written beside your Fennel and still disagree with my justifications for it then you may want to override the :fnl-path->lua-path function to perform in a way you like. Since you get to define a function, how this behaves is entirely up to you. Here's how you could write to a sub-directory rather than just lua, just include this in your .nfnl.fnl configuration file for your project.

(local config (require :nfnl.config))
(local default (config.default))

{:fnl-path->lua-path (fn [fnl-path]
                       (let [rel-fnl-path (vim.fn.fnamemodify fnl-path ":.")]
                         (default.fnl-path->lua-path (.. "some-other-dir/" rel-fnl-path))))}

Development

If you have nfnl installed in Neovim you should be able to just modify Fennel files and have them get recompiled automatically for you. So nfnl is compiled with 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 execute script/bootstrap-dev to run a file watching Fennel compiler and script/bootstrap to compile everything with a one off command. Both of these lean on script/fennel.bb which is a smart Fennel compiler wrapper written in Babashka. This wrapper relies on the bundled Fennel compiler at script/fennel.lua, so it will ignore any 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 initial development of this plugin or if something has gone catastrophically wrong and nfnl can no longer recompile itself. Normally having nfnl installed and editing the .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.

Testing

Tests are written under fnl/spec/nfnl/**/*_spec.fnl. They're compiled into the lua/ directory by nfnl itself and executed by Plenary, you must have this plugin installed in order to run the test suite.

The project local .nfnl.fnl defines the <localleader>pt mapping which allows you to execute the test suite from within Neovim using Plenary.

Unlicensed

Find the full Unlicense in the UNLICENSE file, but here's a snippet.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

lua/nfnl/fennel.lua and script/fennel.lua are excluded from this licensing, they're downloaded from the Fennel website and retains any license used by the original author. We vendor it within this tool to simplify the user experience.

script/fenneldoc.lua is also excluded since it's compiled from the fenneldoc repository.

More Repositories

1

EventEmitter

Evented JavaScript for the browser
JavaScript
3,127
star
2

conjure

Interactive evaluation for Neovim (Clojure, Fennel, Janet, Racket, Hy, MIT Scheme, Guile, Python and more!)
Fennel
1,768
star
3

react-faux-dom

DOM like structure that renders to React (unmaintained, archived)
JavaScript
1,206
star
4

aniseed

Neovim configuration and plugins in Fennel (Lisp compiled to Lua)
Fennel
610
star
5

dotfiles

Configuration for Linux, i3, Kitty, Fish, Neovim and more
Fennel
509
star
6

depot

Find newer versions of your dependencies in your deps.edn file
Clojure
259
star
7

vim-enmasse

Edit every line in a quickfix list at the same time
Vim Script
208
star
8

magic-kit

A starter kit for Conjure, Aniseed and Neovim
Fennel
104
star
9

cljs-test-runner

Discover and run your ClojureScript tests
Clojure
86
star
10

Heir

Helper functions for prototypical inheritance in JavaScript
JavaScript
60
star
11

lazy-array

JavaScript lazy arrays, sort of like Clojure's seqs
JavaScript
56
star
12

nvim-local-fennel

Execute local Fennel Lisp files in Neovim upon startup
Lua
54
star
13

clojure-dap

DAP server for debugging Clojure over nREPL with CIDER's debugger
Clojure
52
star
14

tuple

A tiny JavaScript tuple implementation
JavaScript
49
star
15

propel

Propel helps you start Clojure(Script) REPLs with a prepl
Clojure
48
star
16

snowball

Voice activated Discord bot running on GCP
Clojure
41
star
17

vim-expand

Expand things like {foo,bar}, {1..10} and $HOME inline with a single command
Vim Script
30
star
18

nand2tetris

My workings for book / project. Don't copy them for the Coursera course!
Assembly
29
star
19

vim-scheme

Interact with MIT Scheme from Neovim (deprecated, use Conjure instead!)
Vim Script
29
star
20

gh-pages-theme

A clean concise theme for your GitHub projects
JavaScript
28
star
21

color

Color conversion functions for JavaScript
JavaScript
25
star
22

binary-search

Binary search implementation in JavaScript born from a couple of my blog posts
JavaScript
24
star
23

conjure-sourcery

Experimental rewrite of Conjure in Lua via Fennel - success, this is for historical purposes
Common Lisp
20
star
24

exemplary

Turns your examples into documentation and runnable tests.
Clojure
19
star
25

clojure-giants-shoulders

Tools and scripts I reach for every time I start a new Clojure project
Clojure
18
star
26

spacy-neovim

A base Neovim configuration template inspired by Spacemacs
Vim Script
17
star
27

StateMachine

JavaScript state machine
JavaScript
16
star
28

blog

My personal blog
CSS
15
star
29

github.js

Frontend JavaScript library for interacting with the GitHub API v3
JavaScript
14
star
30

vim-syntax-expand

Expand characters to code if not in a comment or string
Vim Script
13
star
31

bonsai

[WIP] Declarative DOM rendering with integrated state management for ClojureScript
Clojure
12
star
32

Spark

A lightweight yet powerful JavaScript library
JavaScript
12
star
33

d3-react

Render React elements with D3
JavaScript
12
star
34

lein-transcriptor

Execute all of your project's .repl files with transcriptor.
Clojure
11
star
35

Mappa

Map functions under your own names to create your own set of tools.
JavaScript
9
star
36

neofib

An example Neovim plugin written in Rust using neovim-lib
Rust
7
star
37

lab

A place for experiments
CSS
7
star
38

kkslider

A super simple Neovim slide show plugin
Lua
7
star
39

bastion

[DEPRECATED] Combines a modern JavaScript toolchain into a single program so you can stop worrying about configuration and just get to work on your application
JavaScript
7
star
40

more

A framework for LESS, including mixins and a grid system
6
star
41

jsFiddle-extension

A Google chrome extension for browsing and creating fiddles.
JavaScript
6
star
42

clojure-hey-example

A demo repo for my blog post on deps.edn based projects
Clojure
5
star
43

brainfucks

Brainfuck VM implementations in various languages
Rust
5
star
44

conjure-deps

Runtime dependencies for Conjure
Clojure
5
star
45

nfnl-plugin-example

An example Neovim plugin witten in Fennel using nfnl
Fennel
4
star
46

tm-challenge

Room manifest manger built with React / Reflux
JavaScript
4
star
47

prepl-compliance-test

Checks a Clojure prepl server against a bunch of tests
Clojure
4
star
48

clj-dice-roller

Clojure ns that rolls dice, just a transcriptor example
Clojure
3
star
49

impl

[WIP] Homoiconic language with minimal syntax compiling to JavaScript
JavaScript
3
star
50

advent-of-code

My attempts at the Advent of Code
Clojure
3
star
51

sicp

Studying SICP
Scheme
3
star
52

clojure-wake-word-detection

Code for my blog post
Clojure
3
star
53

Package.js

Add package support to the browser
JavaScript
3
star
54

cljs-todo

Simple ClojureScript to do list example using Reagent and Bonsai
Clojure
2
star
55

vim-netrw-signs

Like vim-signify, but for netrw
Vim Script
2
star
56

astronvim-config

My personal AstroNvim user extensions. This is now migrated into my dotfiles repo.
Lua
2
star
57

wlhn-a-star

A* algorithm implementation in Clojure
Clojure
2
star
58

collatz

Collatz conjecture computation with snazzy rendering.
Clojure
2
star
59

crawlers

Clojure(Script) library to identify crawler and bot user agent strings
Clojure
2
star
60

oli.me.uk

[SUPER DEPRECATED] Only keeping for historical reasons.
Ruby
2
star
61

cljs-react-example

A simple example of loading React into ClojureScript from node_modules (yarn)
Clojure
2
star
62

conjure-shadow-cljs-example

How to hook Conjure up to shadow-cljs
Clojure
2
star
63

algos

My coursera algos code
Java
2
star
64

Clarity

A clean and responsive WordPress theme.
PHP
2
star
65

venturi

Hierarchical JavaScript dependency injection
JavaScript
1
star
66

sparkjs.co.uk

Website for the Spark JavaScript library
1
star
67

Physics

A particle physics engine built in JavaScript using MooTools
JavaScript
1
star
68

web-asset-compiler

Combines and minifies your CSS, LESS and JavaScript into one JavaScript file
JavaScript
1
star
69

euler

Solutions for my Project Euler account
Clojure
1
star
70

hello-godot

Experimenting with the Godot game engine
C
1
star
71

tarmac

Tiny and unopinionated AMD MVC JavaScript framework
JavaScript
1
star
72

HashStorage

Watches the URLs hash for changes and merges those changes with an existing object, allowing storage of complex data in a sharable URL
JavaScript
1
star
73

wlhn-elfs-dilemma

Prisoners dilemma client for the West London Hack Night 2017 Christmas special
Clojure
1
star
74

langs

My work for the "7 languages in 7 weeks" book
Io
1
star
75

cljit

Git implemented in Clojure from following along with Building Git
1
star
76

fenneldoc

Turn Fennel docstrings into rich markdown documentation (mirror)
Fennel
1
star
77

project-wide-operations

Project-wide operations in Vim, a talk for Vim London
1
star
78

conjure-loves-fennel

Doesn't work, just sharing so others can take it and run with it!
Lua
1
star
79

sentinel

[DEPRECATED] Watch source files for changes and processes them accordingly
JavaScript
1
star
80

vim-scotch

A few extra mappings for vim-fireplace
Vim Script
1
star
81

how-to-be-a-repl-sorcerer

Clojure
1
star
82

hack-the-tower-clojure-web

A small web project in Clojure from Hack the Tower
Clojure
1
star
83

aniseed-config-from-scratch

A demo repo from my YouTube video showing setting up an Aniseed based Neovim configuration
Vim Script
1
star
84

outline

A web based CSS grid generator.
JavaScript
1
star
85

simple-crypt-cljs

Simple encryption example in the browser with ClojureScript
Clojure
1
star
86

dwarf-fortress-nix

Dwarf Fortress on Nix
Nix
1
star
87

fizzbugged

A broken Clojure Fizz Buzz that I fix with Conjure in a blog post
Clojure
1
star
88

olical.github.io

DEPRECATED, see Olical/blog for new source code
HTML
1
star
89

forc

Clone of Clojure list comprehension in JavaScript
JavaScript
1
star
90

SparkAn

Animate CSS properties of the specified element - For the Spark JavaScript library
JavaScript
1
star
91

vimconf-2020

Lua
1
star
92

ChromePlate

A base layout for your Chrome apps and extensions
JavaScript
1
star
93

matchmaker

Constructs (fairly) balanced teams from the TAW attendance list
Clojure
1
star
94

life

Game of life implementations in different languages
Clojure
1
star
95

clojs

Repository for my post: A JavaScript / Clojure mashup
JavaScript
1
star
96

rust-book

Notes and exercises from working through the Rust book
Rust
1
star