aboutsummaryrefslogtreecommitdiffstats
path: root/neovim/.config/nvim/lua/plugins/dap.lua
diff options
context:
space:
mode:
Diffstat (limited to 'neovim/.config/nvim/lua/plugins/dap.lua')
-rw-r--r--neovim/.config/nvim/lua/plugins/dap.lua181
1 files changed, 181 insertions, 0 deletions
diff --git a/neovim/.config/nvim/lua/plugins/dap.lua b/neovim/.config/nvim/lua/plugins/dap.lua
new file mode 100644
index 0000000..975eaea
--- /dev/null
+++ b/neovim/.config/nvim/lua/plugins/dap.lua
@@ -0,0 +1,181 @@
1-- Debugging via nvim-dap. Adapter installs are bridged through Mason
2-- (mason-nvim-dap.nvim), the same pattern lsp.lua uses for language servers,
3-- so adding a debug adapter for another language later is just adding it to
4-- ensure_installed here - Python specifics live in dap-python.lua, Node
5-- specifics in lua/config/dap_node.lua.
6return {
7 "mfussenegger/nvim-dap",
8 -- Not left to lazy-load off `keys`: a project's .nvim.lua (see
9 -- lua/config/project_run.lua) needs `require("dap")` to work immediately
10 -- when exrc sources it, which requires the plugin already being on
11 -- runtimepath rather than deferred until a <leader>d keymap fires.
12 lazy = false,
13 dependencies = {
14 {
15 "rcarriga/nvim-dap-ui",
16 dependencies = { "nvim-neotest/nvim-nio" },
17 },
18 "theHamsta/nvim-dap-virtual-text",
19 {
20 "jay-babu/mason-nvim-dap.nvim",
21 dependencies = { "mason-org/mason.nvim" },
22 },
23 },
24 keys = {
25 { "<leader>db", function() require("dap").toggle_breakpoint() end, desc = "Toggle breakpoint" },
26 {
27 "<leader>dB",
28 function() require("dap").set_breakpoint(vim.fn.input("Breakpoint condition: ")) end,
29 desc = "Conditional breakpoint",
30 },
31 {
32 "<leader>dc",
33 function()
34 local dap = require("dap")
35
36 if dap.session() then
37 -- Existing session: resume past a breakpoint, or nvim-dap's
38 -- terminate/pause/restart prompt if active but not stopped.
39 -- This branch doesn't consult buffer filetype, so it's fine to
40 -- call from any window.
41 dap.continue()
42 return
43 end
44
45 -- No active session: pick a config to start. dap.continue()'s own
46 -- path for this (select_config_and_run) only offers
47 -- dap.configurations[<current buffer's filetype>], so calling it
48 -- from the task-output dock or some other non-source window finds
49 -- nothing. Flatten across every filetype instead - same as
50 -- <leader>rr does for run tasks - so this works from anywhere and
51 -- you just pick the config you want from the list.
52 local entries = {}
53 for filetype, configs in pairs(dap.configurations) do
54 for _, config in ipairs(configs) do
55 table.insert(entries, { config = config, filetype = filetype })
56 end
57 end
58
59 -- .vscode/launch.json (if present in cwd) is read on-demand by
60 -- nvim-dap's own "dap.launch.json" provider - dap.ext.vscode's
61 -- load_launchjs() is deprecated now that this exists. That provider
62 -- is normally consulted inside dap.continue()'s own
63 -- select_config_and_run, which we're bypassing above, so call it
64 -- directly here to fold its configs into the same picker - the
65 -- debug-side counterpart to overseer's tasks.json support (README's
66 -- Run configurations section). Pair a launch.json entry with a
67 -- same-named tasks.json entry for PyCharm-style "run or debug the
68 -- same thing".
69 local ok, launchjson_configs = pcall(dap.providers.configs["dap.launch.json"])
70 if ok then
71 for _, config in ipairs(launchjson_configs) do
72 table.insert(entries, { config = config, filetype = config.type })
73 end
74 end
75
76 if vim.tbl_isempty(entries) then
77 vim.notify("No debug configurations registered (see project_run.lua)", vim.log.levels.WARN)
78 return
79 end
80
81 vim.ui.select(entries, {
82 prompt = "Configuration: ",
83 format_item = function(e) return e.config.name end,
84 }, function(entry)
85 if entry then
86 -- Pass along the filetype the config was registered under (not
87 -- necessarily the invoking buffer's) so breakpoint-hit window
88 -- jumping still picks a sensible window.
89 dap.run(entry.config, { filetype = entry.filetype })
90 end
91 end)
92 end,
93 desc = "Continue / start debugging",
94 },
95 { "<leader>do", function() require("dap").step_over() end, desc = "Step over" },
96 { "<leader>di", function() require("dap").step_into() end, desc = "Step into" },
97 { "<leader>dO", function() require("dap").step_out() end, desc = "Step out" },
98 { "<leader>dt", function() require("dap").terminate() end, desc = "Terminate session" },
99 { "<leader>dr", function() require("dap").repl.toggle() end, desc = "Toggle REPL" },
100 { "<leader>du", function() require("dapui").toggle() end, desc = "Toggle debug UI" },
101 },
102 config = function()
103 require("dapui").setup()
104 require("nvim-dap-virtual-text").setup()
105
106 -- nvim-dap doesn't define these signs itself - it just places them
107 -- (breakpoints.lua, session.lua's DapStopped placement) and expects the
108 -- config to have already called sign_define, so without this nothing
109 -- renders: no breakpoint marker in the sign column, and no indication
110 -- of the current line when a session stops (the stop itself still
111 -- works, sign_place just errors silently into a pcall).
112 vim.fn.sign_define("DapBreakpoint", { text = "●", texthl = "DiagnosticError" })
113 vim.fn.sign_define("DapBreakpointCondition", { text = "◆", texthl = "DiagnosticWarn" })
114 vim.fn.sign_define("DapBreakpointRejected", { text = "✗", texthl = "DiagnosticError" })
115 vim.fn.sign_define("DapLogPoint", { text = "📝", texthl = "DiagnosticInfo" })
116 vim.fn.sign_define("DapStopped", { text = "▶", texthl = "DiagnosticWarn", linehl = "DapStoppedLine" })
117 -- default = true so a colorscheme (or later config) can override this
118 -- without a load-order fight; Visual is just a visible fallback.
119 vim.api.nvim_set_hl(0, "DapStoppedLine", { default = true, link = "Visual" })
120
121 -- handlers = {} opts out of mason-nvim-dap's automatic adapter
122 -- registration: nvim-dap-python (dap-python.lua) registers its own
123 -- "python" adapter/config, so we don't want a second one clobbering it.
124 -- Adding a language without its own dap-<lang>.lua later should drop
125 -- this override so its adapter gets auto-registered instead.
126 require("mason-nvim-dap").setup({
127 -- ensure_installed wants nvim-dap *adapter/source* names, not Mason
128 -- *package* names - mason-nvim-dap looks each entry up in its own
129 -- name -> package mapping (source.lua: python -> debugpy) and just
130 -- silently skips anything it doesn't recognize, no error/log. Passing
131 -- "debugpy" here (the package name) matched nothing, so it never
132 -- triggered an install at all.
133 ensure_installed = { "python", "js" },
134 automatic_installation = true,
135 handlers = {},
136 })
137
138 -- Node/JS/TS adapter + default configs - not left to mason-nvim-dap's
139 -- own handler since "js" has no default adapter mapping there (only the
140 -- deprecated node2 does), see lua/config/dap_node.lua for why.
141 require("config.dap_node").setup()
142
143 local dap, dapui = require("dap"), require("dapui")
144
145 -- nvim-dap jumps to the stopped line using Neovim's 'switchbuf' option,
146 -- which defaults to "uselast": reuse the current window if it's already
147 -- a plain/source buffer, else fall back to the alternate window - and
148 -- if neither applies (e.g. <leader>dc was started from the task-output
149 -- dock or some other non-source window, which the picker now allows),
150 -- it silently does nothing. No error, no jump - the session *is*
151 -- stopped (stepping/continuing still works), it just never shows where.
152 -- Append "vsplit" so there's always a guaranteed-successful last resort
153 -- that opens the source file if nothing else matched.
154 dap.defaults.fallback.switchbuf = "usevisible,useopen,uselast,vsplit"
155
156 -- Open/close dap-ui automatically around a debug session, so <leader>du
157 -- is only needed to bring it back after manually closing it mid-session.
158 dap.listeners.after.event_initialized["dapui_config"] = function()
159 dapui.open()
160 end
161 dap.listeners.before.event_terminated["dapui_config"] = function()
162 dapui.close()
163 end
164 dap.listeners.before.event_exited["dapui_config"] = function()
165 dapui.close()
166 end
167
168 -- These events arrive asynchronously over debugpy's socket (a libuv
169 -- callback, not a keypress), and nvim-dap updates its state - cursor
170 -- position, signs, dapui panels - without that alone forcing Neovim to
171 -- repaint. So hitting a breakpoint, continuing past one, or a session
172 -- ending can all silently not *look* like anything happened until the
173 -- next keystroke forces a redraw that reveals the already-current
174 -- state (e.g. <leader>dc appearing to need pressing twice to resume).
175 for _, event in ipairs({ "event_stopped", "event_continued", "event_terminated", "event_exited" }) do
176 dap.listeners.after[event]["dap_redraw"] = function()
177 vim.cmd("redraw")
178 end
179 end
180 end,
181}