Skip to content

refactor: switch plenary.log and plenary.async requires to neoplen (PR2 of plenary removal) - #343

Draft
delphinus wants to merge 4 commits into
masterfrom
switch-to-neoplen
Draft

refactor: switch plenary.log and plenary.async requires to neoplen (PR2 of plenary removal)#343
delphinus wants to merge 4 commits into
masterfrom
switch-to-neoplen

Conversation

@delphinus

@delphinus delphinus commented May 21, 2026

Copy link
Copy Markdown
Member

Important

Draft, blocked on nvim-telescope/telescope.nvim#3647.
The neoplen.* modules this PR requires ship only on telescope's feat/neoplen branch. CI on this PR will fail because the test runner clones telescope's master (which does not have those modules). Verified locally against feat/neoplen instead — see "Verification" below.

The neoplen namespace itself is explicitly temporary per clason in the upstream PR body. This branch will need a final rename (mechanical sed) once upstream settles on the final module names.

Summary

PR 2 of the plenary.nvim removal effort. Renames every require "plenary.log" / require "plenary.async" (and lazy_require / inline require("...") variants) in production code and tests to the telescope-vendored equivalent. No call-site rewrites — just the module name on the require line.

Upstream gaps surfaced while migrating

Three gaps vs plenary.* were uncovered. Gaps 1 and 2 have since been fixed on feat/neoplen, so the inline workarounds added earlier in this draft have been removed (commit "drop neoplen.async.uv / neoplen.log compat shims"). Gap 3 is a permanent reimplementation that stays.

1. neoplen.async no longer had the uv submodule — ✅ resolved upstream

Removed in telescope commit b9b203f ("feat(plenary): remove unused async"). frecency calls async.uv.fs_{stat,open,close,read,write,unlink,realpath} from 17 production sites (file_lock, fs, klass, watcher, v2/database) and 6 test sites — the async-aware libuv wrappers are core to how we do non-blocking I/O.

Resolution: restored upstream via nvim-telescope/telescope.nvim#3669 (cherry-picked into feat/neoplen on 2026-05-22). lua/neoplen/async/uv_async.lua now exports exactly those seven fs_* wrappers, and neoplen/async/init.lua resolves neoplen.async.uv through its __index lookups. The plenary.async.uv_async re-attach block in lua/frecency/lazy_require.lua is therefore deleted — this also removes the last production-code reference to plenary in this PR.

2. neoplen.log did not create the outfile parent directory — ✅ resolved upstream

plenary.log did Path:new(outfile):parent():mkdir { parents = true } at log.new() time; neoplen.log had dropped that step when it shed plenary.path. On a fresh XDG_STATE_HOME (e.g. our test sandbox), stdpath("log") does not exist, and the first log line threw ENOENT on io.open(outfile, "a").

Resolution: feat/neoplen's log.new() now vim.fn.mkdir(stdpath("log"), "p")s the log dir itself. The load-time mkdir block in lua/frecency/log.lua is therefore deleted.

3. neoplen.async.tests.add_to_env() is gone — reimplemented in-tree (stays)

Removed in commit 18572e9 ("feat(plenary): remove unused async.tests"). frecency's database_spec and file_lock_spec used it to inject a.describe / a.it / a.before_each / a.after_each / a.pending.

Replacement in this PR: small inline util.add_async_to_env() helper in lua/frecency/tests/util.lua — sets _G.a with five functions wrapping neoplen.async.util.will_block. Same primitive plenary's add_to_env used. Not a workaround, just an inline reimplementation — kept regardless of what upstream decides (async.tests is gone for good).

Changes

  • Rename: plenary.asyncneoplen.async and plenary.logneoplen.log across 14 files (production + tests).
  • lua/frecency/tests/util.lua: add add_async_to_env() helper and export it (replacement for gap 3). Add a -- luacheck: globals describe it ... pragma because util.lua is not a *_spec.lua file and so doesn't auto-pick up busted globals.
  • lua/frecency/tests/{database,file_lock}_spec.lua: replace async.tests.add_to_env() calls with util.add_async_to_env().

