1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
-- 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 <leader>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 = {
{ "<leader>db", function() require("dap").toggle_breakpoint() end, desc = "Toggle breakpoint" },
{
"<leader>dB",
function() require("dap").set_breakpoint(vim.fn.input("Breakpoint condition: ")) end,
desc = "Conditional breakpoint",
},
{
"<leader>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[<current buffer's filetype>], so calling it
-- from the task-output dock or some other non-source window finds
-- nothing. Flatten across every filetype instead - same as
-- <leader>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",
},
{ "<leader>do", function() require("dap").step_over() end, desc = "Step over" },
{ "<leader>di", function() require("dap").step_into() end, desc = "Step into" },
{ "<leader>dO", function() require("dap").step_out() end, desc = "Step out" },
{ "<leader>dt", function() require("dap").terminate() end, desc = "Terminate session" },
{ "<leader>dr", function() require("dap").repl.toggle() end, desc = "Toggle REPL" },
{ "<leader>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-<lang>.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. <leader>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 <leader>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. <leader>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,
}
|