feat(nvim): add testing and debugging support

Add neotest for running tests:
- Jest adapter for Angular/JS/TS
- Python adapter with pytest support
- Rust adapter via rustaceanvim
- Keymaps: <leader>t* for test operations

Add nvim-dap for debugging:
- Python with debugpy (auto-detects venv)
- JavaScript/TypeScript with js-debug-adapter
- Chrome debugging for Angular (port 4200)
- Rust via codelldb (rustaceanvim integration)
- DAP UI with auto-open on debug start
- Keymaps: <leader>d* for debug operations

https://claude.ai/code/session_01UcDoC4n6CWMttYFJPw17ie
This commit is contained in:
Claude 2026-01-31 12:26:03 +00:00
parent f25236fe40
commit a1eb8dc2ba
No known key found for this signature in database
3 changed files with 432 additions and 0 deletions

View file

@ -0,0 +1,307 @@
return {
'mfussenegger/nvim-dap',
dependencies = {
-- UI
{
'rcarriga/nvim-dap-ui',
dependencies = { 'nvim-neotest/nvim-nio' },
keys = {
{
'<leader>du',
function()
require('dapui').toggle {}
end,
desc = 'Toggle DAP UI',
},
{
'<leader>de',
function()
require('dapui').eval()
end,
mode = { 'n', 'v' },
desc = 'Eval expression',
},
},
opts = {},
config = function(_, opts)
local dap = require 'dap'
local dapui = require 'dapui'
dapui.setup(opts)
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
end,
},
-- Virtual text for variable values
{
'theHamsta/nvim-dap-virtual-text',
opts = {},
},
-- Mason integration for installing debug adapters
{
'jay-babu/mason-nvim-dap.nvim',
dependencies = 'mason.nvim',
cmd = { 'DapInstall', 'DapUninstall' },
opts = {
automatic_installation = true,
handlers = {},
ensure_installed = {
'python',
'js',
'codelldb',
},
},
},
},
keys = {
{
'<leader>dB',
function()
require('dap').set_breakpoint(vim.fn.input 'Breakpoint condition: ')
end,
desc = 'Conditional breakpoint',
},
{
'<leader>db',
function()
require('dap').toggle_breakpoint()
end,
desc = 'Toggle breakpoint',
},
{
'<leader>dc',
function()
require('dap').continue()
end,
desc = 'Continue',
},
{
'<leader>dC',
function()
require('dap').run_to_cursor()
end,
desc = 'Run to cursor',
},
{
'<leader>dg',
function()
require('dap').goto_()
end,
desc = 'Go to line (no execute)',
},
{
'<leader>di',
function()
require('dap').step_into()
end,
desc = 'Step into',
},
{
'<leader>dj',
function()
require('dap').down()
end,
desc = 'Down in stack',
},
{
'<leader>dk',
function()
require('dap').up()
end,
desc = 'Up in stack',
},
{
'<leader>dl',
function()
require('dap').run_last()
end,
desc = 'Run last',
},
{
'<leader>do',
function()
require('dap').step_out()
end,
desc = 'Step out',
},
{
'<leader>dO',
function()
require('dap').step_over()
end,
desc = 'Step over',
},
{
'<leader>dp',
function()
require('dap').pause()
end,
desc = 'Pause',
},
{
'<leader>dr',
function()
require('dap').repl.toggle()
end,
desc = 'Toggle REPL',
},
{
'<leader>ds',
function()
require('dap').session()
end,
desc = 'Session',
},
{
'<leader>dt',
function()
require('dap').terminate()
end,
desc = 'Terminate',
},
{
'<leader>dw',
function()
require('dap.ui.widgets').hover()
end,
desc = 'Widgets',
},
},
config = function()
local dap = require 'dap'
-- Signs
vim.fn.sign_define('DapBreakpoint', { text = '', texthl = 'DiagnosticError', linehl = '', numhl = '' })
vim.fn.sign_define('DapBreakpointCondition', { text = '', texthl = 'DiagnosticWarn', linehl = '', numhl = '' })
vim.fn.sign_define('DapLogPoint', { text = '', texthl = 'DiagnosticInfo', linehl = '', numhl = '' })
vim.fn.sign_define('DapStopped', { text = '', texthl = 'DiagnosticOk', linehl = 'DapStoppedLine', numhl = '' })
vim.fn.sign_define('DapBreakpointRejected', { text = '', texthl = 'DiagnosticError', linehl = '', numhl = '' })
-- Highlight for stopped line
vim.api.nvim_set_hl(0, 'DapStoppedLine', { default = true, link = 'Visual' })
-- Python configuration
dap.adapters.python = function(cb, config)
if config.request == 'attach' then
local port = (config.connect or config).port
local host = (config.connect or config).host or '127.0.0.1'
cb {
type = 'server',
port = assert(port, '`connect.port` is required for a python `attach` configuration'),
host = host,
options = {
source_filetype = 'python',
},
}
else
cb {
type = 'executable',
command = vim.fn.exepath 'python3',
args = { '-m', 'debugpy.adapter' },
options = {
source_filetype = 'python',
},
}
end
end
dap.configurations.python = {
{
type = 'python',
request = 'launch',
name = 'Launch file',
program = '${file}',
pythonPath = function()
local cwd = vim.fn.getcwd()
if vim.fn.executable(cwd .. '/venv/bin/python') == 1 then
return cwd .. '/venv/bin/python'
elseif vim.fn.executable(cwd .. '/.venv/bin/python') == 1 then
return cwd .. '/.venv/bin/python'
else
return vim.fn.exepath 'python3'
end
end,
},
{
type = 'python',
request = 'launch',
name = 'Launch file with arguments',
program = '${file}',
args = function()
local args_string = vim.fn.input 'Arguments: '
return vim.split(args_string, ' +')
end,
pythonPath = function()
local cwd = vim.fn.getcwd()
if vim.fn.executable(cwd .. '/venv/bin/python') == 1 then
return cwd .. '/venv/bin/python'
elseif vim.fn.executable(cwd .. '/.venv/bin/python') == 1 then
return cwd .. '/.venv/bin/python'
else
return vim.fn.exepath 'python3'
end
end,
},
}
-- JavaScript/TypeScript configuration
local js_debug_path = vim.fn.stdpath 'data' .. '/mason/packages/js-debug-adapter'
dap.adapters['pwa-node'] = {
type = 'server',
host = 'localhost',
port = '${port}',
executable = {
command = 'node',
args = { js_debug_path .. '/js-debug/src/dapDebugServer.js', '${port}' },
},
}
dap.adapters['pwa-chrome'] = {
type = 'server',
host = 'localhost',
port = '${port}',
executable = {
command = 'node',
args = { js_debug_path .. '/js-debug/src/dapDebugServer.js', '${port}' },
},
}
local js_config = {
{
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}',
},
{
type = 'pwa-chrome',
request = 'launch',
name = 'Launch Chrome',
url = 'http://localhost:4200', -- Angular default port
webRoot = '${workspaceFolder}',
},
}
dap.configurations.javascript = js_config
dap.configurations.typescript = js_config
dap.configurations.javascriptreact = js_config
dap.configurations.typescriptreact = js_config
-- Rust/C/C++ uses codelldb (configured via rustaceanvim)
end,
}

