Vim Language Server Protocol support for C#

If you’re an avid Vim user, you’ll likely try to use the terminal editor for everything text-related.
In this post, I’ll show you how to get convenient support for C# with Vim on Linux.

C# is traditionally one of those programming languages that profit from an IDE, an integrated development environment.

Vim can still be a viable alternative if you need minimal features like type definitions or auto-completion.

Install NET.Core

As an Arch Linux user, my first instinct is to install packages with the Arch package manager.
Somehow, this seems to conflict with the language server we’ll install in a later step.

Thus, I recommend a manual install. The Arch Linux wiki explains how. The instructions work for other distributions, too.

  1. Download the dotnet-install.sh script for Linux.

  2. Run the script for the stable version:

chmod +x dotnet-install.sh
./dotnet-install.sh --install-dir /usr/share/dotnet -channel LTS -version latest

(You might need sudo because the normal user does not have permissions for the /usr/share/dotnet folder.)

Install Language Server

We need OmniSharp Roslyn, a cross-platform language server implementation.

The README of the project is densely packed with information. I originality tried to build the executable from scratch because that’s prominently featured. But it’s not necessary and can lead to frustration.

Go to the releases tab and choose a suitable pre-build release.

For example, download the 1.37.5 release for 64-bit Linux with curl and extract it to $HOME/.bin folder:

curl -sL https://github.com/OmniSharp/omnisharp-roslyn/releases/download/v1.37.5/omnisharp-linux-x64.tar.gz | tar xvzf - -C ~/home/.bin

Install LSP

Vim needs a plugin for the Language Server Protocol.

I am using prabirshrestha/vim-lsp, an asynchronous implementation that works both in Vim 8 and NeoVim. The plugin uses VimL and thus has no external dependencies.

Install with native package support or a plugin manager of your choice. Example:

cd ~/vim/pack
git submodule init
git submodule add https://github.com/prabirshrestha/vim-lsp.git
git add .gitmodules vim/pack/prabirshrestha/vim-lsp
git commit

Now register the OmniSharp Language Server. I’ve copied my setup from an article by a fellow tech blogger (hauleth.dev). I created a new file in my Vim folder (~/vim/plugin/lsp.vim) with the following content:

func! s:setup_ls(...) abort
    let l:servers = lsp#get_whitelisted_servers()

    for l:server in l:servers
        let l:cap = lsp#get_server_capabilities(l:server)

        if has_key(l:cap, 'completionProvider')
            setlocal omnifunc=lsp#complete
        endif

        if has_key(l:cap, 'hoverProvider')
            setlocal keywordprg=:LspHover
        endif

        if has_key(l:cap, 'definitionProvider')
            nmap <silent> <buffer> gd <plug>(lsp-definition)
        endif

        if has_key(l:cap, 'referencesProvider')
            nmap <silent> <buffer> gr <plug>(lsp-references)
        endif
    endfor
endfunc

augroup LSC
    autocmd!
    autocmd User lsp_setup call lsp#register_server({
                \ 'name': 'omnisharp-roslyn',
                \ 'cmd': {_->[&shell, &shellcmdflag, 'mono $HOME/.bin/omnisharp/OmniSharp.exe --languageserver']},
                \ 'whitelist': ['cs']
                \})

    autocmd User lsp_server_init call <SID>setup_ls()
    autocmd BufEnter * call <SID>setup_ls()
augroup END

Note: You don’t need to create a new file for the setup, of course. Just find a way to add the settings to Vim/NeoVim (for example, via init.vim configuration).

Note: If you installed OmniSharp into a different directory than $HOME/.bin, you need to adjust the cmd section.

&shell and &shellcmdflag are specific to vim-lsp (and not really necessary on Linux):

It is recommended to use &shell with &shellcmdflag when running script files that can be executed specially on windows where _.bat and _.cmd files cannot be started without running the shell first. This is common for executable installed by npm for nodejs.

mono is the utility that allows you to run .exe files under Linux. It should be on your machine thanks to the .NET Core installation.

