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.
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.
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.
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`.
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.
`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.
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.
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.
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.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Rename from tiny-debugger to turbo-debug — inspired by Turbo Pascal,
the first IDE with a real visual debugger. This thing isn't tiny,
it's turbo charged.
CHANGELOG for 0.1.0:
Features:
- Zero-config debugging — one line install, everything works
- Modal keybindings (c/s/d/r/q/x/X/D/W/K/E/R/?), active only
during debug mode, cleanly removed on exit
- Persistent breakpoints via persistent-breakpoints.nvim
- Inline virtual text — variable values in source code
- Hover (K), watch (W), REPL eval (E) from the code window
- Help splash (3s auto-dismiss) + manual help (?)
- Debug mode chrome — winbar indicator, cursor highlight
- dapui integration — controls toolbar, labeled panels
- which-key support — <leader>d debug group auto-registered
- quit_exits_mode option
Adapters (12 languages):
- Python (debugpy + venv detection)
- Rust (codelldb + cargo build + vim.ui.select picker)
- Go (delve), C/C++/Swift (codelldb)
- JavaScript/TypeScript (js-debug-adapter)
- C# (netcoredbg), Java, Kotlin, PHP, Ruby, Lua (osv)
Keybindings:
- <leader>dd — toggle debug mode
- <leader>dx / <leader>dX / <leader>dD — breakpoints (always)
- c s d r C x X D W K E q R ? — modal (debug mode only)