Repository navigation
fix!: follow lazy.nvim for inits, merge order, key proxies, :colorscheme, throwing opts and ftdetect/ - #54
Merged
Merged
Conversation
zuqini
force-pushed
the
init-require-eager
branch
from
November 4, 2026 06:56
0bf4c57 to
286eae8
Compare
init can require an eager plugin, and a plugin's specs merge in import order:colorscheme and throwing opts
:colorscheme and throwing opts:colorscheme, throwing opts and ftdetect/
zuqini
force-pushed
the
init-require-eager
branch
2 times, most recently
from
November 5, 2026 09:34
3c15a39 to
0d9c975
Compare
…heme`, throwing `opts` and `ftdetect/` - fix(startup): a `require` from an `init` loads the eager plugin holding the module, as in lazy.nvim, instead of failing with "module not found" - fix(merge)!: a plugin's specs merge in import order, as in lazy.nvim, so a later spec under `dependencies` overrides a top-level one and its function `opts` sees the top-level opts - fix(startup): a lazy plugin's `cmd`, `event`, `ft` and `keys` are set up before the inits, as in lazy.nvim, so an `init` running one loads the plugin - fix(keys): a plugin drops its key proxies as it loads, as in lazy.nvim, so a plugin that maps a key only when it's free (flash.nvim's `;` and `,`) gets it - fix(keys): a key keeps its proxy while a plugin listing it is pending, as in lazy.nvim, so another plugin's `keys` on the same lhs don't stop it loading - fix(startup): `:colorscheme` loads the pending plugin holding the scheme first, as in lazy.nvim, so a Vim colors file can call its plugin's `autoload/` from an `init` or for a lazy plugin, and an eager scheme plugin set from an earlier plugin's `config` (themery.nvim's restore) gets its own `config` first - fix(loader): a function-form `opts` that throws is reported once and the plugin still loads without its `config`, as in lazy.nvim, instead of staying pending - fix(ft): a lazy `ft` plugin's `ftdetect/` is sourced once, as in lazy.nvim, so one that runs `set filetype=` fires `FileType` once per file - fix(startup): an eager plugin's `ftdetect/` runs when filetype detection is on before `setup()`, as in 9C2A lazy.nvim, so LazyVim's vimtex opens a `.cls` as `tex` - fix(loader): a plugin already on 'runtimepath' that loads after a `config` turned detection on gets its `ftdetect/` once, not twice - fix(runtime): zpack escapes the path of each file it sources itself, so a `#` or `%` in the data path no longer fails with E194/E484 and loses the file - fix(hooks): removing a plugin drops its command and key proxies, so its command gives E492 and a command it shared with another lazy plugin works for that plugin; an `init` removing a plugin no longer re-fires its event or crashes `setup()` when the plugin has its own `init`
zuqini
force-pushed
the
init-require-eager
branch
from
November 5, 2026 19:44
59071b2 to
0a9e71f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Title: fix!: follow lazy.nvim for inits, merge order, key proxies, < 8000 code class="notranslate">:colorscheme, throwing
optsandftdetect/Follow-up to #53, found in its round-3 review. For 3.0 (#48). Fixes bead zpack_nvim-0la and reverses decision zpack_nvim-86p. Together the first two changes make AstroNvim's own
mason-tool-installer.luawork: under zpack it errored at every start, first in itsinit, then in astrocore'sopts. The other six came out of this PR's review rounds, each to what lazy.nvim does: a lazy plugin's trigger works from aninit, key proxies (two commits),:colorschemeof a pending plugin's scheme, a throwing function-formopts, and anftplugin'sftdetect/sourced once. On top, an eager plugin'sftdetect/now runs when filetype detection is on beforesetup(), the one item of bead zpack_nvim-ckx fixed here.An
initcan require an eager pluginWhat changed: a
requirefrom aninitnow finds an eager plugin's module and loads that plugin there, as in lazy.nvim: its dependencies first, then itsconfigand keys. The startup loop then skips it. Before this therequirefailed with "module 'x' not found", because everyinitruns before any eager plugin is on 'runtimepath' and the module loader only looked at lazy plugins.Why: AstroNvim's own
mason-tool-installer.luahasinit = function(plugin) require("astrocore").on_load("mason.nvim", plugin.name) end, and astrocore islazy = false, priority = 10000. Under zpack thatiniterrored at every start. The same failed for an eager library at any priority and for a plugin's owninitrequiring its own module. lazy.nvim'sget_unloaded_rtplists every plugin not loaded yet, start plugins included, soauto_loadloads the start plugin early (lua/lazy/core/loader.lua,util.lua). Same at 2.0.4 (ec09994).How:
lazy.setup_triggers(so before the inits) until they are on 'runtimepath' (after the batched add, or the:packaddpass whensetup()runs late). After thatrequirefinds their modules on 'runtimepath' first, so they drop out of the scan, and a missing-module probe in aconfigcosts what it did.plugin_loader.process_spec, the path lazy plugins already take. Whensetup()runs frominit.luathat means:packadd!, so itsplugin/still runs at native startup, after everyinit, and sees thevim.gsettings later inits set, its own included.:packadd!put the plugin ahead of its place on 'runtimepath'. It comes off again just before the batched add, soplugin/order and shadowed runtime files stay asdocs/tips.mddescribes for 3.0: higherpriorityfirst, an eager dependency before the plugin listing it. A lazy dependency it loads stays behind the eager plugins, as any lazy plugin aninitloads already did at 241c9ba.setup()runs late (atVimEnter, or from aplugin/script), the:packaddpass skips a plugin already loaded, so itsplugin/isn't sourced twice, also when its own lazy dependency'sconfigloaded it through a plugin listing it. That also covers a lazy dependency'sconfigrequiring a later eager plugin during that pass, which failed the same way before, and fixes the "E after A" order of zpack_nvim-uqx. The pass also marks a pluginsourcedas it sources it, andplugin_loader.process_specthen runs only itsconfig: so the "E before A" order of uqx (an eager plugin sourced earlier in the pass, then listed by a lazy dependency loading there), and aplugin/requiring a lower-priority eager plugin that lists it underdependencies, no longer source itsplugin/a second time. Whensetup()runs frominit.lua, every eager plugin is on 'runtimepath' by the time anyconfigruns, so thatrequirereturns the module before the plugin'sconfig, as at 241c9ba and 2.0.4; lazy.nvim loads it first. That's zpack_nvim-3ph, not changed here. The module loader leaves a plugin already on 'runtimepath' to the searchers after it, so a lazy dependency'sconfigrequiring its eager parent's compiled module during that pass doesn't load the parent inside thatconfig(thesourcedmark covers the parent's ownplugin/).An
initthat runsvim.cmd.colorschemefor its own eager plugin (kickstart.nvim's old tokyonight spec) works too now; see the:colorschemesection below for a Vim colors file that calls its plugin'sautoload/.Behaviour change: an
initguarded withpcall(require, ...)now gets the module, and that plugin'sconfigruns during theinit, before the other eager plugins'configwhatever theirpriority, and before its owninitif that comes later (inits run highestpriorityfirst; lazy.nvim runs them in hash order, so it can do the same).docs/tips.md"Upgrading to 3.0" and:h zpack-Spec.initsay so. Itsplugin/still runs at native startup whensetup()runs frominit.lua, as for any eager plugin, so a command it defines isn't there yet in theinit; lazy.nvim sourcesplugin/as it loads the plugin.:h zpack-Spec.initand the tips.md compatibility notes now say that. Since 241c9ba, aninitrequiring a module of an eager import owner ({ 'u/dist', import = 'dist.plugins' }) not loaded yet loads the owner there, with itsconfig, as in lazy.nvim. It used to get the module through the import path without loading the owner.Tests: real-process tests in
tests/startup_process_test.lua, on nightly and v0.12.0:initrequiring an eager plugin's module at priority 50, 1000 and 10000, and a plugin's owninitrequiring its own module: therequirereturns the module, the plugin'sconfigran before it returned, andconfig,plugin/andafter/plugineach ran once.cmd-lazy plugin whoseinitrequires it and lists it underdependencieswith functionopts): the library loads, the lazy plugin stays pending.setup()atVimEnterand from the config'splugin/(v:vim_did_initis 1 there, as in real startup): loaded once.configrequiring a later eager plugin during the late:packaddpass: loaded once. The same requiring a plugin that lists its parent underdependencies, atVimEnterand fromplugin/: the parent'splugin/andconfigrun once.priorityorder forconfig, and the early one keeps its place inplugin/andafter/pluginorder; it sources before a plugin listing it as a dependency.plugin/sees thevim.gthe inits set, including one set by the early plugin's own laterinit.plugin/runs before the plugin's, the lazy one's after every eager plugin's.setup()atVimEnteror fromplugin/, an eager plugin whoseplugin/(or its lazy dependency'sconfig) requires its compiled module:plugin/andconfigrun once, and the dependency'sconfigfinishes before the parent'splugin/.make_repo'splugin/script also logs whethervim.g.<name>_flagwas set, andstart()can putsetup()in the config'splugin/. Mutation checks: scanning only lazy plugins again fails all 13inittests; dropping the 'runtimepath' check in the module loader fails "doesn't load it again from its lazy dependency's config" (thesourcedmark covers the twoplugin/ones); dropping the skip in the:packaddpass fails the five late-setup()ones, and checking before the lazy dependencies load fails the two that load the parent from its dependency; dropping the 'runtimepath' re-placement fails the two order ones and "loads its lazy dependency first, once".A plugin's specs merge in import order
What changed: a plugin's specs merge in import order, as in lazy.nvim, with a spec under
dependenciesright after the spec listing it. Before this every spec under some plugin'sdependenciesmerged first and the top-level ones after, so a top-level spec always won (merge_fields.sort_specs, the same since 2.x). The change drops that tier from the sort (-5 lines).Why: AstroNvim's
mason-tool-installer.lualists astrocore underdependencieswithopts = function(_, opts) local maps = opts.mappings; maps.n["<Leader>pM"] = ... end.opts.mappingscomes from astrocore's own spec in_astrocore_mappings.lua, imported earlier. Merged first, the function foundmapsnil, astrocore'sconfigthrew at every start and none of AstroNvim's core mappings were set; with the first change the error also surfaced from mason-tool-installer'sinit. lazy.nvim merges a plugin's fragments in the ordermeta._.fragsholds them, which is import order (lua/lazy/core/meta.lua_rebuild,fragments.luaadd). zpack_nvim-86p kept the old rule so the result didn't depend onlua/plugins/*.luaorder; the owner reversed it for this.Behaviour change: a later spec under
dependenciescan now override a top-level field, replace or disable its key, clear its trigger withfalseand setenabled, and its function-formoptsgets the top-level opts before it.opts_extendlists append in this order too. Which spec's source wins when specs of two owners fold into one plugin hasn't changed:fold.lua's tiers (dev, explicit source, top-level, dependency) pick only the source, and keep #39's shape independent of import order.docs/tips.md"Upgrading to 3.0" gains an entry, its compatibility note on merge order is gone, and:h zpack-Spec.dependenciesdescribes the order, withenabled,opts_extend,eventandkeyspointing to it. One shape still differs from lazy.nvim: a plugin listed inside its own subtree (x->y->xunderdependencies) merges the outer spec first, lazy.nvim the inner one.Tests: six tests pinned the old rule and now pin the new one, each rewritten on purpose: "a standalone spec's key beats a dependency's" (now "a later spec under dependencies replaces a top-level spec's key", plus the reverse case), "give a top-level spec the last word over a later spec under dependencies" (now "let a later spec under dependencies disable a top-level plugin"), "sort_specs puts dependencies before standalone", "opts_extend appends lists from dependencies specs before top-level ones" and two
opts_extendorder assertions inlazy_parity_test.lua({ 'delve', 'stylua' }is now{ 'stylua', 'delve' }, lazy.nvim's result). The resolve fuzz digest changes too: 769 of 1000 seeds decide something different, all from merge order (fragment order, then whatenabled,nameand the merged fields give). New:optssettingopts.mappings, and acmd-lazy plugin whoseinitrequires it and lists it underdependencieswith functionoptswriting intoopts.mappings):configgets both. A later spec underdependenciesoverrides a top-level key'sdesc, clears a top-levelcmdwithfalse(the plugin turns eager) and disables a top-levelenabled = trueplugin. A later top-level spec still overrides an earlier one underdependencies.event,cmd,ftandkeyscleared by a laterfalseunderdependencies.Mutation check: with the dependency tier back, the four order-sensitive real-process tests and all the rewritten and new in-process ones fail; the later-top-level one passes both ways, as it should.
A lazy plugin's trigger works from an
initWhat changed: a lazy plugin's
cmd,event,ftandkeysare set up before the inits, as in lazy.nvim, so aninitrunning one loads the plugin. Before thisvim.cmd('Foo')from aninit,Foobeing a lazy plugin'scmd, failed with "Not an editor command", and an event, filetype or key aninittriggered loaded nothing: the triggers were set up after the eager plugins'config. Same at 241c9ba and 2.0.4. Bead zpack_nvim-1hr, found in round 5.Why: lazy.nvim's
Loader.setup()runsHandler.setup(), which enables every unloaded plugin's handlers, beforeLoader.startup()runs the inits and loads the start plugins (lua/lazy/core/loader.lua,handler/init.lua).How:
lazy.setup_triggers(waslazy.process_all) runs ininit.lua'sprocess_allbeforestartup.process_all, after the module loader is in and its cache covers the eager plugins, so a function-form trigger that requires one loads it as aninitwould (see the behaviour change below). A plugin loaded later in startup (a dependency of an eager plugin, arequire, a pending build) drops its proxy command as it loads, which #52'scmd_proxy.releasealready did, and now its key proxies too (next section). Builds still run after the eager plugins, and the comment on why they had to come before the triggers is gone. TheVeryLazyloads and theUser VeryLazyemit stay where they were, after the eager plugins and builds:fire_very_lazynow also registers theVeryLazyloads thatevent.setupcollected. lazy.nvim also registers itsVeryLazyemit after startup (LazyDone), so a callback an eagerconfigschedules still runs before aVeryLazyplugin loads whensetup()runs atVimEnter.Behaviour change: with the triggers set up earlier, configs that don't touch them in an
initchange in four ways, each to what lazy.nvim does, and in a fifth where lazy.nvim differs. An event an eagerconfigtriggers (aUserevent,ColorSchemefromvim.cmd.colorscheme()) loads the lazy plugins waiting on it there. An autocmd aninitor eagerconfigcreates runs after the lazy plugin waiting on the same event has loaded, not before. A mapping aninitor eagerconfigsets on a lazy plugin'skeyslhs, or a command it defines with a lazy plugin'scmdname, replaces zpack's, so the key or command no longer loads the plugin; it used to be the other way round. One difference from lazy.nvim stays here: when the plugin then loads some other way, a:commandwithout!in itsplugin/fails with E174, sincecmd_proxy.releasekeeps a command that isn't zpack's (as it keeps the real command another plugin claiming the name defined), where lazy.nvim deletes whatever has the name. A function-form trigger is called before the inits, so it doesn't see avim.ganinitsets. Nor does anftplugin'sftdetect/when filetype detection is already on assetup()runs, atVimEnteror after afiletype onininit.lua(lazy.nvim'sfthandler sources it inadd); when it isn't on yet, it runs after the eager plugins, or as the plugin loads if aninitor eagerconfigturned detection on before that (next paragraph). The fifth: arequirein a function-form trigger loads the plugin holding the module right there, so itsconfigruns before everyinit, its own included, and before the other eager plugins'config. 241c9ba loaded a lazy plugin after the eager plugins'config, and an eager one in itspriorityturn; lazy.nvim returns the module and loads the plugin after the inits, in its turn (auto_loadwaits fordid_handlers). An eager plugin withmodule = falseisn't found there, as in lazy.nvim. The fix is to require inside the key's or command's function, as LazyVim's harpoon2keysdo; aneventorftfunction can return a static list and leave the module to the plugin'sconfigor an autocmd's callback. No distro or config we checked has the shape: LazyVim's four trigger functions (harpoon2, leap and mini.surroundkeys, elixir's render-markdownft) require no plugin's module when called, and AstroNvim, kickstart and my config have none that do.docs/tips.md"Upgrading to 3.0" gains an entry,:h zpack-Spec.initsays the triggers are set and what arequirein a function-form one does, and:h zpack-Spec.event,cmd,keysandftpoint there. An eager plugin's ownkeyson a lazy plugin'skeyslhs leave zpack's proxy, as at 241c9ba (next section). A lazy plugin aninitor an eagerconfigloads this way whensetup()runs frominit.luasourcesplugin/at the load and again at native startup, like oneinit.lualoads by its trigger, and as a bare:packaddininit.luadoes in plain Neovim: zpack_nvim-3iy, closed won't-fix; tips.md says so.ftdetect/once: a lazyftplugin'sftdetect/is now sourced once, as in lazy.nvim (Loader.ftdetectkeepsdid_ftdetectper path). zpack sourced it at trigger setup, and:packaddsourced it again when the plugin loaded after filetype detection was on; with the triggers before the inits, Neovim'sfiletype onalso sourced it again for one that loaded during startup frominit.lua(an eager plugin's dependency, arequirefrom an eagerconfig). So anftdetect/that runsset filetype=(cql-vim, pkl-neovim, vim-godot and vim-arduino in my config) firedFileTypetwice for every later file of that type, and ftplugin, syntax, LSP and treesitter attach ran twice. The:packaddhalf is the same at 241c9ba; r6 and r8 dropped it as "lazy.nvim does the same", which it doesn't. Nowruntime.packadd_sourcingloads a plugin whoseftdetect/zpack sourced with:packadd!and sources itsplugin/itself, as:packadddoes (.vimfiles, then.lua, ignoring 'wildignore'); a plugin with noftdetect/still loads with:packadd. Frominit.lua, before filetype detection is on,ft.setupleavesftdetect/until the eager plugins and builds are done and then sources it for theftplugins still pending. A lazy plugininit.lualoads after the config turned detection on (aninitrunningsyntax enable) gets itsftdetect/sourced at the load, as:packaddwould; Neovim'sfiletype onhas run by then, so at 241c9ba it got none. One per-file sourcing helper servesplugin/,after/plugin/andftdetect/.docs/tips.md"Smaller changes" 8000 says so.Tests: real-process, nightly and v0.12.0: an
initrunning a lazy plugin'scmd(itsplugin/defines the real command),Userevent,ft(:setfiletype) andkeys(:normal), withsetup()ininit.luaand atVimEnter: the plugin'sconfig(and the real command or key) ran before theinitwent on. AVeryLazyplugin withsetup()atVimEnterloads after a callback an eagerconfigscheduled. Aninitrunning the staticcmdof a plugin whosekeysare a function. Akeysfunction requiring an eager plugin's module, or its own lazy plugin's: that plugin'sconfigruns right there, before the inits and the higher-priorityconfigs, pinned so a change to it is deliberate; an eagerconfigatpriority10, 50 or 100 requiring that lazy module gets it configured; aninitremoving that plugin still letssetup()finish; aninitremoving a lazyevent = 'FileType'plugin leavesFileTypefiring once, not re-fired by its stale trigger (round 20; mutation: the old guard fails both modes). In-process: acmd, a key and an ft-scoped key that a static and a function-form trigger share load both plugins. The PR's existing real-process tests pass unchanged, includingplugin/order andvim.gfrom inits. Mutation check: with the triggers set up after the eager plugins again, all 10 trigger tests fail; registering theVeryLazyloads inevent.setupagain fails theVeryLazyone; setting only the function-form triggers up after the eager plugins, or having theirrequires load the plugin after the inits, fails the four "before the inits" ones. A lazyftplugin whoseftdetect/runsset filetype=, listed under an eager plugin'sdependenciesor loading on its filetype, withsetup()ininit.luaand atVimEnter:ftdetect/runs once andFileTypefires once per file after the first. Mutation check: with:packaddagain, three of the four fail; sourcingftdetect/at trigger setup frominit.luaagain fails the fourth. Also frominit.lua: on its first install with abuild; with aninitturning filetype detection on before it loads, and an eagerconfigturning it on after; withfiletype onbeforesetup(), where itsftdetect/runs before the inits; withfiletype plugin indent onbeforesetup()and nofttrigger, as alazy = truedependency or required from aninit; with its own dependency'sconfigrunningsyntax enable; and loading on its filetype with 'wildignore' set to*.lua, which sources itsplugin/.vimfirst, as:packadddoes. In-process: anftplugin withoutftdetect/loads with:packadd. Mutation check: each of these fails one of them: running the builds afterftdetect/; noftdetect/at a load once detection is on, or only forftplugins; sourcing it there after the dependencies; sourcing it after the eager plugins for every deferred plugin once detection is on; deferring it with detection already on; a{vim,lua}glob or one honouring 'wildignore'; marking a plugin with noftdetect/files.A lazy plugin's key proxies follow lazy.nvim
What changed: a plugin drops the proxies of its keys that no other pending plugin lists as it starts to load, before its
plugin/andconfig, however it loads (arequire, a dependency, itscmd, a key, a build). And a key keeps its proxy while a plugin listing it is pending: another plugin's key on the same lhs, mode, ft and buffer, eager or lazy, isn't mapped over it. Before this, only proxy commands were dropped (cmd_proxy.release), and a loading plugin'skeysreplaced any proxy. Beads zpack_nvim-cg8 and zpack_nvim-coy, found in round 6.Why: moving the triggers before the inits made two old gaps show at startup. flash.nvim with
keys = { 'f', 'F', 't', 'T', ';', ',' }maps;and,only when they're free; loaded by an eagerconfig'srequire, it found zpack's proxies there, and the first;fell through to the native key. Pressingffirst did the same at 241c9ba. And an eager plugin'skeys, bound after everyconfig, now came after the proxies: in my config snacks.nvim's eager<leader>ghreplaced gitlinker.nvim's proxy, so gitlinker no longer loaded from it. lazy.nvim keeps one handler per key until every plugin claiming it has loaded (handler/init.luaM:add,M:del), andLoader._loadcallsHandler.disablebefore sourcing the plugin.How:
key_proxy.lua, next tocmd_proxy.lua.release(src)runs besidecmd_proxy.releaseinprocess_specand deletes each global or buffer-local proxy of the plugin's keys that no other pending plugin lists, where the mapping is still that proxy (one callback per key, compared by identity), so a mapping aninitorconfigput there stays.bind_keysleaves out the modes whose proxy a pending plugin still lists, and a pressed proxy stops counting.Behaviour change: against 241c9ba, a lazy plugin's other key proxies are gone once it loads, and a key two lazy plugins list stays a proxy until both load: when one loaded by a
require, its mapping used to replace the proxy, so the other never loaded from that key. An eager plugin'skeysagainst a lazy plugin's give the proxy, as at 241c9ba. Which plugin's rhs a shared key ends up with can differ from lazy.nvim, whose pick depends on table order; the pending plugin loads in both (decision zpack_nvim-zhe).docs/tips.md"Smaller changes" and:h zpack-Spec.keyssay so.Tests: real-process, nightly and v0.12.0,
setup()ininit.luaand atVimEnter: an eager plugin's and a lazy plugin's key on one lhs (pressing it loads the lazy plugin and runs its rhs), a lazy plugin with an lhs-only key that itsconfigmaps only when free, loaded by an eagerconfig'srequireand by pressing its other key. Mutation check: withoutunclaimedthe two shared-key tests fail; withoutreleasethe four free-key tests fail. Probes over shared keys across lazy plugins and modes, ft- and buffer-scoped keys, an abbreviation, and aconfigmapping over the proxy match lazy.nvim apart from the rhs pick and that last one (lazy.nvim deletes the user's mapping).:colorschemeloads the plugin holding the schemeWhat changed:
:colorscheme xfirst loads the pending plugin that holdscolors/x.vimorcolors/x.lua, as in lazy.nvim: a lazy one at any time, an eager one before itsconfighas run (during the inits, or from an earlier plugin'sconfigwithsetup()ininit.lua). Before this:colorschemefound the file through 'packpath' and sourced it without loading the plugin, so a Vim colors file calling its plugin'sautoload/failed with E117 from aninit, and for a lazy plugin always (gruvbox-material, everforest and sonokai all start with<name>#get_configuration()). Same at 241c9ba and 2.0.4. Bead zpack_nvim-las.Why: lazy.nvim's
Loader.setup()registers aColorSchemePreautocmd before the inits that loads the unloaded plugin holding the scheme (lua/lazy/core/loader.luaM.colorscheme).How:
lazy_trigger/colorscheme.luasets up oneColorSchemePreautocmd with the lazy triggers, before the inits. It statscolors/<name>.{lua,vim}in each eager plugin not loaded yet thatcondlets load, then, if the name isn't on 'runtimepath', in each such lazy one, and loads the first match throughplugin_loader.process_spec. An eager plugin is on 'runtimepath' before itsconfigruns, which lazy.nvim's isn't, so the name being there doesn't stop it. No work at startup beyond the autocmd; about 0.3 ms per:colorschemeof a name off 'runtimepath' with 150 pending plugins.Behaviour change: a lazy colorscheme plugin now loads, with its
configandplugin/, when its scheme is set (from a picker, say). Before, only its colors file ran. A Lua colorscheme's plugin now loads before its colors file, not from the file'srequire. An eager colorscheme plugin set from an earlier eagerconfig(a higher priority, or the same one listed first), as themery.nvim's README spec restores the saved scheme with a user's eager gruvbox or catppuccin listed after it, now loads there with itsconfig, before its turn, as in lazy.nvim (where LazyVim'scolorschemeoption, at priority 10000, relies on it); at 241c9ba its colors file ran first and itssetup(opts)came in its turn, so theoptswere lost at startup (round 20). Withsetup()atVimEnteror fromplugin/its scripts are sourced before anyconfig, so it still gets only its colors file there; tips.md says to give it the higherpriority.docs/tips.md"Smaller changes" and:h zpack-Spec.lazysay so. Not changed: withsetup()inside a non-nestedVimEnterautocmd,ColorSchemePredoesn't fire, so aninit's:colorschemethere still gives E117 (lazy.nvim: E185), and acond = falseplugin's scheme is still sourced through 'packpath' (lazy.nvim: E185).Tests: 7 real-process tests on nightly and v0.12.0: an eager plugin's own
initand a lazy plugin set from anotherinit, each withsetup()ininit.luaand atVimEnter; an eager plugin set from a priority-10000config, whose colors file sees itsconfigran; a lazy plugin set after startup; a name no plugin holds (E185) and a builtin one, with the lazy plugin left pending. Mutation check: without the autocmd, 5 fail; with only lazy plugins considered, the 2 own-inittests fail; with the 'runtimepath' check ahead of the eager plugins again, the priority-10000 one fails; without it, the builtin one fails.A function-form
optsthat throws no longer stops a loadWhat changed: when a plugin's function-form
optsthrows, the error is reported once ("Failed to run config for x") and the plugin loads without itsconfigorsetup(), as in lazy.nvim. Before this the throw left the plugin pending: arequireof a lazy plugin raised, the key that loaded it did nothing, an install'sbuildwas skipped, and an eager plugin stayed unloaded with no keys mapped. Bead zpack_nvim-dvi.Why: lazy.nvim's
M.configresolvesPlugin.valuesinside itsUtil.try(lua/lazy/core/loader.lua). The first change made the old behaviour reachable from aninit: an eager plugin aninitrequired, whoseoptsthrew, was loaded again by the startup loop, so itsoptsran and were reported twice, itsplugin/was sourced twice whensetup()ran late (round 3, R3-2), and withsetup()ininit.luait stayed behind every batched plugin on 'runtimepath' (round 4, R4-C3). The same double run happens at 241c9ba through a lazy dependency.How:
plugin_loader.run_configpcallsresolve_optsand returns after the report, as it already did for a throwingconfig. The startup loop's pcall andpendingreset aroundrun_configgo, since nothing in it throws now (+9/-12).Behaviour change: such a plugin is loaded, so its keys are mapped, what loaded it goes on,
:ZPack loadno longer lists it, andbuildruns.docs/tips.md"Upgrading to 3.0" (the entry that was "A dependency that fails no longer stops a lazy parent") and:h zpack-Spec.optssay so.Tests: three tests pinned the old behaviour and change on purpose: lifecycle's "startup run_config loop continues after a function-form opts throws" now asserts the plugin loads without its
config, reported once; zload'sload!order test and lazy_parity's "a dependency that throws doesn't stop its parent" used a throwingoptsonly to fail a load and now use a throwing:packadd(theload!order now includes the two startup plugins). New: real-process, aninitrequiring an eager plugin whoseoptsthrow, withsetup()ininit.luaand atVimEnter(opts,plugin/,after/pluginonce, therequirereturns the module) and R4-C3's shape (plugin/in priority order); in-process, a lazy plugin'srequire, a dependency, and an install'sbuild. Mutation check: with the oldrun_configand startup loop, the six new tests and the rewritten lifecycle one fail.An eager plugin's
ftdetect/runs when filetype detection is on firstWhat changed: with
filetype on,filetype plugin indent onorsyntax onbeforesetup()ininit.lua, zpack now sources each eager plugin'sftdetect/once the eager plugins are on 'runtimepath', in load order, as lazy.nvim does. Before this it never ran: Neovim sourcesftdetect/only for what's on 'runtimepath' as detection comes on, and the eager plugins got there later. So LazyVim's vimtex spec (lazy = false) opened a.clsasst, nottex, and leftg:tex_flavorunset. The same for a plugin under an eager plugin'sdependencieswithoutlazy = true, and when aninitturned detection on. Same at 241c9ba. Bead zpack_nvim-ckx (4).Why: Vim-migrated configs put
filetype plugin indent onat the top ofinit.lua. lazy.nvim sources each plugin'sftdetect/as it loads (Loader.packadd,Loader.ftdetect).How: (+11/-1 lua) after the
:packadd!batch, wheng:did_load_filetypesis already set,startup.process_allrunsruntime.source_ftdetect_filesfor each eager plugin that was added and that noinitloaded first. One aninitloaded got itsftdetect/at that load, from zpack or from thefiletype ontheinitran; without the check it ran twice.Behaviour change: an eager vim-godot with detection on first now gets its
ftdetect/, and itsset filetype=firesFileTypea second time per.gdfile after Neovim'ssetfiletype, as with a plain start package.docs/tips.md"Smaller changes" says so, and itsplugin/order now promises only the order among eager plugins: a lazy plugin that loads during startup has no set place among them (bead mle, closed won't-fix). The other five ckx items stay as they are (decision zpack_nvim-1ms).Tests: real-process, nightly and v0.12.0, with
filetype plugin indent onbeforesetup(): an eager plugin, and one under an eager plugin'sdependencieswithoutlazy = true, eachftdetect/once andFileTypeonce per file; withfiletype onfirst, aninitdeleting an eager plugin still letssetup()finish; with detection off, aninitrequiring an eager plugin and then turning detection on sources itsftdetect/once; with detection off, and withsetup()atVimEnter, an eager plugin'sftdetect/once (Neovim'sfiletype onor:packaddsources it). Mutation check: without the loop the twofiletype plugin indent ontests fail; without theinit-loaded check theinitone fails; without the failed-add check the deletion one fails; without thedid_load_filetypesor thectx.loadcheck one of the last two fails.Also: an
initthat removes a plugin with aninitof its own no longer abortssetup()inhooks.try_call_hook(same at 241c9ba and 2.0.4; two real-process tests fail without the guard). zpack escapes the path of eachplugin/,after/plugin/andftdetect/file it sources itself, so a#or%in the data path no longer fails with E194/E484 and loses the file (same at 241c9ba and 2.0.4; two real-process tests fail without it). A plugin that arequire, a:colorschemeor a lazy dependency's load brings in after an earlierconfigranfiletype onorsyntax ongets itsftdetect/once when it was on 'runtimepath' already (an eager plugin a lazy one lists, an eager scheme plugin), since Neovim'sfiletype onsourced it then; this branch sourced it again, so aset filetype=there firedFileTypetwice per file (round 21; three real-process tests, all failing without the 'runtimepath' check). Removing a lazy plugin (aninitrunningvim.pack.del,:ZPack delete,:packdel) drops itscmdandkeysproxies: its command is gone (E492; after aninitthis branch reported "Failed to re-fire", at runtime the stale proxy was a no-op), its key unmapped (the first press was swallowed), and a command it shared with another lazy plugin is free for that plugin's own:command, which failed with E174 and skipped itsconfig(bead 7a2, same at 241c9ba; six real-process and four in-process tests, each failing without itsreleaseline).Checks
zpack-lazyvim-compareSEEDS=40 gives no differing line on all-extras, any subset or user, opts included; my config infilemode: 55 plugins, 33 lazy, identical;real2kon my config (nightly and v0.12.0, with and without a file argument): identical. Startup, paired d-cpu vs 3c15a39 with an A/A: realistic and ftd at 150 and 300 plugins within the A/A spread or under 0.8 ms; withfiletype plugin indent onfirst, -0.9 at 150 and +1.3 at 300 (zpack now sources the eager plugins'ftdetect/that used to be lost); 3 to 25 ms ahead of lazy.nvim in every row.zpack-lazyvim-compare, 241c9ba vs this branch, SEEDS=40, with the lazy.nvim oracle: noP,EorWline differs on all-extras, any subset or my config, so nolazy=flips and nosrc=changes. Merged opts differ only in mason.nvim'sensure_installedorder (all-extras and 22 subsets), and match lazy.nvim'sPlugin.valueson every plugin of all-extras, the 40 subsets and my config; at 241c9ba those mason lines were the only ones that didn't. Run over the same spec tables, zpack's merge order equals lazy.nvim's fragment order for every plugin with several specs (41 on all-extras, 769 across the subsets; 241c9ba: 3 and 79 differ, mason.nvim, plenary.nvim, which-key.nvim and others). My config throughfilemode with{ import = 'plugins' }: 55 plugins, 33 lazy, identical. 0bf4c57 vs this branch: no line differs, opts included, on all-extras, the 40 subsets and my config. That harness doesn't runinitor:colorscheme._astrocore_mappings.luaandmason-tool-installer.luaverbatim, with astrocore's own spec, over stub repos in a real startup: astrocore'sconfiggets<Leader>pMand sets the core mappings, and theinitcallson_load. At 241c9ba and with only the first change, both error.real2.py, 5 probes each): status, 'runtimepath',package.loaded, globals, autocmds, commands and messages identical, 241c9ba vs branch. With a dump of every mode's keymaps, with and without a file argument: identical to 241c9ba and 0bf4c57; before the key proxy fix<leader>ghwas snacks.nvim's instead of gitlinker.nvim's proxy. The:colorschemeandoptschanges against their parent: identical apart from the newzpack.lazy_trigger.colorschememodule and itsColorSchemePreautocmd (my config sets its scheme aftersetup(), when its themes have loaded).inits probing 2 missing modules each: +1.0 [+0.7, +1.3] vs 241c9ba (A/A +0.0 [-0.2, +0.2]; +0.2 [-0.2, +0.5] vs 0bf4c57), all of it from the module loader also scanning the 20 eager plugins there, which is how aninitfinds one (about 12 µs per missing module; lazy.nvim scans every unloaded plugin too); -4.2 [-4.6, -3.9] vs lazy.nvim. Without the narrowing back to lazy plugins in the first change, 100 eagerconfigs probing 3 missing modules each cost +10.4 ms [+5.8, +14.1].BEGIN_COMMIT_OVERRIDE
fix(startup): a
requirefrom aninitloads the eager plugin holding the module, as in lazy.nvim, instead of failing with "module not found"fix(merge)!: a plugin's specs merge in import order, as in lazy.nvim, so a later spec under
dependenciesoverrides a top-level one and its functionoptssees the top-level optsfix(startup): a lazy plugin's
cmd,event,ftandkeysare set up before the inits, as in lazy.nvim, so aninitrunning one loads the pluginfix(keys): a plugin drops its key proxies as it loads, as in lazy.nvim, so a plugin that maps a key only when it's free (flash.nvim's
;and,) gets itfix(keys): a key keeps its proxy while a plugin listing it is pending, as in lazy.nvim, so another plugin's
keyson the same lhs don't stop it loadingfix(startup):
:colorschemeloads the pending plugin holding the scheme first, as in lazy.nvim, so a Vim colors file can call its plugin'sautoload/from aninitor for a lazy plugin, and an eager scheme plugin set from an earlier plugin'sconfig(themery.nvim's restore) gets its ownconfigfirstfix(loader): a function-form
optsthat throws is reported once and the plugin still loads without itsconfig, as in lazy.nvim, instead of staying pendingfix(ft): a lazy
ftplugin'sftdetect/is sourced once, as in lazy.nvim, so one that runsset filetype=firesFileTypeonce per filefix(startup): an eager plugin's
ftdetect/runs when filetype detection is on beforesetup(), as in lazy.nvim, so LazyVim's vimtex opens a.clsastexfix(loader): a plugin already on 'runtimepath' that loads after a
configturned detection on gets itsftdetect/once, not twicefix(runtime): zpack escapes the path of each file it sources itself, so a
#or%in the data path no longer fails with E194/E484 and loses the filefix(hooks): removing a plugin drops its command and key proxies, so its command gives E492 and a command it shared with another lazy plugin works for that plugin; an
initremoving a plugin no longer re-fires its event or crashessetup()when the plugin has its owninitEND_COMMIT_OVERRIDE