The gap 1 / gap 2 workaround blocks that earlier commits added to lazy_require.lua and log.lua have been removed now that feat/neoplen covers both; lazy_require.lua is back to identical with master and log.lua's only diff vs master is the one-line require "neoplen.log".

FrecencyPlenaryAsync* and FrecencyPlenaryLog type aliases are intentionally not renamed — keeping them minimises diff noise until the upstream name finalises; they'll be renamed in the same sed that flips neoplen to its final name.

Startup load-order change (behavior change in this PR)

neoplen.* lives inside telescope.nvim, so a lazy-loading plugin manager loads telescope.nvim when frecency requires neoplen.async. frecency's setup() does async I/O during startup — the bootstrap DB warm-up and the BufWinEnter/BufWritePost register autocmd for command-line-opened files. Pre-neoplen this was cheap, telescope-free work (frecency + plenary only), so doing it during startup to warm the DB before the first :Telescope frecency made sense. Post-neoplen it unavoidably loads telescope, so there is no longer any reason to run it before VimEnter: the async DB warm-up still finishes well before the first picker, while running it mid-startup would drag telescope's load onto Neovim's startup critical path.

Fix:

  • wait.lua: require neoplen.async via lazy_require like every other module, so merely requiring the DB modules no longer loads telescope.
  • init.lua: add async_call_or_defer() — runs immediately once vim_did_enter == 1, else defers to a one-shot VimEnter autocmd. Used for the register autocmd; the bootstrap block is likewise deferred to VimEnter. So during startup frecency touches telescope only at VimEnter (off the startup path, still before the first picker).
  • Docs: this is a 2.0 behavior change — eager frecency loads telescope at VimEnter, so telescope can no longer stay fully lazy while frecency eager-loads to register startup buffers (returns once vim.async lets frecency drop the neoplen.async dependency). Documented in README "Upgrading to 2.0.0" and the :help setup() / bootstrap sections.

Verification

Run against a feat/neoplen clone (with the #3669 cherry-pick), ~/.cache/telescope-frecency/info.json pointing nvim-telescope/telescope.nvim at it:

Full suite passes against feat/neoplen: sorter 3 / database 1 / file_lock 11 / frecency_validate_database 4 / frecency 14 → 33 passed, 0 failed (on a current Neovim nightly; an earlier nightly had an unrelated empty-picker-result issue in the register flow that also reproduced on plain master, and is gone now).

  • stylua + luacheck clean on the changed .lua files.

Reproduction recipe:

git clone --branch feat/neoplen --depth 1 https://github.com/nvim-telescope/telescope.nvim /tmp/telescope-neoplen
# Edit ~/.cache/telescope-frecency/info.json so nvim-telescope/telescope.nvim.path points at /tmp/telescope-neoplen
bin/run-tests

CI on this PR is expected to fail at the require-resolution step until telescope#3647 lands.

Follow-up

  • Rebase after telescope#3647 merges: mechanical sed of neoplen → final name (including the FrecencyPlenary* type aliases).
  • PR 4: test framework migration (path/job in tests, busted → plentest.nvim). Now genuinely possible because production no longer needs plenary at all (only tests/util.lua still does, via plenary.path + plenary.job).

🤖 Generated with Claude Code

…R2 of plenary removal, draft)