Now, as soon as you open a file with filetype cs (for C#), the language server will automatically kick in.
You could type K (in normal mode) when you hover over a keyword, and you’ll get some informations about the word under the cursor.

Syntax Highlighting

Syntax highlighting works out of the box with the Vim runtime.

Bonus: Formatting

I could not find a sanctioned solution for formatting C#. For now, I’m using Uncrustify, a code beautifier for C-style languages.

This tool is not Vim-specific. I run it from the terminal or via external shell command in Vim.

Install a pre-compiled binary from GitHub or use your operating system’s package manager.

You can customize Uncrustify to your liking and you need a default configuration file.

Here are my settings (~/.uncrustify.cfg):

#
# Formatter for c#, java, etc.
#

newlines = LF		# AUTO (default), CRLF, CR, or LF

indent_with_tabs		= 0		# 1=indent to level only, 2=indent with tabs
input_tab_size			= 8		# original tab size
output_tab_size			= 3		# new tab size
indent_columns			= output_tab_size
# indent_label			= 0		# pos: absolute col, neg: relative column
indent_align_string		= False		# align broken strings
indent_brace			= 0
indent_class			= true

nl_start_of_file		    = remove
# nl_start_of_file_min		= 0
nl_end_of_file			    = force
nl_end_of_file_min		    = 1
nl_max				        = 4
nl_before_block_comment		= 2
nl_after_func_body		    = 2
nl_after_func_proto_group 	= 2

nl_assign_brace			= add		# "= {" vs "= \n {"
nl_enum_brace			= add		# "enum {" vs "enum \n {"
nl_union_brace			= add		# "union {" vs "union \n {"
nl_struct_brace			= add		# "struct {" vs "struct \n {"
nl_do_brace			    = add		# "do {" vs "do \n {"
nl_if_brace			    = add		# "if () {" vs "if () \n {"
nl_for_brace			= add		# "for () {" vs "for () \n {"
nl_else_brace			= add		# "else {" vs "else \n {"
nl_while_brace			= add		# "while () {" vs "while () \n {"
nl_switch_brace			= add		# "switch () {" vs "switch () \n {"
nl_func_var_def_blk		= 1
nl_before_case			= 1
nl_fcall_brace			= add		# "foo() {" vs "foo()\n{"
nl_fdef_brace			= add		# "int foo() {" vs "int foo()\n{"
nl_after_return			= TRUE
nl_brace_while			= remove
nl_brace_else			= add
nl_squeeze_ifdef		= TRUE

pos_bool			= trail		# BOOL ops on trailing end

eat_blanks_before_close_brace	= TRUE
eat_blanks_after_open_brace	= TRUE


mod_paren_on_return		    = add		# "return 1;" vs "return (1);"
mod_full_brace_if		    = add		# "if (a) a--;" vs "if (a) { a--; }"
mod_full_brace_for		    = add		# "for () a--;" vs "for () { a--; }"
mod_full_brace_do		    = add		# "do a--; while ();" vs "do { a--; } while ();"
mod_full_brace_while		= add		# "while (a) a--;" vs "while (a) { a--; }"

sp_before_byref			         = remove
sp_before_semi			         = remove
sp_paren_paren			         = remove	# space between (( and ))
sp_return_paren			         = remove	# "return (1);" vs "return(1);"
sp_sizeof_paren			         = remove	# "sizeof (int)" vs "sizeof(int)"
sp_before_sparen		         = force		# "if (" vs "if("
sp_after_sparen			         = force		# "if () {" vs "if (){"
sp_after_cast			         = remove	# "(int) a" vs "(int)a"
sp_inside_braces		         = force		# "{ 1 }" vs "{1}"
sp_inside_braces_struct		     = force		# "{ 1 }" vs "{1}"
sp_inside_braces_enum		     = force		# "{ 1 }" vs "{1}"
sp_inside_paren			         = remove
sp_inside_fparen		         = remove
sp_inside_sparen		         = remove
sp_inside_square		         = remove
#sp_type_func			         = ignore
sp_assign			             = force
sp_arith			             = force
sp_bool				             = force
sp_compare			             = force
sp_assign			             = force
sp_after_comma			         = force
sp_func_def_paren		         = remove	# "int foo (){" vs "int foo(){"
sp_func_call_paren		         = remove	# "foo (" vs "foo("
sp_func_proto_paren		         = remove	# "int foo ();" vs "int foo();"
sp_func_class_paren		         = remove
sp_before_angle			         = remove
sp_after_angle			         = remove
sp_angle_paren			         = remove
sp_angle_paren_empty			 = remove
sp_angle_word			         = ignore
sp_inside_angle			         = remove
sp_inside_angle_empty			 = remove
sp_sparen_brace			         = add
sp_fparen_brace			         = add
sp_after_ptr_star		         = remove
sp_before_ptr_star		         = force
sp_between_ptr_star		         = remove

align_with_tabs			    = FALSE		# use tabs to align
align_on_tabstop		    = FALSE		# align on tabstops
align_enum_equ_span		    = 4
align_nl_cont			    = TRUE
align_var_def_span		    = 1
align_var_def_thresh		= 12
align_var_def_inline		= TRUE
align_var_def_colon		    = TRUE
align_assign_span		    = 1
align_assign_thresh		    = 12
align_struct_init_span		= 3
align_var_struct_span		= 99
align_right_cmt_span		= 3
align_pp_define_span		= 3
align_pp_define_gap		    = 4
align_number_right		    = TRUE
align_typedef_span		    = 5
align_typedef_gap		    = 3
align_var_def_star_style    = 0

cmt_star_cont			= TRUE

Above you see the options from a GitHub configuration file with changes made from this issue.

Thoughts

Using Vim for C# development is a tad wonky. I’ve had better success with languages like Go or OCaml. But in a pinch, it works — even on Linux.