Configuring Neovim with fennel and home-manager


Home manager is a dotfiles manager that declaratively manages your configs using Nix.

One of the many programs that Home manager can configure is Neovim; it can even trivially install plugins, LSPs, and formatters, no lazy/mason/whatever needed.

The below config installs Telescope, and adds/configures nvim-lspconfig using Lua.

{pkgs, ...}: {
  programs.neovim = {
    enable = true;
    plugins = with pkgs.vimPlugins; [
      telescope-nvim
      {
        plugin = nvim-lspconfig;
        config = builtins.readFile ./nvim/plugins/lspconfig.lua;
        type = "lua";
      }
    ];
  };
}

Note the type = "lua"; attribute; it accepts your standard lua and viml, but we’re more interested in the more lispy option: fennel.

  type = mkOption {
        type = types.either (types.enum [
          "lua"
          "viml"
          "teal"
          "fennel"
        ]) types.str;
        description = "Language used in config. Configurations are aggregated per-language.";
        default = "viml";
      };

But, if we scroll down in that same file (modules/programs/neovim.nix), it only automatically combines and adds config values for plugins configured with Lua or Vimscript. it just, silently ignores plugin configs with type fennel or teal. That’s fair - only Vimscript and Lua are first-party options, but its still a little disappointing.

We can do it ourselves though using Hotpot and programs.neovim.plugins._.runtime. The latter allows us to add files to Neovim’s runtime directory. While Hotpot will automagically enable require-ing any fennel files in <runtimepath>/fnl/ directory as-if they were Lua files.

So, our plugin array changes to:

plugins = with pkgs.vimPlugins; [
  {
    plugin = telescope-nvim;
    type = "fennel";
    runtime."fnl/telescopeOptions.fnl".text = builtins.readFile ./nvim/fnl/telescope.fnl;
  }
  {
    plugin = hotpot-nvim;
    config = let
      requireables =
        config.programs.neovim.plugins
        |> builtins.filter (p: p.type == "fennel")
        |> builtins.concatMap (p: builtins.attrNames p.runtime)
        |> builtins.map (rtf: lib.removeSuffix ".fnl" (builtins.baseNameOf rtf));
    in
      ''
        require("hotpot")
      ''
      + lib.concatStrings (map (name: "require(\"${name}\")\n") requireables);
    type = "lua";
  }
];

The Hotpot configuration simply gets the names of all the fennel runtime files, and requires them in init.lua. Additionally, the plugin order only affects the order that the Lua configs are concatenated into init.lua. Nix will correctly resolve the requireables array - without infinite recursion - every time.

Note: You cannot name your runtimedir fennel file the same name as the plugin! require("telescope") will load the plugin, not your config for it.

For non-plugin configuration, you can add them to Hotpot’s runtime attribute, then manually add them to requireables. Just make sure hotpot is required first!

{
  plugin = hotpot-nvim;
  runtime."fnl/options.fnl".text = builtins.readFile ./nvim/fnl/options.fnl;
  config = let
    requireables =
      ["hotpot" "options"]
      ++ (config.programs.neovim.plugins
        |> builtins.filter (p: p.type == "fennel")
        |> builtins.concatMap (p: builtins.attrNames p.runtime)
        |> builtins.map (p':
          p'
          |> builtins.baseNameOf
          |> lib.removeSuffix ".fnl"));
  in
    lib.concatStrings (map (name: "require(\"${name}\")\n") requireables);
  type = "lua";
}