Skip to content

aileot/nvim-thyme

Repository files navigation

πŸ•› nvim-thyme

Zero-overhead Fennel JIT compiler for Neovim

Also welcome, non-lispers
How about trying :Fnl (vim.tbl_extend :force {:foo :bar} {:foo :qux (uh…, typos? Β―\_(ツ)_/Β―),
or :=vim.tbl_extend("force", {foo = "bar"}, {foo = "baz"})?

badge/test badge/semver badge/license
badge/fennel

Welcome Aboard β€’ Installation β€’ Usage β€’ Reference β€’ FAQ

✨ Features

  • JIT Compiler: Compile fennel source at nvim runtime.
  • Rollbacks: Safely roll back to the last successfully compiled backups if compilation fails.
  • Integrations: Evaluate fennel code in cmdline and keymap with the following features:

Caution

Please note that undocumented features are subject to change without notice, regardless of semantic versioning.

πŸ’₯ Comparisons

Feature nvim-thyme hotpot.nvim nfnl tangerine.nvim
Zero Startup Overhead βœ… ❌ βœ… ❌
Runtime Compiler βœ… βœ… ❌ βœ…
(Compile in lua/
at runtime)
βœ…
(optional)
βœ…
(with :source)
❌ ❌
Safety Rollbacks βœ… ❌ ❌ ❌
Parinfer Integration
in Cmdline mode
βœ… ❌ ❌ ❌
Fennel Dependency Not embedded
(Any compatible version
should be on &rtp.
)
Embedded Embedded Embedded

See also Migration Guide and Ex Command Comparisons below.

πŸ”₯ Motivations

  • To cut down startuptime, checking Fennel should be skipped at startup if possible.
  • I don't like to mess up lua/ as I still write Lua when it seems to be more comfortable than Fennel. (Type annotation helps us very much.)

Tip

Optionally, you can manage your Fennel files under lua/ instead of fnl/ directory. The relevant options are fnl-dir and macro-path.

…and more features! So, this project started from scratch.

βœ”οΈ Requirements

Optional Dependencies

  • luajit or lua5.1 (to compile fennel on &rtp on make)
    If none of them is available, nvim --clean --headless -l will be used as a lua fallback.
  • A tree-sitter parser for fennel like tree-sitter-fennel, or via nvim-treesitter on &rtp.
  • The parinfer-rust on &rtp (to improve UX on the commands and keymaps)

πŸŽ‰ Welcome Aboard

  1. Install nvim-thyme with lazy.nvim.

(If you've decided to go along with Fennel, please skip to the Installation section below.)

require("lazy").setup({
  ---@type LazySpec
  {
    "aileot/nvim-thyme",
    version = "^v1.6.0",
    dependencies = {
      { "https://git.sr.ht/~technomancy/fennel" },
    },
    lazy = false,
    priority = 1000,
    build = ":lua require('thyme').setup(); vim.cmd('ThymeCacheClear')",
    init = function()
      -- Make your Fennel modules loadable.
      table.insert(package.loaders, function(...)
        return require("thyme").loader(...)
      end)
      local thyme_cache_prefix = vim.fn.stdpath("cache") .. "/thyme/compiled"
      vim.opt.rtp:prepend(thyme_cache_prefix)
    end,
    config = function()
      -- Create the helper interfaces.
      require("thyme").setup()
    end,
  },
  -- Optional
  {
    "aileot/nvim-laurel",
    build = ":lua require('thyme').setup(); vim.cmd('ThymeCacheClear')",
  },
  {
    "eraserhd/parinfer-rust",
    build = "cargo build --release",
  },
  -- and other plugin specs...
})

Warning

With the config above, you cannot load Fennel modules before the setup of lazy.nvim, but only load Fennel modules after the init setup is done. Please follow the Installation section below if you'd like to write Fennel more!

2. Test Interactive Features in Cmdline

:Fnl (+ 1 2 3) " Evaluate Fennel expression
:Fnl (vim.notify "Hello, Fennel!") " Call nvim APIs
:FnlBuf % " Evaluate Fennel expression in the current buffer

πŸ“¦ Installation

1. Ensure to Install Plugins (3 steps)

1. Make sure to download, and add the path to &runtimepath

It's recommended to define a bootstrap function for simplicity…

(The collapse shows a snippet for folke/lazy.nvim.)

local function bootstrap(url)
  -- To manage the version of repo, the path should be where your plugin manager will download it.
  local name = url:gsub("^.*/", "")
  local path = vim.fn.stdpath("data") .. "/lazy/" .. name
  if not vim.loop.fs_stat(path) then
    vim.fn.system({
      "git",
      "clone",
      "--filter=blob:none",
      url,
      path,
    })
  end
  vim.opt.runtimepath:prepend(path)
end
-- Given the `bootstrap` function defined above,
bootstrap("https://git.sr.ht/~technomancy/fennel")
bootstrap("https://github.com/aileot/nvim-thyme")
-- (Optional) Install your favorite plugin manager.
bootstrap("https://github.com/folke/lazy.nvim")
-- (Optional) Install some Fennel macro plugins before the setup of the plugin manager...
bootstrap("https://github.com/aileot/nvim-laurel")

2. Add require("thyme").loader to package.loaders

-- Wrapping the `require` in `function-end` is important for lazy-load.
table.insert(package.loaders, function(...)
  return require("thyme").loader(...) -- Make sure to `return` the result!
end)

3. Add a cache path for lua cache to &runtimepath

-- Note: Add a cache path to &rtp. The path MUST include the literal substring "/thyme/compile".
local thyme_cache_prefix = vim.fn.stdpath("cache") .. "/thyme/compiled"
vim.opt.rtp:prepend(thyme_cache_prefix)
-- Note: `vim.loader` internally cache &rtp, and recache it if modified.
-- Please test the best place to `vim.loader.enable()` by yourself.
vim.loader.enable() -- (optional) before the `bootstrap`s above, it could increase startuptime.

2. (Optional) Manage nvim-thyme with Plugin Manager

Caution

Please make sure to disable the lazy.nvim's performance.rtp.reset option. (The option is enabled by default.) Otherwise, you would get into "loop or previous error," or would be complained that the literal substring "/thyme/compile" is missing in &runtimepath.

With folke/lazy.nvim,
require("lazy").setup({
  spec = {
    {
      "aileot/nvim-thyme",
      version = "^v1.6.0",
      dependencies = {
        { "https://git.sr.ht/~technomancy/fennel" },
      },
      build = ":lua require('thyme').setup(); vim.cmd('ThymeCacheClear')",
      -- For config, see the "Setup Optional Interfaces" section
      -- and "Options in .nvim-thyme.fnl" below!
      -- config = function()
      -- end,
    },
    -- If you also manage macro plugin versions, please clear the Lua cache on the updates!
    {
      "aileot/nvim-laurel",
      build = ":lua require('thyme').setup(); vim.cmd('ThymeCacheClear')",
      -- and other settings
    },
    -- Optional dependency plugin.
    {
      "eraserhd/parinfer-rust",
      build = "cargo build --release",
    },
    -- and other plugin specs...
  },
  performance = {
    rtp = {
      reset = false, -- Important! It's unfortunately incompatible with nvim-thyme.
    },
  },
})

(If you also manage macro plugin versions, please clear the Lua cache on the updates! You can automate it either on spec hook like above, on user event hook like below; otherwise, please run :ThymeCacheClear manually.)

-- If you also manage other Fennel macro plugin versions, please clear the Lua cache on the updates!
vim.api.nvim_create_autocmd("User", {
  pattern = "LazyUpdate", -- for lazy.nvim
  callback = function()
    require("thyme").setup()
    vim.cmd("ThymeCacheClear")
  end,
})

3. Setup Optional Interfaces

To optimize the nvim startuptime, nvim-thyme suggests you to define the Ex command interfaces and its fnl file state checker some time after VimEnter. For example,

-- In init.lua,
vim.api.nvim_create_autocmd("VimEnter", {
  once = true,
  callback = function() -- You can substitute vim.schedule_wrap if you don't mind its tiny overhead.
    vim.schedule(function()
      require("thyme").setup()
    end)
  end,
})

4. Start nvim

If you don't have .nvim-thyme.fnl at vim.fn.stdpath('config'), generally $XDG_CONFIG_HOME/nvim, you will be asked to generate .nvim-thyme.fnl there with recommended config. See the Configuration section below.

5. checkhealth

Ensure the setup by :checkhealth thyme.

πŸš€ Usage

Please read the reference for the details and additional features.

βš™οΈ Configuration

Options in .nvim-thyme.fnl

nvim-thyme manages all the configurations in a separate config file .nvim-thyme.fnl instead of thyme.setup.

Note

This is a point to optimize the nvim startuptime with the JIT compiler. Apart from thyme.setup but with .nvim-thyme.fnl, the configurations can be lazily evaluated only by need.

Here is a sample config:

{:max-rollback 5
 :compiler-options {:correlate true
                    ;; :compilerEnv _G
                    :error-pinpoint ["|>>" "<<|"]}
 :fnl-dir "fnl"
 :macro-path "./fnl/?.fnlm;./fnl/?/init-macros.fnlm;./fnl/?.fnl;./fnl/?/init-macros.fnl;./fnl/?/init.fnl"}

However, you don't have to prepare it by yourself!

If .nvim-thyme.fnl is missing at vim.fn.stdpath('config') on nvim startup, you will be asked for confirmation. Once you agree, a new .nvim-thyme.fnl will be generated to vim.fn.stdpath('config') with recommended settings there. The generated file is a copy of .nvim-thyme.fnl.example.

For all the available options, see Options in the reference.

🚚 Migration Guide

From hotpot.nvim

require("hotpot").setup({
  compiler = {
    macros = {
      env = "_COMPILER",
      correlate = true,
    },
    modules = {
      correlate = true,
    },
  },
})
;; in .nvim-thyme.fnl at stdpath('config')
;; The thyme's searchers always set "_COMPILER" at "env" in evaluating macro modules.
{:compiler-options {:correlate true}

From nfnl.nvim

  1. (important) Rename lua/ at vim.fn.stdpath('config'), like mv lua/ lua.bk/.
    Otherwise, there's some chances that nvim would unquestionably load lua files under the lua/ directory apart from nvim-thyme.
  2. Add codes to enable thyme's auto-compile system. See the Installation section above.
  3. Start nvim. You will be asked to generate .nvim-thyme.fnl at the directory vim.fn.stdpath('config').

From tangerine.nvim

require([[tangerine]]).setup({})
;; in .nvim-thyme.fnl at stdpath('config')
{:compiler-options {:compilerEnv _G
                    :useBitLib true}

🍿 Ex Command Comparisons

Note: nvim-thyme only provides user commands after you call thyme.setup for performance.

Evaluate expression and print the result

With parinfer-rust,

" nvim-thyme
:Fnl (+ 1 1
" hotpot.nvim
:Fnl= (+ 1 1)
" tangerine.nvim
:Fnl (print (+ 1 1))

Evaluate expression without printing the result

" nvim-thyme
:silent Fnl (+ 1 1
" hotpot.nvim
:Fnl (+ 1 1)
" tangerine.nvim
:Fnl (+ 1 1)

Evaluate current file

" nvim-thyme
:FnlFile %
" hotpot.nvim
:Fnlfile %
" nfnl.nvim
:NfnlFile (vim.fn.expand "%:p")
" tangerine.nvim
:FnlFile %:p

Not in Plan

  • Unlike tangerine.nvim, nvim-thyme does not compile $XDG_CONFIG_HOME/nvim/init.fnl.
  • Unlike hotpot.nvim, nvim-thyme does not load plugin/*.fnl, ftplugin/*.fnl, lsp/*.fnl and so on; nvim-thyme does not support Vim commands (e.g., :source and :runtime) to load your Fennel files. nvim-thyme only supports Lua/Fennel loader like require.
  • Unlike nfnl, nvim-thyme does not compile Fennel files which is not loaded in nvim runtime by default. If you still need to compile Fennel files in a project apart from nvim runtime, you have several options:
    • Define some autocmds in your config or in .nvim.lua.
    • Use another compiler plugin together like nfnl.
    • Use a task runner like overseer.nvim.
    • Use git hooks. (See the .githooks in this project as a WIP example. Help wanted.)

πŸ•ΆοΈ Disclosure

Misleading…?

  • As you may have noticed, the term of Zero overhead only means it does not affect startup time once compiled at an nvim runtime.
  • As you may have noticed, the term of JIT (Just-in-time) might be a bit misleading due to the convention.
    The JIT in this project is more like JIT in JIT Manufacturing than in JIT Compilation: it compiles missing modules, and optionally recompiles them on BufWritePost and FileChangedShellPost.

❓ FAQ

Q. Is it compatible with vim.loader?

A. Yes, it is. vim.loader.enable() optimizes the nvim-thyme loader.

Q: "loop or previous error"?

A. nvim-thyme is incompatible the option performance.rtp.reset of lazy.nvim.

Make sure you've disabled the lazy.nvim's performance.rtp.reset option. (The option is enabled by default.)

Q. Can I disable parinfer for editing buffers, keeping it enabled in the Cmdline integration?

A. Yes, you can. Just set the variable vim.g.parinfer_enabled to false.

Q. Does the rollback system help me avoid starting in nearly mother-naked nvim due to some misconfigurations?

A. Yes, but only for the modules written in Fennel.

Rollbacks are automatically applied when errors are detected at compile time. In addition to that, with the combinations of :ThymeRollbackSwitch and :ThymeRollbackMount, you can also roll back for runtime errors in compiled Lua.

However, it is recommended to put your configuration files under git management first in case nvim even fail to reach the lines that defines the rollback helper commands.

Q. How can I mix Fennel config with Lua config in a directory?

A. By default, or with the recommended config, nvim-thyme will make nvim load Fennel modules in lua/ directory as the default nvim loads the other Lua modules unless fnl/ exists at the directory that stdpath("config") returns (usually ~/.config/nvim).

Note that, if both foo.lua and foo.fnl exist at the lua/ directory, foo.lua is always loaded.

The relevant options are only fnl-dir and macro-path.

The collapse illustrates how to merge `fnl/` into the `lua/` directory as safely as possible.

(Assume your nvim config files are managed by git, at ~/.config/nvim.)

# Commit current status
git add -A
git commit -m 'save states before merging fnl/ into lua/'
# Note the current branch name (main or master, maybe)
git branch --show-current
# Create and switch a new branch. (The branch name is an example.)
git switch -c merge-fnl-into-lua
cd ~/.config/nvim
# Check the results with `--dry-run`.
git mv --dry-run fnl lua
git mv --verbose fnl lua
# Make sure your nvim can start without issues.
nvim

If you have any issues, reset to the previous states where fnl/ and lua/ have co-existed by the following command.

# Assume your default branch is `main`.
git reset --hard main
git switch main
# Put aside the previous cache directory.
mv ~/.cache/nvim/thyme{,.bk}

πŸ“š Acknowledgement

Thanks to Shougo for dein.vim the legendary. The design heavily inspires nvim-thyme.

Thanks to harrygallagher4 for nvim-parinfer-rust. The integration of nvim-thyme with parinfer is based in part on copy extracted from the project, so the file on parinfer is also on the license CC0-1.0.

About

πŸ•› Zero-overhead Fennel JIT compiler at runtime of Neovim with safety rollbacks

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •