refactor: update development tooling

- Refactor dap.lua debugger configuration
- Update neotest.lua test runner setup
- Simplify linting.lua and formatting.lua configs
- Restructure snippets.lua for better organization
This commit is contained in:
Jeremie Fraeys 2026-03-23 20:33:16 -04:00
parent bfb1a63776
commit 99a6cba26a
No known key found for this signature in database
6 changed files with 285 additions and 218 deletions

View file

@ -1,31 +1,18 @@
return {
-- Define language plugins at top level to prevent luarocks installation
{
'mfussenegger/nvim-dap-python',
ft = 'python',
rocks = { enabled = false },
},
{
'leoluz/nvim-dap-go',
ft = 'go',
rocks = { enabled = false },
},
{
'mfussenegger/nvim-dap',
dependencies = {
'rcarriga/nvim-dap-ui',
'nvim-neotest/nvim-nio',
{ 'thehamsta/nvim-dap-virtual-text', opts = {} },
'mfussenegger/nvim-dap-python',
'leoluz/nvim-dap-go',
{ 'theHamsta/nvim-dap-virtual-text', opts = {} },
'jay-babu/mason-nvim-dap.nvim',
},
config = function()
local dap = require('dap')
local dapui = require('dapui')
---@diagnostic disable-next-line: missing-fields
dapui.setup({
-- Prevent entering insert mode when UI opens
enter = false,
layouts = {
{
@ -49,46 +36,75 @@ return {
},
})
-- Mason handles debugger installation and configuration
require('mason-nvim-dap').setup({
automatic_installation = true,
ensure_installed = { 'codelldb', 'python', 'delve' },
handlers = {},
})
-- Python setup with proper path detection
local function get_python_path()
-- Check if we're in a virtual environment
local venv = os.getenv('VIRTUAL_ENV')
if venv then
local venv_python = venv .. '/bin/python'
if vim.fn.executable(venv_python) == 1 then
return venv_python
end
end
-- Fall back to system python3 or python
local python3 = vim.fn.exepath('python3')
if python3 ~= '' then
return python3
end
local python = vim.fn.exepath('python')
if python ~= '' then
return python
end
-- Last resort - use system default
return 'python3'
-- open/close UI automatically with session lifecycle
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
require('dap-python').setup(get_python_path())
-- defer mason disk I/O after first keypress
vim.schedule(function()
require('mason-nvim-dap').setup({
automatic_installation = true,
ensure_installed = { 'codelldb', 'python', 'delve' },
handlers = {},
})
end)
-- Go setup (helper does the heavy lifting)
require('dap-go').setup()
-- VSCode launch.json support
local ok, vscode = pcall(require, 'dap.ext.vscode')
if ok then
vscode.load_launchjs(nil, {
codelldb = { 'c', 'cpp', 'rust', 'zig' },
delve = { 'go' },
python = { 'python' },
})
end
-- C/C++ configurations
dap.configurations.c = {
-- language adapters: only setup when filetype is opened AND dap is loaded
local function get_python_path()
local venv = vim.env.VIRTUAL_ENV or vim.env.CONDA_PREFIX
if venv then
local bin = venv .. '/bin/python'
if vim.fn.executable(bin) == 1 then
return bin
end
end
local local_venv = vim.fn.getcwd() .. '/.venv/bin/python'
if vim.fn.executable(local_venv) == 1 then
return local_venv
end
return vim.fn.exepath('python3') ~= '' and vim.fn.exepath('python3')
or vim.fn.exepath('python') ~= '' and vim.fn.exepath('python')
or 'python3'
end
vim.api.nvim_create_autocmd('FileType', {
pattern = 'go',
once = true,
callback = function()
if package.loaded['dap'] then
require('dap-go').setup()
end
end,
})
vim.api.nvim_create_autocmd('FileType', {
pattern = 'python',
once = true,
callback = function()
if package.loaded['dap'] then
require('dap-python').setup(get_python_path())
end
end,
})
-- C/C++/Rust/Zig via codelldb
local codelldb_config = {
{
name = 'Launch',
type = 'codelldb',
@ -111,9 +127,9 @@ return {
env = { ASAN_OPTIONS = 'detect_leaks=1' },
},
}
dap.configurations.cpp = dap.configurations.c
-- Rust configuration
dap.configurations.c = codelldb_config
dap.configurations.cpp = vim.deepcopy(codelldb_config)
dap.configurations.rust = {
{
name = 'Launch',
@ -126,8 +142,6 @@ return {
stopOnEntry = false,
},
}
-- Zig configuration
dap.configurations.zig = {
{
name = 'Launch',
@ -141,15 +155,17 @@ return {
},
}
-- Auto-open UI when debugging starts
dap.listeners.after.event_initialized['dapui_config'] = dapui.open
-- 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 = '' })
vim.fn.sign_define('DapStopped', { text = '➡️', texthl = '', linehl = 'debugPC', numhl = '' })
-- 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('DapBreakpointRejected', { text = '', texthl = 'DiagnosticError', linehl = '', numhl = '' })
vim.fn.sign_define('DapStopped', { text = '➡️', texthl = 'DiagnosticInfo', linehl = 'debugPC', numhl = '' })
vim.api.nvim_set_hl(0, 'DapStoppedLine', { default = true, link = 'Visual' })
end,
keys = {
{
'<leader>dc',
@ -158,6 +174,26 @@ return {
end,
desc = 'Debug: Continue',
},
{
'<leader>da',
function()
require('dap').continue({
before = function(c)
local args = vim.split(vim.fn.input('Args: '), ' ')
c.args = args
return c
end,
})
end,
desc = 'Debug: Run with Args',
},
{
'<leader>dC',
function()
require('dap').run_to_cursor()
end,
desc = 'Debug: Run to Cursor',
},
{
'<leader>ds',
function()
@ -179,6 +215,27 @@ return {
end,
desc = 'Debug: Step Out',
},
{
'<leader>dj',
function()
require('dap').down()
end,
desc = 'Debug: Down Frame',
},
{
'<leader>dk',
function()
require('dap').up()
end,
desc = 'Debug: Up Frame',
},
{
'<leader>dg',
function()
require('dap').goto_()
end,
desc = 'Debug: Go to Line',
},
{
'<leader>db',
function()
@ -189,10 +246,17 @@ return {
{
'<leader>dB',
function()
require('dap').set_breakpoint(vim.fn.input('Breakpoint condition: '))
require('dap').set_breakpoint(vim.fn.input('Condition: '))
end,
desc = 'Debug: Conditional Breakpoint',
},
{
'<leader>dl',
function()
require('dap').set_breakpoint(nil, nil, vim.fn.input('Log: '))
end,
desc = 'Debug: Log Breakpoint',
},
{
'<leader>dr',
function()
@ -201,12 +265,19 @@ return {
desc = 'Debug: Toggle REPL',
},
{
'<leader>dl',
'<leader>dR',
function()
require('dap').run_last()
end,
desc = 'Debug: Run Last',
},
{
'<leader>dt',
function()
require('dap').terminate()
end,
desc = 'Debug: Terminate',
},
{
'<leader>du',
function()
@ -215,13 +286,15 @@ return {
desc = 'Debug: Toggle UI',
},
{
'<leader>dt',
'<leader>de',
function()
require('dap').terminate()
require('dapui').eval()
end,
desc = 'Debug: Terminate',
desc = 'Debug: Eval',
mode = { 'n', 'v' },
},
},
ft = { 'python', 'go', 'rust', 'c', 'cpp', 'zig' },
},
{ 'mfussenegger/nvim-dap-python', lazy = true, config = false },
{ 'leoluz/nvim-dap-go', lazy = true, config = false },
}

View file

@ -1,10 +1,6 @@
return {
'stevearc/conform.nvim',
dependencies = {
'nvim-lua/plenary.nvim',
'WhoIsSethDaniel/mason-tool-installer.nvim',
},
event = { 'BufReadPre', 'BufNewFile' },
event = { 'BufReadPost', 'BufNewFile' },
opts = {
formatters = {
juliaformatter = {
@ -14,75 +10,31 @@ return {
'--startup-file=no',
'--history-file=no',
'-e',
[[
using JuliaFormatter
print(format_text(read(stdin, String)))
]],
'using JuliaFormatter; print(format_text(read(stdin, String)))',
},
stdin = true,
},
},
formatters_by_ft = {
python = {
'ruff_fix',
'ruff_format',
'ruff_organize_imports',
},
python = { 'ruff_fix', 'ruff_format', 'ruff_organize_imports' },
yaml = { 'yamlfmt' },
sh = { 'shfmt' },
sql = { 'sqlfluff' },
lua = { 'stylua' },
json = { 'jq' },
julia = { 'juliaformatter' }, -- keep JuliaFormatter here
markdown = { 'prettierd', 'prettier' },
html = { 'prettierd' },
css = { 'prettierd' },
javascript = { 'prettierd' },
typescript = { 'prettierd' },
julia = { 'juliaformatter' },
markdown = { 'prettierd' },
},
-- Fallback setup
format_on_save = function(bufnr)
-- julia formatter is slow, skip on save
if vim.bo[bufnr].filetype == 'julia' then
return
end
return {
timeout_ms = 2000,
lsp_fallback = true,
}
return { timeout_ms = 2000, lsp_fallback = true }
end,
notify_on_error = true,
async = true,
},
config = function(_, opts)
-- Setup Conform
require('conform').setup(opts)
-- Auto-install formatters via Mason
local all_formatters = {}
for _, formatters in pairs(opts.formatters_by_ft) do
for _, f in ipairs(formatters) do
all_formatters[f] = true
end
end
local exclude = {
ruff_fix = true,
ruff_format = true,
ruff_organize_imports = true,
juliaformatter = true, -- exclude JuliaFormatter from Mason (install via Julia)
}
local tools = vim.tbl_filter(function(tool)
return not exclude[tool]
end, vim.tbl_keys(all_formatters))
require('mason-tool-installer').setup({
ensure_installed = tools,
auto_update = false,
run_on_start = true,
})
end,
}

View file

@ -1,7 +1,6 @@
return {
'mfussenegger/nvim-lint',
dependencies = { 'WhoIsSethDaniel/mason-tool-installer.nvim' },
event = { 'BufReadPre', 'BufNewFile' },
event = { 'BufReadPost', 'BufNewFile' },
opts = {
linters_by_ft = {
python = { 'ruff', 'mypy' },
@ -12,62 +11,34 @@ return {
lua = { 'luacheck' },
sql = { 'sqlfluff' },
},
mason_to_lint = {
golangcilint = { 'golangci-lint' },
},
},
config = function(_, opts)
local lint = require('lint')
lint.linters_by_ft = opts.linters_by_ft
-- Flatten linters and map to Mason package names
local all_linters = {}
for _, ft_linters in pairs(opts.linters_by_ft) do
for _, l in ipairs(ft_linters) do
local tool = opts.mason_to_lint[l] or { l }
-- Handle both table and string values
if type(tool) == 'table' then
for _, t in ipairs(tool) do
if t ~= '' then
table.insert(all_linters, t)
end
end
elseif tool ~= '' then
table.insert(all_linters, tool)
end
end
end
-- Deduplicate tools
local tools_set = {}
for _, t in ipairs(all_linters) do
tools_set[t] = true
end
local tools = vim.tbl_keys(tools_set)
print('Ensuring installation of linters: ' .. table.concat(tools, ', '))
-- Setup mason-tool-installer safely
require('mason-tool-installer').setup({
ensure_installed = tools,
auto_update = false,
run_on_start = true,
start_delay = 3000,
})
-- Autocommand group for linting
local timer = vim.uv.new_timer()
local augroup = vim.api.nvim_create_augroup('LintAutogroup', { clear = true })
vim.api.nvim_create_autocmd({ 'BufEnter', 'BufWritePost', 'InsertLeave' }, {
group = augroup,
callback = function()
local ft = vim.bo.filetype
local available = lint.linters_by_ft[ft]
if available and #available > 0 then
local ok, err = pcall(lint.try_lint)
if not ok then
vim.notify('[nvim-lint] Error running linter: ' .. err, vim.log.levels.ERROR)
end
if not available or #available == 0 then
return
end
timer:stop()
timer:start(
250,
0,
vim.schedule_wrap(function()
local ok, err = pcall(lint.try_lint)
if not ok then
vim.notify('[nvim-lint] ' .. err, vim.log.levels.ERROR)
end
end)
)
end,
})
end,

24
lua/custom/plugins/neotest.lua Normal file → Executable file
View file

@ -8,9 +8,21 @@ return {
'nvim-neotest/neotest-python',
'alfaix/neotest-gtest',
'andythigpen/nvim-coverage',
'mfussenegger/nvim-dap', -- Required for debug strategy
},
event = 'VeryLazy',
keys = {
{ '<leader>tn', desc = 'Test: Run nearest' },
{ '<leader>tf', desc = 'Test: Run file' },
{ '<leader>ta', desc = 'Test: Run all' },
{ '<leader>td', desc = 'Test: Debug nearest' },
{ '<leader>tx', desc = 'Test: Stop' },
{ '<leader>ts', desc = 'Test: Toggle summary' },
{ '<leader>to', desc = 'Test: Show output' },
{ '<leader>tw', desc = 'Test: Watch file' },
{ '<leader>tC', desc = 'Test: Toggle coverage' },
{ '<leader>tL', desc = 'Test: Load coverage' },
{ ']t', desc = 'Test: Next failed' },
{ '[t', desc = 'Test: Prev failed' },
},
config = function()
local neotest = require('neotest')
@ -70,12 +82,7 @@ return {
-- 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
vim.notify(msg, level or vim.log.levels.INFO)
end
vim.keymap.set('n', '<leader>tn', function()
@ -118,6 +125,7 @@ return {
end, { desc = 'Test: Run all (cwd)' })
vim.keymap.set('n', '<leader>td', function()
require('dap')
local file = vim.fn.expand('%:p')
local is_test = file:match('test_.*%.py$') or file:match('.*_test%.py$') or file:match('tests/.*%.py$')

View file

@ -1,12 +1,60 @@
return {
{
'ThePrimeagen/refactoring.nvim',
event = 'VeryLazy',
dependencies = {
'nvim-lua/plenary.nvim',
'nvim-treesitter/nvim-treesitter',
},
lazy = true,
keys = {
{
'<leader>re',
function()
require('refactoring').refactor('Extract Function')
end,
mode = 'x',
desc = 'Refactor: Extract Function',
},
{
'<leader>rf',
function()
require('refactoring').refactor('Extract Function To File')
end,
mode = 'x',
desc = 'Refactor: Extract To File',
},
{
'<leader>rv',
function()
require('refactoring').refactor('Extract Variable')
end,
mode = 'x',
desc = 'Refactor: Extract Variable',
},
{
'<leader>ri',
function()
require('refactoring').refactor('Inline Variable')
end,
mode = { 'n', 'x' },
desc = 'Refactor: Inline Variable',
},
{
'<leader>rb',
function()
require('refactoring').refactor('Extract Block')
end,
desc = 'Refactor: Extract Block',
},
{
'<leader>rr',
function()
-- require('refactoring').select_refactor()
require('telescope').extensions.refactoring.refactors()
end,
mode = { 'n', 'x' },
desc = 'Refactor: Select',
},
},
opts = {},
},
}

View file

@ -1,41 +1,56 @@
return {
{
'L3MON4D3/LuaSnip',
version = 'v2.*',
build = 'make install_jsregexp',
event = 'InsertEnter',
dependencies = { 'rafamadriz/friendly-snippets' },
config = function()
local luasnip = require('luasnip')
-- Extend filetypes with specific snippets
luasnip.filetype_extend('javascript', { 'jsdoc' })
luasnip.filetype_extend('python', { 'google' })
-- Key mappings for LuaSnip
vim.keymap.set({ 'i' }, '<C-s>e', function()
if luasnip.expand_or_jumpable() then
luasnip.expand()
end
end, { silent = true, desc = 'Expand snippet' })
vim.keymap.set({ 'i', 's' }, '<C-s>;', function()
if luasnip.jumpable(1) then
luasnip.jump(1)
end
end, { silent = true, desc = 'Jump forward in snippet' })
vim.keymap.set({ 'i', 's' }, '<C-s>,', function()
if luasnip.jumpable(-1) then
luasnip.jump(-1)
end
end, { silent = true, desc = 'Jump backward in snippet' })
vim.keymap.set({ 'i', 's' }, '<C-s>', function()
if luasnip.choice_active() then
luasnip.change_choice(1)
end
end, { silent = true, desc = 'Cycle through snippet choices' })
end,
'L3MON4D3/LuaSnip',
version = 'v2.*',
build = 'make install_jsregexp',
event = 'InsertEnter',
dependencies = {
'saadparwaiz1/cmp_luasnip',
'rafamadriz/friendly-snippets',
},
config = function()
local luasnip = require('luasnip')
local loader = require('luasnip.loaders.from_vscode')
-- load snippets on demand per filetype rather than all at once
loader.lazy_load()
vim.api.nvim_create_autocmd('FileType', {
callback = function(args)
local ft = vim.bo[args.buf].filetype
if ft and ft ~= '' then
loader.lazy_load({ include = { ft } })
end
end,
})
luasnip.config.setup({
history = true,
updateevents = 'TextChanged,TextChangedI',
})
luasnip.filetype_extend('python', { 'google' })
vim.keymap.set('i', '<C-l>e', function()
if luasnip.expand_or_jumpable() then
luasnip.expand()
end
end, { silent = true, desc = 'Snippet: expand' })
vim.keymap.set({ 'i', 's' }, '<C-l>;', function()
if luasnip.jumpable(1) then
luasnip.jump(1)
end
end, { silent = true, desc = 'Snippet: jump forward' })
vim.keymap.set({ 'i', 's' }, '<C-l>,', function()
if luasnip.jumpable(-1) then
luasnip.jump(-1)
end
end, { silent = true, desc = 'Snippet: jump backward' })
vim.keymap.set({ 'i', 's' }, '<C-l>c', function()
if luasnip.choice_active() then
luasnip.change_choice(1)
end
end, { silent = true, desc = 'Snippet: cycle choice' })
end,
}