Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[proposal] built-in space for plugins configuration (vim.config) #22665

Closed
wants to merge 1 commit into from

Conversation

mg979
Copy link
Contributor

@mg979 mg979 commented Mar 14, 2023

This is a proposal for a built-in common place where plugin configurations are stored and accessed (in a safe way).

Not a real PR, but you can read the code.

The currently adopted convention is to have lua plugins to call a setup() function. This has the advantage that it doesn't require setting options in a global variable (typically vim.g), but has the disadvantage that it prevents proper lazy loading, since require-ing the setup functions in many cases will source most of the plugin scripts: impossible to lazy-load the plugin at that point, unless the plugin author has care to isolate the setup process from the rest of the plugin. I don't think many do this, and it's a hassle in itself, while setting up plugins should be hassle-free.

Since Vim (and therefore Neovim) have had means for package lazy-loading for years now, I don't think there are good excuses for plugins to prevent the users from correctly loading them at a convenient time, and not before that.

This is an alternative system that has the following advantages:

  1. doesn't spoil lazy loading
  2. doesn't pollute the global namespace
  3. it protects from accidental options corruption
  4. allows plugins to easily get other plugins configurations (for compatibility)
  5. keeps track of configuration updates for debugging purposes
  6. allows extra features with a common interface
  7. allows displaying configuration of plugins in a popup

How it works? Assuming that the functionality resides in vim.config.
All tables are protected so it's not possible to mess up the configurations.

What the user does

The user configures plugins like this:

    vim.config.plugin_name {
        options
    }

This simple. Note that it's actually a function call. If you try to do:

    vim.config.plugin_name = options

it's an error.

When this is done the first time (in the user vimrc), the plugin isn't loaded yet. It will load when it thinks it should. Or maybe it will never be loaded, who knows at this point.

Plugin configuration can be later updated in the same manner. Doing so, will not replace the configuration, will only update it with the new values. To completely replace previous configuration, add true argument:

     vim.config[plugin_name]({
       options
     }, true)

If a setup function has been set in the meanwhile, it will be called.

What the plugin author does

The plugin can fetch its own user configuration with:

    -- local cfg = config.plugin_name -- NO: this is a function to SET options
    local cfg = vim.config().get("plugin_name")

It can set a setup function, so that if the user wants to adjust the options, the setup function will be called automatically:

  -- setup function will now be called when user updates config
  vim.config().setup_func("my_plugin", plugin_setup_func)

Next time the user does:

    config.plugin_name {
        options
    }

that call will not only update the configuration, but also call the setup function, and the plugin can update its configuration (will get the user option directly from vim.config).

There are other functionalities that a plugin can enable:

  -- set up other config functions
  vim.config().reset_func("my_plugin", plugin_reset_func)
  vim.config().query_func("my_plugin", plugin_query_func)
  vim.config().enable_func("my_plugin", plugin_enable_func)

Where:

  • plugin_reset_func can be used to reset plugin options to their default value, and the user configuration will be cleared.
  • plugin_query_func can be used to query the plugin for any current option, or all options.
  • plugin_enable_func can be used to disable/re-enable a plugin.

Of course these functionalities would have to be provided by the plugin, it's not that a plugin can be disabled/re-enabled automatically, or that vim.config can know about internal plugin configurations.

They are all optional anyway, so nobody would have to be obliged to implement them all (even if a setup function at least would be recommended, so that plugin configuration can be updated).

Checking plugin state

When a plugin is loaded, it should call in its initialization process:

vim.config().loaded(plugin_name, true)

That means: set the loaded state as true. Other plugins can check the state with:

plugin_loaded = vim.config().loaded(plugin_name)

Note: having for this a table like vim.loaded or vim.plugin.loaded would be probably better.

Similarly, if a plugin has a disable/enable functionality, a plugin can be disabled with:

vim.config().enable(plugin_name, false)

Re-enabled with:

vim.config().enable(plugin_name, true)

Check the enabled state with:

enabled = vim.config().enabled(plugin_name)

Note: having for this a table like vim.enabled or vim.plugin.enabled would be probably better.

Displaying configurations

