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
This commit is contained in:
7 files changed
+414
-164
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -42,11 +42,18 @@ function M.setup()
|
||||
-- ⭕ U+2B55 heavy large circle = rejected breakpoint
|
||||
-- 👉 U+1F449 backhand pointing right = current execution line (IP)
|
||||
-- 📝 U+1F4DD memo = log point
|
||||
vim.fn.sign_define("DapBreakpoint", { text = "\xf0\x9f\x94\xb4", texthl = "DapBreakpoint", numhl = "DapBreakpoint" })
|
||||
vim.fn.sign_define("DapBreakpointCondition", { text = "\xf0\x9f\x9f\xa1", texthl = "DapBreakpointCondition", numhl = "DapBreakpointCondition" })
|
||||
vim.fn.sign_define("DapBreakpointRejected", { text = "\xe2\xad\x95", texthl = "DapBreakpointRejected", numhl = "DapBreakpointRejected" })
|
||||
vim.fn.sign_define("DapStopped", { text = "\xf0\x9f\x91\x89", texthl = "DapStopped", linehl = "DapStoppedLine", numhl = "DapStopped" })
|
||||
vim.fn.sign_define("DapLogPoint", { text = "\xf0\x9f\x93\x9d", texthl = "DapLogPoint", numhl = "DapLogPoint" })
|
||||
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" },
|
||||
}
|
||||
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
|
||||
|
||||
-- unobtrusive inline virtual text: italic, comment-colored
|
||||
vim.api.nvim_set_hl(0, "NvimDapVirtualText", { default = true, link = "Comment", italic = true })
|
||||
|
||||
+142
-138
@@ -194,11 +194,14 @@ local function define_highlights()
|
||||
fg = src.fg, italic = true, default = true,
|
||||
})
|
||||
end
|
||||
apply_italic_hl()
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
group = vim.api.nvim_create_augroup("TurboDebugBarItalicHL", { clear = true }),
|
||||
callback = apply_italic_hl,
|
||||
})
|
||||
local function install_cs_listener(name, fn)
|
||||
fn()
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
group = vim.api.nvim_create_augroup(name, { clear = true }),
|
||||
callback = fn,
|
||||
})
|
||||
end
|
||||
install_cs_listener("TurboDebugBarItalicHL", apply_italic_hl)
|
||||
|
||||
-- The key letter inside (c)ontinue etc. gets bold + Special's fg.
|
||||
-- No underline — the descender on lowercase q/g/p/y overlaps the
|
||||
@@ -215,11 +218,7 @@ local function define_highlights()
|
||||
fg = src.fg, bg = src.bg, bold = true, default = true,
|
||||
})
|
||||
end
|
||||
apply_key_hl()
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
group = vim.api.nvim_create_augroup("TurboDebugBarKeyHL", { clear = true }),
|
||||
callback = apply_key_hl,
|
||||
})
|
||||
install_cs_listener("TurboDebugBarKeyHL", apply_key_hl)
|
||||
|
||||
-- Separator color: derive from WinBar.fg at runtime. Every theme
|
||||
-- stylistically renders WinBar (pane-title chrome) in a color
|
||||
@@ -249,11 +248,7 @@ local function define_highlights()
|
||||
end
|
||||
vim.api.nvim_set_hl(0, "TurboDebugBarSeparator", { link = "WinSeparator" })
|
||||
end
|
||||
apply_sep_hl()
|
||||
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||
group = vim.api.nvim_create_augroup("TurboDebugBarSepHL", { clear = true }),
|
||||
callback = apply_sep_hl,
|
||||
})
|
||||
install_cs_listener("TurboDebugBarSepHL", apply_sep_hl)
|
||||
end
|
||||
|
||||
-- winhighlight strings applied to dapui panes. `WinBar:WinBar,WinBarNC:WinBar`
|
||||
@@ -296,8 +291,57 @@ end
|
||||
|
||||
local bar_ns = vim.api.nvim_create_namespace("turbo-debug.bar")
|
||||
|
||||
-- click zones, one table per bar. Each entry: { line_1based, dcol_start, dcol_end, fn }
|
||||
local sbar_zones = {}
|
||||
-- ─── small helpers ──────────────────────────────────────────────────────────
|
||||
local function win_ok(w) return w and vim.api.nvim_win_is_valid(w) end
|
||||
local function buf_ok(b) return b and vim.api.nvim_buf_is_valid(b) end
|
||||
|
||||
-- Exact dapui pane filetypes. Different from the pattern `dapui_*` used in
|
||||
-- setup_active_win_highlights / the FileType autocmd, and different from
|
||||
-- schedule_console_redraw's `dapui_console` single-check — don't unify.
|
||||
local DAPUI_PANE_FTS = {
|
||||
dapui_stacks = true, dapui_scopes = true, dapui_watches = true,
|
||||
dapui_breakpoints = true, dapui_console = true, ["dap-repl"] = true,
|
||||
}
|
||||
local function is_dapui_pane_ft(ft) return ft ~= nil and DAPUI_PANE_FTS[ft] == true end
|
||||
|
||||
-- INTENTIONAL ASYMMETRY: callbacks do their own per-window filtering.
|
||||
-- compute_dapui_total_height filters floats (relative==""); pin_dapui_sizes
|
||||
-- does not. Don't hoist the filter into this iterator.
|
||||
local function for_each_dapui_pane(cb)
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
if vim.api.nvim_buf_is_valid(buf) then
|
||||
local ft = vim.bo[buf].filetype
|
||||
if is_dapui_pane_ft(ft) then cb(buf, ft) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Bar extmarks go through bar_ns + pcall. IP marker uses ip_ns separately.
|
||||
local function safe_extmark(buf, row, col, opts)
|
||||
pcall(vim.api.nvim_buf_set_extmark, buf, bar_ns, row, col, opts)
|
||||
end
|
||||
|
||||
local function bar_set_lines(buf, lines)
|
||||
vim.bo[buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].modifiable = false
|
||||
end
|
||||
|
||||
local function safe_set_height(w, h) pcall(vim.api.nvim_win_set_height, w, h) end
|
||||
|
||||
-- Defer fn via vim.schedule; re-check `active` at fire time so the body
|
||||
-- doesn't run after M.exit cleared state.
|
||||
local function if_still_active(fn)
|
||||
vim.schedule(function() if active then fn() end end)
|
||||
end
|
||||
|
||||
local function invalidate_dead_bars()
|
||||
if not win_ok(sbar_win) then sbar_win, sbar_buf = nil, nil end
|
||||
if not win_ok(cbar_win) then cbar_win, cbar_buf = nil, nil end
|
||||
end
|
||||
|
||||
-- click zones for the control bar. Each entry: { line_1based, dcol_start, dcol_end, fn }
|
||||
-- Status bar has no clickable zones (control bar does), so no sbar_zones.
|
||||
local cbar_zones = {}
|
||||
|
||||
local function dap_state()
|
||||
@@ -372,8 +416,8 @@ local function build_control_text(icon, actual_key, label, italic)
|
||||
end
|
||||
|
||||
local function render_sbar()
|
||||
if not (sbar_buf and vim.api.nvim_buf_is_valid(sbar_buf)) then return end
|
||||
if not (sbar_win and vim.api.nvim_win_is_valid(sbar_win)) then return end
|
||||
if not (buf_ok(sbar_buf)) then return end
|
||||
if not (win_ok(sbar_win)) then return end
|
||||
|
||||
local width = vim.api.nvim_win_get_width(sbar_win)
|
||||
if width < 1 then width = vim.o.columns end
|
||||
@@ -411,35 +455,31 @@ local function render_sbar()
|
||||
-- separator; the inward edge abuts dapui's top pane (whose winbar and
|
||||
-- nvim's own horiz separator serve as the visual boundary there). Keeps
|
||||
-- the bar's vertical footprint minimal.
|
||||
vim.bo[sbar_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(sbar_buf, 0, -1, false, { sep, content_line })
|
||||
vim.bo[sbar_buf].modifiable = false
|
||||
bar_set_lines(sbar_buf, { sep, content_line })
|
||||
|
||||
vim.api.nvim_buf_clear_namespace(sbar_buf, bar_ns, 0, -1)
|
||||
pcall(vim.api.nvim_buf_set_extmark, sbar_buf, bar_ns, 0, 0, {
|
||||
safe_extmark(sbar_buf, 0, 0, {
|
||||
end_row = 0, end_col = #sep, hl_group = "TurboDebugBarSeparator",
|
||||
})
|
||||
-- content row
|
||||
pcall(vim.api.nvim_buf_set_extmark, sbar_buf, bar_ns, 1, 0, {
|
||||
safe_extmark(sbar_buf, 1, 0, {
|
||||
end_row = 1, end_col = #brand_text, hl_group = "TurboDebugBarBrand",
|
||||
})
|
||||
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, {
|
||||
safe_extmark(sbar_buf, 1, status_byte_start, {
|
||||
end_row = 1, end_col = status_byte_start + #status_msg, hl_group = "TurboDebugBarStatus",
|
||||
})
|
||||
end
|
||||
local chip_byte_start = #content_line - #state_chip
|
||||
pcall(vim.api.nvim_buf_set_extmark, sbar_buf, bar_ns, 1, chip_byte_start, {
|
||||
safe_extmark(sbar_buf, 1, chip_byte_start, {
|
||||
end_row = 1, end_col = #content_line, hl_group = state_hl,
|
||||
})
|
||||
|
||||
sbar_zones = {}
|
||||
end
|
||||
|
||||
local function render_cbar()
|
||||
if not (cbar_buf and vim.api.nvim_buf_is_valid(cbar_buf)) then return end
|
||||
if not (cbar_win and vim.api.nvim_win_is_valid(cbar_win)) then return end
|
||||
if not (buf_ok(cbar_buf)) then return end
|
||||
if not (win_ok(cbar_win)) then return end
|
||||
|
||||
local width = vim.api.nvim_win_get_width(cbar_win)
|
||||
if width < 1 then width = vim.o.columns end
|
||||
@@ -526,35 +566,33 @@ local function render_cbar()
|
||||
-- 2 rows: content / bottom sep. Only the outward (bottom) edge gets a
|
||||
-- bright separator; the inward edge abuts dapui Console (whose winbar
|
||||
-- + nvim's own horiz separator serve as the visual boundary there).
|
||||
vim.bo[cbar_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(cbar_buf, 0, -1, false, { content_line, sep })
|
||||
vim.bo[cbar_buf].modifiable = false
|
||||
bar_set_lines(cbar_buf, { content_line, sep })
|
||||
|
||||
vim.api.nvim_buf_clear_namespace(cbar_buf, bar_ns, 0, -1)
|
||||
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 1, 0, {
|
||||
safe_extmark(cbar_buf, 1, 0, {
|
||||
end_row = 1, end_col = #sep, hl_group = "TurboDebugBarSeparator",
|
||||
})
|
||||
|
||||
-- control labels on row 0 (content row)
|
||||
local ctrl_byte_offset = ctrl_pad_left
|
||||
for _, span in ipairs(ctrl_spans) do
|
||||
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 0, ctrl_byte_offset + span[1], {
|
||||
safe_extmark(cbar_buf, 0, ctrl_byte_offset + span[1], {
|
||||
end_row = 0, end_col = ctrl_byte_offset + span[2], hl_group = span[3],
|
||||
})
|
||||
end
|
||||
for _, span in ipairs(ctrl_key_spans) do
|
||||
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 0, ctrl_byte_offset + span[1], {
|
||||
safe_extmark(cbar_buf, 0, ctrl_byte_offset + span[1], {
|
||||
end_row = 0, end_col = ctrl_byte_offset + span[2], hl_group = "TurboDebugBarKey",
|
||||
})
|
||||
end
|
||||
for _, span in ipairs(ctrl_italic_spans) do
|
||||
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 0, ctrl_byte_offset + span[1], {
|
||||
safe_extmark(cbar_buf, 0, ctrl_byte_offset + span[1], {
|
||||
end_row = 0, end_col = ctrl_byte_offset + span[2], hl_group = "TurboDebugBarItalic",
|
||||
})
|
||||
end
|
||||
|
||||
local help_byte_start = #content_line - #help_text
|
||||
pcall(vim.api.nvim_buf_set_extmark, cbar_buf, bar_ns, 0, help_byte_start, {
|
||||
safe_extmark(cbar_buf, 0, help_byte_start, {
|
||||
end_row = 0, end_col = #content_line, hl_group = "TurboDebugBarCtrl",
|
||||
})
|
||||
|
||||
@@ -581,17 +619,6 @@ end
|
||||
-- The bars are SPLITS, not floats. Splits consume real layout rows so
|
||||
-- dapui and source windows tuck underneath them naturally — no overlap.
|
||||
|
||||
local function handle_sbar_click()
|
||||
local pos = vim.fn.getmousepos()
|
||||
if not pos or pos.winid ~= sbar_win then return end
|
||||
for _, zone in ipairs(sbar_zones) do
|
||||
if zone[1] == pos.line and pos.wincol - 1 >= zone[2] and pos.wincol - 1 < zone[3] then
|
||||
zone[4]()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_cbar_click()
|
||||
local pos = vim.fn.getmousepos()
|
||||
if not pos or pos.winid ~= cbar_win then return end
|
||||
@@ -661,7 +688,7 @@ local function open_bars()
|
||||
local caller_win = vim.api.nvim_get_current_win()
|
||||
|
||||
-- status bar (top)
|
||||
if not (sbar_win and vim.api.nvim_win_is_valid(sbar_win)) then
|
||||
if not (win_ok(sbar_win)) then
|
||||
vim.cmd("noautocmd keepalt topleft split")
|
||||
sbar_win = vim.api.nvim_get_current_win()
|
||||
sbar_buf = vim.api.nvim_create_buf(false, true)
|
||||
@@ -670,8 +697,6 @@ local function open_bars()
|
||||
setup_bar_buf(sbar_buf)
|
||||
setup_bar_win(sbar_win)
|
||||
vim.wo[sbar_win].winfixheight = true
|
||||
vim.keymap.set("n", "<LeftMouse>", handle_sbar_click, { buffer = sbar_buf, silent = true, nowait = true })
|
||||
vim.keymap.set("n", "<LeftRelease>", "<Nop>", { buffer = sbar_buf, silent = true, nowait = true })
|
||||
bounce_out(sbar_buf)
|
||||
end
|
||||
|
||||
@@ -680,7 +705,7 @@ local function open_bars()
|
||||
end
|
||||
|
||||
-- control bar (bottom)
|
||||
if not (cbar_win and vim.api.nvim_win_is_valid(cbar_win)) then
|
||||
if not (win_ok(cbar_win)) then
|
||||
vim.cmd("noautocmd keepalt botright split")
|
||||
cbar_win = vim.api.nvim_get_current_win()
|
||||
cbar_buf = vim.api.nvim_create_buf(false, true)
|
||||
@@ -708,24 +733,18 @@ end
|
||||
-- fight subsequent WinResized events. Initial sizing only.
|
||||
local function pin_dapui_sizes()
|
||||
local pane_wins = {}
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
if vim.api.nvim_buf_is_valid(buf) then
|
||||
local ft = vim.bo[buf].filetype
|
||||
if ft == "dapui_stacks" or ft == "dapui_scopes" or ft == "dapui_watches"
|
||||
or ft == "dapui_breakpoints" or ft == "dapui_console" or ft == "dap-repl" then
|
||||
local wins = vim.fn.win_findbuf(buf)
|
||||
if wins and wins[1] and vim.api.nvim_win_is_valid(wins[1]) then
|
||||
pane_wins[ft] = wins[1]
|
||||
end
|
||||
end
|
||||
for_each_dapui_pane(function(buf, ft)
|
||||
local wins = vim.fn.win_findbuf(buf)
|
||||
if wins and win_ok(wins[1]) then
|
||||
pane_wins[ft] = wins[1]
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Console to configured height
|
||||
if pane_wins.dapui_console then
|
||||
local target = config.opts.console_height
|
||||
or math.max(8, math.floor(vim.o.lines * 0.2))
|
||||
pcall(vim.api.nvim_win_set_height, pane_wins.dapui_console, target)
|
||||
safe_set_height(pane_wins.dapui_console, target)
|
||||
end
|
||||
|
||||
-- Sidebar panes distributed equally (residual to Breakpoints)
|
||||
@@ -744,20 +763,16 @@ local function pin_dapui_sizes()
|
||||
local residual = total - (each * #present)
|
||||
for i, w in ipairs(present) do
|
||||
local h = each + (i == #present and residual or 0)
|
||||
pcall(vim.api.nvim_win_set_height, w, h)
|
||||
safe_set_height(w, h)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function close_bars()
|
||||
if sbar_win and vim.api.nvim_win_is_valid(sbar_win) then
|
||||
pcall(vim.api.nvim_win_close, sbar_win, true)
|
||||
end
|
||||
if cbar_win and vim.api.nvim_win_is_valid(cbar_win) then
|
||||
pcall(vim.api.nvim_win_close, cbar_win, true)
|
||||
end
|
||||
if win_ok(sbar_win) then pcall(vim.api.nvim_win_close, sbar_win, true) end
|
||||
if win_ok(cbar_win) then pcall(vim.api.nvim_win_close, cbar_win, true) end
|
||||
sbar_win, sbar_buf, cbar_win, cbar_buf = nil, nil, nil, nil
|
||||
sbar_zones, cbar_zones = {}, {}
|
||||
cbar_zones = {}
|
||||
end
|
||||
|
||||
-- `winfixheight` is a preference, not a guarantee. Under grid pressure
|
||||
@@ -775,15 +790,11 @@ end
|
||||
-- subsequent pin_dapui_sizes() call redistributes it across the dapui
|
||||
-- panes according to our initial proportions.
|
||||
local function ensure_bar_heights()
|
||||
if sbar_win and vim.api.nvim_win_is_valid(sbar_win) then
|
||||
if vim.api.nvim_win_get_height(sbar_win) ~= 2 then
|
||||
pcall(vim.api.nvim_win_set_height, sbar_win, 2)
|
||||
end
|
||||
if win_ok(sbar_win) and vim.api.nvim_win_get_height(sbar_win) ~= 2 then
|
||||
safe_set_height(sbar_win, 2)
|
||||
end
|
||||
if cbar_win and vim.api.nvim_win_is_valid(cbar_win) then
|
||||
if vim.api.nvim_win_get_height(cbar_win) ~= 2 then
|
||||
pcall(vim.api.nvim_win_set_height, cbar_win, 2)
|
||||
end
|
||||
if win_ok(cbar_win) and vim.api.nvim_win_get_height(cbar_win) ~= 2 then
|
||||
safe_set_height(cbar_win, 2)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -793,31 +804,18 @@ end
|
||||
-- without changing it).
|
||||
local function compute_dapui_total_height()
|
||||
local total = 0
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
if vim.api.nvim_buf_is_valid(buf) then
|
||||
local ft = vim.bo[buf].filetype
|
||||
if ft == "dapui_stacks" or ft == "dapui_scopes" or ft == "dapui_watches"
|
||||
or ft == "dapui_breakpoints" or ft == "dapui_console" or ft == "dap-repl" then
|
||||
for _, win in ipairs(vim.fn.win_findbuf(buf)) do
|
||||
if vim.api.nvim_win_is_valid(win)
|
||||
and vim.api.nvim_win_get_config(win).relative == "" then
|
||||
total = total + vim.api.nvim_win_get_height(win)
|
||||
end
|
||||
end
|
||||
for_each_dapui_pane(function(buf, _)
|
||||
for _, win in ipairs(vim.fn.win_findbuf(buf)) do
|
||||
-- DELIBERATE: filter floats HERE, not in for_each_dapui_pane —
|
||||
-- pin_dapui_sizes does NOT want this filter.
|
||||
if win_ok(win) and vim.api.nvim_win_get_config(win).relative == "" then
|
||||
total = total + vim.api.nvim_win_get_height(win)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
return total
|
||||
end
|
||||
|
||||
local function reposition_bars()
|
||||
-- Just re-render bar content. We intentionally DON'T re-pin dapui
|
||||
-- sizes here — the user may have manually resized panes, and
|
||||
-- fighting their resize on every event is a worse UX than occasional
|
||||
-- layout drift.
|
||||
render_bars()
|
||||
end
|
||||
|
||||
-- ─── dap-ui setup ────────────────────────────────────────────────────────────
|
||||
|
||||
local function set_dapui_window_opts(win)
|
||||
@@ -969,12 +967,7 @@ local function ensure_dapui()
|
||||
-- focus to a real source window BEFORE dap processes the jump, we
|
||||
-- guarantee new source files (stepped-into library/stdlib files) open
|
||||
-- in the intended source window and the chrome stays intact.
|
||||
dap.listeners.before.event_stopped["turbo-debug-focus-source"] = function()
|
||||
local w = source_window()
|
||||
if w and w ~= vim.api.nvim_get_current_win() then
|
||||
pcall(vim.api.nvim_set_current_win, w)
|
||||
end
|
||||
end
|
||||
dap.listeners.before.event_stopped["turbo-debug-focus-source"] = focus_source_win
|
||||
|
||||
dap.listeners.after.event_stopped["turbo-debug-ip"] = function(session, body)
|
||||
vim.defer_fn(function()
|
||||
@@ -1026,7 +1019,7 @@ local function ensure_dapui()
|
||||
local ok, dapui = pcall(require, "dapui")
|
||||
if not (ok and dapui.elements and dapui.elements.console) then return end
|
||||
local bok, buf = pcall(dapui.elements.console.buffer)
|
||||
if not bok or not buf or not vim.api.nvim_buf_is_valid(buf) then return end
|
||||
if not bok or not buf_ok(buf) then return end
|
||||
pcall(function()
|
||||
local was_mod = vim.bo[buf].modifiable
|
||||
vim.bo[buf].modifiable = true
|
||||
@@ -1137,17 +1130,19 @@ source_window = function()
|
||||
return nil
|
||||
end
|
||||
|
||||
local function run_in_source(fn)
|
||||
-- We can't use nvim_win_call here: dap.continue() may open a vim.ui.select
|
||||
-- picker asynchronously, and by the time the picker opens, nvim_win_call
|
||||
-- has already returned focus to the original window — the picker opens
|
||||
-- but nothing has focus on it, forcing the user to click in to interact.
|
||||
-- Actually shift focus to the source window so the picker takes focus
|
||||
-- properly.
|
||||
-- Shift focus to a source window if one exists and we're not already there.
|
||||
-- We can't use nvim_win_call: dap.continue() may open a vim.ui.select picker
|
||||
-- asynchronously, and by the time the picker opens, nvim_win_call has
|
||||
-- already returned focus — the picker ends up unfocused. Actually shift.
|
||||
local function focus_source_win()
|
||||
local w = source_window()
|
||||
if w and w ~= vim.api.nvim_get_current_win() then
|
||||
pcall(vim.api.nvim_set_current_win, w)
|
||||
end
|
||||
end
|
||||
|
||||
local function run_in_source(fn)
|
||||
focus_source_win()
|
||||
return fn()
|
||||
end
|
||||
|
||||
@@ -1291,16 +1286,23 @@ function M._install_for_buf(buf)
|
||||
end
|
||||
end
|
||||
|
||||
-- Ordering is load-bearing: del our-n → del our-v → restore prev-n →
|
||||
-- restore prev-v. Restore MUST come after del or the user's mapping is
|
||||
-- clobbered by our subsequent del.
|
||||
local function unset_and_restore(buf, name, entry)
|
||||
pcall(vim.keymap.del, "n", entry.key, { buffer = buf })
|
||||
if visual_actions[name] then
|
||||
pcall(vim.keymap.del, "v", entry.key, { buffer = buf })
|
||||
end
|
||||
if entry.prev_n then restore_mapping(buf, "n", entry.prev_n) end
|
||||
if entry.prev_v then restore_mapping(buf, "v", entry.prev_v) end
|
||||
end
|
||||
|
||||
local function clear_all_modal_keys()
|
||||
for buf, entries in pairs(installed) do
|
||||
if vim.api.nvim_buf_is_valid(buf) then
|
||||
for name, entry in pairs(entries) do
|
||||
pcall(vim.keymap.del, "n", entry.key, { buffer = buf })
|
||||
if visual_actions[name] then
|
||||
pcall(vim.keymap.del, "v", entry.key, { buffer = buf })
|
||||
end
|
||||
if entry.prev_n then restore_mapping(buf, "n", entry.prev_n) end
|
||||
if entry.prev_v then restore_mapping(buf, "v", entry.prev_v) end
|
||||
unset_and_restore(buf, name, entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1319,7 +1321,7 @@ local function set_debug_chrome()
|
||||
end
|
||||
|
||||
local function clear_debug_chrome()
|
||||
if active_win and vim.api.nvim_win_is_valid(active_win) then
|
||||
if win_ok(active_win) then
|
||||
vim.wo[active_win].winbar = saved_winbar or ""
|
||||
end
|
||||
active_win = nil
|
||||
@@ -1355,14 +1357,8 @@ function M.enter()
|
||||
vim.api.nvim_create_autocmd("VimResized", {
|
||||
group = group,
|
||||
callback = function()
|
||||
vim.schedule(function()
|
||||
if not active then return end
|
||||
if not (sbar_win and vim.api.nvim_win_is_valid(sbar_win)) then
|
||||
sbar_win, sbar_buf = nil, nil
|
||||
end
|
||||
if not (cbar_win and vim.api.nvim_win_is_valid(cbar_win)) then
|
||||
cbar_win, cbar_buf = nil, nil
|
||||
end
|
||||
if_still_active(function()
|
||||
invalidate_dead_bars()
|
||||
open_bars()
|
||||
ensure_bar_heights()
|
||||
pin_dapui_sizes()
|
||||
@@ -1387,12 +1383,7 @@ function M.enter()
|
||||
callback = function()
|
||||
if not active then return end
|
||||
vim.schedule(function()
|
||||
if not (sbar_win and vim.api.nvim_win_is_valid(sbar_win)) then
|
||||
sbar_win, sbar_buf = nil, nil
|
||||
end
|
||||
if not (cbar_win and vim.api.nvim_win_is_valid(cbar_win)) then
|
||||
cbar_win, cbar_buf = nil, nil
|
||||
end
|
||||
invalidate_dead_bars()
|
||||
if not (sbar_win and cbar_win) then
|
||||
open_bars()
|
||||
end
|
||||
@@ -1426,8 +1417,7 @@ function M.enter()
|
||||
pin_dapui_sizes()
|
||||
|
||||
-- second install pass after dapui has created its buffers
|
||||
vim.schedule(function()
|
||||
if not active then return end
|
||||
if_still_active(function()
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
if vim.api.nvim_buf_is_loaded(buf) then M._install_for_buf(buf) end
|
||||
end
|
||||
@@ -1480,4 +1470,18 @@ end
|
||||
|
||||
function M.is_active() return active end
|
||||
|
||||
-- Test-only exports (unstable API). Direct refs, not copies.
|
||||
M._test = {
|
||||
render_sbar = render_sbar,
|
||||
render_cbar = render_cbar,
|
||||
keyof = keyof,
|
||||
build_control_text = build_control_text,
|
||||
define_highlights = define_highlights,
|
||||
source_window = function() return source_window() end,
|
||||
set_bars = function(sw, sb, cw, cb) sbar_win, sbar_buf, cbar_win, cbar_buf = sw, sb, cw, cb end,
|
||||
get_bars = function() return sbar_win, sbar_buf, cbar_win, cbar_buf end,
|
||||
get_sbar_zones = function() return {} end,
|
||||
get_cbar_zones = function() return cbar_zones end,
|
||||
}
|
||||
|
||||
return M
|
||||
@@ -6,4 +6,16 @@ function M.assert_eq(a, b, msg)
|
||||
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
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -0,0 +1,147 @@
|
||||
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")
|
||||
Reference in new issue
Block a user