diff --git a/lua/custom/plugins/dap.lua b/lua/custom/plugins/dap.lua index cf41bbe..3a992ef 100755 --- a/lua/custom/plugins/dap.lua +++ b/lua/custom/plugins/dap.lua @@ -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 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 = { { 'dc', @@ -158,6 +174,26 @@ return { end, desc = 'Debug: Continue', }, + { + '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', + }, + { + 'dC', + function() + require('dap').run_to_cursor() + end, + desc = 'Debug: Run to Cursor', + }, { 'ds', function() @@ -179,6 +215,27 @@ return { end, desc = 'Debug: Step Out', }, + { + 'dj', + function() + require('dap').down() + end, + desc = 'Debug: Down Frame', + }, + { + 'dk', + function() + require('dap').up() + end, + desc = 'Debug: Up Frame', + }, + { + 'dg', + function() + require('dap').goto_() + end, + desc = 'Debug: Go to Line', + }, { 'db', function() @@ -189,10 +246,17 @@ return { { 'dB', function() - require('dap').set_breakpoint(vim.fn.input('Breakpoint condition: ')) + require('dap').set_breakpoint(vim.fn.input('Condition: ')) end, desc = 'Debug: Conditional Breakpoint', }, + { + 'dl', + function() + require('dap').set_breakpoint(nil, nil, vim.fn.input('Log: ')) + end, + desc = 'Debug: Log Breakpoint', + }, { 'dr', function() @@ -201,12 +265,19 @@ return { desc = 'Debug: Toggle REPL', }, { - 'dl', + 'dR', function() require('dap').run_last() end, desc = 'Debug: Run Last', }, + { + 'dt', + function() + require('dap').terminate() + end, + desc = 'Debug: Terminate', + }, { 'du', function() @@ -215,13 +286,15 @@ return { desc = 'Debug: Toggle UI', }, { - 'dt', + '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 }, } diff --git a/lua/custom/plugins/formatting.lua b/lua/custom/plugins/formatting.lua index bd78726..2d55ced 100755 --- a/lua/custom/plugins/formatting.lua +++ b/lua/custom/plugins/formatting.lua @@ -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, } diff --git a/lua/custom/plugins/linting.lua b/lua/custom/plugins/linting.lua index ae6f908..dcc6b07 100755 --- a/lua/custom/plugins/linting.lua +++ b/lua/custom/plugins/linting.lua @@ -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, diff --git a/lua/custom/plugins/neotest.lua b/lua/custom/plugins/neotest.lua old mode 100644 new mode 100755 index ce38762..79792d7 --- a/lua/custom/plugins/neotest.lua +++ b/lua/custom/plugins/neotest.lua @@ -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 = { + { 'tn', desc = 'Test: Run nearest' }, + { 'tf', desc = 'Test: Run file' }, + { 'ta', desc = 'Test: Run all' }, + { 'td', desc = 'Test: Debug nearest' }, + { 'tx', desc = 'Test: Stop' }, + { 'ts', desc = 'Test: Toggle summary' }, + { 'to', desc = 'Test: Show output' }, + { 'tw', desc = 'Test: Watch file' }, + { 'tC', desc = 'Test: Toggle coverage' }, + { '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', 'tn', function() @@ -118,6 +125,7 @@ return { end, { desc = 'Test: Run all (cwd)' }) vim.keymap.set('n', '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$') diff --git a/lua/custom/plugins/refactoring.lua b/lua/custom/plugins/refactoring.lua index 904feac..60d4ae5 100755 --- a/lua/custom/plugins/refactoring.lua +++ b/lua/custom/plugins/refactoring.lua @@ -1,12 +1,60 @@ return { { 'ThePrimeagen/refactoring.nvim', - event = 'VeryLazy', dependencies = { 'nvim-lua/plenary.nvim', 'nvim-treesitter/nvim-treesitter', }, - lazy = true, + keys = { + { + 're', + function() + require('refactoring').refactor('Extract Function') + end, + mode = 'x', + desc = 'Refactor: Extract Function', + }, + { + 'rf', + function() + require('refactoring').refactor('Extract Function To File') + end, + mode = 'x', + desc = 'Refactor: Extract To File', + }, + { + 'rv', + function() + require('refactoring').refactor('Extract Variable') + end, + mode = 'x', + desc = 'Refactor: Extract Variable', + }, + { + 'ri', + function() + require('refactoring').refactor('Inline Variable') + end, + mode = { 'n', 'x' }, + desc = 'Refactor: Inline Variable', + }, + { + 'rb', + function() + require('refactoring').refactor('Extract Block') + end, + desc = 'Refactor: Extract Block', + }, + { + 'rr', + function() + -- require('refactoring').select_refactor() + require('telescope').extensions.refactoring.refactors() + end, + mode = { 'n', 'x' }, + desc = 'Refactor: Select', + }, + }, opts = {}, }, } diff --git a/lua/custom/plugins/snippets.lua b/lua/custom/plugins/snippets.lua index 3e40842..ecfe5f2 100755 --- a/lua/custom/plugins/snippets.lua +++ b/lua/custom/plugins/snippets.lua @@ -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' }, 'e', function() - if luasnip.expand_or_jumpable() then - luasnip.expand() - end - end, { silent = true, desc = 'Expand snippet' }) - - vim.keymap.set({ 'i', 's' }, ';', function() - if luasnip.jumpable(1) then - luasnip.jump(1) - end - end, { silent = true, desc = 'Jump forward in snippet' }) - - vim.keymap.set({ 'i', 's' }, ',', function() - if luasnip.jumpable(-1) then - luasnip.jump(-1) - end - end, { silent = true, desc = 'Jump backward in snippet' }) - - vim.keymap.set({ 'i', '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', 'e', function() + if luasnip.expand_or_jumpable() then + luasnip.expand() + end + end, { silent = true, desc = 'Snippet: expand' }) + + vim.keymap.set({ 'i', 's' }, ';', function() + if luasnip.jumpable(1) then + luasnip.jump(1) + end + end, { silent = true, desc = 'Snippet: jump forward' }) + + vim.keymap.set({ 'i', 's' }, ',', function() + if luasnip.jumpable(-1) then + luasnip.jump(-1) + end + end, { silent = true, desc = 'Snippet: jump backward' }) + + vim.keymap.set({ 'i', 's' }, 'c', function() + if luasnip.choice_active() then + luasnip.change_choice(1) + end + end, { silent = true, desc = 'Snippet: cycle choice' }) + end, }