diff --git a/init.lua b/init.lua index 8da2a65e..4e81bfc1 100644 --- a/init.lua +++ b/init.lua @@ -416,6 +416,15 @@ require('lazy').setup({ group = vim.api.nvim_create_augroup('telescope-lsp-attach', { clear = true }), callback = function(event) local buf = event.buf + local client = vim.lsp.get_client_by_id(event.data.client_id) + + local goto_declaration = function() + if client and client:supports_method('textDocument/declaration', buf) then + vim.lsp.buf.declaration() + else + builtin.lsp_definitions() + end + end -- Find references for the word under your cursor. vim.keymap.set('n', 'grr', builtin.lsp_references, { buffer = buf, desc = '[G]oto [R]eferences' }) @@ -427,7 +436,9 @@ require('lazy').setup({ -- Jump to the definition of the word under your cursor. -- This is where a variable was first declared, or where a function is defined, etc. -- To jump back, press . + vim.keymap.set('n', 'gd', builtin.lsp_definitions, { buffer = buf, desc = '[G]oto [D]efinition' }) vim.keymap.set('n', 'grd', builtin.lsp_definitions, { buffer = buf, desc = '[G]oto [D]efinition' }) + vim.keymap.set('n', 'gD', goto_declaration, { buffer = buf, desc = '[G]oto [D]eclaration' }) -- Fuzzy find all the symbols in your current document. -- Symbols are things like variables, functions, types, etc. @@ -480,7 +491,15 @@ require('lazy').setup({ -- Automatically install LSPs and related tools to stdpath for Neovim -- Mason must be loaded before its dependents so we need to set it up here. -- NOTE: `opts = {}` is the same as calling `require('mason').setup({})` - { 'mason-org/mason.nvim', opts = {} }, + { + 'mason-org/mason.nvim', + opts = { + registries = { + 'github:mason-org/mason-registry', + 'github:Crashdummyy/mason-registry', + }, + }, + }, { 'mason-org/mason-lspconfig.nvim', opts = {} }, 'WhoIsSethDaniel/mason-tool-installer.nvim', @@ -523,6 +542,8 @@ require('lazy').setup({ vim.api.nvim_create_autocmd('LspAttach', { group = vim.api.nvim_create_augroup('kickstart-lsp-attach', { clear = true }), callback = function(event) + local client = vim.lsp.get_client_by_id(event.data.client_id) + -- NOTE: Remember that Lua is a real programming language, and as such it is possible -- to define small helper and utility functions so you don't have to repeat yourself. -- @@ -537,20 +558,28 @@ require('lazy').setup({ -- Most Language Servers support renaming across files, etc. map('grn', vim.lsp.buf.rename, '[R]e[n]ame') + -- Show documentation for the symbol under your cursor. + map('K', vim.lsp.buf.hover, 'Hover Documentation') + -- Execute a code action, usually your cursor needs to be on top of an error -- or a suggestion from your LSP for this to activate. map('gra', vim.lsp.buf.code_action, '[G]oto Code [A]ction', { 'n', 'x' }) -- WARN: This is not Goto Definition, this is Goto Declaration. -- For example, in C this would take you to the header. - map('grD', vim.lsp.buf.declaration, '[G]oto [D]eclaration') + map('grD', function() + if client and client:supports_method('textDocument/declaration', event.buf) then + vim.lsp.buf.declaration() + else + vim.lsp.buf.definition() + end + end, '[G]oto [D]eclaration') -- The following two autocommands are used to highlight references of the -- word under your cursor when your cursor rests there for a little while. -- See `:help CursorHold` for information about when this is executed -- -- When you move your cursor, the highlights will be cleared (the second autocommand). - local client = vim.lsp.get_client_by_id(event.data.client_id) if client and client:supports_method('textDocument/documentHighlight', event.buf) then local highlight_augroup = vim.api.nvim_create_augroup('kickstart-lsp-highlight', { clear = false }) vim.api.nvim_create_autocmd({ 'CursorHold', 'CursorHoldI' }, { @@ -853,12 +882,25 @@ require('lazy').setup({ { -- Highlight, edit, and navigate code 'nvim-treesitter/nvim-treesitter', + build = ':TSUpdate', config = function() - local filetypes = { 'bash', 'c', 'diff', 'html', 'lua', 'luadoc', 'markdown', 'markdown_inline', 'query', 'vim', 'vimdoc' } - require('nvim-treesitter').install(filetypes) vim.api.nvim_create_autocmd('FileType', { - pattern = filetypes, - callback = function() vim.treesitter.start() end, + pattern = { + 'bash', + 'c', + 'cs', + 'diff', + 'html', + 'lua', + 'markdown', + 'query', + 'vim', + 'help', + }, + callback = function(args) + local lang = vim.treesitter.language.get_lang(args.match) or args.match + pcall(vim.treesitter.start, args.buf, lang) + end, }) end, }, @@ -883,7 +925,7 @@ require('lazy').setup({ -- This is the easiest way to modularize your config. -- -- Uncomment the following line and add your plugins to `lua/custom/plugins/*.lua` to get going. - -- { import = 'custom.plugins' }, + { import = 'custom.plugins' }, -- -- For additional information with loading, sourcing and examples see `:help lazy.nvim-๐Ÿ”Œ-plugin-spec` -- Or use telescope! diff --git a/lua/custom/plugins/dotnet.lua b/lua/custom/plugins/dotnet.lua new file mode 100644 index 00000000..1b3d46f5 --- /dev/null +++ b/lua/custom/plugins/dotnet.lua @@ -0,0 +1,486 @@ +-- ============================================================================= +-- .NET / C# Development Setup +-- ============================================================================= +-- This file wires up everything needed for a productive .NET experience in +-- Neovim: Roslyn LSP, GitHub Copilot (via blink.cmp), DAP debugging with +-- netcoredbg, and Neotest for running xUnit/NUnit/MSTest tests. +-- +-- PREREQUISITES (install once on your system): +-- - .NET SDK: https://dotnet.microsoft.com/download +-- - Node.js > 20 (for Copilot): https://nodejs.org +-- - On first launch run: :Copilot auth (one-time GitHub login) +-- - Mason will auto-install: netcoredbg, csharpier +-- - roslyn.nvim downloads its own language server on first use +-- +-- QUICK REFERENCE โ€” KEY BINDINGS +-- (language-agnostic binds work everywhere; .NET-specific are noted) +-- ============================================================================= +-- +-- โ”€โ”€ LSP NAVIGATION โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +-- grd Go to Definition (jump to where a symbol is defined) +-- grD Go to Declaration (e.g. interface declaration) +-- gri Go to Implementation (jump to concrete implementation) +-- grr Find All References (list all usages in Telescope) +-- grt Go to Type Definition (jump to the type of a variable) +-- gO Document Symbols (fuzzy search symbols in this file) +-- gW Workspace Symbols (fuzzy search symbols in whole project) +-- K Hover Documentation (show docs / type info under cursor) +-- grn Rename Symbol (rename across the whole project) +-- gra Code Actions (fix suggestions, extract method, etc.) +-- f Format Buffer (run csharpier on the current file) +-- th Toggle Inlay Hints (show/hide parameter name hints) +-- [d / ]d Previous / Next Diagnostic +-- q Diagnostic Quickfix List +-- sd Search Diagnostics (Telescope) +-- +-- โ”€โ”€ COMPLETION (blink.cmp) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +-- Open completion menu (or open docs if menu is open) +-- / Next / Previous item (also / ) +-- Accept completion (auto-imports if LSP supports it) +-- Dismiss menu +-- Toggle signature help (see function parameter hints) +-- / Move through snippet expansions +-- +-- โ”€โ”€ GITHUB COPILOT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +-- Copilot suggestions appear in the blink.cmp completion menu automatically. +-- They are ranked alongside LSP completions โ€” just navigate with /. +-- :Copilot auth Re-authenticate with GitHub (run once on first setup) +-- :Copilot status Check if Copilot is active +-- +-- โ”€โ”€ DEBUGGING (nvim-dap + netcoredbg) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +-- dc Continue / Start debug session +-- db Toggle Breakpoint (set/unset on current line) +-- dB Conditional Breakpoint (prompt for a condition expression) +-- ds Step Over (F10 equivalent) +-- di Step Into (F11 equivalent) +-- do Step Out (Shift-F11 equivalent) +-- dr Open REPL (interactive debug console) +-- dl Re-run Last Session (repeat last debug config) +-- du Toggle Debug UI (show/hide the dap-ui panels) +-- de Evaluate Expression (hover eval under cursor) +-- dE Evaluate (input prompt) (type an expression to evaluate) +-- +-- The debug UI opens automatically when a session starts and shows: +-- Left panel: Scopes ยท Breakpoints ยท Call Stack ยท Watches +-- Bottom panel: REPL ยท Console output +-- +-- โ”€โ”€ TESTING (neotest + neotest-dotnet) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +-- nn Run Nearest Test (test method under cursor) +-- nf Run File Tests (all tests in current file) +-- na Run All Tests (entire project/solution) +-- ns Toggle Test Summary (tree view of all tests + status) +-- no Show Test Output (output of last test run) +-- np Show Short Output (condensed output panel) +-- nd Debug Nearest Test (run test under cursor with DAP) +-- nw Watch Nearest Test (re-run on file save) +-- nx Stop Running Tests +-- +-- ============================================================================= + +return { + + -- =========================================================================== + -- 1. ROSLYN LSP (seblj/roslyn.nvim) + -- =========================================================================== + -- The same Roslyn language server that powers Visual Studio and Rider. + -- Downloads Microsoft.CodeAnalysis.LanguageServer automatically on first use. + -- Provides: IntelliSense, go-to-definition, find-references, rename, + -- code actions (extract method, add using, etc.), inlay hints, + -- and full project/solution awareness. + { + 'seblj/roslyn.nvim', + ft = { 'cs', 'vb' }, + dependencies = { + 'mason-org/mason.nvim', -- ensures mason is loaded first + }, + opts = { + -- roslyn.nvim finds and downloads the language server into stdpath('data'). + -- Set exe to override with a custom path if you manage the server yourself. + config = { + -- Pass extra capabilities from blink.cmp so the LSP knows we support + -- snippet completions, resolve, etc. + capabilities = vim.tbl_deep_extend( + 'force', + vim.lsp.protocol.make_client_capabilities(), + -- blink.cmp may not be loaded yet at spec parse time; we wrap in a + -- function via on_attach / capabilities below instead. + {} + ), + }, + -- In larger repos / monoliths this helps Roslyn search a bit wider for + -- the correct solution or project root. + broad_search = true, + -- Allow target switching when multiple solutions exist in the repo. + lock_target = false, + }, + config = function(_, opts) + -- Inject blink.cmp capabilities now that blink is guaranteed loaded. + local ok, blink = pcall(require, 'blink.cmp') + if ok then + opts.config = opts.config or {} + opts.config.capabilities = blink.get_lsp_capabilities(opts.config.capabilities) + end + + require('roslyn').setup(opts) + end, + }, + + -- =========================================================================== + -- 2. GITHUB COPILOT (zbirenbaum/copilot.lua + blink-cmp-copilot) + -- =========================================================================== + -- copilot.lua runs the Copilot LSP in the background. + -- We disable the built-in ghost-text / panel UI and instead surface + -- completions through blink.cmp so everything comes from one menu. + { + 'zbirenbaum/copilot.lua', + event = 'InsertEnter', + opts = { + -- Disable copilot.lua's own UI โ€” blink.cmp owns the completion menu. + suggestion = { enabled = false }, + panel = { enabled = false }, + -- Explicitly enable C# (and a few other useful filetypes). + filetypes = { + cs = true, + lua = true, + python = true, + javascript = true, + typescript = true, + -- Disable noisy filetypes. + markdown = false, + help = false, + gitcommit = false, + }, + -- Node.js must be > 20. + copilot_node_command = 'node', + }, + }, + + -- blink-cmp-copilot bridges copilot.lua into blink.cmp as a source provider. + -- Copilot completions appear in the menu with a icon and "Copilot" label. + { + 'giuxtaposition/blink-cmp-copilot', + dependencies = { 'zbirenbaum/copilot.lua' }, + -- blink.cmp is already configured in init.lua; we patch its sources table + -- here so we don't have to touch the existing config. + specs = { + { + 'saghen/blink.cmp', + optional = true, + opts = { + sources = { + default = { 'lsp', 'path', 'snippets', 'copilot' }, + providers = { + copilot = { + name = 'copilot', + module = 'blink-cmp-copilot', + -- Show Copilot items after LSP items in the menu. + score_offset = -1, + async = true, + -- Give it a recognisable icon in the completion menu. + transform_items = function(_, items) + for _, item in ipairs(items) do + item.kind_icon = '' + item.kind_name = 'Copilot' + end + return items + end, + }, + }, + }, + }, + }, + }, + }, + + -- =========================================================================== + -- 3. DEBUGGING (nvim-dap + nvim-dap-ui + mason-nvim-dap) + -- =========================================================================== + -- netcoredbg is the open-source .NET debugger that understands the DAP + -- protocol. mason-nvim-dap installs it automatically. + { + 'mfussenegger/nvim-dap', + dependencies = { + -- Automatic installer / bridge between mason and dap adapters. + { + 'jay-babu/mason-nvim-dap.nvim', + dependencies = { 'mason-org/mason.nvim' }, + opts = { + -- Let mason-nvim-dap install and configure netcoredbg. + ensure_installed = { 'netcoredbg' }, + automatic_installation = true, + handlers = {}, -- use default handlers for installed adapters + }, + }, + + -- A full debug UI: scopes, locals, call stack, breakpoints, REPL. + { + 'rcarriga/nvim-dap-ui', + dependencies = { 'nvim-neotest/nvim-nio' }, + config = function() + local dap = require 'dap' + local dapui = require 'dapui' + + dapui.setup { + -- Layout: left sidebar + bottom panel โ€” mirrors a typical IDE layout. + layouts = { + { + -- Left sidebar: variables, call stack, breakpoints, watches. + position = 'left', + size = 40, + elements = { + { id = 'scopes', size = 0.35 }, + { id = 'stacks', size = 0.35 }, + { id = 'breakpoints', size = 0.15 }, + { id = 'watches', size = 0.15 }, + }, + }, + { + -- Bottom tray: REPL for live evaluation + program console output. + position = 'bottom', + size = 12, + elements = { + { id = 'repl', size = 0.5 }, + { id = 'console', size = 0.5 }, + }, + }, + }, + } + + -- Auto-open the UI when a debug session starts. + dap.listeners.after.event_initialized['dapui_config'] = function() + dapui.open() + end + -- Auto-close the UI when the session ends or terminates. + 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 shows variable values inline while stepping through code. + { + 'theHamsta/nvim-dap-virtual-text', + opts = { + -- Show the value of the variable at the current scope inline. + display_callback = function(variable, _, _, _, options) + if #variable.value > 60 then + return ' ' .. string.sub(variable.value, 1, 57) .. '...' + end + return ' ' .. variable.value + end, + }, + }, + }, + + config = function() + local dap = require 'dap' + + -- โ”€โ”€ Adapter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + -- mason-nvim-dap installs netcoredbg into mason's bin directory. + -- We resolve the path at runtime so it works on any machine. + local mason_bin = vim.fn.stdpath 'data' .. '/mason/bin' + local netcoredbg_cmd = mason_bin .. '/netcoredbg' + -- On Windows mason wraps executables in .cmd files. + if vim.fn.has 'win32' == 1 then + netcoredbg_cmd = mason_bin .. '/netcoredbg.cmd' + end + + dap.adapters.coreclr = { + type = 'executable', + command = netcoredbg_cmd, + args = { '--interpreter=vscode' }, + } + + -- โ”€โ”€ Helper: prompt for / remember DLL path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + -- On first launch you'll be asked for the path to the compiled DLL. + -- The answer is cached for the rest of the session; you're only re-asked + -- if you confirm you want to change it. + local function get_dll_path() + if vim.g.dotnet_last_dll_path == nil then + vim.g.dotnet_last_dll_path = vim.fn.input( + 'Path to DLL: ', + vim.fn.getcwd() .. '/bin/Debug/', + 'file' + ) + else + local change = vim.fn.confirm( + 'Reuse last DLL?\n' .. vim.g.dotnet_last_dll_path, + '&Yes\n&No', + 1 + ) + if change == 2 then + vim.g.dotnet_last_dll_path = vim.fn.input( + 'Path to DLL: ', + vim.g.dotnet_last_dll_path, + 'file' + ) + end + end + return vim.g.dotnet_last_dll_path + end + + -- โ”€โ”€ Helper: optional pre-build โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + local function maybe_build() + local build = vim.fn.confirm('Build project first?', '&Yes\n&No', 2) + if build == 1 then + local proj = vim.fn.input( + 'Path to .csproj/.sln: ', + vim.g.dotnet_last_proj_path or (vim.fn.getcwd() .. '/'), + 'file' + ) + vim.g.dotnet_last_proj_path = proj + vim.notify('Building ' .. proj .. ' โ€ฆ', vim.log.levels.INFO) + local result = os.execute('dotnet build -c Debug "' .. proj .. '" > /dev/null 2>&1') + if result == 0 then + vim.notify('Build succeeded', vim.log.levels.INFO) + else + vim.notify('Build FAILED โ€” check :messages', vim.log.levels.ERROR) + end + end + end + + -- โ”€โ”€ Launch configurations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + -- Shared across cs and fsharp. + local dotnet_launch = { + { + type = 'coreclr', + name = 'Launch .NET (netcoredbg)', + request = 'launch', + program = function() + maybe_build() + return get_dll_path() + end, + }, + { + -- Attach to a running process (e.g. an already-running web server). + type = 'coreclr', + name = 'Attach to process', + request = 'attach', + processId = require('dap.utils').pick_process, + }, + } + + dap.configurations.cs = dotnet_launch + dap.configurations.fsharp = dotnet_launch + + -- โ”€โ”€ Keybinds โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + -- See the header comment at the top of this file for full descriptions. + local map = function(keys, func, desc) + vim.keymap.set('n', keys, func, { desc = 'Debug: ' .. desc }) + end + + map('dc', dap.continue, '[C]ontinue / Start') + map('db', dap.toggle_breakpoint, 'Toggle [B]reakpoint') + map('dB', function() + dap.set_breakpoint(vim.fn.input 'Breakpoint condition: ') + end, 'Conditional [B]reakpoint') + map('ds', dap.step_over, '[S]tep Over') + map('di', dap.step_into, 'Step [I]nto') + map('do', dap.step_out, 'Step [O]ut') + map('dr', dap.repl.open, 'Open [R]EPL') + map('dl', dap.run_last, 'Run [L]ast') + map('du', function() require('dapui').toggle() end, 'Toggle [U]I') + map('de', function() require('dapui').eval() end, '[E]val under cursor') + map('dE', function() + require('dapui').eval(vim.fn.input 'Expression: ') + end, '[E]val expression') + + -- Register the d group name with which-key (if loaded). + local ok, wk = pcall(require, 'which-key') + if ok then + wk.add { { 'd', group = '[D]ebug' } } + end + end, + }, + + -- =========================================================================== + -- 4. TESTING (nvim-neotest + neotest-dotnet) + -- =========================================================================== + -- neotest-dotnet discovers xUnit, NUnit, and MSTest tests automatically by + -- scanning for [Fact], [Theory], [Test], [TestCase], etc. attributes. + { + 'nvim-neotest/neotest', + dependencies = { + 'nvim-lua/plenary.nvim', + 'antoinemadec/FixCursorHold.nvim', -- required by neotest + 'nvim-treesitter/nvim-treesitter', + -- .NET adapter โ€” supports xUnit, NUnit, MSTest. + 'Issafalcon/neotest-dotnet', + }, + config = function() + require('neotest').setup { + adapters = { + require('neotest-dotnet') { + -- dap integration: nd will attach the debugger to the test. + dap = { justMyCode = false }, + -- Discover tests using the dotnet CLI runner. + -- Set to 'omnisharp' if you prefer the OmniSharp test runner. + dotnet_additional_args = {}, + }, + }, + -- Show pass/fail icons in the gutter next to each test. + status = { enabled = true, signs = true, virtual_text = false }, + -- Open the output panel automatically for short failures. + output = { enabled = true, open_on_run = 'short' }, + -- Use a bottom split for the output panel. + output_panel = { + enabled = true, + open = 'botright split | resize 15', + }, + } + + -- โ”€โ”€ Keybinds โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + -- See the header comment at the top of this file for full descriptions. + local nt = require 'neotest' + local map = function(keys, func, desc) + vim.keymap.set('n', keys, func, { desc = 'Test: ' .. desc }) + end + + map('nn', function() nt.run.run() end, 'Run [N]earest test') + map('nf', function() nt.run.run(vim.fn.expand '%') end, 'Run [F]ile tests') + map('na', function() nt.run.run(vim.fn.getcwd()) end, 'Run [A]ll tests') + map('ns', function() nt.summary.toggle() end, 'Toggle [S]ummary panel') + map('no', function() nt.output.open { enter = true } end, 'Show [O]utput') + map('np', function() nt.output_panel.toggle() end, 'Toggle output [P]anel') + map('nd', function() nt.run.run { strategy = 'dap' } end, '[D]ebug nearest test') + map('nw', function() nt.watch.toggle(vim.fn.expand '%') end, '[W]atch file tests') + map('nx', function() nt.run.stop() end, 'Stop running tests') + + -- Register the n group name with which-key (if loaded). + local ok, wk = pcall(require, 'which-key') + if ok then + wk.add { { 'n', group = '[N]eotest' } } + end + end, + }, + + -- =========================================================================== + -- 5. CSHARPIER (formatter, installed via Mason) + -- =========================================================================== + -- csharpier is the opinionated C# formatter (like gofmt/prettier for C#). + -- It is installed by mason-tool-installer below and wired into conform.nvim. + { + -- Re-use mason-tool-installer (already set up in init.lua) to ensure + -- csharpier is installed. We declare it as a separate spec here so this + -- file is self-contained; lazy.nvim merges duplicate plugin specs. + 'WhoIsSethDaniel/mason-tool-installer.nvim', + optional = true, + opts = function(_, opts) + opts.ensure_installed = opts.ensure_installed or {} + vim.list_extend(opts.ensure_installed, { 'csharpier', 'roslyn' }) + end, + }, + { + -- Patch conform.nvim to format C# files with csharpier. + -- This merges cleanly with the existing conform setup in init.lua. + 'stevearc/conform.nvim', + optional = true, + opts = function(_, opts) + opts.formatters_by_ft = opts.formatters_by_ft or {} + opts.formatters_by_ft.cs = { 'csharpier' } + end, + }, +}