vim.config().info(plugin_name)

Will open a popup window with all configured plugins, telling if it's enabled or not, when it's been updated and in which file, also telling with which options it has been configured each time (for debugging purposes).

It will tell which other functionalities the plugin implements.

quickshot_230314_111024

Example:

-- User configures plugin, plugin isn't loaded yet
vim.config.my_plugin {
  a = 1,
  b = 2,
}

----------------------------------------------------------------------------------------------------
-- Start of plugin stuff (when it loads)
----------------------------------------------------------------------------------------------------

local default = {
  a = 3,
  b = 4,
  c = 5,
  d = 6
}

local options = {} -- current options

local enabled = false -- enabled state

-- Setup function gets user options from vim.config
local function setup()
  options = vim.deepcopy(default)
  for k, v in pairs(vim.config().get("my_plugin")) do
    options[k] = v
  end
  -- DO NOT FORGET!
  -- sets the `loaded` and `enabled` states in vim.config
  vim.config().loaded("my_plugin", true)
end

-- A sample `reset` function implementation
local function reset()
  options = vim.deepcopy(default)
end

-- A sample `query` function implementation
-- Argument is either an array of options we want to know about, or `nil` to
-- have all options.
local function query(opts)
  if opts then
    local q = {}
    for _, v in ipairs(opts) do
      q[v] = options[v]
    end
    return opts
  else
    return vim.deepcopy(options)
  end
end

-- A sample `enable` function implementation.
-- It should return the current state (enabled or not).
local function enable(state)
  if state == enabled then
    -- nothing to do
    return enabled
  end
  if state == false then
    -- disable the plugin: remove mappings, autocommands, etc.
    print("Plugin has been disabled")
  else
    -- enable the plugin: reapply mappings, autocommands, etc.
    print("Plugin has been enabled")
  end
  enabled = not enabled
  return enabled
end

-- setup function will now be called when user updates config
vim.config().setup_func("my_plugin", setup)

-- plugin runs its own setup function
setup()

-- set up other config functions
vim.config().reset_func("my_plugin", reset)
vim.config().query_func("my_plugin", query)
vim.config().enable_func("my_plugin", enable)

----------------------------------------------------------------------------------------------------
-- End of plugin stuff
----------------------------------------------------------------------------------------------------

-- vim.config().reset("my_plugin")
vim.config().info()

@github-actions github-actions bot added the lua stdlib label Mar 14, 2023
@clason clason added plugin plugins and Vim "pack" needs:discussion For PRs that propose significant changes to some part of the architecture or API labels Mar 14, 2023
@clason
Copy link
Member

clason commented Mar 14, 2023

Thanks for making the effort to propose this, but I don't really see why this is something that needs new code in Neovim. Plugins very deliberately can do whatever they want, and we do not want to constrain that; it is up to the ecosystem to come up with best practices and share them.

Note that plugins can do very different things, and what makes sense for some will make no sense for others; centralizing -- and ossifying! -- one particular approach is not helpful.

Also, your premise is flawed: lazy.nvim handles lazy-loading with setup (and pre-config) very well!

Finally, there really is no way of a guaranteed "resetting" of a plugin short of restarting Nvim, and implying that by providing such a function is actively harmful.

@justinmk
Copy link
Member

justinmk commented Mar 14, 2023

Nicely done, very constructive. We've rejected similar idea in the past #9517 #8677 cc @KillTheMule .

In those cases and this one, main question is which of these actually are valuable as formal interfaces: can we get 80% of the way by (first) providing guidance #22366 ?

A counterargument is :checkhealth : it's a very thin "framework", but just the fact that it exists seems to be useful for something that otherwise isn't "worth bothering with". And it hasn't crowded out "better" solutions.

There are other functionalities that a plugin can enable: ...

  • plugin_reset_func can be used to reset plugin options to
    their default value, and the user configuration will be cleared.
  • plugin_query_func can be used to query the plugin for any current option,
    or all options.
  • plugin_enable_func can be used to disable/re-enable a plugin.

Of course these functionalities would have to be provided by the plugin, it's not that a plugin can be disabled/re-enabled automatically, or that vim.config can know about internal plugin configurations.

