Rename from tiny-debugger to turbo-debug — inspired by Turbo Pascal, the first IDE with a real visual debugger. This thing isn't tiny, it's turbo charged. CHANGELOG for 0.1.0: Features: - Zero-config debugging — one line install, everything works - Modal keybindings (c/s/d/r/q/x/X/D/W/K/E/R/?), active only during debug mode, cleanly removed on exit - Persistent breakpoints via persistent-breakpoints.nvim - Inline virtual text — variable values in source code - Hover (K), watch (W), REPL eval (E) from the code window - Help splash (3s auto-dismiss) + manual help (?) - Debug mode chrome — winbar indicator, cursor highlight - dapui integration — controls toolbar, labeled panels - which-key support — <leader>d debug group auto-registered - quit_exits_mode option Adapters (12 languages): - Python (debugpy + venv detection) - Rust (codelldb + cargo build + vim.ui.select picker) - Go (delve), C/C++/Swift (codelldb) - JavaScript/TypeScript (js-debug-adapter) - C# (netcoredbg), Java, Kotlin, PHP, Ruby, Lua (osv) Keybindings: - <leader>dd — toggle debug mode - <leader>dx / <leader>dX / <leader>dD — breakpoints (always) - c s d r C x X D W K E q R ? — modal (debug mode only)
44 KiB
turbo-debug.nvim — Product Specification
What Is This?
A Neovim 0.12+ plugin that gives you a fully-featured, beautiful debugging experience out of one line of config. The kind of debugger you remember from Turbo Pascal, Delphi, Visual Studio — where you just hit a key and everything works: breakpoints persist, variables show their values inline, you can hover anything, watch expressions update live, and the whole UI is laid out like someone cared.
Under the hood it wires together nvim-dap, nvim-dap-ui, nvim-dap-virtual-text, and persistent-breakpoints into a single cohesive experience with modal keybindings. The user installs one plugin and gets everything.
vim.pack.add("swaits/turbo-debug.nvim")
That's it. That's the whole setup.
Design Principles
-
Just works. Install it, press
<leader>d, start debugging. No adapter config needed for common languages. No DAP boilerplate. No reading wiki pages to get "hello world" to hit a breakpoint. -
A real debugger. Inline variable values in the source code. Hover any expression to see its value. Persistent breakpoints that survive restarts. Watch expressions you can add from the code window. Scopes, stacks, REPL, console — all laid out cleanly. This is not a toy.
-
Minimal code, maximal experience. We don't reimplement anything. We configure nvim-dap, nvim-dap-ui, nvim-dap-virtual-text, and persistent-breakpoints with opinionated defaults and glue them together with modal keybindings. The smallest amount of code that delivers a professional debugging experience.
-
Principle of least surprise. Keybindings should be guessable.
cis continue.sis step.qis quit. If you can't guess it, the help box is right there. No cleverness that requires documentation. -
Modal, not polluting. Debug keybindings exist only during a debug session. They shadow editing keys you don't need while debugging. They never shadow navigation keys you do need. When the session ends, your editor is exactly as it was.
-
Configurable, not opinionated to a fault. Every keybinding is overridable. The dap-ui layout can be replaced. Adapter configs can be extended. But the defaults should be good enough that most people never touch any of it.
-
Clean, simple code everywhere. No abstraction for abstraction's sake. No metaprogramming tricks. A new contributor should read the source top to bottom in ten minutes and understand everything.
What The User Gets
One plugin install. Zero configuration. All of this:
- Inline virtual text showing variable values right in the source code, updated live as you step. (via nvim-dap-virtual-text)
- Hover inspection — press
Kon any expression to see its value in a floating window. Works on complex expressions, not just single variables. - Persistent breakpoints — breakpoints survive Neovim restarts. They're part of your project, not your session. Visible in the sign column at all times. (via persistent-breakpoints.nvim)
- Watch expressions — press
Won any variable in your code and it gets added to the watches panel. No focus switch, no prompt. - Scopes panel — all local/global variables in the current frame, expandable, updated live.
- Stack traces — navigate the call stack, jump to any frame.
- REPL — evaluate arbitrary expressions in the debug context.
- Console — program stdout/stderr.
- Conditional breakpoints —
Xto set a breakpoint with a condition. - Modal keybindings — single-key controls for all debug actions, active only during debug sessions.
- Help box — shows all keybindings on debug mode entry, re-openable with
?at any time. - Zero-config adapters for the world's top languages.
Dependencies (Managed Automatically)
The user never sees or declares these. turbo-debug calls vim.pack.add for
each one internally:
| Dependency | What it provides |
|---|---|
mfussenegger/nvim-dap |
DAP client — the core debug engine |
rcarriga/nvim-dap-ui |
Scopes, stacks, watches, REPL, console UI |
nvim-neotest/nvim-nio |
Async IO (required by nvim-dap-ui) |
theHamsta/nvim-dap-virtual-text |
Inline variable values in source code |
Weissle/persistent-breakpoints.nvim |
Breakpoints that survive restarts |
Everything loads eagerly at startup. No lazy loading. Startup cost is negligible.
Why These Five and Nothing Else
We evaluated every plugin in the nvim-dap ecosystem. The rule: if we can replicate what a plugin does in <50 lines of our own code, we don't add the dependency. If the plugin does something non-trivial that would take hundreds of lines and deep domain knowledge to replicate, we depend on it.
nvim-dap-virtual-text — KEEP. Uses treesitter to find variable definition sites, handles scope-aware display, changed-value highlighting, comment-style formatting. 400+ lines of treesitter integration we'd have to replicate. Not worth it.
persistent-breakpoints.nvim — KEEP. File-based breakpoint persistence with sign management. Solves a real problem cleanly.
nvim-dap-python, nvim-dap-go, nvim-dap-ruby, nvim-dap-lldb, nvim-dap-kotlin,
nvim-dap-cs, nvim-dap-vscode-js — DO NOT DEPEND. Each is just an adapter
table + config table + maybe 20 lines of environment detection. We steal the
configs and inline them as plain Lua tables in our configs/ directory. No
plugin needed.
mason-nvim-dap — DO NOT DEPEND. Our principle is "we configure, the user installs binaries." We just look for adapters on PATH or in standard mason install paths as a fallback.
dap-buddy — DO NOT DEPEND. Abandoned since 2022. Dead project.
nvim-dap-rr — DO NOT DEPEND. Reverse debugging is niche (Linux-only, C/C++/Rust only). Out of scope.
dap-utils — DO NOT DEPEND. Grab bag of features we either already have (breakpoint persistence) or don't want (telescope integration).
Installation
-- init.lua
vim.pack.add("swaits/turbo-debug.nvim")
One line. One plugin. Everything works.
Configuration (optional)
require("turbo-debug").setup({
-- all fields optional, these are the defaults
help_on_enter = true, -- show help box when entering debug mode
keys = { ... }, -- override any keybinding
adapters = { ... }, -- override or add language adapters
dapui = { ... }, -- override nvim-dap-ui layout/settings
virtual_text = { ... }, -- override nvim-dap-virtual-text settings
})
Debug Mode
<leader>d toggles debug mode. Entering debug mode:
- Opens nvim-dap-ui with the configured layout.
- Enables nvim-dap-virtual-text (inline variable values appear in source).
- Activates modal keybindings (buffer-local, vanish on exit).
- Changes the cursor highlight to indicate you're in debug mode.
- Shows the help box (bottom-right float, auto-closes on first keypress).
Disable with
help_on_enter = false. - If no DAP session is active, waits for
c(continue) to launch one.
Exiting debug mode (any of):
<leader>dagain (manual toggle)q(terminate session — auto-exits debug mode)- DAP session ends naturally (program exits, etc.)
On exit: all modal keybindings removed, nvim-dap-ui closed, virtual text cleared, cursor highlight restored. The editor is exactly as it was before.
Keybindings
Philosophy
During debugging you need to navigate code (read, jump around, search) and you need to interact with the REPL/terminal (insert mode). You do not need to edit code. Therefore we shadow editing operators and leave all motions and insert intact.
Left hand = flow control. Right hand = inspection. ? = help.
Default Map
| Key | Action | Mnemonic / Rationale |
|---|---|---|
c |
Continue | Universal across all debuggers |
s |
Step over | Step |
d |
Step into | Descend into the function |
r |
Step out | Return from the function |
C |
Run to cursor | Continue to cursor — extends c |
x |
Toggle breakpoint | X marks the spot (persistent) |
X |
Conditional breakpoint | Shift variant of x (also persistent) |
W |
Watch expr under cursor | Watch (uses <cexpr> or visual selection) |
K |
Hover / inspect | Matches vim LSP hover convention |
E |
Eval in REPL | Evaluate |
q |
Terminate session | Quit debugging |
R |
Restart session | Rerun |
? |
Help popup | Universal "show me what I can do" |
What's Sacrificed (and Why It's Fine)
| Key | Normal function | Why it's safe to shadow |
|---|---|---|
c |
Change operator | Not editing during debug |
s |
Substitute char | Not editing; flash/leap: modal, so no conflict |
d |
Delete operator | Not editing during debug |
r |
Replace char | Not editing during debug |
x |
Delete char | Not editing during debug |
q |
Record macro | Not recording macros during debug |
C |
Change to EOL | Not editing during debug |
X |
Backspace | Not editing during debug |
R |
Replace mode | Nobody enters replace mode during debug |
W |
WORD forward | Minor loss; w (word forward) still works |
E |
End of WORD | Minor loss; e (end of word) still works |
? |
Reverse search | Minor loss; / still works for forward search |
What's Preserved
All navigation: h j k l w b e f F t T / n N 0 ^ $ G g { } % [ ]
Insert mode access: i a o O (needed for REPL, terminal, editing watches)
Visual, yank, put: v V y p u (useful for copying values, inspecting ranges)
All Ctrl/leader combos: untouched.
Configuration
keys = {
continue = "c",
step_over = "s",
step_into = "d",
step_out = "r",
run_to_cursor = "C",
breakpoint = "x",
cond_breakpoint = "X",
watch = "W",
hover = "K",
eval = "E",
terminate = "q",
restart = "R",
help = "?",
}
Override any key. Set a key to false to disable it.
Persistent Breakpoints
Breakpoints are managed by persistent-breakpoints.nvim, not raw nvim-dap. This means:
- Breakpoints survive across Neovim restarts.
xcallsrequire("persistent-breakpoints.api").toggle_breakpoint().Xcalls the conditional breakpoint equivalent.- Breakpoint signs are visible at all times, not just during debug sessions.
Note: x and X work at all times — they are the few keybindings active
both inside and outside debug mode. Setting breakpoints should never require
entering a special mode first.
The Debugging Experience
Inline Virtual Text
When a debug session is active, variable values appear as virtual text inline in the source code, right next to where they're used. This updates live as you step. No need to hover or check the scopes panel for simple values — they're just there in your code.
Powered by nvim-dap-virtual-text with sensible defaults (commented style so it doesn't look like real code, changed values highlighted, current frame only).
Hover
K on any expression opens a floating window showing its current value.
Works in normal mode (uses word under cursor) and visual mode (uses
selection). Follows the same K convention as LSP hover.
Watch
W grabs <cexpr> under the cursor (or the visual selection) and adds it
to the watches panel in nvim-dap-ui. No focus switch, no prompt, no typing.
The watch updates live as you step through code.
To remove or edit watches, navigate to the watches panel in the sidebar and use the standard nvim-dap-ui element keybindings.
UI Layout
turbo-debug configures nvim-dap-ui with a clean, purposeful layout:
┌──────────────────────────────────┬──────────────────┐
│ │ Scopes (50%) │
│ │ > local │
│ Source Code │ x = 42 │
│ │ name = "hi" │
│ x = 42 -- « virtual text │ │
│ name = "hello" ├──────────────────┤
│ >> result = compute(x) │ Watches (25%) │
│ return result │ compute(x): 84 │
│ ├──────────────────┤
│ │ Stacks (25%) │
│ │ main:12 │
│ │ > compute:7 │
├──────────────────────────────────┴──────────────────┤
│ REPL (50%) │ Console (50%) │
│ > print(x) │ [stdout] Starting... │
│ 42 │ [stdout] Processing... │
└──────────────────────────┴──────────────────────────┘
Layout Rationale
Left sidebar (40 columns):
- Scopes gets 50% — this is what you look at 80% of the time.
- Watches gets 25% — right below scopes, so
Wresults appear in your peripheral vision immediately. - Stacks gets 25% — you glance at it, you don't study it.
Bottom tray (12 lines):
- REPL and Console share the space. REPL is interactive (you type expressions), Console shows program output.
What's excluded from the layout:
- Breakpoints panel — persistent-breakpoints shows signs in the gutter.
- Controls toolbar — our modal keys replace it.
Overridable: Pass opts.dapui to replace the entire layout.
Preconfigured Adapters
The plugin ships working DAP adapter + configuration for the world's top languages. No language-specific plugins are added as dependencies — we inline the configs as plain Lua tables, stealing the good parts from existing plugins and the nvim-dap wiki.
Adapter Table
| Language | Adapter | Config source | Our code |
|---|---|---|---|
| Python | debugpy | nvim-dap-python (mfussenegger) | ~30 lines |
| JavaScript / TS | js-debug | nvim-dap-vscode-js (mxsdev) | ~25 lines |
| Go | delve | nvim-dap-go (leoluz) | ~20 lines |
| C / C++ | codelldb | nvim-dap-lldb (julianolf) + wiki | ~20 lines |
| Rust | codelldb | Same as C/C++, shared config | ~5 lines (alias) |
| C# | netcoredbg | nvim-dap-cs (NicholasMata) + wiki | ~15 lines |
| Java | java-debug | nvim-jdtls patterns + wiki | ~20 lines |
| PHP | php-debug | nvim-dap wiki + community configs | ~15 lines |
| Ruby | rdbg | nvim-dap-ruby (suketa) | ~20 lines |
| Kotlin | kotlin-debug | nvim-dap-kotlin (Mgenuit) | ~15 lines |
| Swift | codelldb | Same as C/C++, shared config | ~5 lines (alias) |
| Lua (Neovim) | osv | one-small-step-for-vimkind + wiki | ~15 lines |
What We Steal From Each Source
nvim-dap-python (~200 LOC plugin → ~30 lines for us):
- Adapter definition (executable type,
python -m debugpy.adapter) - Virtualenv auto-detection: checks
VIRTUAL_ENV,CONDA_PREFIX, then probes for.venv/venv/env/.envrelative to cwd and LSP root - Launch config for current file
- We skip: test runner integration (test_method, test_class) — out of scope
nvim-dap-go (~300 LOC plugin → ~20 lines for us):
- Adapter definition (server type, auto-launches dlv on random port)
- Launch config for current file and package
- We skip: individual test debugging — out of scope
nvim-dap-vscode-js (~400 LOC plugin → ~25 lines for us):
- Adapter definition for
pwa-node(usesjs-debug-adapterbinary) - Launch configs for file, attach, and node process
- Shared across
javascript,typescript,javascriptreact,typescriptreactfiletypes - We skip: Chrome/Edge/extension host adapters — uncommon for most users
nvim-dap-lldb / codelldb wiki (~150 LOC → ~20 lines for us):
- Adapter definition (server type, TCP, codelldb binary)
- Mason path fallback (
~/.local/share/nvim/mason/bin/codelldb) - Launch config with program picker (
vim.fn.input) - Shared across
c,cpp,rust,swiftfiletypes
nvim-dap-cs (~100 LOC → ~15 lines for us):
- Adapter definition (executable type,
netcoredbg --interpreter=vscode) - Launch config for .NET DLL with path picker
- Mason path fallback
nvim-dap-ruby (~100 LOC → ~20 lines for us):
- Adapter definition (server type,
rdbgwith DAP mode) - Launch config for current file and attach config
- Rails server launch config (rdbg +
bin/rails s)
nvim-dap-kotlin (~150 LOC → ~15 lines for us):
- Adapter definition (executable type,
kotlin-debug-adapter) - Launch config for main function
- We skip: test debugging — out of scope
PHP (wiki + community → ~15 lines for us):
- Adapter definition (executable type, node +
phpDebug.js) - Mason path fallback for php-debug-adapter
- Launch config to listen for Xdebug on port 9003
Java (nvim-jdtls patterns → ~20 lines for us):
- Adapter definition (executable type, java-debug-adapter)
- Launch config for current class
- Note: Java debugging is tightly coupled to nvim-jdtls for full functionality. Our config provides the basics; users with complex Java setups will likely use nvim-jdtls directly.
Lua/Neovim (wiki → ~15 lines for us):
- Adapter definition via one-small-step-for-vimkind (osv)
- Launch config for current Neovim instance debugging
- Optional dependency — only activated if osv is installed
Adapter Path Resolution
For every adapter, we check paths in this order:
- User override via
opts.adapters - Binary on
$PATH - Standard mason install path (
~/.local/share/nvim/mason/bin/) - Common system paths (
/usr/bin/,/usr/local/bin/)
Behavior
- On first
c(continue), if nodap.configurationsexist for the current filetype, turbo-debug installs its built-in config automatically. - If the user has already configured adapters for a filetype via nvim-dap directly, turbo-debug does not override them.
- Adapter binary installation is the user's responsibility (mason, system package manager, or manual). turbo-debug configures, it doesn't install.
Configuration
require("turbo-debug").setup({
adapters = {
python = { ... }, -- override built-in
zig = { ... }, -- add new language
},
builtin_adapters = false, -- disable all built-ins
})
Help Box
The help box is a small floating window (bottom-right corner) that lists all
active keybindings. It appears automatically when entering debug mode
(configurable via help_on_enter). It auto-closes on the first keypress so
it never blocks workflow.
? re-opens it at any time during debug mode.
Content is generated dynamically from the active keymap table, so it always reflects the user's configuration.
Mode Lifecycle
NORMAL MODE
│
├── x / X work here too (persistent breakpoints, always available)
│
├── <leader>d ──► DEBUG MODE
│ │
│ ├── Modal keybindings active
│ ├── nvim-dap-ui open (scopes, watches, stacks, repl, console)
│ ├── Virtual text enabled (inline variable values)
│ ├── Cursor highlight changed
│ ├── Help box shown (if help_on_enter)
│ │
│ ├── c ──► Launches/continues DAP session
│ ├── s/d/r ──► Step over/into/out
│ ├── K ──► Hover inspect value under cursor
│ ├── W ──► Add watch for expr under cursor
│ ├── E ──► Eval expression in REPL
│ ├── q ──► Terminates session ──► exits debug mode
│ ├── <leader>d ──► exits debug mode (session stays if active)
│ │
│ └── Session ends naturally ──► exits debug mode
│
◄── Editor restored (keymaps, UI, virtual text, cursor)
Implementation Notes
Scope
Clean, readable Lua. A plugin/ entry point, a core module, and a configs/
directory for per-language adapter definitions (plain tables, no logic). Every
file should be short enough to read in one sitting.
File Structure
This is a canonical Neovim plugin with proper help docs, license, and everything a user or contributor expects.
turbo-debug.nvim/
├── LICENSE -- MIT
├── README.md -- install, quick start, screenshots
├── CONTRIBUTING.md -- how to add adapters, run tests
├── doc/
│ └── turbo-debug.txt -- :help turbo-debug (vimdoc format)
├── plugin/
│ └── turbo-debug.lua -- vim.pack.add deps, global x/X maps, setup
├── lua/
│ └── turbo-debug/
│ ├── init.lua -- public API (setup, toggle, active)
│ ├── config.lua -- default opts, deep merge with user opts
│ ├── mode.lua -- enter/exit debug mode, keybinding mgmt
│ ├── help.lua -- help box float
│ ├── ui.lua -- dap-ui layout config and wiring
│ ├── virtual_text.lua -- nvim-dap-virtual-text config and wiring
│ ├── breakpoints.lua -- persistent-breakpoints config and wiring
│ └── adapters/
│ ├── init.lua -- adapter loader, path resolution, register
│ ├── python.lua -- adapter + config + venv detection
│ ├── javascript.lua -- js/ts/jsx/tsx adapter + configs
│ ├── go.lua -- delve adapter + configs
│ ├── c.lua -- codelldb adapter (c/cpp/rust/swift)
│ ├── cs.lua -- netcoredbg adapter
│ ├── java.lua -- java-debug adapter
│ ├── php.lua -- php-debug adapter
│ ├── ruby.lua -- rdbg adapter
│ ├── kotlin.lua -- kotlin-debug adapter
│ └── lua.lua -- osv adapter (optional)
└── tests/
├── minimal_init.lua -- minimal Neovim config for test harness
├── test_mode.lua -- debug mode enter/exit tests
├── test_keys.lua -- keybinding activation/restoration tests
├── test_help.lua -- help box open/close/content tests
├── test_config.lua -- opts merging, key override, key disable
├── test_adapters.lua -- adapter path resolution, registration
└── test_breakpoints.lua -- persistent breakpoint integration tests
Key Mechanisms
Dependency management: plugin/turbo-debug.lua calls vim.pack.add
for each of the five dependencies. The user never sees or manages them.
Plugin wiring: turbo-debug calls setup() on nvim-dap-ui,
nvim-dap-virtual-text, and persistent-breakpoints with its opinionated
defaults, deep-merged with any user overrides from opts.
Modal keybindings: Use vim.keymap.set with buffer = 0 on enter,
delete them on exit. Stash any conflicting user mappings and restore them.
Always-on breakpoints: x and X are mapped globally (not just in
debug mode) and delegate to persistent-breakpoints.nvim API.
Watch from code: W grabs vim.fn.expand("<cexpr>") in normal mode or
the visual selection, then programmatically inserts it into the watches
element buffer.
Auto-exit: Hook into dap.listeners.after.event_terminated and
dap.listeners.after.event_exited to leave debug mode when session ends.
Cursor highlight: Set dCursor highlight group on enter, clear on exit.
Help box: vim.api.nvim_open_win with scratch buffer, bottom-right.
Autocmd to close on any keypress. Content built from the keys table.
What We Don't Do
- No custom UI widgets. nvim-dap-ui handles that.
- No statusline integration beyond exposing
require("turbo-debug").active(). - No telescope/fzf integration. Out of scope.
- No reverse debugging built-in. Users can remap keys.
- No test runner integration (debug nearest test). Out of scope.
- No project-specific launch.json parsing. nvim-dap supports this natively.
- No lazy loading. Everything loads. Startup cost is negligible.
- No adapter binary installation. We configure, the user installs.
- No language-specific plugin dependencies. We inline all adapter configs.
Success Criteria
A user who has never configured DAP should be able to:
- Add one line to their init.lua:
vim.pack.add("swaits/turbo-debug.nvim") - Open a file in any of the world's top 10 languages.
- Press
xon a line to set a breakpoint (visible immediately in gutter). - Press
<leader>dto enter debug mode. See the help box. See the UI. - Press
cto start debugging. See the program stop at the breakpoint. - See variable values inline in the source code as virtual text.
- Press
Kto hover a variable and see its full value. - Press
Wto add a watch expression. - Press
sto step, see everything update live. - Press
qto quit. Everything disappears. Editor is clean. - Close Neovim, reopen, see the breakpoint still there.
Language coverage target: Works out of the box (assuming the adapter binary is installed) for: Python, JavaScript/TypeScript, Java/Kotlin, C/C++, C#, Go, Rust, PHP, Ruby, Swift. And Lua for the Neovim plugin developers.
The experience target: This should feel like Visual Studio or IntelliJ, not like a terminal debugger. Inline values, hover tooltips, persistent state, clean layout — the full experience, in your terminal, from one line of config.
Total time from install to first breakpoint: under 2 minutes. Total lines of user config: 1. Total plugin dependencies the user manages: 0. Total language-specific plugin dependencies: 0.
Development Task Breakdown
Each task below is a single jj commit. Tasks are ordered so that each
builds on the last and the plugin is functional (if incomplete) at every
commit. Every task includes acceptance criteria and how to test it.
All source control uses jj (Jujutsu), never git.
Task 1: Scaffold — canonical plugin structure
What: Create the repo with all directories, empty files, LICENSE (MIT),
empty README.md, CONTRIBUTING.md stub, and doc/turbo-debug.txt with
the vimdoc header and a skeleton table of contents.
Files:
LICENSE
README.md
CONTRIBUTING.md
doc/turbo-debug.txt
plugin/turbo-debug.lua (empty, just a header comment)
lua/turbo-debug/init.lua (returns empty table)
lua/turbo-debug/config.lua (returns default opts table)
tests/minimal_init.lua (minimal nvim init for testing)
Acceptance criteria:
nvim --clean -u tests/minimal_init.luastarts without errors:help turbo-debugopens the help file (even if sparse):lua print(type(require("turbo-debug")))printstablejj logshows exactly one clean commit
Test: Manual smoke test in Neovim.
jj commit: scaffold: canonical plugin structure with docs and license
Task 2: Dependency management via vim.pack
What: plugin/turbo-debug.lua calls vim.pack.add for all five
dependencies. Validates Neovim >= 0.12. Prints a clear error and returns
if version check fails.
Files:
plugin/turbo-debug.lua
Acceptance criteria:
- On Neovim 0.12+: all five dependencies are downloaded and loadable
(
require("dap"),require("dapui"), etc. all succeed) - On Neovim < 0.12: clear error message, no crash
- Dependencies do not appear in the user's plugin manager
Test: nvim --clean -u tests/minimal_init.lua -c 'lua require("dap")'
should not error. Test version gate by temporarily faking vim.version().
jj commit: deps: manage all dependencies via vim.pack
Task 3: Config module — defaults and deep merge
What: lua/turbo-debug/config.lua defines the full default options
table (keys, help_on_enter, dapui layout, virtual_text settings, etc.)
and exports a merge(user_opts) function that deep-merges user overrides.
Files:
lua/turbo-debug/config.lua
tests/test_config.lua
Acceptance criteria:
require("turbo-debug.config").defaultsreturns the full table- Merging
{ keys = { continue = "g" } }overrides only that key - Merging
{ keys = { continue = false } }disables that key - Merging
{}returns defaults unchanged - All other default values survive a partial override
Test: Automated. tests/test_config.lua runs assertions on merge
outputs. Run with nvim --headless -u tests/minimal_init.lua -c 'luafile tests/test_config.lua'.
jj commit: config: default options table with deep merge
Task 4: Debug mode — enter, exit, keybinding lifecycle
What: lua/turbo-debug/mode.lua implements enter/exit debug mode.
On enter: sets buffer-local keymaps (stashing conflicts), sets dCursor
highlight. On exit: removes keymaps (restoring originals), clears highlight.
lua/turbo-debug/init.lua exposes toggle() and active().
plugin/turbo-debug.lua maps <leader>d to toggle().
Files:
lua/turbo-debug/init.lua
lua/turbo-debug/mode.lua
tests/test_mode.lua
tests/test_keys.lua
Acceptance criteria:
<leader>denters debug mode; all configured keys are active<leader>dagain exits; all keys are removed- A pre-existing user mapping for
cis stashed on enter and restored on exit (verified by checkingvim.fn.maparg) require("turbo-debug").active()returnstrue/falsecorrectly- Navigation keys (
h,j,w,b,/,n, etc.) still work in debug mode dCursorhighlight group is set on enter, cleared on exit
Test: Automated. tests/test_mode.lua enters mode, checks keymaps
exist via vim.fn.maparg, exits mode, checks keymaps removed and originals
restored. tests/test_keys.lua sets a custom mapping for c, enters
debug mode, verifies it's overridden, exits, verifies it's restored.
jj commit: mode: enter/exit debug mode with keybinding lifecycle
Task 5: Help box
What: lua/turbo-debug/help.lua implements the floating help window.
Reads from the active keys config to build content dynamically. Opens
anchored to bottom-right. Closes on any keypress via autocmd.
Files:
lua/turbo-debug/help.lua
tests/test_help.lua
Acceptance criteria:
?in debug mode opens a floating window listing all keybindings- The window closes on the next keypress (any key)
- Content reflects the user's config (if
continueis remapped tog, the help box showsgnotc) - If
help_on_enter = true, the box opens automatically on mode enter - If
help_on_enter = false, it does not - The window is non-focusable and non-intrusive (doesn't steal cursor)
Test: Automated. tests/test_help.lua opens help, checks
vim.api.nvim_win_is_valid, feeds a keypress, verifies window closed.
Tests with a remapped key to verify dynamic content.
jj commit: help: floating help box with dynamic keybinding content
Task 6: UI wiring — nvim-dap-ui layout
What: lua/turbo-debug/ui.lua calls dapui.setup() with our
opinionated layout. Wires dap listeners to open/close UI on session
start/end. Integrates with mode enter/exit.
Files:
lua/turbo-debug/ui.lua
Acceptance criteria:
- Entering debug mode opens nvim-dap-ui with the configured layout (scopes 50%, watches 25%, stacks 25% left; repl + console bottom)
- Controls toolbar is disabled
- Exiting debug mode closes nvim-dap-ui
- DAP session ending naturally closes UI and exits debug mode
- User can override layout via
opts.dapui
Test: Manual. Open a Python file with debugpy installed, enter debug mode, verify layout matches the spec diagram. Exit, verify clean. Override layout in opts, verify override takes effect.
jj commit: ui: opinionated nvim-dap-ui layout with auto open/close
Task 7: Virtual text wiring
What: lua/turbo-debug/virtual_text.lua calls
require("nvim-dap-virtual-text").setup() with our defaults (commented
style, highlight changed variables, current frame only). Integrates with
mode enter/exit (enable on enter, disable on exit).
Files:
lua/turbo-debug/virtual_text.lua
Acceptance criteria:
- During a debug session, variable values appear as virtual text inline
- Virtual text uses comment syntax for the current filetype
- Changed values are highlighted differently
- Virtual text clears when exiting debug mode
- User can override settings via
opts.virtual_text
Test: Manual. Debug a Python script, step through, verify virtual text appears next to variable definitions. Exit debug mode, verify text clears.
jj commit: virtual-text: inline variable values with sensible defaults
Task 8: Persistent breakpoints wiring
What: lua/turbo-debug/breakpoints.lua calls
require("persistent-breakpoints").setup() and maps x/X globally
(outside debug mode) to toggle/conditional breakpoint.
Files:
lua/turbo-debug/breakpoints.lua
tests/test_breakpoints.lua
Acceptance criteria:
xtoggles a breakpoint on the current line (sign appears in gutter)Xprompts for a condition and sets a conditional breakpoint- Both work outside of debug mode (no
<leader>dneeded first) - Breakpoints persist: close Neovim, reopen, signs are still there
- Breakpoints are passed correctly to nvim-dap when a session starts
Test: Automated + manual. tests/test_breakpoints.lua verifies that
calling the breakpoint API places a sign. Manual test: set breakpoint,
quit Neovim, reopen, verify sign persists. Start debug session, verify
program stops at the breakpoint.
jj commit: breakpoints: persistent breakpoints with global x/X mapping
Task 9: Watch from code window
What: Wire the W keybinding in debug mode to grab <cexpr> under
cursor (or visual selection) and insert it into the nvim-dap-ui watches
element programmatically.
Files:
lua/turbo-debug/mode.lua (add watch action)
Acceptance criteria:
Win normal mode in debug mode adds the expression under cursor to the watches panelWin visual mode adds the selected text to the watches panel- The watch value updates live as you step
- No focus switch occurs — cursor stays in the code window
Test: Manual. Debug a Python script, place cursor on a variable, press
W, verify it appears in the watches panel. Select a complex expression
in visual mode, press W, verify it appears. Step, verify value updates.
jj commit: watch: add expression under cursor to watches with W
Task 10: Hover and eval
What: Wire K to require("dapui").eval() (hover float with value
under cursor or visual selection). Wire E to
require("dapui").float_element("repl") (open REPL float for expression
evaluation).
Files:
lua/turbo-debug/mode.lua (add hover + eval actions)
Acceptance criteria:
Kin normal mode shows a floating window with the value of the word under cursorKin visual mode shows the value of the selected expressionEopens the REPL in a floating window for arbitrary expression input- Both work during an active debug session
Test: Manual. Debug a Python script, hover a variable with K, verify
float shows value. Select x + y in visual mode, K, verify evaluated
result. Press E, type an expression in the REPL, verify output.
jj commit: inspect: hover with K, eval with E
Task 11: Adapter configs — Python and Go
What: First two adapter configs. Python includes venv auto-detection.
Go includes delve auto-launch. Adapter loader in
lua/turbo-debug/adapters/init.lua handles registration and path
resolution.
Files:
lua/turbo-debug/adapters/init.lua
lua/turbo-debug/adapters/python.lua
lua/turbo-debug/adapters/go.lua
tests/test_adapters.lua
Acceptance criteria:
- Opening a
.pyfile and pressing<leader>dthenclaunches debugpy - If a virtualenv is active, debugpy uses it
- Opening a
.gofile and pressing<leader>dthenclaunches delve - If the user already has
dap.configurations.pythonset, we don't override it - Path resolution checks PATH, then mason, then common system paths
Test: tests/test_adapters.lua verifies adapter tables are well-formed
and path resolution logic returns expected results for mock scenarios.
Manual: debug a real Python and Go program end-to-end.
jj commit: adapters: python (with venv detection) and go (delve)
Task 12: Adapter configs — C/C++/Rust/Swift (codelldb)
What: Single adapter config shared across C, C++, Rust, and Swift.
Uses codelldb. Program picker via vim.fn.input.
Files:
lua/turbo-debug/adapters/c.lua
Acceptance criteria:
- Opening a
.c,.cpp,.rs, or.swiftfile and debugging works - Prompts for executable path on launch
- Finds codelldb via PATH or mason
Test: Manual. Compile a C program with -g, debug it, verify
breakpoints and stepping work.
jj commit: adapters: c/cpp/rust/swift via codelldb
Task 13: Adapter configs — JavaScript/TypeScript
What: JS/TS adapter using js-debug-adapter (pwa-node). Shared across
javascript, typescript, javascriptreact, typescriptreact filetypes.
Files:
lua/turbo-debug/adapters/javascript.lua
Acceptance criteria:
- Opening a
.jsor.tsfile and debugging launches pwa-node - Launch file and attach configs both present
- Finds js-debug-adapter via PATH or mason
Test: Manual. Debug a Node.js script end-to-end.
jj commit: adapters: javascript/typescript via js-debug-adapter
Task 14: Adapter configs — C#, Java, Kotlin
What: C# via netcoredbg. Java via java-debug-adapter. Kotlin via kotlin-debug-adapter.
Files:
lua/turbo-debug/adapters/cs.lua
lua/turbo-debug/adapters/java.lua
lua/turbo-debug/adapters/kotlin.lua
Acceptance criteria:
- Each language launches its respective adapter when debugging
- Path resolution works for each
- Java config includes note in docs about nvim-jdtls for full support
Test: Manual per language where toolchain is available. Adapter table
structure verified in tests/test_adapters.lua.
jj commit: adapters: csharp, java, kotlin
Task 15: Adapter configs — PHP, Ruby, Lua
What: PHP via php-debug (Xdebug listener on port 9003). Ruby via rdbg. Lua via osv (optional, only if one-small-step-for-vimkind is available).
Files:
lua/turbo-debug/adapters/php.lua
lua/turbo-debug/adapters/ruby.lua
lua/turbo-debug/adapters/lua.lua
Acceptance criteria:
- PHP: listens for Xdebug on port 9003
- Ruby: launches rdbg in DAP mode
- Lua: only registers if osv is available, no error if it's not
- All path resolution works
Test: Manual per language. Lua adapter tested by checking graceful skip when osv is not installed.
jj commit: adapters: php, ruby, lua
Task 16: Vimdoc — complete help file
What: Write the full doc/turbo-debug.txt in vimdoc format. Covers
installation, configuration, all keybindings, all options, adapter
customization, and troubleshooting. Generate helptags.
Files:
doc/turbo-debug.txt
doc/tags (generated)
Acceptance criteria:
:help turbo-debugopens the help file:help turbo-debug.setupjumps to the setup section:help turbo-debug.keysjumps to the keybindings section:help turbo-debug.adaptersjumps to the adapters section- Every public function and option is documented
- All tag references resolve correctly
Test: Open Neovim, run :helptags doc/, then :help turbo-debug
and verify all sections and tags work.
jj commit: docs: complete vimdoc help file with all tags
Task 17: README — install, screenshots, quick start
What: Write README.md with: one-line install, feature list, screenshot
or ASCII diagram of the UI, keybinding table, minimal config, full config
example, supported languages table, FAQ, and links to :help.
Files:
README.md
CONTRIBUTING.md (finalize)
Acceptance criteria:
- README renders correctly on GitHub
- One-line install is the first code block
- Keybinding table matches the spec
- Supported languages table matches the spec
- CONTRIBUTING.md explains how to add a new adapter config, how to run tests, and that we use jj not git
Test: Visual review on GitHub after push.
jj commit: docs: readme, contributing guide
Task 18: End-to-end acceptance test
What: Run through the full success criteria checklist from the spec for at least Python and one compiled language (C or Go). Fix any issues discovered.
Acceptance criteria:
- All 11 success criteria steps pass for Python
- All 11 success criteria steps pass for C or Go
- No errors in
:messagesafter a full debug session lifecycle - Clean startup: no errors on
nvim --cleanwith only turbo-debug
Test: Manual walkthrough of the full success criteria. Document results.
jj commit: test: end-to-end acceptance for python and c/go
Task Summary
| # | Task | Type | Test method |
|---|---|---|---|
| 1 | Scaffold | Setup | Manual smoke |
| 2 | vim.pack dependencies | Infra | Manual smoke |
| 3 | Config defaults + merge | Core | Automated |
| 4 | Debug mode lifecycle | Core | Automated |
| 5 | Help box | UI | Automated |
| 6 | nvim-dap-ui wiring | UI | Manual |
| 7 | Virtual text wiring | UI | Manual |
| 8 | Persistent breakpoints | Core | Automated+Manual |
| 9 | Watch from code | Feature | Manual |
| 10 | Hover and eval | Feature | Manual |
| 11 | Adapters: Python, Go | Adapter | Automated+Manual |
| 12 | Adapters: C/C++/Rust/Swift | Adapter | Manual |
| 13 | Adapters: JS/TS | Adapter | Manual |
| 14 | Adapters: C#, Java, Kotlin | Adapter | Manual |
| 15 | Adapters: PHP, Ruby, Lua | Adapter | Manual |
| 16 | Vimdoc help file | Docs | Manual |
| 17 | README + CONTRIBUTING | Docs | Visual review |
| 18 | End-to-end acceptance | QA | Manual walkthrough |
Each row = one jj commit. 18 commits total. The plugin is usable after
task 6 (basic debug mode with UI). It's feature-complete after task 15.
It's shippable after task 18.