diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff009fff..00592970 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,9 +30,14 @@ This is done by making most of the options of the type `types.nullOr ....`, and Most of nixvim is dedicated to wrapping neovim plugins such that we can configure them in Nix. To add a new plugin you need to do the following. -1. Add a file in the correct sub-directory of [plugins](plugins). This depends on your exact plugin. +1. Add a file in the correct sub-directory of [`plugins`](plugins). + - Most plugins should be added to [`plugins/by-name/`](plugins/by-name). + Plugins in `by-name` are automatically imported 🚀 + - Occasionally, you may wish to add a plugin to a directory outside of `by-name`, such as [`plugins/colorschemes`](plugins/colorschemes). + If so, you will also need to add your plugin to [`plugins/default.nix`](plugins/default.nix) to ensure it gets imported. + Note: the imports list is sorted and grouped. In vim, you can usually use `V` (visual-line mode) with the `:sort` command to achieve the desired result. -The vast majority of plugins fall into one of those two categories: +2. The vast majority of plugins fall into one of those two categories: - _vim plugins_: They are configured through **global variables** (`g:plugin_foo_option` in vimscript and `vim.g.plugin_foo_option` in lua).\ For those, you should use the `lib.nixvim.vim-plugin.mkVimPlugin`.\ -> See [this plugin](plugins/utils/direnv.nix) for an example. @@ -40,7 +45,7 @@ The vast majority of plugins fall into one of those two categories: For those, you should use the `lib.nixvim.neovim-plugin.mkNeovimPlugin`.\ -> See the [template](plugins/TEMPLATE.nix). -2. Add the necessary parameters for the `mkNeovimPlugin`/`mkVimPlugin`: +3. Add the necessary parameters for the `mkNeovimPlugin`/`mkVimPlugin`: - `name`: The name of the plugin. The resulting nixvim module will have `plugins.` as a path.\ For a plugin named `foo-bar.nvim`, set this to `foo-bar` (subject to exceptions). - `originalName`: The "real" name of the plugin (i.e. `foo-bar.nvim`). This is used mostly in documentation. @@ -55,10 +60,6 @@ The vast majority of plugins fall into one of those two categories: See below for more information - `settingsExample`: An example of what could the `settings` attrs look like. -3. Add to plugins/default.nix - - As a final step, please add your plugin to `plugins/default.nix` to ensure it gets imported. - - Note: the imports list is sorted and grouped. In vim, you can usually use `V` (visual-line mode) with the `:sort` command to achieve the desired result. - [nixpkgs-maintainers]: https://github.com/NixOS/nixpkgs/blob/master/maintainers/maintainer-list.nix #### Declaring plugin options diff --git a/flake-modules/dev/default.nix b/flake-modules/dev/default.nix index e8461d28..1bd4419b 100644 --- a/flake-modules/dev/default.nix +++ b/flake-modules/dev/default.nix @@ -77,6 +77,16 @@ args = [ ".#checks.${system}.maintainers" ]; pass_filenames = false; }; + plugins-by-name = { + enable = true; + name = "plugins-by-name"; + description = "Check `plugins/by-name` when it's modified."; + files = "^(?:tests/test-sources/)?plugins/by-name/"; + package = pkgs.nix; + entry = "nix build --no-link --print-build-logs"; + args = [ ".#checks.${system}.plugins-by-name" ]; + pass_filenames = false; + }; }; }; }; diff --git a/flake-modules/tests.nix b/flake-modules/tests.nix index bf80e78c..12b3f990 100644 --- a/flake-modules/tests.nix +++ b/flake-modules/tests.nix @@ -45,6 +45,8 @@ maintainers = import ../tests/maintainers.nix { inherit pkgs; }; + plugins-by-name = pkgs.callPackage ../tests/plugins-by-name.nix { inherit evaluatedNixvim; }; + generated = pkgs.callPackage ../tests/generated.nix { }; package-options = pkgs.callPackage ../tests/package-options.nix { inherit evaluatedNixvim; }; diff --git a/modules/plugins.nix b/modules/plugins.nix index 5c864b3a..fdb64b92 100644 --- a/modules/plugins.nix +++ b/modules/plugins.nix @@ -1,4 +1,17 @@ -{ ... }: +{ lib, ... }: +let + inherit (builtins) readDir pathExists; + inherit (lib.attrsets) foldlAttrs; + inherit (lib.lists) optional optionals; + by-name = ../plugins/by-name; +in { - imports = [ ../plugins/default.nix ]; + imports = + [ ../plugins ] + ++ optionals (pathExists by-name) ( + foldlAttrs ( + prev: name: type: + prev ++ optional (type == "directory") (by-name + "/${name}") + ) [ ] (readDir by-name) + ); } diff --git a/tests/plugins-by-name.nix b/tests/plugins-by-name.nix new file mode 100644 index 00000000..a8749ed8 --- /dev/null +++ b/tests/plugins-by-name.nix @@ -0,0 +1,151 @@ +{ + lib, + evaluatedNixvim, + linkFarmFromDrvs, + runCommandNoCCLocal, +}: +let + by-name = ../plugins/by-name; + options = lib.collect lib.isOption evaluatedNixvim.options; + + # Option namespaces that we allow by-name plugins to declare + knownPluginNamespaces = [ + "colorschemes" + "plugins" + ]; + + # Group by-name children by filetype; "regular", "directory", "symlink" and "unknown". + children = + let + apply = + prev: name: type: + prev // { ${type} = prev.${type} ++ [ name ]; }; + + nil = { + regular = [ ]; + directory = [ ]; + symlink = [ ]; + unknown = [ ]; + }; + in + lib.foldlAttrs apply nil (builtins.readDir by-name); + + # Find plugins by looking for `*.*.enable` options that are declared in `plugins/by-name` + by-name-enable-opts = + let + regex = ''/nix/store/[^/]+/plugins/by-name/(.*)''; + optionalPair = + opt: file: + let + result = builtins.match regex file; + in + lib.optional (result != null) { + # Use the file name relative to `plugins/by-name/` + name = builtins.head result; + # Use only the first two parts of the option location + value = lib.genList (builtins.elemAt opt.loc) 2; + }; + in + lib.pipe options [ + (builtins.filter (opt: builtins.length opt.loc == 3 && lib.last opt.loc == "enable")) + (builtins.concatMap (opt: (builtins.concatMap (optionalPair opt) opt.declarations))) + builtins.listToAttrs + ]; +in +linkFarmFromDrvs "plugins-by-name" [ + # Ensures all files matching `plugins/by-name/*` are directories + (runCommandNoCCLocal "file-types" + { + __structuredAttrs = true; + inherit (children) regular symlink unknown; + } + '' + declare -i errs=0 + + showErrs() { + type="$1" + shift + + if (( $# > 0 )); then + ((++errs)) + echo "Unexpected $type in plugins/by-name ($#):" + for f in "$@"; do + echo " - $f" + done + echo + fi + } + + showErrs 'symlinks' "''${symlink[@]}" + showErrs 'regular files' "''${regular[@]}" + showErrs 'unknown-type files' "''${unknown[@]}" + + if (( $errs > 0 )); then + exit 1 + fi + touch $out + '' + ) + + # Ensures all plugin enable options are declared in a directory matching the plugin name + (runCommandNoCCLocal "mismatched-plugin-names" + { + __structuredAttrs = true; + + options = lib.pipe by-name-enable-opts [ + (lib.filterAttrs (file: loc: file != lib.last loc)) + (lib.mapAttrs (file: loc: lib.showOption loc)) + ]; + + passthru = { + inherit evaluatedNixvim; + }; + } + '' + if (( ''${#options[@]} > 0 )); then + echo "Found plugin modules with mismatched option & directory names (''${#options[@]})" + for file in "''${!options[@]}"; do + echo "- ''${options[$file]} is declared in '$file'" + done + exit 1 + fi + touch $out + '' + ) + + # Ensure all plugin enable option are declared under an expected namespace + (runCommandNoCCLocal "unknown-plugin-namespaces" + { + __structuredAttrs = true; + + # I'm sorry, I couldn't help implementing oxford-comma... + expected = + let + len = builtins.length knownPluginNamespaces; + in + lib.concatImapStringsSep ", " ( + i: str: lib.optionalString (i > 1 && i == len) "or " + "`${str}`" + ) knownPluginNamespaces; + + options = lib.pipe by-name-enable-opts [ + (lib.filterAttrs (file: loc: !(builtins.elem (builtins.head loc) knownPluginNamespaces))) + (lib.mapAttrs (file: loc: "`${lib.showOption loc}`")) + ]; + + passthru = { + inherit evaluatedNixvim; + }; + } + '' + if (( ''${#options[@]} > 0 )); then + echo "Found plugin modules with unknown option namespaces (''${#options[@]})" + echo "Expected all plugins to be scoped as $expected" + for file in "''${!options[@]}"; do + echo "- ''${options[$file]} is declared in '$file'" + done + exit 1 + fi + touch $out + '' + ) +]