Files
turbo-debug.nvim/turbo-debug-spec.md
T
swaits e9089b63a2 chore: release 0.1.0 — rename to turbo-debug.nvim
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)
2026-04-11 20:09:29 -06:00

1121 lines
44 KiB
Markdown

# 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.
```lua
vim.pack.add("swaits/turbo-debug.nvim")
```
That's it. That's the whole setup.
## Design Principles
1. **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.
2. **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.
3. **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.
4. **Principle of least surprise.** Keybindings should be guessable. `c` is
continue. `s` is step. `q` is quit. If you can't guess it, the help box is
right there. No cleverness that requires documentation.
5. **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.
6. **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.
7. **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 `K` on 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 `W` on 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** — `X` to 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
```lua
-- init.lua
vim.pack.add("swaits/turbo-debug.nvim")
```
One line. One plugin. Everything works.
### Configuration (optional)
```lua
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:
1. Opens nvim-dap-ui with the configured layout.
2. Enables nvim-dap-virtual-text (inline variable values appear in source).
3. Activates modal keybindings (buffer-local, vanish on exit).
4. Changes the cursor highlight to indicate you're in debug mode.
5. Shows the help box (bottom-right float, auto-closes on first keypress).
Disable with `help_on_enter = false`.
6. If no DAP session is active, waits for `c` (continue) to launch one.
Exiting debug mode (any of):
- `<leader>d` again (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 | **S**tep |
| `d` | Step into | **D**escend into the function |
| `r` | Step out | **R**eturn from the function |
| `C` | Run to cursor | **C**ontinue 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| **W**atch (uses `<cexpr>` or visual selection) |
| `K` | Hover / inspect | Matches vim LSP hover convention |
| `E` | Eval in REPL | **E**valuate |
| `q` | Terminate session | **Q**uit debugging |
| `R` | Restart session | **R**erun |
| `?` | 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
```lua
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.
- `x` calls `require("persistent-breakpoints.api").toggle_breakpoint()`.
- `X` calls 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 `W` results 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`/`.env` relative 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` (uses `js-debug-adapter` binary)
- Launch configs for file, attach, and node process
- Shared across `javascript`, `typescript`, `javascriptreact`,
`typescriptreact` filetypes
- 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`, `swift` filetypes
**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, `rdbg` with 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:
1. User override via `opts.adapters`
2. Binary on `$PATH`
3. Standard mason install path (`~/.local/share/nvim/mason/bin/`)
4. Common system paths (`/usr/bin/`, `/usr/local/bin/`)
### Behavior
- On first `c` (continue), if no `dap.configurations` exist 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
```lua
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:
1. Add one line to their init.lua: `vim.pack.add("swaits/turbo-debug.nvim")`
2. Open a file in any of the world's top 10 languages.
3. Press `x` on a line to set a breakpoint (visible immediately in gutter).
4. Press `<leader>d` to enter debug mode. See the help box. See the UI.
5. Press `c` to start debugging. See the program stop at the breakpoint.
6. See variable values inline in the source code as virtual text.
7. Press `K` to hover a variable and see its full value.
8. Press `W` to add a watch expression.
9. Press `s` to step, see everything update live.
10. Press `q` to quit. Everything disappears. Editor is clean.
11. 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.lua` starts without errors
- `:help turbo-debug` opens the help file (even if sparse)
- `:lua print(type(require("turbo-debug")))` prints `table`
- `jj log` shows 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").defaults` returns 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>d` enters debug mode; all configured keys are active
- `<leader>d` again exits; all keys are removed
- A pre-existing user mapping for `c` is stashed on enter and restored on
exit (verified by checking `vim.fn.maparg`)
- `require("turbo-debug").active()` returns `true`/`false` correctly
- Navigation keys (`h`, `j`, `w`, `b`, `/`, `n`, etc.) still work in
debug mode
- `dCursor` highlight 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 `continue` is remapped to `g`,
the help box shows `g` not `c`)
- 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:**
- `x` toggles a breakpoint on the current line (sign appears in gutter)
- `X` prompts for a condition and sets a conditional breakpoint
- Both work outside of debug mode (no `<leader>d` needed 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:**
- `W` in normal mode in debug mode adds the expression under cursor to
the watches panel
- `W` in 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:**
- `K` in normal mode shows a floating window with the value of the word
under cursor
- `K` in visual mode shows the value of the selected expression
- `E` opens 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 `.py` file and pressing `<leader>d` then `c` launches debugpy
- If a virtualenv is active, debugpy uses it
- Opening a `.go` file and pressing `<leader>d` then `c` launches delve
- If the user already has `dap.configurations.python` set, 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 `.swift` file 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 `.js` or `.ts` file 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-debug` opens the help file
- `:help turbo-debug.setup` jumps to the setup section
- `:help turbo-debug.keys` jumps to the keybindings section
- `:help turbo-debug.adapters` jumps 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 `:messages` after a full debug session lifecycle
- Clean startup: no errors on `nvim --clean` with 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.