45 Commits
Author SHA1 Message Date
swaits 20dec9bc7b chore: release 0.2.2
Bug-fix release: Python program output now shows in the Console pane, and
breakpoints restore for files opened on the command line.

### Fixed

- **Python `print()` output appears in the Console pane.** The debugpy
  launch config now sets `console = "integratedTerminal"`, so debugpy
  routes stdout/stderr via `RunInTerminalRequest` into dapui's Console
  buffer (which is already registered as
  `dap.defaults.fallback.terminal_win_cmd`). Previously debugpy defaulted
  to `internalConsole` and emitted `OutputEvent`s that nvim-dap forwards
  to the REPL buffer — which turbo-debug's layout deliberately omits, so
  program output vanished. Interactive `input()` now also works.
- **Breakpoints restore for files opened on the command line.** `nvim
  foo.py` fires `BufReadPost` for `foo.py` before turbo-debug's deferred
  `setup()` runs, so `persistent-breakpoints.nvim`'s `BufReadPost`
  autocmd missed the startup buffer and saved breakpoints never came
  back. After setup, turbo-debug now sweeps already-loaded buffers once
  via `load_breakpoints()` (idempotent; placed after `sign_define` so
  the first paint uses turbo-debug's emoji signs instead of dap's
  default `B`).
2026-04-12 21:44:54 -06:00
swaits 5118c03f83 fix: python console output and startup breakpoint restoration
adapters/python: set `console = "integratedTerminal"` so debugpy routes stdout/stderr via RunInTerminalRequest into dapui's Console pane. Under the default (internalConsole), debugpy emits DAP OutputEvents that nvim-dap forwards to the REPL buffer — which turbo-debug's layout deliberately omits, so print() output vanished.

breakpoints: sweep already-loaded buffers with load_breakpoints() right after sign_define. plugin/turbo-debug.lua defers setup via vim.schedule so deps register first, which means BufReadPost has already fired for any file opened on the command line — persistent-breakpoints' autocmd misses it. The one-shot sweep is idempotent; placing it after sign_define avoids a first-paint flicker of dap's default 'B' sign.
2026-04-12 21:35:50 -06:00
swaits 31daa1582c chore: release 0.2.1
Language coverage release. 10 new DAP adapters bring built-in support from
12 to 22 languages, prioritized by language popularity and DAP adapter
maturity.

ADDED
- Dart (with optional Flutter) — auto-detects `flutter` on PATH and adds
  a "Launch Flutter app" configuration alongside the plain Dart file
  launch. Uses `dart debug_adapter` / `flutter debug_adapter`.
- Bash / Sh — `bash-debug-adapter` with automatic `bashdb_dir` probing
  from the mason package path.
- R — `vscDebugger` R-package integration. Preflights the package via
  `Rscript` and silently skips registration if it's missing.
- Elixir — `elixir-ls-debugger` with `mix_task` launch (default task
  `test`). Falls back to the mason package `debugger.sh` if the wrapper
  binary isn't on PATH.
- Haskell — `haskell-debug-adapter`.
- OCaml — `ocamlearlybird`.
- Zig / Nim / Crystal — routed through the existing `codelldb` adapter
  via filetype extension (no new adapter; just added filetypes to the
  existing codelldb configuration).
- F# — routed through the existing `netcoredbg` adapter (same
  `.dll`-launch flow as C#).

INTERNAL
- Extended `tests/test_adapters.lua` to cover the three new modular
  adapter modules (`dart`, `bash`, `r`).
- All new registrations follow the existing `find_executable` +
  `loader.register` pattern and silently skip when the required binary
  isn't found — matches the "no noise when prereqs missing" invariant of
  the previously-shipped adapters.

