wbthomason / packer.nvim

A use-package inspired plugin manager for Neovim. Uses native packages, supports Luarocks dependencies, written in Lua, allows for expressive config
MIT License
7.89k stars 264 forks source link

Simplify and make more consistent the usage of `requires`, `after`, `wants` and `opt` keys #810

Open xulongwu4 opened 2 years ago

xulongwu4 commented 2 years ago

The Current Situation

packer.nvim is a feature-rich plugin manager and can do a lot of things for the user, such as dependency and load ordering managements, and I absolutely love these advanced features. However, the current situation is that users have to use keys such as requries, after, wants, and opt to fine-tune the behavior of the plugins, which is not as easy to understand as it seems in my experience. What's worse is that the README does not describe the behaviors of these keys very clearly. For example, if I use after = "B" key in the configuration of a plugin A, that means I want to control the order in which the plugin is loaded, so packer installs that plugin in the opt/ folder, which is as described by the README. However, if the plugin B is installed in start/, and I don't specify any cmd or keys etc. key for A, the plugin A will actually be loaded using packadd when nvim starts up, which means A will be available for the user in all cases just like any plugin that is installed in start/. I think the behavior is fine. It is just not explicitly stated in the README and caused a lot of confusion when I started using packer.nvim.

Then we also have the wants key, which is not described by README because it is just a short-time workaround.

Currently the requires key seems to serve as syntactic sugar for users to download dependencies.

My Proposal

I think the management of dependencies and loading orders can be simplified. Furthermore, I think they should be managed in a unified way. After all, if plugin A depends on (requires) plugin B, that means when we load plugin A, plugin B should be available on your runtimepath, i.e., it implies an after key here if B is an optional plugin.

