diff --git a/lib/maintainers.nix b/lib/maintainers.nix index 275634dbc..3ce229827 100644 --- a/lib/maintainers.nix +++ b/lib/maintainers.nix @@ -107,4 +107,10 @@ githubId = 6452260; name = "Andrew Plaza"; }; + my7h3le = { + email = "mandate-word-dupe@duck.com"; + github = "my7h3le"; + githubId = 7899547; + name = "Tash"; + }; } diff --git a/plugins/pluginmanagers/lazy.nix b/plugins/pluginmanagers/lazy.nix index 1c11c4af7..5de7eee59 100644 --- a/plugins/pluginmanagers/lazy.nix +++ b/plugins/pluginmanagers/lazy.nix @@ -1,222 +1,340 @@ { - lib, - helpers, config, + lib, pkgs, ... }: -with lib; let - cfg = config.plugins.lazy; - lazyPlugins = cfg.plugins; + inherit (lib.nixvim) + defaultNullOpts + mkNullOrOption + mkNullOrLuaFn + mkNullOrStrLuaFnOr + ; + inherit (lib) types; + + # The regex used in this match function matches a string containing two + # segments separated by a single forward slash ('/'). Each segment is + # composed of one or more characters that are neither newlines ('\n') + # nor forward slashes ('/'). + # + # The pattern ensures that both segments must be non-empty and must not + # include line breaks or slashes within them. + isShortGitURL = x: lib.isStringLike x && builtins.match "[^\n/]+/[^\n/]+" (toString x) != null; + + # The regex used in this match function matches any string that starts + # with either `https://` or `http://`. + isGitURL = x: lib.isStringLike x && builtins.match "https?://.*" (toString x) != null; + + inherit (config) isDocs; +in +lib.nixvim.neovim-plugin.mkNeovimPlugin { + name = "lazy"; + originalName = "lazy.nvim"; + package = "lazy-nvim"; + + maintainers = with lib.maintainers; [ + MattSturgeon + my7h3le + ]; + + settingsOptions = with types; { + git.url_format = defaultNullOpts.mkStr "https://github.com/%s.git" '' + The default url format that `lazy.nvim` expects short plugin urls to be + in. (See: upstream docs for `config.git.url_format` defined here + https://lazy.folke.io/configuration); + ''; + + dev.fallback = defaultNullOpts.mkBool false '' + When false, `lazy.nvim` won't try to use git to fetch local plugins that + don't exist. + ''; + + install.missing = defaultNullOpts.mkBool false '' + When false, `lazy.nvim` won't try to install missing plugins on startup. + Setting this to true won't increase startup time. + ''; + }; - processPlugin = - plugin: + extraOptions = let - mkEntryFromDrv = - p: - if lib.isDerivation p then - { - name = "${lib.getName p}"; - path = p; - } - else - { - name = "${lib.getName p.pkg}"; - path = p.pkg; - }; - processDependencies = - if plugin ? dependencies && plugin.dependencies != null then - builtins.concatMap processPlugin plugin.dependencies - else - [ ]; - in - [ (mkEntryFromDrv plugin) ] ++ processDependencies; - - processedPlugins = builtins.concatLists (builtins.map processPlugin lazyPlugins); - lazyPath = pkgs.linkFarm "lazy-plugins" processedPlugins; -in -{ - options = { - plugins.lazy = { - enable = mkEnableOption "lazy.nvim"; + shortGitURL = lib.mkOptionType { + name = "shorGitURL"; + description = "a short git url of the form `owner/repo`"; + descriptionClass = "noun"; + check = isShortGitURL; + merge = lib.mergeEqualOption; + }; - gitPackage = lib.mkPackageOption pkgs "git" { - nullable = true; + gitURL = lib.mkOptionType { + name = "gitURL"; + description = "a git url"; + descriptionClass = "noun"; + check = isGitURL; + merge = lib.mergeEqualOption; }; - plugins = + lazyPluginSourceType = with types; - let - pluginType = either package (submodule { - options = { - dir = helpers.mkNullOrOption str "A directory pointing to a local plugin"; - - pkg = mkOption { - type = package; - description = "Vim plugin to install"; - }; + oneOf [ + package + path + shortGitURL + gitURL + ]; + + lazyPluginType = + with types; + types.coercedTo lazyPluginSourceType (source: { inherit source; }) ( + submodule ( + { config, ... }: - name = helpers.mkNullOrOption str "Name of the plugin to install"; - - dev = helpers.defaultNullOpts.mkBool false '' - When true, a local plugin directory will be used instead. - See config.dev - ''; - - lazy = helpers.defaultNullOpts.mkBool true '' - When true, the plugin will only be loaded when needed. - Lazy-loaded plugins are automatically loaded when their Lua modules are required, - or when one of the lazy-loading handlers triggers - ''; - - enabled = helpers.defaultNullOpts.mkStrLuaFnOr types.bool "`true`" '' - When false then this plugin will not be included in the spec. (accepts fun():boolean) - ''; - - cond = helpers.defaultNullOpts.mkStrLuaFnOr types.bool "`true`" '' - When false, or if the function returns false, - then this plugin will not be loaded. Useful to disable some plugins in vscode, - or firenvim for example. (accepts fun(LazyPlugin):boolean) - ''; - - dependencies = helpers.mkNullOrOption (helpers.nixvimTypes.eitherRecursive str listOfPlugins) "Plugin dependencies"; - - init = helpers.mkNullOrLuaFn "init functions are always executed during startup"; - - config = helpers.mkNullOrStrLuaFnOr (types.enum [ true ]) '' - config is executed when the plugin loads. - The default implementation will automatically run require(MAIN).setup(opts). - Lazy uses several heuristics to determine the plugin's MAIN module automatically based on the plugin's name. - See also opts. To use the default implementation without opts set config to true. - ''; - - main = helpers.mkNullOrOption str '' - You can specify the main module to use for config() and opts(), - in case it can not be determined automatically. See config() - ''; - - submodules = helpers.defaultNullOpts.mkBool true '' - When false, git submodules will not be fetched. - Defaults to true - ''; - - event = - with helpers.nixvimTypes; - helpers.mkNullOrOption (maybeRaw (either str (listOf str))) "Lazy-load on event. Events can be specified as BufEnter or with a pattern like BufEnter *.lua"; - - cmd = - with helpers.nixvimTypes; - helpers.mkNullOrOption (maybeRaw (either str (listOf str))) "Lazy-load on command"; - - ft = - with helpers.nixvimTypes; - helpers.mkNullOrOption (maybeRaw (either str (listOf str))) "Lazy-load on filetype"; - - keys = - with helpers.nixvimTypes; - helpers.mkNullOrOption (maybeRaw (either str (listOf str))) "Lazy-load on key mapping"; - - module = helpers.mkNullOrOption (enum [ false ]) '' - Do not automatically load this Lua module when it's required somewhere - ''; - - priority = helpers.mkNullOrOption number '' - Only useful for start plugins (lazy=false) to force loading certain plugins first. - Default priority is 50. It's recommended to set this to a high number for colorschemes. - ''; - - optional = helpers.defaultNullOpts.mkBool false '' - When a spec is tagged optional, it will only be included in the final spec, - when the same plugin has been specified at least once somewhere else without optional. - This is mainly useful for Neovim distros, to allow setting options on plugins that may/may not be part - of the user's plugins - ''; - - opts = - with helpers.nixvimTypes; - helpers.mkNullOrOption (maybeRaw (attrsOf anything)) '' - opts should be a table (will be merged with parent specs), - return a table (replaces parent specs) or should change a table. - The table will be passed to the Plugin.config() function. - Setting this value will imply Plugin.config() + { + freeformType = attrsOf anything; + options = { + source = mkNullOrOption lazyPluginSourceType '' + The `source` attribute can either be one of: + + - a local plugin directory path. + - a full plugin url. + - a short plugin url. + + If a short plugin url is given e.g. "echasnovski/mini.ai" + it will be expanded by `lazy.nvim` using + `plugins.lazy.settings.git.url_format` + ``` ''; - }; - }); - - listOfPlugins = types.listOf pluginType; - in - mkOption { - type = listOfPlugins; - default = [ ]; - description = "List of plugins"; - }; - }; - }; - config = mkIf cfg.enable { - extraPlugins = [ pkgs.vimPlugins.lazy-nvim ]; + name = mkNullOrOption str "Name of the plugin to install"; - extraPackages = [ cfg.gitPackage ]; + dev = defaultNullOpts.mkBool false '' + When true, `lazy.nvim` will look for this plugin in the local + plugin directory defined at `plugin.lazy.settings.dev.path`. + ''; - extraConfigLua = - let - pluginToLua = - plugin: - let - keyExists = keyToCheck: attrSet: lib.elem keyToCheck (lib.attrNames attrSet); - in - if isDerivation plugin then - { dir = "${lazyPath}/${lib.getName plugin}"; } - else - { - "__unkeyed" = plugin.name; - - inherit (plugin) - cmd - cond - config - dev - enabled - event - ft - init - keys - lazy - main - module - name - optional - opts - priority - submodules - ; - - dependencies = helpers.ifNonNull' plugin.dependencies ( - if isList plugin.dependencies then (pluginListToLua plugin.dependencies) else plugin.dependencies - ); + lazy = defaultNullOpts.mkBool true '' + When true, the plugin will only be loaded when needed. + + Lazy-loaded plugins are automatically loaded when their Lua + modules are required, or when one of the lazy-loading + handlers are triggered. + + You can define which triggers load this plugin using + `plugins.lazy.plugins..[event|cmd|ft|keys]`. + ''; + + enabled = defaultNullOpts.mkStrLuaFnOr bool true '' + When false or if a function that returns false is defined + then this plugin will not be included in the final spec. The + plugin will also be uninstalled when true if the plugin is an + out of tree non nix package plugin. (accepts fun():boolean). + ''; + + cond = defaultNullOpts.mkStrLuaFnOr bool true '' + Behaves the same as enabled, but won't uninstall the plugin + when the condition is false. Useful to disable some plugins + in vscode, or firenvim for example. (this only affects out of + tree non nix package plugins as those are the only ones that + will be uninstalled by `lazy.nvim`). + ''; + + # WARNING: Be very careful if changing the type of + # `dependencies`. Choosing the wrong type may cause a stack + # overflow due to infinite recursion, and it's very possible + # that the test cases won't catch this problem. To be safe, + # perform thorough manual testing if you do change the type of + # `dependencies`. Also use `types.eitherRecursive` instead of + # `types.either` here, as using just `types.either` also leads + # to stack overflow. + dependencies = + let + lazyPluginDependenciesType = + if isDocs then + # Use a stub type for documentation purposes + # We don't need to repeat all the plugin-type sub-options again in the docs + # It'd also be infinitely recursive + lib.mkOptionType { + name = "pluginDependencies"; + description = "plugin submodule"; + descriptionClass = "noun"; + check = throw "should not be used"; + merge = throw "should not be used"; + } + else + # NOTE: use attrsOf for the LHS, because coercedTo only supports submodules on the RHS + types.coercedTo (either (attrsOf anything) lazyPluginSourceType) lib.toList (listOf lazyPluginType); + in + mkNullOrOption lazyPluginDependenciesType '' + A list of plugin names or plugin specs that should be + loaded when the plugin loads. Dependencies are always + lazy-loaded unless specified otherwise. When specifying a + name, make sure the plugin spec has been defined somewhere + else. This can also be a single string such as for a short + plugin url (See: https://lazy.folke.io/spec). + ''; + + init = mkNullOrLuaFn "init functions are always executed during startup"; + + config = mkNullOrStrLuaFnOr (enum [ true ]) '' + config is executed when the plugin loads. + + The default implementation will automatically run require(MAIN).setup(opts). + Lazy uses several heuristics to determine the plugin's MAIN module automatically based on the plugin's name. + See also opts. To use the default implementation without opts set config to true. + ''; - dir = - if plugin ? dir && plugin.dir != null then plugin.dir else "${lazyPath}/${lib.getName plugin.pkg}"; - }; + main = mkNullOrOption str '' + You can specify the main module to use for config() and opts(), + in case it can not be determined automatically. See config() + ''; - pluginListToLua = map pluginToLua; + submodules = defaultNullOpts.mkBool true '' + When false, git submodules will not be fetched. + Defaults to true + ''; + + event = mkNullOrOption (maybeRaw (either str (listOf str))) "Lazy-load on event. Events can be specified as BufEnter or with a pattern like BufEnter *.lua"; + + cmd = mkNullOrOption (maybeRaw (either str (listOf str))) "Lazy-load on command"; + + ft = mkNullOrOption (maybeRaw (either str (listOf str))) '' + Lazy-load plugin on filetype e.g.: + + ```nix + plugins.lazy.plugins = with pkgs.vimPlugins; [ + { + source = neorg; + # Only load plugin on "norg" filetyes + ft = "norg"; + opts = { + load."['core.defaults']" = [ ]; + }; + } + ]; + ``` + ''; - plugins = pluginListToLua cfg.plugins; + keys = mkNullOrOption (maybeRaw (either str (listOf str))) "Lazy-load on key mapping"; + + module = mkNullOrOption (enum [ false ]) '' + Do not automatically load this Lua module when it's required somewhere. + ''; + + priority = defaultNullOpts.mkInt 50 '' + Only useful for start plugins i.e. + `plugins.lazy.plugins..lazy = false` to force loading + certain plugins first. Default priority is 50. It's + recommended to set this to a high number for colorschemes. + ''; + + optional = defaultNullOpts.mkBool false '' + Optional specs are only included in the final configuration + if the corresponding plugin is also specified as a required + (non-optional) plugin elsewhere. This feature is + particularly helpful for Neovim distributions, allowing + them to pre-configure settings for plugins that users may + or may not have installed. + ''; + + opts = defaultNullOpts.mkAttrsOf anything { } '' + The opts value can be one of the following: + + - A table: This table will be merged with any existing + configuration settings from parent specifications. + - A function that returns a table: The returned table will + completely replace any existing configuration settings from + parent specifications. + - A function that modifies a table: This function will + receive the existing configuration table as an argument and + can modify it directly. + + In all cases, the resulting configuration table will be + passed to the Plugin.config() function. Setting the opts + value automatically implies that Plugin.config() will be + called. (See: https://lazy.folke.io/spec#spec-setup) + ''; + }; + + config.name = lib.mkIf (config.source != null) ( + if (lib.isDerivation config.source) then + lib.mkDefault "${lib.getName config.source}" + else + lib.mkDefault "${builtins.baseNameOf config.source}" + ); + } + ) + ); + + lazyPluginsListType = types.listOf lazyPluginType; + in + { + gitPackage = lib.mkPackageOption pkgs "git" { nullable = true; }; + luarocksPackage = lib.mkPackageOption pkgs "luarocks" { nullable = true; }; + + plugins = lib.mkOption { + type = lazyPluginsListType; + default = [ ]; + description = "List of plugins"; + }; + }; + + extraConfig = cfg: { + extraPackages = [ + cfg.gitPackage + cfg.luarocksPackage + ]; + plugins.lazy.settings.spec = + # The `source` option i.e. `plugins.lazy.plugins..source` is just + # a convenience nixvim option that makes it easier to specify a plugin + # source whether it be a: + # + # - nix package + # - directory path + # - git url + # - short git url of the form `owner/repo` + # + # and the `source` option itself is not a part of the upstream + # `lazy.nvim` plugin spec (See: https://lazy.folke.io/spec). + # + # As a result the values given to `source` need to be mapped to properties in the + # upstream `lazy.nvim` plugin spec i.e. ([1]|dir|url). After which the + # `source` attribute itself will be stripped. + let + pluginToSpec = + plugin: + lib.concatMapAttrs ( + key: value: + if key == "source" && value != null then + { + dir = if (lib.isDerivation value || lib.isPath value) then "${value}" else null; + __unkeyed = if (isShortGitURL value) then value else null; + url = if (isGitURL value) then value else null; + } + # + # If a plugin has a custom `name` but no `source`, it suggests that + # the plugin might be defined elsewhere in the plugin list or as + # part of another plugin (e.g. a Neovim distribution like LazyVim). + # + # In these cases, a custom `name` can be used instead of `source` + # and the plugin spec options given will be applied or merged with + # the original plugin definition. This is particularly useful for + # plugins that bundle multiple modules (e.g. mini-nvim, which + # includes mini-ai, mini-trailspace, etc.), where you need to + # modify options for individual modules without affecting the + # entire bundle. + # + # To achieve this, we map `name` to `__unkeyed` (which corresponds + # to `[1]` in `lazy.nvim`). + # + else if (key == "name" && value != null) && ((plugin.source or null) == null) then + { __unkeyed = value; } + else if key == "dependencies" && value != null then + { dependencies = map pluginToSpec value; } + else + { "${key}" = value; } + ) plugin; - packedPlugins = if length plugins == 1 then head plugins else plugins; in - mkIf (cfg.plugins != [ ]) '' - require('lazy').setup( - { - dev = { - path = "${lazyPath}", - patterns = {"."}, - fallback = false - }, - spec = ${helpers.toLuaObject packedPlugins} - } - ) - ''; + map pluginToSpec cfg.plugins; }; } diff --git a/tests/test-sources/plugins/pluginmanagers/lazy.nix b/tests/test-sources/plugins/pluginmanagers/lazy.nix index 90c0bdb39..234317846 100644 --- a/tests/test-sources/plugins/pluginmanagers/lazy.nix +++ b/tests/test-sources/plugins/pluginmanagers/lazy.nix @@ -1,20 +1,85 @@ { pkgs, ... }: + +# Note: do not use `plenary-nvim` or any plugin that is a `rockspec` for tests, +# this is because `lazy.nvim` by default uses the luarocks package manager to +# process rockspecs. It might be possible to use a rockspec in a test if the +# rockspec itself does not depend on any other rockspecs but this has not been +# tested (See: +# https://github.com/nix-community/nixvim/pull/2082#discussion_r1746585453). +# +# Also the plugins and dependency combinations used in the tests are +# arbitrary. + { # Empty configuration empty = { plugins.lazy.enable = true; }; - test = { + no-packages = { + plugins.lazy = { + enable = true; + gitPackage = null; + luarocksPackage = null; + }; + }; + + nix-package-plugins = { + plugins.lazy = { + enable = true; + + plugins = with pkgs.vimPlugins; [ + # A plugin can be just a nix package + vim-closer + + # A plugin can also be an attribute set with `source` set to a nix + # package. + { source = trouble-nvim; } + ]; + }; + }; + + out-of-tree-plugins = { + # Don't run neovim for this test, as it's purely to test module evaluation. + test.runNvim = false; + plugins.lazy = { + enable = true; + + plugins = [ + # `source` can also be a short git url of the form `owner/repo` + { source = "echasnovski/mini.align"; } + + # `source` can also be a full git url with `http://` or `https://` + { + source = "https://github.com/nvim-telescope/telescope.nvim"; + enabled = true; + version = false; + } + { + source = "http://github.com/norcalli/nvim-colorizer.lua"; + enabled = true; + version = false; + } + ]; + }; + }; + + general-tests = { plugins.lazy = with pkgs.vimPlugins; { enable = true; plugins = [ - vim-closer + # Test freeform + { + source = trouble-nvim; + # The below is not actually a property in the `lazy.nvim` plugin spec + # but is purely to test freeform capabilities of the `lazyPluginType`. + blah = "test"; + } # Load on specific commands { - pkg = vim-dispatch; + source = vim-dispatch; optional = true; cmd = [ "Dispatch" @@ -26,13 +91,13 @@ # Load on an autocommand event { - pkg = vim-matchup; + source = vim-matchup; event = "VimEnter"; } # Load on a combination of conditions: specific filetypes or commands { - pkg = ale; + source = ale; name = "w0rp/ale"; ft = [ "sh" @@ -50,50 +115,205 @@ cmd = "ALEEnable"; } + # Plugins can have post-install/update hooks + { + source = markdown-preview-nvim; + cmd = "MarkdownPreview"; + } + + # Post-install/update hook with neovim command + { + source = nvim-treesitter; + opts = { + ensure_installed = { }; + }; + } + ]; + }; + }; + + nix-pkg-plugin-dependencies = { + plugins.lazy = { + enable = true; + plugins = with pkgs.vimPlugins; [ # Plugins can have dependencies on other plugins { - pkg = completion-nvim; + source = completion-nvim; optional = true; dependencies = [ { - pkg = vim-vsnip; + source = vim-vsnip; optional = true; } { - pkg = vim-vsnip-integ; + source = vim-vsnip-integ; optional = true; } ]; } - # Plugins can have post-install/update hooks + # Use dependency and run lua function after load { - pkg = markdown-preview-nvim; - cmd = "MarkdownPreview"; + source = nvim-colorizer-lua; + dependencies = [ nvim-cursorline ]; + config = '' + function() + require("nvim-cursorline").setup{} + end ''; } - # Post-install/update hook with neovim command + # Dependencies can be a single package { - pkg = nvim-treesitter; - opts = { - ensure_installed = { }; - }; + source = LazyVim; + dependencies = trouble-nvim; } - # Use dependency and run lua function after load + # Dependencies can be multiple packages { - pkg = gitsigns-nvim; - dependencies = [ plenary-nvim ]; - config = ''function() require("gitsigns").setup() end''; + source = nvim-cmp; + dependencies = [ + cmp-cmdline + cmp-vsnip + ]; } ]; }; }; - no-packages = { + out-of-tree-plugin-dependencies = { + # Don't run neovim for this test, as it's purely to test module evaluation. + test.runNvim = false; plugins.lazy = { enable = true; - gitPackage = null; + plugins = [ + # A single plugin url's can be passed by itself to `dependencies` + { + source = "kristijanhusak/vim-dadbod-completion"; + dependencies = "kristijanhusak/vim-dadbod"; + } + # An out of tree plugin can have several dependencies + { + source = "hrsh7th/nvim-cmp"; + version = false; + event = "InsertEnter"; + dependencies = [ + "hrsh7th/cmp-nvim-lsp" + "hrsh7th/cmp-buffer" + "hrsh7th/cmp-path" + ]; + } + # Full git urls can also be used to dependencies + { + source = "https://github.com/mfussenegger/nvim-dap"; + dependencies = [ + "https://github.com/folke/which-key.nvim" + "https://github.com/mfussenegger/nvim-jdtls" + ]; + } + ]; }; }; + + disabling-plugins = { + plugins.lazy = + with pkgs.vimPlugins; + let + test_plugin1_path = "${yanky-nvim}"; + test_plugin2_path = "${whitespace-nvim}"; + in + { + enable = true; + plugins = [ + # Enable and then later disable a plugin using it's custom name. + { + name = "mini.ai"; + source = mini-nvim; + enabled = true; + } + { + name = "mini.ai"; + enabled = false; + } + + # Enable and then later disable a plugin using `source`. + { + source = vim-closer; + enabled = true; + } + { + source = vim-closer; + enabled = false; + } + + # Enable plugin using `source` and then later disable it using the nix + # package's default name. + { + source = vim-dispatch; + enabled = true; + } + { + name = "vim-dispatch"; + source = vim-dispatch; + enabled = true; + } + ]; + }; + }; + + local-directory-plugins = { + plugins.lazy = + with pkgs.vimPlugins; + let + inherit (pkgs) lib; + mkEntryFromDrv = drv: { + name = "${lib.getName drv}"; + path = drv; + }; + + # Symlink a bunch of test packages to a path in the nix store + devPath = pkgs.linkFarm "dev-test-plugins" ( + map mkEntryFromDrv [ + nui-nvim + vim-vsnip-integ + vim-vsnip + completion-nvim + ] + ); + in + { + enable = true; + settings = { + dev = { + # Use `devPath` to simulate a local plugin directory + path = "${devPath}"; + patterns = [ "." ]; + fallback = false; + }; + }; + + plugins = [ + # Use local plugin that resides in path specified in `devPath` i.e. + # `plugins.lazy.settings.dev.path` (See: https://lazy.folke.io/spec) + { + dev = true; + name = "nui.nvim"; + } + # local plugins can have dependencies on other plugins + { + name = "completion.nvim"; + dev = true; + dependencies = [ + { + dev = true; + name = "vim.vsnip"; + } + { + name = "vim.vsnip.integ"; + dev = true; + } + ]; + } + ]; + }; + }; }