From 303a3ed6a874bb5bdebf11ecdf99e1dfa3eed2c3 Mon Sep 17 00:00:00 2001
From: Folke Lemaitre <folke.lemaitre@gmail.com>
Date: Wed, 11 Oct 2023 14:24:18 +0200
Subject: [PATCH] feat(event): added support for structured events (see readme
 on event)

---
 README.md                       | 58 +++++++++++------------
 lua/lazy/core/handler/event.lua | 83 +++++++++++++++++++++------------
 lua/lazy/types.lua              |  2 +-
 3 files changed, 82 insertions(+), 61 deletions(-)

diff --git a/README.md b/README.md
index e8d87c7..f17dd46 100644
--- a/README.md
+++ b/README.md
@@ -79,35 +79,35 @@ require("lazy").setup({
 
 ## 🔌 Plugin Spec
 
-| Property         | Type                                                                                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                            |
-| ---------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **[1]**          | `string?`                                                                                               | Short plugin url. Will be expanded using `config.git.url_format`                                                                                                                                                                                                                                                                                                                                                       |
-| **dir**          | `string?`                                                                                               | A directory pointing to a local plugin                                                                                                                                                                                                                                                                                                                                                                                 |
-| **url**          | `string?`                                                                                               | A custom git url where the plugin is hosted                                                                                                                                                                                                                                                                                                                                                                            |
-| **name**         | `string?`                                                                                               | A custom name for the plugin used for the local plugin directory and as the display name                                                                                                                                                                                                                                                                                                                               |
-| **dev**          | `boolean?`                                                                                              | When `true`, a local plugin directory will be used instead. See `config.dev`                                                                                                                                                                                                                                                                                                                                           |
-| **lazy**         | `boolean?`                                                                                              | When `true`, the plugin will only be loaded when needed. Lazy-loaded plugins are automatically loaded when their Lua modules are `required`, or when one of the lazy-loading handlers triggers                                                                                                                                                                                                                         |
-| **enabled**      | `boolean?` or `fun():boolean`                                                                           | When `false`, or if the `function` returns false, then this plugin will not be included in the spec                                                                                                                                                                                                                                                                                                                    |
-| **cond**         | `boolean?` or `fun(LazyPlugin):boolean`                                                                 | When `false`, or if the `function` returns false, then this plugin will not be loaded. Useful to disable some plugins in vscode, or firenvim for example.                                                                                                                                                                                                                                                              |
-| **dependencies** | `LazySpec[]`                                                                                            | A list of plugin names or plugin specs that should be loaded when the plugin loads. Dependencies are always lazy-loaded unless specified otherwise. When specifying a name, make sure the plugin spec has been defined somewhere else.                                                                                                                                                                                 |
-| **init**         | `fun(LazyPlugin)`                                                                                       | `init` functions are always executed during startup                                                                                                                                                                                                                                                                                                                                                                    |
-| **opts**         | `table` or `fun(LazyPlugin, opts:table)`                                                                | `opts` should be a table (will be merged with parent specs), return a table (replaces parent specs) or should change a table. The table will be passed to the `Plugin.config()` function. Setting this value will imply `Plugin.config()`                                                                                                                                                                              |
-| **config**       | `fun(LazyPlugin, opts:table)` or `true`                                                                 | `config` is executed when the plugin loads. The default implementation will automatically run `require(MAIN).setup(opts)`. Lazy uses several heuristics to determine the plugin's `MAIN` module automatically based on the plugin's **name**. See also `opts`. To use the default implementation without `opts` set `config` to `true`.                                                                                |
-| **main**         | `string?`                                                                                               | You can specify the `main` module to use for `config()` and `opts()`, in case it can not be determined automatically. See `config()`                                                                                                                                                                                                                                                                                   |
-| **build**        | `fun(LazyPlugin)` or `string` or a list of build commands                                               | `build` is executed when a plugin is installed or updated. Before running `build`, a plugin is first loaded. If it's a string it will be ran as a shell command. When prefixed with `:` it is a Neovim command. You can also specify a list to executed multiple build commands. Some plugins provide their own `build.lua` which is automatically used by lazy. So no need to specify a build step for those plugins. |
-| **branch**       | `string?`                                                                                               | Branch of the repository                                                                                                                                                                                                                                                                                                                                                                                               |
-| **tag**          | `string?`                                                                                               | Tag of the repository                                                                                                                                                                                                                                                                                                                                                                                                  |
-| **commit**       | `string?`                                                                                               | Commit of the repository                                                                                                                                                                                                                                                                                                                                                                                               |
-| **version**      | `string?` or `false` to override the default                                                            | Version to use from the repository. Full [Semver](https://devhints.io/semver) ranges are supported                                                                                                                                                                                                                                                                                                                     |
-| **pin**          | `boolean?`                                                                                              | When `true`, this plugin will not be included in updates                                                                                                                                                                                                                                                                                                                                                               |
-| **submodules**   | `boolean?`                                                                                              | When false, git submodules will not be fetched. Defaults to `true`                                                                                                                                                                                                                                                                                                                                                     |
-| **event**        | `string?` or `string[]` or `fun(self:LazyPlugin, event:string[]):string[]`                              | Lazy-load on event. Events can be specified as `BufEnter` or with a pattern like `BufEnter *.lua`                                                                                                                                                                                                                                                                                                                      |
-| **cmd**          | `string?` or `string[]` or `fun(self:LazyPlugin, cmd:string[]):string[]`                                | Lazy-load on command                                                                                                                                                                                                                                                                                                                                                                                                   |
-| **ft**           | `string?` or `string[]` or `fun(self:LazyPlugin, ft:string[]):string[]`                                 | Lazy-load on filetype                                                                                                                                                                                                                                                                                                                                                                                                  |
-| **keys**         | `string?` or `string[]` or `LazyKeys[]` or `fun(self:LazyPlugin, keys:string[]):(string \| LazyKeys)[]` | Lazy-load on key mapping                                                                                                                                                                                                                                                                                                                                                                                               |
-| **module**       | `false?`                                                                                                | Do not automatically load this Lua module when it's required somewhere                                                                                                                                                                                                                                                                                                                                                 |
-| **priority**     | `number?`                                                                                               | Only useful for **start** plugins (`lazy=false`) to force loading certain plugins first. Default priority is `50`. It's recommended to set this to a high number for colorschemes.                                                                                                                                                                                                                                     |
-| **optional**     | `boolean?`                                                                                              | When a spec is tagged optional, it will only be included in the final spec, when the same plugin has been specified at least once somewhere else without `optional`. This is mainly useful for Neovim distros, to allow setting options on plugins that may/may not be part of the user's plugins                                                                                                                      |
+| Property         | Type                                                                                                                                | Description                                                                                                                                                                                                                                                                                                                                                                                                            |
+| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **[1]**          | `string?`                                                                                                                           | Short plugin url. Will be expanded using `config.git.url_format`                                                                                                                                                                                                                                                                                                                                                       |
+| **dir**          | `string?`                                                                                                                           | A directory pointing to a local plugin                                                                                                                                                                                                                                                                                                                                                                                 |
+| **url**          | `string?`                                                                                                                           | A custom git url where the plugin is hosted                                                                                                                                                                                                                                                                                                                                                                            |
+| **name**         | `string?`                                                                                                                           | A custom name for the plugin used for the local plugin directory and as the display name                                                                                                                                                                                                                                                                                                                               |
+| **dev**          | `boolean?`                                                                                                                          | When `true`, a local plugin directory will be used instead. See `config.dev`                                                                                                                                                                                                                                                                                                                                           |
+| **lazy**         | `boolean?`                                                                                                                          | When `true`, the plugin will only be loaded when needed. Lazy-loaded plugins are automatically loaded when their Lua modules are `required`, or when one of the lazy-loading handlers triggers                                                                                                                                                                                                                         |
+| **enabled**      | `boolean?` or `fun():boolean`                                                                                                       | When `false`, or if the `function` returns false, then this plugin will not be included in the spec                                                                                                                                                                                                                                                                                                                    |
+| **cond**         | `boolean?` or `fun(LazyPlugin):boolean`                                                                                             | When `false`, or if the `function` returns false, then this plugin will not be loaded. Useful to disable some plugins in vscode, or firenvim for example.                                                                                                                                                                                                                                                              |
+| **dependencies** | `LazySpec[]`                                                                                                                        | A list of plugin names or plugin specs that should be loaded when the plugin loads. Dependencies are always lazy-loaded unless specified otherwise. When specifying a name, make sure the plugin spec has been defined somewhere else.                                                                                                                                                                                 |
+| **init**         | `fun(LazyPlugin)`                                                                                                                   | `init` functions are always executed during startup                                                                                                                                                                                                                                                                                                                                                                    |
+| **opts**         | `table` or `fun(LazyPlugin, opts:table)`                                                                                            | `opts` should be a table (will be merged with parent specs), return a table (replaces parent specs) or should change a table. The table will be passed to the `Plugin.config()` function. Setting this value will imply `Plugin.config()`                                                                                                                                                                              |
+| **config**       | `fun(LazyPlugin, opts:table)` or `true`                                                                                             | `config` is executed when the plugin loads. The default implementation will automatically run `require(MAIN).setup(opts)`. Lazy uses several heuristics to determine the plugin's `MAIN` module automatically based on the plugin's **name**. See also `opts`. To use the default implementation without `opts` set `config` to `true`.                                                                                |
+| **main**         | `string?`                                                                                                                           | You can specify the `main` module to use for `config()` and `opts()`, in case it can not be determined automatically. See `config()`                                                                                                                                                                                                                                                                                   |
+| **build**        | `fun(LazyPlugin)` or `string` or a list of build commands                                                                           | `build` is executed when a plugin is installed or updated. Before running `build`, a plugin is first loaded. If it's a string it will be ran as a shell command. When prefixed with `:` it is a Neovim command. You can also specify a list to executed multiple build commands. Some plugins provide their own `build.lua` which is automatically used by lazy. So no need to specify a build step for those plugins. |
+| **branch**       | `string?`                                                                                                                           | Branch of the repository                                                                                                                                                                                                                                                                                                                                                                                               |
+| **tag**          | `string?`                                                                                                                           | Tag of the repository                                                                                                                                                                                                                                                                                                                                                                                                  |
+| **commit**       | `string?`                                                                                                                           | Commit of the repository                                                                                                                                                                                                                                                                                                                                                                                               |
+| **version**      | `string?` or `false` to override the default                                                                                        | Version to use from the repository. Full [Semver](https://devhints.io/semver) ranges are supported                                                                                                                                                                                                                                                                                                                     |
+| **pin**          | `boolean?`                                                                                                                          | When `true`, this plugin will not be included in updates                                                                                                                                                                                                                                                                                                                                                               |
+| **submodules**   | `boolean?`                                                                                                                          | When false, git submodules will not be fetched. Defaults to `true`                                                                                                                                                                                                                                                                                                                                                     |
+| **event**        | `string?` or `string[]` or `fun(self:LazyPlugin, event:string[]):string[]` or `{event:string[]\|string, pattern?:string[]\|string}` | Lazy-load on event. Events can be specified as `BufEnter` or with a pattern like `BufEnter *.lua`                                                                                                                                                                                                                                                                                                                      |
+| **cmd**          | `string?` or `string[]` or `fun(self:LazyPlugin, cmd:string[]):string[]`                                                            | Lazy-load on command                                                                                                                                                                                                                                                                                                                                                                                                   |
+| **ft**           | `string?` or `string[]` or `fun(self:LazyPlugin, ft:string[]):string[]`                                                             | Lazy-load on filetype                                                                                                                                                                                                                                                                                                                                                                                                  |
+| **keys**         | `string?` or `string[]` or `LazyKeys[]` or `fun(self:LazyPlugin, keys:string[]):(string \| LazyKeys)[]`                             | Lazy-load on key mapping                                                                                                                                                                                                                                                                                                                                                                                               |
+| **module**       | `false?`                                                                                                                            | Do not automatically load this Lua module when it's required somewhere                                                                                                                                                                                                                                                                                                                                                 |
+| **priority**     | `number?`                                                                                                                           | Only useful for **start** plugins (`lazy=false`) to force loading certain plugins first. Default priority is `50`. It's recommended to set this to a high number for colorschemes.                                                                                                                                                                                                                                     |
+| **optional**     | `boolean?`                                                                                                                          | When a spec is tagged optional, it will only be included in the final spec, when the same plugin has been specified at least once somewhere else without `optional`. This is mainly useful for Neovim distros, to allow setting options on plugins that may/may not be part of the user's plugins                                                                                                                      |
 
 ### Lazy Loading
 
diff --git a/lua/lazy/core/handler/event.lua b/lua/lazy/core/handler/event.lua
index 3a8d9a7..69f9086 100644
--- a/lua/lazy/core/handler/event.lua
+++ b/lua/lazy/core/handler/event.lua
@@ -4,12 +4,14 @@ local Util = require("lazy.core.util")
 
 ---@class LazyEventOpts
 ---@field event string
----@field pattern? string
 ---@field group? string
 ---@field exclude? string[]
 ---@field data? any
 ---@field buffer? number
 
+---@alias LazyEvent {id:string, event:string[]|string, pattern?:string[]|string}
+---@alias LazyEventSpec string|{event?:string|string[], pattern?:string|string[]}|string[]
+
 ---@class LazyEventHandler:LazyHandler
 ---@field events table<string,true>
 ---@field group number
@@ -23,28 +25,64 @@ M.triggers = {
 
 M.group = vim.api.nvim_create_augroup("lazy_handler_event", { clear = true })
 
----@param value string
-function M:_add(value)
-  local event_spec = self:_event(value)
-  ---@type string?, string?
-  local event, pattern = event_spec:match("^(%w+)%s+(.*)$")
-  event = event or event_spec
+---@param spec LazyEventSpec
+---@return LazyEvent
+function M:parse(spec)
+  local ret = M.mappings[spec] --[[@as LazyEvent?]]
+  if ret then
+    return ret
+  end
+  if type(spec) == "string" then
+    local event, pattern = spec:match("^(%w+)%s+(.*)$")
+    event = event or spec
+    return { id = spec, event = event, pattern = pattern }
+  elseif Util.is_list(spec) then
+    ret = { id = table.concat(spec, "|"), event = spec }
+  else
+    ret = spec --[[@as LazyEvent]]
+    if not ret.id then
+      ---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch
+      ret.id = type(ret.event) == "string" and ret.event or table.concat(ret.event, "|")
+      if ret.pattern then
+        ---@diagnostic disable-next-line: assign-type-mismatch, param-type-mismatch
+        ret.id = ret.id .. " " .. (type(ret.pattern) == "string" and ret.pattern or table.concat(ret.pattern, ", "))
+      end
+    end
+  end
+  return ret
+end
+
+---@param plugin LazyPlugin
+function M:values(plugin)
+  ---@type table<string,any>
+  local values = {}
+  ---@diagnostic disable-next-line: no-unknown
+  for _, value in ipairs(plugin[self.type] or {}) do
+    local event = self:parse(value)
+    values[event.id] = event
+  end
+  return values
+end
+
+---@param event LazyEvent
+function M:_add(event)
   local done = false
-  vim.api.nvim_create_autocmd(event, {
+  vim.api.nvim_create_autocmd(event.event, {
     group = self.group,
     once = true,
-    pattern = pattern,
+    pattern = event.pattern,
     callback = function(ev)
-      if done or not self.active[value] then
+      if done or not self.active[event.id] then
         return
       end
+      -- HACK: work-around for https://github.com/neovim/neovim/issues/25526
       done = true
-      Util.track({ [self.type] = value })
+      Util.track({ [self.type] = event.id })
 
-      local state = M.get_state(ev.event, pattern, ev.buf, ev.data)
+      local state = M.get_state(ev.event, ev.buf, ev.data)
 
       -- load the plugins
-      Loader.load(self.active[value], { [self.type] = value })
+      Loader.load(self.active[event.id], { [self.type] = event.id })
 
       -- check if any plugin created an event handler for this event and fire the group
       for _, s in ipairs(state) do
@@ -57,38 +95,23 @@ end
 
 -- Get the current state of the event and all the events that will be fired
 ---@param event string
----@param pattern? string
 ---@param buf number
 ---@param data any
-function M.get_state(event, pattern, buf, data)
+function M.get_state(event, buf, data)
   local state = {} ---@type LazyEventOpts[]
   while event do
     table.insert(state, 1, {
       event = event,
-      pattern = pattern,
       exclude = event ~= "FileType" and M.get_augroups(event) or nil,
       buffer = buf,
       data = data,
     })
     data = nil -- only pass the data to the first event
-    if event == "FileType" then
-      pattern = nil -- only use the pattern for the first event
-    end
     event = M.triggers[event]
   end
   return state
 end
 
----@param value string
-function M:_event(value)
-  if value == "VeryLazy" then
-    return "User VeryLazy"
-  elseif value == "BufRead" then
-    return "BufReadPost"
-  end
-  return value
-end
-
 -- Get all augroups for the events
 ---@param event string
 function M.get_augroups(event)
@@ -127,7 +150,6 @@ function M._trigger(opts)
     Util.info({
       "# Firing Events",
       "  - **event:** " .. opts.event,
-      opts.pattern and ("  - **pattern:** " .. opts.pattern),
       opts.group and ("  - **group:** " .. opts.group),
       opts.buffer and ("  - **buffer:** " .. opts.buffer),
     })
@@ -135,7 +157,6 @@ function M._trigger(opts)
   Util.track({ event = opts.group or opts.event })
   Util.try(function()
     vim.api.nvim_exec_autocmds(opts.event, {
-      -- pattern = opts.pattern,
       buffer = opts.buffer,
       group = opts.group,
       modeline = false,
diff --git a/lua/lazy/types.lua b/lua/lazy/types.lua
index 3e7f74d..e83b9c4 100644
--- a/lua/lazy/types.lua
+++ b/lua/lazy/types.lua
@@ -29,7 +29,7 @@
 ---@field opts? PluginOpts
 
 ---@class LazyPluginHandlers
----@field event? string[]
+---@field event? LazyEventSpec[]
 ---@field cmd? string[]
 ---@field ft? string[]
 ---@field keys? (string|LazyKeysSpec)[]