They are all optional anyway,

My sense is that these kinds of "standard" interfaces can be very useful, but we may be able to skip the "active registration", in favor of "passive discovery": e.g. if require('foo').info() exists, it will be called by :info (or whatever), no need for plugins to "register".

  • not sure about "reset". I'm wary of adding mechanisms outside of Lua's already well-understand and capable package system.
  • not sure about "query". Another mechanism for getting options/variables is another concept for users/plugin authors to learn, and we have a high bar for introducing new concepts.
  • wary of introducing new state like loaded

@lewis6991
Copy link
Member

lewis6991 commented Mar 14, 2023

unless the plugin author has care to isolate the setup process from the rest of the plugin. I don't think many do this, and it's a hassle in itself, while setting up plugins should be hassle-free.

This is what plugins should do, it isn't a hassle, and overall results in a more robust plugin. We aim to provide better guidance on how to achieve this (#22366).

@mg979
Copy link
Contributor Author

mg979 commented Mar 14, 2023

If there is no interest in having a interface for plugins, this is obviously useless, but I'll answer to the objections.

@clason

Plugins very deliberately can do whatever they want, and we do not want to constrain that; it is up to the ecosystem to come up with best practices and share them.

What the ecosystem came up with (the require"x".setup() method) is what motivated me to write this, because I think it's a terrible system.

Also, your premise is flawed: lazy.nvim handles lazy-loading with setup (and pre-config) very well!

The kind of argumentation that I care zero about. I don't want to rely on package managers to have a sane configuration.

Finally, there really is no way of a guaranteed "resetting" of a plugin short of restarting Nvim, and implying that by providing such a function is actively harmful.

This system makes no guarantees. It provides an interface for plugins that want to implement this functionality, in a way that users and plugin authors don't have to think much about how to expose the functionality (or use it), and also in a way that you can know (with the popup) if a plugin supports that functionality or not.

@justinmk

In those cases and this one, main question is which of these actually are valuable as formal interfaces: can we get 80% of the way by (first) providing guidance #22366 ?

A counterargument is :checkhealth : it's a very thin "framework", but just the fact that it exists seems to be useful for something that otherwise isn't "worth bothering with". And it hasn't crowded out "better" solutions.

There is a fundamental problem, that if the configurations aren't stored in a centralized place, where plugins can fetch them without being sourced, lazy-loading becomes difficult, and in any case not complete, even in the case that authors isolate the setup process in their plugins.

Other than this, other approaches can work, but this was my main motivation.

My sense is that these kinds of "standard" interfaces can be very useful, but we may be able to skip the "active registration", in favor of "passive discovery": e.g. if require('foo').info() exists, it will be called by :info (or whatever), no need for plugins to "register".

These extra features I added last, because I thought they could be useful once a centralized place was there. Of course there are other ways.

* not sure about "reset". I'm wary of adding mechanisms outside of Lua's already well-understand and capable package system.

I mostly thought about mappings being unmapped and such, not actually removing a plugin altogether, that would be dangerous.

Edit: reset means just a signal to the plugin to use only default configuration, and ignore user options. Yeah, maybe dangerous, also this was added last, I thought it could be useful.

* not sure about "query". Another mechanism for getting options/variables is another concept for users/plugin authors to learn, and we have a high bar for introducing new concepts.

Added only because possible, not sure how it can really be useful. I don't think there are many cases where a plugin wants to inquire some other plugin configuration.

* wary of introducing new state like `loaded`

I thought it as a way to avoid to use vim.g.loaded_ etc. Of course it's dangerous to rely on it if it's not an established convention.

@lewis6991

This is what plugins should do, it isn't a hassle, and overall results in a more robust plugin. We aim to provide better guidance on how to achieve this (#22366).

It doesn't solve all problems. Some plugins have dependencies, if you want to correctly load the plugin with a dependency, you must fully load the dependency, and they can be big like telescope and such.

Simply put, the vim.g.plugin_variable mechanism was ugly and limited, but it allowed lazy loading, I think calling setup() functions cannot.

@mg979
Copy link
Contributor Author