Renames every `require "plenary.log"` / `require "plenary.async"` (including
`lazy_require` and inline `require("...")` forms) to the telescope-vendored
neoplen equivalent. The neoplen modules ship in nvim-telescope/telescope.nvim
on the `feat/neoplen` branch (PR nvim-telescope/telescope.nvim#3647) and will
land in master once that PR merges; the `neoplen` namespace itself is
explicitly temporary per clason.

Two upstream gaps were uncovered while migrating and are worked around inline
so the test suite stays green when run against feat/neoplen telescope. Both
are flagged for clason to address before this PR can leave draft:

1. neoplen.async dropped its `uv` submodule (telescope commit b9b203f,
   "feat(plenary): remove unused async"). Re-attach plenary's `uv_async` to
   the cached neoplen.async table from `lua/frecency/lazy_require.lua`.

2. neoplen.log dropped the parent-directory creation step that plenary.log
   used to do (Path:mkdir { parents = true } at log.new time), so the first
   log line throws ENOENT on a fresh XDG_STATE_HOME. Create `stdpath("log")`
   once at `lua/frecency/log.lua` load time.

`async.tests.add_to_env()` is also gone from neoplen, so the two specs that
relied on it (`database_spec`, `file_lock_spec`) now call a tiny
`util.add_async_to_env()` helper that injects `a.describe / a.it /
a.before_each / a.after_each / a.pending` using `neoplen.async.util.will_block`
(the same primitive plenary's add_to_env used).

CI will fail until telescope#3647 merges — the cloned telescope on `master`
does not provide `neoplen.*`. Verified locally with feat/neoplen pinned in
~/.cache/telescope-frecency/info.json: 33/33 tests pass on macOS / Apple
Silicon / Neovim stable.

See #340 for the overall plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
clason pushed a commit to nvim-telescope/telescope.nvim that referenced this pull request May 22, 2026
Per #3647 (comment): bring back `neoplen.async.uv` with only the wrappers
extensions actually depend on.

Surveyed against telescope-frecency (draft PR
nvim-telescope/telescope-frecency.nvim#343), which uses these 7:
fs_stat, fs_open, fs_close, fs_read, fs_write, fs_unlink, fs_realpath.

`init.lua` gets a `uv = "neoplen.async.uv_async"` entry so call sites that
did `require("neoplen.async").uv.fs_*` keep working unchanged.

Adding more wrappers later is a single `add(name, argc)` call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
clason pushed a commit to nvim-telescope/telescope.nvim that referenced this pull request May 23, 2026
Per #3647 (comment): bring back `neoplen.async.uv` with only the wrappers
extensions actually depend on.

Surveyed against telescope-frecency (draft PR
nvim-telescope/telescope-frecency.nvim#343), which uses these 7:
fs_stat, fs_open, fs_close, fs_read, fs_write, fs_unlink, fs_realpath.

`init.lua` gets a `uv = "neoplen.async.uv_async"` entry so call sites that
did `require("neoplen.async").uv.fs_*` keep working unchanged.

Adding more wrappers later is a single `add(name, argc)` call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread lua/frecency/lazy_require.lua Outdated
@@ -1,3 +1,16 @@
-- Compat: neoplen.async dropped its `uv` submodule in telescope.nvim commit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's back now :) So this can be removed.

Comment thread lua/frecency/log.lua Outdated
if vim.fn.isdirectory(log_dir) == 0 then
vim.fn.mkdir(log_dir, "p")
end
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also moved that back in neoplen.log

feat/neoplen now ships the bits these draft-only workarounds stood in for,
so they can go:

- gap 1: async.uv was restored on feat/neoplen (nvim-telescope/telescope.nvim#3669,
  cherry-picked 2026-05-22), so neoplen.async.uv resolves via the module's
  __index lookups. Drop the plenary.async.uv_async re-attach block in
  lazy_require.lua (this also removes the last production-code reference to
  plenary in this PR).
- gap 2: neoplen.log's log.new() now mkdir's stdpath("log"), so drop the
  load-time mkdir block in log.lua.

gap 3 (tests/util.lua add_async_to_env) stays: it replaces the permanently
removed async.tests submodule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clason

clason commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Hmm, that's interesting -- neoplen is an independent module so requiring it should not pull in anything from telescope. (Of course, lazy loading plugin managers may mess with that...)

Startup cost improvements in Telescope are very much needed but probably require a larger refactor than I feel comfortable with. Definitely welcome follow-up work once the neoplen branch is merged!

(P.S.: Current thinking is to rename neoplen to shared before the merge.)

delphinus and others added 2 commits June 10, 2026 20:34
Before the neoplen switch, frecency's startup DB warm-up (the `bootstrap`
option) and launch-buffer registration were cheap, telescope-free work
(frecency + plenary only), so running them during startup -- to warm the DB
before the first `:Telescope frecency` -- was worthwhile.

Now that the async I/O goes through `neoplen.async` (vendored inside
telescope.nvim), any such call makes a lazy-loading plugin manager load
telescope.nvim. There is no longer any reason to do this before VimEnter: the
async DB warm-up still finishes well before the first picker either way, while
running it mid-startup would drag telescope's load onto Neovim's startup
critical path. VimEnter is the natural spot -- off the startup path, still
before the first picker.

- wait.lua: require neoplen.async via lazy_require like every other module, so
  merely requiring the DB modules no longer loads telescope.
- init.lua: add async_call_or_defer() and use it for the register autocmd;
  defer the bootstrap DB init to a VimEnter autocmd. Both run immediately once
  vim_did_enter == 1, so only the startup phase is deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Since async I/O now lives in telescope.nvim's vendored neoplen module, the
startup DB warm-up can no longer be a minimal frecency + plenary load -- it
pulls in telescope.nvim, so it is deferred to VimEnter (off the startup path).
Loading frecency non-lazily therefore also loads telescope at VimEnter; keeping
telescope fully lazy while eager-loading frecency is no longer possible until
vim.async lands.

- README: bullet in "Upgrading to 2.0.0".
- :help: correct the setup() "load telescope lazily (recommended way)" wording,
  the inline cmd comment, and the bootstrap option note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@delphinus
delphinus force-pushed the switch-to-neoplen branch from c30a812 to e8d9232 Compare June 10, 2026 11:34
@delphinus

Copy link
Copy Markdown
Member Author

Right — at the Lua level neoplen is independent, but with a lazy-loading plugin manager require "neoplen.async" loads telescope.nvim (and runs its config/extensions), which is exactly the caveat you noted.

Why frecency does this at startup: it warms its DB asynchronously so the first :Telescope frecency is fast. That used to be cheap, telescope-free work (frecency + plenary only) — the whole point was to load just frecency at startup and keep telescope.nvim itself unloaded until it's actually used. With the DB I/O now going through neoplen.async, that's no longer possible.

So on the frecency side I no longer run that work before VimEnter: there's no benefit to doing it earlier anymore (the async warm-up still finishes before the first picker), while doing it mid-startup would only drag telescope's load onto Neovim's startup critical path. VimEnter keeps it off the startup path while still warming the DB in time.

(My earlier comment/PR wording was unclear about the actual rationale, so I've reworded the code comments and docs.)

It's a breaking change — you can no longer keep telescope fully lazy while eager-loading frecency — but it affects a very niche setup (probably just me), so it shouldn't block the branch. Once vim.async lands and frecency can drop the neoplen.async dependency, the telescope-free startup can come back.

And 👍 on neoplenshared; I'll do the mechanical rename on this branch once it's settled.

@clason

clason commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Yeah, I would counsel against hyper-optimizing "lazy" (out-of-the box deferred as opposed to on-demand) loading -- "prewarming" may not even be desirable if you don't use frecency at all in a session!

In general, a plugin should strive to do as little "up front" as possible and avoid eagerly pulling in "expensive" submodules if they're only needed in a few functions. The rest is best left to users to set up keybinds and autocommands to only load the plugin when it's actually needed. (This is how I do it with telescope including frecency, which works just fine -- for me.)

(Of course, you know what you are doing, so I don't want to lecture you.)

The story would change significantly if Neovim ever becomes truly multithreaded, in which case background threads are "free real estate" for precomputation and warm-up.

clason pushed a commit to nvim-telescope/telescope.nvim that referenced this pull request Jun 25, 2026
Per #3647 (comment): bring back `neoplen.async.uv` with only the wrappers
extensions actually depend on.

Surveyed against telescope-frecency (draft PR
nvim-telescope/telescope-frecency.nvim#343), which uses these 7:
fs_stat, fs_open, fs_close, fs_read, fs_write, fs_unlink, fs_realpath.

`init.lua` gets a `uv = "neoplen.async.uv_async"` entry so call sites that
did `require("neoplen.async").uv.fs_*` keep working unchanged.

Adding more wrappers later is a single `add(name, argc)` call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants