Open xulongwu4 opened 2 years ago
Use a single
requires
key to specify dependencies. Thisrequires
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?
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 pluginsrequires = { "https://github.com/nvim-lua/plenary.nvim" }
why would I need touse
it somewhere? This defeats the whole purpose of therequires
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.
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 anafter
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
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
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 theconfig
key and follow the same pattern as the just adding a plugin to thestartup
function already provides this functionality, so there's no real benefit of makingrequires
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.
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
?
As users, describing our package configuration roughly amounts to describing a vertex-annotated graph where:
packer
configuration for the corresponding packageFrom 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.
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 asrequries
,after
,wants
, andopt
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 theREADME
does not describe the behaviors of these keys very clearly. For example, if I useafter = "B"
key in the configuration of a plugin A, that means I want to control the order in which the plugin is loaded, sopacker
installs that plugin in theopt/
folder, which is as described by theREADME
. However, if the plugin B is installed instart/
, and I don't specify anycmd
orkeys
etc. key for A, the plugin A will actually be loaded usingpackadd
when nvim starts up, which means A will be available for the user in all cases just like any plugin that is installed instart/
. I think the behavior is fine. It is just not explicitly stated in theREADME
and caused a lot of confusion when I started usingpacker.nvim
.Then we also have the
wants
key, which is not described byREADME
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 anafter
key here if B is an optional plugin.I propose the following way for specifying dependencies and load ordering for plugin management:
Use a single
requires
key to specify dependencies. Thisrequires
key should only take dependency names. The configuration of dependencies should not appear in therequires
field. It should be specified with its ownuse
statement elsewhere (either before or after the reverse dependency).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 theopt/
folder, then we need to check where A is going to be installed. If A is going to be installed instart/
folder (becauseopt/
is not set in A's configuration), then we need to change B's installation tostart/
as well. If A is going to be installed in theopt/
folder (because of keysopt
orcmd
,events
,keys
, etc.), then A has to be loaded afterB
inpacker_compiled.lua
.Because of bullet 2, it is possible that plugin B is specified as
opt = true
, but it ends up in thestart/
folder due to dependency requirements.If plugin A is installed in the
opt/
folder, and if any of thecmd
,events
, orkeys
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.