Skip to content

Build command with autocomplete for .csproj files

rene-descartes2021 edited this page Sep 22, 2021 · 21 revisions

Intro:

I needed a way to make a specific .csproj instead of every project in the .sln.

So I made it so the <tab> key should autocomplete to a .csproj.

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

See :help wildmenu.

The only plugin this approach depends on is something which defines asyncdo#run(), either hauleth/asyncdo.vim or an alternative like skywind3000/asyncrun.vim which features real-time population of the QuickFix list.

Vim configuration files

~/.vim/plugin/asyncdo.vim:

"First a generalized way to have the makeprg be called async, not just C#.
"I think I found this on ThePrimeagen YouTube channel or other YouTube video.
"Not necessary for C# but is a generalized approach to calling make Async within Vim with args.

"Display status of :AsyncDo (asyncdo#run) in status line:
let &statusline .= "%{exists('g:asyncdo')?'runing':''}"
"Make makeprg (:make) run async
command! -bang -nargs=* -complete=file Make call asyncdo#run(<bang>0, &makeprg, <f-args>)
"Make :grep run async
command! -bang -nargs=* -complete=dir Grep call asyncdo#run(
	\ <bang>0,
	\ { 'job': &grepprg, 'errorformat': &grepformat  },
	\ <f-args>)

~/.vim/ftplugin/cs.vim:

"Reference: https://github.com/OmniSharp/omnisharp-vim/issues/386
compiler dotnet

" automatically open quickfix window after build is completed
augroup QUICKFIX_CS
	autocmd!
	autocmd QuickFixCmdPost [^l]* nested cwindow
	autocmd QuickFixCmdPost    l* nested lwindow
augroup END

~/.vim/compiler/dotnet.vim:

if exists("current_compiler")
	finish
endif
let current_compiler = "dotnet"
setlocal makeprg=dotnet\ build\ /v:q\ /property:GenerateFullPaths=true\ /clp:ErrorsOnly
setlocal errorformat=\ %#%f(%l\\\,%c):\ %m

set wildmenu

let s:omnisharp_ready=0
augroup OMNISHARP_READY
	autocmd!
	autocmd User OmniSharpReady let s:omnisharp_ready=1
augroup END

"Redefines earlier :Make command for .cs files.
command! -bang -nargs=* -complete=customlist,DotNetFileComplete Make call asyncdo#run(<bang>0, &makeprg, <f-args>)

"Find relevant .csproj files to populate autocomplete list
":help command-completion-custom
fun 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
		"First try to find match from current file's folder and search up to . (cwd)
		let matches=split(globpath(searchdir,'*.csproj'),'\n')
		while '.' !=# searchdir && empty(matches)
			let searchdir=fnamemodify(searchdir,':h')
			let matches=split(globpath(searchdir,'*.csproj'),'\n')
		endwhile
		if empty(matches)
			if s:omnisharp_ready
				"Query for all .csproj files associated with OmniSharp sln
				let csprojs=deepcopy(OmniSharp#proc#GetJob(OmniSharp#GetHost().sln_or_dir).projects)
				let matches=map(csprojs, {index, value -> fnamemodify(value['path'],':.')})
			else
				"Omnisharp-vim not started, try solution directory subdirectories:
				let matches=split(globpath('**','*.csproj'),'\n')
			endif
		endif
	endif
	return matches
endfun

Notes

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 result/selection and any other args for a given directory in a tempfile(). So :Make no args would prefer the cached choice. Such caching the argument to build to a custom directory "-o /tmp/build/bin/"
  • The test to see if we're in :help (file open not relative to cwd) to avoid an infinite loop is not great. Pity findfile with built-in support for an upwards-search doesn't work with wildcards.

Debugger integration

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 :]

augroup OMNISHARP_READY_CONFIG
	autocmd!
	autocmd User OmniSharpReady call WriteVimspectorConfig()
augroup END

fun WriteVimspectorConfig()
	let csprojs=deepcopy(OmniSharp#proc#GetJob(OmniSharp#GetHost().sln_or_dir).projects)
	let configurations=map(csprojs, {index,
		\ value -> { value['name']: {
			\ "adapter":"netcoredbg",
			\ "configuration:": {
				\ "request":"launch",
				\ "program":"${workspaceRoot}".s:dir_separator.fnamemodify(value['target'],':.'),
				\ "args":[],
				\ "stopAtEntry":"true",
				\ "cwd":"${workspaceRoot}",
				\ "env":{}}}}})
	let dict={"configurations":configurations}
	let vimspector_json=json_encode(dict)
	call writefile([vimspector_json],".vimspector.json")
endfun

Notes

To debug the built executable, call call vimspector#LaunchWithSettings( #{ configuration: 'project-name' } ) where project-name is the name of the .csproj. Or F5 or what Vimspector continue keymapping is, and select project name from the prompt. Calling WriteVimspectorConfig() on OmniSharpReady doesn't seem to work.