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>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-04-12 14:08:17 -06:00
1 parent fb695ce359
commit 988bd06d54
2 files changed
+104 -41

No files matched your search

+12
View File
@@ -22,6 +22,18 @@ M.defaults = {
-- 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 so old output from a
-- previous run doesn't linger. Set false to preserve across runs.
clear_console_on_start = true,
-- modal keys (active only during debug mode, single-key)
keys = {
continue = "c",
+92 -41
View File
@@ -82,11 +82,10 @@ local function build_default_layouts()
local pos = config.opts.sidebar == "right" and "right" or "left"
-- Console height: 15% of the available vertical space, floored at 5 rows.
-- "Available" = screen rows minus our two 3-row bars, the statusline,
-- and the cmdline. With Breakpoints at 25% of a much-taller sidebar,
-- Bp visibly exceeds Console so the user can see they're separate panes.
local top_bar_rows = 3
local bottom_bar_rows = 3
-- "Available" = screen rows minus the two 2-row bars, the statusline,
-- and the cmdline.
local top_bar_rows = 2
local bottom_bar_rows = 2
local statusline_rows = vim.o.laststatus > 0 and 1 or 0
local cmdline_rows = math.max(1, vim.o.cmdheight)
local avail = vim.o.lines - top_bar_rows - bottom_bar_rows - statusline_rows - cmdline_rows
@@ -270,31 +269,27 @@ local function render_sbar()
local pad_right = remain - pad_left
local content_line = brand_text .. string.rep(" ", pad_left) .. status_msg .. string.rep(" ", pad_right) .. state_chip
-- 3 rows: sep / content / sep
-- 2 rows: sep / content (no bottom separator — dapui's pane borders
-- act as the visual break between the bar and the source area below)
vim.bo[sbar_buf].modifiable = true
vim.api.nvim_buf_set_lines(sbar_buf, 0, -1, false, { sep, content_line, sep })
vim.api.nvim_buf_set_lines(sbar_buf, 0, -1, false, { sep, content_line })
vim.bo[sbar_buf].modifiable = false
vim.api.nvim_buf_clear_namespace(sbar_buf, bar_ns, 0, -1)
-- separators
-- top separator
pcall(vim.api.nvim_buf_set_extmark, sbar_buf, bar_ns, 0, 0, {
end_row = 0, end_col = #sep, hl_group = "TurboDebugBarSeparator",
})
pcall(vim.api.nvim_buf_set_extmark, sbar_buf, bar_ns, 2, 0, {
end_row = 2, end_col = #sep, hl_group = "TurboDebugBarSeparator",
})
-- content: brand on left
pcall(vim.api.nvim_buf_set_extmark, sbar_buf, bar_ns, 1, 0, {
end_row = 1, end_col = #brand_text, hl_group = "TurboDebugBarBrand",
})
-- status message in the middle
if status_dw > 0 then
local status_byte_start = #brand_text + pad_left
pcall(vim.api.nvim_buf_set_extmark, sbar_buf, bar_ns, 1, status_byte_start, {
end_row = 1, end_col = status_byte_start + #status_msg, hl_group = "TurboDebugBarStatus",
})
end
-- state chip on right
local chip_byte_start = #content_line - #state_chip
pcall(vim.api.nvim_buf_set_extmark, sbar_buf, bar_ns, 1, chip_byte_start, {
end_row = 1, end_col = #content_line, hl_group = state_hl,
@@ -317,8 +312,8 @@ local function render_cbar()
-- Modal control labels. State-dependent text so the label shows what
-- the key will ACTUALLY do right now:
-- no session: (c) start (q)uit
-- active: (c)ontinue (q) terminate
-- no session: (c) start (q)uit (Restart hidden)
-- active: (c)ontinue (q) terminate (R)estart shown
local has_session = state ~= "READY"
local continue_icon = has_session and ICON.go or ICON.start_rkt
local continue_tail = has_session and "ontinue" or " start"
@@ -329,9 +324,12 @@ local function render_cbar()
{ icon = ICON.step_over, key = keyof("step_over", "s"), tail = "tep", fn = action("step_over") },
{ icon = ICON.step_into, key = keyof("step_into", "d"), tail = "escend", fn = action("step_into") },
{ icon = ICON.step_out, key = keyof("step_out", "r"), tail = "eturn", fn = action("step_out") },
{ icon = ICON.restart, key = keyof("restart", "R"), tail = "estart", fn = action("restart") },
{ icon = ICON.stop, key = keyof("terminate", "q"), tail = quit_tail, fn = action("terminate") },
}
-- (R)estart is only meaningful during an active session
if has_session then
controls[#controls + 1] = { icon = ICON.restart, key = keyof("restart", "R"), tail = "estart", fn = action("restart") }
end
controls[#controls + 1] = { icon = ICON.stop, key = keyof("terminate", "q"), tail = quit_tail, fn = action("terminate") }
local ctrl_parts = {}
local ctrl_spans = {}
@@ -361,36 +359,35 @@ local function render_cbar()
if ctrl_pad_right < 1 then ctrl_pad_right = 1 end
local content_line = string.rep(" ", ctrl_pad_left) .. controls_text .. string.rep(" ", ctrl_pad_right) .. help_text
-- 3 rows: sep / content / sep
-- 2 rows: content / sep (no top separator — the source/dapui area
-- above has its own pane borders as the visual break)
vim.bo[cbar_buf].modifiable = true
vim.api.nvim_buf_set_lines(cbar_buf, 0, -1, false, { sep, content_line, sep })
vim.api.nvim_buf_set_lines(cbar_buf, 0, -1, false, { content_line, sep })
vim.bo[cbar_buf].modifiable = false
vim.api.nvim_buf_clear_namespace(cbar_buf, bar_ns, 0, -1)
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 0, 0, {
end_row = 0, end_col = #sep, hl_group = "TurboDebugBarSeparator",
})
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 2, 0, {
end_row = 2, end_col = #sep, hl_group = "TurboDebugBarSeparator",
-- bottom separator (on row 1 now since content is on row 0)
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 1, 0, {
end_row = 1, end_col = #sep, hl_group = "TurboDebugBarSeparator",
})
-- control labels (content is on row 1, index 1)
-- control labels on row 0 (content row)
local ctrl_byte_offset = ctrl_pad_left
for _, span in ipairs(ctrl_spans) do
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 1, ctrl_byte_offset + span[1], {
end_row = 1, end_col = ctrl_byte_offset + span[2], hl_group = span[3],
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 0, ctrl_byte_offset + span[1], {
end_row = 0, end_col = ctrl_byte_offset + span[2], hl_group = span[3],
})
end
for _, span in ipairs(ctrl_key_spans) do
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 1, ctrl_byte_offset + span[1], {
end_row = 1, end_col = ctrl_byte_offset + span[2], hl_group = "TurboDebugBarKey",
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 0, ctrl_byte_offset + span[1], {
end_row = 0, end_col = ctrl_byte_offset + span[2], hl_group = "TurboDebugBarKey",
})
end
-- help (right-anchored)
-- help (right-anchored) on row 0
local help_byte_start = #content_line - #help_text
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 1, help_byte_start, {
end_row = 1, end_col = #content_line, hl_group = "TurboDebugBarCtrl",
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 0, help_byte_start, {
end_row = 0, end_col = #content_line, hl_group = "TurboDebugBarCtrl",
})
cbar_zones = {}
@@ -399,12 +396,12 @@ local function render_cbar()
local byte_end = ctrl_byte_offset + z[2]
local dcol_start = vim.fn.strdisplaywidth(content_line:sub(1, byte_start))
local dcol_end = vim.fn.strdisplaywidth(content_line:sub(1, byte_end))
cbar_zones[#cbar_zones + 1] = { 2, dcol_start, dcol_end, z[3] }
-- content is on buffer row 0 = window line 1
cbar_zones[#cbar_zones + 1] = { 1, dcol_start, dcol_end, z[3] }
end
-- help click zone (also on row 2 since the content is the middle row of 3)
local help_dcol_start = vim.fn.strdisplaywidth(content_line:sub(1, help_byte_start))
local help_dcol_end = vim.fn.strdisplaywidth(content_line)
cbar_zones[#cbar_zones + 1] = { 2, help_dcol_start, help_dcol_end,
cbar_zones[#cbar_zones + 1] = { 1, help_dcol_start, help_dcol_end,
function() require("turbo-debug.help").open() end }
end
@@ -485,7 +482,7 @@ local function open_bars()
sbar_win = vim.api.nvim_get_current_win()
sbar_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_win_set_buf(sbar_win, sbar_buf)
vim.api.nvim_win_set_height(sbar_win, 3)
vim.api.nvim_win_set_height(sbar_win, 2)
setup_bar_buf(sbar_buf)
setup_bar_win(sbar_win)
vim.wo[sbar_win].winfixheight = true
@@ -505,7 +502,7 @@ local function open_bars()
cbar_win = vim.api.nvim_get_current_win()
cbar_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_win_set_buf(cbar_win, cbar_buf)
vim.api.nvim_win_set_height(cbar_win, 3)
vim.api.nvim_win_set_height(cbar_win, 2)
setup_bar_buf(cbar_buf)
setup_bar_win(cbar_win)
vim.wo[cbar_win].winfixheight = true
@@ -598,13 +595,26 @@ local function schedule_console_redraw()
end, config.opts.console_refresh_ms or 50)
end
local function ensure_dapui()
if dapui_initialized then return end
dapui_initialized = true
-- Call dapui.setup each M.enter() so the layout is recomputed against
-- current terminal dimensions AND against whatever splits our bars have
-- pinned at top and bottom. Otherwise dapui caches first-call sizes and
-- re-entering debug mode lays out against stale dimensions (Console
-- ballooning, sidebar proportions drifting away from 25% each — the
-- exact bug the user reported on second toggle).
local function refresh_dapui_layouts()
local dapui_opts = vim.deepcopy(config.opts.dapui)
if not dapui_opts.layouts then dapui_opts.layouts = build_default_layouts() end
require("dapui").setup(dapui_opts)
patch_format_value()
end
local function ensure_dapui()
if dapui_initialized then
refresh_dapui_layouts()
return
end
dapui_initialized = true
refresh_dapui_layouts()
local dap = require("dap")
dap.listeners.after.event_initialized["turbo-debug"] = function()
@@ -688,6 +698,42 @@ local function ensure_dapui()
dap.listeners.before.event_terminated["turbo-debug-ip"] = function() clear_ip() end
dap.listeners.before.event_exited["turbo-debug-ip"] = function() clear_ip() end
-- Clear the Console pane on session start/restart. Runs BEFORE the
-- adapter emits output so the new session's log starts clean.
local function clear_console()
if not config.opts.clear_console_on_start then return end
local ok, dapui = pcall(require, "dapui")
if not ok then return end
local buf_ok, buf = pcall(function() return dapui.elements.console.buffer() end)
if not buf_ok or not buf or not vim.api.nvim_buf_is_valid(buf) then return end
pcall(function()
local prev = vim.bo[buf].modifiable
vim.bo[buf].modifiable = true
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {})
vim.bo[buf].modifiable = prev
end)
end
dap.listeners.before.event_initialized["turbo-debug-clear-console"] = clear_console
-- Default-collapse scopes the user considers noisy (Registers on
-- codelldb, for example). We intercept the scopes response and set
-- `expensive = true` on any matching scope — dapui's own component
-- auto-collapses anything marked expensive (see dapui/components/
-- scopes.lua line 22-24), so this reuses their existing mechanism.
dap.listeners.before.scopes["turbo-debug-collapse"] = function(_, _, response, _)
local collapse = config.opts.collapsed_scopes
if not (collapse and #collapse > 0) then return end
if not (response and response.scopes) then return end
for _, scope in ipairs(response.scopes) do
for _, name in ipairs(collapse) do
if scope.name == name then
scope.expensive = true
break
end
end
end
end
-- winbar titles on dapui panes — colorful emoji for instant pane
-- recognition. REPL is iconless per user preference (its prompt is
-- already visually distinct enough).
@@ -978,9 +1024,14 @@ function M.enter()
end
set_debug_chrome()
-- Bars BEFORE dapui.open so the sidebar and bottom tray compute their
-- proportions against the screen MINUS our bars, not against the full
-- screen. Calling ensure_dapui() here re-runs dapui.setup with freshly-
-- computed layout sizes based on current terminal dimensions — this is
-- what fixes the "Console balloons on second toggle" bug.
open_bars()
ensure_dapui()
require("dapui").open()
open_bars()
-- second install pass after dapui has created its buffers
vim.schedule(function()