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. 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. 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).\ - _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`.\ For those, you should use the `lib.nixvim.vim-plugin.mkVimPlugin`.\
-> See [this plugin](plugins/utils/direnv.nix) for an example. -> 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`.\ For those, you should use the `lib.nixvim.neovim-plugin.mkNeovimPlugin`.\
-> See the [template](plugins/TEMPLATE.nix). -> 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.\ - `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). 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. - `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 See below for more information
- `settingsExample`: An example of what could the `settings` attrs look like. - `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 [nixpkgs-maintainers]: https://github.com/NixOS/nixpkgs/blob/master/maintainers/maintainer-list.nix
#### Declaring plugin options #### Declaring plugin options

View file

@ -77,6 +77,16 @@
args = [ ".#checks.${system}.maintainers" ]; args = [ ".#checks.${system}.maintainers" ];
pass_filenames = false; 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; }; 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 { }; generated = pkgs.callPackage ../tests/generated.nix { };
package-options = pkgs.callPackage ../tests/package-options.nix { inherit evaluatedNixvim; }; 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
''
)
]