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

feature: reloading plugins #445

Closed
1 task done
WieeRd opened this issue Jan 23, 2023 · 24 comments · Fixed by #1147
Closed
1 task done

feature: reloading plugins #445

WieeRd opened this issue Jan 23, 2023 · 24 comments · Fixed by #1147
Labels
enhancement New feature or request

Comments

@WieeRd
Copy link

WieeRd commented Jan 23, 2023

Did you check the docs?

  • I have read all the lazy.nvim docs

Is your feature request related to a problem? Please describe.

I wanted to have a convenient way to reload plugin configs without restarting Neovim.
Not reloading plugin specs, I mean running all the config functions again.

The problem here is that some plugins are safe to reload, while some are not.
There is no guarantee running .setup() for the second time will break the plugin or not.

Describe the solution you'd like

  • Add reloadable key with boolean? type to plugin specs.
    It can be used to explicitly specify if the plugin is safe to reload or not.

  • Add command :Lazy reload [plugins] (and .reload() API)
    It'll run the config function of the specified plugins.
    Without any arguments, all plugins with reloadable = true are used.

  • Add shortcut r and R inside :Lazy window.
    r reloads the plugin under the cursor, and R is the same as :Lazy reload.

  • Add these options to lazy.setup()
    defaults.reloadable - makes plugins reloadable by default.
    change_detection.reload - auto reload plugins when config change is detected.

@WieeRd WieeRd added the enhancement New feature or request label Jan 23, 2023
@WieeRd
Copy link
Author

WieeRd commented Jan 23, 2023

I also came up with an interesting alternative to using reloadable key.
How about using the return value of config key to indicate reloadable?

A proposal to plugin authors:
Let's use the return value of .setup(), which currently serves no purpose, to indicate if the plugin is reloadable!

After that, since lazy.nvim can generate a simple config function for us,
just config = true is enough to setup and specify "reloadability" of a plugin.

-- in the plugin spec
config = true

-- lazy.nvim converts above to something like this
config = function(plugin, opts)
  return require(plugin.name).setup(opts)
end

Maybe this is a bit too much, but I just wanted to share my idea :)

@folke
Copy link
Owner

folke commented Jan 23, 2023

I'm actually working on something like this, but I'm still not sure I should add it.

More details soon. But it involves a new property deactivate which can be a function that does additional unloading of a plugin besides all the general thing like clearing modules etc.

For noice for example:

{
    "folke/noice.nvim",
    event = "VeryLazy",
    unload = function()
      require("noice").disable()
    end,
}

What the reloading currently does:

  1. execute main module's deactivate if it exists. (same module that has setup)
  2. execute Plugin.deactivate if set
  3. disable lazy handlers for the plugin (keys, ft, ...)
  4. clear all loaded modules from the plugin's /lua directory
  5. clear vim.g.loaded_... for every /plugin/
  6. enable lazy handlers
  7. execute Plugin.init if it exists
  8. if it's a start plugin, or it has event VeryLazy, VimEnter or UIEnter, then load the plugin immediately

It all seems to work pretty good like this, especially with some custom unload functions, but I need to do more testing.

@WieeRd
Copy link
Author

WieeRd commented Jan 23, 2023

That sounds great! If plugin authors start to provide .unload() alongside .setup() updating and reloading stuffs would become so much easier and cleaner.

@WieeRd WieeRd changed the title feature: add reloadable key to plugin spec feature: reloading plugins Jan 23, 2023
@WieeRd
Copy link
Author

WieeRd commented Jan 23, 2023

I changed the issue title to be more generic, so that people can discuss and collect ideas about said features.

@ibhagwan
Copy link

ibhagwan commented Feb 7, 2023

I needed something like this for the convenience of quickly testing plug-in changes during development, if someone needs an intermediate solution, the below serves me well and can be easily modified to any setup, it’s quite simple, unload specific lua patterns from package.loaded and run a “reload” function after unloading.

Note that since it reloads all neovim options I also reload the after directory in order to reset custom filetype settings (expandtab, shiftwidth, etc)

M.reload_config = function()
  M.unload_modules({
    { "^options$", fn = function() require("options") end },
    { "^autocmd$", fn = function() require("autocmd") end },
    { "^keymaps$", fn = function() require("keymaps") end },
    { "^utils$" },
    { "^workdirs$" },
    { mod = "ts%-vimdoc" },
    { mod = "smartyank", fn = function() require("smartyank") end },
    { mod = "fzf%-lua", fn = function() require("plugins.fzf-lua.setup").setup() end },
  })
  -- re-source all language specific settings, scans all runtime files under
  -- '/usr/share/nvim/runtime/(indent|syntax)' and 'after/ftplugin'
  local ft = vim.bo.filetype
  vim.tbl_filter(function(s)
    for _, e in ipairs({ "vim", "lua" }) do
      if ft and #ft > 0 and s:match(("/%s.%s"):format(ft, e)) then
        local file = vim.fn.expand(s:match("[^: ]*$"))
        vim.cmd("source " .. file)
        M.warn("RESOURCED " .. vim.fn.fnamemodify(file, ":."))
        return s
      end
    end
    return nil
  end, vim.fn.split(vim.fn.execute("scriptnames"), "\n"))