I propose the following way for specifying dependencies and load ordering for plugin management:

  1. Use a single requires key to specify dependencies. This requires key should only take dependency names. The configuration of dependencies should not appear in the requires field. It should be specified with its own use statement elsewhere (either before or after the reverse dependency).

  2. If A requires B, then A should be loaded no earlier than B. If B is going to be installed in the start/ folder, then everything if fine. If B is going to be installed in the opt/ folder, then we need to check where A is going to be installed. If A is going to be installed in start/ folder (because opt/ is not set in A's configuration), then we need to change B's installation to start/ as well. If A is going to be installed in the opt/ folder (because of keys opt or cmd, events, keys, etc.), then A has to be loaded after B in packer_compiled.lua.

  3. Because of bullet 2, it is possible that plugin B is specified as opt = true, but it ends up in the start/ folder due to dependency requirements.

  4. If plugin A is installed in the opt/ folder, and if any of the cmd, events, or keys key is triggered, we will start the process of loading A. The process starts by loading all A's dependencies that haven't been loaded, and A is loaded at last.

These are my initial thoughts towards simplifying the process of dependency and loading sequence management. It probably has its own issues as well, but I will write my thoughts down for more discussions here.

filipekiss commented 2 years ago

Use a single requires key to specify dependencies. This requires key should only take dependency names. The configuration of dependencies should not appear in the requires field. It should be specified with its own use statement elsewhere (either before or after the reverse dependency).

Having to write an use statement for a plugin that sometimes is just a requirement is adding an unnecessary overhead to the overall configuration. If a plugins requires = { "https://github.com/nvim-lua/plenary.nvim" } why would I need to use it somewhere? This defeats the whole purpose of the requires key if I still have to manage this on my own.

Can you clarify what kind of problem making this change would fix?

zanglg commented 2 years ago

Having to write an use statement for a plugin that sometimes is just a requirement is adding an unnecessary overhead to the overall configuration. If a plugins requires = { "https://github.com/nvim-lua/plenary.nvim" } why would I need to use it somewhere? This defeats the whole purpose of the requires key if I still have to manage this on my own.

Maybe @xulongwu4 is indicating a use case like this: A require C, B require C. where does C's config need to put?

A used package should exist as a "library" and loaded when someone require it. I like his proposal, packer's requires, after, wants and opt was really confusing.

danielo515 commented 2 years ago

After all, if plugin A depends on (requires) plugin B, that means when we load plugin A, plugin B should be available on your runtimepath, i.e., it implies an after key here if B is an optional plugin.

Wait, isn't that the default behavior? So ,additionally to specify requires I also have to take care of specifying after ? Indeed it is confusing.

Having to write an use statement for a plugin that sometimes is just a requirement is adding an unnecessary overhead

I also fully agree with this. I don't see why can't requires do both

filipekiss commented 2 years ago

In my setup, when I have something like this (common for nvim-cmp plugins, for example), I put them in their own file and require them with lua:

-- tabnine.lua

return {
  "https://github.com/tzachar/cmp-tabnine",
  run = "./install.sh",
}

then on the config for the nvim-cmp:

-- cmp.lua
local tabnine = require(`tabnine`) -- will require the file above
return {
  "https://github.com/hrsh7th/nvim-cmp", -- The completion plugin

  config = function()
    -- my config goes here
  end,
  requires = {
    { "https://github.com/hrsh7th/cmp-buffer" }, -- buffer completions
    tabnine,
  },
}

This way the config for tabnine lives on it't own file and I can still use it in the requires table and I can even mix with simple string only requires

filipekiss commented 2 years ago

So I have given this some though and I don't think the requires key behavior should change. Right now, if you have a dependency, you add it to the requires key of that plugin. If you dependency has configurations, you can simply put those configurations in the requires table:

require('packer').startup(function()
 use { 
    "https://github.com/hrsh7th/nvim-cmp",
    config = function()
        -- ...
    end,
    requires = {
    "https://github.com/hrsh7th/cmp-buffer", -- a plugin with no config
    { 
        "https://github.com/L3MON4D3/LuaSnip",  -- a plugin with config
        config = function()
            -- ...
        end
    }
  },
}

If you want to put the config into a separate file for organization purposes, you can just return a table from any lua file and pass it to requires (like I showed here).

This has the added bonus that if you remove all plugins that require that dependency, the dependency itself can be safely uninstalled without additional work. Having requires accept the config key and follow the same pattern as the just adding a plugin to the startup function already provides this functionality, so there's no real benefit of making requires do less of what it does today.

And having requires behave just like use behaves means that we have less overhead to think about when adding a dependency to my used plugins.

gotgenes commented 2 years ago

If you want to put the config into a separate file for organization purposes, you can just return a table from any lua file and pass it to requires (like I showed here).

To me, this isn't about code organization, or Don't Repeat Yourself. It's about having ambiguous or unambiguous behavior, and the fact that we're human and we make mistakes when we do have to remember to do the same action in multiple places.

So to illustrate what I mean, let's keep rolling with this example:

Maybe @xulongwu4 is indicating a use case like this: A require C, B require C. where does C's config need to put?

We start with plugin A first.

 require('packer').startup(function(use)
  use({
    'A',
    requires = {
      {
        'C', -- a plugin needing config
        -- We pass config in here
        config = function()
          -- ...
        end,
      },
    },
  })
end)

It is unambiguous: we know we're going to call C with the config function. We use plugin A for a while. We love it. Life is good.

A month later, we hear about the B plugin. It sounds amazing! So, we add to the top of our plugins:

require('packer').startup(function(use)
  use({
    'B',
    requires = {
      {
        'C', -- same plugin needing config as below
        -- OOPS! We forgot to pass config in here!
        -- Now what happens?
      },
    },
  })
  use({
    'A',
    requires = {
      {
        'C',
        -- We pass config in here
        config = function()
          -- ...
        end,
      },
    },
  })
end)

But we forgot we had to configure C plugin when we added it to the requires of B!

Now it is ambiguous: what is supposed to happen? What will happen?

Will B try to load first, so C is unconfigured, and then the unconfigured C will also be given to A? Will A load first with C configured and B will get a configured C, even though we forgot to specify a C configuration with B when using requires? Will C be loaded twice, configured for A but unconfigured for B?

masaeedu commented 1 year ago

As users, describing our package configuration roughly amounts to describing a vertex-annotated graph where:

From this perspective the current design seems a bit muddled. It's almost an adjacency list, but where one would normally find the identifiers of inbound vertices there's vertex name + vertex annotations. This yields some kind of weird overdetermined not-quite-annotated-graph structure, which is hard to reason about.