From ae05edbd0de48db9111fb5dad0a2f64222663a97 Mon Sep 17 00:00:00 2001 From: Patrick Perkins Date: Wed, 11 Mar 2026 11:49:13 -0400 Subject: [PATCH 1/5] add nvim-dap with ruby/debug adapter and per-project exrc support Installs nvim-dap + nvim-dap-ui for interactive debugging. Configures a Ruby adapter that handles both local rdbg launch and TCP attach for remote sessions (e.g. Vagrant+Docker). Adds d* keymaps for breakpoints, stepping, and UI. Enables exrc so per-project .nvim.lua files can override DAP launch configs. Closes #5 --- .config/nvim/lua/config/keymaps.lua | 10 ++++ .config/nvim/lua/config/options.lua | 4 ++ .config/nvim/lua/config/plugins/dap.lua | 74 +++++++++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 .config/nvim/lua/config/plugins/dap.lua diff --git a/.config/nvim/lua/config/keymaps.lua b/.config/nvim/lua/config/keymaps.lua index d21f77e..a13db8b 100644 --- a/.config/nvim/lua/config/keymaps.lua +++ b/.config/nvim/lua/config/keymaps.lua @@ -75,6 +75,16 @@ vim.keymap.set( vim.keymap.set("n", "xL", "Trouble loclist toggle", { desc = "Location List (Trouble)" }) vim.keymap.set("n", "xQ", "Trouble qflist toggle", { desc = "Quickfix List (Trouble)" }) +-- DAP (debugging) +vim.keymap.set("n", "db", function() require("dap").toggle_breakpoint() end, { desc = "Toggle breakpoint" }) +vim.keymap.set("n", "dc", function() require("dap").continue() end, { desc = "Continue / start" }) +vim.keymap.set("n", "dn", function() require("dap").step_over() end, { desc = "Step over" }) +vim.keymap.set("n", "di", function() require("dap").step_into() end, { desc = "Step into" }) +vim.keymap.set("n", "do", function() require("dap").step_out() end, { desc = "Step out" }) +vim.keymap.set("n", "dq", function() require("dap").terminate() end, { desc = "Terminate session" }) +vim.keymap.set("n", "du", function() require("dapui").toggle() end, { desc = "Toggle DAP UI" }) +vim.keymap.set("n", "dr", function() require("dap").repl.toggle() end, { desc = "Toggle REPL" }) + -- LazyGit vim.keymap.set("n", "lg", "LazyGit", { desc = "LazyGit" }) diff --git a/.config/nvim/lua/config/options.lua b/.config/nvim/lua/config/options.lua index ddbc628..d4014e4 100644 --- a/.config/nvim/lua/config/options.lua +++ b/.config/nvim/lua/config/options.lua @@ -19,3 +19,7 @@ vim.opt.conceallevel = 0 -- Window options vim.wo.wrap = true + +-- Load per-project .nvim.lua files (used for DAP launch configs, etc.) +vim.opt.exrc = true +vim.opt.secure = true diff --git a/.config/nvim/lua/config/plugins/dap.lua b/.config/nvim/lua/config/plugins/dap.lua new file mode 100644 index 0000000..5765b7e --- /dev/null +++ b/.config/nvim/lua/config/plugins/dap.lua @@ -0,0 +1,74 @@ +return { + { + "mfussenegger/nvim-dap", + dependencies = { + { "rcarriga/nvim-dap-ui", dependencies = { "nvim-neotest/nvim-nio" } }, + }, + config = function() + local dap = require("dap") + local dapui = require("dapui") + + dapui.setup() + + -- Ruby adapter. + -- "attach" connects to a running rdbg TCP server (local or remote). + -- "launch" starts rdbg locally and attaches immediately. + -- For Vagrant+Docker, always use "attach" with port forwarded to host. + dap.adapters.ruby = function(callback, config) + if config.request == "attach" then + callback({ + type = "server", + host = config.host or "127.0.0.1", + port = config.port or 12345, + }) + else + callback({ + type = "server", + host = "127.0.0.1", + port = config.port or 12345, + executable = { + command = "bundle", + args = { + "exec", "rdbg", + "--open", "--port", tostring(config.port or 12345), + "-c", "--", config.command or "ruby", + unpack(config.args or {}), + }, + }, + }) + end + end + + -- Default configurations for local development. + -- For Vagrant projects, override these in a .nvim.lua at the project root + -- (requires vim.opt.exrc = true in options.lua). + dap.configurations.ruby = { + { + type = "ruby", + name = "Attach to rdbg (local)", + request = "attach", + port = 12345, + }, + { + type = "ruby", + name = "Run current file", + request = "launch", + command = "ruby", + args = { "${file}" }, + }, + { + type = "ruby", + name = "Run RSpec (current file)", + request = "launch", + command = "rspec", + args = { "${file}" }, + }, + } + + -- Auto open/close dap-ui with sessions + dap.listeners.after.event_initialized["dapui"] = dapui.open + dap.listeners.before.event_terminated["dapui"] = dapui.close + dap.listeners.before.event_exited["dapui"] = dapui.close + end, + }, +} From 09c6c5bbe4370897ab0f51dcc8a9543a180df570 Mon Sep 17 00:00:00 2001 From: Patrick Perkins Date: Thu, 12 Mar 2026 10:58:46 -0400 Subject: [PATCH 2/5] improve dap for rspec and rails server workflows Add --nonstop to rdbg launch so execution runs freely until binding.b rather than pausing at the first line. Add ds/dl to run the current spec file or line directly under the debugger without going through the picker. Simplify configurations to a single Attach entry; spec runs use dap.run() directly from keymaps. --- .config/nvim/lua/config/keymaps.lua | 17 ++++++++++++- .config/nvim/lua/config/plugins/dap.lua | 34 ++++++++++--------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/.config/nvim/lua/config/keymaps.lua b/.config/nvim/lua/config/keymaps.lua index cc5964c..3446508 100644 --- a/.config/nvim/lua/config/keymaps.lua +++ b/.config/nvim/lua/config/keymaps.lua @@ -83,13 +83,28 @@ vim.keymap.set("n", "xQ", "Trouble qflist toggle", { desc = "Qu -- DAP (debugging) vim.keymap.set("n", "db", function() require("dap").toggle_breakpoint() end, { desc = "Toggle breakpoint" }) -vim.keymap.set("n", "dc", function() require("dap").continue() end, { desc = "Continue / start" }) +vim.keymap.set("n", "dc", function() require("dap").continue() end, { desc = "Continue / attach" }) vim.keymap.set("n", "dn", function() require("dap").step_over() end, { desc = "Step over" }) vim.keymap.set("n", "di", function() require("dap").step_into() end, { desc = "Step into" }) vim.keymap.set("n", "do", function() require("dap").step_out() end, { desc = "Step out" }) vim.keymap.set("n", "dq", function() require("dap").terminate() end, { desc = "Terminate session" }) vim.keymap.set("n", "du", function() require("dapui").toggle() end, { desc = "Toggle DAP UI" }) vim.keymap.set("n", "dr", function() require("dap").repl.toggle() end, { desc = "Toggle REPL" }) +-- Run RSpec under the debugger — skips the picker, binding.b breaks into dap-ui +vim.keymap.set("n", "ds", function() + require("dap").run({ + type = "ruby", request = "launch", + name = "RSpec (current file)", + command = "rspec", args = { vim.fn.expand("%:p") }, + }) +end, { desc = "Debug: RSpec current file" }) +vim.keymap.set("n", "dl", function() + require("dap").run({ + type = "ruby", request = "launch", + name = "RSpec (current line)", + command = "rspec", args = { vim.fn.expand("%:p") .. ":" .. vim.fn.line(".") }, + }) +end, { desc = "Debug: RSpec current line" }) -- LazyGit vim.keymap.set("n", "lg", "LazyGit", { desc = "LazyGit" }) diff --git a/.config/nvim/lua/config/plugins/dap.lua b/.config/nvim/lua/config/plugins/dap.lua index 5765b7e..9c21965 100644 --- a/.config/nvim/lua/config/plugins/dap.lua +++ b/.config/nvim/lua/config/plugins/dap.lua @@ -12,8 +12,9 @@ return { -- Ruby adapter. -- "attach" connects to a running rdbg TCP server (local or remote). - -- "launch" starts rdbg locally and attaches immediately. - -- For Vagrant+Docker, always use "attach" with port forwarded to host. + -- "launch" starts rdbg as a child process and connects immediately. + -- --nonstop: don't pause at the first line; run freely until binding.b + -- For Vagrant+Docker, use "attach" with the port forwarded to localhost. dap.adapters.ruby = function(callback, config) if config.request == "attach" then callback({ @@ -31,7 +32,7 @@ return { args = { "exec", "rdbg", "--open", "--port", tostring(config.port or 12345), - "-c", "--", config.command or "ruby", + "--nonstop", "-c", "--", config.command or "ruby", unpack(config.args or {}), }, }, @@ -39,30 +40,21 @@ return { end end - -- Default configurations for local development. - -- For Vagrant projects, override these in a .nvim.lua at the project root - -- (requires vim.opt.exrc = true in options.lua). + -- Picker configurations (dc to select). + -- For RSpec, prefer the direct keymaps (ds / dl) which + -- skip the picker and pass the current file/line automatically. + -- + -- For a running Rails server, start it with: + -- RUBY_DEBUG_OPEN=1 RUBY_DEBUG_PORT=12345 RUBY_DEBUG_HOST=127.0.0.1 bundle exec rails s + -- then use "Attach" below. When the browser hits a binding.b, dap-ui opens. + -- For Vagrant projects, override in a .nvim.lua at the project root. dap.configurations.ruby = { { type = "ruby", - name = "Attach to rdbg (local)", + name = "Attach", request = "attach", port = 12345, }, - { - type = "ruby", - name = "Run current file", - request = "launch", - command = "ruby", - args = { "${file}" }, - }, - { - type = "ruby", - name = "Run RSpec (current file)", - request = "launch", - command = "rspec", - args = { "${file}" }, - }, } -- Auto open/close dap-ui with sessions From aa4bc440e3ae0e3e1c0e38e820c0165eea213452 Mon Sep 17 00:00:00 2001 From: Patrick Perkins Date: Thu, 12 Mar 2026 11:03:28 -0400 Subject: [PATCH 3/5] fix rdbg cwd by finding Gemfile root --- .config/nvim/lua/config/plugins/dap.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.config/nvim/lua/config/plugins/dap.lua b/.config/nvim/lua/config/plugins/dap.lua index 9c21965..f17ee5f 100644 --- a/.config/nvim/lua/config/plugins/dap.lua +++ b/.config/nvim/lua/config/plugins/dap.lua @@ -23,6 +23,10 @@ return { port = config.port or 12345, }) else + -- Find the project root by locating the Gemfile upward from the + -- current file, so bundle exec runs in the right directory. + local gemfile = vim.fn.findfile("Gemfile", ".;") + local cwd = gemfile ~= "" and vim.fn.fnamemodify(gemfile, ":p:h") or vim.fn.getcwd() callback({ type = "server", host = "127.0.0.1", @@ -35,6 +39,7 @@ return { "--nonstop", "-c", "--", config.command or "ruby", unpack(config.args or {}), }, + cwd = cwd, }, }) end From 15d2031588f9bb49423b59c02f7c577853d654b7 Mon Sep 17 00:00:00 2001 From: Patrick Perkins Date: Thu, 12 Mar 2026 15:07:38 -0400 Subject: [PATCH 4/5] remove --nonstop and auto-close dapui Without --nonstop rdbg pauses before executing anything, waits for DAP to register breakpoints, then continues on dc. This eliminates the race where tests complete before breakpoints are set. Remove auto-close listeners so dapui stays open after the session ends and the last state remains inspectable. Close manually with du. --- .config/nvim/lua/config/plugins/dap.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.config/nvim/lua/config/plugins/dap.lua b/.config/nvim/lua/config/plugins/dap.lua index f17ee5f..179e06c 100644 --- a/.config/nvim/lua/config/plugins/dap.lua +++ b/.config/nvim/lua/config/plugins/dap.lua @@ -12,8 +12,11 @@ return { -- Ruby adapter. -- "attach" connects to a running rdbg TCP server (local or remote). - -- "launch" starts rdbg as a child process and connects immediately. - -- --nonstop: don't pause at the first line; run freely until binding.b + -- "launch" starts rdbg as a child process. + -- Without --nonstop, rdbg pauses at the first line and waits for DAP + -- to finish setup (setBreakpoints, configurationDone) before running. + -- This avoids a race where the test completes before breakpoints are set. + -- Press dc once after launch to start execution. -- For Vagrant+Docker, use "attach" with the port forwarded to localhost. dap.adapters.ruby = function(callback, config) if config.request == "attach" then @@ -36,7 +39,7 @@ return { args = { "exec", "rdbg", "--open", "--port", tostring(config.port or 12345), - "--nonstop", "-c", "--", config.command or "ruby", + "-c", "--", config.command or "ruby", unpack(config.args or {}), }, cwd = cwd, @@ -62,10 +65,9 @@ return { }, } - -- Auto open/close dap-ui with sessions + -- Auto-open dap-ui when a session starts. Not auto-closing on exit so + -- the last state stays visible — close manually with du. dap.listeners.after.event_initialized["dapui"] = dapui.open - dap.listeners.before.event_terminated["dapui"] = dapui.close - dap.listeners.before.event_exited["dapui"] = dapui.close end, }, } From 37826ca5d0d26956b0d8b4f37f9b566c6507bdd5 Mon Sep 17 00:00:00 2001 From: Patrick Perkins Date: Thu, 12 Mar 2026 15:20:50 -0400 Subject: [PATCH 5/5] auto-continue past rdbg entry pause for one-keypress rspec workflow --- .config/nvim/lua/config/plugins/dap.lua | 44 +++++++++++++++---------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/.config/nvim/lua/config/plugins/dap.lua b/.config/nvim/lua/config/plugins/dap.lua index 179e06c..a0c4534 100644 --- a/.config/nvim/lua/config/plugins/dap.lua +++ b/.config/nvim/lua/config/plugins/dap.lua @@ -11,13 +11,18 @@ return { dapui.setup() -- Ruby adapter. - -- "attach" connects to a running rdbg TCP server (local or remote). - -- "launch" starts rdbg as a child process. - -- Without --nonstop, rdbg pauses at the first line and waits for DAP - -- to finish setup (setBreakpoints, configurationDone) before running. - -- This avoids a race where the test completes before breakpoints are set. - -- Press dc once after launch to start execution. - -- For Vagrant+Docker, use "attach" with the port forwarded to localhost. + -- + -- "attach" mode: connect to an already-running rdbg TCP server. + -- Used for Rails server (started externally with RUBY_DEBUG_OPEN=1) + -- and for Vagrant+Docker (port forwarded to localhost). + -- + -- "launch" mode: rdbg starts the process as a child. + -- rdbg opens a TCP server, pauses before running any code, and waits + -- for nvim-dap to connect and register breakpoints. Once that handshake + -- completes, the "entry" stopped event fires — we auto-continue past it + -- (see listener below) so execution runs straight to your breakpoint. + -- cwd is resolved by finding the Gemfile so bundle exec works from any + -- subdirectory. dap.adapters.ruby = function(callback, config) if config.request == "attach" then callback({ @@ -26,8 +31,6 @@ return { port = config.port or 12345, }) else - -- Find the project root by locating the Gemfile upward from the - -- current file, so bundle exec runs in the right directory. local gemfile = vim.fn.findfile("Gemfile", ".;") local cwd = gemfile ~= "" and vim.fn.fnamemodify(gemfile, ":p:h") or vim.fn.getcwd() callback({ @@ -48,14 +51,10 @@ return { end end - -- Picker configurations (dc to select). - -- For RSpec, prefer the direct keymaps (ds / dl) which - -- skip the picker and pass the current file/line automatically. - -- -- For a running Rails server, start it with: -- RUBY_DEBUG_OPEN=1 RUBY_DEBUG_PORT=12345 RUBY_DEBUG_HOST=127.0.0.1 bundle exec rails s - -- then use "Attach" below. When the browser hits a binding.b, dap-ui opens. - -- For Vagrant projects, override in a .nvim.lua at the project root. + -- then dc → "Attach". For Vagrant projects, override in .nvim.lua. + -- For RSpec use ds (file) or dl (line) — skips this picker. dap.configurations.ruby = { { type = "ruby", @@ -65,9 +64,20 @@ return { }, } - -- Auto-open dap-ui when a session starts. Not auto-closing on exit so - -- the last state stays visible — close manually with du. + -- Auto-open dap-ui when a session initialises. + -- dap-ui stays open after the session ends so you can inspect the final + -- state — close it manually with du. dap.listeners.after.event_initialized["dapui"] = dapui.open + + -- In launch mode rdbg pauses at the program entry point before running + -- any code, ensuring nvim-dap has registered all breakpoints first. This + -- listener auto-continues past that initial pause so ds is one + -- keypress — execution runs straight to your first breakpoint. + dap.listeners.after.event_stopped["ruby_skip_entry"] = function(_, body) + if body.reason == "entry" then + dap.continue() + end + end end, }, }