end

and the reload function:

M.unload_modules = function(patterns)
  for _, p in ipairs(patterns) do
    if not p.mod and type(p[1]) == "string" then
      p = { mod = p[1], fn = p.fn }
    end
    local unloaded = false
    for m, _ in pairs(package.loaded) do
      if m:match(p.mod) then
        unloaded = true
        package.loaded[m] = nil
        M.info(string.format("UNLOADED module '%s'", m))
      end
    end
    if unloaded and p.fn then
      p.fn()
      M.warn(string.format("RELOADED module '%s'", p.mod))
    end
  end
end

@miversen33
Copy link

miversen33 commented Feb 10, 2023

Just popping in here, I was going to ask if a PR would be welcome for setting up reloading plugins lol.
I've got something that does this already (along with import safety) in import.nvim, however I have no issue in folding the reload functionality (and maybe import safety??) into lazy.nvim.

Edit:

Alternatively, feel free to yoink the reload code powering import, it will likely cover most of the bases you are looking for and (with a bit of coercion into your current codebase) :)

@folke
Copy link
Owner

folke commented Feb 10, 2023

It's not as simple as that, since you need to properly update the lazy loading handlers as well.

Anyhow, the code to reload plugins is actually live already. It's just not used at this point

function M.reload(plugin)

Right now, you could use this like:

local plugin = require("lazy.core.config").plugins["trouble.nvim"]
require("lazy.core.loader").reload(plugin)

Still need to do more proper testing, documenting everything and optionally included automatic reloading on install/updates.

@folke
Copy link
Owner

folke commented Mar 8, 2023

@idr4n you should not use reload for this purpose, that makes no sense.
For lualine, you can just change the theme I believe.

Other plugins typically have an autocmd that triggers on colorscheme changes. If not that should be fixed in those plugins.

Also, for proper reloading, complex plugins would eventually have to implement a deactivate function. Reloading treesitter sounds like a really bad idea for now.

And why do you even reload tokyonight?

Your code makes no sense at all.

reload is NOT the way to go here

@idr4n
Copy link

idr4n commented Mar 8, 2023

@folke Sorry I just moved that to a discussion instead.

Thanks for replying to this. I was reloading plugins as I have some highlight groups defined in the tokyonight config, that's why, same for gitsigns. But you are right, I was just trying things to see whether it works or not. Unfortunately plugins such as lualine don't have an autocmd that triggers changes on colorsheme or an option to change the theme without restarting Neovim.

But thanks for letting me know that reload in not the way to go in this case. Appreciate it!

aaronkollasch added a commit to aaronkollasch/dotfiles that referenced this issue Apr 2, 2023
@lervag
Copy link
Contributor

lervag commented May 24, 2023

@folke This is very interesting. I've implemented my own reloaders for my plugins, e.g. for VimTeX (which is mostly Vimscript). If lazy.nvim should ever have a "spec" for reloading, then I would be more than happy to update my plugins to enable some kind of seamless integration.

@folke
Copy link
Owner

folke commented May 24, 2023

@lervag the reloading is a two step process. First lazy completely deactivates a plugin by removing any lazy handlers, unloading its lua modules and clearing vim.g.loaded_.... for vim plugins. It also check if the plugin's main lua module has a deactivate function and executes it if it exists. Additionlally, it will execute the deactivate property of a plugin spec if it exists.

Once a plugin is deactivated, the plugin is loaded again as if during startup.

If you want to provide reloading support for lazy, you would need to add a file lua/vimtex.lua with the following contents:

local M = {}

function M.deactivate()
  -- add any logic to deactivate here
end

Alternatively, you can provide users with a code snippet they can use to add a deactivate property to the vimtex plugin spec.

Do know that this is all still very experimental :)

You can find the actual code in lazy that does deactivation here: https://github.com/folke/lazy.nvim/blob/main/lua/lazy/core/loader.lua#L187

@lervag
Copy link
Contributor

lervag commented May 25, 2023

Thanks, @folke!

the reloading is a two step process. First lazy completely deactivates a plugin by removing any lazy handlers, unloading its lua modules and clearing vim.g.loaded_.... for vim plugins.

Does this include adjusting the runtimepath as well? E.g., after calling lazy.code.loader.disable("vimtex"), would I still be able to do :call vimtex#...#function(...)?

It also check if the plugin's main lua module has a deactivate function and executes it if it exists. Additionlally, it will execute the deactivate property of a plugin spec if it exists.