mg979 commented Mar 14, 2023

Anyway I'm closing this since it's not a real PR, just a proposal.

@mg979 mg979 closed this Mar 14, 2023
@lewis6991
Copy link
Member

You're assuming that lazy-loading is a widely accepted and recommended way of handling plugins. It isn't!

We have require to load modules on-demand (this can be a whole plugin or a part of one), and we have plugin/ for plugins to initialize themselves and provide hook points.

@justinmk
Copy link
Member

justinmk commented Mar 14, 2023

@mg979 This is definitely appreciated because it advances the conversation around this topic and is a great reference. So thanks again.

There is a fundamental problem, that if the configurations aren't stored in a centralized place, where plugins can fetch them without being sourced, lazy-loading becomes difficult,

vim.g is the established convention. It's true that other plugins could clobber things in vim.g, but I see that as an acceptable risk since "well behaved" is a requirement of all plugins.

@mg979
Copy link
Contributor Author

mg979 commented Mar 14, 2023

@lewis6991

What annoys me is that you can easily lazy-load a plugin if you don't want to change any configuration options, otherwise you must load it. And that if you want to lazy-load it, you must do everything by yourself, setting up autocommands, wrappers, and even fight with the plugin itself when it assumes that its setup is called at startup. This wasn't the case when the vim.g convention was used.

You're assuming that lazy-loading is a widely accepted and recommended way of handling plugins. It isn't!

It was in vim. Anyway, this was a proposal, not a way to be polemic with the currently accepted convention.

@justinmk

Today vim.g convention isn't used at all, everybody uses setup(). Not that I find the vim.g system that great either, it works much better for vim.b variables in ftplugins though.

@lewis6991
Copy link
Member

What annoys me is that you can easily lazy-load a plugin if you don't want to change any configuration options, otherwise you must load it. And that if you want to lazy-load it, you must do everything by yourself, setting up autocommands, wrappers, and even fight with the plugin itself when it assumes that its setup is called at startup. This wasn't the case when the vim.g convention was used.

You will save yourself a tonne of time by simply not lazy loading. If there's a plugin that is slowing down your startup, then raise an issue on said plugin, or simply don't use it and find a better alternative.

@hrsh7th
Copy link
Sponsor Contributor

hrsh7th commented Mar 14, 2023

I'm the developer of a plugin that uses the setup style, but I don't see much benefit from the setup style. (It simply adopts ecosystem conventions.)

The vim.g/vim.bo style doesn't have lazy loading issues, it's a convention from vim days, and I think it's a simple solution.

