From d5331800b42f50d73002ad7660364951c5872eb0 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Sat, 11 Apr 2026 19:47:53 -0600 Subject: [PATCH] fix: restore x/X/D as modal debug keys, add quit_exits_mode config, track mapped keys exactly --- README.md | 13 ++++++------ lua/tiny-debugger/config.lua | 4 ++++ lua/tiny-debugger/help.lua | 3 +++ lua/tiny-debugger/mode.lua | 40 +++++++++++++++++++++++++----------- tests/test_config.lua | 10 +++++---- tests/test_mode.lua | 20 ++++++++++++++---- 6 files changed, 64 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 42af51d..14f44cf 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,13 @@ During debug mode, single-key controls: | Key | Action | Key | Action | |-----|--------|-----|--------| -| `c` | Continue | `W` | Watch expression | -| `s` | Step over | `K` | Hover / inspect | -| `d` | Step into | `E` | Eval in REPL | -| `r` | Step out | `q` | Terminate session | -| `C` | Run to cursor | `R` | Restart session | -| `?` | Help popup | | | +| `c` | Continue | `x` | Toggle breakpoint | +| `s` | Step over | `X` | Conditional breakpoint | +| `d` | Step into | `D` | Clear all breakpoints | +| `r` | Step out | `W` | Watch expression | +| `C` | Run to cursor | `K` | Hover / inspect | +| `q` | Terminate session | `E` | Eval in REPL | +| `R` | Restart session | `?` | Help popup | All navigation keys preserved. Modal keybindings vanish when you exit debug mode. diff --git a/lua/tiny-debugger/config.lua b/lua/tiny-debugger/config.lua index afef858..18eecd7 100644 --- a/lua/tiny-debugger/config.lua +++ b/lua/tiny-debugger/config.lua @@ -3,6 +3,7 @@ local M = {} M.defaults = { help_on_enter = true, builtin_adapters = true, + quit_exits_mode = true, -- q terminates AND exits debug mode (false = terminate only) -- modal keys (active only during debug mode, single-key) keys = { @@ -11,6 +12,9 @@ M.defaults = { step_into = "d", step_out = "r", run_to_cursor = "C", + breakpoint = "x", + cond_breakpoint = "X", + clear_breakpoints = "D", watch = "W", hover = "K", eval = "E", diff --git a/lua/tiny-debugger/help.lua b/lua/tiny-debugger/help.lua index fb5f956..1d968e1 100644 --- a/lua/tiny-debugger/help.lua +++ b/lua/tiny-debugger/help.lua @@ -12,6 +12,9 @@ local modal_info = { { "step_into", "Step into" }, { "step_out", "Step out" }, { "run_to_cursor", "Run to cursor" }, + { "breakpoint", "Toggle breakpoint" }, + { "cond_breakpoint", "Conditional breakpoint" }, + { "clear_breakpoints", "Clear all breakpoints" }, { "watch", "Watch expression" }, { "hover", "Hover / inspect" }, { "eval", "Eval in REPL" }, diff --git a/lua/tiny-debugger/mode.lua b/lua/tiny-debugger/mode.lua index a200155..f2569de 100644 --- a/lua/tiny-debugger/mode.lua +++ b/lua/tiny-debugger/mode.lua @@ -8,6 +8,7 @@ local active_buf = nil local active_win = nil local stashed_maps = {} local saved_winbar = nil +local mapped_keys = {} -- track exactly which keys we set, for reliable cleanup -- one-time dap-ui and virtual-text initialization local dapui_initialized = false @@ -92,16 +93,27 @@ local visual_actions = { watch = true, hover = true } local function setup_actions() if next(actions) then return end local dap = require("dap") + local pb = require("persistent-breakpoints.api") + actions.continue = function() dap.continue() end actions.step_over = function() dap.step_over() end actions.step_into = function() dap.step_into() end actions.step_out = function() dap.step_out() end actions.run_to_cursor = function() dap.run_to_cursor() end - actions.terminate = function() dap.terminate(); M.exit() end actions.restart = function() dap.restart() end actions.help = function() if help.is_open() then help.close() else help.open() end end + actions.terminate = function() + dap.terminate() + if config.opts.quit_exits_mode then M.exit() end + end + + -- breakpoint actions (modal shortcuts for the d* globals) + actions.breakpoint = function() pb.toggle_breakpoint() end + actions.cond_breakpoint = function() pb.set_conditional_breakpoint() end + actions.clear_breakpoints = function() pb.clear_all_breakpoints() end + actions.watch = function() local expr if vim.fn.mode() == "v" or vim.fn.mode() == "V" then @@ -119,8 +131,10 @@ local function setup_actions() end -- keymap lifecycle +-- track every key we set so cleanup is exact — never miss one, never clear the wrong one local function set_keymaps(buf) + mapped_keys = {} local keys = config.opts.keys for name, key in pairs(keys) do if key and actions[name] then @@ -129,21 +143,25 @@ local function set_keymaps(buf) if visual_actions[name] then vim.keymap.set("v", key, actions[name], { buffer = buf, silent = true, desc = "tiny-debugger: " .. name }) end + mapped_keys[#mapped_keys + 1] = { name = name, key = key } end end end local function clear_keymaps(buf) - local keys = config.opts.keys - for name, key in pairs(keys) do - if key and actions[name] then - pcall(vim.keymap.del, "n", key, { buffer = buf }) - if visual_actions[name] then - pcall(vim.keymap.del, "v", key, { buffer = buf }) - end - restore_mapping(buf, key) - end + if not vim.api.nvim_buf_is_valid(buf) then + mapped_keys = {} + stashed_maps = {} + return end + for _, mk in ipairs(mapped_keys) do + pcall(vim.keymap.del, "n", mk.key, { buffer = buf }) + if visual_actions[mk.name] then + pcall(vim.keymap.del, "v", mk.key, { buffer = buf }) + end + restore_mapping(buf, mk.key) + end + mapped_keys = {} stashed_maps = {} end @@ -153,7 +171,6 @@ local function set_debug_chrome() active_win = vim.api.nvim_get_current_win() saved_winbar = vim.wo[active_win].winbar vim.wo[active_win].winbar = "%#DiagnosticError# 🐛 DEBUG %* %f" - vim.api.nvim_set_hl(0, "Cursor", { bg = "#ff6600", fg = "#000000" }) end @@ -163,7 +180,6 @@ local function clear_debug_chrome() end active_win = nil saved_winbar = nil - vim.api.nvim_set_hl(0, "Cursor", {}) end diff --git a/tests/test_config.lua b/tests/test_config.lua index 5f67b8c..e7bf7f1 100644 --- a/tests/test_config.lua +++ b/tests/test_config.lua @@ -4,9 +4,11 @@ local config = require("tiny-debugger.config") -- defaults unchanged after empty merge local opts = config.merge({}) assert_eq(opts.keys.continue, "c", "default continue key") +assert_eq(opts.keys.breakpoint, "x", "default modal breakpoint key") assert_eq(opts.global_keys.toggle, "dd", "default toggle key") -assert_eq(opts.global_keys.breakpoint, "dx", "default breakpoint key") +assert_eq(opts.global_keys.breakpoint, "dx", "default global breakpoint key") assert_eq(opts.help_on_enter, true, "default help_on_enter") +assert_eq(opts.quit_exits_mode, true, "default quit_exits_mode") -- partial key override opts = config.merge({ keys = { continue = "g" } }) @@ -22,9 +24,9 @@ opts = config.merge({ global_keys = { toggle = "D" } }) assert_eq(opts.global_keys.toggle, "D", "overridden toggle key") assert_eq(opts.global_keys.breakpoint, "dx", "preserved breakpoint key") --- disable a global key -opts = config.merge({ global_keys = { breakpoint = false } }) -assert_eq(opts.global_keys.breakpoint, false, "disabled breakpoint key") +-- quit_exits_mode override +opts = config.merge({ quit_exits_mode = false }) +assert_eq(opts.quit_exits_mode, false, "overridden quit_exits_mode") -- merge doesn't mutate defaults config.merge({ keys = { continue = "z" } }) diff --git a/tests/test_mode.lua b/tests/test_mode.lua index 230cbf0..eda8568 100644 --- a/tests/test_mode.lua +++ b/tests/test_mode.lua @@ -15,8 +15,8 @@ assert_eq(td.active(), false, "starts inactive") td.toggle() assert_eq(td.active(), true, "active after toggle") --- keymaps are set -for _, key in ipairs({ "c", "s", "d", "r", "q" }) do +-- all modal keymaps are set (including breakpoint keys) +for _, key in ipairs({ "c", "s", "d", "r", "q", "x", "X", "D" }) do assert_eq(has_buf_map(key), true, key .. " key mapped") end @@ -24,9 +24,21 @@ end td.toggle() assert_eq(td.active(), false, "inactive after second toggle") --- keymaps are removed -for _, key in ipairs({ "c", "s", "d", "r", "q" }) do +-- all modal keymaps are removed +for _, key in ipairs({ "c", "s", "d", "r", "q", "x", "X", "D" }) do assert_eq(has_buf_map(key), false, key .. " key removed") end +-- double exit is safe +td.toggle() -- enter +td.toggle() -- exit +td.toggle() -- enter +td.toggle() -- exit +assert_eq(td.active(), false, "stable after multiple toggles") + +-- verify no leaked keymaps after rapid toggling +for _, key in ipairs({ "c", "s", "d", "r", "q", "x", "X", "D" }) do + assert_eq(has_buf_map(key), false, key .. " clean after rapid toggle") +end + print("ALL MODE TESTS PASSED")