I assume that lazy's deactivation does not automatically remove all mappings and commands defined by a plugin? And that this is what the plugin's main lua module would have to implement?

For VimTeX (and possibly other Vimscript based plugins), I believe we might want to use a reloader that uses another mechanism: i.e. to manually re-source all (previously sourced) vimscript files. I'm not sure you want to implement that, but it may be interesting to at least have some kind of hook that we could use. .deactivate does not "feel" right here; it could be nice to have something like .reload?

Do know that this is all still very experimental :)

I like experimental :)

@folke
Copy link
Owner

folke commented May 25, 2023

clearing keymaps and autocmds should be handled by the plugin indeed, but in most cases that's not needed for a reload.

After deactivation, the plugin is loaded again, meaning for vimscript that their plugin ftplugin etc files are sourced again.

I'll look into your idea to re-source, or unsource (if that's a thing, need to check) any other vimscript files part of the plugin's rtp.

But again, probably not a good idea for you to make any changes to vimtex at this point. reloading/deactivating of plugins is currently not even exposed to lazy users :)

@utkarshgupta137
Copy link

utkarshgupta137 commented May 25, 2023

One sub-feature which should be easy(er) to implement: hot-reloading the keys section. I define all the keymaps related to a plugin using the keys section, following the style of LazyVim. It is easy to reload keymaps when defining them in a file, but not when with lazy.nvim. The same goes for event/cmd/ft, but they are rarely changed.

Edit: Not asking for removed keys to be unmapped (although that would be nice), but just updating new/modified keys should be fine.

@folke
Copy link
Owner

folke commented May 25, 2023

lazy keys are already handled by a reload, so those are fine

@utkarshgupta137
Copy link

lazy keys are already handled by a reload, so those are fine

No, I'm talking about actually setting the keymaps.

So if I add the keys section here like this:

{
  "cshuaimin/ssr.nvim",
  keys = {
    { "<leader>lR", '<cmd>lua require("ssr").open()<cr>', desc = "Structural Search & Replace" },
  },
  opts = {},
}

The keys should automatically be usable from then on. Same if I change what happens when an existing key is pressed.

I'm on the latest LazyVim btw.

@folke
Copy link
Owner

folke commented May 25, 2023

I'm talking about the same thing. Reload supports that.
But as I said before, reloading is not enabled right now in lazy.

@lervag
Copy link
Contributor

lervag commented May 26, 2023

I'll look into your idea to re-source, or unsource (if that's a thing, need to check) any other vimscript files part of the plugin's rtp.

But again, probably not a good idea for you to make any changes to vimtex at this point. reloading/deactivating of plugins is currently not even exposed to lazy users :)

Cool, thanks! I'll just keep watching and will consider changes at a later stage, then :)

@folke
Copy link
Owner

folke commented May 27, 2023

I just added automatic re-sourcing of loaded vimscript for the plugin that you want to reload. Thank you for the idea @lervag!

I also added :Lazy reload plugin_name to reload a plugin.

Still all very experimental, so use with caution. Things will likely break

@lervag
Copy link
Contributor

lervag commented May 27, 2023

Wow, thanks; that was fast! I was not aware of getscriptinfo, that will be useful to know about! I'll be sure to test this with VimTeX and other plugins I have/develop. I'll let you know if I notice something that does not work as expected!

@folke
Copy link
Owner

folke commented May 27, 2023

Be aware that reloading is still not automatic.
You need to explicitly do :Lazy reload vimtex

@kamalmarhubi
Copy link

I'm way out of my depth with this, but I've wondered about an alternative approach to reloading: restoring a "clean slate" state for the neovim version, and the reloading the config. This would include restoring global variables, mappings, highlights, autocommands, lua _G object and package.loaded members. Probably all LSP servers would need to be terminated and be restarted after reloading too. After reloading, all buffers have all ftplugin and FileType autocommands rerun.

There are probably a whole lot of reasons this won't work. Some I can think of: what should happen to buffers from plugins like neotree, especially if they affect the current window layout; buffer variables a user might have set manually.

@WieeRd
Copy link
Author

WieeRd commented Aug 8, 2023

@kamalmarhubi I think that level of reloading should be supported by Neovim itself.
There is not much we can do to figure out what the default/original state was, other than manually storing backups.

@miversen33
Copy link

I dug into this a bit in my deprecated project import.nvim and the general answer I found was "ya this is rough".

I attempted to do a deep copy of the various vim states available but quite a few of the vim variables are faux lua tables, so you can't really copy them without knowing exactly what you are trying to copy. Which is tough when you are trying to copy "everything". That would have to be something that is done in vimscript I think, and I just didn't feel like trying to figure that out. Honestly, @WieeRd is right, state management should be left to neovim (and this is not something that neovim supports currently).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants