From a4aed1822cfd622c2fbd96903f2fcc05a364daaf Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Sat, 11 Apr 2026 18:59:39 -0600 Subject: [PATCH] =?UTF-8?q?audit:=20comprehensive=20cleanup=20=E2=80=94=20?= =?UTF-8?q?delete=208=20files,=20fix=20buffer=20bug,=20cut=20bloat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 71 ++---------- README.md | 125 +++++----------------- doc/tiny-debugger.txt | 3 +- lua/tiny-debugger/adapters/c.lua | 1 - lua/tiny-debugger/adapters/cs.lua | 25 ----- lua/tiny-debugger/adapters/init.lua | 102 +++++++++++++----- lua/tiny-debugger/adapters/java.lua | 25 ----- lua/tiny-debugger/adapters/javascript.lua | 1 - lua/tiny-debugger/adapters/kotlin.lua | 25 ----- lua/tiny-debugger/adapters/lua.lua | 25 ----- lua/tiny-debugger/adapters/php.lua | 22 ---- lua/tiny-debugger/adapters/ruby.lua | 33 ------ lua/tiny-debugger/breakpoints.lua | 41 ++++--- lua/tiny-debugger/config.lua | 7 +- lua/tiny-debugger/help.lua | 79 +++++--------- lua/tiny-debugger/init.lua | 6 +- lua/tiny-debugger/mode.lua | 112 ++++++++++--------- lua/tiny-debugger/ui.lua | 42 -------- lua/tiny-debugger/virtual_text.lua | 24 ----- plugin/tiny-debugger.lua | 9 +- tests/helpers.lua | 9 ++ tests/minimal_init.lua | 1 + tests/test_adapters.lua | 33 ++---- tests/test_breakpoints.lua | 20 ++-- tests/test_config.lua | 19 ++-- tests/test_help.lua | 28 ++--- tests/test_keys.lua | 11 +- tests/test_mode.lua | 36 +++---- 28 files changed, 279 insertions(+), 656 deletions(-) delete mode 100644 lua/tiny-debugger/adapters/cs.lua delete mode 100644 lua/tiny-debugger/adapters/java.lua delete mode 100644 lua/tiny-debugger/adapters/kotlin.lua delete mode 100644 lua/tiny-debugger/adapters/lua.lua delete mode 100644 lua/tiny-debugger/adapters/php.lua delete mode 100644 lua/tiny-debugger/adapters/ruby.lua delete mode 100644 lua/tiny-debugger/ui.lua delete mode 100644 lua/tiny-debugger/virtual_text.lua create mode 100644 tests/helpers.lua diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c021e43..7918fb9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,70 +1,17 @@ -# Contributing to tiny-debugger.nvim +# Contributing -## Source Control +We use **jj** (Jujutsu) for source control, not git. -We use **jj** (Jujutsu), not git. All commits should be made with `jj commit`. +## Adding a new adapter -## Adding a New Adapter +1. Create `lua/tiny-debugger/adapters/.lua` (see `go.lua` for reference) +2. Add the module name to the loader list in `adapters/init.lua` +3. Add the language to vimdoc -1. Create `lua/tiny-debugger/adapters/.lua` -2. Implement a `register(dap)` function that calls `loader.register()` -3. Add the module name to the adapters list in `lua/tiny-debugger/adapters/init.lua` -4. Add the language to the README and vimdoc tables +For simple executable adapters, add them inline in `adapters/init.lua` instead of a separate file. -Example adapter (minimal): - -```lua -local loader = require("tiny-debugger.adapters.init") - -local M = {} - -function M.register(dap) - local binary = loader.find_executable({ "my-debug-adapter" }) - if not binary then return end - - loader.register(dap, "my-adapter", { - type = "executable", - command = binary, - }, { - { - type = "my-adapter", - request = "launch", - name = "Launch file", - program = "${file}", - }, - }, { "mylang" }) -end - -return M -``` - -## Running Tests +## Running tests ```sh -nvim --headless -u tests/minimal_init.lua -c 'luafile tests/test_config.lua' -c 'qa!' -nvim --headless -u tests/minimal_init.lua -c 'luafile tests/test_mode.lua' -c 'qa!' -nvim --headless -u tests/minimal_init.lua -c 'luafile tests/test_keys.lua' -c 'qa!' -nvim --headless -u tests/minimal_init.lua -c 'luafile tests/test_help.lua' -c 'qa!' -nvim --headless -u tests/minimal_init.lua -c 'luafile tests/test_breakpoints.lua' -c 'qa!' -nvim --headless -u tests/minimal_init.lua -c 'luafile tests/test_adapters.lua' -c 'qa!' +for t in tests/test_*.lua; do nvim --headless -u tests/minimal_init.lua -c "luafile $t" -c 'qa!'; done ``` - -## Code Style - -- Clean, readable Lua. No metaprogramming tricks. -- Every file should be short enough to read in one sitting. -- No abstraction for abstraction's sake. -- If a plugin does something we can do in <50 lines, we don't add the dependency. - -## Dependencies - -We depend on exactly five plugins (managed via `vim.pack.add`): - -- nvim-dap -- nvim-dap-ui -- nvim-nio -- nvim-dap-virtual-text -- persistent-breakpoints.nvim - -No language-specific plugin dependencies. Adapter configs are inlined as plain -Lua tables. diff --git a/README.md b/README.md index 04644da..ccf156c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # tiny-debugger.nvim -A zero-config debugging experience for Neovim 0.12+. One line of config, full +A zero-config debugging experience for Neovim 0.12+. One plugin install, full IDE-class debugging: inline variable values, hover inspection, persistent breakpoints, watch expressions, and modal keybindings that just work. @@ -10,115 +10,38 @@ breakpoints, watch expressions, and modal keybindings that just work. vim.pack.add("swaits/tiny-debugger.nvim") ``` -That's it. No other config needed. +That's it. No other config needed. See `:help tiny-debugger` for customization. -## What You Get +## Usage -- **Inline virtual text** — variable values appear right in your source code, - updated live as you step -- **Hover inspection** — press `K` on any expression to see its value -- **Persistent breakpoints** — survive Neovim restarts, visible in the sign - column at all times -- **Watch expressions** — press `W` to add the expression under your cursor -- **Scopes panel** — all local/global variables in the current frame -- **Stack traces** — navigate the call stack, jump to any frame -- **REPL** — evaluate arbitrary expressions in the debug context -- **Console** — program stdout/stderr -- **Modal keybindings** — single-key controls, active only during debug sessions -- **Help box** — shows all keybindings, re-openable with `?` -- **Zero-config adapters** for 10+ languages +`d` toggles debug mode. `x` sets breakpoints (works anytime). -## UI Layout +During debug mode, single-key controls: -``` -┌──────────────────────────────────┬──────────────────┐ -│ │ Scopes (50%) │ -│ Source Code │ │ -│ ├──────────────────┤ -│ x = 42 -- « virtual text │ Watches (25%) │ -│ ├──────────────────┤ -│ │ Stacks (25%) │ -├──────────────────────────────────┴──────────────────┤ -│ REPL (50%) │ Console (50%) │ -└──────────────────────────┴──────────────────────────┘ -``` +| 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 | +| `x` | Toggle breakpoint | `D` | Clear all breakpoints | +| `X` | Conditional breakpoint | `?` | Help popup | -## Keybindings - -`d` toggles debug mode. During debug mode: - -| Key | Action | Mnemonic | -|-----|------------------------|---------------------| -| `c` | Continue | Continue | -| `s` | Step over | Step | -| `d` | Step into | Descend | -| `r` | Step out | Return | -| `C` | Run to cursor | Continue to cursor | -| `x` | Toggle breakpoint | X marks the spot | -| `X` | Conditional breakpoint | Shift-x | -| `W` | Watch expression | Watch | -| `K` | Hover / inspect | LSP hover convention| -| `E` | Eval in REPL | Evaluate | -| `q` | Terminate session | Quit | -| `R` | Restart session | Rerun | -| `?` | Help popup | Help | - -`x` and `X` work outside debug mode too. - -All navigation keys preserved: `h j k l w b e f / n` etc. +All navigation keys preserved. Keybindings vanish when you exit debug mode. ## Supported Languages -| Language | Adapter | Binary needed | -|-----------------|-----------------|----------------------| -| Python | debugpy | `python -m debugpy` | -| JavaScript / TS | js-debug | `js-debug-adapter` | -| Go | delve | `dlv` | -| C / C++ | codelldb | `codelldb` | -| Rust | codelldb | `codelldb` | -| Swift | codelldb | `codelldb` | -| C# | netcoredbg | `netcoredbg` | -| Java | java-debug | `java-debug-adapter` | -| Kotlin | kotlin-debug | `kotlin-debug-adapter`| -| PHP | php-debug | `php-debug-adapter` | -| Ruby | rdbg | `rdbg` | -| Lua (Neovim) | osv | one-small-step-for-vimkind | +Adapter binaries must be installed (via mason, package manager, etc). -Adapter binaries are the user's responsibility to install (via mason, system -package manager, or manually). tiny-debugger configures, it doesn't install. - -## Configuration (optional) - -```lua -require("tiny-debugger").setup({ - help_on_enter = true, -- show help box when entering debug mode - builtin_adapters = true, -- register built-in language adapters - - keys = { - continue = "c", -- override any key - step_over = false, -- set to false to disable - }, - - adapters = { - zig = { ... }, -- add new language adapters - }, - - dapui = { ... }, -- override nvim-dap-ui layout - virtual_text = { ... }, -- override virtual text settings -}) -``` - -## Statusline - -```lua -require("tiny-debugger").active() -- returns true/false -``` - -## Help - -``` -:help tiny-debugger -``` +| Language | Adapter | Language | Adapter | +|-----------------|------------|--------------|--------------------| +| Python | debugpy | C# | netcoredbg | +| JavaScript / TS | js-debug | Java | java-debug-adapter | +| Go | delve | Kotlin | kotlin-debug | +| C / C++ | codelldb | PHP | php-debug | +| Rust | codelldb | Ruby | rdbg | +| Swift | codelldb | Lua (Neovim) | osv | ## License diff --git a/doc/tiny-debugger.txt b/doc/tiny-debugger.txt index 1ecffb8..4c981dd 100644 --- a/doc/tiny-debugger.txt +++ b/doc/tiny-debugger.txt @@ -101,6 +101,7 @@ Default keymap: `C` Run to cursor Continue to cursor `x` Toggle breakpoint X marks the spot (always active) `X` Conditional breakpoint Shift variant of x (always active) + `D` Clear all breakpoints Delete all (always active) `W` Watch expr under cursor Watch `K` Hover / inspect Matches LSP hover convention `E` Eval in REPL Evaluate @@ -108,7 +109,7 @@ Default keymap: `R` Restart session Rerun `?` Help popup Show keybindings -Note: `x` and `X` work outside debug mode too (persistent breakpoints). +Note: `x`, `X`, and `D` work outside debug mode too (persistent breakpoints). Override any key: >lua require("tiny-debugger").setup({ diff --git a/lua/tiny-debugger/adapters/c.lua b/lua/tiny-debugger/adapters/c.lua index 9767cf8..91127cb 100644 --- a/lua/tiny-debugger/adapters/c.lua +++ b/lua/tiny-debugger/adapters/c.lua @@ -24,7 +24,6 @@ function M.register(dap) return vim.fn.input("Path to executable: ", vim.fn.getcwd() .. "/", "file") end, cwd = "${workspaceFolder}", - stopOnEntry = false, }, } diff --git a/lua/tiny-debugger/adapters/cs.lua b/lua/tiny-debugger/adapters/cs.lua deleted file mode 100644 index a77671a..0000000 --- a/lua/tiny-debugger/adapters/cs.lua +++ /dev/null @@ -1,25 +0,0 @@ -local loader = require("tiny-debugger.adapters.init") - -local M = {} - -function M.register(dap) - local netcoredbg = loader.find_executable({ "netcoredbg" }) - if not netcoredbg then return end - - loader.register(dap, "netcoredbg", { - type = "executable", - command = netcoredbg, - args = { "--interpreter=vscode" }, - }, { - { - type = "netcoredbg", - request = "launch", - name = "Launch .NET", - program = function() - return vim.fn.input("Path to DLL: ", vim.fn.getcwd() .. "/bin/Debug/", "file") - end, - }, - }, { "cs" }) -end - -return M diff --git a/lua/tiny-debugger/adapters/init.lua b/lua/tiny-debugger/adapters/init.lua index e49f81b..8ee16a2 100644 --- a/lua/tiny-debugger/adapters/init.lua +++ b/lua/tiny-debugger/adapters/init.lua @@ -6,30 +6,83 @@ local mason_bin = vim.fn.stdpath("data") .. "/mason/bin/" function M.find_executable(names) for _, name in ipairs(names) do - -- check PATH - if vim.fn.executable(name) == 1 then - return name - end - -- check mason + if vim.fn.executable(name) == 1 then return name end local mason_path = mason_bin .. name - if vim.fn.executable(mason_path) == 1 then - return mason_path - end + if vim.fn.executable(mason_path) == 1 then return mason_path end end - return nil end function M.register(dap, name, adapter, configurations, filetypes) - -- don't override user-configured adapters if dap.adapters[name] then return end - dap.adapters[name] = adapter - for _, ft in ipairs(filetypes) do - if not dap.configurations[ft] then - dap.configurations[ft] = configurations + dap.configurations[ft] = dap.configurations[ft] or configurations + end +end + +-- simple executable adapters (inlined — no separate files needed) +local function register_simple_adapters(dap) + local simple = { + { "netcoredbg", { "netcoredbg" }, { + type = "executable", args = { "--interpreter=vscode" }, + }, { { + type = "netcoredbg", request = "launch", name = "Launch .NET", + program = function() return vim.fn.input("Path to DLL: ", vim.fn.getcwd() .. "/bin/Debug/", "file") end, + } }, { "cs" } }, + + { "java", { "java-debug-adapter" }, { + type = "executable", + }, { { + type = "java", request = "launch", name = "Launch class", + mainClass = function() return vim.fn.input("Main class: ") end, + cwd = "${workspaceFolder}", + } }, { "java" } }, + + { "kotlin", { "kotlin-debug-adapter" }, { + type = "executable", + }, { { + type = "kotlin", request = "launch", name = "Launch main", + mainClass = function() return vim.fn.input("Main class: ") end, + projectRoot = "${workspaceFolder}", + } }, { "kotlin" } }, + + { "php", { "php-debug-adapter" }, { + type = "executable", + }, { { + type = "php", request = "launch", name = "Listen for Xdebug", port = 9003, + } }, { "php" } }, + + { "rdbg", { "rdbg" }, { + type = "server", port = "${port}", + executable = { command = nil, args = { "-n", "--open", "--port", "${port}", "-c", "--", "ruby", "${file}" } }, + }, { { + type = "rdbg", request = "launch", name = "Launch file", command = "ruby", script = "${file}", + }, { + type = "rdbg", request = "attach", name = "Attach", localfsMap = "${workspaceFolder}", + } }, { "ruby" } }, + } + + for _, s in ipairs(simple) do + local name, exe_names, adapter_tpl, configurations, filetypes = s[1], s[2], s[3], s[4], s[5] + local exe = M.find_executable(exe_names) + if exe then + local adapter = vim.deepcopy(adapter_tpl) + adapter.command = adapter.command or exe + if adapter.executable then adapter.executable.command = exe end + M.register(dap, name, adapter, configurations, filetypes) end end + + -- lua/neovim (osv) — optional, skip if not installed + local ok, osv = pcall(require, "osv") + if ok then + M.register(dap, "nlua", { + type = "server", host = "127.0.0.1", + port = function() return osv.launch({ port = 0 }) end, + }, { { + type = "nlua", request = "attach", name = "Attach to running Neovim", + } }, { "lua" }) + end end function M.setup() @@ -37,26 +90,21 @@ function M.setup() local dap = require("dap") - -- load built-in adapter modules - local adapters = { - "python", "go", "c", "javascript", "cs", "java", "kotlin", "php", "ruby", "lua", - } - - for _, name in ipairs(adapters) do + -- load adapters with non-trivial logic from separate files + for _, name in ipairs({ "python", "go", "c", "javascript" }) do local ok, adapter_mod = pcall(require, "tiny-debugger.adapters." .. name) if ok and adapter_mod.register then adapter_mod.register(dap) end end - -- apply user adapter overrides + -- register simple adapters inlined above + register_simple_adapters(dap) + + -- apply user adapter overrides (last — wins over everything) for name, adapter_config in pairs(config.opts.adapters) do - if adapter_config.adapter then - dap.adapters[name] = adapter_config.adapter - end - if adapter_config.configurations then - dap.configurations[name] = adapter_config.configurations - end + if adapter_config.adapter then dap.adapters[name] = adapter_config.adapter end + if adapter_config.configurations then dap.configurations[name] = adapter_config.configurations end end end diff --git a/lua/tiny-debugger/adapters/java.lua b/lua/tiny-debugger/adapters/java.lua deleted file mode 100644 index f68132a..0000000 --- a/lua/tiny-debugger/adapters/java.lua +++ /dev/null @@ -1,25 +0,0 @@ -local loader = require("tiny-debugger.adapters.init") - -local M = {} - -function M.register(dap) - local java_debug = loader.find_executable({ "java-debug-adapter" }) - if not java_debug then return end - - loader.register(dap, "java", { - type = "executable", - command = java_debug, - }, { - { - type = "java", - request = "launch", - name = "Launch class", - mainClass = function() - return vim.fn.input("Main class: ") - end, - cwd = "${workspaceFolder}", - }, - }, { "java" }) -end - -return M diff --git a/lua/tiny-debugger/adapters/javascript.lua b/lua/tiny-debugger/adapters/javascript.lua index bf1f864..3e4534d 100644 --- a/lua/tiny-debugger/adapters/javascript.lua +++ b/lua/tiny-debugger/adapters/javascript.lua @@ -8,7 +8,6 @@ function M.register(dap) local adapter = { type = "server", - host = "localhost", port = "${port}", executable = { command = js_debug, diff --git a/lua/tiny-debugger/adapters/kotlin.lua b/lua/tiny-debugger/adapters/kotlin.lua deleted file mode 100644 index fb4c873..0000000 --- a/lua/tiny-debugger/adapters/kotlin.lua +++ /dev/null @@ -1,25 +0,0 @@ -local loader = require("tiny-debugger.adapters.init") - -local M = {} - -function M.register(dap) - local kotlin_debug = loader.find_executable({ "kotlin-debug-adapter" }) - if not kotlin_debug then return end - - loader.register(dap, "kotlin", { - type = "executable", - command = kotlin_debug, - }, { - { - type = "kotlin", - request = "launch", - name = "Launch main", - mainClass = function() - return vim.fn.input("Main class: ") - end, - projectRoot = "${workspaceFolder}", - }, - }, { "kotlin" }) -end - -return M diff --git a/lua/tiny-debugger/adapters/lua.lua b/lua/tiny-debugger/adapters/lua.lua deleted file mode 100644 index 26219b1..0000000 --- a/lua/tiny-debugger/adapters/lua.lua +++ /dev/null @@ -1,25 +0,0 @@ -local loader = require("tiny-debugger.adapters.init") - -local M = {} - -function M.register(dap) - -- osv (one-small-step-for-vimkind) is optional — skip gracefully if not installed - local ok, osv = pcall(require, "osv") - if not ok then return end - - loader.register(dap, "nlua", { - type = "server", - host = "127.0.0.1", - port = function() - return osv.launch({ port = 0 }) - end, - }, { - { - type = "nlua", - request = "attach", - name = "Attach to running Neovim", - }, - }, { "lua" }) -end - -return M diff --git a/lua/tiny-debugger/adapters/php.lua b/lua/tiny-debugger/adapters/php.lua deleted file mode 100644 index 568dc5e..0000000 --- a/lua/tiny-debugger/adapters/php.lua +++ /dev/null @@ -1,22 +0,0 @@ -local loader = require("tiny-debugger.adapters.init") - -local M = {} - -function M.register(dap) - local php_debug = loader.find_executable({ "php-debug-adapter" }) - if not php_debug then return end - - loader.register(dap, "php", { - type = "executable", - command = php_debug, - }, { - { - type = "php", - request = "launch", - name = "Listen for Xdebug", - port = 9003, - }, - }, { "php" }) -end - -return M diff --git a/lua/tiny-debugger/adapters/ruby.lua b/lua/tiny-debugger/adapters/ruby.lua deleted file mode 100644 index 29b5fc6..0000000 --- a/lua/tiny-debugger/adapters/ruby.lua +++ /dev/null @@ -1,33 +0,0 @@ -local loader = require("tiny-debugger.adapters.init") - -local M = {} - -function M.register(dap) - local rdbg = loader.find_executable({ "rdbg" }) - if not rdbg then return end - - loader.register(dap, "rdbg", { - type = "server", - port = "${port}", - executable = { - command = rdbg, - args = { "-n", "--open", "--port", "${port}", "-c", "--", "ruby", "${file}" }, - }, - }, { - { - type = "rdbg", - request = "launch", - name = "Launch file", - command = "ruby", - script = "${file}", - }, - { - type = "rdbg", - request = "attach", - name = "Attach", - localfsMap = "${workspaceFolder}", - }, - }, { "ruby" }) -end - -return M diff --git a/lua/tiny-debugger/breakpoints.lua b/lua/tiny-debugger/breakpoints.lua index 766fc9d..71445e8 100644 --- a/lua/tiny-debugger/breakpoints.lua +++ b/lua/tiny-debugger/breakpoints.lua @@ -12,37 +12,32 @@ function M.setup() load_breakpoints_event = { "BufReadPost" }, }) - -- sign highlights - vim.api.nvim_set_hl(0, "DapBreakpoint", { fg = "#e06c75" }) - vim.api.nvim_set_hl(0, "DapBreakpointCondition", { fg = "#e5c07b" }) - vim.api.nvim_set_hl(0, "DapBreakpointRejected", { fg = "#5c6370" }) - vim.api.nvim_set_hl(0, "DapStopped", { fg = "#98c379" }) - vim.api.nvim_set_hl(0, "DapStoppedLine", { bg = "#2e3b2e" }) - vim.api.nvim_set_hl(0, "DapLogPoint", { fg = "#61afef" }) + -- sign highlights (default = true so colorschemes can override) + vim.api.nvim_set_hl(0, "DapBreakpoint", { default = true, fg = "#e06c75" }) + vim.api.nvim_set_hl(0, "DapBreakpointCondition", { default = true, fg = "#e5c07b" }) + vim.api.nvim_set_hl(0, "DapBreakpointRejected", { default = true, fg = "#5c6370" }) + vim.api.nvim_set_hl(0, "DapStopped", { default = true, fg = "#98c379" }) + vim.api.nvim_set_hl(0, "DapStoppedLine", { default = true, bg = "#2e3b2e" }) + vim.api.nvim_set_hl(0, "DapLogPoint", { default = true, fg = "#61afef" }) - -- gutter signs vim.fn.sign_define("DapBreakpoint", { text = "🔴", texthl = "DapBreakpoint" }) vim.fn.sign_define("DapBreakpointCondition", { text = "🟡", texthl = "DapBreakpointCondition" }) vim.fn.sign_define("DapBreakpointRejected", { text = "⭕", texthl = "DapBreakpointRejected" }) vim.fn.sign_define("DapStopped", { text = "▶️", texthl = "DapStopped", linehl = "DapStoppedLine" }) vim.fn.sign_define("DapLogPoint", { text = "📝", texthl = "DapLogPoint" }) - -- global mappings — these work outside debug mode + -- global mappings (work outside debug mode) local keys = config.opts.keys - if keys.breakpoint then - vim.keymap.set("n", keys.breakpoint, function() - require("persistent-breakpoints.api").toggle_breakpoint() - end, { silent = true, desc = "tiny-debugger: toggle breakpoint" }) - end - if keys.cond_breakpoint then - vim.keymap.set("n", keys.cond_breakpoint, function() - require("persistent-breakpoints.api").set_conditional_breakpoint() - end, { silent = true, desc = "tiny-debugger: conditional breakpoint" }) - end - if keys.clear_breakpoints then - vim.keymap.set("n", keys.clear_breakpoints, function() - require("persistent-breakpoints.api").clear_all_breakpoints() - end, { silent = true, desc = "tiny-debugger: clear all breakpoints" }) + local pb = require("persistent-breakpoints.api") + local bindings = { + { keys.breakpoint, pb.toggle_breakpoint, "toggle breakpoint" }, + { keys.cond_breakpoint, pb.set_conditional_breakpoint, "conditional breakpoint" }, + { keys.clear_breakpoints, pb.clear_all_breakpoints, "clear all breakpoints" }, + } + for _, b in ipairs(bindings) do + if b[1] then + vim.keymap.set("n", b[1], b[2], { silent = true, desc = "tiny-debugger: " .. b[3] }) + end end end diff --git a/lua/tiny-debugger/config.lua b/lua/tiny-debugger/config.lua index a0bf780..ff89184 100644 --- a/lua/tiny-debugger/config.lua +++ b/lua/tiny-debugger/config.lua @@ -54,16 +54,13 @@ M.defaults = { adapters = {}, } -M.opts = vim.deepcopy(M.defaults) +M.opts = M.defaults function M.merge(user_opts) M.opts = vim.tbl_deep_extend("force", vim.deepcopy(M.defaults), user_opts or {}) - -- preserve false values for disabled keys (tbl_deep_extend drops them) if user_opts and user_opts.keys then for k, v in pairs(user_opts.keys) do - if v == false then - M.opts.keys[k] = false - end + if v == false then M.opts.keys[k] = false end end end return M.opts diff --git a/lua/tiny-debugger/help.lua b/lua/tiny-debugger/help.lua index 6f769d1..0686275 100644 --- a/lua/tiny-debugger/help.lua +++ b/lua/tiny-debugger/help.lua @@ -5,50 +5,37 @@ local M = {} local win_id = nil local buf_id = nil -local key_labels = { - continue = "Continue", - step_over = "Step over", - 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", - terminate = "Quit debugging", - restart = "Restart session", - help = "Show this help", +local key_info = { + { "continue", "Continue" }, + { "step_over", "Step over" }, + { "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" }, + { "terminate", "Quit debugging" }, + { "restart", "Restart session" }, + { "help", "Show this help" }, } --- ordered list of key names for consistent display -local key_order = { - "continue", "step_over", "step_into", "step_out", "run_to_cursor", - "breakpoint", "cond_breakpoint", "clear_breakpoints", "watch", "hover", "eval", - "terminate", "restart", "help", -} - -local function build_lines() +local function build_content() local lines = { " tiny-debugger keybindings", "" } + local width = #lines[1] local keys = config.opts.keys - for _, name in ipairs(key_order) do - local key = keys[name] + for _, pair in ipairs(key_info) do + local key = keys[pair[1]] if key then - local label = key_labels[name] or name - table.insert(lines, string.format(" %s %s", key, label)) + local line = string.format(" %s %s", key, pair[2]) + lines[#lines + 1] = line + if #line > width then width = #line end end end - table.insert(lines, "") - return lines -end - -local function calc_width(lines) - local max = 0 - for _, line in ipairs(lines) do - max = math.max(max, #line) - end - return max + 2 + lines[#lines + 1] = "" + return lines, width + 2 end function M.close() @@ -65,37 +52,29 @@ end function M.open() M.close() - local lines = build_lines() - local width = calc_width(lines) - local height = #lines + local lines, width = build_content() buf_id = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, lines) vim.bo[buf_id].modifiable = false vim.bo[buf_id].bufhidden = "wipe" - local editor_width = vim.o.columns - local editor_height = vim.o.lines - win_id = vim.api.nvim_open_win(buf_id, false, { relative = "editor", anchor = "SE", - row = editor_height - 2, - col = editor_width, + row = vim.o.lines - 2, + col = vim.o.columns, width = width, - height = height, + height = #lines, style = "minimal", border = "rounded", focusable = false, noautocmd = true, }) - -- auto-close on next keypress vim.api.nvim_create_autocmd({ "CursorMoved", "InsertEnter", "CmdlineEnter" }, { once = true, - callback = function() - M.close() - end, + callback = function() M.close() end, }) end diff --git a/lua/tiny-debugger/init.lua b/lua/tiny-debugger/init.lua index d7b32dc..626ce76 100644 --- a/lua/tiny-debugger/init.lua +++ b/lua/tiny-debugger/init.lua @@ -1,14 +1,12 @@ local config = require("tiny-debugger.config") local mode = require("tiny-debugger.mode") -local breakpoints = require("tiny-debugger.breakpoints") -local adapters = require("tiny-debugger.adapters") local M = {} function M.setup(opts) config.merge(opts) - breakpoints.setup() - adapters.setup() + require("tiny-debugger.breakpoints").setup() + require("tiny-debugger.adapters").setup() end function M.toggle() diff --git a/lua/tiny-debugger/mode.lua b/lua/tiny-debugger/mode.lua index a0b3724..3dcdefc 100644 --- a/lua/tiny-debugger/mode.lua +++ b/lua/tiny-debugger/mode.lua @@ -1,16 +1,40 @@ local config = require("tiny-debugger.config") local help = require("tiny-debugger.help") -local ui = require("tiny-debugger.ui") -local virtual_text = require("tiny-debugger.virtual_text") local M = {} local active = false +local active_buf = nil local stashed_maps = {} +-- one-time dap-ui and virtual-text initialization +local dapui_initialized = false +local vt_initialized = false + +local function ensure_dapui() + if dapui_initialized then return end + dapui_initialized = true + require("dapui").setup(config.opts.dapui) + + local dap = require("dap") + dap.listeners.after.event_terminated["tiny-debugger"] = function() + if active then M.exit() end + end + dap.listeners.after.event_exited["tiny-debugger"] = function() + if active then M.exit() end + end +end + +local function ensure_vt() + if vt_initialized then return end + vt_initialized = true + require("nvim-dap-virtual-text").setup(config.opts.virtual_text) +end + +-- keymap stash/restore + local function stash_mapping(buf, lhs) - local maps = vim.api.nvim_buf_get_keymap(buf, "n") - for _, map in ipairs(maps) do + for _, map in ipairs(vim.api.nvim_buf_get_keymap(buf, "n")) do if map.lhs == lhs then stashed_maps[lhs] = map return @@ -37,35 +61,24 @@ local function restore_mapping(buf, lhs) end end --- actions table: key name → function --- populated by other modules via M.register_action +-- actions (set up once) + local actions = {} +local visual_actions = { watch = true, hover = true } -function M.register_action(name, fn) - actions[name] = fn -end - --- default actions that map directly to dap calls -local function setup_default_actions() +local function setup_actions() + if next(actions) then return end 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.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 + if help.is_open() then help.close() else help.open() end end - actions.watch = function() local expr if vim.fn.mode() == "v" or vim.fn.mode() == "V" then @@ -78,18 +91,11 @@ local function setup_default_actions() require("dapui").elements.watches.add(expr) end end - - actions.hover = function() - require("dapui").eval() - end - - actions.eval = function() - require("dapui").float_element("repl") - end + actions.hover = function() require("dapui").eval() end + actions.eval = function() require("dapui").float_element("repl") end end --- actions that also get visual mode mappings -local visual_actions = { watch = true, hover = true } +-- keymap lifecycle local function set_keymaps(buf) local keys = config.opts.keys @@ -118,6 +124,8 @@ local function clear_keymaps(buf) stashed_maps = {} end +-- cursor highlight + local saved_cursor_hl = nil local function set_cursor_highlight() @@ -126,25 +134,26 @@ local function set_cursor_highlight() 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 + vim.api.nvim_set_hl(0, "Cursor", saved_cursor_hl or {}) + saved_cursor_hl = nil end +-- public API + function M.enter() if active then return end active = true + setup_actions() - setup_default_actions() - - local buf = vim.api.nvim_get_current_buf() - set_keymaps(buf) + active_buf = vim.api.nvim_get_current_buf() + set_keymaps(active_buf) set_cursor_highlight() - ui.open() - virtual_text.enable() + + ensure_dapui() + require("dapui").open() + + ensure_vt() + require("nvim-dap-virtual-text").enable() if config.opts.help_on_enter then help.open() @@ -155,20 +164,17 @@ function M.exit() if not active then return end active = false - local buf = vim.api.nvim_get_current_buf() - clear_keymaps(buf) + clear_keymaps(active_buf or vim.api.nvim_get_current_buf()) + active_buf = nil restore_cursor_highlight() help.close() - ui.close() - virtual_text.disable() + + if dapui_initialized then require("dapui").close() end + if vt_initialized then require("nvim-dap-virtual-text").disable() end end function M.toggle() - if active then - M.exit() - else - M.enter() - end + if active then M.exit() else M.enter() end end function M.is_active() diff --git a/lua/tiny-debugger/ui.lua b/lua/tiny-debugger/ui.lua deleted file mode 100644 index 29cda86..0000000 --- a/lua/tiny-debugger/ui.lua +++ /dev/null @@ -1,42 +0,0 @@ -local config = require("tiny-debugger.config") - -local M = {} - -local initialized = false - -local function ensure_setup() - if initialized then return end - initialized = true - - local dapui = require("dapui") - dapui.setup(config.opts.dapui) - - -- auto-exit debug mode when session ends - local dap = require("dap") - local mode = require("tiny-debugger.mode") - - dap.listeners.after.event_terminated["tiny-debugger"] = function() - if mode.is_active() then - mode.exit() - end - end - - dap.listeners.after.event_exited["tiny-debugger"] = function() - if mode.is_active() then - mode.exit() - end - end -end - -function M.open() - ensure_setup() - require("dapui").open() -end - -function M.close() - if initialized then - require("dapui").close() - end -end - -return M diff --git a/lua/tiny-debugger/virtual_text.lua b/lua/tiny-debugger/virtual_text.lua deleted file mode 100644 index 3f1235c..0000000 --- a/lua/tiny-debugger/virtual_text.lua +++ /dev/null @@ -1,24 +0,0 @@ -local config = require("tiny-debugger.config") - -local M = {} - -local initialized = false - -function M.setup() - if initialized then return end - initialized = true - require("nvim-dap-virtual-text").setup(config.opts.virtual_text) -end - -function M.enable() - M.setup() - require("nvim-dap-virtual-text").enable() -end - -function M.disable() - if initialized then - require("nvim-dap-virtual-text").disable() - end -end - -return M diff --git a/plugin/tiny-debugger.lua b/plugin/tiny-debugger.lua index d3effda..59a4b0f 100644 --- a/plugin/tiny-debugger.lua +++ b/plugin/tiny-debugger.lua @@ -1,22 +1,21 @@ -- tiny-debugger.nvim — entry point --- require Neovim 0.12+ if vim.fn.has("nvim-0.12") ~= 1 then vim.notify("tiny-debugger.nvim requires Neovim >= 0.12", vim.log.levels.ERROR) return end --- load dependencies via vim.pack vim.pack.add({ "mfussenegger/nvim-dap" }) 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" }) --- initialize with defaults (user can call setup() to override) -require("tiny-debugger").setup() +-- defer setup so user has time to call setup() with overrides +vim.schedule(function() + require("tiny-debugger").setup() +end) --- 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/helpers.lua b/tests/helpers.lua new file mode 100644 index 0000000..c7cb60f --- /dev/null +++ b/tests/helpers.lua @@ -0,0 +1,9 @@ +local M = {} + +function M.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 + +return M diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua index 4cbc7d9..6c6b3b8 100644 --- a/tests/minimal_init.lua +++ b/tests/minimal_init.lua @@ -1,2 +1,3 @@ -- Minimal init for testing tiny-debugger.nvim vim.opt.runtimepath:prepend(vim.fn.getcwd()) +package.path = vim.fn.getcwd() .. "/tests/?.lua;" .. package.path diff --git a/tests/test_adapters.lua b/tests/test_adapters.lua index 3ef39a6..4f033ec 100644 --- a/tests/test_adapters.lua +++ b/tests/test_adapters.lua @@ -1,41 +1,30 @@ --- tests for adapter loader and path resolution +local assert_eq = require("helpers").assert_eq local loader = require("tiny-debugger.adapters.init") +local dap = require("dap") -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 - --- test 1: find_executable returns nil for non-existent binaries +-- find_executable returns nil for non-existent binaries assert_eq(loader.find_executable({ "totally-fake-binary-xyz" }), nil, "non-existent binary returns nil") --- test 2: find_executable finds common system binaries -local found = loader.find_executable({ "sh" }) -assert_eq(found ~= nil, true, "find_executable finds sh") +-- find_executable finds common system binaries +assert_eq(loader.find_executable({ "sh" }) ~= nil, true, "find_executable finds sh") --- test 3: python adapter module loads and has register function -local py = require("tiny-debugger.adapters.python") -assert_eq(type(py.register), "function", "python adapter has register()") +-- all adapter modules load and have register() +for _, name in ipairs({ "python", "go", "c", "javascript" }) do + assert_eq(type(require("tiny-debugger.adapters." .. name).register), "function", name .. " has register()") +end --- test 4: go adapter module loads and has register function -local go = require("tiny-debugger.adapters.go") -assert_eq(type(go.register), "function", "go adapter has register()") - --- test 5: register doesn't override existing adapter -local dap = require("dap") +-- register doesn't override existing adapter dap.adapters.test_adapter = { type = "executable", command = "test" } loader.register(dap, "test_adapter", { type = "executable", command = "new" }, {}, {}) assert_eq(dap.adapters.test_adapter.command, "test", "register doesn't override existing") dap.adapters.test_adapter = nil --- test 6: register sets adapter and configs for new adapter +-- register sets adapter and configs for new adapter loader.register(dap, "test_new", { type = "executable", command = "test" }, { { type = "test_new", request = "launch", name = "test" }, }, { "testlang" }) assert_eq(dap.adapters.test_new.command, "test", "new adapter registered") assert_eq(#dap.configurations.testlang, 1, "configs registered for filetype") --- cleanup dap.adapters.test_new = nil dap.configurations.testlang = nil diff --git a/tests/test_breakpoints.lua b/tests/test_breakpoints.lua index 0829bc5..1c7fd5b 100644 --- a/tests/test_breakpoints.lua +++ b/tests/test_breakpoints.lua @@ -1,22 +1,14 @@ --- tests for persistent breakpoints +local assert_eq = require("helpers").assert_eq local td = require("tiny-debugger") td.setup() -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 +-- global x mapping exists +assert_eq(vim.fn.maparg("x", "n", false, true).desc, "tiny-debugger: toggle breakpoint", "global x mapping") --- test 1: global x mapping exists -local maparg = vim.fn.maparg("x", "n", false, true) -assert_eq(maparg.desc, "tiny-debugger: toggle breakpoint", "global x mapping exists") +-- global X mapping exists +assert_eq(vim.fn.maparg("X", "n", false, true).desc, "tiny-debugger: conditional breakpoint", "global X mapping") --- test 2: global X mapping exists -maparg = vim.fn.maparg("X", "n", false, true) -assert_eq(maparg.desc, "tiny-debugger: conditional breakpoint", "global X mapping exists") - --- test 3: persistent-breakpoints API is callable +-- persistent-breakpoints API is callable local api = require("persistent-breakpoints.api") assert_eq(type(api.toggle_breakpoint), "function", "toggle_breakpoint is a function") assert_eq(type(api.set_conditional_breakpoint), "function", "set_conditional_breakpoint is a function") diff --git a/tests/test_config.lua b/tests/test_config.lua index 8efa5be..59fe071 100644 --- a/tests/test_config.lua +++ b/tests/test_config.lua @@ -1,36 +1,29 @@ --- tests for config module +local assert_eq = require("helpers").assert_eq local config = require("tiny-debugger.config") -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 - --- test 1: defaults unchanged after empty merge +-- defaults unchanged after empty merge local opts = config.merge({}) assert_eq(opts.keys.continue, "c", "default continue key") assert_eq(opts.help_on_enter, true, "default help_on_enter") assert_eq(opts.builtin_adapters, true, "default builtin_adapters") -assert_eq(#opts.dapui.layouts, 2, "default layout count") --- test 2: partial key override +-- partial key override opts = config.merge({ keys = { continue = "g" } }) assert_eq(opts.keys.continue, "g", "overridden continue key") assert_eq(opts.keys.step_over, "s", "preserved step_over key") assert_eq(opts.keys.terminate, "q", "preserved terminate key") --- test 3: disable a key with false +-- disable a key with false opts = config.merge({ keys = { continue = false } }) assert_eq(opts.keys.continue, false, "disabled continue key") assert_eq(opts.keys.step_over, "s", "preserved step_over after disable") --- test 4: override help_on_enter +-- override help_on_enter opts = config.merge({ help_on_enter = false }) assert_eq(opts.help_on_enter, false, "overridden help_on_enter") assert_eq(opts.keys.continue, "c", "keys preserved after help override") --- test 5: merge doesn't mutate defaults +-- merge doesn't mutate defaults config.merge({ keys = { continue = "z" } }) assert_eq(config.defaults.keys.continue, "c", "defaults not mutated") diff --git a/tests/test_help.lua b/tests/test_help.lua index 807069f..36837d3 100644 --- a/tests/test_help.lua +++ b/tests/test_help.lua @@ -1,40 +1,26 @@ --- tests for help box +local assert_eq = require("helpers").assert_eq local help = require("tiny-debugger.help") local config = require("tiny-debugger.config") -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 - --- test 1: open creates a valid window +-- open creates a valid window help.open() assert_eq(help.is_open(), true, "help window is open") --- test 2: close removes the window +-- close removes the window help.close() assert_eq(help.is_open(), false, "help window is closed") --- test 3: remapped key shows in help content +-- remapped key shows in help content config.merge({ keys = { continue = "g" } }) help.open() --- read the buffer content -local wins = vim.api.nvim_list_wins() local found_g = false -for _, w in ipairs(wins) do - local buf = vim.api.nvim_win_get_buf(w) - local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) - for _, line in ipairs(lines) do - if line:find("g%s+Continue") then - found_g = true - end +for _, w in ipairs(vim.api.nvim_list_wins()) do + for _, line in ipairs(vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(w), 0, -1, false)) do + if line:find("g%s+Continue") then found_g = true end end end assert_eq(found_g, true, "remapped key 'g' appears in help") help.close() - --- reset config config.merge({}) print("ALL HELP TESTS PASSED") diff --git a/tests/test_keys.lua b/tests/test_keys.lua index 7a9b9c3..55765e9 100644 --- a/tests/test_keys.lua +++ b/tests/test_keys.lua @@ -1,15 +1,8 @@ --- tests for keybinding stash/restore +local assert_eq = require("helpers").assert_eq 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" }) +vim.keymap.set("n", "c", function() end, { buffer = 0, desc = "custom c" }) -- verify custom mapping exists local maparg = vim.fn.maparg("c", "n", false, true) diff --git a/tests/test_mode.lua b/tests/test_mode.lua index 3b0bdb8..230cbf0 100644 --- a/tests/test_mode.lua +++ b/tests/test_mode.lua @@ -1,42 +1,32 @@ --- tests for debug mode enter/exit +local assert_eq = require("helpers").assert_eq 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 + for _, m in ipairs(vim.api.nvim_buf_get_keymap(0, "n")) do if m.lhs == key then return true end end return false end --- test 1: starts inactive +-- starts inactive assert_eq(td.active(), false, "starts inactive") --- test 2: toggle enters debug mode +-- 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") +-- keymaps are set +for _, key in ipairs({ "c", "s", "d", "r", "q" }) do + assert_eq(has_buf_map(key), true, key .. " key mapped") +end --- test 4: toggle exits debug mode +-- 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") +-- keymaps are removed +for _, key in ipairs({ "c", "s", "d", "r", "q" }) do + assert_eq(has_buf_map(key), false, key .. " key removed") +end print("ALL MODE TESTS PASSED")