From 7a21d372ffc7f63824df3ed820869c85a1036d6e Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Sun, 12 Apr 2026 18:01:02 -0600 Subject: [PATCH] fix: deferred dep loading + idempotent setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related bugs surfaced when the user tried adding `setup({...})` to their init.lua alongside our auto-setup: 1. **`inmemory.bps = nil` crash.** When a user's `init.lua` does `vim.pack.add(..., turbo-debug)` and then calls `setup({...})` synchronously right after, the nested `vim.pack.add` calls inside our own `plugin/turbo-debug.lua` (for dap/dapui/persistent- breakpoints) are DEFERRED by nvim's pack module — so the user's follow-up `require('persistent-breakpoints')` throws "module not found". On the previous `initialized = true` guard, the error set the flag but left setup half-complete; any later call short- circuited and `inmemory.bps` stayed `nil`, exploding the first breakpoint toggle (`api.lua:15: attempt to index field 'bps'`). 2. **Colorscheme override ignored.** Same root cause: user's `setup({ colorscheme = "nightfox" })` errored before reaching `config.merge`, so `config.opts.colorscheme` stayed `nil` and `M.enter` never switched themes. Three changes that together fix both: - **`init.lua` `setup()` self-defers** when `require('persistent- breakpoints')` fails, `vim.schedule`s itself once deps can load. - **`plugin/turbo-debug.lua` auto-setup is now `vim.schedule`d** and skips entirely if the user's explicit `setup({...})` already ran (tracked via `M._setup_called`) — no more stomping user opts with a second `config.merge(nil)`. - **`breakpoints.setup` is now fully idempotent**: dropped the `initialized` flag, since every call inside it (`persistent-breakpoints.setup`, `sign_define`, `nvim_set_hl`, `vim.keymap.set`) is safe to re-run. --- lua/turbo-debug/breakpoints.lua | 24 +++++++++++++++++++----- lua/turbo-debug/init.lua | 15 +++++++++++++++ plugin/turbo-debug.lua | 17 +++++++++++++++-- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/lua/turbo-debug/breakpoints.lua b/lua/turbo-debug/breakpoints.lua index e114812..17cc7fc 100644 --- a/lua/turbo-debug/breakpoints.lua +++ b/lua/turbo-debug/breakpoints.lua @@ -2,12 +2,26 @@ local config = require("turbo-debug.config") local M = {} -local initialized = false - +-- This function is idempotent — every operation inside is safe to re-run. +-- That property matters because turbo-debug.setup() can get called twice in +-- practice: +-- (1) Deferred auto-setup from plugin/turbo-debug.lua, which runs after +-- vim.pack.add has finished registering all deps in runtimepath. The +-- defer is necessary — calling setup synchronously during +-- vim.pack.add would try to `require('persistent-breakpoints.api')` +-- before the plugin's lua/ directory is on the runtimepath and +-- crash with "module not found". +-- (2) User's explicit setup({...}) in their init.lua with config +-- overrides. It runs immediately (before the deferred auto-setup). +-- +-- Previously a single `initialized` flag guarded this function so it only +-- ran once. That guard was fragile: if the first call errored partway +-- (case above), the flag was set but the setup was incomplete, and the +-- second call skipped the retry. We now let every call do its full work; +-- persistent-breakpoints.setup re-reads its bps file, sign_define overrides +-- a previous definition, nvim_set_hl overrides a previous link, and +-- vim.keymap.set overrides a previous binding. function M.setup() - if initialized then return end - initialized = true - require("persistent-breakpoints").setup({ load_breakpoints_event = { "BufReadPost" }, }) diff --git a/lua/turbo-debug/init.lua b/lua/turbo-debug/init.lua index bb7867b..a3752cc 100644 --- a/lua/turbo-debug/init.lua +++ b/lua/turbo-debug/init.lua @@ -4,6 +4,21 @@ local mode = require("turbo-debug.mode") local M = {} function M.setup(opts) + -- Self-defer if dependencies aren't yet on the runtimepath. This happens + -- when the user calls `setup({...})` from their init.lua right after + -- `vim.pack.add(..., turbo-debug)`: our plugin/turbo-debug.lua fires + -- nested `vim.pack.add` calls for dap/dapui/persistent-breakpoints, but + -- those nested adds only register the paths on the NEXT event loop tick + -- — so a synchronous follow-up `require('persistent-breakpoints')` + -- crashes with "module not found". `vim.schedule` lets the pack module + -- finalize before we retry. `_setup_called` still gets set on the retry, + -- so plugin/turbo-debug.lua's own deferred auto-setup correctly skips. + if not pcall(require, "persistent-breakpoints") then + vim.schedule(function() M.setup(opts) end) + return + end + + M._setup_called = true config.merge(opts) require("turbo-debug.breakpoints").setup() require("turbo-debug.adapters").setup() diff --git a/plugin/turbo-debug.lua b/plugin/turbo-debug.lua index 35259f2..ad0228d 100644 --- a/plugin/turbo-debug.lua +++ b/plugin/turbo-debug.lua @@ -16,5 +16,18 @@ pcall(function() require("which-key").add({ { "d", group = "debug" } }) end) --- setup immediately (idempotent — user can call setup() again with overrides) -require("turbo-debug").setup() +-- Deferred auto-setup. Must be deferred (not called synchronously) because +-- the vim.pack.add calls above might not have finished registering the +-- dep modules in runtimepath by the time this script's top-level runs — +-- a synchronous `require('persistent-breakpoints.api')` inside setup would +-- crash with "module not found". vim.schedule guarantees we run AFTER the +-- current event loop tick, at which point pack has finalized the paths. +-- +-- If the user calls setup({...}) themselves (typically from init.lua after +-- vim.pack.add), `M._setup_called` flips true and this deferred call +-- becomes a no-op — otherwise we'd overwrite their user opts with defaults. +vim.schedule(function() + local td = require("turbo-debug") + if td._setup_called then return end + pcall(td.setup) +end)