feat(neotest): add notifications, smart test discovery, fix DAP integration, and improve keymaps
- Add notify function for user feedback on all test operations - Make <leader>tn smart: runs nearest in test files, finds matching test file from source - Add same smart logic to <leader>td (debug) and <leader>tw (watch) - Fix <leader>ts to auto-focus summary window for Enter navigation - Add summary keymaps for <CR> to expand/jump to tests - Add nvim-dap as dependency to fix debug strategy - Fix DAP UI to not auto-close and not enter insert mode - Fix watch to load test file buffer for LSP attachment - Make python path detection use VIRTUAL_ENV env var first - Fix neotest-python adapter configuration
This commit is contained in:
parent
a8c06ae101
commit
375584629e
2 changed files with 188 additions and 21 deletions
|
|
@ -24,7 +24,30 @@ return {
|
|||
local dap = require('dap')
|
||||
local dapui = require('dapui')
|
||||
|
||||
dapui.setup()
|
||||
dapui.setup({
|
||||
-- Prevent entering insert mode when UI opens
|
||||
enter = false,
|
||||
layouts = {
|
||||
{
|
||||
elements = {
|
||||
{ id = 'scopes', size = 0.25 },
|
||||
{ id = 'breakpoints', size = 0.25 },
|
||||
{ id = 'stacks', size = 0.25 },
|
||||
{ id = 'watches', size = 0.25 },
|
||||
},
|
||||
size = 40,
|
||||
position = 'left',
|
||||
},
|
||||
{
|
||||
elements = {
|
||||
{ id = 'repl', size = 0.5 },
|
||||
{ id = 'console', size = 0.5 },
|
||||
},
|
||||
size = 10,
|
||||
position = 'bottom',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
-- Mason handles debugger installation and configuration
|
||||
require('mason-nvim-dap').setup({
|
||||
|
|
@ -118,10 +141,10 @@ return {
|
|||
},
|
||||
}
|
||||
|
||||
-- Auto-open/close UI
|
||||
-- Auto-open UI when debugging starts
|
||||
dap.listeners.after.event_initialized['dapui_config'] = dapui.open
|
||||
dap.listeners.before.event_terminated['dapui_config'] = dapui.close
|
||||
dap.listeners.before.event_exited['dapui_config'] = dapui.close
|
||||
-- Don't auto-close UI - let user close it manually with <leader>du
|
||||
-- This prevents UI disappearing when running multiple debug sessions
|
||||
|
||||
-- Breakpoint signs
|
||||
vim.fn.sign_define('DapBreakpoint', { text = '🔴', texthl = '', linehl = '', numhl = '' })
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ return {
|
|||
'nvim-neotest/neotest-python',
|
||||
'alfaix/neotest-gtest',
|
||||
'andythigpen/nvim-coverage',
|
||||
'mfussenegger/nvim-dap', -- Required for debug strategy
|
||||
},
|
||||
event = 'VeryLazy',
|
||||
config = function()
|
||||
|
|
@ -17,17 +18,47 @@ return {
|
|||
adapters = {
|
||||
require('neotest-python')({
|
||||
runner = 'pytest',
|
||||
dap = { justMyCode = false },
|
||||
args = { '-v', '--tb=short' },
|
||||
-- Discover tests in tests/ directory
|
||||
pytest_discover_instances = true,
|
||||
}),
|
||||
require('neotest-gtest').setup({
|
||||
debug_adapter = 'codelldb',
|
||||
python = function()
|
||||
-- Check if VIRTUAL_ENV is set (activated venv)
|
||||
local venv = os.getenv('VIRTUAL_ENV')
|
||||
if venv then
|
||||
return venv .. '/bin/python'
|
||||
end
|
||||
-- Try common venv names
|
||||
local venv_paths = {
|
||||
'.venv/bin/python',
|
||||
'venv/bin/python',
|
||||
'.env/bin/python',
|
||||
'env/bin/python',
|
||||
}
|
||||
for _, path in ipairs(venv_paths) do
|
||||
if vim.fn.filereadable(path) == 1 then
|
||||
return path
|
||||
end
|
||||
end
|
||||
-- Fallback to system python
|
||||
return vim.fn.exepath('python3') or vim.fn.exepath('python') or 'python'
|
||||
end,
|
||||
}),
|
||||
require('neotest-gtest'),
|
||||
},
|
||||
output = { open_on_run = false },
|
||||
summary = { open = 'botright vsplit | vertical resize 60' },
|
||||
summary = {
|
||||
open = 'botright vsplit | vertical resize 60',
|
||||
mappings = {
|
||||
expand = { '<CR>', 'l' },
|
||||
expand_all = 'E',
|
||||
output = 'o',
|
||||
short = 'O',
|
||||
run = 'r',
|
||||
debug = 'd',
|
||||
mark = 'm',
|
||||
run_marked = 'R',
|
||||
clear_marked = 'M',
|
||||
target = 't',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
local ok_cov, coverage = pcall(require, 'coverage')
|
||||
|
|
@ -38,44 +69,157 @@ return {
|
|||
end
|
||||
|
||||
-- Keymaps (defined after setup to ensure modules are available)
|
||||
local function notify(msg, level)
|
||||
local ok, notify_mod = pcall(require, 'notify')
|
||||
if ok then
|
||||
notify_mod(msg, level or vim.log.levels.INFO)
|
||||
else
|
||||
vim.notify(msg, level or vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
vim.keymap.set('n', '<leader>tn', function()
|
||||
neotest.run.run()
|
||||
end, { desc = 'Test: Run nearest' })
|
||||
local file = vim.fn.expand('%:p')
|
||||
local is_test = file:match('test_.*%.py$') or file:match('.*_test%.py$') or file:match('tests/.*%.py$')
|
||||
|
||||
if is_test then
|
||||
neotest.run.run()
|
||||
notify('Running nearest test')
|
||||
else
|
||||
-- Try to find corresponding test file
|
||||
local basename = vim.fn.expand('%:t:r')
|
||||
local test_patterns = {
|
||||
'tests/test_' .. basename .. '.py',
|
||||
'tests/' .. basename .. '_test.py',
|
||||
'test_' .. basename .. '.py',
|
||||
basename .. '_test.py',
|
||||
}
|
||||
|
||||
for _, pattern in ipairs(test_patterns) do
|
||||
if vim.fn.filereadable(pattern) == 1 then
|
||||
neotest.run.run(pattern)
|
||||
notify('Running test file: ' .. pattern)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
notify('No test file found for ' .. basename, vim.log.levels.WARN)
|
||||
end
|
||||
end, { desc = 'Test: Run nearest or matching test file' })
|
||||
|
||||
vim.keymap.set('n', '<leader>tf', function()
|
||||
neotest.run.run(vim.fn.expand('%'))
|
||||
notify('Running file tests: ' .. vim.fn.expand('%:t'), vim.log.levels.INFO)
|
||||
end, { desc = 'Test: Run file' })
|
||||
|
||||
vim.keymap.set('n', '<leader>ta', function()
|
||||
neotest.run.run(vim.fn.getcwd())
|
||||
notify('Running all tests in ' .. vim.fn.getcwd(), vim.log.levels.INFO)
|
||||
end, { desc = 'Test: Run all (cwd)' })
|
||||
|
||||
vim.keymap.set('n', '<leader>td', function()
|
||||
neotest.run.run({ strategy = 'dap' })
|
||||
end, { desc = 'Test: Debug nearest' })
|
||||
local file = vim.fn.expand('%:p')
|
||||
local is_test = file:match('test_.*%.py$') or file:match('.*_test%.py$') or file:match('tests/.*%.py$')
|
||||
|
||||
if is_test then
|
||||
neotest.run.run({ strategy = 'dap' })
|
||||
notify('Debugging nearest test')
|
||||
else
|
||||
-- Try to find corresponding test file
|
||||
local basename = vim.fn.expand('%:t:r')
|
||||
local test_patterns = {
|
||||
'tests/test_' .. basename .. '.py',
|
||||
'tests/' .. basename .. '_test.py',
|
||||
'test_' .. basename .. '.py',
|
||||
basename .. '_test.py',
|
||||
}
|
||||
|
||||
for _, pattern in ipairs(test_patterns) do
|
||||
if vim.fn.filereadable(pattern) == 1 then
|
||||
neotest.run.run({ pattern, strategy = 'dap' })
|
||||
notify('Debugging test file: ' .. pattern)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
notify('No test file found for ' .. basename, vim.log.levels.WARN)
|
||||
end
|
||||
end, { desc = 'Test: Debug nearest or matching test file' })
|
||||
|
||||
vim.keymap.set('n', '<leader>tx', function()
|
||||
neotest.run.stop()
|
||||
notify('Stopped test run', vim.log.levels.WARN)
|
||||
end, { desc = 'Test: Stop' })
|
||||
|
||||
vim.keymap.set('n', '<leader>ts', function()
|
||||
neotest.summary.toggle()
|
||||
end, { desc = 'Test: Toggle summary' })
|
||||
-- Focus the summary window so Enter/click works
|
||||
vim.defer_fn(function()
|
||||
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
if name:match('Neotest Summary') then
|
||||
vim.api.nvim_set_current_win(win)
|
||||
break
|
||||
end
|
||||
end
|
||||
end, 50)
|
||||
end, { desc = 'Test: Toggle and focus summary' })
|
||||
|
||||
vim.keymap.set('n', '<leader>to', function()
|
||||
neotest.output.open({ enter = true, auto_close = false })
|
||||
neotest.output.open({ auto_close = false })
|
||||
end, { desc = 'Test: Show output' })
|
||||
|
||||
vim.keymap.set('n', '<leader>tw', function()
|
||||
neotest.watch.toggle(vim.fn.expand('%'))
|
||||
end, { desc = 'Test: Watch file' })
|
||||
local file = vim.fn.expand('%')
|
||||
local is_test = file:match('test_.*%.py$') or file:match('.*_test%.py$') or file:match('tests/.*%.py$')
|
||||
|
||||
local target_file
|
||||
if is_test then
|
||||
target_file = file
|
||||
else
|
||||
-- Find the matching test file
|
||||
local basename = vim.fn.expand('%:t:r')
|
||||
local test_patterns = {
|
||||
'tests/test_' .. basename .. '.py',
|
||||
'tests/' .. basename .. '_test.py',
|
||||
'test_' .. basename .. '.py',
|
||||
basename .. '_test.py',
|
||||
}
|
||||
for _, pattern in ipairs(test_patterns) do
|
||||
if vim.fn.filereadable(pattern) == 1 then
|
||||
target_file = pattern
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not target_file then
|
||||
notify('No test file found', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Open the test file in background so LSP attaches, then start watching
|
||||
local buf = vim.fn.bufadd(target_file)
|
||||
vim.fn.bufload(buf)
|
||||
vim.defer_fn(function()
|
||||
neotest.watch.toggle(target_file)
|
||||
notify('Watching: ' .. target_file)
|
||||
end, 100)
|
||||
end, { desc = 'Test: Watch file or matching test' })
|
||||
|
||||
vim.keymap.set('n', ']t', function()
|
||||
neotest.jump.next({ status = 'failed' })
|
||||
local ok, _ = pcall(neotest.jump.next, { status = 'failed' })
|
||||
if not ok then
|
||||
notify('No tests found - run <leader>ta to discover tests', vim.log.levels.WARN)
|
||||
end
|
||||
end, { desc = 'Test: Next failed' })
|
||||
|
||||
vim.keymap.set('n', '[t', function()
|
||||
neotest.jump.prev({ status = 'failed' })
|
||||
local ok, _ = pcall(neotest.jump.prev, { status = 'failed' })
|
||||
if not ok then
|
||||
notify('No tests found - run <leader>ta to discover tests', vim.log.levels.WARN)
|
||||
end
|
||||
end, { desc = 'Test: Prev failed' })
|
||||
|
||||
vim.keymap.set('n', '<leader>tC', function()
|
||||
|
|
|
|||
Loading…
Reference in a new issue