Coding C# in Neovim
In this post I’ll detail how I set up a new C# coding environment using Neovim‘s native LSP support. This guide assumes you’re starting completely fresh, as I did just prior to writing this. My goal was to see if I like the Neovim setup as much as or better than the Vim setup I’ve been using for years. My conclusion on that is at the end of the article.
Should be easy enough. Install it using your distribution’s package manager or whatever other method you’re comfortable with. On my Arch machine I did it like this:
pacman -S neovim
I don’t believe anything I’m using requires the bleeding-edge
nightly version. I’m not using it, anyway, and everything I wanted seems to be working.
You’ll want the latest version of omnisharp-roslyn, which is what Neovim will launch in the background and use as the LSP server whenever you open a
.cs file. You might be able to find it with your package manager (it’s in the AUR, for example), or you can just download the latest
omnisharp-linux-x64.tar.gz release and decompress that anywhere you want.
Neovim has a variety of package managers available. My goal is to use plugins written and configured in Lua whenever possible, because I really don’t like (or understand) vimscript and Lua looks and feels a lot nicer to me. I chose packer.nvim and simply followed the instructions on their github:
$> git clone --depth 1 https://github.com/wbthomason/packer.nvim\ ~/.local/share/nvim/site/pack/packer/start/packer.nvim
I created the file
~/.config/nvim/lua/plugins.lua with the following content:
- telescope.nvim - Not strictly required, but provides a nice floating-window interface to several of the LSP functions, and is also a nice pure-Lua substitute for both the
grepperplugins I use in Vim.
- nvim-lspconfig - This is what automatically launches LSP servers (such as Omnisharp) in the background so that Neovim can talk to them.
- nvim-cmp - This seems to be the most widely recommended completion engine for Neovim.
- cmp-nvim-lsp - Enables LSP completions.
Then at the top of your
~/.config/nvim/init.vim (which you should create if it doesn’t exist already—perhaps by making a copy of your
~/.vimrc file, although I created mine from scratch because my vim config is quite bloated and I wanted to selectively enable only the things I actually still need or want):
After you save that file, then close and re-launch Neovim, you can run this ex command to install all the plugins we just added:
These configuration blocks go into the same
plugins.lua file you created in the previous step, below the
use statements and above
This is enabling the autocomplete plugin, adding a couple keybindings so you can tab and shift-tab between items in the menu, and telling it to use LSP as a completions source. I haven’t played with the keybindings too much, so that is still a prime target for tweaking.
If you also installed other completion sources you would also add those here to the
This is instructing
nvim-lspconfig that we want to enable the
omnisharp LSP provider. It’s also setting up completions with the
on_attach lines, but I’m not entirely sure what those are actually doing. All I know is that completions don’t work without them.
You’ll want to replace the path on line
7 with the correct path to
omnisharp-roslyn that you obtained earlier.
These are the keybindings I use, as an example, but obviously you should tweak these to your own personal preference. In your
~/.config/nvim/init.vim file. In general, the functions you’ll probably be interested in mapping are the
:Telescope lsp_* ones, the
:lua vim.lsp.buf.* ones, and the
:lua vim.lsp.diagnostic.* ones:
Explanation by line number:
find-usings, will open a Telescope window with all the places in your code that the current symbol under your cursor is used in the rest of the project.
goto-definition, will jump to the definition of the current symbol under your cursor (or provide a Telescope list if there are more than one).
rename, will prompt you for a new name to give the current symbol under your cursor, and refactor all references to it in the current project
<leader>dnwill jump to the next warning or error in the current buffer.
<leader>dNwill jump to the previous warning or error in the current buffer.
<leader>ddwill open a Telescope window listing all errors or warnings in the current buffer.
<leader>dDwill open a Telecsope window listing all errors or warnings across the current project.
<leader>xxwill open a Telescope window with all of the possible code actions on the error or warning item under the cursor.
<leader>xXwill open a Telescope window with all of the possible code actions on all errors or warnings in the current buffer.
I edited the
<leader>xX mapping above to a more useful action (document-wide code actions instead of project-wide code actions).
I edited the mappings above to reflect the new keybindings I’ve arrived at after using it a bit more.
If everything is set up correctly, when you open a
.cs file with Neovim, it should launch
omnisharp-roslyn in the background and start giving you feedback (errors, warnings, autocomplete-as-you-type, and so on). When you’re in the file you can run
:LspInfo for a dialog with details about the current LSP provider(s). It should say
1 client(s) attached to this buffer and
Client: omnisharp in there.
Overall I’m pretty happy with this setup so far. The completions feel snappier than in Vim, and I really like Telescope in general. Some other plugins that I’ve also got going right now to complete the setup:
- lualine.nvim for a slightly fancier status line (replacing
vim-airlinefrom my Vim setup)
- gitsigns.nvim for git indicators (replacing
gitgutterfrom my Vim setup)
- I don’t need a replacement for
vim-grepperbecause Telescope already covers those
- vim-filebeagle is the one hold-out that I copied from my Vim setup—it’s just too useful, simple, and ingrained in my muscle memory to live without or care about finding an alternative
I’ve also installed a few additional supported LSP providers and configured them similar to how Omnisharp was configured above. For languages that don’t have LSP providers (such as
shellcheck for POSIX shell scripts) I’ve installed efm-langserver, which I’m hoping should let me avoid something like
ALE or language-specific plugins, and just stick with Neovim’s built-in LSP support across the board. That way the general behavior and keybindings I configured above will be consistent regardless of what language I’m working in.
So far the only thing I’m missing during C# development is the
OmniSharpFixUsings command that
omnisharp-vim provided. As far as I can tell, that functionality is not (yet) exposed through the LSP interface of
omnisharp-roslyn. It’s not too terrible a loss, however, because my
<leader>xX mapping will pull up all possible code actions in the current document, so I can repeat that and add all the missing
using statements one at a time. It’s not as quick as running a single command and having all the missing
usings added for you, but it’s an acceptable alternative, especially since the
OmniSharpFixUsings feature had some annoying idiosyncrasies around when there were multiple different choices for a
I hope this helps some fellow Vim-loving Linux C# developers out there. Happy (Neo)vimming!
I’ve figured out how to get code analyzers and
.editorconfig support working now. Create a file named
omnisharp.json at your project root if you don’t have one already, and make sure it has the following options enabled:
Now create your
.editorconfig at the project root. You can pull in the example config from the omnisharp-roslyn repo. After restarting everything, you should start seeing code analysis and formatting suggestions along side your syntax errors and warnings.
Then if you also install the editorconfig.nvim plugin, it will make it so you don’t need filetype plugins or other custom Neovim configuration to change your indent type or size for
.cs files (it will read those from your
.editorconfig and do that for you).
If you haven’t installed
dotnet-script yet, you’re missing out. It’s a
dotnet CLI plugin that allows you to use C# as a scripting language. You install it like so:
dotnet tool install -g dotnet-script
Now you can create C# scripts with a
.csx extension like this one:
And execute it like this:
dotnet script main.csx
Or (provided you have the
#! on line
1) even like this:
chmod 755 main.csx ./main.csx
It even supports loading external nuget packages (an exercise I’ll leave up to the reader). You’ll note, however, that typing
:LspInfo while editing that file shows that the
omnisharp LSP client is not attaching. To fix this we need to do two things.
You can do this in your
Or if you prefer to do it in one of your Lua config files, like
By default, the
omnisharp LSP provider traverses up the file path from your current C# file looking for either a
.csproj file in order to tell
omnisharp where the root directory of the current project is. When you’re editing a
.csx script, there is no
.csproj file, so
omnisharp doesn’t know where to look.
We can solve this by overriding the default
root_dir configuration to simply return the current directory if the file being edited ends with
.csx (or use the default behavior if not). In your
plugins.lua file, add this inside the curly braces after
You’ll also need to import
util at the top of your config like this:
I fixed a problem with the above function where it wasn’t searching for the
.sln file on its own first, so multiple lsp clients could end up attaching if you were working with multiple projects in the same solution.
If you did these two steps correctly, the next time you open a
.csx file, the LSP provider should (eventually) attach itself and you’ll get diagnostics and autocompletions in your C# script!
Short Permalink for Attribution: rdsm.ca/4fpea