View file

@ -57,10 +57,12 @@ return {
'ast_grep',
'cbfmt',
'csharpier',
'debugpy',
'editorconfig-checker',
'eslint_d',
'goimports',
'hadolint',
'js-debug-adapter',
'luacheck',
'prettier',
'prettierd',

View file

@ -0,0 +1,123 @@
return {
'nvim-neotest/neotest',
dependencies = {
'nvim-neotest/nvim-nio',
'nvim-lua/plenary.nvim',
'antoinemadec/FixCursorHold.nvim',
'nvim-treesitter/nvim-treesitter',
-- Adapters
'nvim-neotest/neotest-jest',
'nvim-neotest/neotest-python',
-- Rust uses rustaceanvim's built-in neotest adapter
},
keys = {
{
'<leader>tt',
function()
require('neotest').run.run()
end,
desc = 'Run nearest test',
},
{
'<leader>tf',
function()
require('neotest').run.run(vim.fn.expand '%')
end,
desc = 'Run current file tests',
},
{
'<leader>ta',
function()
require('neotest').run.run(vim.uv.cwd())
end,
desc = 'Run all tests',
},
{
'<leader>ts',
function()
require('neotest').summary.toggle()
end,
desc = 'Toggle test summary',
},
{
'<leader>to',
function()
require('neotest').output.open { enter = true, auto_close = true }
end,
desc = 'Show test output',
},
{
'<leader>tO',
function()
require('neotest').output_panel.toggle()
end,
desc = 'Toggle output panel',
},
{
'<leader>tS',
function()
require('neotest').run.stop()
end,
desc = 'Stop test run',
},
{
'<leader>tw',
function()
require('neotest').watch.toggle(vim.fn.expand '%')
end,
desc = 'Toggle test watch',
},
{
'<leader>td',
function()
require('neotest').run.run { strategy = 'dap' }
end,
desc = 'Debug nearest test',
},
{
'[t',
function()
require('neotest').jump.prev { status = 'failed' }
end,
desc = 'Previous failed test',
},
{
']t',
function()
require('neotest').jump.next { status = 'failed' }
end,
desc = 'Next failed test',
},
},
config = function()
require('neotest').setup {
adapters = {
require 'neotest-jest' {
jestCommand = 'npm test --',
jestConfigFile = 'jest.config.js',
env = { CI = true },
cwd = function()
return vim.fn.getcwd()
end,
},
require 'neotest-python' {
dap = { justMyCode = false },
args = { '--log-level', 'DEBUG' },
runner = 'pytest',
},
require 'rustaceanvim.neotest',
},
status = {
virtual_text = true,
},
output = {
open_on_run = true,
},
quickfix = {
open = function()
vim.cmd 'copen'
end,
},
}
end,
}