Skip to content

Build command with autocomplete for .csproj files

rene-descartes2021 edited this page May 13, 2022 · 21 revisions

Intro:

After typing :Make ...<tab> it should autocomplete to a .csproj/.sln to build, which calls dotnet build on that .csproj/.sln.

See :help wildmenu. E.g. typing :Make <tab> will result in a menu of options, or only one option if only one .csproj was found in the search space:

:Make Content.Shared/Content.Shared.csproj
:Make
BuildChecker/BuildChecker.csproj                          Content.Shared/Content.Shared.csproj
Content.Server.Database/Content.Server.Database.csproj

Vim configuration files

" `~/.vim/ftplugin/cs.vim`
" Reference: https://github.com/OmniSharp/omnisharp-vim/issues/386
compiler cs

" [re]defines :Make command, with autocomplete to a csproj in the sln
command! -bang -nargs=* -bar -complete=custom,s:DotNetFileComplete Make call asyncrun#run(<bang>0, #{program: 'make'}, <f-args>)
" Or alternatively:
"command! -bang -nargs=* -bar -complete=custom,s:DotNetFileComplete Make AsyncRun -program=make @ <args>
" Or with AsyncDo instead of AsyncRun (a little different):
"command! -bang -nargs=* -bar -complete=custom,s:DotNetFileComplete Make call asyncdo#run(<bang>0, &makeprg, <f-args>)

" Find relevant .csproj files to populate autocomplete list. See `:help command-completion-custom`
" s:DotNetFileComplete() requires the 'project' data from OmniSharp-Roslyn, without better IPC
" in Vim it is best to cache that data earlier, using s:CacheOmniSharpProjectInBuffer()
function! s:DotNetFileComplete(A,L,P)
  let searchdir = expand('%:.:h')
  let matches = ''
  " If we're not relative to the cwd (e.g. in :help), don't try to search
  if fnamemodify(searchdir,':p:h') !=? searchdir
    let host = OmniSharp#GetHost(bufnr('%'))
    let csprojs = deepcopy(host.job.projects)
    let csprojs_relative = map(csprojs, {index, value -> fnamemodify(value['path'], ':.')})
    if has_key(host, 'project')
      " Make the first project this file is in first in the sln list
      let project = fnamemodify(host['project']['MsBuildProject']['Path'], ':.')
      let i = index(csprojs_relative, project)
      call remove(csprojs_relative, i)
      let matches = join(insert(csprojs_relative, project), "\n")
    else
      let matches = join(csprojs_relative, "\n")
    endif
  endif
  return matches
endfunction
" `~/.vim/compiler/cs.vim`
if exists("current_compiler")
	finish
endif
let current_compiler = "cs"

" As of Vim 8.2, the distribution includes a cs CompilerSet `:help compiler`
"  which uses an obsolete frontent, csc.
" This replaces that obsolute frontend with `dotnet build`.
" Considering both dotnet and mono frontends to compiling C#, you may define
"  a CompilerSet for mono in `compiler/mono.vim` if you wish, and use with `:compiler mono`

if exists(":CompilerSet") != 2		" older Vim always used :setlocal
  command -nargs=* CompilerSet setlocal <args>
endif

CompilerSet makeprg=dotnet\ build\ /v:q\ /property:GenerateFullPaths=true\ /clp:ErrorsOnly
CompilerSet errorformat=\ %#%f(%l\\\,%c):\ %m
" `~/.vim/plugin/omnisharp-vim.vim`
" Cache the 'project' data from OmniSharp-Roslyn to each cs buffer
augroup cacheOmniSharpProject
  autocmd!
  autocmd FileType cs call s:CacheOmniSharpProjectInBuffer()
augroup END

function! s:CacheOmniSharpProjectInBuffer()
  if &filetype == 'cs'
    let bufnr = bufnr('%')
    let host = OmniSharp#GetHost(bufnr)
    if !has_key(host, 'project')
      " 'project' not in cache, must query from OmniSharp-Roslyn, is async
      let l:F = {-> 0}
      call OmniSharp#actions#project#Get(bufnr, l:F)
    endif
  endif
endfunction

Notes

This approach optionally depends on an Async plugin, either:

  1. hauleth/asyncdo.vim, or
  2. skywind3000/asyncrun.vim (which features real-time population of the QuickFix list).

set wildmenu somewhere in your vimrc. If you have many .csproj files in your .sln, then maybe setting the vertical option in :help wildmode may help navigation.

The DotNetFileComplete function above could use generalized refinement:

  • Maybe some caching of the selection and any args for a given project in a tempfile(). So :Make no args would prefer the cached choice, such as with an argument to build to a custom directory '-o /tmp/build/bin/'.

Debugger integration [Obsolete]

[Obsolete] Use :OmniSharpDebugProject and :OmniSharpCreateDebugConfig instead. The instructions below are obsolete/old.

Vimspector appears to be the best option for debugging in Vim. Vimspector requires the configuration file .vimspector.json in order to know what to debug. The following function will use information available from OmniSharp to build said configuration file in the .sln folder:

let s:dir_separator = fnamemodify('.', ':p')[-1 :]

function! WriteVimspectorConfig() abort
  let projects = OmniSharp#GetHost().job.projects
  let config = { 'configurations': {} }
  for project in projects
    let config.configurations[project['name']] = {
    \ 'adapter': 'netcoredbg',
    \ 'configuration': {
    \   'request': 'launch',
    \   'program': project['target'],
    \   'args': [],
    \   'stopAtEntry': v:true
    \ }
    \}
  endfor
  let sln_or_dir = OmniSharp#GetHost().sln_or_dir
  let slndir = sln_or_dir =~? '\.sln$' ? fnamemodify(sln_or_dir, ':h') : sln_or_dir
  let filename = slndir . s:dir_separator . '.vimspector.json'
  call writefile([json_encode(config)], filename)
endfunction

Notes

  • To debug the built executable, call call vimspector#LaunchWithSettings( #{ configuration: 'project-name' } ) where project-name is the name of the .csproj.
  • Or F5 (what the Vimspector continue keymapping is) and select project name from the prompt.