8.8 KiB
Problem Statement
On a freshly cloned dotfiles checkout (or any machine where ~/.local/share/nvim/lazy/ is empty or stale), lazy.nvim only discovers that plugins are missing when nvim is actually launched. The first interactive launch then silently spends a long time cloning nord.nvim, nvim-treesitter, and render-markdown.nvim and compiling every nvim-treesitter parser listed in ensure_installed, with no obvious progress indication in a normal terminal session — it reads as "nvim isn't starting" rather than "nvim is installing plugins." Nothing in dot proactively drives this sync, even though the exact plugin versions are already pinned and tracked in ~/.config/nvim/lazy-lock.json.
Separately, nvim-treesitter's parser build step has a known race: concurrent parser installs can collide on a relative tree-sitter-<lang>-tmp directory, causing one parser (e.g. bash) to fail to compile. Because the compiled .so never lands in ~/.local/share/nvim/lazy/nvim-treesitter/parser/, that parser gets retried (and can fail again) on every subsequent nvim launch until it eventually succeeds — a silent, recurring cost with no clear signal to the user that anything is wrong.
Solution
Add an nvim task to the dot setup family (introduced by the dot-setup-folders spec as the general home for idempotent, re-runnable machine-setup tasks). dot setup nvim drives a headless nvim session that syncs installed plugins to exactly what lazy-lock.json already pins, and verifies afterward that every pinned plugin actually landed on disk — turning a silent, ambiguous first-launch stall into an explicit, scriptable, pass/fail setup step. Bare dot setup (no task name) runs this alongside folders (and any future tasks).
User Stories
- As the machine owner, I want
dot setup nvimto install/sync every plugin pinned inlazy-lock.jsonbefore I ever opennviminteractively, so that my first real editing session isn't interrupted by an unexplained multi-second-to-multi-minute stall that looks like a hang. - As the machine owner, I want
dot setup nvimto use the already-trackedlazy-lock.jsonas the source of truth (not re-resolve latest versions), so that a fresh machine ends up with the exact plugin commits I've already vetted, not whatever is newest upstream that day. - As the machine owner, I want
dot setup nvimto exit non-zero and say clearly which plugin(s) failed to install, so that a partial/broken sync is an obvious, actionable failure rather than something I only notice later inside nvim. - As the machine owner, I want re-running
dot setup nvimwhen everything is already in sync to be a fast no-op that still exits 0, so that it's safe to include unconditionally indot setup's bare "run everything" mode without slowing down every re-run. - As the machine owner, I want to be able to run
dot setup nvimin isolation (not just as part of baredot setup), so that I can re-sync plugins on their own after e.g. manually editinglazy-lock.jsonor clearing the plugin directory. - As the machine owner, I want
dot setup nvim helpto print usage without touching any plugin state, so that it's consistent with every otherdotsubcommand'shelpbehavior.
Implementation Decisions
- Subcommand family: lives under the
dot setupdispatcher established by thedot-setup-foldersspec — same nested-subcommand convention (help-then-argparse,_dot_setup_nvim_usage), same dual-mode shape (baredot setupruns every task;dot setup nvimruns just this one). This spec does not re-describe the shared dispatcher scaffolding itself; seedot-setup-folders.mdfor that. - Core action: run
nvim --headless "+Lazy! restore" +qa.Lazy! restorechecks out every plugin in the spec to the exact commit recorded inlazy-lock.json(installing it first via clone if missing), so it both fixes "missing plugin" and "plugin present but on the wrong commit" in one call. No separateTSUpdate/TSInstallstep is needed: because none of the current plugins (nord.nvim,nvim-treesitter,render-markdown.nvim) declare a lazy-loading trigger (event/cmd/ft), they load eagerly as part of this same headless session, which drivesnvim-treesitter's ownensure_installedparser-compilation step as a natural side effect — matching what was observed when reproducing the issue. - Failure detection:
nvim's process exit code from--headless ... +qadoes not reliably reflect whetherLazy! restoreitself succeeded (Lazy reports failures via its own UI/messages, not necessarily the process exit status).dot setup nvimmust independently verify success after the headless run completes, by checking that every plugin name declared inlazy-lock.jsonhas a corresponding directory under~/.local/share/nvim/lazy/. Any pinned plugin missing a directory is treated as a failure: print which plugin(s) didn't install and exit non-zero. - Parser-compile failures are out of scope for pass/fail: the
tree-sitter-<lang>-tmpcollision race affectsnvim-treesitter's internal parser build, not the plugin-directory check above (nvim-treesitter's own directory will exist regardless of whether an individual parser compiled).dot setup nvim's success criterion is "all pinned plugins are present," not "all treesitter parsers compiled" — a parser-level compile flake is expected to self-heal on a laternvimlaunch or:TSUpdate, per theFurther Notesin this spec's investigation. Detecting and retrying individual parser build failures is not attempted here. - No package-list file: unlike
dot install, there's nothing to record —lazy-lock.jsonis already the tracked source of truth, sodot setup nvimnever writes to it.
Testing Decisions
- Guiding principle: test
dot setup nvim's own logic (that it invokesnvimcorrectly, that it correctly detects success vs. a missing plugin) through the real CLI entry point, faking only the externalnvimbinary — not real plugin installs, real git clones, or real compilation, which would be slow and network-dependent in tests. - Primary seam: full CLI invocation of
dot setup nvim(and baredot setup), run against a scratch$HOMEper test case — the existing project convention (seedot install's and the planneddot setup folders' tests). No new seam is introduced. - Faking
nvim: aPATH-prepended fakenvimbinary, mirroring the fake-pacman/fake-sudo/fake-xdg-user-dirs-updatetechnique already used/planned intests/dot.fish. The fake logs its invocation args (so a test can assertdot setup nvimcalled it with--headless "+Lazy! restore" +qa) and, driven by an env var or scratch-$HOMEfixture, can simulate "all plugins present" vs. "one plugin missing" by controlling whether it creates the expected directories under the scratch~/.local/share/nvim/lazy/. - Cases to cover: a successful sync (fake
nvimcreates all pinned plugin directories) exits 0; a plugin missing after the fake run exits non-zero and names the missing plugin; re-running against an already-fully-synced scratch$HOMEis still a pass (idempotency) without requiring the fake to do anything different; baredot setupruns thenvimtask alongsidefolders;dot setup nvim helpprints usage and never invokes the fakenvimat all. - Prior art:
tests/dot.fish's scratch-$HOME-plus-fishtapepattern, and specifically the fake-binary-via-PATHtechnique used fordot install(and planned fordot setup folders'sxdg-user-dirs-updatefake).
Out of Scope
- The
dot setupdispatcher scaffolding itself (bare-runs-everything, per-task dispatch,_dot_setup_usage) — already specified indot-setup-folders.md; this spec only adds thenvimtask onto it. - The
foldersand any future (e.g.groups)dot setuptasks — unaffected by this spec beyond now running alongsidenvimin baredot setup. - Fixing the underlying
nvim-treesittertree-sitter-<lang>-tmprace itself (an upstream plugin behavior) —dot setup nvimtolerates it rather than working around it. - Any change to
~/.config/nvim's plugin specs,lazy-lock.jsoncontents, or which plugins/parsers are installed — this spec only adds a way to proactively sync to what's already pinned. - A
~/.github/README.mdcommand-table row — not written here, but required by the project's standard "adding a subcommand" checklist at implementation time.
Further Notes
- This spec grew out of debugging a real "nvim isn't starting" report: the actual cause was an empty
lazy.nvimplugin directory triggering a full, slow reinstall on first launch, compounded by atree-sitter-bash-tmpmkdir collision that made thebashparser fail and re-attempt on every subsequent launch until it happened to succeed.dot setup nvimaddresses the first (silent first-launch stall) directly; the second (parser race) is a pre-existing upstream flake this spec does not attempt to fix.