diff --git a/CHANGELOG.md b/CHANGELOG.md index cd64088..412ef6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,112 @@ # Changelog +## 0.2.0 — 2026-04-12 + +Major UI rework and stability release. Two floating chrome bars replace the +old single toolbar, every resize/tabline-toggle edge case is handled, and +every highlight now derives from the user's colorscheme. + +### Breaking changes + +- **`quit_exits_mode` option removed.** `q` is now context-aware: if a DAP + session is running, `q` terminates the session but keeps debug mode + active (so you can read the Console / inspect post-mortem state). Press + `q` again to exit debug mode. +- **`help_on_enter` default is now `false`** (was `true`). The control bar + always shows `(?) help`, so the auto-popup is no longer needed. Set to + `true` to restore the splash behavior. +- **Default layout changed.** Sidebar panes are now Call Stack → Scopes → + Watches → Breakpoints (all 25%). REPL is no longer in the default + layout — press `E` during debug mode to open it as a floating window. +- **Console pane sizing.** Bottom Console pane is `max(8 rows, 20% of + editor lines)` (previously hardcoded). Configurable via `console_height`. + +### Added + +- **Two-bar floating chrome**, independent of any statusline plugin: + - Status bar (top, 2 rows): brand, live `dap.status()` text, state chip + (` DEBUG · READY ` / ` · RUNNING ` / ` · PAUSED `) + - Control bar (bottom, 2 rows, above statusline): clickable buttons for + continue, step over, step into, step out, restart (during session), + terminate; `(?) help` right-anchored + - Italic qualifiers (`over`, `into`, `out`) on step controls — dropped + automatically at narrow widths +- **Keymap-aware control bar**: legend reflects the user's actual + remapped keys (honors `keys` override; disabled actions are omitted) +- **IP arrow markers** on stopped lines: paired 👉 gutter sign (priority + 500 so it always wins over breakpoint signs) + 👈 REASON virtual text + at EOL +- **Optional debug-mode colorscheme** via `colorscheme` option — theme + swaps on debug enter, restores on exit +- **Theme-agnostic separator color**: derives from `WinBar.fg` so the bar + chrome matches any colorscheme; global `WinSeparator` override during + debug mode gives the whole UI a consistent edge color +- **`clear_console_on_start`** (default `true`): wipe Console scrollback + on session start/restart while preserving the terminal channel +- **`collapsed_scopes`** (default `{ "Registers" }`): auto-collapse noisy + scopes; codelldb doesn't mark Registers expensive even though it's a + huge dump +- **`max_value_width`** (default `2000`): per-line truncation for values + in Scopes/Watches; prevents register-dump bleed +- **`console_refresh_ms`** (default `50`): trailing-edge throttle for + Console repaints under high-throughput stdout +- **`sidebar`** option — `"left"` (default) or `"right"` +- **`active_window_highlight`** (default `true`): dim inactive dapui pane + borders so the focused pane is visually obvious +- **`W` and `K` in visual mode**: watch / hover operate on the selection +- Live `dap.status()` polling via `User DapProgressUpdate` autocmd — + status text stays current across progress messages +- Source window is focused before DAP jumps on `event_stopped`, so + stepped-into library files open in the source area (not in a bar or + dapui pane) +- Console repaint throttle prevents editor lag under chatty programs + +### Changed + +- Bars are now real splits (not overlapping floats) — dapui and source + windows tuck between them naturally +- `pin_dapui_sizes()` only re-pins on EXTERNAL geometry changes + (bufferline tabline toggle, `wincmd =`) detected by total-height + delta; user's manual pane drags survive +- `ensure_dapui()` runs ONCE per plugin lifetime (not per `M.enter`), + avoiding "Debug adapter didn't respond" timeouts on toggle +- Bars clamp to exactly 2 rows on every VimResized / WinResized (prevents + stray EOB `~` when nvim grows a fixed-height window) +- `setup()` is now fully idempotent — safe to call multiple times with + different opts, and `plugin/turbo-debug.lua`'s deferred auto-setup + yields to explicit user `setup({...})` +- Copyright holder updated to `Stephen Waits ` + +### Fixed + +- Bars recreate themselves if killed by shrink-below-minimum resize or + tabline appearance/disappearance +- Inherited `winbar` from the splitting source window is cleared on bar + windows (was showing phantom separator rows) +- Layout re-asserts correctly after `wincmd =` (tiny-equalizer pass) +- Sidebar proportions stable across re-entry (no drift across toggles) +- Bar separators bright on both outward edges (top-of-sbar, bottom-of-cbar) +- Deferred dependency loading: `plugin/turbo-debug.lua` waits for + `vim.pack.add` to finalize runtimepath before calling `setup()` + +### Internal + +- Extracted 10+ helper functions in `mode.lua` (`win_ok`, `buf_ok`, + `is_dapui_pane_ft`, `for_each_dapui_pane`, `safe_extmark`, + `bar_set_lines`, `safe_set_height`, `if_still_active`, + `invalidate_dead_bars`, `focus_source_win`, `unset_and_restore`, + `install_cs_listener`) — deduped ~25 call sites +- Data-driven sign definitions (`breakpoints.lua`) and Python + interpreter candidate probing +- Dead code removed: unused `reposition_bars` stub, vestigial + `sbar_zones` / `handle_sbar_click` click infrastructure (status bar + has no clickable zones) +- Added regression tests: `test_render_bars` (sbar/cbar output, + dimensions, zones, overflow fallback), `test_highlights_cs` + (ColorScheme augroups, highlight re-apply), `test_helpers` + (keyof branching, source_window filtering) +- `stylua.toml` added; all Lua code formatted + ## 0.1.0 — 2026-04-11 Initial release. diff --git a/LICENSE b/LICENSE index 6e0146f..694cb77 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Steve Waits +Copyright (c) 2026 Stephen Waits Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8d7a134..6d237e1 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,12 @@ require("turbo-debug").setup({ toggle = "D", -- change the toggle chord }, sidebar = "right", -- "left" (default) or "right" - help_on_enter = false, -- disable the help splash + help_on_enter = true, -- show help popup on debug mode entry (default false) + + -- Optional theme swap for debug sessions. `nil` (default) keeps the + -- current colorscheme; any string is passed to `:colorscheme` on enter + -- and the original is restored on exit. + colorscheme = nil, -- Auto stop-on-entry when no breakpoints are set (Turbo Pascal model): -- press `c` on an empty-BP project and the debugger pauses at main(). @@ -112,23 +117,44 @@ require("turbo-debug").setup({ -- Console repaint throttle (ms). Max 1000/console_refresh_ms redraws per -- second when the debugged program spams stdout. Prevents editor DOS. console_refresh_ms = 50, + + -- Clear the Console pane on session start/restart (preserves the + -- terminal channel — only wipes scrollback). + clear_console_on_start = true, + + -- Scope names to default-collapse in Scopes. dapui auto-collapses any + -- scope the adapter marks `expensive`, but codelldb doesn't flag + -- Registers as expensive even though it's a huge dump. + collapsed_scopes = { "Registers" }, + + -- Dim inactive dapui pane borders so the focused pane is obvious. + active_window_highlight = true, }) ``` -### Control bar +### Chrome bars -When debug mode is active a 1-row floating bar appears directly above your -statusline, showing: +When debug mode is active, two 2-row floating bars appear, completely +independent of lualine, heirline, or any other statusline plugin: -- `● READY` / `● RUNNING` / `● PAUSED` session state chip (color-coded) -- Clickable controls: continue `▶`, step over, step into, step out, - restart, terminate, exit debug mode -- `dap.status()` progress text (right-aligned) -- Cursor position +**Status bar (top)** shows: +- The `turbo-debug` brand +- Live `dap.status()` progress text +- Session state chip: ` DEBUG · READY ` / ` · RUNNING ` / ` · PAUSED ` + (color-coded via `DiagnosticHint` / `DiagnosticInfo` / `DiagnosticError`) -The bar is completely independent of lualine, heirline, or any other -statusline plugin — it opens in its own floating window and closes cleanly -when debug mode exits. +**Control bar (bottom, above your statusline)** shows: +- Clickable buttons: `(c)ontinue` / `(s)tep` *over* / `(d)escend` *into* / + `(r)eturn` *out* / `(R)estart` (during session) / `(q) terminate` +- `(?) help` right-anchored +- Italic qualifiers (`over`, `into`, `out`) are inline when width allows; + auto-dropped on narrow terminals + +Both bars: +- Recreate themselves if killed by layout changes (bufferline tabline + toggles, `wincmd =`, etc.) +- Re-render on every DAP state change (|User DapProgressUpdate|) +- Close cleanly on exit ### Design notes @@ -136,18 +162,21 @@ when debug mode exits. the help popup (`?`), the hover popup (`K`), and the debug-eval popup (`E`). Floating windows are the only windows Neovim draws borders around in the terminal grid. Splits (the Scopes/Watches/Stacks/Breakpoints/ - REPL/Console panes) use the user's `WinSeparator` character — that's a + Console panes) use the user's `WinSeparator` character — that's a terminal-rendering limit, not a plugin choice. - **No hardcoded colors.** Every highlight links to a standard group (`DiagnosticError`, `Function`, `Comment`, `CursorLine`, `Visual`, - `Search`, `Title`, `Special`, etc.) so the plugin automatically inherits - your colorscheme on the fly. Switch themes mid-session and everything - re-paints. + `Search`, `Title`, `Special`, `WinBar`, etc.) so the plugin + automatically inherits your colorscheme on the fly. Switch themes + mid-session (or set `colorscheme = "high-contrast"` in `setup()` for + a dedicated debug theme) and everything re-paints. -- **Nerd Font Codicons** for all functional iconography (gutter signs, - control buttons, pane titles). Emoji (`👈` / `👉`) for the paused-line - arrows, because fingers-pointing-at-the-line is unmissable. +- **Emoji** (supplementary-plane, not VS16-dependent) for iconography: + 🔴 🟡 ⭕ 👉 📝 for gutter signs; 🟢 🚀 👣 🔽 🔼 🔄 🛑 for control + buttons; 🔎 👀 📚 🔴 💻 for pane titles. Colored emoji render + consistently in modern terminals (Ghostty, Kitty, WezTerm, iTerm) + without font-dependent rendering quirks. ## Supported Languages diff --git a/assets/screenshot.png b/assets/screenshot.png index 8b36910..f1055ac 100644 Binary files a/assets/screenshot.png and b/assets/screenshot.png differ diff --git a/doc/turbo-debug.txt b/doc/turbo-debug.txt index 5f0ab1a..80463db 100644 --- a/doc/turbo-debug.txt +++ b/doc/turbo-debug.txt @@ -9,28 +9,31 @@ CONTENTS *turbo-debug-contents* 4. Debug Mode ............................ |turbo-debug-debug-mode| 5. Keybindings ........................... |turbo-debug.keys| 6. Breakpoints ........................... |turbo-debug-breakpoints| - 7. UI Layout ............................. |turbo-debug-layout| - 8. Adapters .............................. |turbo-debug.adapters| - 9. API ................................... |turbo-debug-api| - 10. Troubleshooting ....................... |turbo-debug-troubleshooting| + 7. Chrome Bars ........................... |turbo-debug-chrome| + 8. UI Layout ............................. |turbo-debug-layout| + 9. Adapters .............................. |turbo-debug.adapters| + 10. API ................................... |turbo-debug-api| + 11. Troubleshooting ....................... |turbo-debug-troubleshooting| ============================================================================== 1. Introduction *turbo-debug-introduction* -turbo-debug.nvim gives you a fully-featured, beautiful debugging experience -out of one line of config. 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. +turbo-debug.nvim gives you a fully-featured debugging experience out of one +line of config. Under the hood it wires together nvim-dap, nvim-dap-ui, +nvim-dap-virtual-text, and persistent-breakpoints into a single cohesive +modal experience. Features: +- Modal keybindings active only during debug sessions — single-key controls + that shadow editing keys, cleanly restored on exit - Inline virtual text showing variable values in source code -- Hover inspection with `K` -- Persistent breakpoints that survive restarts -- Watch expressions from the code window with `W` -- Scopes, stacks, REPL, and console panels -- Modal keybindings active only during debug sessions -- Help box showing all keybindings -- Zero-config adapters for 10+ languages +- Persistent breakpoints that survive Neovim restarts +- Hover inspection (|K|) and watch expressions (|W|) from the code window +- Floating REPL eval (|E|) +- Floating status bar + control bar chrome (independent of any statusline + plugin) +- Zero-config adapters for 12 languages +- Theme-agnostic colors (every highlight links to a standard group) ============================================================================== 2. Installation *turbo-debug-installation* @@ -47,26 +50,125 @@ Requires Neovim >= 0.12. Setup is optional. The plugin works with no configuration. To customize: >lua require("turbo-debug").setup({ - help_on_enter = true, -- show help splash on debug mode entry - quit_exits_mode = true, -- q terminates AND exits debug mode - builtin_adapters = true, -- register built-in language adapters - keys = { ... }, -- override modal keybindings - global_keys = { ... }, -- override always-available keybindings - adapters = { ... }, -- override or add language adapters - dapui = { ... }, -- override nvim-dap-ui layout/settings - virtual_text = { ... }, -- override nvim-dap-virtual-text settings + help_on_enter = false, + builtin_adapters = true, + stop_on_entry_when_no_breakpoints = true, + max_value_width = 2000, + console_refresh_ms = 50, + collapsed_scopes = { "Registers" }, + clear_console_on_start = true, + sidebar = "left", + colorscheme = nil, + active_window_highlight = true, + keys = { ... }, + global_keys = { ... }, + adapters = { ... }, + dapui = { ... }, + virtual_text = { ... }, }) < *turbo-debug.setup.help_on_enter* help_on_enter ~ - Type: boolean, Default: `true` - Show the help box automatically when entering debug mode. + Type: boolean, Default: `false` + Show the help popup automatically when entering debug mode. The control + bar always shows `(?) help`, so most users don't need the splash. *turbo-debug.setup.builtin_adapters* builtin_adapters ~ Type: boolean, Default: `true` - Register built-in adapters for supported languages. Set to `false` to - disable all built-in adapter registration. + Register the built-in language adapters. Set to `false` to opt out of + all built-in adapter registration and manage adapters yourself. + + *turbo-debug.setup.stop_on_entry_when_no_breakpoints* +stop_on_entry_when_no_breakpoints ~ + Type: boolean, Default: `true` + When `c` is pressed with no breakpoints set, launch the first adapter + configuration for the filetype with `stopOnEntry = true` so the debugger + pauses at the program's entry point. Matches the Turbo Pascal "press + key to start debugging" model. Set `false` to always run until a + breakpoint. + + *turbo-debug.setup.max_value_width* +max_value_width ~ + Type: integer, Default: `2000` + Per-line character cap for variable values rendered in Scopes/Watches. + Values exceeding this are truncated with `…`. Default 2000 only catches + pathological multi-KB dumps; normal values pass through. Set to `0` to + disable truncation entirely. + + *turbo-debug.setup.console_refresh_ms* +console_refresh_ms ~ + Type: integer, Default: `50` + Trailing-edge throttle (ms) for Console pane repaints when the debugged + program outputs. Max `1000/console_refresh_ms` redraws per second. + Prevents editor lag under high-throughput output. + + *turbo-debug.setup.collapsed_scopes* +collapsed_scopes ~ + Type: string[], Default: `{ "Registers" }` + Scope names (as reported by the DAP adapter) to default-collapse. Some + adapters (codelldb for Rust/C++, for example) don't mark Registers as + "expensive" even though it's a huge dump. Any scope name in this list + is treated as expensive — dapui auto-collapses expensive scopes. + + *turbo-debug.setup.clear_console_on_start* +clear_console_on_start ~ + Type: boolean, Default: `true` + Clear the Console pane on session start/restart. The terminal channel + is preserved — only the displayed scrollback is wiped. + + *turbo-debug.setup.sidebar* +sidebar ~ + Type: string, Default: `"left"` + Sidebar position: `"left"` or `"right"`. Controls which side of the + editor the Stacks/Scopes/Watches/Breakpoints column appears on. + + *turbo-debug.setup.colorscheme* +colorscheme ~ + Type: string|nil, Default: `nil` + Optional colorscheme to switch to while debug mode is active. `nil` + keeps the user's current colorscheme. Any other string value is passed + to `:colorscheme`. The previous colorscheme is restored on exit. + + *turbo-debug.setup.active_window_highlight* +active_window_highlight ~ + Type: boolean, Default: `true` + Dim inactive dapui panes' borders so the focused pane is visually + obvious. + + *turbo-debug.setup.keys* +keys ~ + Type: table, Default: see |turbo-debug.keys| + Override modal keybindings (active only during debug mode). Set any + entry to `false` to disable that action entirely. + + *turbo-debug.setup.global_keys* +global_keys ~ + Type: table, Default: see |turbo-debug.keys| + Override always-available keybindings. Set any entry to `false` to + disable. + + *turbo-debug.setup.adapters* +adapters ~ + Type: table, Default: `{}` + Override or add language adapters. Keyed by adapter name; each value + is a table with `adapter` (passed to |dap.adapters|) and/or + `configurations` (passed to |dap.configurations|) fields. User + overrides are applied after the built-ins, so existing adapter entries + are replaced. + + *turbo-debug.setup.dapui* +dapui ~ + Type: table + Pass-through configuration for nvim-dap-ui's |dapui.setup()|. Override + icons, controls, floating window style, render options, etc. + + *turbo-debug.setup.virtual_text* +virtual_text ~ + Type: table + Pass-through configuration for nvim-dap-virtual-text. Set + `virtual_text = { enabled = false }` to disable inline variable + values entirely. ============================================================================== 4. Debug Mode *turbo-debug-debug-mode* @@ -74,17 +176,28 @@ builtin_adapters ~ Press `dd` to toggle debug mode. Entering debug mode: -1. Opens nvim-dap-ui with the configured layout -2. Enables inline virtual text for variable values -3. Activates modal keybindings (buffer-local) -4. Shows winbar indicator and help splash (3s) +1. Switches colorscheme (if |turbo-debug.setup.colorscheme| is set) +2. Applies turbo-debug highlight groups +3. Opens nvim-dap-ui (sidebar + Console pane) +4. Creates the floating status bar (top) and control bar (bottom) +5. Installs modal keybindings on every buffer +6. Enables nvim-dap-virtual-text +7. Optionally shows the help popup (|turbo-debug.setup.help_on_enter|) Exiting debug mode (any of): - `dd` again (manual toggle) -- `q` (terminate session — exits debug mode if `quit_exits_mode`) -- DAP session ends naturally +- `q` when no DAP session is running +- Context-aware `q`: if a session IS running, `q` terminates the session + but leaves debug mode active so you can inspect the Console / watch + post-mortem state. Press `q` again to exit debug mode. -On exit, everything is restored: keymaps, UI, virtual text, cursor. +On exit, everything is restored: +- Modal keymaps removed, any prior user mappings re-installed +- Source window's original `winbar` restored +- `WinSeparator` highlight restored byte-for-byte +- Original colorscheme restored (if one was swapped in) +- dapui panes closed, control bars removed +- Virtual text disabled ============================================================================== 5. Keybindings *turbo-debug.keys* @@ -95,7 +208,7 @@ keys you don't need while debugging, but preserve all navigation keys. Default keymap: Key Action Mnemonic ~ - `c` Continue Universal across debuggers + `c` Continue / start Universal across debuggers `s` Step over Step `d` Step into Descend into the function `r` Step out Return from the function @@ -105,8 +218,8 @@ Default keymap: `D` Clear all breakpoints Delete all `W` Watch expr under cursor Watch `K` Hover / inspect Matches LSP hover convention - `E` Eval in REPL Evaluate - `q` Terminate session Quit debugging + `E` Eval in REPL Evaluate (floating REPL) + `q` Smart quit Context-aware terminate / exit `R` Restart session Rerun `?` Help popup Show keybindings @@ -117,29 +230,30 @@ Always available (under `d` prefix, with which-key support): `dX` Conditional breakpoint Shift variant `dD` Clear all breakpoints Delete all +`W` and `K` also work in visual mode — they operate on the visual +selection. + Override any key: >lua require("turbo-debug").setup({ - keys = { -- modal keys (debug mode only) - continue = "g", - step_over = false, -- set to false to disable + keys = { + continue = "g", -- remap a modal key + step_over = false, -- disable a modal key }, - global_keys = { -- always-available keys + global_keys = { toggle = "DD", breakpoint = "db", }, }) < -Preserved keys (not shadowed): -- Navigation: `h j k l w b e f F t T / n N 0 ^ $ G g { } % [ ]` -- Insert: `i a o O` (needed for REPL/terminal) -- Visual/yank: `v V y p u` -- All Ctrl/leader combos +Inside dapui panes (Watches / Scopes / Stacks / Breakpoints / REPL), the +native dapui mappings `` / `e` / `o` / `d` / `r` / `t` still work — +our modal `d` (step-into) and `r` (step-out) yield inside those buffers. - *turbo-debug.setup.quit_exits_mode* -quit_exits_mode ~ - Type: boolean, Default: `true` - Whether `q` (terminate) also exits debug mode. Set to `false` if you - want `q` to terminate the process but keep the debug UI open. +Preserved keys (never shadowed in source buffers): +- Navigation: `h j k l w b e f F t T / n N 0 ^ $ G g { } % [ ]` +- Insert: `i a o O` +- Visual/yank/edit: `v V y p u` +- All Ctrl/Alt/leader combos ============================================================================== 6. Breakpoints *turbo-debug-breakpoints* @@ -147,23 +261,57 @@ quit_exits_mode ~ Breakpoints are managed by persistent-breakpoints.nvim. They persist across Neovim restarts and are visible in the sign column at all times. -`dx` toggles a breakpoint (always available). -`dX` sets a conditional breakpoint (always available). -`x`, `X`, `D` also work as shortcuts during debug mode. +Global shortcuts (always available): + `dx` Toggle breakpoint + `dX` Conditional breakpoint (prompts for condition) + `dD` Clear all breakpoints + +Modal shortcuts (debug mode only): `x` `X` `D` do the same. + +Gutter signs (colorful emoji in modern terminals): + 🔴 breakpoint + 🟡 conditional breakpoint + ⭕ rejected breakpoint (DAP server couldn't set it) + 👉 current execution line (paired with `👈 REASON` virtual text) + 📝 logpoint ============================================================================== -7. UI Layout *turbo-debug-layout* +7. Chrome Bars *turbo-debug-chrome* + +Debug mode opens two floating bars, independent of any statusline plugin: + +Status bar (top, 2 rows) shows: +- `turbo-debug` brand +- `dap.status()` progress text (right of brand) +- State chip: ` DEBUG · READY ` / ` · RUNNING ` / ` · PAUSED ` + +Control bar (bottom, 2 rows, above your statusline) shows: +- Clickable buttons for `(c)ontinue`, `(s)tep` over, `(d)escend` into, + `(r)eturn` out, `(R)estart` (during session), `(q)uit` +- Italic qualifiers (`over`, `into`, `out`) shown inline when width allows +- `(?) help` anchored on the right +- Click any button to fire its action + +Both bars update live on DAP state transitions (|User DapProgressUpdate|) +and on editor resize. They survive tabline toggles (bufferline appear/ +disappear) — killed bars are recreated automatically. + +============================================================================== +8. UI Layout *turbo-debug-layout* Default layout: - Left sidebar (40 columns): - - Scopes (50%) — local/global variables in current frame - - Watches (25%) — user-added watch expressions - - Stacks (25%) — call stack navigation + Sidebar (|turbo-debug.setup.sidebar|, left or right, 25-40 cols): + - Call Stack (25%) — current frames, navigate with `` + - Scopes (25%) — local/global variables in current frame + - Watches (25%) — user-added watch expressions (|W|) + - Breakpoints (25%) — all persistent breakpoints, jump with `` - Bottom tray (12 lines): - - REPL (50%) — evaluate expressions - - Console (50%) — program stdout/stderr + Bottom pane (20% of editor lines, min 8 rows): + - Console — program stdout/stderr + adapter output + +REPL is NOT in the default layout. Press `E` during debug mode to open it +as a floating window on demand. Override the layout: >lua require("turbo-debug").setup({ @@ -173,7 +321,7 @@ Override the layout: >lua }) < ============================================================================== -8. Adapters *turbo-debug.adapters* +9. Adapters *turbo-debug.adapters* Built-in adapter support (adapter binary must be installed): @@ -192,25 +340,39 @@ Built-in adapter support (adapter binary must be installed): Lua (Neovim) osv one-small-step-for-vimkind Path resolution order: -1. User override via `opts.adapters` +1. User override via |turbo-debug.setup.adapters| 2. Binary on `$PATH` 3. Standard mason install path (~/.local/share/nvim/mason/bin/) -Adapter binaries are the user's responsibility to install. +Rust gets special treatment: pressing `c` runs `cargo build` automatically +and picks the binary via `vim.ui.select` if multiple artifacts exist. + +Python auto-detects virtualenvs: `VIRTUAL_ENV`, `CONDA_PREFIX`, then the +common directories `.venv`, `venv`, `env`, `.env` relative to `cwd`. + +Adapter binaries are the user's responsibility to install (mason.nvim is +the easiest route). Override or add adapters: >lua require("turbo-debug").setup({ adapters = { - python = { ... }, -- override built-in - zig = { ... }, -- add new language + python = { adapter = { ... }, configurations = { ... } }, + zig = { adapter = { ... }, configurations = { ... } }, }, builtin_adapters = false, -- disable all built-ins }) < -Existing user nvim-dap configurations are never overridden. +Existing user nvim-dap configurations are never overridden — if a user +has already set `dap.adapters[name]` before `turbo-debug.setup()` runs, +we skip it. ============================================================================== -9. API *turbo-debug-api* +10. API *turbo-debug-api* + + *turbo-debug.setup()* +require("turbo-debug").setup({opts}) + Initialize the plugin with optional configuration. Idempotent — safe + to call multiple times; the last call's opts win. *turbo-debug.toggle()* require("turbo-debug").toggle() @@ -222,24 +384,29 @@ require("turbo-debug").active() Useful for statusline integration. ============================================================================== -10. Troubleshooting *turbo-debug-troubleshooting* +11. Troubleshooting *turbo-debug-troubleshooting* No adapter found ~ Ensure the adapter binary is installed and on your PATH, or installed - via mason.nvim. turbo-debug configures adapters, it does not install + via mason.nvim. turbo-debug configures adapters; it does not install them. Breakpoints not persisting ~ - Check that persistent-breakpoints.nvim is loaded. Run: - `:lua print(require("persistent-breakpoints") and "ok")` - + Check that persistent-breakpoints.nvim is loaded: >vim + :lua print(require("persistent-breakpoints") and "ok") +< Virtual text not appearing ~ Virtual text only appears during an active DAP session in debug mode. - Ensure nvim-dap-virtual-text is loaded: - `:lua print(require("nvim-dap-virtual-text") and "ok")` + Ensure nvim-dap-virtual-text is loaded: >vim + :lua print(require("nvim-dap-virtual-text") and "ok") +< +Bars look squished / have stray `~` markers ~ + The bars auto-clamp to 2 rows on every resize event. If you see this + persistently, please open an issue with your terminal and + `:set lines? columns?` output. Version error ~ - turbo-debug.nvim requires Neovim >= 0.12 for vim.pack support. + turbo-debug.nvim requires Neovim >= 0.12 for `vim.pack` support. ============================================================================== vim:tw=78:ts=8:ft=help:norl: diff --git a/lua/turbo-debug/adapters/c.lua b/lua/turbo-debug/adapters/c.lua index bde768c..7b90d2b 100644 --- a/lua/turbo-debug/adapters/c.lua +++ b/lua/turbo-debug/adapters/c.lua @@ -4,15 +4,11 @@ local M = {} local function pick_executable(executables) local co = coroutine.running() - if not co then - return executables[1] - end + if not co then return executables[1] end vim.ui.select(executables, { prompt = "Select executable", format_item = function(path) return vim.fn.fnamemodify(path, ":t") end, - }, function(choice) - coroutine.resume(co, choice) - end) + }, function(choice) coroutine.resume(co, choice) end) return coroutine.yield() end @@ -54,9 +50,7 @@ function M.register(dap) }, } - if not dap.adapters.codelldb then - dap.adapters.codelldb = adapter - end + if not dap.adapters.codelldb then dap.adapters.codelldb = adapter end -- C/C++/Swift: pick from compiled executables in cwd local prompt_config = { @@ -64,9 +58,7 @@ function M.register(dap) type = "codelldb", request = "launch", name = "Launch executable", - program = function() - return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/", "file") - end, + program = function() return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/", "file") end, cwd = "${workspaceFolder}", }, } @@ -75,15 +67,16 @@ function M.register(dap) end -- Rust: cargo build + picker - dap.configurations.rust = dap.configurations.rust or { - { - type = "codelldb", - request = "launch", - name = "Cargo build & launch", - program = cargo_build_and_pick, - cwd = "${workspaceFolder}", - }, - } + dap.configurations.rust = dap.configurations.rust + or { + { + type = "codelldb", + request = "launch", + name = "Cargo build & launch", + program = cargo_build_and_pick, + cwd = "${workspaceFolder}", + }, + } end return M diff --git a/lua/turbo-debug/adapters/init.lua b/lua/turbo-debug/adapters/init.lua index 75edd20..5c99968 100644 --- a/lua/turbo-debug/adapters/init.lua +++ b/lua/turbo-debug/adapters/init.lua @@ -23,43 +23,102 @@ end -- simple executable adapters (inlined — no separate files needed) local function register_simple_adapters(dap) local simple = { - { "netcoredbg", { "netcoredbg" }, { - type = "executable", args = { "--interpreter=vscode" }, - }, { { - type = "netcoredbg", request = "launch", name = "Launch .NET", - program = function() return vim.fn.input("Path to DLL: ", vim.fn.getcwd() .. "/bin/Debug/", "file") end, - } }, { "cs" } }, + { + "netcoredbg", + { "netcoredbg" }, + { + type = "executable", + args = { "--interpreter=vscode" }, + }, + { + { + type = "netcoredbg", + request = "launch", + name = "Launch .NET", + program = function() return vim.fn.input("Path to DLL: ", vim.fn.getcwd() .. "/bin/Debug/", "file") end, + }, + }, + { "cs" }, + }, - { "java", { "java-debug-adapter" }, { - type = "executable", - }, { { - type = "java", request = "launch", name = "Launch class", - mainClass = function() return vim.fn.input("Main class: ") end, - cwd = "${workspaceFolder}", - } }, { "java" } }, + { + "java", + { "java-debug-adapter" }, + { + type = "executable", + }, + { + { + type = "java", + request = "launch", + name = "Launch class", + mainClass = function() return vim.fn.input("Main class: ") end, + cwd = "${workspaceFolder}", + }, + }, + { "java" }, + }, - { "kotlin", { "kotlin-debug-adapter" }, { - type = "executable", - }, { { - type = "kotlin", request = "launch", name = "Launch main", - mainClass = function() return vim.fn.input("Main class: ") end, - projectRoot = "${workspaceFolder}", - } }, { "kotlin" } }, + { + "kotlin", + { "kotlin-debug-adapter" }, + { + type = "executable", + }, + { + { + type = "kotlin", + request = "launch", + name = "Launch main", + mainClass = function() return vim.fn.input("Main class: ") end, + projectRoot = "${workspaceFolder}", + }, + }, + { "kotlin" }, + }, - { "php", { "php-debug-adapter" }, { - type = "executable", - }, { { - type = "php", request = "launch", name = "Listen for Xdebug", port = 9003, - } }, { "php" } }, + { + "php", + { "php-debug-adapter" }, + { + type = "executable", + }, + { + { + type = "php", + request = "launch", + name = "Listen for Xdebug", + port = 9003, + }, + }, + { "php" }, + }, - { "rdbg", { "rdbg" }, { - type = "server", port = "${port}", - executable = { command = nil, args = { "-n", "--open", "--port", "${port}", "-c", "--", "ruby", "${file}" } }, - }, { { - type = "rdbg", request = "launch", name = "Launch file", command = "ruby", script = "${file}", - }, { - type = "rdbg", request = "attach", name = "Attach", localfsMap = "${workspaceFolder}", - } }, { "ruby" } }, + { + "rdbg", + { "rdbg" }, + { + type = "server", + port = "${port}", + executable = { command = nil, args = { "-n", "--open", "--port", "${port}", "-c", "--", "ruby", "${file}" } }, + }, + { + { + type = "rdbg", + request = "launch", + name = "Launch file", + command = "ruby", + script = "${file}", + }, + { + type = "rdbg", + request = "attach", + name = "Attach", + localfsMap = "${workspaceFolder}", + }, + }, + { "ruby" }, + }, } for _, s in ipairs(simple) do @@ -77,10 +136,13 @@ local function register_simple_adapters(dap) local ok, osv = pcall(require, "osv") if ok then M.register(dap, "nlua", { - type = "server", host = "127.0.0.1", + type = "server", + host = "127.0.0.1", port = function() return osv.launch({ port = 0 }) end, }, { { - type = "nlua", request = "attach", name = "Attach to running Neovim", + type = "nlua", + request = "attach", + name = "Attach to running Neovim", } }, { "lua" }) end end @@ -93,9 +155,7 @@ function M.setup() -- load adapters with non-trivial logic from separate files for _, name in ipairs({ "python", "go", "c", "javascript" }) do local ok, adapter_mod = pcall(require, "turbo-debug.adapters." .. name) - if ok and adapter_mod.register then - adapter_mod.register(dap) - end + if ok and adapter_mod.register then adapter_mod.register(dap) end end -- register simple adapters inlined above diff --git a/lua/turbo-debug/adapters/javascript.lua b/lua/turbo-debug/adapters/javascript.lua index acc56c5..6850d21 100644 --- a/lua/turbo-debug/adapters/javascript.lua +++ b/lua/turbo-debug/adapters/javascript.lua @@ -27,15 +27,16 @@ function M.register(dap) type = "pwa-node", request = "attach", name = "Attach", - processId = function() - return require("dap.utils").pick_process() - end, + processId = function() return require("dap.utils").pick_process() end, cwd = "${workspaceFolder}", }, } loader.register(dap, "pwa-node", adapter, configurations, { - "javascript", "typescript", "javascriptreact", "typescriptreact", + "javascript", + "typescript", + "javascriptreact", + "typescriptreact", }) end diff --git a/lua/turbo-debug/breakpoints.lua b/lua/turbo-debug/breakpoints.lua index 5d73776..a5ff7e0 100644 --- a/lua/turbo-debug/breakpoints.lua +++ b/lua/turbo-debug/breakpoints.lua @@ -42,6 +42,7 @@ function M.setup() -- ⭕ U+2B55 heavy large circle = rejected breakpoint -- 👉 U+1F449 backhand pointing right = current execution line (IP) -- 📝 U+1F4DD memo = log point + -- stylua: ignore start local signs = { { "DapBreakpoint", "\xf0\x9f\x94\xb4" }, { "DapBreakpointCondition", "\xf0\x9f\x9f\xa1" }, @@ -49,9 +50,14 @@ function M.setup() { "DapStopped", "\xf0\x9f\x91\x89", { linehl = "DapStoppedLine" } }, { "DapLogPoint", "\xf0\x9f\x93\x9d" }, } + -- stylua: ignore end for _, s in ipairs(signs) do local spec = { text = s[2], texthl = s[1], numhl = s[1] } - if s[3] then for k, v in pairs(s[3]) do spec[k] = v end end + if s[3] then + for k, v in pairs(s[3]) do + spec[k] = v + end + end vim.fn.sign_define(s[1], spec) end @@ -62,6 +68,7 @@ function M.setup() vim.api.nvim_set_hl(0, "NvimDapVirtualTextInfo", { default = true, link = "DiagnosticInfo", italic = true }) -- turbo-debug's own UI groups (help float, active-pane borders, winbar badge) + -- stylua: ignore start vim.api.nvim_set_hl(0, "TurboDebugHelpBrand", { default = true, link = "Title" }) vim.api.nvim_set_hl(0, "TurboDebugHelpTagline", { default = true, link = "Comment", italic = true }) vim.api.nvim_set_hl(0, "TurboDebugHelpSection", { default = true, link = "Function" }) @@ -70,6 +77,7 @@ function M.setup() vim.api.nvim_set_hl(0, "TurboDebugHelpParen", { default = true, link = "Comment" }) vim.api.nvim_set_hl(0, "TurboDebugCtrlButton", { default = true, link = "Function" }) vim.api.nvim_set_hl(0, "TurboDebugCtrlMuted", { default = true, link = "Comment" }) + -- stylua: ignore end -- derived highlight: the IP "reason" vtext (STEP / BREAKPOINT / EXCEPTION / -- PAUSE / etc.) — bold + reverse + WarningMsg color so it renders as an @@ -80,7 +88,11 @@ function M.setup() local ok, src = pcall(vim.api.nvim_get_hl, 0, { name = "WarningMsg", link = false }) if not ok or not src or not src.fg then return end vim.api.nvim_set_hl(0, "TurboDebugIPReason", { - fg = src.fg, bg = src.bg, bold = true, reverse = true, default = true, + fg = src.fg, + bg = src.bg, + bold = true, + reverse = true, + default = true, }) end apply_ip_reason_hl() @@ -98,9 +110,7 @@ function M.setup() { gk.clear_breakpoints, pb.clear_all_breakpoints, "Clear all breakpoints" }, } for _, b in ipairs(bindings) do - if b[1] then - vim.keymap.set("n", b[1], b[2], { silent = true, desc = b[3] }) - end + if b[1] then vim.keymap.set("n", b[1], b[2], { silent = true, desc = b[3] }) end end end diff --git a/lua/turbo-debug/config.lua b/lua/turbo-debug/config.lua index 66c712d..6b43636 100644 --- a/lua/turbo-debug/config.lua +++ b/lua/turbo-debug/config.lua @@ -76,8 +76,8 @@ M.defaults = { -- ▾ U+25BE down-triangle (expanded), ▸ U+25B8 right-triangle (collapsed), -- ▶ U+25B6 right-pointing triangle (current frame / play-head) icons = { - expanded = "\xe2\x96\xbe", - collapsed = "\xe2\x96\xb8", + expanded = "\xe2\x96\xbe", + collapsed = "\xe2\x96\xb8", current_frame = "\xe2\x96\xb6", }, controls = { @@ -86,14 +86,14 @@ M.defaults = { -- ▶ play, ⏸ pause, ⤓ step-into, ⇒ step-over, ⤒ step-out, ↶ step-back, -- ↻ run-last, ■ terminate, ⊗ disconnect icons = { - play = "\xe2\x96\xb6", - pause = "\xe2\x8f\xb8", - step_into = "\xe2\xa4\x93", - step_over = "\xe2\x87\x92", - step_out = "\xe2\xa4\x92", - step_back = "\xe2\x86\xb6", - run_last = "\xe2\x86\xbb", - terminate = "\xe2\x96\xa0", + play = "\xe2\x96\xb6", + pause = "\xe2\x8f\xb8", + step_into = "\xe2\xa4\x93", + step_over = "\xe2\x87\x92", + step_out = "\xe2\xa4\x92", + step_back = "\xe2\x86\xb6", + run_last = "\xe2\x86\xbb", + terminate = "\xe2\x96\xa0", disconnect = "\xe2\x8a\x97", }, }, diff --git a/lua/turbo-debug/help.lua b/lua/turbo-debug/help.lua index e80ad60..9491c4c 100644 --- a/lua/turbo-debug/help.lua +++ b/lua/turbo-debug/help.lua @@ -11,25 +11,25 @@ local buf_id = nil -- config.opts.keys[action] lookup. Highlight spans are computed against -- the actual key letter in the final line. local flow = { - { "continue", "c", "(", ")ontinue" }, - { "step_over", "s", "(", ")tep over" }, - { "step_into", "d", "(", ")escend into" }, - { "step_out", "r", "(", ")eturn out" }, - { "run_to_cursor", "C", "run to (", ") cursor" }, - { "restart", "R", "(", ")estart session" }, - { "terminate", "q", "(", ")uit debugging" }, + { "continue", "c", "(", ")ontinue" }, + { "step_over", "s", "(", ")tep over" }, + { "step_into", "d", "(", ")escend into" }, + { "step_out", "r", "(", ")eturn out" }, + { "run_to_cursor", "C", "run to (", ") cursor" }, + { "restart", "R", "(", ")estart session" }, + { "terminate", "q", "(", ")uit debugging" }, } local breakpoints = { - { "breakpoint", "x", "(", ") toggle breakpoint" }, - { "cond_breakpoint", "X", "(", ") conditional breakpoint" }, + { "breakpoint", "x", "(", ") toggle breakpoint" }, + { "cond_breakpoint", "X", "(", ") conditional breakpoint" }, { "clear_breakpoints", "D", "(", ")elete all breakpoints" }, } local inspect = { { "watch", "W", "(", ")atch expression" }, { "hover", "K", "(", ") hover / inspect" }, - { "eval", "E", "(", ")val in REPL" }, + { "eval", "E", "(", ")val in REPL" }, } local meta = { @@ -37,27 +37,27 @@ local meta = { } local globals = { - { "breakpoint", "toggle breakpoint" }, - { "cond_breakpoint", "conditional breakpoint" }, + { "breakpoint", "toggle breakpoint" }, + { "cond_breakpoint", "conditional breakpoint" }, { "clear_breakpoints", "delete all breakpoints" }, } local dapui_panes = { { "", "expand / jump to target" }, - { "o", "open target (jump to frame / breakpoint)" }, - { "e", "edit (watches)" }, - { "d", "remove (watches)" }, - { "r", "send to REPL" }, - { "t", "toggle subtle frames (call stack)" }, + { "o", "open target (jump to frame / breakpoint)" }, + { "e", "edit (watches)" }, + { "d", "remove (watches)" }, + { "r", "send to REPL" }, + { "t", "toggle subtle frames (call stack)" }, } -- ▶ U+25B6, ● U+25CF, ◉ U+25C9, ◆ U+25C6, ❯ U+276F, ≡ U+2261 -local ICON_FLOW = "\xe2\x96\xb6" -local ICON_BRK = "\xe2\x97\x8f" +local ICON_FLOW = "\xe2\x96\xb6" +local ICON_BRK = "\xe2\x97\x8f" local ICON_INSPECT = "\xe2\x97\x89" -local ICON_META = "\xe2\x97\x86" +local ICON_META = "\xe2\x97\x86" local ICON_CHEVRON = "\xe2\x9d\xaf" -local ICON_STACK = "\xe2\x89\xa1" +local ICON_STACK = "\xe2\x89\xa1" -- a brand header and tagline local BRAND_TITLE = " " .. ICON_FLOW .. " TURBO DEBUG" @@ -68,7 +68,7 @@ local function build() local hl = {} -- list of { line_idx, col_start, col_end, hl_group } -- title + tagline highlights - hl[#hl + 1] = { 1, 0, #BRAND_TITLE, "TurboDebugHelpBrand" } + hl[#hl + 1] = { 1, 0, #BRAND_TITLE, "TurboDebugHelpBrand" } hl[#hl + 1] = { 2, 0, #BRAND_TAGLINE, "TurboDebugHelpTagline" } local function section(icon, title) @@ -161,18 +161,16 @@ local function build() lines[#lines + 1] = "" local width = 0 - for _, l in ipairs(lines) do if vim.fn.strdisplaywidth(l) > width then width = vim.fn.strdisplaywidth(l) end end + for _, l in ipairs(lines) do + if vim.fn.strdisplaywidth(l) > width then width = vim.fn.strdisplaywidth(l) end + end return lines, width + 2, hl end function M.close() - if win_id and vim.api.nvim_win_is_valid(win_id) then - vim.api.nvim_win_close(win_id, true) - end + if win_id and vim.api.nvim_win_is_valid(win_id) then vim.api.nvim_win_close(win_id, true) end win_id = nil - if buf_id and vim.api.nvim_buf_is_valid(buf_id) then - vim.api.nvim_buf_delete(buf_id, { force = true }) - end + if buf_id and vim.api.nvim_buf_is_valid(buf_id) then vim.api.nvim_buf_delete(buf_id, { force = true }) end buf_id = nil end @@ -188,7 +186,9 @@ function M.open() local ns = vim.api.nvim_create_namespace("turbo-debug.help") for _, span in ipairs(hl) do pcall(vim.api.nvim_buf_set_extmark, buf_id, ns, span[1], span[2], { - end_row = span[1], end_col = span[3], hl_group = span[4], + end_row = span[1], + end_col = span[3], + hl_group = span[4], }) end @@ -216,8 +216,6 @@ function M.open() }) end -function M.is_open() - return win_id ~= nil and vim.api.nvim_win_is_valid(win_id) -end +function M.is_open() return win_id ~= nil and vim.api.nvim_win_is_valid(win_id) end return M diff --git a/lua/turbo-debug/init.lua b/lua/turbo-debug/init.lua index a3752cc..dc542fa 100644 --- a/lua/turbo-debug/init.lua +++ b/lua/turbo-debug/init.lua @@ -27,17 +27,14 @@ function M.setup(opts) local toggle_key = config.opts.global_keys.toggle if toggle_key then vim.keymap.set("n", toggle_key, function() mode.toggle() end, { - silent = true, desc = "Toggle debug mode", + silent = true, + desc = "Toggle debug mode", }) end end -function M.toggle() - mode.toggle() -end +function M.toggle() mode.toggle() end -function M.active() - return mode.is_active() -end +function M.active() return mode.is_active() end return M diff --git a/lua/turbo-debug/mode.lua b/lua/turbo-debug/mode.lua index efe8247..45a608f 100644 --- a/lua/turbo-debug/mode.lua +++ b/lua/turbo-debug/mode.lua @@ -57,6 +57,7 @@ local vt_initialized = false -- All in the PUA range U+EA60..U+EC1E. Encoded as UTF-8 bytes so the source -- file stays ASCII through any tool pipeline that strips PUA codepoints. +-- stylua: ignore start local ICON = { -- Control-bar emoji — strictly supplementary-plane (U+1F000+) or -- standalone-emoji-default (like 🛑) codepoints. No VS16-dependent @@ -105,6 +106,7 @@ local ICON = { wrap_mark = "\xe2\x86\xb3", -- ↳ downward arrow w/ tip right hline = "\xe2\x94\x80", -- ─ light horizontal } +-- stylua: ignore end -- ─── layout ────────────────────────────────────────────────────────────────── @@ -114,8 +116,7 @@ local function build_default_layouts() -- Console height: max(8 rows, 20% of total editor lines). More -- generous than the previous 15% — user reported the Console kept -- feeling "laughably small" so we're allocating real estate. - local console_size = config.opts.console_height - or math.max(8, math.floor(vim.o.lines * 0.2)) + local console_size = config.opts.console_height or math.max(8, math.floor(vim.o.lines * 0.2)) -- Sidebar width: ideal 40 cols, never less than 25, never more than 1/3 -- of tty width. Scales down gracefully on narrow terminals, stops at 40 @@ -129,9 +130,9 @@ local function build_default_layouts() -- Order: Call Stack → Scopes → Watches → Breakpoints. -- Equal 25% share per user preference. elements = { - { id = "stacks", size = 0.25 }, - { id = "scopes", size = 0.25 }, - { id = "watches", size = 0.25 }, + { id = "stacks", size = 0.25 }, + { id = "scopes", size = 0.25 }, + { id = "watches", size = 0.25 }, { id = "breakpoints", size = 0.25 }, }, }, @@ -151,14 +152,14 @@ end local function define_highlights() local hl = vim.api.nvim_set_hl - hl(0, "TurboDebugBorderActive", { default = true, link = "Function" }) + hl(0, "TurboDebugBorderActive", { default = true, link = "Function" }) hl(0, "TurboDebugBorderInactive", { default = true, link = "Comment" }) - hl(0, "TurboDebugWinbar", { default = true, link = "DiagnosticError" }) + hl(0, "TurboDebugWinbar", { default = true, link = "DiagnosticError" }) -- Bar background: link to Normal so the bar blends with dapui's own -- pane borders (which use Normal or WinBar — both share the editor's -- default background). StatusLine was too dark and created a visible -- seam between the bar and the surrounding panes. - hl(0, "TurboDebugBar", { default = true, link = "Normal" }) + hl(0, "TurboDebugBar", { default = true, link = "Normal" }) -- Bar separator: fg derived from WinBar.fg at runtime so it matches -- whatever color the user's theme uses for pane-title chrome -- (dapui renders each pane's title in its winbar, so this is @@ -168,17 +169,17 @@ local function define_highlights() -- -- Placeholder link so the group exists as a sane default even -- before the derive runs. - hl(0, "TurboDebugBarSeparator", { default = true, link = "WinSeparator" }) + hl(0, "TurboDebugBarSeparator", { default = true, link = "WinSeparator" }) -- Control labels: link to Normal so the text blends with the bar's -- Normal-bg surface. StatusLine bg was creating a visible color patch -- behind each label. The key letter still pops via TurboDebugBarKey -- (Special fg, bold), and icons carry their own emoji color. - hl(0, "TurboDebugBarCtrl", { default = true, link = "Normal" }) - hl(0, "TurboDebugBarStatus", { default = true, link = "Comment" }) - hl(0, "TurboDebugBarBrand", { default = true, link = "Title" }) - hl(0, "TurboDebugBarReady", { default = true, link = "DiagnosticHint" }) - hl(0, "TurboDebugBarRunning", { default = true, link = "DiagnosticInfo" }) - hl(0, "TurboDebugBarPaused", { default = true, link = "DiagnosticError" }) + hl(0, "TurboDebugBarCtrl", { default = true, link = "Normal" }) + hl(0, "TurboDebugBarStatus", { default = true, link = "Comment" }) + hl(0, "TurboDebugBarBrand", { default = true, link = "Title" }) + hl(0, "TurboDebugBarReady", { default = true, link = "DiagnosticHint" }) + hl(0, "TurboDebugBarRunning", { default = true, link = "DiagnosticInfo" }) + hl(0, "TurboDebugBarPaused", { default = true, link = "DiagnosticError" }) -- Italic qualifier words ("over", "into", "out") shown after the -- step/descend/return labels. Subordinate styling: Comment fg + italic, -- so the key label ("(s)tep") reads primary and the qualifier ("over") @@ -191,7 +192,9 @@ local function define_highlights() return end vim.api.nvim_set_hl(0, "TurboDebugBarItalic", { - fg = src.fg, italic = true, default = true, + fg = src.fg, + italic = true, + default = true, }) end local function install_cs_listener(name, fn) @@ -215,7 +218,10 @@ local function define_highlights() return end vim.api.nvim_set_hl(0, "TurboDebugBarKey", { - fg = src.fg, bg = src.bg, bold = true, default = true, + fg = src.fg, + bg = src.bg, + bold = true, + default = true, }) end install_cs_listener("TurboDebugBarKeyHL", apply_key_hl) @@ -256,8 +262,10 @@ end -- forces the pane's winbar row to stay bright even when the pane is -- unfocused (nvim renders unfocused wins with WinBarNC). Nordfox happens to -- make them identical; other themes don't. -local DAPUI_WINHL_ACTIVE = "WinBar:WinBar,WinBarNC:WinBar,WinSeparator:TurboDebugBorderActive,FloatBorder:TurboDebugBorderActive" -local DAPUI_WINHL_INACTIVE = "WinBar:WinBar,WinBarNC:WinBar,WinSeparator:TurboDebugBorderInactive,FloatBorder:TurboDebugBorderInactive" +local DAPUI_WINHL_ACTIVE = + "WinBar:WinBar,WinBarNC:WinBar,WinSeparator:TurboDebugBorderActive,FloatBorder:TurboDebugBorderActive" +local DAPUI_WINHL_INACTIVE = + "WinBar:WinBar,WinBarNC:WinBar,WinSeparator:TurboDebugBorderInactive,FloatBorder:TurboDebugBorderInactive" local function setup_active_win_highlights() local active_hl = DAPUI_WINHL_ACTIVE @@ -267,9 +275,7 @@ local function setup_active_win_highlights() local function apply(win, hl) if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_config(win).relative == "" then local ft = vim.bo[vim.api.nvim_win_get_buf(win)].filetype or "" - if ft:match("^dapui_") or ft == "dap-repl" then - vim.wo[win].winhighlight = hl - end + if ft:match("^dapui_") or ft == "dap-repl" then vim.wo[win].winhighlight = hl end end end vim.api.nvim_create_autocmd("WinEnter", { @@ -299,8 +305,12 @@ local function buf_ok(b) return b and vim.api.nvim_buf_is_valid(b) end -- setup_active_win_highlights / the FileType autocmd, and different from -- schedule_console_redraw's `dapui_console` single-check — don't unify. local DAPUI_PANE_FTS = { - dapui_stacks = true, dapui_scopes = true, dapui_watches = true, - dapui_breakpoints = true, dapui_console = true, ["dap-repl"] = true, + dapui_stacks = true, + dapui_scopes = true, + dapui_watches = true, + dapui_breakpoints = true, + dapui_console = true, + ["dap-repl"] = true, } local function is_dapui_pane_ft(ft) return ft ~= nil and DAPUI_PANE_FTS[ft] == true end @@ -317,9 +327,7 @@ local function for_each_dapui_pane(cb) end -- Bar extmarks go through bar_ns + pcall. IP marker uses ip_ns separately. -local function safe_extmark(buf, row, col, opts) - pcall(vim.api.nvim_buf_set_extmark, buf, bar_ns, row, col, opts) -end +local function safe_extmark(buf, row, col, opts) pcall(vim.api.nvim_buf_set_extmark, buf, bar_ns, row, col, opts) end local function bar_set_lines(buf, lines) vim.bo[buf].modifiable = true @@ -332,12 +340,18 @@ local function safe_set_height(w, h) pcall(vim.api.nvim_win_set_height, w, h) en -- Defer fn via vim.schedule; re-check `active` at fire time so the body -- doesn't run after M.exit cleared state. local function if_still_active(fn) - vim.schedule(function() if active then fn() end end) + vim.schedule(function() + if active then fn() end + end) end local function invalidate_dead_bars() - if not win_ok(sbar_win) then sbar_win, sbar_buf = nil, nil end - if not win_ok(cbar_win) then cbar_win, cbar_buf = nil, nil end + if not win_ok(sbar_win) then + sbar_win, sbar_buf = nil, nil + end + if not win_ok(cbar_win) then + cbar_win, cbar_buf = nil, nil + end end -- click zones for the control bar. Each entry: { line_1based, dcol_start, dcol_end, fn } @@ -354,7 +368,10 @@ local function dap_state() end local function action(name) - return function() local a = M.actions(); if a[name] then a[name]() end end + return function() + local a = M.actions() + if a[name] then a[name]() end + end end -- Returns the user's configured key for an action, or the fallback default @@ -388,11 +405,14 @@ local function build_control_text(icon, actual_key, label, italic) local function add(s) parts[#parts + 1] = s end local function col() return #table.concat(parts) end - local inline = actual_key and label and #actual_key == 1 - and #label >= 1 - and label:sub(1, 1):lower() == actual_key:lower() + local inline = actual_key + and label + and #actual_key == 1 + and #label >= 1 + and label:sub(1, 1):lower() == actual_key:lower() - add(icon); add(" ") + add(icon) + add(" ") add("(") local key_col = col() add(actual_key) @@ -459,21 +479,29 @@ local function render_sbar() vim.api.nvim_buf_clear_namespace(sbar_buf, bar_ns, 0, -1) safe_extmark(sbar_buf, 0, 0, { - end_row = 0, end_col = #sep, hl_group = "TurboDebugBarSeparator", + end_row = 0, + end_col = #sep, + hl_group = "TurboDebugBarSeparator", }) -- content row safe_extmark(sbar_buf, 1, 0, { - end_row = 1, end_col = #brand_text, hl_group = "TurboDebugBarBrand", + end_row = 1, + end_col = #brand_text, + hl_group = "TurboDebugBarBrand", }) if status_dw > 0 then local status_byte_start = #brand_text + pad_left safe_extmark(sbar_buf, 1, status_byte_start, { - end_row = 1, end_col = status_byte_start + #status_msg, hl_group = "TurboDebugBarStatus", + end_row = 1, + end_col = status_byte_start + #status_msg, + hl_group = "TurboDebugBarStatus", }) end local chip_byte_start = #content_line - #state_chip safe_extmark(sbar_buf, 1, chip_byte_start, { - end_row = 1, end_col = #content_line, hl_group = state_hl, + end_row = 1, + end_col = #content_line, + hl_group = state_hl, }) end @@ -504,17 +532,27 @@ local function render_cbar() local continue_label = has_session and "continue" or "start" local terminate_label = has_session and "terminate" or "quit" + -- stylua: ignore start local controls = { { id = "continue", icon = continue_icon, default = "c", label = continue_label, italic = nil, fn = action("continue") }, { id = "step_over", icon = ICON.step_over, default = "s", label = "step", italic = "over", fn = action("step_over") }, { id = "step_into", icon = ICON.step_into, default = "d", label = "descend", italic = "into", fn = action("step_into") }, { id = "step_out", icon = ICON.step_out, default = "r", label = "return", italic = "out", fn = action("step_out") }, } + -- stylua: ignore end -- (R)estart is only meaningful during an active session if has_session then - controls[#controls + 1] = { id = "restart", icon = ICON.restart, default = "R", label = "restart", italic = nil, fn = action("restart") } + controls[#controls + 1] = + { id = "restart", icon = ICON.restart, default = "R", label = "restart", italic = nil, fn = action("restart") } end - controls[#controls + 1] = { id = "terminate", icon = ICON.stop, default = "q", label = terminate_label, italic = nil, fn = action("terminate") } + controls[#controls + 1] = { + id = "terminate", + icon = ICON.stop, + default = "q", + label = terminate_label, + italic = nil, + fn = action("terminate"), + } local gap = " " local function build_controls(include_italic) @@ -534,9 +572,7 @@ local function render_cbar() parts[#parts + 1] = txt spans[#spans + 1] = { prefix_len, prefix_len + #txt, "TurboDebugBarCtrl" } key_spans[#key_spans + 1] = { prefix_len + kcol, prefix_len + kend } - if icol then - italic_spans[#italic_spans + 1] = { prefix_len + icol, prefix_len + iend } - end + if icol then italic_spans[#italic_spans + 1] = { prefix_len + icol, prefix_len + iend } end zones[#zones + 1] = { prefix_len, prefix_len + #txt, c.fn } end end @@ -546,12 +582,10 @@ local function render_cbar() -- Try with italic qualifiers first; fall back to the compact form if -- the label row would overflow the available width. Min 1-char padding -- on each side of the controls-plus-help block. - local controls_text, ctrl_spans, ctrl_key_spans, ctrl_italic_spans, ctrl_zones_bytes = - build_controls(true) + local controls_text, ctrl_spans, ctrl_key_spans, ctrl_italic_spans, ctrl_zones_bytes = build_controls(true) local controls_dw = vim.fn.strdisplaywidth(controls_text) if controls_dw + help_dw + 2 > width then - controls_text, ctrl_spans, ctrl_key_spans, ctrl_italic_spans, ctrl_zones_bytes = - build_controls(false) + controls_text, ctrl_spans, ctrl_key_spans, ctrl_italic_spans, ctrl_zones_bytes = build_controls(false) controls_dw = vim.fn.strdisplaywidth(controls_text) end @@ -570,30 +604,40 @@ local function render_cbar() vim.api.nvim_buf_clear_namespace(cbar_buf, bar_ns, 0, -1) safe_extmark(cbar_buf, 1, 0, { - end_row = 1, end_col = #sep, hl_group = "TurboDebugBarSeparator", + end_row = 1, + end_col = #sep, + hl_group = "TurboDebugBarSeparator", }) -- control labels on row 0 (content row) local ctrl_byte_offset = ctrl_pad_left for _, span in ipairs(ctrl_spans) do safe_extmark(cbar_buf, 0, ctrl_byte_offset + span[1], { - end_row = 0, end_col = ctrl_byte_offset + span[2], hl_group = span[3], + end_row = 0, + end_col = ctrl_byte_offset + span[2], + hl_group = span[3], }) end for _, span in ipairs(ctrl_key_spans) do safe_extmark(cbar_buf, 0, ctrl_byte_offset + span[1], { - end_row = 0, end_col = ctrl_byte_offset + span[2], hl_group = "TurboDebugBarKey", + end_row = 0, + end_col = ctrl_byte_offset + span[2], + hl_group = "TurboDebugBarKey", }) end for _, span in ipairs(ctrl_italic_spans) do safe_extmark(cbar_buf, 0, ctrl_byte_offset + span[1], { - end_row = 0, end_col = ctrl_byte_offset + span[2], hl_group = "TurboDebugBarItalic", + end_row = 0, + end_col = ctrl_byte_offset + span[2], + hl_group = "TurboDebugBarItalic", }) end local help_byte_start = #content_line - #help_text safe_extmark(cbar_buf, 0, help_byte_start, { - end_row = 0, end_col = #content_line, hl_group = "TurboDebugBarCtrl", + end_row = 0, + end_col = #content_line, + hl_group = "TurboDebugBarCtrl", }) cbar_zones = {} @@ -601,14 +645,13 @@ local function render_cbar() local byte_start = ctrl_byte_offset + z[1] local byte_end = ctrl_byte_offset + z[2] local dcol_start = vim.fn.strdisplaywidth(content_line:sub(1, byte_start)) - local dcol_end = vim.fn.strdisplaywidth(content_line:sub(1, byte_end)) + local dcol_end = vim.fn.strdisplaywidth(content_line:sub(1, byte_end)) -- content is on buffer row 0 = window line 1 cbar_zones[#cbar_zones + 1] = { 1, dcol_start, dcol_end, z[3] } end local help_dcol_start = vim.fn.strdisplaywidth(content_line:sub(1, help_byte_start)) - local help_dcol_end = vim.fn.strdisplaywidth(content_line) - cbar_zones[#cbar_zones + 1] = { 1, help_dcol_start, help_dcol_end, - function() require("turbo-debug.help").open() end } + local help_dcol_end = vim.fn.strdisplaywidth(content_line) + cbar_zones[#cbar_zones + 1] = { 1, help_dcol_start, help_dcol_end, function() require("turbo-debug.help").open() end } end local function render_bars() @@ -652,8 +695,9 @@ local function setup_bar_win(win) -- might pick up (with laststatus=2 every window gets one) blends -- into the bar's Normal bg instead of rendering in the dark default -- StatusLine color — which was the "wrong color border below top bar". - vim.wo[win].winhighlight = "Normal:TurboDebugBar,EndOfBuffer:TurboDebugBar,StatusLine:TurboDebugBar,StatusLineNC:TurboDebugBar" - vim.wo[win].statusline = " " -- empty content; hl makes it invisible + vim.wo[win].winhighlight = + "Normal:TurboDebugBar,EndOfBuffer:TurboDebugBar,StatusLine:TurboDebugBar,StatusLineNC:TurboDebugBar" + vim.wo[win].statusline = " " -- empty content; hl makes it invisible vim.wo[win].winfixheight = true vim.wo[win].list = false vim.wo[win].cursorline = false @@ -671,9 +715,7 @@ local function bounce_out(buf) buffer = buf, callback = function() vim.schedule(function() - if vim.api.nvim_get_current_buf() == buf then - pcall(vim.cmd, "wincmd p") - end + if vim.api.nvim_get_current_buf() == buf then pcall(vim.cmd, "wincmd p") end end) end, }) @@ -700,9 +742,7 @@ local function open_bars() bounce_out(sbar_buf) end - if vim.api.nvim_win_is_valid(caller_win) then - pcall(vim.api.nvim_set_current_win, caller_win) - end + if vim.api.nvim_win_is_valid(caller_win) then pcall(vim.api.nvim_set_current_win, caller_win) end -- control bar (bottom) if not (win_ok(cbar_win)) then @@ -719,9 +759,7 @@ local function open_bars() bounce_out(cbar_buf) end - if vim.api.nvim_win_is_valid(caller_win) then - pcall(vim.api.nvim_set_current_win, caller_win) - end + if vim.api.nvim_win_is_valid(caller_win) then pcall(vim.api.nvim_set_current_win, caller_win) end render_bars() end @@ -735,15 +773,12 @@ local function pin_dapui_sizes() local pane_wins = {} for_each_dapui_pane(function(buf, ft) local wins = vim.fn.win_findbuf(buf) - if wins and win_ok(wins[1]) then - pane_wins[ft] = wins[1] - end + if wins and win_ok(wins[1]) then pane_wins[ft] = wins[1] end end) -- Console to configured height if pane_wins.dapui_console then - local target = config.opts.console_height - or math.max(8, math.floor(vim.o.lines * 0.2)) + local target = config.opts.console_height or math.max(8, math.floor(vim.o.lines * 0.2)) safe_set_height(pane_wins.dapui_console, target) end @@ -790,12 +825,8 @@ end -- subsequent pin_dapui_sizes() call redistributes it across the dapui -- panes according to our initial proportions. local function ensure_bar_heights() - if win_ok(sbar_win) and vim.api.nvim_win_get_height(sbar_win) ~= 2 then - safe_set_height(sbar_win, 2) - end - if win_ok(cbar_win) and vim.api.nvim_win_get_height(cbar_win) ~= 2 then - safe_set_height(cbar_win, 2) - end + if win_ok(sbar_win) and vim.api.nvim_win_get_height(sbar_win) ~= 2 then safe_set_height(sbar_win, 2) end + if win_ok(cbar_win) and vim.api.nvim_win_get_height(cbar_win) ~= 2 then safe_set_height(cbar_win, 2) end end -- Sum the heights of all currently-open dapui pane windows. Used to detect @@ -828,6 +859,7 @@ local function set_dapui_window_opts(win) -- rendered gutter), and the `~` end-of-buffer markers — these are pure -- noise in debug-info panes where there's no file to navigate. if vim.api.nvim_win_is_valid(win) then + -- stylua: ignore start vim.wo[win].wrap = true vim.wo[win].breakindent = true vim.wo[win].linebreak = true @@ -845,6 +877,7 @@ local function set_dapui_window_opts(win) -- first render (not just after the first WinEnter/WinLeave cycle). -- setup_active_win_highlights overwrites on focus change. vim.wo[win].winhighlight = DAPUI_WINHL_INACTIVE + -- stylua: ignore end end end @@ -896,19 +929,18 @@ local function ensure_dapui() patch_format_value() local dap = require("dap") - dap.listeners.after.event_initialized["turbo-debug"] = function() - require("dapui").open() - end + dap.listeners.after.event_initialized["turbo-debug"] = function() require("dapui").open() end -- K = hover during a live dap session (global, not per-buffer) dap.listeners.after.event_initialized["turbo-debug-hover"] = function() - vim.keymap.set("n", "K", function() - require("dap.ui.widgets").hover(nil, { border = "rounded" }) - end, { silent = true, desc = "DAP hover" }) - end - dap.listeners.after.event_terminated["turbo-debug-hover"] = function() - pcall(vim.keymap.del, "n", "K") + vim.keymap.set( + "n", + "K", + function() require("dap.ui.widgets").hover(nil, { border = "rounded" }) end, + { silent = true, desc = "DAP hover" } + ) end + dap.listeners.after.event_terminated["turbo-debug-hover"] = function() pcall(vim.keymap.del, "n", "K") end -- console refresh: trailing-edge throttled so chatty programs don't DOS us dap.listeners.after.event_output["turbo-debug-redraw"] = schedule_console_redraw @@ -927,9 +959,7 @@ local function ensure_dapui() local ok, placed = pcall(vim.fn.sign_getplaced, buf, { group = "*" }) if ok and placed and placed[1] then for _, s in ipairs(placed[1].signs or {}) do - if s.name == "DapStopped" then - pcall(vim.fn.sign_unplace, s.group or "", { buffer = buf, id = s.id }) - end + if s.name == "DapStopped" then pcall(vim.fn.sign_unplace, s.group or "", { buffer = buf, id = s.id }) end end end end @@ -948,11 +978,11 @@ local function ensure_dapui() -- event_stopped/event_continued leaves queued messages behind and -- the bar shows stale state until the next render. local function refresh_bars() vim.schedule(render_bars) end - dap.listeners.after.event_initialized["turbo-debug-bar"] = refresh_bars - dap.listeners.after.event_stopped["turbo-debug-bar"] = refresh_bars - dap.listeners.after.event_continued["turbo-debug-bar"] = refresh_bars - dap.listeners.after.event_terminated["turbo-debug-bar"] = refresh_bars - dap.listeners.after.event_exited["turbo-debug-bar"] = refresh_bars + dap.listeners.after.event_initialized["turbo-debug-bar"] = refresh_bars + dap.listeners.after.event_stopped["turbo-debug-bar"] = refresh_bars + dap.listeners.after.event_continued["turbo-debug-bar"] = refresh_bars + dap.listeners.after.event_terminated["turbo-debug-bar"] = refresh_bars + dap.listeners.after.event_exited["turbo-debug-bar"] = refresh_bars vim.api.nvim_create_autocmd("User", { pattern = "DapProgressUpdate", group = vim.api.nvim_create_augroup("TurboDebugDapProgress", { clear = true }), @@ -985,11 +1015,11 @@ local function ensure_dapui() -- Our extmark sign at priority 500 always wins, guaranteeing the -- paused line has the finger-pointing icon in the gutter. pcall(vim.api.nvim_buf_set_extmark, buf, ip_ns, line, 0, { - sign_text = "\xf0\x9f\x91\x89", -- 👉 U+1F449 + sign_text = "\xf0\x9f\x91\x89", -- 👉 U+1F449 sign_hl_group = "DapStopped", virt_text = { { " " .. ICON.ip_left .. " ", "DapStopped" }, - { reason, "TurboDebugIPReason" }, + { reason, "TurboDebugIPReason" }, }, virt_text_pos = "eol", hl_mode = "combine", @@ -997,9 +1027,9 @@ local function ensure_dapui() }) end, 50) end - dap.listeners.after.event_continued["turbo-debug-ip"] = function() clear_ip() end + dap.listeners.after.event_continued["turbo-debug-ip"] = function() clear_ip() end dap.listeners.before.event_terminated["turbo-debug-ip"] = function() clear_ip() end - dap.listeners.before.event_exited["turbo-debug-ip"] = function() clear_ip() end + dap.listeners.before.event_exited["turbo-debug-ip"] = function() clear_ip() end -- Clear the Console pane on session start/restart. -- @@ -1029,8 +1059,8 @@ local function ensure_dapui() end -- Fire before every session-starting verb so old scrollback is gone -- before the new session's output begins. - dap.listeners.before.launch["turbo-debug-clear-console"] = clear_console - dap.listeners.before.attach["turbo-debug-clear-console"] = clear_console + dap.listeners.before.launch["turbo-debug-clear-console"] = clear_console + dap.listeners.before.attach["turbo-debug-clear-console"] = clear_console dap.listeners.before.restart["turbo-debug-clear-console"] = clear_console -- Default-collapse scopes the user considers noisy (Registers on @@ -1056,12 +1086,12 @@ local function ensure_dapui() -- recognition. REPL is iconless per user preference (its prompt is -- already visually distinct enough). local titles = { - dapui_scopes = " " .. ICON.scopes .. " Scopes", - dapui_watches = " " .. ICON.watches .. " Watches", - dapui_stacks = " " .. ICON.stacks .. " Call Stack", + dapui_scopes = " " .. ICON.scopes .. " Scopes", + dapui_watches = " " .. ICON.watches .. " Watches", + dapui_stacks = " " .. ICON.stacks .. " Call Stack", dapui_breakpoints = " " .. ICON.breakpoints .. " Breakpoints", - dapui_console = " " .. ICON.terminal .. " Console", - ["dap-repl"] = " REPL", + dapui_console = " " .. ICON.terminal .. " Console", + ["dap-repl"] = " REPL", } vim.api.nvim_create_autocmd("FileType", { pattern = { "dapui_*", "dap-repl" }, @@ -1118,10 +1148,7 @@ source_window = function() if not vim.api.nvim_win_is_valid(w) then return false end if vim.api.nvim_win_get_config(w).relative ~= "" then return false end local ft = vim.bo[vim.api.nvim_win_get_buf(w)].filetype or "" - return ft ~= "" - and not ft:match("^dapui_") - and ft ~= "dap-repl" - and ft ~= "TurboDebugBar" + return ft ~= "" and not ft:match("^dapui_") and ft ~= "dap-repl" and ft ~= "TurboDebugBar" end if is_source(cur) then return cur end for _, w in ipairs(vim.api.nvim_list_wins()) do @@ -1136,9 +1163,7 @@ end -- already returned focus — the picker ends up unfocused. Actually shift. local function focus_source_win() local w = source_window() - if w and w ~= vim.api.nvim_get_current_win() then - pcall(vim.api.nvim_set_current_win, w) - end + if w and w ~= vim.api.nvim_get_current_win() then pcall(vim.api.nvim_set_current_win, w) end end local function run_in_source(fn) @@ -1162,7 +1187,10 @@ end local function smart_continue() local dap = require("dap") - if dap.session() then dap.continue(); return end + if dap.session() then + dap.continue() + return + end run_in_source(function() if config.opts.stop_on_entry_when_no_breakpoints ~= false and not has_any_breakpoint() then launch_with_stop_on_entry() @@ -1198,18 +1226,22 @@ local function setup_actions() local pb = require("persistent-breakpoints.api") actions_cache = { - continue = smart_continue, - step_over = function() step_or_launch(dap.step_over) end, - step_into = function() step_or_launch(dap.step_into) end, - step_out = function() step_or_launch(dap.step_out) end, + continue = smart_continue, + step_over = function() step_or_launch(dap.step_over) end, + step_into = function() step_or_launch(dap.step_into) end, + step_out = function() step_or_launch(dap.step_out) end, run_to_cursor = function() dap.run_to_cursor() end, - restart = function() dap.restart() end, - terminate = smart_terminate, + restart = function() dap.restart() end, + terminate = smart_terminate, help = function() - if help.is_open() then help.close() else help.open() end + if help.is_open() then + help.close() + else + help.open() + end end, - breakpoint = function() pb.toggle_breakpoint() end, - cond_breakpoint = function() pb.set_conditional_breakpoint() end, + breakpoint = function() pb.toggle_breakpoint() end, + cond_breakpoint = function() pb.set_conditional_breakpoint() end, clear_breakpoints = function() pb.clear_all_breakpoints() end, watch = function() local expr @@ -1219,12 +1251,10 @@ local function setup_actions() else expr = vim.fn.expand("") end - if expr and expr ~= "" then - require("dapui").elements.watches.add(expr) - end + if expr and expr ~= "" then require("dapui").elements.watches.add(expr) end end, hover = function() require("dapui").eval() end, - eval = function() require("dapui").float_element("repl") end, + eval = function() require("dapui").float_element("repl") end, } return actions_cache end @@ -1278,9 +1308,7 @@ function M._install_for_buf(buf) end local opts = { buffer = buf, silent = true, nowait = true, desc = "turbo-debug: " .. name } pcall(vim.keymap.set, "n", key, acts[name], opts) - if visual_actions[name] then - pcall(vim.keymap.set, "v", key, acts[name], opts) - end + if visual_actions[name] then pcall(vim.keymap.set, "v", key, acts[name], opts) end end end end @@ -1291,9 +1319,7 @@ end -- clobbered by our subsequent del. local function unset_and_restore(buf, name, entry) pcall(vim.keymap.del, "n", entry.key, { buffer = buf }) - if visual_actions[name] then - pcall(vim.keymap.del, "v", entry.key, { buffer = buf }) - end + if visual_actions[name] then pcall(vim.keymap.del, "v", entry.key, { buffer = buf }) end if entry.prev_n then restore_mapping(buf, "n", entry.prev_n) end if entry.prev_v then restore_mapping(buf, "v", entry.prev_v) end end @@ -1321,9 +1347,7 @@ local function set_debug_chrome() end local function clear_debug_chrome() - if win_ok(active_win) then - vim.wo[active_win].winbar = saved_winbar or "" - end + if win_ok(active_win) then vim.wo[active_win].winbar = saved_winbar or "" end active_win = nil saved_winbar = nil end @@ -1335,8 +1359,11 @@ function M.enter() active = true -- Switch colorscheme BEFORE define_highlights so the TurboDebug* -- highlights derive their colors from the debug-mode theme. - if type(config.opts.colorscheme) == "string" and config.opts.colorscheme ~= "" - and vim.g.colors_name ~= config.opts.colorscheme then + if + type(config.opts.colorscheme) == "string" + and config.opts.colorscheme ~= "" + and vim.g.colors_name ~= config.opts.colorscheme + then saved_colorscheme = vim.g.colors_name pcall(vim.cmd.colorscheme, config.opts.colorscheme) end @@ -1384,13 +1411,10 @@ function M.enter() if not active then return end vim.schedule(function() invalidate_dead_bars() - if not (sbar_win and cbar_win) then - open_bars() - end + if not (sbar_win and cbar_win) then open_bars() end ensure_bar_heights() local dapui_total = compute_dapui_total_height() - if last_dapui_total_height ~= nil - and dapui_total ~= last_dapui_total_height then + if last_dapui_total_height ~= nil and dapui_total ~= last_dapui_total_height then pin_dapui_sizes() dapui_total = compute_dapui_total_height() end @@ -1465,7 +1489,11 @@ function M.exit() end function M.toggle() - if active then M.exit() else M.enter() end + if active then + M.exit() + else + M.enter() + end end function M.is_active() return active end @@ -1478,7 +1506,9 @@ M._test = { build_control_text = build_control_text, define_highlights = define_highlights, source_window = function() return source_window() end, - set_bars = function(sw, sb, cw, cb) sbar_win, sbar_buf, cbar_win, cbar_buf = sw, sb, cw, cb end, + set_bars = function(sw, sb, cw, cb) + sbar_win, sbar_buf, cbar_win, cbar_buf = sw, sb, cw, cb + end, get_bars = function() return sbar_win, sbar_buf, cbar_win, cbar_buf end, get_sbar_zones = function() return {} end, get_cbar_zones = function() return cbar_zones end, diff --git a/plugin/turbo-debug.lua b/plugin/turbo-debug.lua index ad0228d..e287325 100644 --- a/plugin/turbo-debug.lua +++ b/plugin/turbo-debug.lua @@ -12,9 +12,7 @@ vim.pack.add({ "theHamsta/nvim-dap-virtual-text" }) vim.pack.add({ "Weissle/persistent-breakpoints.nvim" }) -- register which-key group if available -pcall(function() - require("which-key").add({ { "d", group = "debug" } }) -end) +pcall(function() require("which-key").add({ { "d", group = "debug" } }) end) -- Deferred auto-setup. Must be deferred (not called synchronously) because -- the vim.pack.add calls above might not have finished registering the diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..7bfa1e5 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,17 @@ +# stylua.toml +# A modern, idiomatic configuration optimized for Neovim/Luau ecosystems. + +# General Formatting +column_width = 120 # Provides enough horizontal space for modern monitors without wrapping too early. +line_endings = "Unix" # Enforces LF (\n) line endings to prevent cross-platform Git issues. + +# Indentation +indent_type = "Spaces" # Spaces are the modern standard over tabs in Lua ecosystems. +indent_width = 2 # 2 spaces prevent deep nesting (common in Lua tables/callbacks) from drifting too far right. + +# Quotes & Strings +quote_style = "AutoPreferDouble" # Defaults to " but uses ' if it prevents needing escape characters. + +# Function Calls & Statements +call_parentheses = "Always" # Forces `require("module")` instead of `require "module"` for clarity. +collapse_simple_statement = "Always" # Allows simple statements (like `if not x then return end`) to live on one line. diff --git a/tests/helpers.lua b/tests/helpers.lua index 04c4317..8b153ad 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -1,21 +1,15 @@ local M = {} function M.assert_eq(a, b, msg) - if a ~= b then - error(string.format("FAIL: %s — expected %s, got %s", msg, vim.inspect(b), vim.inspect(a))) - end + if a ~= b then error(string.format("FAIL: %s — expected %s, got %s", msg, vim.inspect(b), vim.inspect(a))) end end function M.assert_true(v, msg) - if not v then - error(string.format("FAIL: %s — expected truthy, got %s", msg, vim.inspect(v))) - end + if not v then error(string.format("FAIL: %s — expected truthy, got %s", msg, vim.inspect(v))) end end function M.assert_false(v, msg) - if v then - error(string.format("FAIL: %s — expected falsy, got %s", msg, vim.inspect(v))) - end + if v then error(string.format("FAIL: %s — expected falsy, got %s", msg, vim.inspect(v))) end end return M diff --git a/tests/test_render_bars.lua b/tests/test_render_bars.lua index 6ea4940..b455611 100644 --- a/tests/test_render_bars.lua +++ b/tests/test_render_bars.lua @@ -21,14 +21,10 @@ local function make_bar_win() end local function cleanup(win) - if win and vim.api.nvim_win_is_valid(win) then - pcall(vim.api.nvim_win_close, win, true) - end + if win and vim.api.nvim_win_is_valid(win) then pcall(vim.api.nvim_win_close, win, true) end end -local function line(buf, i) - return vim.api.nvim_buf_get_lines(buf, i, i + 1, false)[1] or "" -end +local function line(buf, i) return vim.api.nvim_buf_get_lines(buf, i, i + 1, false)[1] or "" end -- build_control_text ------------------------------------------------------- @@ -120,8 +116,10 @@ do local content = line(buf, 0) -- italic qualifiers are "over", "into", "out" — on overflow they're omitted. -- At width 25 with 5+ controls, there's no way they all fit with italics. - assert_false(content:find(" over ") or content:find(" into ") or content:find(" out "), - "overflow: italic qualifiers absent at narrow width") + assert_false( + content:find(" over ") or content:find(" into ") or content:find(" out "), + "overflow: italic qualifiers absent at narrow width" + ) mode._test.set_bars(nil, nil, nil, nil) cleanup(win) end diff --git a/turbo-debug-spec.md b/turbo-debug-spec.md deleted file mode 100644 index 81a24ff..0000000 --- a/turbo-debug-spec.md +++ /dev/null @@ -1,1120 +0,0 @@ -# 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") -``` - -That's it. That's the whole setup. - -## Design Principles - -1. **Just works.** Install it, press `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") -``` - -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 - -`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): - -- `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 `` 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 `` 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) - │ - ├── 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 - │ ├── 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("")` 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")` -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 `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 `d` to `toggle()`. - -**Files:** -``` -lua/turbo-debug/init.lua -lua/turbo-debug/mode.lua -tests/test_mode.lua -tests/test_keys.lua -``` - -**Acceptance criteria:** -- `d` enters debug mode; all configured keys are active -- `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 `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 `` 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 `d` then `c` launches debugpy -- If a virtualenv is active, debugpy uses it -- Opening a `.go` file and pressing `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.