plugins/by-name: init

Add support for automatically importing any directories under
`plugins/by-name`.

Includes a validation test, which is run by CI and by the pre-commit hook.
This commit is contained in:
Matt Sturgeon 2024-09-04 16:22:17 +01:00
parent 6df273540c
commit faff32b9f1
No known key found for this signature in database
GPG key ID: 4F91844CED1A8299
5 changed files with 186 additions and 9 deletions

View file

@ -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/<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.<name>` 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

View file

@ -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;
};
};
};
};

View file

@ -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; };

View file

@ -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)
);
}

151
tests/plugins-by-name.nix Normal file
View file

@ -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
''
)
]