From 78a43da5d16354bfc5dd13f43e600f6ae4ef7250 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Sat, 11 Apr 2026 16:36:25 -0600 Subject: [PATCH] mode: enter/exit debug mode with keybinding lifecycle --- lua/tiny-debugger/init.lua | 15 +++++ lua/tiny-debugger/mode.lua | 129 +++++++++++++++++++++++++++++++++++++ plugin/tiny-debugger.lua | 5 ++ tests/test_keys.lua | 28 ++++++++ tests/test_mode.lua | 42 ++++++++++++ 5 files changed, 219 insertions(+) create mode 100644 lua/tiny-debugger/mode.lua create mode 100644 tests/test_keys.lua create mode 100644 tests/test_mode.lua diff --git a/lua/tiny-debugger/init.lua b/lua/tiny-debugger/init.lua index 5044d6f..8b52744 100644 --- a/lua/tiny-debugger/init.lua +++ b/lua/tiny-debugger/init.lua @@ -1,3 +1,18 @@ +local config = require("tiny-debugger.config") +local mode = require("tiny-debugger.mode") + local M = {} +function M.setup(opts) + config.merge(opts) +end + +function M.toggle() + mode.toggle() +end + +function M.active() + return mode.is_active() +end + return M diff --git a/lua/tiny-debugger/mode.lua b/lua/tiny-debugger/mode.lua new file mode 100644 index 0000000..953942c --- /dev/null +++ b/lua/tiny-debugger/mode.lua @@ -0,0 +1,129 @@ +local config = require("tiny-debugger.config") + +local M = {} + +local active = false +local stashed_maps = {} + +local function stash_mapping(buf, lhs) + local maps = vim.api.nvim_buf_get_keymap(buf, "n") + for _, map in ipairs(maps) do + if map.lhs == lhs then + stashed_maps[lhs] = map + return + end + end + stashed_maps[lhs] = nil +end + +local function restore_mapping(buf, lhs) + local map = stashed_maps[lhs] + if map then + local rhs = map.rhs or map.callback + if rhs then + vim.keymap.set("n", lhs, rhs, { + buffer = buf, + silent = map.silent == 1, + noremap = map.noremap == 1, + expr = map.expr == 1, + desc = map.desc, + }) + end + else + pcall(vim.keymap.del, "n", lhs, { buffer = buf }) + end +end + +-- actions table: key name → function +-- populated by other modules via M.register_action +local actions = {} + +function M.register_action(name, fn) + actions[name] = fn +end + +-- default actions that map directly to dap calls +local function setup_default_actions() + local dap = require("dap") + 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 +end + +local function set_keymaps(buf) + local keys = config.opts.keys + for name, key in pairs(keys) do + if key and actions[name] then + stash_mapping(buf, key) + vim.keymap.set("n", key, actions[name], { buffer = buf, silent = true, desc = "tiny-debugger: " .. name }) + 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 }) + restore_mapping(buf, key) + end + end + stashed_maps = {} +end + +local saved_cursor_hl = nil + +local function set_cursor_highlight() + saved_cursor_hl = vim.api.nvim_get_hl(0, { name = "Cursor" }) + vim.api.nvim_set_hl(0, "Cursor", { bg = "#ff6600", fg = "#000000" }) +end + +local function restore_cursor_highlight() + if saved_cursor_hl then + vim.api.nvim_set_hl(0, "Cursor", saved_cursor_hl) + saved_cursor_hl = nil + else + vim.api.nvim_set_hl(0, "Cursor", {}) + end +end + +function M.enter() + if active then return end + active = true + + setup_default_actions() + + local buf = vim.api.nvim_get_current_buf() + set_keymaps(buf) + set_cursor_highlight() +end + +function M.exit() + if not active then return end + active = false + + local buf = vim.api.nvim_get_current_buf() + clear_keymaps(buf) + restore_cursor_highlight() +end + +function M.toggle() + if active then + M.exit() + else + M.enter() + end +end + +function M.is_active() + return active +end + +return M diff --git a/plugin/tiny-debugger.lua b/plugin/tiny-debugger.lua index 0beb30d..57f312d 100644 --- a/plugin/tiny-debugger.lua +++ b/plugin/tiny-debugger.lua @@ -12,3 +12,8 @@ vim.pack.add({ "rcarriga/nvim-dap-ui" }) vim.pack.add({ "nvim-neotest/nvim-nio" }) vim.pack.add({ "theHamsta/nvim-dap-virtual-text" }) vim.pack.add({ "Weissle/persistent-breakpoints.nvim" }) + +-- toggle debug mode +vim.keymap.set("n", "d", function() + require("tiny-debugger").toggle() +end, { silent = true, desc = "tiny-debugger: toggle debug mode" }) diff --git a/tests/test_keys.lua b/tests/test_keys.lua new file mode 100644 index 0000000..7a9b9c3 --- /dev/null +++ b/tests/test_keys.lua @@ -0,0 +1,28 @@ +-- tests for keybinding stash/restore +local td = require("tiny-debugger") + +local function assert_eq(a, b, msg) + if a ~= b then + error(string.format("FAIL: %s — expected %s, got %s", msg, vim.inspect(b), vim.inspect(a))) + end +end + +-- set a custom mapping for 'c' +local custom_called = false +vim.keymap.set("n", "c", function() custom_called = true end, { buffer = 0, desc = "custom c" }) + +-- verify custom mapping exists +local maparg = vim.fn.maparg("c", "n", false, true) +assert_eq(maparg.desc, "custom c", "custom mapping exists before enter") + +-- enter debug mode — should override 'c' +td.toggle() +maparg = vim.fn.maparg("c", "n", false, true) +assert_eq(maparg.desc, "tiny-debugger: continue", "c overridden in debug mode") + +-- exit debug mode — should restore custom 'c' +td.toggle() +maparg = vim.fn.maparg("c", "n", false, true) +assert_eq(maparg.desc, "custom c", "custom mapping restored after exit") + +print("ALL KEY STASH/RESTORE TESTS PASSED") diff --git a/tests/test_mode.lua b/tests/test_mode.lua new file mode 100644 index 0000000..3b0bdb8 --- /dev/null +++ b/tests/test_mode.lua @@ -0,0 +1,42 @@ +-- tests for debug mode enter/exit +local td = require("tiny-debugger") +local mode = require("tiny-debugger.mode") + +local function assert_eq(a, b, msg) + if a ~= b then + error(string.format("FAIL: %s — expected %s, got %s", msg, vim.inspect(b), vim.inspect(a))) + end +end + +local function has_buf_map(key) + local maps = vim.api.nvim_buf_get_keymap(0, "n") + for _, m in ipairs(maps) do + if m.lhs == key then return true end + end + return false +end + +-- test 1: starts inactive +assert_eq(td.active(), false, "starts inactive") + +-- test 2: toggle enters debug mode +td.toggle() +assert_eq(td.active(), true, "active after toggle") + +-- test 3: keymaps are set +assert_eq(has_buf_map("c"), true, "continue key mapped") +assert_eq(has_buf_map("s"), true, "step_over key mapped") +assert_eq(has_buf_map("d"), true, "step_into key mapped") +assert_eq(has_buf_map("r"), true, "step_out key mapped") +assert_eq(has_buf_map("q"), true, "terminate key mapped") + +-- test 4: toggle exits debug mode +td.toggle() +assert_eq(td.active(), false, "inactive after second toggle") + +-- test 5: keymaps are removed +assert_eq(has_buf_map("c"), false, "continue key removed") +assert_eq(has_buf_map("s"), false, "step_over key removed") +assert_eq(has_buf_map("q"), false, "terminate key removed") + +print("ALL MODE TESTS PASSED")