folke / lazy.nvim

💤 A modern plugin manager for Neovim
https://lazy.folke.io/
Apache License 2.0
14k stars 336 forks source link

bug: some autocmd events are not fired when lazy loading #1049

Closed fent closed 11 months ago

fent commented 11 months ago

Did you check docs and existing issues?

Neovim version (nvim -v)

0.9.0

Operating system/version

MacOS 13.4

Describe the bug

Some plugins depend on autocmd events such as BufEnter, FileType, etc that are set during that pugin's setup. If these plugins are lazy loaded, they may miss some of these events being fired, and so, the plugin won't properly setup and may not work properly or at all.

This is partly mitigated in https://github.com/folke/lazy.nvim/blob/main/lua/lazy/core/handler/event.lua by executing the autocmds in which the plugin is lazy loaded, but it's not enough. For example, a plugin lazy loaded by the FileType event may depend on the BufRead event, which happens before FileType. In this case, the BufRead event, and any event before FileType would have to be re-executed, not just FileType.

Here's the order of autocmd events that plugins can depend on (likely not complete) (source):

BufReadPre
BufRead
BufReadPost
FileType
Syntax
BufWinEnter
BufEnter
VimEnter

Buf even if these plugins are lazy loaded via the VeryLazy, a command, or a keymap, it may still need these events to fire for proper setup.

example of plugins where lazy loading is broken:

Steps To Reproduce

  1. run nvim -u repro.lua to open nvim and setup plugins
  2. run :LspInstall lua_ls to install the lsp server
  3. reopen nvim with nvim -u repro.lua repro.lua to open that lua file with nvim
  4. :LspInfo will show that no lsp sersver is attached
  5. if you run :e on the file, then :LspInfo does show lsp is attached (this is because :e will make all of the autocmds run again from re-opening the file)
  6. comment out the line `event = "VeryLazy" from the config
  7. reopen vim with nvim -u repro.lua repro.lua again
  8. this time the lsp server does attach since the lsp plugin is not lazy loaded

Expected Behavior

to be able to lazy load the plugin and have it work

Repro

-- DO NOT change the paths and don't remove the colorscheme
local root = vim.fn.fnamemodify("./.repro", ":p")

-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "cache" }) do
  vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end

-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", lazypath, })
end
vim.opt.runtimepath:prepend(lazypath)

-- install plugins
local plugins = {
  "folke/tokyonight.nvim",
  -- add any other plugins here
  {
    "neovim/nvim-lspconfig",
    event = "VeryLazy",
    dependencies = {
      'williamboman/mason.nvim',
      'williamboman/mason-lspconfig.nvim',
    },
    config = function()
      require("mason").setup()
      require("mason-lspconfig").setup()
      require("lspconfig").lua_ls.setup({})
      vim.keymap.set('n', 'gk', '<cmd>lua vim.lsp.buf.hover()<CR>')
    end
  },
}
require("lazy").setup(plugins, {
  root = root .. "/plugins",
})

vim.cmd.colorscheme("tokyonight")
-- add anything else here
max397574 commented 11 months ago

:h nvim_do_autocmd in config?

abeldekat commented 11 months ago

Hello @fent,

The expected behavior you describe is already supported.

As the user, only you know how lazy.nvim needs to load a specific plugin. That said, the following issue is interesting: feature: Allow plugins to configure their own laziness

Some plugins depend on autocmd events such as BufEnter, FileType, etc that are set during that pugin's setup

The solution: Specify the proper event instead of just VeryLazy. There are other mechanisms as well: ft, cmd, keys. Plugins are loaded automatically when required, just setting lazy=true is also an option.

You should only specify VeryLazy when you know that the plugin is able to function correctly when its scheduled to load after VimEnter/UIEnter.

The source code of LazyVim provides some good examples of how to load various popular plugins.

The lazy loading of nvim-lspconfig is not broken. This is a heavily optimized plugin, loaded on "BufReadPre", "BufNewFile" in LazyVim. When you open neovim without arguments, the plugin is not loaded. When the plugin loads it registers filetype autocommands, for each lsp you configured. Thus, the lsp only kicks in when the filetype matches.

Sometimes, loading a plugin lazily is not the best option, due to the nature of that plugin. It is possible to tell lazy.nvim that a plugin should not be lazy loaded. That's the default behavior when your spec does not contain any lazy handlers(event, ft, cmd, keys)

Best regards!

fent commented 11 months ago

:h nvim_do_autocmd in config?

the problem with this is 1) running vim.api.nvim_exec_autocmds(event) will run all autocmds for that event, even ones set by other plugins, which can result in unexpected behavior. events.lua solves this by taking note of which autocmds exist before the plugin is loaded, and then filters those out 2) it would be nice if this works out of the box without extra setup by users. a user may assume lazy loading works for a plugin, and then get puzzled when it doesn't (example: https://www.reddit.com/r/neovim/comments/1308ie7/help_how_to_lazy_load_lspconfig/)

You should only specify VeryLazy when you know that the plugin is able to function correctly when its scheduled to load after VimEnter/UIEnter.

My goal is to load a plugin as lazily as possible, even if that plugin depends on BufReadPre/BufNewFile. another goal I have is to reduce neovim startup time. if it's an option to lazy load a plugin, and that plugin's function is not needed immediately at startup, we should provide the option to

abeldekat commented 11 months ago

@fent,

Just a non-authoritative opinion:

Do note that setting up the handlers wrapping ft, event, keys or cmd also takes time. Forcing the plugin to load on an event it's not designed for adds a lot of complexity.

You explicitly state that the lazy loading of nvim-lspconfig is broken. As said, I don't think that is correct. I also do not completely understand the title of your issue. In my opinion, all autocommands are fired. Lazy.nvim only catches the autocommands you configure in the spec. Perhaps, your issue should be of type feature instead of bug.

Regarding the user, the aforementioned feature request is still open.

folke commented 11 months ago

That's because you load lspconfig on VeryLazy. That indeed won't work. Use this instead: event = { "BufReadPre", "BufNewFile" },

fent commented 11 months ago

But I would like to lazy load it on VeryLazy, to save on nvim startup time. that is one of the goals of this project right?

Here is a proof of concept I made for loading on VeryLazy to show that it would work to re-execute autocmd events before it for plugins such a lspconfig

local M = {}

-- Events to check autocmds for. We target events that could fire before vim fully loads.
local events = { "BufEnter", "BufRead", "BufReadPost", "BufReadPre", "BufWinEnter", "FileType" }

local getAutocmdKey = function(autocmd)
  return table.concat({
    autocmd.event,
    autocmd.group or "",
    autocmd.id or "",
    autocmd.command or "",
    autocmd.buffer or "",
  }, "-")
end

local existingAutocmds = {}
vim.api.nvim_create_autocmd("User", {
  pattern = "VeryLazy",
  once = true,
  callback = function()
    -- Take note of which autocmds exist before any plugins are loaded.
    for _, autocmd in pairs(vim.api.nvim_get_autocmds({ event = events })) do
      existingAutocmds[getAutocmdKey(autocmd)] = true
    end
    for _, autocmd in pairs(vim.api.nvim_get_autocmds({ event = events, buffer = vim.api.nvim_list_bufs() })) do
      existingAutocmds[getAutocmdKey(autocmd)] = true
    end
  end
})

M.veryLazy = function(spec)
  local originalConfig = spec.config

  return vim.tbl_extend("force", spec, {
    event = "VeryLazy",
    config = function(plugin, opts)
      if type(originalConfig) == "function" then
        originalConfig(plugin, opts)
      end

      -- Execute any missed autocmd events that fired before the plugin was loaded,
      -- and only for autocmds that were set by this plugin.
      for _, autocmd in pairs(vim.api.nvim_get_autocmds({ event = events })) do
        local autocmd_key = getAutocmdKey(autocmd)
        if not existingAutocmds[autocmd_key] then
          existingAutocmds[getAutocmdKey(autocmd)] = true
          vim.api.nvim_exec_autocmds(autocmd.event, { group = autocmd.group })
        end
      end
      for _, autocmd in pairs(vim.api.nvim_get_autocmds({ event = events, buffer = vim.api.nvim_list_bufs() })) do
        local autocmd_key = getAutocmdKey(autocmd)
        if not existingAutocmds[autocmd_key] then
          existingAutocmds[getAutocmdKey(autocmd)] = true
          vim.api.nvim_exec_autocmds(autocmd.event, { group = autocmd.group, buffer = autocmd.buffer })
        end
      end

      -- Source any ftplugin files for opened buffers.
      for _, bufnr in pairs(vim.api.nvim_list_bufs()) do
        vim.api.nvim_buf_call(bufnr, function()
          local ftplugin_file = plugin.dir .. "/ftplugin/" .. vim.bo.filetype .. ".vim"
          if vim.fn.filereadable(ftplugin_file) == 1 then
            vim.cmd("source " .. ftplugin_file)
          end
        end)
      end
    end
  })
end

return M
folke commented 11 months ago

If you do it as I mentioned, it IS lazy-loaded and will properly work.

But I would like to lazy load it on VeryLazy, to save one nvim startup time. that is one of the goals of this project right?

NO! The goal of this project is to provide a plugin manager and additionally multiple ways (like event) to support lazy-loading plugins.

You can't just use VeryLazy for everything.