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.
Download the dotnet-install.sh script for Linux.
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 use 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.