I myself use vim like this:

  • No lazy loading
  • Define autocommands/keymaps in plugin/*.lua.

I agree with the concern that the ecosystem is currently in confusing.

google translated

@mg979
Copy link
Contributor Author

mg979 commented Mar 14, 2023

You will save yourself a tonne of time by simply not lazy loading.

I don't do that (wrappers and such, tired of that madness). I only meant that you'd need to do it if you wanted lazy load stuff.

@gpanders
Copy link
Member

@mg979 I am sympathetic to all of the points you've raised. I too find the commonly used setup() pattern suboptimal and share your concerns about making package managers increasingly sophisticated and complex in order to address the shortcomings of that approach. I do not, however, share your concerns about using vim.g for plugin configuration. The old Vim pattern of using g:myplugin_ as a prefix for every variable was a bit tedious to write, but it worked just fine. In Lua, one can simply make vim.g.myplugin a table with options, which avoids some of those issues.

Just to clarify terms here, I think of "lazy loading" as "loading on demand", a feature which Vim has supported for quite some time through autoloaded functions. I think this is a great feature and plugins should be written in a way to take advantage of this (as Vim plugins have been for many years). Essentially, a plugin should load as little code as possible at Nvim startup, and defer loading the rest of its code until it's actually needed. The use of a setup() function (nearly) precludes this approach by forcing the plugin to load almost all of its code even when setting configuration options (or in some cases, even to enable the plugin at all!), thus the introduction of "lazy loading" features in package managers.

However, this need not be the case. A Lua plugin is really not much different than a Vim plugin: instead of Vimscript functions in an autoload/ directory, we instead can use Lua functions in a lua/ directory, which can be loaded on demand with require().

The fundamental issue is not, in my opinion, a deficiency in support from Neovim core, but rather a lack of guidance or awareness of these patterns in the broader plugin community, which #22366 is (in part) meant to address.

There is one part of your proposal that I found very compelling though, which was the vim.config.info popup. This is something seriously worth considering in core I think. Right now, we can answer the question: "which plugin defines mapping X?" using :verbose map X, but the inverse question, "what mappings are defined by plugin X?" cannot be answered outside of inspecting the source code (the same is true for autocommands and user commands). The ability to display that information to users would be nice to have (how that is implemented though is a tricker question).

@clason
Copy link
Member

clason commented Mar 14, 2023

Right now, we can answer the question: "which plugin defines mapping X?" using :verbose map X, but the inverse question, "what mappings are defined by plugin X?" cannot be answered outside of inspecting the source code (the same is true for autocommands and user commands).

This was something we discussed in the context of "plugin specifications", where I always argued that such a specification should contain an "API manifest" listing

  1. defined commands
  2. exported functions
  3. global variables it listens to.

(I also argued that plugins should not define mappings themselves, and especially not for something that isn't listed in the manifest.) But again, I think that this is the proper level of handling that (which may lead to something similar in core, but less ad hoc).

@mg979
Copy link
Contributor Author

mg979 commented Mar 14, 2023

@gpanders

Just to clarify terms here, I think of "lazy loading" as "loading on demand", a feature which Vim has supported for quite some time through autoloaded functions. I think this is a great feature and plugins should be written in a way to take advantage of this (as Vim plugins have been for many years). Essentially, a plugin should load as little code as possible at Nvim startup, and defer loading the rest of its code until it's actually needed. The use of a setup() function (nearly) precludes this approach by forcing the plugin to load almost all of its code even when setting configuration options (or in some cases, even to enable the plugin at all!), thus the introduction of "lazy loading" features in package managers.

This is essentially what I've been trying to say. In theory, plugins could separate the 'gather configuration' part, from the actual initialization part. User calls setup(), but that wouldn't load the plugin, it would just require the setup script, that would store the configuration for when the initialization actually happens. Then these setup scripts would also need to handle plugins that depend on them, if there can be. But for many plugins it's added complexity for little gain.

Right now, we can answer the question: "which plugin defines mapping X?" using :verbose map X, but the inverse question, "what mappings are defined by plugin X?" cannot be answered outside of inspecting the source code (the same is true for autocommands and user commands).

I think at least vim.set.keymap could register from where the mapping is coming from, currently verbose nmap doesn't work for lua (in stable at least). If it could do that, you could have a filtered output of verbose nmap with only the mappings you're interested in, with mappings defined in vimscript you can do it.

@justinmk
Copy link
Member

"API manifest" listing

  1. defined commands
  2. exported functions
  3. global variables it listens to.

Not sure we need a manifest. We have the desc concept on nvim_set_keymap() and nvim_create_autocmd().
We can get the plugin path and report it in :verbose and other places.

@mfussenegger
Copy link
Member

mfussenegger commented Mar 14, 2023

To add my 2 cents:

  • I see it similarly to @gpanders in that there's no big benefit in vim.config over vim.g.pluginname. If plugins don't behave well and clobber a global namespace they could do that with vim.config too
  • it's unlikely that all plugin authors would migrate, so users would have to learn one more way to configure plugins
  • Plugin authors really seem to like .setup, https://zignar.net/2022/11/06/structuring-neovim-lua-plugins/ made its round on social media and I hardly saw anyone going back to vim.g.plugin
  • Looking at my own plugins, I don't see many opportunities to use this. A couple of them require per buffer activation with per buffer settings. The others usually just expose some properties to set - this leaves the door open so that I could add metatables for eager setting validation. Using a mix of plugin/ and internal lazy loading of submodules keeps the costs reasonable
  • there's opt/ as built-in way to lazy load plugins

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lua stdlib needs:discussion For PRs that propose significant changes to some part of the architecture or API plugin plugins and Vim "pack"
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants