-- Debugging via nvim-dap. Adapter installs are bridged through Mason -- (mason-nvim-dap.nvim), the same pattern lsp.lua uses for language servers, -- so adding a debug adapter for another language later is just adding it to -- ensure_installed here - Python specifics live in dap-python.lua, Node -- specifics in lua/config/dap_node.lua. return { "mfussenegger/nvim-dap", -- Not left to lazy-load off `keys`: a project's .nvim.lua (see -- lua/config/project_run.lua) needs `require("dap")` to work immediately -- when exrc sources it, which requires the plugin already being on -- runtimepath rather than deferred until a d keymap fires. lazy = false, dependencies = { { "rcarriga/nvim-dap-ui", dependencies = { "nvim-neotest/nvim-nio" }, }, "theHamsta/nvim-dap-virtual-text", { "jay-babu/mason-nvim-dap.nvim", dependencies = { "mason-org/mason.nvim" }, }, }, keys = { { "db", function() require("dap").toggle_breakpoint() end, desc = "Toggle breakpoint" }, { "dB", function() require("dap").set_breakpoint(vim.fn.input("Breakpoint condition: ")) end, desc = "Conditional breakpoint", }, { "dc", function() local dap = require("dap") if dap.session() then -- Existing session: resume past a breakpoint, or nvim-dap's -- terminate/pause/restart prompt if active but not stopped. -- This branch doesn't consult buffer filetype, so it's fine to -- call from any window. dap.continue() return end -- No active session: pick a config to start. dap.continue()'s own -- path for this (select_config_and_run) only offers -- dap.configurations[], so calling it -- from the task-output dock or some other non-source window finds -- nothing. Flatten across every filetype instead - same as -- rr does for run tasks - so this works from anywhere and -- you just pick the config you want from the list. local entries = {} for filetype, configs in pairs(dap.configurations) do for _, config in ipairs(configs) do table.insert(entries, { config = config, filetype = filetype }) end end -- .vscode/launch.json (if present in cwd) is read on-demand by -- nvim-dap's own "dap.launch.json" provider - dap.ext.vscode's -- load_launchjs() is deprecated now that this exists. That provider -- is normally consulted inside dap.continue()'s own -- select_config_and_run, which we're bypassing above, so call it -- directly here to fold its configs into the same picker - the -- debug-side counterpart to overseer's tasks.json support (README's -- Run configurations section). Pair a launch.json entry with a -- same-named tasks.json entry for PyCharm-style "run or debug the -- same thing". local ok, launchjson_configs = pcall(dap.providers.configs["dap.launch.json"]) if ok then for _, config in ipairs(launchjson_configs) do table.insert(entries, { config = config, filetype = config.type }) end end if vim.tbl_isempty(entries) then vim.notify("No debug configurations registered (see project_run.lua)", vim.log.levels.WARN) return end vim.ui.select(entries, { prompt = "Configuration: ", format_item = function(e) return e.config.name end, }, function(entry) if entry then -- Pass along the filetype the config was registered under (not -- necessarily the invoking buffer's) so breakpoint-hit window -- jumping still picks a sensible window. dap.run(entry.config, { filetype = entry.filetype }) end end) end, desc = "Continue / start debugging", }, { "do", function() require("dap").step_over() end, desc = "Step over" }, { "di", function() require("dap").step_into() end, desc = "Step into" }, { "dO", function() require("dap").step_out() end, desc = "Step out" }, { "dt", function() require("dap").terminate() end, desc = "Terminate session" }, { "dr", function() require("dap").repl.toggle() end, desc = "Toggle REPL" }, { "du", function() require("dapui").toggle() end, desc = "Toggle debug UI" }, }, config = function() require("dapui").setup() require("nvim-dap-virtual-text").setup() -- nvim-dap doesn't define these signs itself - it just places them -- (breakpoints.lua, session.lua's DapStopped placement) and expects the -- config to have already called sign_define, so without this nothing -- renders: no breakpoint marker in the sign column, and no indication -- of the current line when a session stops (the stop itself still -- works, sign_place just errors silently into a pcall). vim.fn.sign_define("DapBreakpoint", { text = "●", texthl = "DiagnosticError" }) vim.fn.sign_define("DapBreakpointCondition", { text = "◆", texthl = "DiagnosticWarn" }) vim.fn.sign_define("DapBreakpointRejected", { text = "✗", texthl = "DiagnosticError" }) vim.fn.sign_define("DapLogPoint", { text = "📝", texthl = "DiagnosticInfo" }) vim.fn.sign_define("DapStopped", { text = "▶", texthl = "DiagnosticWarn", linehl = "DapStoppedLine" }) -- default = true so a colorscheme (or later config) can override this -- without a load-order fight; Visual is just a visible fallback. vim.api.nvim_set_hl(0, "DapStoppedLine", { default = true, link = "Visual" }) -- handlers = {} opts out of mason-nvim-dap's automatic adapter -- registration: nvim-dap-python (dap-python.lua) registers its own -- "python" adapter/config, so we don't want a second one clobbering it. -- Adding a language without its own dap-.lua later should drop -- this override so its adapter gets auto-registered instead. require("mason-nvim-dap").setup({ -- ensure_installed wants nvim-dap *adapter/source* names, not Mason -- *package* names - mason-nvim-dap looks each entry up in its own -- name -> package mapping (source.lua: python -> debugpy) and just -- silently skips anything it doesn't recognize, no error/log. Passing -- "debugpy" here (the package name) matched nothing, so it never -- triggered an install at all. ensure_installed = { "python", "js" }, automatic_installation = true, handlers = {}, }) -- Node/JS/TS adapter + default configs - not left to mason-nvim-dap's -- own handler since "js" has no default adapter mapping there (only the -- deprecated node2 does), see lua/config/dap_node.lua for why. require("config.dap_node").setup() local dap, dapui = require("dap"), require("dapui") -- nvim-dap jumps to the stopped line using Neovim's 'switchbuf' option, -- which defaults to "uselast": reuse the current window if it's already -- a plain/source buffer, else fall back to the alternate window - and -- if neither applies (e.g. dc was started from the task-output -- dock or some other non-source window, which the picker now allows), -- it silently does nothing. No error, no jump - the session *is* -- stopped (stepping/continuing still works), it just never shows where. -- Append "vsplit" so there's always a guaranteed-successful last resort -- that opens the source file if nothing else matched. dap.defaults.fallback.switchbuf = "usevisible,useopen,uselast,vsplit" -- Open/close dap-ui automatically around a debug session, so du -- is only needed to bring it back after manually closing it mid-session. dap.listeners.after.event_initialized["dapui_config"] = function() dapui.open() end dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close() end dap.listeners.before.event_exited["dapui_config"] = function() dapui.close() end -- These events arrive asynchronously over debugpy's socket (a libuv -- callback, not a keypress), and nvim-dap updates its state - cursor -- position, signs, dapui panels - without that alone forcing Neovim to -- repaint. So hitting a breakpoint, continuing past one, or a session -- ending can all silently not *look* like anything happened until the -- next keystroke forces a redraw that reveals the already-current -- state (e.g. dc appearing to need pressing twice to resume). for _, event in ipairs({ "event_stopped", "event_continued", "event_terminated", "event_exited" }) do dap.listeners.after[event]["dap_redraw"] = function() vim.cmd("redraw") end end end, }