487 lines
21 KiB
Lua
487 lines
21 KiB
Lua
-- =============================================================================
|
|
-- .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.)
|
|
-- <leader>f Format Buffer (run csharpier on the current file)
|
|
-- <leader>th Toggle Inlay Hints (show/hide parameter name hints)
|
|
-- [d / ]d Previous / Next Diagnostic
|
|
-- <leader>q Diagnostic Quickfix List
|
|
-- <leader>sd Search Diagnostics (Telescope)
|
|
--
|
|
-- ── COMPLETION (blink.cmp) ────────────────────────────────────────────────────
|
|
-- <C-Space> Open completion menu (or open docs if menu is open)
|
|
-- <C-n> / <C-p> Next / Previous item (also <Up> / <Down>)
|
|
-- <C-y> Accept completion (auto-imports if LSP supports it)
|
|
-- <C-e> Dismiss menu
|
|
-- <C-k> Toggle signature help (see function parameter hints)
|
|
-- <Tab>/<S-Tab> 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 <C-n>/<C-p>.
|
|
-- :Copilot auth Re-authenticate with GitHub (run once on first setup)
|
|
-- :Copilot status Check if Copilot is active
|
|
--
|
|
-- ── DEBUGGING (nvim-dap + netcoredbg) ─────────────────────────────────────────
|
|
-- <leader>dc Continue / Start debug session
|
|
-- <leader>db Toggle Breakpoint (set/unset on current line)
|
|
-- <leader>dB Conditional Breakpoint (prompt for a condition expression)
|
|
-- <leader>ds Step Over (F10 equivalent)
|
|
-- <leader>di Step Into (F11 equivalent)
|
|
-- <leader>do Step Out (Shift-F11 equivalent)
|
|
-- <leader>dr Open REPL (interactive debug console)
|
|
-- <leader>dl Re-run Last Session (repeat last debug config)
|
|
-- <leader>du Toggle Debug UI (show/hide the dap-ui panels)
|
|
-- <leader>de Evaluate Expression (hover eval under cursor)
|
|
-- <leader>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) ────────────────────────────────────────
|
|
-- <leader>nn Run Nearest Test (test method under cursor)
|
|
-- <leader>nf Run File Tests (all tests in current file)
|
|
-- <leader>na Run All Tests (entire project/solution)
|
|
-- <leader>ns Toggle Test Summary (tree view of all tests + status)
|
|
-- <leader>no Show Test Output (output of last test run)
|
|
-- <leader>np Show Short Output (condensed output panel)
|
|
-- <leader>nd Debug Nearest Test (run test under cursor with DAP)
|
|
-- <leader>nw Watch Nearest Test (re-run on file save)
|
|
-- <leader>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('<leader>dc', dap.continue, '[C]ontinue / Start')
|
|
map('<leader>db', dap.toggle_breakpoint, 'Toggle [B]reakpoint')
|
|
map('<leader>dB', function()
|
|
dap.set_breakpoint(vim.fn.input 'Breakpoint condition: ')
|
|
end, 'Conditional [B]reakpoint')
|
|
map('<leader>ds', dap.step_over, '[S]tep Over')
|
|
map('<leader>di', dap.step_into, 'Step [I]nto')
|
|
map('<leader>do', dap.step_out, 'Step [O]ut')
|
|
map('<leader>dr', dap.repl.open, 'Open [R]EPL')
|
|
map('<leader>dl', dap.run_last, 'Run [L]ast')
|
|
map('<leader>du', function() require('dapui').toggle() end, 'Toggle [U]I')
|
|
map('<leader>de', function() require('dapui').eval() end, '[E]val under cursor')
|
|
map('<leader>dE', function()
|
|
require('dapui').eval(vim.fn.input 'Expression: ')
|
|
end, '[E]val expression')
|
|
|
|
-- Register the <leader>d group name with which-key (if loaded).
|
|
local ok, wk = pcall(require, 'which-key')
|
|
if ok then
|
|
wk.add { { '<leader>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: <leader>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('<leader>nn', function() nt.run.run() end, 'Run [N]earest test')
|
|
map('<leader>nf', function() nt.run.run(vim.fn.expand '%') end, 'Run [F]ile tests')
|
|
map('<leader>na', function() nt.run.run(vim.fn.getcwd()) end, 'Run [A]ll tests')
|
|
map('<leader>ns', function() nt.summary.toggle() end, 'Toggle [S]ummary panel')
|
|
map('<leader>no', function() nt.output.open { enter = true } end, 'Show [O]utput')
|
|
map('<leader>np', function() nt.output_panel.toggle() end, 'Toggle output [P]anel')
|
|
map('<leader>nd', function() nt.run.run { strategy = 'dap' } end, '[D]ebug nearest test')
|
|
map('<leader>nw', function() nt.watch.toggle(vim.fn.expand '%') end, '[W]atch file tests')
|
|
map('<leader>nx', function() nt.run.stop() end, 'Stop running tests')
|
|
|
|
-- Register the <leader>n group name with which-key (if loaded).
|
|
local ok, wk = pcall(require, 'which-key')
|
|
if ok then
|
|
wk.add { { '<leader>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,
|
|
},
|
|
}
|