README.md |
Create a Docker Image for Testing
For the purpose of demonstration or to try out how everything works before you break your system, we can run all the upcoming commands inside a docker container. In this demo that container is based on the latest ubuntu image.
Create the Dockerfile
Create a Dockerfile
in your desired directory that looks like this:
# Use Ubuntu as the base image
FROM ubuntu:latest
# Set environment variables to avoid interactive prompts during package installation
ENV DEBIAN_FRONTEND=noninteractive
# Install required packages
RUN apt update && apt install -y \
sudo \
curl \
git \
ripgrep \
unzip \
wget \
dotnet-sdk-8.0 \
&& rm -rf /var/lib/apt/lists/*
# Create a new user '<yourname>' and add to sudo group (ramboe in my case)
RUN useradd -m -s /bin/bash ramboe && \
echo "ramboe ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Switch to the new user
USER ramboe
WORKDIR /home/ramboe
RUN mkdir Downloads && mkdir Documents
# Default command to keep the container running
CMD ["/bin/bash"]
Create the Container from the Dockerfile
Within the same directory execute the following commands
-
docker build -t ubuntu-ramboe .
-
docker run -it --name my_ubuntu_container ubuntu-ramboe
Congratulations, you now have your ubuntu docker environment set up and are ready go to the actually interesting part.
Install NeoVim and nodejs
While in fedora you would simply do sudo dnf install nodejs neovim
, Debian based systems require some extra hoops that we are going to address now.
[Ubuntu] Install homebrew for neovim installation
The apt package has of old version 0.9.x
, other distros like fedora or arch have the latest version in their repositories (we want 0.10.x
)
Therefore we use homebrew to install neovim since it provides us with the latest version
This is how you install homebrew and then neovim through the terminal
-
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-
follow instructions on the screen
-
apt install build-essential
-
install neovim with homebrew:
brew install neovim
[Ubuntu] install node (if necessary)
In order for mason to work properly, node must be installed on the system. Otherwise mason can't install packages.
While in fedora you would do something like sudo dnf install nodejs
the installation in Debian based system looks like this:
curl -fsSL https://deb.nodesource.com/setup_23.x \
-o nodesource_setup.sh && \
sudo -E bash nodesource_setup.sh && \
sudo apt-get install -y nodejs
Install NvChad for neovim and C# dependencies
For the sake of convenience we use nvchad in this tutorial since it already comes with a lot of comfort pre-installed and uses common package managers like Mason and LazyVim
git clone https://github.com/NvChad/starter ~/.config/nvim && nvim
Configure NeoVim for C#
Install the necessary plugins via Mason.
add the following lines to plugins/init.lua
-- ~/.config/nvim/lua/plugins/init.lua
-- ...
{
"williamboman/mason.nvim",
opts = {
ensure_installed = {
"lua-language-server",
"csharp-language-server",
"omnisharp",
"xmlformatter",
"stylua",
"bicep-lsp",
"html-lsp",
"css-lsp",
"csharpier",
"prettier",
"json-lsp"
},
},
},
{
"nvim-treesitter/nvim-treesitter",
opts = {
-- ensure_installed = "all"
ensure_installed = {
"vim",
"lua",
"vimdoc",
"html",
"css",
"c_sharp",
"bicep"
},
},
},
Then, inside neovim execute :MasonInstallAll
Initiate 'omnisharp', the C# language server
add the following lines to lspconfig.lua
-- ~/.config/nvim/lua/configs/lspconfig.lua
-- ...
-- omnisharp languageserver
local pid = vim.fn.getpid()
lspconfig.omnisharp.setup({
cmd = { "omnisharp", "--languageserver", "--hostPID", tostring(pid) },
on_attach = nvlsp.on_attach,
on_init = nvlsp.on_init,
capabilities = nvlsp.capabilities,
})
Omnisharp is not the only language server for C#, there is also the csharp-language-server - both have different capabilities and caveats, you might need to try out what's best for you
Setup NeoVim for Debugging and Unit Testing in C#
Configure lua files for debugging
back inside plugins/init.lua
add the following lines:
-- ~/.config/nvim/lua/plugins/init.lua
-- ...
{
-- Debug Framework
"mfussenegger/nvim-dap",
dependencies = {
"rcarriga/nvim-dap-ui",
},
config = function()
require "configs.nvim-dap"
end,
event = "VeryLazy",
},
{ "nvim-neotest/nvim-nio" },
{
-- UI for debugging
"rcarriga/nvim-dap-ui",
dependencies = {
"mfussenegger/nvim-dap",
},
config = function()
require "configs.nvim-dap-ui"
end,
},
{
"nvim-neotest/neotest",
requires = {
{
"Issafalcon/neotest-dotnet",
}
},
dependencies = {
"nvim-neotest/nvim-nio",
"nvim-lua/plenary.nvim",
"antoinemadec/FixCursorHold.nvim",
"nvim-treesitter/nvim-treesitter"
}
},
{
"Issafalcon/neotest-dotnet",
lazy = false,
dependencies = {
"nvim-neotest/neotest"
}
},
add the following lines to init.lua
-- ~/.config/nvim/lua/init.lua
-- ...
require("neotest").setup({
adapters = {
require("neotest-dotnet")
}
})
create nvim-dap.lua
and nvim-dap-ui.lua
touch ~/.config/nvim/lua/configs/nvim-dap.lua && touch ~/.config/nvim/lua/configs/nvim-dap-ui.lua
Put the debugger (dap) in place
we need to make sure the debugger is installed as well
the old way (obsolete)
mkdir -p ~/Documents/debuggers && wget -O ~/Downloads/netcoredbg-linux-amd64.tar.gz https://github.com/Samsung/netcoredbg/releases/download/3.1.2-1054/netcoredbg-linux-amd64.tar.gz && tar -xzf ~/Downloads/netcoredbg-linux-amd64.tar.gz -C ~/Documents/debuggers
This will put an executable dotnet debugger into the
~/Documents/debuggers
directory.
If you are not happy with that path, simply put it somewhere else, but make sure to update the file path innvim-dap.lua
.
Also be aware that this will download explicitely version3.1.2-1054
of the netcoredbg-linux-amd64 debugger. By the time you read this, this version might be outdated already.
paste these lines into nvim-dap.lua
, this will configure the debugger and add the keybindings that we need while we debug our code
-- ~/.config/nvim/lua/configs/nvim-dap.lua
local dap = require("dap")
dap.adapters.coreclr = {
type = "executable",
command = "/home/ramboe/Documents/debuggers/netcoredbg/netcoredbg",
args = { "--interpreter=vscode" },
}
dap.adapters.netcoredbg = {
type = "executable",
command = "/home/ramboe/Documents/debuggers/netcoredbg/netcoredbg",
args = { "--interpreter=vscode" },
}
dap.configurations.cs = {
{
type = "coreclr",
name = "launch - netcoredbg",
request = "launch",
program = function()
-- return vim.fn.input("Path to dll: ", vim.fn.getcwd() .. "/src/", "file")
return vim.fn.input("Path to dll: ", vim.fn.getcwd() .. "/bin/Debug/net8.0/", "file")
end,
-- justMyCode = false,
-- stopAtEntry = false,
-- -- program = function()
-- -- -- todo: request input from ui
-- -- return "/path/to/your.dll"
-- -- end,
-- env = {
-- ASPNETCORE_ENVIRONMENT = function()
-- -- todo: request input from ui
-- return "Development"
-- end,
-- ASPNETCORE_URLS = function()
-- -- todo: request input from ui
-- return "http://localhost:5050"
-- end,
-- },
-- cwd = function()
-- -- todo: request input from ui
-- return vim.fn.getcwd()
-- end,
},
}
local map = vim.keymap.set
local opts = { noremap = true, silent = true }
map("n", "<F5>", "<Cmd>lua require'dap'.continue()<CR>", opts)
map("n", "<F6>", "<Cmd>lua require('neotest').run.run({strategy = 'dap'})<CR>", opts)
map("n", "<F9>", "<Cmd>lua require'dap'.toggle_breakpoint()<CR>", opts)
map("n", "<F10>", "<Cmd>lua require'dap'.step_over()<CR>", opts)
map("n", "<F11>", "<Cmd>lua require'dap'.step_into()<CR>", opts)
map("n", "<F8>", "<Cmd>lua require'dap'.step_out()<CR>", opts)
-- map("n", "<F12>", "<Cmd>lua require'dap'.step_out()<CR>", opts)
map("n", "<leader>dr", "<Cmd>lua require'dap'.repl.open()<CR>", opts)
map("n", "<leader>dl", "<Cmd>lua require'dap'.run_last()<CR>", opts)
map("n", "<leader>dt", "<Cmd>lua require('neotest').run.run({strategy = 'dap'})<CR>",
{ noremap = true, silent = true, desc = 'debug nearest test' })
the better way
I refactored the old way's code and added DLL auto-detection, you can find the whole update here.
Set up the debugging UI
paste these lines into nvim-dap-ui.lua
, This will configure the UI while debugging.
-- ~/.config/nvim/lua/configs/nvim-dap-ui.lua
local dap, dapui = require("dap"), require("dapui")
dapui.setup({
icons = { expanded = "▾", collapsed = "▸", current_frame = "▸" },
mappings = {
-- Use a table to apply multiple mappings
expand = { "<CR>", "<2-LeftMouse>" },
open = "o",
remove = "d",
edit = "e",
repl = "r",
toggle = "t",
},
-- Use this to override mappings for specific elements
element_mappings = {
-- Example:
-- stacks = {
-- open = "<CR>",
-- expand = "o",
-- }
},
-- Expand lines larger than the window
expand_lines = vim.fn.has("nvim-0.7") == 1,
-- Layouts define sections of the screen to place windows.
-- The position can be "left", "right", "top" or "bottom".
-- The size specifies the height/width depending on position. It can be an Int
-- or a Float. Integer specifies height/width directly (i.e. 20 lines/columns) while
-- Float value specifies percentage (i.e. 0.3 - 30% of available lines/columns)
-- Elements are the elements shown in the layout (in order).
-- Layouts are opened in order so that earlier layouts take priority in window sizing.
layouts = {
{
elements = {
-- Elements can be strings or table with id and size keys.
{ id = "scopes", size = 0.25 },
"breakpoints",
"stacks",
"watches",
},
size = 40, -- 40 columns
position = "left",
},
{
elements = {
"repl",
"console",
},
size = 0.25, -- 25% of total lines
position = "bottom",
},
},
controls = {
-- Requires Neovim nightly (or 0.8 when released)
enabled = true,
-- Display controls in this element
element = "repl",
icons = {
pause = "",
play = "",
step_into = "",
step_over = "",
step_out = "",
step_back = "",
run_last = "↻",
terminate = "□",
},
},
floating = {
max_height = nil, -- These can be integers or a float between 0 and 1.
max_width = nil, -- Floats will be treated as percentage of your screen.
border = "single", -- Border style. Can be "single", "double" or "rounded"
mappings = {
close = { "q", "<Esc>" },
},
},
windows = { indent = 1 },
render = {
max_type_length = nil, -- Can be integer or nil.
max_value_lines = 100, -- Can be integer or nil.
},
})
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
vim.api.nvim_set_hl(0, "blue", { fg = "#3d59a1" })
vim.api.nvim_set_hl(0, "green", { fg = "#9ece6a" })
vim.api.nvim_set_hl(0, "yellow", { fg = "#FFFF00" })
vim.api.nvim_set_hl(0, "orange", { fg = "#f09000" })
vim.fn.sign_define('DapBreakpoint',
{
text = '', -- nerdfonts icon here
-- text = '🔴', -- nerdfonts icon here
texthl = 'DapBreakpointSymbol',
linehl = 'DapBreakpoint',
numhl = 'DapBreakpoint'
})
vim.fn.sign_define('DapStopped',
{
text = '', -- nerdfonts icon here
texthl = 'yellow',
linehl = 'DapBreakpoint',
numhl = 'DapBreakpoint'
})
vim.fn.sign_define('DapBreakpointRejected',
{
text = '', -- nerdfonts icon here
texthl = 'DapStoppedSymbol',
linehl = 'DapBreakpoint',
numhl = 'DapBreakpoint'
})
Test the whole Setup
Test 1) debug a console application
dotnet new console -n 'MyConsole'
set a breakpoint with F9
, then start debugging with F5
Test 2) debug a unit test
dotnet new mstest -n 'MyTest'
set a breakpoint with F9
, then start debugging with dt
Caveats
This setup covers 95% of what you are ever going to do with C# in neovim. However, there are two things that I could not come up with a solution yet.
Razor LSP Support - .cshtml
and .razor
files and anything that comes along with it are not being supported (natively) at this very moment. tris203/rzls aims to solve this problem but this is still under construction and I did not manage to get it to work yet. Would be nice if there was something 'official' that works as reliable and natural as the typescript-language-server for example.
Azure function Debugging - although KaiWalter/azure-functions addresses this use case, I did not manage to get it to work yet. I have to say that I did not find this to be a big problem although working with azure functions every day. They should be designed in a unit-testable way anyways so you should not even have to run the function app itself in order to test if your code works
Further Reading
https://git.ramboe.io/configuration/nvchad#1-prerequisites
aaronbos.dev
https://aaronbos.dev/posts/debugging-csharp-neovim-nvim-dap
https://aaronbos.dev/posts/dotnet-roslyn-editorconfig-neovim