local dap, dapui = require("dap"), require("dapui") -- Install debug adapters via Mason (same pattern as the LSP tool loop in -- plugins.lua). codelldb -> c/cpp/rust, debugpy -> python, -- js-debug-adapter -> node/ts, haskell-debug-adapter -> haskell. -- java-debug-adapter is installed by the LSP ensure loop and wired through -- jdtls (see lua/lsp.lua). -- Note: haskell-debug-adapter builds from source via cabal on first install -- (needs ghc/cabal on PATH) so it can take a few minutes to appear. local registry = require("mason-registry") registry.refresh(function() for _, tool in ipairs({ "debugpy", "codelldb", "js-debug-adapter", "haskell-debug-adapter" }) do local ok, pkg = pcall(registry.get_package, tool) if ok and not pkg:is_installed() then pkg:install() end end end) dapui.setup() require("nvim-dap-virtual-text").setup() -- Auto-wire adapters + default launch configs for every mason-installed -- adapter. Empty handlers = use mason-nvim-dap's default handler for all. -- To add a language later: install its adapter in the loop above; it is -- configured here automatically. Override by defining dap.configurations. -- below this call. require("mason-nvim-dap").setup({ ensure_installed = {}, automatic_installation = false, handlers = {}, }) -- JS/TS (node): mason-nvim-dap ships no default handler for js-debug-adapter, -- so wire the pwa-node adapter and launch/attach configs manually. dap.adapters["pwa-node"] = { type = "server", host = "localhost", port = "${port}", executable = { command = "node", args = { vim.fs.joinpath(vim.fn.stdpath("data"), "mason", "packages", "js-debug-adapter", "js-debug", "src", "dapDebugServer.js"), "${port}", }, }, } for _, ft in ipairs({ "javascript", "typescript" }) do dap.configurations[ft] = { { type = "pwa-node", request = "launch", name = "Launch file", program = "${file}", cwd = "${workspaceFolder}", }, { type = "pwa-node", request = "attach", name = "Attach to process", processId = require("dap.utils").pick_process, cwd = "${workspaceFolder}", }, } end -- Haskell (plain ghc): mason-nvim-dap's default handler auto-wires the -- haskell-debug-adapter, but its default launch config assumes `stack ghci` -- (with a literal "TARGET" placeholder). Override it to drive ghci-dap -- directly so a bare `.hs` file debugs with — no cabal/stack project -- required. ghci-dap resolves via PATH (mason bin). Switch ghciCmd to -- `cabal exec -- ghci-dap ...` or `stack ghci --with-ghc=ghci-dap ...` if you -- later move to a build tool. Verified end-to-end: loads ${file}, stops at -- entry, hits breakpoints. dap.configurations.haskell = { { type = "haskell", request = "launch", name = "Debug (ghc)", workspace = "${workspaceFolder}", startup = "${file}", stopOnEntry = true, logFile = vim.fs.joinpath(vim.fn.stdpath("data"), "haskell-dap.log"), logLevel = "WARNING", ghciEnv = vim.empty_dict(), ghciPrompt = "H>>= ", ghciInitialPrompt = "ghci> ", ghciCmd = "ghci-dap --interactive -i${workspaceFolder} ${startup}", }, } -- Run debuggees in dap-ui's Console terminal so programs that read user input -- work. Without this, nvim-dap uses an output-only internal console with no -- stdin (the program just hangs on input()/getLine/scanf). dap-ui registers its -- Console as dap.defaults.fallback.terminal_win_cmd, so any adapter that runs -- the program in an integrated terminal is routed there. Adapters disagree on -- the key -- debugpy/pwa-node use `console`, codelldb uses `terminal` -- and -- each ignores the other, so set both on every launch config. Applies to the -- mason-auto-generated configs (python/cpp/rust) too, since it runs after their -- setup. To type: focus the Console window and press `i` (terminal insert). for _, configs in pairs(dap.configurations) do for _, cfg in ipairs(configs) do if cfg.request == "launch" then if cfg.console == nil then cfg.console = "integratedTerminal" end if cfg.terminal == nil then cfg.terminal = "integrated" end end end end -- Open/close the dap-ui panels automatically with the session. dap.listeners.before.attach.dapui_config = function() dapui.open() end dap.listeners.before.launch.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 -- VS Code standard F-keys local map = vim.keymap.set map("n", "", dap.continue, { desc = "Debug: continue / start" }) map("n", "", dap.terminate, { desc = "Debug: stop" }) map("n", "", dap.restart, { desc = "Debug: restart" }) map("n", "", dap.pause, { desc = "Debug: pause" }) map("n", "", dap.toggle_breakpoint, { desc = "Debug: toggle breakpoint" }) map("n", "", dap.step_over, { desc = "Debug: step over" }) map("n", "", dap.step_into, { desc = "Debug: step into" }) map("n", "", dap.step_out, { desc = "Debug: step out" }) -- Leader maps (preserve the previous d* bindings + a few niceties; -- discoverable via Pick keymaps thanks to desc). map("n", "duo", dapui.open, { desc = "Debug: open UI" }) map("n", "duc", dapui.close, { desc = "Debug: close UI" }) map("n", "dbp", dap.toggle_breakpoint, { desc = "Debug: toggle breakpoint" }) map("n", "drc", dap.run_to_cursor, { desc = "Debug: run to cursor" }) map("n", "dr", dap.repl.toggle, { desc = "Debug: toggle REPL" }) map("n", "de", function() dapui.eval() end, { desc = "Debug: eval under cursor" }) map("n", "dB", function() dap.set_breakpoint(vim.fn.input("Breakpoint condition: ")) end, { desc = "Debug: conditional breakpoint" })