See CHANGELOG.md for the full 0.2.1 entry.
2026-04-12 20:44:47 -06:00
swaits f8cce8da37 feat: add 10 new language adapters
Adds Dart (with optional Flutter auto-detection), Bash (bash-debug-adapter with mason bashdb_dir probing), R (vscDebugger package preflight), Elixir (mix_task), Haskell, OCaml (ocamlearlybird), Zig/Nim/Crystal (via codelldb filetype extension), and F# (via netcoredbg filetype extension). Expands built-in adapter coverage from 12 to 22 languages.
2026-04-12 20:42:49 -06:00
swaits d062ac27ff chore: release 0.2.0
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`). Control bar always
  shows `(?) help`, so the auto-popup is no longer needed.
- Default layout changed: sidebar is 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 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),
  control bar (bottom, 2 rows, clickable flow buttons + `(?) help`
  right-anchored). Italic qualifiers on step controls auto-drop at
  narrow widths.
- Keymap-aware control bar — legend reflects user's actual remapped
  keys; disabled actions are omitted entirely.
- IP arrow markers on stopped lines: 👉 gutter sign (priority 500) +
  👈 REASON virtual text at EOL.
- Optional debug-mode colorscheme via `colorscheme` option.
- Theme-agnostic separator color (derives from WinBar.fg); global
  WinSeparator override during debug mode for consistent edge color.
- `clear_console_on_start` (default true) — wipe Console scrollback on
  session start while preserving the terminal channel.
- `collapsed_scopes` (default {"Registers"}) — auto-collapse noisy
  scopes that adapters don't flag expensive.
- `max_value_width` (default 2000) — per-line truncation for values in
  Scopes/Watches.
- `console_refresh_ms` (default 50) — trailing-edge Console repaint
  throttle.
- `sidebar` option — "left" (default) or "right".
- `active_window_highlight` (default true) — dim inactive dapui pane
  borders.
- `W` and `K` now work in visual mode (operate on the selection).
- Source window focused before DAP jumps on event_stopped.

CHANGED
- Bars are real splits (not overlapping floats).
- pin_dapui_sizes() only re-pins on external geometry changes (tabline
  toggle); user manual pane drags survive.
- ensure_dapui() runs ONCE per plugin lifetime, avoiding "Debug adapter
  didn't respond" timeouts on toggle.
- Bars clamp to exactly 2 rows on every resize (no stray EOB `~`).
- setup() is fully idempotent; plugin/turbo-debug.lua's deferred
  auto-setup yields to explicit user setup({...}).
- Copyright holder: Stephen Waits <steve@waits.net>.

FIXED
- Bars recreate if killed by shrink-below-minimum resize or tabline
  appear/disappear.
- Inherited winbar cleared on bar windows (no phantom separator rows).
- Layout re-asserts after `wincmd =`.
- Sidebar proportions stable across re-entry.
- Bar separators bright on both outward edges.
- Deferred dependency loading waits for vim.pack.add to finalize.

INTERNAL
- Extracted 10+ helper functions in mode.lua; deduped ~25 call sites.
- Data-driven sign definitions and Python interpreter probing.
- Dead code removed: unused reposition_bars, vestigial sbar_zones/
  handle_sbar_click.
- Added regression tests: test_render_bars, test_highlights_cs,
  test_helpers.
- stylua.toml added; all Lua code formatted.

See CHANGELOG.md for the full 0.2.0 entry.
2026-04-12 20:00:59 -06:00
swaits 85ff3b1688 refactor: extract helpers, dedup patterns, add regression tests
Mechanical cleanup pass, behavior-preserving:
- remove dead code: reposition_bars (unused), sbar click infrastructure
  (zones never populated, handler iterated empty list)
- extract helpers in mode.lua: win_ok, buf_ok, is_dapui_pane_ft,
  for_each_dapui_pane (asymmetric filter preserved), safe_extmark,
  bar_set_lines, safe_set_height, if_still_active, invalidate_dead_bars,
  focus_source_win, unset_and_restore
- inline install_cs_listener inside define_highlights to dedup the three
  ColorScheme augroup+autocmd triples (italic, key, sep)
- data-drive sign_define in breakpoints.lua
- data-drive python interpreter candidate probing
- add tests/test_render_bars.lua pinning sbar/cbar output dimensions,
  zones, and overflow fallback behavior
- add tests/test_highlights_cs.lua verifying ColorScheme augroups and
  highlight group existence / re-apply on event
- add tests/test_helpers.lua unit-testing keyof and source_window
- expose M._test (direct function refs) so render/highlight internals are
  testable in headless mode
2026-04-12 19:42:54 -06:00
swaits 7a21d372ff fix: deferred dep loading + idempotent setup
Two related bugs surfaced when the user tried adding `setup({...})` to
their init.lua alongside our auto-setup:

1. **`inmemory.bps = nil` crash.** When a user's `init.lua` does
   `vim.pack.add(..., turbo-debug)` and then calls `setup({...})`
   synchronously right after, the nested `vim.pack.add` calls inside
   our own `plugin/turbo-debug.lua` (for dap/dapui/persistent-
   breakpoints) are DEFERRED by nvim's pack module — so the user's
   follow-up `require('persistent-breakpoints')` throws "module not
   found". On the previous `initialized = true` guard, the error set
   the flag but left setup half-complete; any later call short-
   circuited and `inmemory.bps` stayed `nil`, exploding the first
   breakpoint toggle (`api.lua:15: attempt to index field 'bps'`).

2. **Colorscheme override ignored.** Same root cause: user's
   `setup({ colorscheme = "nightfox" })` errored before reaching
   `config.merge`, so `config.opts.colorscheme` stayed `nil` and
   `M.enter` never switched themes.

Three changes that together fix both:

- **`init.lua` `setup()` self-defers** when `require('persistent-
  breakpoints')` fails, `vim.schedule`s itself once deps can load.
- **`plugin/turbo-debug.lua` auto-setup is now `vim.schedule`d** and
  skips entirely if the user's explicit `setup({...})` already ran
  (tracked via `M._setup_called`) — no more stomping user opts with
  a second `config.merge(nil)`.
- **`breakpoints.setup` is now fully idempotent**: dropped the
  `initialized` flag, since every call inside it
  (`persistent-breakpoints.setup`, `sign_define`, `nvim_set_hl`,
  `vim.keymap.set`) is safe to re-run.
2026-04-12 18:01:02 -06:00
swaits 98c943885a feat: keymap-aware legend, live dap.status, debug-mode colorscheme
Three things the user caught in testing:

1. **Legend keymap-aware.** The cbar labels used hardcoded tails
   ("tep", "escend", "eturn") that assumed the default first-letter
   keys. If a user configured `keys.step_over = "n"` it would render
   as "(n)tep" — garbage. And disabled keys (`false` in config) still
   showed up because `keyof` fell back to the default letter.

   Now `keyof` returns nil for disabled actions (caller SKIPS rendering
   the entry) and `build_control_text` auto-picks between inline form
   "(s)tep" when the actual key matches the label's first letter and
   prefix form "(n) step" when it doesn't. Controls carry their full
   label ("step") plus the canonical default key — the render figures
   out which form fits the user's current keymap.

2. **dap.status() lag.** `dap.status()` polls a message queue one at
   a time per call (nvim-dap/lua/dap/progress.lua:50). We were only
   polling on event_stopped/continued/terminated/etc., so queued
   state strings drained behind reality — user saw "Running" when
   already paused, "All threads stopped" one step late, etc. Now we
   also subscribe to the `User DapProgressUpdate` autocmd that dap
   fires on EVERY status update, keeping the bar in sync.

3. **Debug-mode colorscheme.** New `config.opts.colorscheme` option
   (default nil). If set to a string, M.enter switches to it BEFORE
   define_highlights (so our TurboDebug* groups derive from the new
   theme's colors); M.exit restores the previous `vim.g.colors_name`.
2026-04-12 17:38:20 -06:00
swaits acc698ad80 feat: italic qualifiers on step controls + unified WinSeparator color
Control-bar step labels now show the standard debugger qualifier after
the key mnemonic, styled italic:

  (s)tep over       (was (s)tep)
  (d)escend into    (was (d)escend)
  (r)eturn out      (was (r)eturn)

The qualifier is rendered with a new `TurboDebugBarItalic` highlight
(Comment fg + italic). When the cbar can't fit all controls with
qualifiers, `build_controls(true)` is retried as `build_controls(false)`
and the qualifiers are dropped — the compact labels survive.

Also overrides the global `WinSeparator` highlight to the bright
TurboDebugBarSeparator color during debug mode, so every split divider
in the editor — between dapui panes, between source and console,
between our bars and their neighbors — renders one consistent chrome
color. Original WinSeparator captured on first apply, restored byte-
for-byte on M.exit.
2026-04-12 17:30:00 -06:00
swaits 8fbccb9391 fix: clamp bars to EXACTLY 2 rows, not just >= 2
`winfixheight` is a hint, not a rule. When the tabline disappears and
nvim has to put the reclaimed row SOMEWHERE, it can shove it into one
of our bars, growing it to 3 rows. Our buffer only has 2 lines, so
the 3rd row renders `~` — the stray EOB line the user saw between
the cbar's bottom sep and the lualine statusline.

Previously `ensure_bar_heights()` only corrected shrinkage (`< 2`).
Now it clamps in both directions (`~= 2`). When we shrink an over-
sized bar, the freed row has to go somewhere — the WinResized path's
pin_dapui_sizes() redistributes it across dapui panes proportionally.
2026-04-12 17:18:50 -06:00
swaits 1e19db2101 fix: 2-row bars, drop fillchars hack, re-pin dapui on tabline toggle
Three concrete fixes plus a documented architectural direction.

1. Bars are now 2 rows: sbar = sep + content (top border only), cbar =
   content + sep (bottom border only). The inward edges abut dapui's
   own chrome (pane winbar + nvim's native horiz separator), which
   serve as the inner visual boundary. Cuts each bar's vertical
   footprint by one row.

2. Reverts the `fillchars.horiz = " "` hack from c1c3143. That fix
   just converted nvim's dark phantom separator into a blank-but-still-
   present row, still wasting vertical space. With 2-row bars the
   inward edge is dapui/nvim-provided chrome by design — no need to
   hide it.

3. New bug: closing a second buffer made bufferline's tabline
   disappear, nvim gave the reclaimed row to the Breakpoints pane,
   and because dapui has no WinResized listener (confirmed by reading
   dapui/windows/layout.lua:82-138) its `win_states` went stale —
   Breakpoints ended up 1 row taller than its buffer, rendering a
   stray `~` EOB marker. Fix: track total dapui area in
   `last_dapui_total_height`; on WinResized, if the total changed
   (tabline toggled), call pin_dapui_sizes() to redistribute. User-
   internal resizes (dragging a pane border within dapui) don't
   change the total, so their customization still survives.

Plan/critique saved at ~/.claude/plans/snoopy-shimmying-puzzle.md —
the honest architectural verdict is that splits are the wrong
primitive for non-interactive chrome bars (3 parallel agents
converged on this). If 2-row bars still hit bugs, the documented
migration path is 1-row splits with `winbar = '%!v:lua.foo()'`
expression rendering.
2026-04-12 16:59:00 -06:00
swaits c1c3143532 fix: hide nvim's horiz window separator to stop the phantom dark row
Pixel analysis of the user's screenshot confirmed the "second darker
line" below/above our bars is nvim's built-in horizontal window
separator. When `laststatus=3` (globalstatus=true), nvim draws `─`
between every pair of stacked windows using `fillchars.horiz` painted
with `WinSeparator.fg`. Nordfox's WinSeparator.fg is #232831 — dark —
which reads as a dim phantom row adjacent to our own bright sep.

On debug-mode enter, replace horiz/horizup/horizdown with space so nvim
has nothing visible to draw; our bars' own top/bottom separator rows
become the only visible chrome between them and neighboring panes. On
exit, restore the user's original fillchars.
2026-04-12 16:39:47 -06:00
swaits 53cc0802c3 fix: clear inherited winbar on bar windows
The source window has `winbar = " %f"` set by set_debug_chrome for the
filename chip. When open_bars does `:topleft split` / `:botright split`
from that window, the new bar windows inherit its winbar. The inherited
winbar renders an extra chrome row at the TOP of each bar window,
ABOVE our Row 0 separator — showing the scratch buffer's empty
filename, which reads as a phantom empty row sandwiched between our
separator and whatever sits above/below.

Explicitly `vim.wo[win].winbar = ""` in setup_bar_win first-thing, so
the bar shows only its own 3 rows (top sep / content / bottom sep)
with no inherited chrome.
2026-04-12 16:21:23 -06:00
swaits c02021ad5e fix: 3-row bars with both edges bright + un-squish on tabline toggle
Bars now render top+bottom bright separators (not just outward edge) so
the sbar's bottom and cbar's top read as proper chrome regardless of
what the adjacent dapui pane puts in its winbar. Fixes the "top bar's
bottom border is the wrong color" reading where the sbar's inward edge
was falling back to dapui's grey pane-title row.

Adds `ensure_bar_heights()` to un-squish bars that nvim compressed to 0
rows (not closed — just hidden) when bufferline's tabline reclaims a
row on buffer-count change. `winfixheight` is a preference, not a
guarantee; when the grid has to give up a row, nvim sometimes picks a
`winfixheight` window anyway. Wire into VimResized + WinResized so the
bar pops back on every geometry shift.

Also add `WinBar:WinBar,WinBarNC:WinBar` to the dapui winhighlight
strings (and apply it as an initial baseline in set_dapui_window_opts)
so pane titles stay bright on themes that dim WinBarNC.
2026-04-12 16:04:00 -06:00
swaitsandClaude Opus 4.6 1cdde2a177 fix: recover bars on WinResized, blend statusline into bar bg
Two regressions from the cleanup:

1. Bottom bar vanishing when bufferline's tabline appears. Tabline
   toggle fires WinResized but not VimResized; my cleanup left
   WinResized doing only redraw!, no bar-recovery, so once a bar was
   nuked by a grid-shrink it stayed dead. WinResized handler now does
   a cheap O(1) "are both bars valid?" check and only runs recovery
   when one's missing. Common case = 1 nil check + redraw, no extra
   work. Uncommon case = recreate the nuked bar.

2. "Wrong color border below top bar" — that was the per-window
   StatusLine row (laststatus=2 gives every window a statusline)
   rendering in its default dark StatusLine highlight. My winhighlight
   only mapped Normal + EndOfBuffer, leaving StatusLine untouched.
   Added StatusLine:TurboDebugBar and StatusLineNC:TurboDebugBar so
   any statusline row on a bar window blends into the bar's Normal
   bg. Also set `statusline = " "` on bar windows so it renders as
   empty (no "[No Name]" default).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:46:58 -06:00
swaitsandClaude Opus 4.6 ffe561402f fix: theme-agnostic bar separator + 2-row bars to avoid stacked borders
Two related changes to address "two separator lines stacked between
the bars and the dapui panes":

1. 2-row bars again. Only the OUTWARD edge of each bar draws a
   separator (sep/content for status bar, content/sep for control
   bar). The INWARD edge yields to dapui's pane winbar row — dapui
   always renders a 1-row header with the pane title
   (🧊 Call Stack, 👀 Watches, etc.) and that acts as the visual
   divider. Drawing our own separator there was producing two
   bright rows of chrome stacked on top of each other.

2. Theme-agnostic separator color. User's objection was that it
   looked dark in their nightfox theme; before I flipped it to
   FloatBorder which happened to be bright in nightfox, but that's
   not portable to other themes. Now derive TurboDebugBarSeparator.fg
   at runtime from WinBar.fg — the color every theme uses for
   pane-title chrome, which is specifically what dapui renders for
   its pane headers. Theme-agnostic: in ANY theme, our bar edges will
   match that theme's pane-title chrome color. Re-applied on
   ColorScheme so theme swaps stay in sync. Falls back to
   WinSeparator link if WinBar has no fg (rare).

Removed `default = true` from the derived set_hl call — with it, the
placeholder link I set in the default highlights table wins because
"default" means "only set if not already defined". Unconditional set
is needed for the derive to actually take effect.

Also: Watches icon 👁️ → 👀 per user preference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:42:48 -06:00
swaitsandClaude Opus 4.6 c8f37bcb7e fix: bar separators use FloatBorder (bright), 3-row bars frame both edges
User kept complaining the status bar's separator looked dark while
dapui's pane dividers looked bright. I had WinSeparator vs
FloatBorder backwards.

Actual values in nightfox (and most dark colorschemes):
  WinSeparator.fg = #131721 (very dark — what I was using, the dark
                             look user objected to)
  FloatBorder.fg  = #71839B (bright grey — what user wants, matches
                             WinBar.fg which is what dapui's pane
                             header text renders in)

Switched TurboDebugBarSeparator back to FloatBorder. Also restored
3-row bars for BOTH status and control bars so each has separators
on BOTH top AND bottom edges in the bright color. Symmetric framing
top to bottom of the debug UI.

Also: Watches pane icon switched from 👁️ (eye, U+1F441+VS16) to 👀
(eyes, U+1F440) per user preference. Supplementary-plane emoji with
no VS16 dependency, always renders as color emoji.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:36:47 -06:00
swaitsandClaude Opus 4.6 c420acd599 cleanup: remove redundant layout-recovery defenses
Now that WinResized does a simple `redraw!` on every window-geometry
change, the accumulated defensive autocmds are redundant. Removed:

- `OptionSet showtabline` handler — when bufferline uses an expr
  tabline format, the option itself never changes, so this never
  fired for the primary case we cared about. When it DOES fire
  (explicit `:set showtabline=N`), a window geometry change
  immediately follows and WinResized handles the redraw.
- `BufEnter/BufWinEnter/TabEnter` fallback — was guarding against
  bars going missing on buffer transitions. In practice bars only
  get killed on editor resize (VimResized covers that), not on
  buffer changes.
- Orphan bar cleanup in recover_chrome — earlier theory that the
  "duplicate turbo-debug" rows came from stale windows. User
  correctly diagnosed it as leftover render pixels; WinResized's
  redraw! is the actual fix. No orphans to clean up.
- Internal `redraw!` in recover_chrome — redundant because when
  recover_chrome runs (VimResized), it creates splits which trigger
  WinResized which does the redraw for us.

What's left:
- VimResized: recreates bars killed by editor shrink + re-renders
- WinResized: single `redraw!` to clear stale pixels on any shift

Cleaner and easier to reason about.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:31:49 -06:00
swaitsandClaude Opus 4.6 33d9675318 fix: WinResized forces redraw to clear stale pixels on tabline shift
The duplicate "turbo-debug" rows stacked at the top weren't from
orphan windows — they were leftover render pixels. When bufferline's
tabline disappears (buffer count drops back to 1), the tabline row
is reclaimed and every window shifts up one row. Nvim redraws at the
new positions but doesn't clear the pixels where the old render was,
so the content appears doubled.

OptionSet on showtabline doesn't fire in this case because
bufferline uses a %{% expr format for tabline — the tabline string
becomes empty but the showtabline option never changes. My earlier
BufEnter/TabEnter fallbacks miss it for the same reason. Only
WinResized reliably fires when windows shift from the reclaimed row.

New minimal WinResized autocmd: just schedules `redraw!` and does
nothing else. No re-pinning (user's manual resizes still stick), no
bar recreation (not needed for this case). Cheap and targeted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:27:48 -06:00
swaitsandClaude Opus 4.6 77be1ac578 fix: close orphan bars + force redraw on tabline-disappear
When bufferline removes the tabline row (buffer count drops back to 1
after stepping out of a secondary file), the recovery ran but the
screen showed DUPLICATE status bar rows stacked on top of each other.
Root cause: during the tabline-disappear → window-shift cascade, nvim
occasionally leaves a stale bar window while our recover path creates
a new one, and/or keeps stale pixels from the pre-shift layout.

Two defenses added to recover_chrome():

1. Walk all windows; close any with filetype=TurboDebugBar that we're
   NOT tracking in sbar_win/cbar_win. Any orphaned bar window from a
   partial layout transition gets nuked before we create new ones.

2. Call `redraw!` at the end of recovery to force a full screen
   refresh, clearing any leftover pixels from the previous layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:26:45 -06:00
swaitsandClaude Opus 4.6 475e14bfa7 fix: detect tabline appear/disappear, recreate killed bars
Two visual regressions:

1. bufferline.nvim dynamically toggles the tabline when the buffer
   count crosses 2 (stepping into a library file adds a buffer,
   showtabline flips from 0 to 2). That reserves a row at the top of
   the editor grid; every window shifts down and our bars can either
   get squeezed or outright killed. Previous VimResized handler only
   caught editor-level resizes, not the subtle tabline-toggle event.

   Now we listen to:
     - VimResized  — editor size change
     - OptionSet showtabline — bufferline / user flips showtabline
     - BufEnter/BufWinEnter/TabEnter — catch the case where bufferline
       computes its tabline string via %{%...%} which nvim renders
       without firing OptionSet. Guarded so it only runs recovery
       when a bar is actually missing.
   All three call the same recover_chrome() which recreates any bar
   that's gone and re-renders content. dapui sizes are not touched
   (user's manual resizes stick).

2. TurboDebugBarSeparator was linked to FloatBorder which reads
   darker than WinSeparator in most colorschemes. Dapui draws its own
   pane dividers with WinSeparator, producing a visible color seam
   between our bar's bottom edge and the adjacent dapui pane top.
   Relinked to WinSeparator so the bar edges match dapui's dividers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:18:58 -06:00
swaitsandClaude Opus 4.6 8dc460c0aa fix: forward-declare source_window so ensure_dapui can see it
source_window was defined around line 880 but referenced in the
before.event_stopped listener inside ensure_dapui (around line 730).
Lua closures only capture upvalues that existed as locals BEFORE
the closure was created — source_window fell back to a global
lookup which resolved to nil, breaking every attempt to start a
debug session.

Forward-declare `local source_window` at the top of the file and
assign (rather than declare) the implementation later. Both
ensure_dapui's listener and the later call sites now refer to the
same upvalue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:14:29 -06:00
swaitsandClaude Opus 4.6 372afcdd4f fix: focus source window before dap jumps on event_stopped
When stepping into a library file (e.g., Rust stdlib fmt.rs) or any
file not already open, dap uses its switchbuf logic to decide which
window to load the new source into. The 'uselast' variant targets the
current window if it looks like source (empty buftype or matching
filetype); otherwise it falls back to `winnr('#')`.

Problem: if the user pressed `s` / `d` / `r` while focus was on a
dapui pane (which is common — clicking into Scopes or Watches to
inspect, then stepping), or if the event fires while focus is on our
bar window, dap's current-window check fails and it dumps the new
source into whatever `winnr('#')` resolves to. Combined with
bufferline.nvim's tabline, this produces the chrome-breaking layout
shown in the screenshot.

Fix: register `dap.listeners.before.event_stopped` that shifts focus
to a known source window (via the existing source_window() helper)
BEFORE dap's jump handler runs. Dap's uselast then sees a real source
window as current and lands the new file there. Chrome stays intact.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:11:24 -06:00
swaitsandClaude Opus 4.6 c202038455 fix: remove winfixheight from dapui panes, let user resize freely
winfixheight was locking Breakpoints, Console, Scopes, etc. to their
configured heights, fighting any attempt to manually resize. That's
hostile to user agency — someone with many breakpoints wants a taller
Breakpoints pane; someone tailing log output wants a taller Console.

pin_dapui_sizes still runs ONCE per M.enter to set the initial
proportions (25% each sidebar pane, 8-or-20% Console). After that,
user is free to drag splits, :resize, etc. and those sticks.

Removed the WinResized handler entirely (it was the aggressive
re-pinner that made user resizes bounce back). The VimResized handler
stays but only recreates bars that got killed by shrink; it no longer
re-pins dapui sizes. Trade-off: tiny-equalizer-style `wincmd =`
plugins will now successfully equalize during debug mode, but since
we've already told the user to disable that plugin, it's acceptable.

Bar windows themselves (status + legend) KEEP winfixheight because
they're chrome, not data — users shouldn't be resizing a status bar,
and locking them down prevents accidental drift.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:03:08 -06:00
swaitsandClaude Opus 4.6 1758fad8ca fix: recreate bars killed by shrink-below-minimum resize
When the user shrinks the terminal window small enough that nvim
can't fit all splits (including our two bars + dapui panes + source),
nvim closes some windows to make room. winfixheight prevents resize
but doesn't prevent close. When the user expands again, the closed
bars don't come back automatically — the layout is permanently
broken until debug mode is toggled.

Fix: on VimResized (deferred via vim.schedule), check if sbar_win
and cbar_win are still valid. If either got killed, clear the
stale reference and call open_bars() which idempotently recreates
anything missing. Then re-pin dapui sizes to restore the intended
layout.

WinResized autocmd retained for intra-session drift (mouse-drag
resizes, plugin-driven wincmd=).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:01:16 -06:00
swaitsandClaude Opus 4.6 c143525f30 fix: re-assert layout after tiny-equalizer's VimResized wincmd=
User's tiny-equalizer.nvim runs 'tabdo wincmd =' on every VimResized,
equalizing every window — which destroys our explicit sidebar/console
sizing and bar pinning. Not focus.nvim like I guessed; the real
culprit is their own plugin doing exactly what it says on the tin.

Fix: defer our VimResized reposition via vim.schedule so our handler
runs AFTER tiny-equalizer's `wincmd =` in the same event-loop tick.
We re-pin the layout and reverse the equalization damage.

Also added a WinResized autocmd (guarded against recursion via
M._pinning flag) so that individual-window resizes — including
manual user drags on pane separators and other `wincmd =` invocations
from anywhere else — also trigger a re-pin. This is more aggressive
than the VimResized coverage but necessary because mouse-drag resizes
don't fire VimResized.

Trade-off: the user can no longer manually resize a single dapui pane
and have the size stick — winfixheight + our re-pin will snap it back.
That's the correct behavior for debug mode (we own the layout); users
who want manual resize can toggle debug mode off.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:54:00 -06:00
swaitsandClaude Opus 4.6 f5647e53f6 fix: status bar 2 rows, explicit sidebar distribution, bigger Console
Three tweaks per latest feedback:

1. Status bar back to 2 rows (sep + content). dapui renders its own
   pane border above its top elements, which serves perfectly well as
   the bottom edge of our bar. Drawing our own bottom separator was
   producing two stacked rows of separator — the "dapui border on its
   own row under our status bar border" the user called out.

2. Sidebar pane distribution: was relying on dapui's 0.25-each config
   with rounding quirks sometimes making Breakpoints smaller than its
   neighbors. New pin_dapui_sizes explicitly computes total sidebar
   height and divides equally, handing any residual rows to Breakpoints
   (last pane) so it's always ≥ the other three.

3. Console height formula bumped: was max(5, floor(avail * 0.15))
   giving 5-7 rows on most terminals which felt "laughably small". Now
   max(8, floor(lines * 0.2)) — Console always at least 8 rows, scales
   to ~20% on tall terminals.

Bonus: VimResized now also re-pins dapui sizes, so if the user has a
plugin like focus.nvim auto-resizing on focus change, our layout heals
itself on any window-resize event rather than staying permanently
skewed.

Note on the "mouse-resize totally breaks the layout" bug: that's
almost certainly focus.nvim (which the user has installed) fighting
our winfixheight pins. focus.nvim actively resizes windows on cursor
movement; nothing we pin sticks while it's running. User should either
disable focus.nvim during debug mode or set its ignore list to cover
dapui_* and TurboDebugBar filetypes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:48:16 -06:00
swaitsandClaude Opus 4.6 33f1e6ab73 fix: console sizing, sidebar drift on re-entry, status bar bottom border
Three interrelated bugs from the latest feedback round:

1. Console was idiotically small (fixed 5 rows). Reverted to the
   originally-requested formula: max(5 rows, 15% of available vertical
   space). The drift-back-to-huge problem is solved the right way in
   fix #2, not by clamping size small.

2. Sidebar proportions drifted on every toggle (Call Stack ballooning,
   Scopes/Watches shrinking to 1 row each after several in-and-outs).
   Root cause found in dapui/windows/layout.lua: `update_sizes` writes
   CURRENT window ratios back into `win_state.size` after each
   operation, so the next open_wh uses those drifted ratios as its
   target. Fix: call `dapui.open({ reset = true })`. The `reset` flag
   tells dapui to ignore the drifted cached state and use `init_size`
   (our original 0.25 per pane) on every open.

3. Status bar's bottom border rendered dark while everywhere else
   showed a light-grey FloatBorder-style edge. Restored the bottom
   separator row on the status bar, making it 3 rows again
   (sep / content / sep). The legend bar stays 2 rows (content / sep)
   because its top transition borrows from dapui's pane border below.
   Symmetry across all 4 visible bar edges now.

Console double-output on multiple restarts still reported; that's a
separate in-place-terminal-clear issue still being investigated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:39:24 -06:00
swaitsandClaude Opus 4.6 4f5fecfb34 fix: stable sidebar + console sizing across toggle cycles
The "console fills the whole column" + "sidebar panes all fucked up on
re-entry" bug traces to three interacting problems:

1. I was creating bars BEFORE dapui.open, then using `wincmd K/J` to
   force them to full width. That cascade recomputed every window
   height. dapui windows without winfixheight absorbed freed space
   unpredictably, and the drift accumulated on every toggle.

2. M.exit was closing our bars BEFORE dapui. When our bars closed,
   their 4 rows were freed; dapui grabbed those rows into Console
   and remembered the new size. Next open → Console ballooned.

3. Console height was coming out right on first entry but dapui's
   internal size tracker was letting it grow back on re-open.

Fixes:

- Open bars AFTER dapui.open. `:topleft split` and `:botright split`
  natively produce full-width horizontal splits above/below whatever
  layout already exists — no wincmd K/J rearrangement needed.
- Close dapui BEFORE close_bars in M.exit so dapui doesn't see our
  bars disappearing and grow to fill the void.
- New pin_dapui_sizes(): after dapui.open, iterate dapui panes and
  set winfixheight = true on all of them. Console gets an explicit
  set_height to config.opts.console_height (default 5) first. Panes
  are now locked — wincmd/resize can't mess with them.
- Console height: fixed 5 rows (config.opts.console_height), no
  more dynamic min(5, 15% of avail) which was producing unstable
  values on re-entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:31:01 -06:00
swaitsandClaude Opus 4.6 32c7e465c8 fix: bar blends with dapui bg, console capped at 5 rows, restart clears
Three refinements driven by the latest screenshot:

- TurboDebugBar highlight now links to Normal (was StatusLine). The
  StatusLine bg is usually darker than Normal in most colorschemes,
  which created a visible seam between our bar and the surrounding
  dapui pane headers (which inherit Normal/WinBar bg). Linking to
  Normal makes the bar blend so the overall UI reads as one unified
  surface rather than two mismatched strips.

- Console height now `min(5, 15% of avail)` with a 3-row absolute
  floor. Previously used `max(5, 15%)` which let Console balloon on
  tall terminals — misread the user's "minimum of 15% OR 5 rows"
  direction. Console is ancillary; caps at 5 to give the sidebar and
  source the vertical space they deserve.

- Clear-on-restart actually works now. Previous attempt deleted the
  entire console buffer on start/restart so dapui would create a fresh
  one — this worked on `(c)` start (new terminal opens) but broke on
  `(R)estart` because the adapter reuses the terminal channel, and
  deleting the buffer orphaned the channel (Console went unresponsive).
  New approach: KEEP the buffer, briefly toggle modifiable=true, and
  call nvim_buf_set_lines with empty content to wipe the scrollback.
  The PTY state and output stream stay intact; only the visible lines
  are gone. Default `clear_console_on_start = true` again.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:24:59 -06:00
swaitsandClaude Opus 4.6 c7e9fb799d fix: adapter timeouts + bars now span full editor width
Regression fixes from the previous commit:

1. Debug adapter timing out / general slowness — root cause was calling
   dapui.setup() on every M.enter() to "refresh layouts". setup is
   heavy (tears down windows, re-registers listeners, rebuilds state)
   and running it while the adapter handshake is in flight caused
   "Debug adapter didn't respond" timeouts. Reverted to one-time setup;
   layouts are computed once per nvim session. Trade-off: Console/
   sidebar proportions may drift slightly if the terminal is resized
   between toggles, but that's vastly preferable to a broken adapter.

2. Console broken / unreachable — clear_console was calling buf_set_lines
   on a terminal buffer after dapui had made it a terminal, which leaves
   the terminal in an inconsistent state. Kept the config option but
   disabled by default (clear_console_on_start = false). Users who want
   it can opt in; the feature remains risky on terminal-backed Console
   buffers. Better design pending.

3. Bars not spanning full editor width — :topleft split and :botright
   split create full-width splits only when no competing vertical
   splits exist. dapui's sidebar creates a full-height vertical split
   AFTER our bars, which squeezes the bars into the non-sidebar column.
   Added force_bars_full_width() which runs `wincmd K` (on sbar) and
   `wincmd J` (on cbar) AFTER dapui.open(). These commands re-layout
   the window tree with the bar window spanning full editor width,
   pushing sidebar + source + tray into the middle rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:18:28 -06:00
swaitsandClaude Opus 4.6 988bd06d54 feat: 2-row bars, console clear, registers collapse, fix re-entry layout
Cluster of UX refinements + one real bug:

Bar chrome:
- Shrunk both bars from 3 rows to 2 rows. dapui's own pane borders act
  as the visual separation between the bar and the main content area,
  so the inner-facing separator lines on each bar were redundant.
    Top bar:    sep / content       (no bottom line)
    Bottom bar: content / sep       (no top line)
- Sidebar/Console sizing recalculated: 2-row bars instead of 3 frees
  up 2 extra rows for content.

Smarter legend:
- (R)estart is now conditionally rendered — only shown when a dap
  session is active. When there's no session, "restart" is meaningless
  (there's nothing to restart), so the button hides and the remaining
  controls re-center around the smaller set.

Console clear on start/restart:
- New config.opts.clear_console_on_start (default true). Fires on
  dap.listeners.before.event_initialized — wipes the Console buffer
  so output from a previous run doesn't linger when a new session
  begins.

Registers default-collapsed:
- dapui's own scopes component auto-collapses any scope the adapter
  marks `expensive = true` (see dapui/components/scopes.lua:22-24).
  codelldb (Rust/C++) doesn't mark Registers expensive even though it
  dumps hundreds of entries. We intercept the scopes response via
  dap.listeners.before.scopes and inject `expensive = true` on any
  scope name in config.opts.collapsed_scopes (default `{"Registers"}`).
  Reuses dapui's existing collapse mechanism, so no monkey-patching.

Re-entry layout fix:
- The Console "balloon" bug on second ,dd toggle was dapui caching
  first-call layout dimensions. ensure_dapui() now re-runs dapui.setup
  with freshly computed layouts on EVERY M.enter, so the 25% / 25% /
  25% / 25% sidebar split and 15%-of-available Console all recompute
  against current terminal + current bar heights.
- Bars are now opened BEFORE dapui.open() so dapui's layout algorithm
  sees the reduced available space from the start rather than computing
  against full screen and then getting squished.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:08:17 -06:00
swaitsandClaude Opus 4.6 fb695ce359 feat: bars become real splits (no overlap), gutter IP sign, responsive layout
Critical fix — the top status bar was overlapping the first rows of the
source window and Call Stack pane. Root cause: both bars were floating
windows positioned at row 0 and just-above-statusline. dapui's layout
algorithm doesn't know about floats, so its windows extend into the
rows our floats were covering. Overlap = content hidden.

Fix: convert both bars from floating windows to real splits.
  :topleft split  → top status bar (3 rows, winfixheight)
  :botright split → bottom control bar (3 rows, winfixheight)
Splits consume real layout space so dapui's panes lay out below/above
them naturally. No more overlap.

Also:
- Bars are now 3 rows each (sep / content / sep) for clear visual
  separation from the main content area, per user design preference.
- IP gutter sign (👉) now placed via extmark with sign_text at
  priority 500, overriding nvim-dap's DapStopped sign which was
  losing the priority tie with the breakpoint sign on the paused line.
  Both the gutter finger and the EOL-vtext finger now show.
- Sidebar width: max(25, min(40, columns/3)). Scales down on narrow
  terminals, caps at 40 on wide ones so the source window stays the
  star. Minimum 25 keeps the panes readable.
- Console (bottom tray) height: max(5, 15% of available vertical
  space) — floors at 5 rows so very tall terminals don't blow it up
  while small terminals still get a usable Console.
- Sidebar panes: equal 25% each (Stack / Scopes / Watches /
  Breakpoints). User preference — lets Breakpoints be visibly taller
  than the 5-row minimum Console so they read as distinct panes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:59:06 -06:00
swaitsandClaude Opus 4.6 869a4a36d9 feat: split bar into top status + bottom controls, drop REPL pane
Layout overhaul per UX feedback — the 4-row bottom bar was too much and
mixing status info with action controls in one block felt crammed.

New chrome:
  ┌── row 0 ──────────────────────────────────────────────┐
  │  turbo-debug       dap.status msg     DEBUG · STATE   │  ← top bar
  ├───────────────────────────────────────────────────────┤
  │  source code…                                         │
  │  (filename winbar now shows just %f, no DEBUG prefix) │
  ├── bottom tray (Console, 10 rows, full width) ─────────┤
  ├───────────────────────────────────────────────────────┤
  │  (c)ontinue (s)tep (d)escend (r)eturn …      ❓ help  │  ← control bar
  │  statusline (user's lualine untouched)                │
  │  cmdline                                              │
  └───────────────────────────────────────────────────────┘

- Top bar (2 rows, sbar_win): brand on left, dap.status() in middle,
  colored state chip (DEBUG · READY/RUNNING/PAUSED) on right.
- Bottom bar (2 rows, cbar_win): controls centered, help right-anchored.
  Down from 4 rows to 2 — no more crammed single-row layout.
- REPL pane removed from the bottom tray. Duplicates Console for most
  workflows and the horizontal space is better spent on program output.
  Users who want a REPL can hit E in debug mode to open it as a float.
- Console (bottom tray) now gets the full width and size 10 (was 8 in a
  split).
- Source file winbar simplified to just " %f" — the DEBUG · STATE chip
  has moved to the top bar where it belongs.

Total chrome cost: 2 (top) + 10 (tray) + 2 (bottom) + 1 (statusline) +
1 (cmdline) = 16 rows. Previous: 4 (bar) + 8 (tray) + 1 + 1 = 14.
Trade 2 extra chrome rows for vastly better layout clarity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:43:55 -06:00
swaitsandClaude Opus 4.6 5477df4578 feat: 4-row bar (state+help row above controls row), ❓ help, no underlines
Layout restructure per UX feedback — the single-row-crammed layout
couldn't fit state + status + controls + help on narrow windows, and
byte-based truncation was clipping emoji at the right edge.

New shape:
  ┌────────────────── width ──────────────────┐
  ├ ─ ─ ─ ─ top separator ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
  │ 🐛 DEBUG · PAUSED · status…        ❓ help │
  │          🟢 (c)ontinue  …  🛑 (q) terminate│
  ├ ─ ─ ─ ─ bottom separator ─ ─ ─ ─ ─ ─ ─ ─ ─┤
  └────────────────────────────────────────────┘

- Help has its own right-anchored slot on row 1 — it can never be
  squeezed off by long status messages or wide control labels. Solves
  the "no help visible on READY state" regression where dap's
  "All threads stopped" message was pushing it past the byte-truncation.
- Controls get the full width of row 2 without fighting status text,
  centered horizontally.
- If status message is too long for row 1, we truncate status first with
  an ellipsis — help stays put.

Other fixes:
- TurboDebugBarKey no longer applies underline. The descender of
  lowercase q crossed the underline and read as a lowercase g. Kept
  bold; dropped underline.
- Help emoji switched back to ❓ (U+2753). It's BMP but has
  Emoji_Presentation=Yes by default, so Ghostty renders it as color.
  The previous "❓ invisible" symptom was caused by the broken-extmark
  underline leak; fixed in the prior commit.
- Bottom tray shrunk 9 → 8 rows to preserve the 12-row total footprint
  of the original dapui default (8 tray + 4 bar = 12).

Click zones now include a line number so we can dispatch correctly
between the state row (help click) and the controls row (control clicks).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:33:53 -06:00
swaitsandClaude Opus 4.6 be8a3ac93f fix: underscore-after-terminate (stray underline), ♻️→🔄, iconless REPL
Three fixes to bar rendering:

- The "terminate_____" trailing underscores were TurboDebugBarKey
  (bold+underline) bleeding from a misaligned extmark. The old code set
  the key-hint extmark starting at `right_byte_start + 1` — 1 byte INTO
  the 4-byte help emoji — spanning 4 bytes across the emoji boundary.
  When 💡 (U+1F4A1) didn't render as a glyph in the user's font, the
  underline still rendered on those cells: producing visible underscores
  in empty space. Drop the key-hint extmark on the help label entirely.
  The emoji (if rendered) is already the visual; underline isn't needed.

- Restart icon ♻️ (U+267B + VS16) switched to 🔄 (U+1F504). VS16 forces
  emoji presentation only when the font HAS an emoji glyph for that
  codepoint — many don't, leaving text-mode rendering or blank. 🔄 is
  supplementary-plane so it always renders as color emoji regardless.

- REPL pane title: dropped 💬 icon per user preference. Title is now
  just " REPL" — the prompt format `dap>` is already a clear visual
  differentiator, no need for a chrome icon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:24:30 -06:00
swaitsandClaude Opus 4.6 4cec465a16 feat: modal bar labels, all-emoji icons, picker focus, dapui gutter cleanup
Control bar refinements:
- All-emoji icons, supplementary plane only (plus ♻️ with VS16 per user
  preference). Dropped Nerd Font Codicons — they rendered as unrelated
  glyphs in the user's font (debug-start as a gear, step-into arrow
  backwards, etc.). Replaced set:
    🚀 start / 🟢 continue (dynamic), 👣 step over, 🔽 descend,
    🔼 return, ♻️ restart, 🛑 quit/terminate, 💡 help.
- Modal label text — the tail after the (key) updates based on dap state:
    no session:  "(c) start"   / "(q)uit"
    active:      "(c)ontinue"  / "(q) terminate"
  So the label always shows what pressing the key will actually do.
- Help icon switched from ❓ to 💡 — ❓ (U+2753) was rendering zero-width
  in the user's font so the help label effectively disappeared. 💡 is
  supplementary-plane and renders reliably.

Launch picker focus fix:
- run_in_source() now calls nvim_set_current_win instead of nvim_win_call.
  nvim_win_call returns focus to the caller's window synchronously, so
  when dap.continue() triggered an async vim.ui.select picker, the picker
  opened but no window held focus — user had to click into it manually.
  Shifting focus directly means the picker inherits source-window focus.

Bar buffer robustness:
- buftype=nofile + BufEnter autocmd that bounces focus back if anything
  accidentally lands in the bar. Prevents E21 "Cannot make changes,
  'modifiable' is off" that the user hit after a mouse drag landed focus
  in the bar and their next keypress tried to edit.

dapui pane gutter cleanup:
- set_dapui_window_opts now forces number=false, relativenumber=false,
  signcolumn=no, statuscolumn="", foldcolumn=0, and sets fillchars=eob:
  to hide the `~` end-of-buffer marks. snacks statuscolumn was rendering
  a gutter with line-number-like marks in dapui panes where they have no
  semantic meaning.

Sidebar reorder from last commit stands: Call Stack → Scopes → Watches
→ Breakpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:18:09 -06:00
swaitsandClaude Opus 4.6 95cfcf915b feat: 3-zone bar layout, consistent icons, pane reorder, bolder key hints
Control bar — design overhaul per UX feedback:
- 3 zones: state chip (left), controls (centered), help (right). Each zone
  gets a dynamic-width spacer between. As the window narrows, drop first
  dap.status(), then controls (text collapses), then chip detail — help is
  NEVER sacrificed per user priority.
- Control icons are all Nerd Font Codicons now (       ).
  Mixed emoji + VS16-text combinations were rendering inconsistently in
  the user's font (▶️ ⬇️ as text-mode glyphs while 🔴 🐛 rendered as emoji).
  Codicons all ship in the same Nerd Font range → uniform look.
- Help label drops the redundant "(?)" — the ❓ emoji is self-documenting.
  Now reads "❓ help" instead of "❓ (?) help".
- Key letter (the s in (s)tep, etc.) gets bold + underline + Special's fg
  via a derived highlight (nvim_set_hl can't combine link + bold attrs).
  Re-applied on ColorScheme so theme swaps don't lose the styling.

Sidebar reorder — ergonomic over convention:
- New order: Call Stack, Scopes, Watches, Breakpoints (was Scopes first).
- When paused, the first question is "where am I?" → Call Stack first.
- Call Stack adjacent to Scopes means clicking a frame updates the Scopes
  right below it — visual affordance for the Call-Stack-drives-Scopes
  relationship. VS Code puts Scopes first; IntelliJ/Xcode put Frames
  first. We side with the ergonomic argument over convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:04:19 -06:00
swaitsandClaude Opus 4.6 b4644cfc9f fix: borrow source-window context when continuing from a dapui pane
The symptom: pressing c in dapui_scopes/watches/stacks/breakpoints/console
printed "No configuration found for dapui_<pane>" because nvim-dap looks
up dap.configurations[vim.bo.filetype] from the current window — and in
dapui panes that filetype is dapui_* (no configs), not the source file's
filetype.

The modal key WAS firing; our smart_continue() ran but then ended up
calling dap.continue() from the dapui pane's context, which blew up on
config lookup.

Fix: wrap the launch in run_in_source() which finds a window whose
filetype isn't dapui_*/dap-repl/TurboDebugBar, then nvim_win_call()s
into that window so nvim-dap reads the real source filetype (rust,
python, go, etc.). Post-session dap.continue() resumes work as normal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:53:23 -06:00
swaitsandClaude Opus 4.6 8d1ddc9c78 feat: 3-row control bar with key hints, box lines, help in bar
UX refinement per design feedback:

- Bar is now 3 rows tall: ─ top separator, content row, ─ bottom separator.
  The separator chars were invisible before because TurboDebugBarSeparator
  linked to WinSeparator (muted in most themes). Switched to FloatBorder.
- Bottom tray default shrunk 12→9 rows so the 3-row bar doesn't overlap
  REPL/Console. Net vertical footprint unchanged (12 tray = 9 tray + 3 bar).
- Action labels now teach the keyboard shortcut inline:
    ▶️ (c)ontinue   ⏭️ (s)tep   ⬇️ (d)escend   ⬆️ (r)eturn
    🔄 (R)estart   🛑 (q)uit   ❓ (?)help
  The (k) letter is highlighted with TurboDebugBarKey → Special so the
  shortcut pops. The entire label is a click zone.
- help_on_enter default flipped true → false. The (?)help label in the bar
  is always visible, so new users discover help via the bar instead of an
  auto-popup on every debug-mode entry.
- No top bar added. The source file winbar already shows 🐛 DEBUG · <file>,
  so a second chrome row would just duplicate context and eat screen space.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:51:27 -06:00
swaitsandClaude Opus 4.6 a728450607 feat: buffer-rendered control bar, emoji icons, fix IP sign leak
Addresses live-feedback issues that persisted after v3:

- Control bar now uses buffer content + extmarks instead of winbar —
  winbar doesn't render in minimal-style floats, which is why the bar
  was empty. Two rows: ─ separator on top, state chip + controls below.
  Click dispatch via buffer-local <LeftMouse> keymap + cursor-column
  lookup against a zones table.
- Bar auto-refreshes on every dap event (initialized/stopped/continued/
  terminated/exited) so the READY/RUNNING/PAUSED chip and dap.status()
  text stay live.
- Colorful emoji throughout for instant recognition:
    Signs:    🔴 BP, 🟡 conditional, ⭕ rejected, 👉 stopped, 📝 logpoint
    Titles:   🔎 Scopes, 👁️ Watches, 📚 Call Stack, 🔴 Breakpoints,
              💻 Console, 💬 REPL
    Badge:    🐛 DEBUG · <file>
    Controls: ▶️ ⏭️ ⬇️ ⬆️ 🔄 🛑 ❌
- Modal key install no longer skips buffers that already have entries —
  every BufEnter/FileType force-reinstalls the keymaps so dapui's own
  render pass can't leave them in a broken state.
- IP sign leak fixed: clear_ip() now unplaces lingering DapStopped signs
  via sign_getplaced + sign_unplace, so restart no longer leaves a stale
  👉 in the gutter of the previously-paused line.
- IP reason text (STEP/BREAKPOINT/EXCEPTION) now uses bold + reverse for
  an inverted-color badge that really demands attention.
- dapui.expand_lines = false — this was spawning a cursor-pinned float
  when a line was wider than its pane, causing the "hover text bleeds
  across the screen" bug. With wrap+breakindent inside the pane, the
  hover-float is redundant and destructive.
- Control-bar buffer is marked filetype=TurboDebugBar and explicitly
  skipped by the modal key installer, so clicking a button doesn't
  accidentally fire modal actions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:38:38 -06:00
swaitsandClaude Opus 4.6 8d8dd23b3a feat: floating control bar, throttled console, wrapped values, Codicon icons
Major v3 fixes driven by live-debugging feedback:

- Control bar: replace vim.o.statusline override (lualine fights it) with
  a 1-row floating window pinned above the statusline. Shows state chip
  (READY/RUNNING/PAUSED) + seven clickable Codicon controls + dap.status().
  Survives resize via VimResized autocmd. Fully independent of lualine.
- Console refresh: trailing-edge throttle at config.console_refresh_ms
  (default 50ms = 20 Hz ceiling). Targets console windows via nvim_win_call
  + redraw! to avoid screen-cache miss. Chatty programs can't DOS the editor.
- Modal keys in dapui panes: install synchronously on FileType (drop inner
  vim.schedule); all modal keys get nowait=true (was continue-only) so
  single-key presses aren't swallowed by <leader>d* prefix matching.
- Wrap in dapui panes: wrap=true + breakindent + showbreak="  ↳ " so long
  register values stay visible inside the pane without bleeding into the
  source window. Safety valve at config.max_value_width (default 2000,
  set 0 to disable) truncates only pathological multi-KB dumps with "…".
- Smart q: context-aware terminate. If a session is running, q calls
  dap.terminate() and stays in debug mode (so user can read the console
  postmortem). If no session, q exits debug mode. Removes quit_exits_mode.
- Icons: full Nerd Font Codicon set for signs, control bar buttons, pane
  titles, and debug badge. 👈 emoji in the paused-line vtext pairs with
  👉 in the gutter — fingers literally point at the paused line from both
  sides.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:22:00 -06:00
swaits be628194ec WIP: aesthetics! 2026-04-12 11:35:15 -06:00
swaits 31f9ae1733 docs: add screenshot to README 2026-04-11 20:33:54 -06:00
swaits 151ff538db docs: rewrite README with modal explanation, credits, screenshot placeholder 2026-04-11 20:25:55 -06:00
27 changed files with 3022 additions and 1557 deletions

No files matched your search

+166
View File
@@ -1,5 +1,171 @@
# Changelog
## 0.2.2 — 2026-04-12
Bug-fix release: Python program output now shows in the Console pane, and
breakpoints restore for files opened on the command line.
### Fixed
- **Python `print()` output appears in the Console pane.** The debugpy
launch config now sets `console = "integratedTerminal"`, so debugpy
routes stdout/stderr via `RunInTerminalRequest` into dapui's Console
buffer (which is already registered as
`dap.defaults.fallback.terminal_win_cmd`). Previously debugpy defaulted
to `internalConsole` and emitted `OutputEvent`s that nvim-dap forwards
to the REPL buffer — which turbo-debug's layout deliberately omits, so
program output vanished. Interactive `input()` now also works.
- **Breakpoints restore for files opened on the command line.** `nvim
foo.py` fires `BufReadPost` for `foo.py` before turbo-debug's deferred
`setup()` runs, so `persistent-breakpoints.nvim`'s `BufReadPost`
autocmd missed the startup buffer and saved breakpoints never came
back. After setup, turbo-debug now sweeps already-loaded buffers once
via `load_breakpoints()` (idempotent; placed after `sign_define` so
the first paint uses turbo-debug's emoji signs instead of dap's
default `B`).
## 0.2.1 — 2026-04-12
Language coverage release. 10 new DAP adapters bring built-in support from
12 to 22 languages, prioritized by language popularity and DAP adapter
maturity.
### Added
- **Dart** (with optional **Flutter**) — auto-detects `flutter` on PATH and
adds a "Launch Flutter app" configuration alongside the plain Dart file
launch. Uses `dart debug_adapter` / `flutter debug_adapter`.
- **Bash / Sh** — `bash-debug-adapter` with automatic `bashdb_dir` probing
from the mason package path.
- **R** — `vscDebugger` R-package integration. Preflights the package via
`Rscript` and silently skips registration if it's missing.
- **Elixir** — `elixir-ls-debugger` with `mix_task` launch (default task
`test`). Falls back to the mason package `debugger.sh` if the wrapper
binary isn't on PATH.
- **Haskell** — `haskell-debug-adapter`.
- **OCaml** — `ocamlearlybird`.
- **Zig / Nim / Crystal** — routed through the existing `codelldb` adapter
via filetype extension (no new adapter; just added filetypes to the
existing codelldb configuration).
- **F#** — routed through the existing `netcoredbg` adapter (same
`.dll`-launch flow as C#).
### Internal
- Extended `tests/test_adapters.lua` to cover the three new modular adapter
modules (`dart`, `bash`, `r`).
- All new registrations follow the existing `find_executable` +
`loader.register` pattern and silently skip when the required binary
isn't found — matches the "no noise when prereqs missing" invariant of
the previously-shipped adapters.
## 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 <steve@waits.net>`
### 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.
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 Steve Waits
Copyright (c) 2026 Stephen Waits <steve@waits.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+202 -17
View File
@@ -4,24 +4,64 @@ A zero-config debugging experience for Neovim 0.12+. One plugin install, full
IDE-class debugging: inline variable values, hover inspection, persistent
breakpoints, watch expressions, and modal keybindings that just work.
Inspired by Turbo Pascal — the first IDE where you could just hit a key
and start debugging.
![turbo-debug screenshot](assets/screenshot.png)
## Install
```lua
vim.pack.add("swaits/turbo-debug.nvim")
vim.pack.add("swaits/turbo-debug")
```
That's it. No other config needed. See `:help turbo-debug` for customization.
## Usage
## How It Works
Everything lives under `<leader>d` (with which-key support if installed):
turbo-debug is **modal**. Press `<leader>dd` and you enter debug mode — the
dap-ui layout opens, keybindings activate, virtual text lights up, and
you're debugging. Press `<leader>dd` again (or `q`) and everything
disappears: the session terminates, the UI closes, and every keybinding is
restored to exactly what it was.
| Key | Action | Available |
|-----|--------|-----------|
| `<leader>dd` | Toggle debug mode | Always |
| `<leader>dx` | Toggle breakpoint | Always |
| `<leader>dX` | Conditional breakpoint | Always |
| `<leader>dD` | Clear all breakpoints | Always |
This modal approach is inspired by
[debugmaster.nvim](https://github.com/MironPascalCaseFan/debugmaster.nvim).
During a debug session you **navigate** code and **inspect** state; you
don't **edit**. So debug mode shadows editing keys (`c`, `d`, `s`, `r`,
`x`) with debug actions and leaves all navigation keys untouched (`h`,
`j`, `k`, `l`, `w`, `b`, `e`, `f`, `/`, `n`, etc.).
### A nice text *and* GUI debugger
The modal keymap is deliberately **left-hand optimized** so the right hand
stays on the mouse for interacting with the dap-ui panes:
- **Left hand on keyboard**: `c s d r C x X D W E q R` fire the twelve
rapid-use debug actions without leaving home row.
- **Right hand on mouse**: click into Watches/Scopes/Stacks/Breakpoints
panes to expand trees, jump to frames, add watch expressions, or toggle
breakpoints from the GUI.
Only two modal keys live on the right hand, both by universal convention:
`K` for hover (matches the LSP hover key) and `?` for help.
Inside dap-ui panes, the native dap-ui mappings still work — `d` removes a
watch, `r` sends to REPL, `e` edits. The modal `d` (step-into) and `r`
(step-out) yield to dap-ui in those buffers so you can manage the GUI
without exiting debug mode.
## Keybindings
Everything lives under `<leader>d` (with
[which-key](https://github.com/folke/which-key.nvim) support if installed):
| Key | Action |
|-----|--------|
| `<leader>dd` | Toggle debug mode |
| `<leader>dx` | Toggle breakpoint |
| `<leader>dX` | Conditional breakpoint |
| `<leader>dD` | Clear all breakpoints |
During debug mode, single-key controls:
@@ -32,23 +72,168 @@ During debug mode, single-key controls:
| `d` | Step into | `D` | Clear all breakpoints |
| `r` | Step out | `W` | Watch expression |
| `C` | Run to cursor | `K` | Hover / inspect |
| `q` | Terminate session | `E` | Eval in REPL |
| `q` | Smart quit (terminate → exit) | `E` | Eval in REPL |
| `R` | Restart session | `?` | Help popup |
All navigation keys preserved. Modal keybindings vanish when you exit debug mode.
`q` is context-aware: if a session is running, it terminates the session
and leaves you in debug mode (so you can read the console and inspect
the exit state). If no session is running, `q` exits debug mode entirely.
Press `?` during debug mode for a quick reference that also lists dap-ui's
native in-pane bindings (`<CR>`, `e`, `o`, `d`, `r`, `t`).
### Extending keymaps
Every key is user-overridable. Remap, disable, or add bindings via
`setup()`:
```lua
require("turbo-debug").setup({
keys = {
step_over = "n", -- remap a modal key
watch = false, -- disable a modal key
},
global_keys = {
toggle = "<leader>D", -- change the toggle chord
},
sidebar = "right", -- "left" (default) or "right"
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().
-- Set false to always run until a breakpoint instead.
stop_on_entry_when_no_breakpoints = true,
-- Cap per-line value length in Scopes/Watches. Values longer than this
-- get truncated with `…`. Default 2000 only catches pathological register
-- dumps. Set 0 to disable truncation entirely.
max_value_width = 2000,
-- 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,
})
```
### Chrome bars
When debug mode is active, two 2-row floating bars appear, completely
independent of lualine, heirline, or any other statusline plugin:
**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`)
**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
- **Rounded borders** are on every *floating* window this plugin opens:
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/
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`, `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.
- **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
Adapter binaries must be installed (via mason, package manager, etc).
turbo-debug ships adapter configs for 22 languages. You install the adapter
binary (via [mason.nvim](https://github.com/williamboman/mason.nvim), your
package manager, or manually) — turbo-debug handles all the configuration.
| Language | Adapter | Language | Adapter |
|----------|---------|----------|---------|
| Python | debugpy | C# | netcoredbg |
| JavaScript / TS | js-debug | Java | java-debug-adapter |
| Go | delve | Kotlin | kotlin-debug |
| C / C++ | codelldb | PHP | php-debug |
| Rust | codelldb | Ruby | rdbg |
| Swift | codelldb | Lua (Neovim) | osv |
| JavaScript / TS | js-debug | F# | netcoredbg |
| Go | delve | Java | java-debug-adapter |
| C / C++ | codelldb | Kotlin | kotlin-debug |
| Rust | codelldb | PHP | php-debug |
| Swift | codelldb | Ruby | rdbg |
| Zig | codelldb | Elixir | elixir-ls |
| Nim | codelldb | Haskell | haskell-debug-adapter |
| Crystal | codelldb | OCaml | ocamlearlybird |
| Dart / Flutter | dart-debug | R | vscDebugger |
| Bash / Sh | bash-debug-adapter | Lua (Neovim) | osv |
Special treatment:
- **Rust**: pressing `c` runs `cargo build` automatically and picks the
binary via `vim.ui.select`.
- **Python**: auto-detects virtualenvs (`VIRTUAL_ENV`, `CONDA_PREFIX`,
`.venv`, `venv`, `env`, `.env`).
- **Dart**: if `flutter` is on PATH, a "Launch Flutter app" configuration
is added alongside the plain Dart file launch.
- **R**: preflights that the `vscDebugger` R package is installed; silently
skips registration if missing.
## What This Plugin Actually Does
turbo-debug is **glue**. It does not implement a debugger, a UI framework,
virtual text rendering, or breakpoint persistence. It configures and wires
together these excellent plugins into a single cohesive experience:
| Plugin | What it provides |
|--------|-----------------|
| [nvim-dap](https://github.com/mfussenegger/nvim-dap) | DAP client — the core debug engine |
| [nvim-dap-ui](https://github.com/rcarriga/nvim-dap-ui) | Scopes, stacks, watches, REPL, console UI |
| [nvim-nio](https://github.com/nvim-neotest/nvim-nio) | Async IO (required by nvim-dap-ui) |
| [nvim-dap-virtual-text](https://github.com/theHamsta/nvim-dap-virtual-text) | Inline variable values in source code |
| [persistent-breakpoints.nvim](https://github.com/Weissle/persistent-breakpoints.nvim) | Breakpoints that survive restarts |
All five are loaded automatically via `vim.pack.add`. You never declare them.
Adapter configurations are inlined from the work of these projects (no
runtime dependency, we just studied their configs and wrote plain Lua tables):
- [nvim-dap-python](https://github.com/mfussenegger/nvim-dap-python)
- [nvim-dap-go](https://github.com/leoluz/nvim-dap-go)
- [nvim-dap-vscode-js](https://github.com/mxsdev/nvim-dap-vscode-js)
- [nvim-dap-lldb](https://github.com/julianolf/nvim-dap-lldb)
- [nvim-dap-cs](https://github.com/NicholasMata/nvim-dap-cs)
- [nvim-dap-ruby](https://github.com/suketa/nvim-dap-ruby)
- [nvim-dap-kotlin](https://github.com/Mgenuit/nvim-dap-kotlin)
## License
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

+265 -78
View File
@@ -9,34 +9,37 @@ 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 22 languages
- Theme-agnostic colors (every highlight links to a standard group)
==============================================================================
2. Installation *turbo-debug-installation*
Add one line to your init.lua: >lua
vim.pack.add("swaits/turbo-debug.nvim")
vim.pack.add("swaits/turbo-debug")
<
That's it. All dependencies are managed automatically.
@@ -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 `<leader>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):
- `<leader>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 `<leader>d` prefix, with which-key support):
`<leader>dX` Conditional breakpoint Shift variant
`<leader>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 = "<leader>DD",
breakpoint = "<leader>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 `<CR>` / `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.
`<leader>dx` toggles a breakpoint (always available).
`<leader>dX` sets a conditional breakpoint (always available).
`x`, `X`, `D` also work as shortcuts during debug mode.
Global shortcuts (always available):
`<leader>dx` Toggle breakpoint
`<leader>dX` Conditional breakpoint (prompts for condition)
`<leader>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 `<CR>`
- Scopes (25%) — local/global variables in current frame
- Watches (25%) — user-added watch expressions (|W|)
- Breakpoints (25%) — all persistent breakpoints, jump with `<CR>`
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):
@@ -184,33 +332,67 @@ Built-in adapter support (adapter binary must be installed):
C/C++ codelldb codelldb
Rust codelldb codelldb
Swift codelldb codelldb
Zig codelldb codelldb
Nim codelldb codelldb
Crystal codelldb codelldb
C# netcoredbg netcoredbg
F# netcoredbg netcoredbg
Java java-debug java-debug-adapter
Kotlin kotlin-debug kotlin-debug-adapter
PHP php-debug php-debug-adapter
Ruby rdbg rdbg
Dart dart-debug dart debug_adapter
Flutter flutter-debug flutter debug_adapter
Bash / Sh bash-debug bash-debug-adapter
Elixir elixir-ls elixir-ls-debugger
Haskell haskell-debug haskell-debug-adapter
OCaml earlybird ocamlearlybird
R vscDebugger R (with vscDebugger pkg)
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`.
Dart auto-detects Flutter: if `flutter` is on PATH alongside `dart`, a
"Launch Flutter app" configuration is added to the `dart` filetype in
addition to the plain Dart file launch.
R requires the `vscDebugger` R package. turbo-debug preflights it via
`Rscript` and silently skips registration if the package is missing. Install
with `install.packages("vscDebugger")` or
`remotes::install_github("ManuelHentschel/vscDebugger")`.
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 +404,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:
+39
View File
@@ -0,0 +1,39 @@
local loader = require("turbo-debug.adapters.init")
local M = {}
function M.register(dap)
local exe = loader.find_executable({ "bash-debug-adapter" })
if not exe then return end
local bashdb_dir = vim.fn.stdpath("data") .. "/mason/packages/bash-debug-adapter/extension/bashdb_dir"
if vim.fn.isdirectory(bashdb_dir) == 0 then bashdb_dir = nil end
loader.register(dap, "bashdb", {
type = "executable",
command = exe,
name = "bashdb",
}, {
{
type = "bashdb",
request = "launch",
name = "Launch file",
showDebugOutput = true,
pathBashdb = bashdb_dir and (bashdb_dir .. "/bashdb") or "bashdb",
pathBashdbLib = bashdb_dir,
trace = true,
file = "${file}",
program = "${file}",
cwd = "${workspaceFolder}",
pathCat = "cat",
pathBash = "/bin/bash",
pathMkfifo = "mkfifo",
pathPkill = "pkill",
args = {},
env = {},
terminalKind = "integrated",
},
}, { "sh", "bash" })
end
return M
+7 -14
View File
@@ -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,18 +58,17 @@ 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}",
},
}
for _, ft in ipairs({ "c", "cpp", "swift" }) do
for _, ft in ipairs({ "c", "cpp", "swift", "zig", "nim", "crystal" }) do
dap.configurations[ft] = dap.configurations[ft] or prompt_config
end
-- Rust: cargo build + picker
dap.configurations.rust = dap.configurations.rust or {
dap.configurations.rust = dap.configurations.rust
or {
{
type = "codelldb",
request = "launch",
+46
View File
@@ -0,0 +1,46 @@
local loader = require("turbo-debug.adapters.init")
local M = {}
function M.register(dap)
local dart = loader.find_executable({ "dart" })
if not dart then return end
local configs = {
{
type = "dart",
request = "launch",
name = "Launch Dart file",
program = "${file}",
cwd = "${workspaceFolder}",
toolArgs = { "--enable-vm-service" },
},
}
local flutter = loader.find_executable({ "flutter" })
if flutter then
if not dap.adapters.flutter then
dap.adapters.flutter = {
type = "executable",
command = flutter,
args = { "debug_adapter" },
}
end
table.insert(configs, {
type = "flutter",
request = "launch",
name = "Launch Flutter app",
program = "lib/main.dart",
cwd = "${workspaceFolder}",
flutterMode = "debug",
})
end
loader.register(dap, "dart", {
type = "executable",
command = dart,
args = { "debug_adapter" },
}, configs, { "dart" })
end
return M
+163 -32
View File
@@ -23,43 +23,173 @@ 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" } },
{ "java", { "java-debug-adapter" }, {
{
"netcoredbg",
{ "netcoredbg" },
{
type = "executable",
}, { {
type = "java", request = "launch", name = "Launch class",
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", "fsharp" },
},
{
"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" },
},
{ "kotlin", { "kotlin-debug-adapter" }, {
{
"kotlin",
{ "kotlin-debug-adapter" },
{
type = "executable",
}, { {
type = "kotlin", request = "launch", name = "Launch main",
},
{
{
type = "kotlin",
request = "launch",
name = "Launch main",
mainClass = function() return vim.fn.input("Main class: ") end,
projectRoot = "${workspaceFolder}",
} }, { "kotlin" } },
},
},
{ "kotlin" },
},
{ "php", { "php-debug-adapter" }, {
{
"php",
{ "php-debug-adapter" },
{
type = "executable",
}, { {
type = "php", request = "launch", name = "Listen for Xdebug", port = 9003,
} }, { "php" } },
},
{
{
type = "php",
request = "launch",
name = "Listen for Xdebug",
port = 9003,
},
},
{ "php" },
},
{ "rdbg", { "rdbg" }, {
type = "server", port = "${port}",
{
"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" } },
},
{
{
type = "rdbg",
request = "launch",
name = "Launch file",
command = "ruby",
script = "${file}",
},
{
type = "rdbg",
request = "attach",
name = "Attach",
localfsMap = "${workspaceFolder}",
},
},
{ "ruby" },
},
{
"mix_task",
{
"elixir-ls-debugger",
"elixir-ls-debug-adapter",
vim.fn.stdpath("data") .. "/mason/packages/elixir-ls/debugger.sh",
},
{
type = "executable",
},
{
{
type = "mix_task",
request = "launch",
name = "mix test",
task = "test",
taskArgs = { "--trace" },
startApps = true,
projectDir = "${workspaceFolder}",
requireFiles = { "test/**/test_helper.exs", "test/**/*_test.exs" },
},
},
{ "elixir" },
},
{
"haskell",
{ "haskell-debug-adapter" },
{
type = "executable",
},
{
{
type = "haskell",
request = "launch",
name = "Launch file",
workspace = "${workspaceFolder}",
startup = "${file}",
stopOnEntry = true,
logFile = vim.fn.stdpath("data") .. "/haskell-dap.log",
logLevel = "WARNING",
ghciEnv = vim.empty_dict(),
ghciPrompt = "H>>= ",
ghciInitialPrompt = "Prelude>",
ghciCmd = "cabal exec -- ghci-dap --interactive -i -i${workspaceFolder}",
},
},
{ "haskell" },
},
{
"ocamlearlybird",
{ "ocamlearlybird" },
{
type = "executable",
args = { "debug" },
},
{
{
type = "ocamlearlybird",
request = "launch",
name = "Launch bytecode",
program = function()
return vim.fn.input("Path to bytecode: ", vim.fn.getcwd() .. "/_build/default/", "file")
end,
stopOnEntry = false,
},
},
{ "ocaml" },
},
}
for _, s in ipairs(simple) do
@@ -77,10 +207,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
@@ -91,11 +224,9 @@ function M.setup()
local dap = require("dap")
-- load adapters with non-trivial logic from separate files
for _, name in ipairs({ "python", "go", "c", "javascript" }) do
for _, name in ipairs({ "python", "go", "c", "javascript", "dart", "bash", "r" }) 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
+5 -4
View File
@@ -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
+20 -21
View File
@@ -2,31 +2,22 @@ local loader = require("turbo-debug.adapters.init")
local M = {}
-- Try in order: active virtualenv, conda, common local venv dirs, system.
-- First executable match wins. Using a single ordered list keeps the probe
-- sequence explicit (ipairs preserves numeric order).
local function find_python()
-- check active virtualenv
local venv = vim.env.VIRTUAL_ENV
if venv then
local path = venv .. "/bin/python"
if vim.fn.executable(path) == 1 then return path end
end
-- check conda
local conda = vim.env.CONDA_PREFIX
if conda then
local path = conda .. "/bin/python"
if vim.fn.executable(path) == 1 then return path end
end
-- probe common venv directories relative to cwd
local candidates = {}
if vim.env.VIRTUAL_ENV then candidates[#candidates + 1] = vim.env.VIRTUAL_ENV .. "/bin/python" end
if vim.env.CONDA_PREFIX then candidates[#candidates + 1] = vim.env.CONDA_PREFIX .. "/bin/python" end
local cwd = vim.fn.getcwd()
for _, dir in ipairs({ ".venv", "venv", "env", ".env" }) do
local path = cwd .. "/" .. dir .. "/bin/python"
if vim.fn.executable(path) == 1 then return path end
candidates[#candidates + 1] = cwd .. "/" .. dir .. "/bin/python"
end
candidates[#candidates + 1] = "python3"
candidates[#candidates + 1] = "python"
for _, p in ipairs(candidates) do
if vim.fn.executable(p) == 1 then return p end
end
-- fallback to system python
if vim.fn.executable("python3") == 1 then return "python3" end
if vim.fn.executable("python") == 1 then return "python" end
return nil
end
@@ -45,6 +36,14 @@ function M.register(dap)
name = "Launch file",
program = "${file}",
pythonPath = function() return find_python() end,
-- integratedTerminal routes the program via RunInTerminalRequest,
-- which nvim-dap spawns in a terminal buffer. dapui registers its
-- Console element as `dap.defaults.fallback.terminal_win_cmd`, so
-- stdout/stderr land in the visible Console pane. Default
-- (internalConsole) sends OutputEvents that nvim-dap routes to the
-- REPL buffer — which turbo-debug's layout deliberately omits, so
-- print() output vanishes. `input()` also works under this mode.
console = "integratedTerminal",
},
}, { "python" })
end
+39
View File
@@ -0,0 +1,39 @@
local loader = require("turbo-debug.adapters.init")
local M = {}
-- vscDebugger is an R package, not a standalone binary. It must be installed
-- in the user's R library (install.packages("vscDebugger") or remotes::install_github).
-- Silently skip registration if it's missing — matches the "no noise when
-- prereqs missing" invariant used by the other adapters.
local function has_vsc_debugger()
if vim.fn.executable("Rscript") ~= 1 then return false end
vim.fn.system({
"Rscript",
"-e",
"if (!requireNamespace('vscDebugger', quietly=TRUE)) quit(status=1)",
})
return vim.v.shell_error == 0
end
function M.register(dap)
local R = loader.find_executable({ "R" })
if not R then return end
if not has_vsc_debugger() then return end
loader.register(dap, "r", {
type = "executable",
command = R,
args = { "-e", "vscDebugger::.vsc.listenForDAP()" },
}, {
{
type = "r",
request = "launch",
name = "Launch R file",
program = "${file}",
cwd = "${workspaceFolder}",
},
}, { "r", "rmd" })
end
return M
+103 -20
View File
@@ -2,29 +2,114 @@ local config = require("turbo-debug.config")
local M = {}
local initialized = false
-- This function is idempotent — every operation inside is safe to re-run.
-- That property matters because turbo-debug.setup() can get called twice in
-- practice:
-- (1) Deferred auto-setup from plugin/turbo-debug.lua, which runs after
-- vim.pack.add has finished registering all deps in runtimepath. The
-- defer is necessary — calling setup synchronously during
-- vim.pack.add would try to `require('persistent-breakpoints.api')`
-- before the plugin's lua/ directory is on the runtimepath and
-- crash with "module not found".
-- (2) User's explicit setup({...}) in their init.lua with config
-- overrides. It runs immediately (before the deferred auto-setup).
--
-- Previously a single `initialized` flag guarded this function so it only
-- ran once. That guard was fragile: if the first call errored partway
-- (case above), the flag was set but the setup was incomplete, and the
-- second call skipped the retry. We now let every call do its full work;
-- persistent-breakpoints.setup re-reads its bps file, sign_define overrides
-- a previous definition, nvim_set_hl overrides a previous link, and
-- vim.keymap.set overrides a previous binding.
function M.setup()
if initialized then return end
initialized = true
require("persistent-breakpoints").setup({
load_breakpoints_event = { "BufReadPost" },
})
-- sign highlights (default = true so colorschemes can override)
vim.api.nvim_set_hl(0, "DapBreakpoint", { default = true, fg = "#e06c75" })
vim.api.nvim_set_hl(0, "DapBreakpointCondition", { default = true, fg = "#e5c07b" })
vim.api.nvim_set_hl(0, "DapBreakpointRejected", { default = true, fg = "#5c6370" })
vim.api.nvim_set_hl(0, "DapStopped", { default = true, fg = "#98c379" })
vim.api.nvim_set_hl(0, "DapStoppedLine", { default = true, bg = "#2e3b2e" })
vim.api.nvim_set_hl(0, "DapLogPoint", { default = true, fg = "#61afef" })
-- link sign highlights to standard diagnostic groups so they pick up the
-- user's colorscheme instead of hardcoding hex colors.
vim.api.nvim_set_hl(0, "DapBreakpoint", { default = true, link = "DiagnosticError" })
vim.api.nvim_set_hl(0, "DapBreakpointCondition", { default = true, link = "DiagnosticWarn" })
vim.api.nvim_set_hl(0, "DapBreakpointRejected", { default = true, link = "DiagnosticHint" })
vim.api.nvim_set_hl(0, "DapStopped", { default = true, link = "DiagnosticOk" })
vim.api.nvim_set_hl(0, "DapStoppedLine", { default = true, link = "Search" })
vim.api.nvim_set_hl(0, "DapLogPoint", { default = true, link = "DiagnosticInfo" })
vim.fn.sign_define("DapBreakpoint", { text = "🔴", texthl = "DapBreakpoint" })
vim.fn.sign_define("DapBreakpointCondition", { text = "🟡", texthl = "DapBreakpointCondition" })
vim.fn.sign_define("DapBreakpointRejected", { text = "⭕", texthl = "DapBreakpointRejected" })
vim.fn.sign_define("DapStopped", { text = "➤", texthl = "DapStopped", linehl = "DapStoppedLine" })
vim.fn.sign_define("DapLogPoint", { text = "📝", texthl = "DapLogPoint" })
-- Gutter signs: colorful emoji for instant recognition. Modern terminals
-- (Ghostty, Kitty, WezTerm, iTerm) render these full-color.
-- 🔴 U+1F534 large red circle = breakpoint
-- 🟡 U+1F7E1 large yellow circle = conditional breakpoint
-- ⭕ 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" },
{ "DapBreakpointRejected", "\xe2\xad\x95" },
{ "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
vim.fn.sign_define(s[1], spec)
end
-- persistent-breakpoints.setup() only installs its BufReadPost autocmd —
-- for files opened from the command line (`nvim foo.py`), BufReadPost has
-- already fired before our deferred plugin/turbo-debug.lua setup runs, so
-- the autocmd misses the startup buffer and saved breakpoints never
-- reappear. load_breakpoints() iterates all listed buffers and is
-- idempotent, so one post-sign_define sweep catches the missed ones
-- without a flicker of dap's default 'B' sign. Placed after sign_define
-- so the restored breakpoints render with our emoji from the first paint.
pcall(require("persistent-breakpoints.api").load_breakpoints)
-- unobtrusive inline virtual text: italic, comment-colored
vim.api.nvim_set_hl(0, "NvimDapVirtualText", { default = true, link = "Comment", italic = true })
vim.api.nvim_set_hl(0, "NvimDapVirtualTextChanged", { default = true, link = "DiagnosticWarn", italic = true })
vim.api.nvim_set_hl(0, "NvimDapVirtualTextError", { default = true, link = "DiagnosticError", italic = true })
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" })
vim.api.nvim_set_hl(0, "TurboDebugHelpSectionIcon", { default = true, link = "Constant" })
vim.api.nvim_set_hl(0, "TurboDebugHelpKey", { default = true, link = "Special" })
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
-- inverted-color badge that absolutely demands attention. nvim_set_hl
-- can't combine link + bold, so copy attrs explicitly and re-apply on
-- ColorScheme so theme switches don't blank it out.
local function apply_ip_reason_hl()
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,
})
end
apply_ip_reason_hl()
vim.api.nvim_create_autocmd("ColorScheme", {
group = vim.api.nvim_create_augroup("TurboDebugColors", { clear = true }),
callback = apply_ip_reason_hl,
})
-- global mappings (work outside debug mode)
local gk = config.opts.global_keys
@@ -35,9 +120,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
+81 -23
View File
@@ -1,9 +1,37 @@
local M = {}
M.defaults = {
help_on_enter = true,
-- The 3-row control bar shows `(?)help` so users can always see where to
-- get the key reference. No auto-popup needed on entry.
help_on_enter = false,
builtin_adapters = true,
quit_exits_mode = true, -- q terminates AND exits debug mode (false = terminate only)
-- When `c` is pressed with no breakpoints set, launch the first adapter
-- configuration for the filetype with stopOnEntry 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.
stop_on_entry_when_no_breakpoints = true,
-- Maximum per-line character width for variable values rendered in dapui
-- panes. Anything over this gets truncated with `…`. Set to 0 to disable.
-- Default 2000 only catches pathological multi-KB register/memory dumps;
-- normal values pass through and wrap naturally inside the pane.
max_value_width = 2000,
-- Throttle window (ms) for console repaints when the debugged program
-- outputs. Trailing-edge — max 1000/console_refresh_ms redraws per second.
console_refresh_ms = 50,
-- Scope names (from the DAP adapter) to default-collapse. dapui auto-
-- collapses scopes that the adapter marks `expensive`, but many adapters
-- (codelldb for Rust/C++, for example) don't mark Registers expensive
-- even though it's a huge dump. Any scope name in this list is treated
-- as expensive by intercepting the `scopes` response before dapui sees
-- it. Users can click/<CR> to expand manually.
collapsed_scopes = { "Registers" },
-- Clear the Console pane on session start/restart.
clear_console_on_start = true,
-- modal keys (active only during debug mode, single-key)
keys = {
@@ -31,40 +59,70 @@ M.defaults = {
clear_breakpoints = "<leader>dD",
},
-- sidebar position: "left" (default) or "right"
sidebar = "left",
-- Optional colorscheme to switch to while debug mode is active.
-- `nil` (default) keeps the user's current colorscheme. Any other
-- string value is passed directly to `:colorscheme`. On M.exit the
-- previous colorscheme is restored.
colorscheme = nil,
-- highlight the active dapui pane's border/separator vs. dim inactive panes
active_window_highlight = true,
dapui = {
icons = { expanded = "▼", collapsed = "▶", current_frame = "➤" },
-- Unicode icons via byte escapes so they survive tool round-trips.
-- ▾ 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",
current_frame = "\xe2\x96\xb6",
},
controls = {
enabled = true,
element = "repl",
-- ▶ 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",
disconnect = "\xe2\x8a\x97",
},
floating = { border = "rounded" },
},
floating = { border = "rounded", mappings = { close = { "q", "<Esc>" } } },
render = { indent = 2, max_type_length = 20 },
layouts = {
{
position = "right",
size = 40,
elements = {
{ id = "scopes", size = 0.50 },
{ id = "watches", size = 0.25 },
{ id = "stacks", size = 0.25 },
},
},
{
position = "bottom",
size = 12,
elements = {
{ id = "repl", size = 0.50 },
{ id = "console", size = 0.50 },
},
},
},
-- dapui's `expand_lines` spawns a cursor-pinned floating window when a
-- line is longer than the pane width — that float bleeds across the
-- entire screen for long register dumps. We wrap inside the pane instead
-- (wrap=true + breakindent + showbreak in set_dapui_window_opts), so this
-- hover is both redundant and destructive. Kill it.
expand_lines = false,
-- layouts are built dynamically from `sidebar` in mode.lua; override
-- `layouts` here to fully customize.
layouts = nil,
},
virtual_text = {
enabled = true,
commented = true,
virt_text_pos = "eol",
show_stop_reason = true,
highlight_changed_variables = true,
highlight_new_as_changed = false,
only_first_definition = false,
all_frames = false,
display_callback = function(variable)
if #variable.value > 80 then return " = " .. variable.value:sub(1, 77) .. "…" end
return " = " .. variable.value
end,
},
adapters = {},
+158 -70
View File
@@ -4,90 +4,193 @@ local M = {}
local win_id = nil
local buf_id = nil
local timer = nil
local modal_info = {
{ "continue", "Continue" },
{ "step_over", "Step over" },
{ "step_into", "Step into" },
{ "step_out", "Step out" },
{ "run_to_cursor", "Run to cursor" },
{ "breakpoint", "Toggle breakpoint" },
{ "cond_breakpoint", "Conditional breakpoint" },
{ "clear_breakpoints", "Clear all breakpoints" },
{ "watch", "Watch expression" },
{ "hover", "Hover / inspect" },
{ "eval", "Eval in REPL" },
{ "terminate", "Quit debugging" },
{ "restart", "Restart session" },
-- Each entry:
-- { action, key_letter, pre, post }
-- The formatted line is: " <pre>(<key>)<post>" with <key> taken from the
-- 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" },
}
local global_info = {
{ "toggle", "Toggle debug mode" },
{ "breakpoint", "Toggle breakpoint" },
{ "cond_breakpoint", "Conditional breakpoint" },
{ "clear_breakpoints", "Clear all breakpoints" },
local breakpoints = {
{ "breakpoint", "x", "(", ") toggle breakpoint" },
{ "cond_breakpoint", "X", "(", ") conditional breakpoint" },
{ "clear_breakpoints", "D", "(", ")elete all breakpoints" },
}
local function build_content()
local lines = { " turbo-debug", "" }
local width = #lines[1]
local inspect = {
{ "watch", "W", "(", ")atch expression" },
{ "hover", "K", "(", ") hover / inspect" },
{ "eval", "E", "(", ")val in REPL" },
}
local function add(key, label)
local line = string.format(" %-12s %s", key, label)
local meta = {
{ "help", "?", "(", ") this popup" },
}
local globals = {
{ "breakpoint", "toggle breakpoint" },
{ "cond_breakpoint", "conditional breakpoint" },
{ "clear_breakpoints", "delete all breakpoints" },
}
local dapui_panes = {
{ "<CR>", "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)" },
}
-- ▶ 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_INSPECT = "\xe2\x97\x89"
local ICON_META = "\xe2\x97\x86"
local ICON_CHEVRON = "\xe2\x9d\xaf"
local ICON_STACK = "\xe2\x89\xa1"
-- a brand header and tagline
local BRAND_TITLE = " " .. ICON_FLOW .. " TURBO DEBUG"
local BRAND_TAGLINE = " modal text + GUI debugger · left hand keys, right hand mouse"
local function build()
local lines = { "", BRAND_TITLE, BRAND_TAGLINE, "" }
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] = { 2, 0, #BRAND_TAGLINE, "TurboDebugHelpTagline" }
local function section(icon, title)
lines[#lines + 1] = " " .. icon .. " " .. title
local idx = #lines - 1
hl[#hl + 1] = { idx, 2, 2 + #icon, "TurboDebugHelpSectionIcon" }
hl[#hl + 1] = { idx, 2 + #icon + 2, 2 + #icon + 2 + #title, "TurboDebugHelpSection" }
end
local function entry(pre, keyletter, post)
-- format: " <pre>(<keyletter>)<tail>"
-- e.g., " (s)tep over" or " run to (C) cursor"
-- the key letter is inside the parens pair
local indent = " "
-- pre already ends at "("; post already starts with ")"
local line = indent .. pre .. keyletter .. post
local idx = #lines
lines[#lines + 1] = line
if #line > width then width = #line end
-- highlight: the key letter itself
local col = #indent + #pre
hl[#hl + 1] = { idx, col, col + #keyletter, "TurboDebugHelpKey" }
-- highlight the surrounding parens dimmer
hl[#hl + 1] = { idx, col - 1, col, "TurboDebugHelpParen" }
hl[#hl + 1] = { idx, col + #keyletter, col + #keyletter + 1, "TurboDebugHelpParen" }
end
lines[#lines + 1] = " Debug mode:"
for _, pair in ipairs(modal_info) do
local key = config.opts.keys[pair[1]]
if key then add(key, pair[2]) end
local function add_modal(list)
for _, row in ipairs(list) do
local action, letter, pre, post = row[1], row[2], row[3], row[4]
local configured = config.opts.keys[action]
if configured then
-- if the user remapped, show their actual key (preserves surprise-free behavior)
local lk = configured
entry(pre, lk, post)
end
end
end
local function add_global(list)
for _, row in ipairs(list) do
local action, label = row[1], row[2]
local key = config.opts.global_keys[action]
if key then
local indent = " "
local line = indent .. string.format("%-13s ", key) .. label
local idx = #lines
lines[#lines + 1] = line
hl[#hl + 1] = { idx, #indent, #indent + #key, "TurboDebugHelpKey" }
end
end
end
local function add_pane(list)
for _, row in ipairs(list) do
local key, label = row[1], row[2]
local indent = " "
local line = indent .. string.format("%-6s ", key) .. label
local idx = #lines
lines[#lines + 1] = line
hl[#hl + 1] = { idx, #indent, #indent + #key, "TurboDebugHelpKey" }
end
end
section(ICON_FLOW, "Flow")
add_modal(flow)
lines[#lines + 1] = ""
section(ICON_BRK, "Breakpoints")
add_modal(breakpoints)
lines[#lines + 1] = ""
section(ICON_INSPECT, "Inspect")
add_modal(inspect)
lines[#lines + 1] = ""
section(ICON_META, "Help")
add_modal(meta)
lines[#lines + 1] = ""
section(ICON_CHEVRON, "Always available")
add_global(globals)
lines[#lines + 1] = ""
section(ICON_STACK, "In dap-ui panes")
add_pane(dapui_panes)
lines[#lines + 1] = ""
lines[#lines + 1] = " Always available:"
for _, pair in ipairs(global_info) do
local key = config.opts.global_keys[pair[1]]
if key then add(key, pair[2]) end
end
-- highlight line at the bottom
local help_key = config.opts.keys.help or "?"
lines[#lines + 1] = ""
lines[#lines + 1] = " Press " .. help_key .. " for help"
lines[#lines + 1] = " press " .. help_key .. " anytime to reopen"
lines[#lines + 1] = ""
return lines, width + 2
local width = 0
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 timer then
timer:stop()
timer:close()
timer = nil
end
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
local function show(auto_dismiss)
function M.open()
M.close()
local lines, width = build_content()
local lines, width, hl = build()
local height = #lines
buf_id = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, lines)
local hl_line = height - 2
vim.api.nvim_buf_add_highlight(buf_id, -1, "Bold", hl_line, 0, -1)
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
vim.bo[buf_id].modifiable = false
vim.bo[buf_id].bufhidden = "wipe"
@@ -107,27 +210,12 @@ local function show(auto_dismiss)
noautocmd = true,
})
if auto_dismiss then
timer = vim.uv.new_timer()
timer:start(3000, 0, vim.schedule_wrap(function() M.close() end))
else
vim.api.nvim_create_autocmd({ "CursorMoved", "InsertEnter", "CmdlineEnter" }, {
once = true,
callback = function() M.close() end,
})
end
end
function M.open()
show(false)
end
function M.splash()
show(true)
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
+19 -7
View File
@@ -4,6 +4,21 @@ local mode = require("turbo-debug.mode")
local M = {}
function M.setup(opts)
-- Self-defer if dependencies aren't yet on the runtimepath. This happens
-- when the user calls `setup({...})` from their init.lua right after
-- `vim.pack.add(..., turbo-debug)`: our plugin/turbo-debug.lua fires
-- nested `vim.pack.add` calls for dap/dapui/persistent-breakpoints, but
-- those nested adds only register the paths on the NEXT event loop tick
-- — so a synchronous follow-up `require('persistent-breakpoints')`
-- crashes with "module not found". `vim.schedule` lets the pack module
-- finalize before we retry. `_setup_called` still gets set on the retry,
-- so plugin/turbo-debug.lua's own deferred auto-setup correctly skips.
if not pcall(require, "persistent-breakpoints") then
vim.schedule(function() M.setup(opts) end)
return
end
M._setup_called = true
config.merge(opts)
require("turbo-debug.breakpoints").setup()
require("turbo-debug.adapters").setup()
@@ -12,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
+1423 -135
View File
File diff suppressed because it is too large. Load diff
+16 -5
View File
@@ -12,9 +12,20 @@ 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({ { "<leader>d", group = "debug" } })
end)
pcall(function() require("which-key").add({ { "<leader>d", group = "debug" } }) end)
-- setup immediately (idempotent — user can call setup() again with overrides)
require("turbo-debug").setup()
-- Deferred auto-setup. Must be deferred (not called synchronously) because
-- the vim.pack.add calls above might not have finished registering the
-- dep modules in runtimepath by the time this script's top-level runs —
-- a synchronous `require('persistent-breakpoints.api')` inside setup would
-- crash with "module not found". vim.schedule guarantees we run AFTER the
-- current event loop tick, at which point pack has finalized the paths.
--
-- If the user calls setup({...}) themselves (typically from init.lua after
-- vim.pack.add), `M._setup_called` flips true and this deferred call
-- becomes a no-op — otherwise we'd overwrite their user opts with defaults.
vim.schedule(function()
local td = require("turbo-debug")
if td._setup_called then return end
pcall(td.setup)
end)
+17
View File
@@ -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.
+8 -2
View File
@@ -1,9 +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)))
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
end
function M.assert_false(v, msg)
if v then error(string.format("FAIL: %s — expected falsy, got %s", msg, vim.inspect(v))) end
end
return M
+1 -1
View File
@@ -9,7 +9,7 @@ assert_eq(loader.find_executable({ "totally-fake-binary-xyz" }), nil, "non-exist
assert_eq(loader.find_executable({ "sh" }) ~= nil, true, "find_executable finds sh")
-- all adapter modules load and have register()
for _, name in ipairs({ "python", "go", "c", "javascript" }) do
for _, name in ipairs({ "python", "go", "c", "javascript", "dart", "bash", "r" }) do
assert_eq(type(require("turbo-debug.adapters." .. name).register), "function", name .. " has register()")
end
+7 -5
View File
@@ -7,8 +7,10 @@ assert_eq(opts.keys.continue, "c", "default continue key")
assert_eq(opts.keys.breakpoint, "x", "default modal breakpoint key")
assert_eq(opts.global_keys.toggle, "<leader>dd", "default toggle key")
assert_eq(opts.global_keys.breakpoint, "<leader>dx", "default global breakpoint key")
assert_eq(opts.help_on_enter, true, "default help_on_enter")
assert_eq(opts.quit_exits_mode, true, "default quit_exits_mode")
assert_eq(opts.help_on_enter, false, "default help_on_enter")
assert_eq(opts.stop_on_entry_when_no_breakpoints, true, "default stop_on_entry_when_no_breakpoints")
assert_eq(opts.max_value_width, 2000, "default max_value_width")
assert_eq(opts.console_refresh_ms, 50, "default console_refresh_ms")
-- partial key override
opts = config.merge({ keys = { continue = "g" } })
@@ -24,9 +26,9 @@ opts = config.merge({ global_keys = { toggle = "<leader>D" } })
assert_eq(opts.global_keys.toggle, "<leader>D", "overridden toggle key")
assert_eq(opts.global_keys.breakpoint, "<leader>dx", "preserved breakpoint key")
-- quit_exits_mode override
opts = config.merge({ quit_exits_mode = false })
assert_eq(opts.quit_exits_mode, false, "overridden quit_exits_mode")
-- max_value_width override
opts = config.merge({ max_value_width = 0 })
assert_eq(opts.max_value_width, 0, "overridden max_value_width")
-- merge doesn't mutate defaults
config.merge({ keys = { continue = "z" } })
+2 -2
View File
@@ -10,13 +10,13 @@ assert_eq(help.is_open(), true, "help window is open")
help.close()
assert_eq(help.is_open(), false, "help window is closed")
-- remapped key shows in help content
-- remapped key shows in help content (new format wraps the key letter in parens)
config.merge({ keys = { continue = "g" } })
help.open()
local found_g = false
for _, w in ipairs(vim.api.nvim_list_wins()) do
for _, line in ipairs(vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(w), 0, -1, false)) do
if line:find("g%s+Continue") then found_g = true end
if line:find("%(g%)") then found_g = true end
end
end
assert_eq(found_g, true, "remapped key 'g' appears in help")
+49
View File
@@ -0,0 +1,49 @@
-- Unit tests for the helpers introduced during cleanup refactor.
-- Exercises helpers indirectly via observable side effects on bar/pane state.
local h = require("helpers")
local assert_eq = h.assert_eq
local assert_true = h.assert_true
local assert_false = h.assert_false
local mode = require("turbo-debug.mode")
local config = require("turbo-debug.config")
config.merge({})
-- keyof ---------------------------------------------------------------------
-- Reset, then exercise the three return paths.
config.merge({})
assert_eq(mode._test.keyof("continue", "c"), "c", "default key")
config.merge({ keys = { continue = "n" } })
assert_eq(mode._test.keyof("continue", "c"), "n", "user override")
config.merge({ keys = { continue = false } })
assert_eq(mode._test.keyof("continue", "c"), nil, "disabled returns nil")
config.merge({})
-- source_window -------------------------------------------------------------
-- Skips bars/dapui-panes; returns a source window when one exists.
do
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].filetype = "lua"
vim.api.nvim_set_current_buf(buf)
local w = mode._test.source_window()
assert_true(w ~= nil, "source_window finds lua buffer")
end
do
-- Switch current to a dapui-pane filetype; source_window should still
-- find another source window if one exists.
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].filetype = "dapui_scopes"
vim.cmd("split")
vim.api.nvim_set_current_buf(buf)
local w = mode._test.source_window()
if w then
local ft = vim.bo[vim.api.nvim_win_get_buf(w)].filetype
assert_false(ft:match("^dapui_"), "source_window skips dapui_* filetypes")
assert_true(ft ~= "dap-repl", "source_window skips dap-repl")
assert_true(ft ~= "TurboDebugBar", "source_window skips TurboDebugBar")
end
end
print("ALL HELPER TESTS PASSED")
+40
View File
@@ -0,0 +1,40 @@
local h = require("helpers")
local assert_eq = h.assert_eq
local assert_true = h.assert_true
local mode = require("turbo-debug.mode")
local config = require("turbo-debug.config")
config.merge({})
-- Invoke define_highlights via the _test hook.
mode._test.define_highlights()
-- The three ColorScheme augroups must exist with at least one autocmd each.
local function group_has_colorscheme_autocmd(group_name)
local ok, autocmds = pcall(vim.api.nvim_get_autocmds, { group = group_name, event = "ColorScheme" })
return ok and autocmds and #autocmds >= 1
end
assert_true(group_has_colorscheme_autocmd("TurboDebugBarItalicHL"), "italic augroup has ColorScheme autocmd")
assert_true(group_has_colorscheme_autocmd("TurboDebugBarKeyHL"), "key augroup has ColorScheme autocmd")
assert_true(group_has_colorscheme_autocmd("TurboDebugBarSepHL"), "sep augroup has ColorScheme autocmd")
-- The highlight groups must exist after define_highlights.
local function hl_exists(name)
local ok, hl = pcall(vim.api.nvim_get_hl, 0, { name = name })
return ok and hl ~= nil and next(hl) ~= nil
end
assert_true(hl_exists("TurboDebugBarItalic"), "TurboDebugBarItalic exists")
assert_true(hl_exists("TurboDebugBarKey"), "TurboDebugBarKey exists")
assert_true(hl_exists("TurboDebugBarSeparator"), "TurboDebugBarSeparator exists")
assert_true(hl_exists("TurboDebugBar"), "TurboDebugBar exists")
assert_true(hl_exists("TurboDebugBarBrand"), "TurboDebugBarBrand exists")
-- Fire ColorScheme event; the callbacks should re-apply highlights (idempotent).
vim.cmd("doautocmd ColorScheme")
assert_true(hl_exists("TurboDebugBarItalic"), "TurboDebugBarItalic still exists after ColorScheme")
assert_true(hl_exists("TurboDebugBarKey"), "TurboDebugBarKey still exists after ColorScheme")
assert_true(hl_exists("TurboDebugBarSeparator"), "TurboDebugBarSeparator still exists after ColorScheme")
print("ALL HIGHLIGHT CS TESTS PASSED")
+145
View File
@@ -0,0 +1,145 @@
local h = require("helpers")
local assert_eq = h.assert_eq
local assert_true = h.assert_true
local assert_false = h.assert_false
local mode = require("turbo-debug.mode")
local config = require("turbo-debug.config")
-- Ensure config is initialized with defaults so keyof/render_cbar can read it
config.merge({})
-- Helpers --------------------------------------------------------------------
local function make_bar_win()
vim.cmd("noautocmd keepalt topleft split")
local win = vim.api.nvim_get_current_win()
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_win_set_buf(win, buf)
vim.api.nvim_win_set_height(win, 2)
return win, buf
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
end
local function line(buf, i) return vim.api.nvim_buf_get_lines(buf, i, i + 1, false)[1] or "" end
-- build_control_text -------------------------------------------------------
-- Inline form when key matches first letter case-insensitively.
local text, kcol, kend, icol, iend = mode._test.build_control_text(">", "s", "step", "over")
assert_true(text:find("%(s%)tep") ~= nil, "inline form: (s)tep present")
assert_true(kcol < kend, "inline form: key_col < key_end_col")
assert_true(icol ~= nil and iend ~= nil, "inline form: italic span present")
assert_true(text:sub(icol + 1, iend) == "over", "inline form: italic text is 'over'")
-- Prefix form when key does NOT match first letter.
local text2, _, _, icol2 = mode._test.build_control_text(">", "n", "step", "over")
assert_true(text2:find("%(n%) step") ~= nil, "prefix form: (n) step present")
assert_true(icol2 ~= nil, "prefix form: italic still rendered")
-- No italic when italic param is nil.
local text3, _, _, icol3 = mode._test.build_control_text(">", "c", "continue", nil)
assert_eq(icol3, nil, "no italic span when italic=nil")
assert_true(text3:find("%(c%)ontinue") ~= nil, "no italic: (c)ontinue rendered")
-- keyof ----------------------------------------------------------------------
-- Save any user override then reset to defaults
config.merge({})
assert_eq(mode._test.keyof("continue", "c"), "c", "keyof returns default when unset")
-- Disabled action returns nil
config.merge({ keys = { continue = false } })
assert_eq(mode._test.keyof("continue", "c"), nil, "keyof returns nil when key=false")
-- User override
config.merge({ keys = { continue = "g" } })
assert_eq(mode._test.keyof("continue", "c"), "g", "keyof returns user override")
-- Empty string falls back to default
config.merge({ keys = { continue = "" } })
assert_eq(mode._test.keyof("continue", "c"), "c", "keyof falls back on empty string")
-- Reset config to defaults for subsequent render tests
config.merge({})
-- render_sbar ----------------------------------------------------------------
do
local win, buf = make_bar_win()
mode._test.set_bars(win, buf, nil, nil)
mode._test.render_sbar()
local count = vim.api.nvim_buf_line_count(buf)
assert_eq(count, 2, "sbar has exactly 2 lines")
local sep = line(buf, 0)
local content = line(buf, 1)
assert_true(#sep > 0, "sbar separator non-empty")
assert_true(content:find(" turbo%-debug ") ~= nil, "sbar contains brand text")
assert_true(content:find("DEBUG") ~= nil, "sbar contains DEBUG chip")
-- sbar has no click zones
local zones = mode._test.get_sbar_zones()
assert_eq(#zones, 0, "sbar has zero click zones")
mode._test.set_bars(nil, nil, nil, nil)
cleanup(win)
end
-- render_cbar ----------------------------------------------------------------
do
local win, buf = make_bar_win()
vim.api.nvim_win_set_width(win, 120)
mode._test.set_bars(nil, nil, win, buf)
mode._test.render_cbar()
local count = vim.api.nvim_buf_line_count(buf)
assert_eq(count, 2, "cbar has exactly 2 lines")
local content = line(buf, 0)
local sep = line(buf, 1)
assert_true(#sep > 0, "cbar separator non-empty")
assert_true(content:find(" help ") ~= nil, "cbar contains 'help' text")
-- Click zones: at least 5 controls (continue, step_over, step_into, step_out, terminate) + 1 help
-- When no session, restart is hidden so zone count = 5 + 1 = 6.
local zones = mode._test.get_cbar_zones()
assert_true(#zones >= 6, "cbar has at least 6 click zones: " .. #zones)
mode._test.set_bars(nil, nil, nil, nil)
cleanup(win)
end
-- overflow fallback: at narrow widths, italic qualifiers are dropped
do
local win, buf = make_bar_win()
vim.api.nvim_win_set_width(win, 25)
mode._test.set_bars(nil, nil, win, buf)
mode._test.render_cbar()
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"
)
mode._test.set_bars(nil, nil, nil, nil)
cleanup(win)
end
-- disabling step_over reduces zone count
do
local win, buf = make_bar_win()
vim.api.nvim_win_set_width(win, 120)
mode._test.set_bars(nil, nil, win, buf)
mode._test.render_cbar()
local default_count = #mode._test.get_cbar_zones()
config.merge({ keys = { step_over = false } })
mode._test.render_cbar()
local disabled_count = #mode._test.get_cbar_zones()
assert_eq(disabled_count, default_count - 1, "disabling step_over drops one zone")
config.merge({}) -- reset
mode._test.set_bars(nil, nil, nil, nil)
cleanup(win)
end
print("ALL RENDER BAR TESTS PASSED")
-1120
View File
File diff suppressed because it is too large. Load diff