diff --git a/autoload/OmniSharp/actions/test.vim b/autoload/OmniSharp/actions/test.vim index 26bab3777..6cce08588 100644 --- a/autoload/OmniSharp/actions/test.vim +++ b/autoload/OmniSharp/actions/test.vim @@ -1,74 +1,199 @@ let s:save_cpo = &cpoptions set cpoptions&vim -let s:runningTest = 0 - -function! s:BindTest(bufnr, Callback, ...) abort - if !s:CheckCapabilities() | return | endif - if !has_key(OmniSharp#GetHost(a:bufnr), 'project') - " Initialize the test by fetching the project for the buffer - then call - " this function again in the callback - call OmniSharp#actions#project#Get(a:bufnr, - \ function('s:BindTest', [a:bufnr, a:Callback])) - return +let s:debug = {} +let s:debug.process = {} +let s:run = {} +let s:run.running = 0 +let s:run.single = {} +let s:run.multiple = {} +let s:utils = {} +let s:utils.init = {} +let s:utils.log = {} + +function! OmniSharp#actions#test#Debug(nobuild, ...) abort + if !s:utils.capabilities() | return | endif + if !s:utils.isrunning() | return | endif + let s:nobuild = a:nobuild + if !OmniSharp#util#HasVimspector() + return s:utils.log.warn('Vimspector required to debug tests') endif - let s:runningTest = 1 - call OmniSharp#actions#codestructure#Get(a:bufnr, - \ a:Callback) + let bufnr = a:0 ? (type(a:1) == type('') ? bufnr(a:1) : a:1) : bufnr('%') + let DebugTest = funcref('s:debug.prepare', [a:0 > 1 ? a:2 : '']) + call s:utils.initialize([bufnr], DebugTest) endfunction -function! OmniSharp#actions#test#Run(nobuild) abort - let s:nobuild = a:nobuild - call s:BindTest(bufnr('%'), function('s:RunTest', [function('s:CBRunTest')])) +function! s:debug.prepare(testName, bufferTests) abort + let bufnr = a:bufferTests[0].bufnr + let tests = a:bufferTests[0].tests + let currentTest = s:utils.findTest(tests, a:testName) + if type(currentTest) != type([]) || len(currentTest) == 0 + return s:utils.log.warn('No test found') + endif + let currentTest = currentTest[0] + let project = OmniSharp#GetHost(bufnr).project + let targetFramework = project.MsBuildProject.TargetFramework + let opts = { + \ 'ResponseHandler': funcref('s:debug.launch', [bufnr, currentTest.name]), + \ 'BufNum': bufnr, + \ 'Parameters': { + \ 'MethodName': currentTest.name, + \ 'NoBuild': get(s:, 'nobuild', 0), + \ 'TestFrameworkName': currentTest.framework, + \ 'TargetFrameworkVersion': targetFramework + \ }, + \ 'SendBuffer': 0 + \} + echomsg 'Debugging test ' . currentTest.name + call OmniSharp#stdio#Request('/v2/debugtest/getstartinfo', opts) +endfunction + +function! s:debug.launch(bufnr, testname, response) abort + let args = split(substitute(a:response.Body.Arguments, '\"', '', 'g'), ' ') + let cmd = a:response.Body.FileName + let testhost = [cmd] + args + if !s:debug.process.start(testhost) | return | endif + let s:run.running = 1 + call OmniSharp#testrunner#StateRunning(a:bufnr, a:testname) + let s:debug.bufnr = a:bufnr + let s:omnisharp_pre_debug_cwd = getcwd() + call vimspector#LaunchWithConfigurations({ + \ 'attach': { + \ 'adapter': 'netcoredbg', + \ 'configuration': { + \ 'request': 'attach', + \ 'processId': s:debug.process.pid + \ } + \ } + \}) + let project_dir = fnamemodify(OmniSharp#GetHost(a:bufnr).sln_or_dir, ':p:h') + execute 'tcd' project_dir + let opts = { + \ 'ResponseHandler': s:debug.complete, + \ 'Parameters': { + \ 'TargetProcessId': s:debug.process.pid + \ } + \} + echomsg 'Launching debugged test' + call OmniSharp#stdio#Request('/v2/debugtest/launch', opts) +endfunction + +function! s:debug.complete(response) abort + if !a:response.Success + call s:utils.log.warn(['Error debugging unit test', a:response.Message]) + call OmniSharp#testrunner#StateError(s:debug.bufnr, + \ split(trim(a:response.Message), '\r\?\n', 1)) + else + call OmniSharp#testrunner#StateSkipped(s:debug.bufnr) + endif +endfunction + +function! s:debug.process.start(command) abort + if OmniSharp#proc#supportsNeovimJobs() + let jobid = jobstart(a:command, { 'on_exit': self.closed }) + let self.pid = jobpid(jobid) + elseif OmniSharp#proc#supportsVimJobs() + let job = job_start(a:command, { 'close_cb': self.closed }) + let self.pid = split(job, ' ')[1] + else + return s:utils.log.warn('Cannot launch test process.') + endif + return 1 endfunction -function! OmniSharp#actions#test#Debug(nobuild) abort +function! s:debug.process.closed(...) abort + call OmniSharp#stdio#Request('/v2/debugtest/stop', {}) + let s:run.running = 0 + call vimspector#Reset() + execute 'tcd' s:omnisharp_pre_debug_cwd + unlet s:omnisharp_pre_debug_cwd +endfunction + + +function! OmniSharp#actions#test#Reset() abort + let s:run.running = 0 +endfunction + + +function! OmniSharp#actions#test#Run(nobuild, ...) abort + if !s:utils.capabilities() | return | endif + if !s:utils.isrunning() | return | endif let s:nobuild = a:nobuild - if !OmniSharp#util#HasVimspector() - echohl WarningMsg - echomsg 'Vimspector required to debug tests' - echohl None - return + let bufnr = a:0 ? (type(a:1) == type('') ? bufnr(a:1) : a:1) : bufnr('%') + let RunTest = funcref('s:run.single.test', [a:0 > 1 ? a:2 : '']) + call s:utils.initialize([bufnr], RunTest) +endfunction + +function! s:run.single.test(testName, bufferTests) abort + let bufnr = a:bufferTests[0].bufnr + let tests = a:bufferTests[0].tests + let currentTest = s:utils.findTest(tests, a:testName) + if type(currentTest) != type([]) || len(currentTest) == 0 + return s:utils.log.warn('No test found') endif - call s:BindTest(bufnr('%'), function('s:DebugTest', [function('s:CBDebugTest')])) + let s:run.running = 1 + for ct in currentTest + call OmniSharp#testrunner#StateRunning(bufnr, ct.name) + endfor + let currentTest = currentTest[0] + let project = OmniSharp#GetHost(bufnr).project + let targetFramework = project.MsBuildProject.TargetFramework + let currentTestName = substitute(currentTest.name, '(.*)$', '', '') + let opts = { + \ 'ResponseHandler': funcref('s:run.process', [s:run.single.complete, bufnr, tests]), + \ 'BufNum': bufnr, + \ 'Parameters': { + \ 'MethodName': currentTestName, + \ 'NoBuild': get(s:, 'nobuild', 0), + \ 'TestFrameworkName': currentTest.framework, + \ 'TargetFrameworkVersion': targetFramework + \ }, + \ 'SendBuffer': 0 + \} + echomsg 'Running test ' . currentTestName + call OmniSharp#stdio#Request('/v2/runtest', opts) endfunction -function! s:CBRunTest(summary) abort +function! s:run.single.complete(summary) abort + let locations = filter(copy(a:summary.locations), 'has_key(v:val, "bufnr")') + if len(locations) > 1 + " A single test was run, but multiple test results were returned. This can + " happen when using e.g. NUnit TestCaseSources which re-run the test using + " different arguments. + call s:run.multiple.complete([a:summary]) + return + endif + if a:summary.pass && len(locations) == 0 + echomsg 'No tests were run' + " Do we ever reach here? + " call OmniSharp#testrunner#StateSkipped(bufnr) + endif + let location = locations[0] + call OmniSharp#testrunner#StateComplete(location) if a:summary.pass - if len(a:summary.locations) == 0 - echomsg 'No tests were run' - elseif get(a:summary.locations[0], 'type', '') ==# 'W' - echohl WarningMsg - echomsg a:summary.locations[0].name . ': skipped' - echohl None + if get(location, 'type', '') ==# 'W' + call s:utils.log.warn(location.name . ': skipped') else - echohl Title - echomsg a:summary.locations[0].name . ': passed' - echohl None + call s:utils.log.emphasize(location.name . ': passed') endif else - echomsg a:summary.locations[0].name . ': failed' - let title = 'Test failure: ' . a:summary.locations[0].name + echomsg location.name . ': failed' + let title = 'Test failure: ' . location.name + if get(g:, 'OmniSharp_runtests_quickfix', 0) == 0 | return | endif let what = {} if len(a:summary.locations) > 1 - let what = {'quickfixtextfunc': function('s:QuickfixTextFuncStackTrace')} + let what.quickfixtextfunc = {info-> + \ map(getqflist({'id': info.id, 'items': 1}).items, {_,i -> i.text})} endif call OmniSharp#locations#SetQuickfix(a:summary.locations, title, what) endif endfunction -function! s:CBDebugTest(response) abort - if !a:response.Success - echohl WarningMsg - echomsg 'Error debugging unit test' - echomsg a:response.Message - echohl None - endif -endfunction function! OmniSharp#actions#test#RunInFile(nobuild, ...) abort let s:nobuild = a:nobuild - if !s:CheckCapabilities() | return | endif + if !s:utils.capabilities() | return | endif + if !s:utils.isrunning() | return | endif if a:0 && type(a:1) == type([]) let files = a:1 elseif a:0 && type(a:1) == type('') @@ -85,22 +210,60 @@ function! OmniSharp#actions#test#RunInFile(nobuild, ...) abort if filereadable(l:file) let nr = bufadd(l:file) else - echohl WarningMsg | echomsg 'File not found: ' . l:file | echohl None + call s:utils.log.warn('File not found: ' . l:file) continue endif endif call add(buffers, nr) endfor - if len(buffers) == 0 - return + if len(buffers) == 0 | return | endif + call s:utils.initialize(buffers, s:run.multiple.prepare) +endfunction + +function! s:run.multiple.prepare(bufferTests) abort + let Requests = [] + for btests in a:bufferTests + let bufnr = btests.bufnr + let tests = btests.tests + let testnames = map(copy(tests), {_,t -> t.name}) + if len(tests) + call OmniSharp#testrunner#StateRunning(bufnr, testnames) + call add(Requests, funcref('s:run.multiple.inBuffer', [bufnr, tests])) + endif + endfor + if len(Requests) == 0 | return s:utils.log.warn('No tests found') | endif + let s:run.running = 1 + if g:OmniSharp_runtests_parallel + if g:OmniSharp_runtests_echo_output + echomsg '---- Running tests ----' + endif + call OmniSharp#util#AwaitParallel(Requests, s:run.multiple.complete) + else + call OmniSharp#util#AwaitSequence(Requests, s:run.multiple.complete) endif - let s:runningTest = 1 - call OmniSharp#util#AwaitParallel( - \ map(copy(buffers), {i,b -> function('OmniSharp#actions#project#Get', [b])}), - \ function('s:FindTestsInFiles', [function('s:CBRunTestsInFile'), buffers])) endfunction -function! s:CBRunTestsInFile(summary) abort +function! s:run.multiple.inBuffer(bufnr, tests, Callback) abort + if !g:OmniSharp_runtests_parallel && g:OmniSharp_runtests_echo_output + echomsg '---- Running tests: ' . bufname(a:bufnr) . ' ----' + endif + let project = OmniSharp#GetHost(a:bufnr).project + let targetFramework = project.MsBuildProject.TargetFramework + let opts = { + \ 'ResponseHandler': funcref('s:run.process', [a:Callback, a:bufnr, a:tests]), + \ 'BufNum': a:bufnr, + \ 'Parameters': { + \ 'MethodNames': map(copy(a:tests), {i,t -> substitute(t.name, '(.*)$', '', '')}), + \ 'NoBuild': get(s:, 'nobuild', 0), + \ 'TestFrameworkName': a:tests[0].framework, + \ 'TargetFrameworkVersion': targetFramework + \ }, + \ 'SendBuffer': 0 + \} + call OmniSharp#stdio#Request('/v2/runtestsinclass', opts) +endfunction + +function! s:run.multiple.complete(summary) abort let pass = 1 let locations = [] for summary in a:summary @@ -109,9 +272,12 @@ function! s:CBRunTestsInFile(summary) abort let pass = 0 endif endfor + for location in locations + call OmniSharp#testrunner#StateComplete(location) + endfor if pass let title = len(locations) . ' tests passed' - echohl Title + call s:utils.log.emphasize(title) else let passed = 0 let noStackTrace = 0 @@ -127,63 +293,46 @@ function! s:CBRunTestsInFile(summary) abort if noStackTrace let title .= '. Check :messages for details.' endif - echohl WarningMsg + call s:utils.log.warn(title) endif - echomsg title - echohl None + if get(g:, 'OmniSharp_runtests_quickfix', 0) == 0 | return | endif call OmniSharp#locations#SetQuickfix(locations, title) endfunction -function! s:RunTest(Callback, bufnr, codeElements) abort - let tests = s:FindTests(a:codeElements) - let currentTest = s:FindTest(tests) - if type(currentTest) != type({}) - echohl WarningMsg | echomsg 'No test found' | echohl None - let s:runningTest = 0 - return - endif - let project = OmniSharp#GetHost(a:bufnr).project - let targetFramework = project.MsBuildProject.TargetFramework - let opts = { - \ 'ResponseHandler': function('s:RunTestsRH', [a:Callback, a:bufnr, tests]), - \ 'Parameters': { - \ 'MethodName': currentTest.name, - \ 'NoBuild': get(s:, 'nobuild', 0), - \ 'TestFrameworkName': currentTest.framework, - \ 'TargetFrameworkVersion': targetFramework - \ }, - \ 'SendBuffer': 0 - \} - echomsg 'Running test ' . currentTest.name - call OmniSharp#stdio#Request('/v2/runtest', opts) -endfunction -function! s:RunTestsRH(Callback, bufnr, tests, response) abort - let s:runningTest = 0 - if !a:response.Success | return | endif +" Response handler used when running a single test, or multiple tests in files +function! s:run.process(Callback, bufnr, tests, response) abort + let s:run.running = 0 + if !a:response.Success + call OmniSharp#testrunner#StateError(a:bufnr, + \ split(trim(eval(a:response.Message)), '\r\?\n', 1)) + return s:utils.log.warn('An error has occurred. This may indicate a failed build') + endif if type(a:response.Body.Results) != type([]) - echohl WarningMsg - echomsg 'Error: "' . a:response.Body.Failure . - \ '" - this may indicate a failed build' - echohl None - return + call OmniSharp#testrunner#StateError(a:bufnr, + \ split(trim(a:response.Body.Failure), '\r\?\n', 1)) + return s:utils.log.warn('Error: "' . a:response.Body.Failure . + \ '" - this may indicate a failed build') endif let summary = { \ 'pass': a:response.Body.Pass, \ 'locations': [] \} for result in a:response.Body.Results - " Strip namespace and classname from test method name let location = { + \ 'bufnr': a:bufnr, + \ 'fullname': result.MethodName, \ 'filename': bufname(a:bufnr), \ 'name': substitute(result.MethodName, '^.*\.', '', '') \} let locations = [location] " Write any standard output to message-history if len(get(result, 'StandardOutput', [])) + let location.output = [] echomsg 'Standard output from test ' . location.name . ':' for output in result.StandardOutput for line in split(trim(output), '\r\?\n', 1) + call add(location.output, line) echomsg ' ' . line endfor endfor @@ -191,6 +340,8 @@ function! s:RunTestsRH(Callback, bufnr, tests, response) abort if result.Outcome =~? 'failed' let location.type = 'E' let location.text = location.name . ': ' . result.ErrorMessage + let location.message = split(result.ErrorMessage, '\r\?\n') + let location.stacktrace = split(result.ErrorStackTrace, '\r\?\n') let st = result.ErrorStackTrace let parsed = matchlist(st, '.* in \(.\+\):line \(\d\+\)') if len(parsed) > 0 @@ -226,10 +377,10 @@ function! s:RunTestsRH(Callback, bufnr, tests, response) abort endif if !has_key(location, 'lnum') " Success, or unexpected test failure. - let test = s:FindTest(a:tests, result.MethodName) - if type(test) == type({}) - let location.lnum = test.nameRange.Start.Line - let location.col = test.nameRange.Start.Column + let test = s:utils.findTest(a:tests, result.MethodName) + if type(test) == type([]) && len(test) > 0 + let location.lnum = test[0].nameRange.Start.Line + let location.col = test[0].nameRange.Start.Column let location.vcol = 0 endif endif @@ -240,197 +391,133 @@ function! s:RunTestsRH(Callback, bufnr, tests, response) abort call a:Callback(summary) endfunction -function! s:DebugTest(Callback, bufnr, codeElements) abort - let tests = s:FindTests(a:codeElements) - let currentTest = s:FindTest(tests) - if type(currentTest) != type({}) - echohl WarningMsg | echomsg 'No test found' | echohl None - let s:runningTest = 0 - return - endif - let project = OmniSharp#GetHost(a:bufnr).project - let targetFramework = project.MsBuildProject.TargetFramework - let opts = { - \ 'ResponseHandler': function('s:DebugTestsRH', [a:Callback, a:bufnr, tests]), - \ 'Parameters': { - \ 'MethodName': currentTest.name, - \ 'NoBuild': get(s:, 'nobuild', 0), - \ 'TestFrameworkName': currentTest.framework, - \ 'TargetFrameworkVersion': targetFramework - \ }, - \ 'SendBuffer': 0 - \} - echomsg 'Debugging test ' . currentTest.name - call OmniSharp#stdio#Request('/v2/debugtest/getstartinfo', opts) -endfunction - -function! s:DebugTestsRH(Callback, bufnr, tests, response) abort - let testhost = [a:response.Body.FileName] + split(substitute(a:response.Body.Arguments, '\"', '', 'g'), ' ') - let testhost_job_pid = s:StartTestProcess(testhost) - let g:testhost_job_pid = testhost_job_pid - - let host = OmniSharp#GetHost() - let s:omnisharp_pre_debug_cwd = getcwd() - let new_cwd = fnamemodify(host.sln_or_dir, ':p:h') - call vimspector#LaunchWithConfigurations({ - \ 'attach': { - \ 'adapter': 'netcoredbg', - \ 'configuration': { - \ 'request': 'attach', - \ 'processId': testhost_job_pid - \ } - \ } - \}) - execute 'tcd '.new_cwd - - call s:LaunchDebuggedTest(a:Callback, testhost_job_pid) -endfunction - -function! s:LaunchDebuggedTest(Callback, pid) abort - let opts = { - \ 'ResponseHandler': function('s:LaunchDebuggedTestRH', [a:Callback, a:pid]), - \ 'Parameters': { - \ 'TargetProcessId': a:pid - \ } - \} - echomsg 'Launching debugged test' - call OmniSharp#stdio#Request('/v2/debugtest/launch', opts) -endfunction - -function! s:LaunchDebuggedTestRH(Callback, pid, response) abort - call a:Callback(a:response) -endfunction -function! s:FindTestsInFiles(Callback, buffers, ...) abort - call OmniSharp#util#AwaitParallel( - \ map(copy(a:buffers), {i,b -> function('OmniSharp#actions#codestructure#Get', [b])}), - \ function('s:RunTestsInFiles', [a:Callback])) +function! OmniSharp#actions#test#Validate() abort + return s:utils.capabilities() endfunction -function! s:RunTestsInFiles(Callback, bufferCodeStructures) abort - let Requests = [] - for bcs in a:bufferCodeStructures - let bufnr = bcs[0] - let codeElements = bcs[1] - let tests = s:FindTests(codeElements) - if len(tests) - call add(Requests, function('s:RunTestsInFile', [bufnr, tests])) - endif - endfor - if len(Requests) == 0 - echohl WarningMsg | echomsg 'No tests found' | echohl None - let s:runningTest = 0 - return +function! s:utils.capabilities() abort + if !g:OmniSharp_server_stdio + return self.log.warn('stdio only, sorry') endif - if g:OmniSharp_runtests_parallel - if g:OmniSharp_runtests_echo_output - echomsg '---- Running tests ----' - endif - call OmniSharp#util#AwaitParallel(Requests, a:Callback) - else - call OmniSharp#util#AwaitSequence(Requests, a:Callback) + if g:OmniSharp_translate_cygwin_wsl + return self.log.warn('Tests do not work in WSL unfortunately') endif + return 1 endfunction -function! s:RunTestsInFile(bufnr, tests, Callback) abort - if !g:OmniSharp_runtests_parallel && g:OmniSharp_runtests_echo_output - echomsg '---- Running tests: ' . bufname(a:bufnr) . ' ----' +function! s:utils.isrunning() abort + if s:run.running + return self.log.warn('A test is already running') endif - let project = OmniSharp#GetHost(a:bufnr).project - let targetFramework = project.MsBuildProject.TargetFramework - let opts = { - \ 'ResponseHandler': function('s:RunTestsRH', [a:Callback, a:bufnr, a:tests]), - \ 'BufNum': a:bufnr, - \ 'Parameters': { - \ 'MethodNames': map(copy(a:tests), {i,t -> t.name}), - \ 'NoBuild': get(s:, 'nobuild', 0), - \ 'TestFrameworkName': a:tests[0].framework, - \ 'TargetFrameworkVersion': targetFramework - \ }, - \ 'SendBuffer': 0 - \} - call OmniSharp#stdio#Request('/v2/runtestsinclass', opts) -endfunction - -function! s:FindTest(tests, ...) abort - for test in a:tests - if a:0 - if test.name ==# a:1 - return test - endif - else - if line('.') >= test.range.Start.Line && line('.') <= test.range.End.Line - return test - endif - endif - endfor - return 0 + return 1 endfunction -function! s:FindTests(codeElements) abort +" Find all of the test methods in a CodeStructure response +function! s:utils.extractTests(bufnr, codeElements) abort if type(a:codeElements) != type([]) | return [] | endif + let filename = fnamemodify(bufname(a:bufnr), ':p') + let testlines = map( + \ filter( + \ copy(OmniSharp#GetHost(a:bufnr).project.tests), + \ {_,dt -> dt.CodeFilePath ==# filename}), + \ {_,dt -> dt.LineNumber}) let tests = [] for element in a:codeElements if has_key(element, 'Properties') \ && type(element.Properties) == type({}) \ && has_key(element.Properties, 'testMethodName') \ && has_key(element.Properties, 'testFramework') - call add(tests, { - \ 'name': element.Properties.testMethodName, - \ 'framework': element.Properties.testFramework, - \ 'range': element.Ranges.full, - \ 'nameRange': element.Ranges.name, - \}) + " Compare with project discovered tests. Note that test discovery may + " include a test multiple times, if the test can be run with different + " arguments (e.g. NUnit TestCaseSource) + + " Discovered test line numbers begin at the first line of code, not the + " line containing the test name, so when the method opening brace is not + " on the same line as the test method name, the line numbers will not + " match. We therefore search ahead for the closest line number, and use + " that. + let testStart = element.Ranges.name.Start.Line + let testStart = min(filter(copy(testlines), {_,l -> l >= testStart})) + for dt in OmniSharp#GetHost(a:bufnr).project.tests + if dt.CodeFilePath ==# filename && dt.LineNumber == testStart + call add(tests, { + \ 'name': dt.FullyQualifiedName, + \ 'framework': element.Properties.testFramework, + \ 'range': element.Ranges.full, + \ 'nameRange': element.Ranges.name, + \}) + endif + endfor endif - call extend(tests, s:FindTests(get(element, 'Children', []))) + call extend(tests, self.extractTests(a:bufnr, get(element, 'Children', []))) endfor return tests endfunction -function! s:CheckCapabilities() abort - if !g:OmniSharp_server_stdio - echohl WarningMsg | echomsg 'stdio only, sorry' | echohl None - return 0 - endif - if g:OmniSharp_translate_cygwin_wsl - echohl WarningMsg - echomsg 'Tests do not work in WSL unfortunately' - echohl None - return 0 - endif - if s:runningTest - echohl WarningMsg | echomsg 'A test is already running' | echohl None - return 0 +" Find the test in a list of tests that matches the current cursor position +function! s:utils.findTest(tests, testName) abort + if a:testName !=# '' + for test in a:tests + if test.name ==# a:testName + return [test] + endif + endfor + else + let found = [] + for test in a:tests + if line('.') >= test.range.Start.Line && line('.') <= test.range.End.Line + call add(found, test) + endif + endfor + return found endif - return 1 + return 0 endfunction -function! s:TestProcessClosed(...) abort - call OmniSharp#stdio#Request('/v2/debugtest/stop', {}) - let s:runningTest = 0 - call vimspector#Reset() - execute 'tcd '.s:omnisharp_pre_debug_cwd - unlet s:omnisharp_pre_debug_cwd +" For the given buffers, discover the project's tests (which includes fetching +" the project structure if it hasn't already been fetched. Finally, fetch the +" buffer code structures. All operations are performed asynchronously, and the +" a:Callback is called when all buffer code structures have been fetched. +function! s:utils.initialize(buffers, Callback) abort + call OmniSharp#testrunner#Init(a:buffers) + call s:utils.init.await(a:buffers, 'OmniSharp#testrunner#Discover', + \ funcref('s:utils.init.await', [a:buffers, 'OmniSharp#actions#codestructure#Get', + \ funcref('s:utils.init.extract', [a:Callback])])) endfunction -function! s:StartTestProcess(command) abort - if OmniSharp#proc#supportsNeovimJobs() - let job = jobpid(jobstart(a:command, { - \ 'on_exit': function('s:TestProcessClosed') - \ })) - elseif OmniSharp#proc#supportsVimJobs() - let job = split(job_start(a:command, { - \ 'close_cb': function('s:TestProcessClosed') - \ }), ' ',)[1] - else - echohl WarningMsg | echomsg 'Cannot launch test process.' | echohl None - endif - return job +function! s:utils.init.await(buffers, functionName, Callback, ...) abort + let Funcs = map(copy(a:buffers), {i,b -> function(a:functionName, [b])}) + call OmniSharp#util#AwaitParallel(Funcs, a:Callback) endfunction -function! s:QuickfixTextFuncStackTrace(info) abort - let items = getqflist({'id' : a:info.id, 'items' : 1}).items - return map(items, {_,i -> i.text}) +function! s:utils.init.extract(Callback, codeStructures) abort + let bufferTests = map(a:codeStructures, {i, cs -> { + \ 'bufnr': cs[0], + \ 'tests': s:utils.extractTests(cs[0], cs[1]) + \}}) + call OmniSharp#testrunner#SetTests(bufferTests) + let dict = { 'f': a:Callback } + call dict.f(bufferTests) +endfunction + +function! s:utils.log.echo(highlightGroup, message) abort + let messageLines = type(a:message) == type([]) ? a:message : [a:message] + execute 'echohl' a:highlightGroup + for messageLine in messageLines + echomsg messageLine + endfor + echohl None +endfunction + +function! s:utils.log.emphasize(message) abort + call self.echo('Title', a:message) + return 1 +endfunction + +function! s:utils.log.warn(message) abort + call self.echo('WarningMsg', a:message) + return 0 endfunction let &cpoptions = s:save_cpo diff --git a/autoload/OmniSharp/popup.vim b/autoload/OmniSharp/popup.vim index a684277ef..479a4d6d9 100644 --- a/autoload/OmniSharp/popup.vim +++ b/autoload/OmniSharp/popup.vim @@ -318,6 +318,8 @@ function! s:VimOpen(what, opts) abort endif " Prevent popup buffer from being listed in buffer list (`:ls`) call setbufvar(winbufnr(winid), '&buflisted', 0) + " Make wrapping occur at word boundaries + call setwinvar(winid, '&linebreak', 1) return winid endfunction diff --git a/autoload/OmniSharp/preview.vim b/autoload/OmniSharp/preview.vim index 4f5ec532d..0a7399b6c 100644 --- a/autoload/OmniSharp/preview.vim +++ b/autoload/OmniSharp/preview.vim @@ -6,10 +6,10 @@ function! OmniSharp#preview#Display(content, title) abort silent wincmd P setlocal modifiable noreadonly setlocal nobuflisted buftype=nofile bufhidden=wipe - 0,$d + 0,$delete silent put =a:content - 0d_ - setfiletype omnisharpdoc + 0delete _ + set filetype=omnisharpdoc setlocal conceallevel=3 setlocal nomodifiable readonly let winid = winnr() diff --git a/autoload/OmniSharp/stdio.vim b/autoload/OmniSharp/stdio.vim index 034313ece..123d97cf2 100644 --- a/autoload/OmniSharp/stdio.vim +++ b/autoload/OmniSharp/stdio.vim @@ -106,7 +106,8 @@ function! s:HandleServerEvent(job, res) abort " Diagnostics received while running tests if get(a:res, 'Event', '') ==# 'TestMessage' - let lines = split(body.Message, '\n') + let lines = split(body.Message, '\r\?\n') + call OmniSharp#testrunner#Log(lines) for line in lines if get(body, 'MessageLevel', '') ==# 'error' echohl WarningMsg | echomsg line | echohl None diff --git a/autoload/OmniSharp/testrunner.vim b/autoload/OmniSharp/testrunner.vim new file mode 100644 index 000000000..b46802abc --- /dev/null +++ b/autoload/OmniSharp/testrunner.vim @@ -0,0 +1,725 @@ +scriptencoding utf-8 +let s:save_cpo = &cpoptions +set cpoptions&vim + +let s:current = get(s:, 'current', {}) +let s:runner = get(s:, 'runner', {}) +let s:tests = get(s:, 'tests', {}) + +" Expose s:tests for custom scripting +function! OmniSharp#testrunner#GetTests() abort + return s:tests +endfunction + + +" Discover all tests in the project. +" Optional argument: A dict containing the following optional items: +" Callback: funcref to be called after the response is returned +" Display: flag indicating that the tests should immediately be displayed in +" the testrunner +function! OmniSharp#testrunner#Discover(bufnr, ...) abort + if a:0 && type(a:1) == type(function('tr')) + let opts = { 'Callback': a:1 } + else + let opts = a:0 ? a:1 : {} + endif + let opts.Display = get(opts, 'Display', 1) + if !has_key(OmniSharp#GetHost(a:bufnr), 'project') + " Fetch the project structure, then call this function again + call OmniSharp#actions#project#Get(a:bufnr, + \ function('OmniSharp#testrunner#Discover', [a:bufnr, opts])) + return + endif + let project = OmniSharp#GetHost(a:bufnr).project + let opts = { + \ 'ResponseHandler': function('s:DiscoverRH', [a:bufnr, opts]), + \ 'BufNum': a:bufnr, + \ 'Parameters': { + \ 'TargetFrameworkVersion': project['MsBuildProject']['TargetFramework'] + \ }, + \ 'SendBuffer': 0 + \} + call OmniSharp#stdio#Request('/v2/discovertests', opts) +endfunction + +function! s:DiscoverRH(bufnr, opts, response) abort + if !a:response.Success | return | endif + let project = OmniSharp#GetHost(a:bufnr).project + let project.tests = a:response.Body.Tests + if a:opts.Display + " TODO: add tests to testrunner display + endif + if has_key(a:opts, 'Callback') + call a:opts.Callback(a:response) + endif +endfunction + + +function! OmniSharp#testrunner#Debug() abort + let filename = '' + let line = getline('.') + if line =~# '^;' || line =~# '^ \f' + return s:utils.log.warn('Select a test to debug') + else + let test = s:utils.findTest() + if has_key(test, 'filename') + call OmniSharp#actions#test#Debug(0, test.filename, test.name) + endif + endif +endfunction + + +function! OmniSharp#testrunner#Init(buffers) abort + let s:current.log = [] + let s:current.singlebuffer = len(a:buffers) == 1 ? a:buffers[0] : -1 + let s:current.testnames = {} +endfunction + + +function! OmniSharp#testrunner#FoldText() abort + let line = getline(v:foldstart) + if line =~# '^;' + " Project + let projectkey = matchlist(line, '^\S\+')[0] + let [assembly, _] = split(projectkey, ';') + let ntests = 0 + for filename in keys(s:tests[projectkey].files) + let ntests += len(s:tests[projectkey].files[filename].tests) + endfor + let err = match(line, '; ERROR$') == -1 ? '' : ' ERROR' + return printf('%s [%d]%s', assembly, ntests, err) + elseif line =~# '^ \f' + " File + let filename = trim(line) + let displayname = matchlist(filename, '^\f\{-}\([^/\\]\+\)\.csx\?$')[1] + " Position the cursor so that search() is relative to the fold, not the + " actual cursor position + let winview = winsaveview() + call cursor(v:foldstart, 0) + let projectline = search('^;', 'bcnWz') + call winrestview(winview) + let projectkey = matchlist(getline(projectline), '^\S\+')[0] + let ntests = len(s:tests[projectkey].files[filename].tests) + return printf(' %s [%d]', displayname, ntests) + elseif line =~# '^<' + return printf(' Error details (%d lines)', v:foldend - v:foldstart + 1) + elseif line =~# '^>' + return printf(' Results (%d lines)', v:foldend - v:foldstart + 1) + elseif line =~# '^//' + return printf(' Output (%d lines)', v:foldend - v:foldstart + 1) + endif + return printf('%s (%d lines)', line, v:foldend - v:foldstart + 1) +endfunction + +function! OmniSharp#testrunner#Log(message) abort + call extend(s:current.log, a:message) +endfunction + + +function! OmniSharp#testrunner#Run() abort + let filename = '' + let line = getline('.') + if line =~# '^;' + " Project selected - run all tests + let projectkey = matchlist(getline('.'), '^\S\+')[0] + let filenames = filter(keys(s:tests[projectkey].files), + \ {_,f -> s:tests[projectkey].files[f].visible}) + call OmniSharp#actions#test#RunInFile(1, filenames) + elseif line =~# '^ \f' + " File selected + let filename = trim(line) + call OmniSharp#actions#test#RunInFile(0, filename) + return + else + let test = s:utils.findTest() + if has_key(test, 'filename') + call OmniSharp#actions#test#Run(0, test.filename, test.name) + endif + endif +endfunction + + +function! OmniSharp#testrunner#Remove() abort + let filename = '' + let line = getline('.') + if line =~# '^;' + " Project selected - run all tests + let projectkey = matchlist(getline('.'), '^\S\+')[0] + let s:tests[projectkey].visible = 0 + elseif line =~# '^ \f' + " File selected + let filename = trim(line) + let projectline = search('^;', 'bcnWz') + let projectkey = matchlist(getline(projectline), '^\S\+')[0] + let s:tests[projectkey].files[filename].visible = 0 + else + let test = s:utils.findTest() + let test.state = 'hidden' + endif + call s:buffer.paint() +endfunction + + +function! OmniSharp#testrunner#Navigate() abort + if &filetype !=# 'omnisharptest' | return | endif + let bufnr = -1 + let filename = '' + let lnum = -1 + let col = -1 + let line = getline('.') + if line =~# '^;' + " Project selected - do nothing + elseif line =~# '^ \f' + " File selected + let filename = trim(line) + let bufnr = bufnr(filename) + else + " Stack trace with valid location (filename and possible line number) + let parsed = matchlist(line, '^> \+__ .* ___ \(.*\) __ \%(line \(\d\+\)\)\?$') + if len(parsed) + let filename = parsed[1] + if parsed[2] !=# '' + let lnum = str2nr(parsed[2]) + endif + endif + if filename ==# '' + let test = s:utils.findTest() + if has_key(test, 'filename') + let filename = test.filename + let lnum = test.lnum + endif + endif + endif + if bufnr == -1 + if filename !=# '' + let bufnr = bufnr(filename) + if bufnr == -1 + let bufnr = bufadd(filename) + call bufload(bufnr) + endif + endif + if bufnr == -1 | return | endif + endif + for winnr in range(1, winnr('$')) + if winbufnr(winnr) == bufnr + call win_gotoid(win_getid(winnr)) + break + endif + endfor + if bufnr() != bufnr + execute 'aboveleft split' filename + endif + if lnum != -1 + call cursor(lnum, max([col, 0])) + if col == -1 + normal! ^ + endif + endif +endfunction + + +function! OmniSharp#testrunner#Open() abort + if !OmniSharp#actions#test#Validate() | return | endif + call s:Open() +endfunction + +function s:Open() abort + let ft = 'omnisharptest' + let title = 'OmniSharp Test Runner' + " If the buffer is listed in a window in the current tab, then focus it + for winnr in range(1, winnr('$')) + if getbufvar(winbufnr(winnr), '&filetype') ==# ft + call win_gotoid(win_getid(winnr)) + break + endif + endfor + if &filetype !=# ft + " If a buffer with filetype omnisharptest exists, open it in a new split + for buffer in getbufinfo() + if getbufvar(buffer.bufnr, '&filetype') ==# ft + botright split + execute 'buffer' buffer.bufnr + break + endif + endfor + endif + if &filetype !=# ft + botright new + endif + let s:runner.bufnr = bufnr() + let &filetype = ft + execute 'file' title + call s:buffer.paint() +endfunction + + +let s:buffer = {} +function! s:buffer.delimiter() abort + return get(g:, 'OmniSharp_testrunner_banner_delimeter', '─') +endfunction + +function! s:buffer.focus() abort + if !has_key(s:runner, 'bufnr') | return | endif + if getbufvar(s:runner.bufnr, '&ft') !=# 'omnisharptest' | return | endif + " If the buffer is listed in a window in the current tab, then focus it + for winnr in range(1, winnr('$')) + if winbufnr(winnr) == s:runner.bufnr + call win_gotoid(win_getid(winnr)) + return v:true + endif + endfor + return v:false +endfunction + +function! s:buffer.paint() abort + if get(g:, 'OmniSharp_testrunner_banner', 1) + let lines = self.paintbanner() + else + let lines = [] + endif + for key in sort(keys(s:tests)) + if !s:tests[key].visible | continue | endif + call extend(lines, self.paintproject(key)) + for testfile in sort(keys(s:tests[key].files)) + if !s:tests[key].files[testfile].visible | continue | endif + let tests = s:tests[key].files[testfile].tests + call add(lines, ' ' . testfile) + for name in sort(keys(tests), {a,b -> tests[a].lnum > tests[b].lnum}) + let test = tests[name] + call extend(lines, self.painttest(test, len(lines) + 1)) + endfor + call add(lines, '__') + endfor + call add(lines, '') + endfor + + if bufnr() == s:runner.bufnr | let winview = winsaveview() | endif + call setbufvar(s:runner.bufnr, '&modifiable', 1) + call deletebufline(s:runner.bufnr, 1, '$') + call setbufline(s:runner.bufnr, 1, lines) + call setbufvar(s:runner.bufnr, '&modifiable', 0) + call setbufvar(s:runner.bufnr, '&modified', 0) + if bufnr() == s:runner.bufnr + call winrestview(winview) + syn sync fromstart + endif +endfunction + +function! s:buffer.paintbanner() abort + let lines = [] + call add(lines, '`' . repeat(self.delimiter(), 80)) + call add(lines, '` OmniSharp Test Runner') + call add(lines, '` ' . repeat(self.delimiter(), 76)) + call add(lines, '` Toggle this menu (:help omnisharp-testrunner for more)') + call add(lines, '` Run test or tests in file under cursor') + call add(lines, '` Debug test under cursor') + call add(lines, '` Navigate to test or stack trace') + call add(lines, '`' . repeat(self.delimiter(), 80)) + return lines +endfunction + +function! s:buffer.paintproject(key) abort + let [assembly, sln] = split(a:key, ';') + let lines = [] + call add(lines, a:key . (len(s:tests[a:key].errors) ? ' ERROR' : '')) + for errorline in s:tests[a:key].errors + call add(lines, '< ' . trim(errorline, ' ', 2)) + endfor + let loglevel = get(g:, 'OmniSharp_testrunner_loglevel', 'error') + if loglevel ==? 'all' || (loglevel ==? 'error' && len(s:tests[a:key].errors)) + " The diagnostic logs (build output) are only displayed when a single file + " is tested, otherwise multiple build outputs are intermingled + if s:current.singlebuffer != -1 + let [ssln, sass, _] = s:utils.getProject(s:current.singlebuffer) + if ssln ==# sln && sass ==# assembly + if len(s:tests[a:key].errors) > 0 && len(s:current.log) > 1 + call add(lines, '< ' . repeat(self.delimiter(), 10)) + endif + for log in s:current.log + call add(lines, '< ' . trim(log, ' ', 2)) + endfor + endif + endif + endif + return lines +endfunction + +function! s:buffer.repaintproject(key) abort + if !has_key(s:runner, 'bufnr') | return | endif + let projectlines = s:buffer.paintproject(a:key) + call setbufvar(s:runner.bufnr, '&modifiable', 1) + let lines = getbufline(s:runner.bufnr, 1, '$') + let pattern = '^' . substitute(a:key, '/', '\\/', 'g') + let projectline = match(lines, pattern) + 1 + let pattern = '^ ' + let endline = match(lines, '^ ', projectline) + call deletebufline(s:runner.bufnr, projectline, endline) + call appendbufline(s:runner.bufnr, projectline - 1, projectlines) + call setbufvar(s:runner.bufnr, '&modifiable', 0) + call setbufvar(s:runner.bufnr, '&modified', 0) +endfunction + +function! s:buffer.painttest(test, lnum) abort + if a:test.state ==# 'hidden' + return [] + endif + let lines = [] + let state = s:utils.state2char[a:test.state] + let glyph = '' + if state ==# '*' && get(g:, 'OmniSharp_testrunner_glyph', 1) + let glyph = get(g:, 'OmniSharp_testrunner_glyph_passed', '✔') + elseif state ==# '!' && get(g:, 'OmniSharp_testrunner_glyph', 1) + let glyph = get(g:, 'OmniSharp_testrunner_glyph_failed', '✘') + endif + if glyph !=# '' + let glyph = printf('|| %s || ', glyph) + endif + call add(lines, printf('%s %s%s', state, glyph, a:test.name)) + if state ==# '-' && !has_key(a:test, 'spintimer') + call s:spinner.start(a:test, a:lnum) + endif + for messageline in get(a:test, 'message', []) + call add(lines, '> ' . trim(messageline, ' ', 2)) + endfor + for stacktraceline in get(a:test, 'stacktrace', []) + let line = trim(stacktraceline.text) + if has_key(stacktraceline, 'filename') + let line = '__ ' . line . ' ___ ' . stacktraceline.filename . ' __ ' + else + let line = '_._ ' . line . ' _._ ' + endif + if has_key(stacktraceline, 'lnum') + let line .= 'line ' . stacktraceline.lnum + endif + call add(lines, '> ' . line) + endfor + for outputline in get(a:test, 'output', []) + call add(lines, '// ' . trim(outputline, ' ', 2)) + endfor + return lines +endfunction + +function! s:buffer.repainttest(filename, testname, test) abort + if !has_key(s:runner, 'bufnr') | return | endif + call setbufvar(s:runner.bufnr, '&modifiable', 1) + let lines = getbufline(s:runner.bufnr, 1, '$') + let pattern = '^ ' . substitute(a:filename, '/', '\\/', 'g') + let fileline = match(lines, pattern) + 1 + let pattern = '^[-|*!] \%(|| .\{-} || \)\?' . a:testname + let testline = match(lines, pattern, fileline) + 1 + let endline = min( + \ filter( + \ map( + \ ['^[-|*!] \S', '^__$', '^$'], + \ {_,pattern -> match(lines, pattern, testline)}), + \ {_,matchline -> matchline >= testline})) + let testlines = s:buffer.painttest(a:test, testline) + call deletebufline(s:runner.bufnr, testline, endline) + call appendbufline(s:runner.bufnr, testline - 1, testlines) + call setbufvar(s:runner.bufnr, '&modifiable', 0) + call setbufvar(s:runner.bufnr, '&modified', 0) +endfunction + + +function! OmniSharp#testrunner#Reset() abort + let s:current = {} + let s:runner = {} + let s:tests = {} + call OmniSharp#actions#test#Reset() + call s:Open() +endfunction + + +function! OmniSharp#testrunner#SetBreakpoints() abort + if !OmniSharp#util#HasVimspector() + return s:utils.log.warn('Vimspector required to set breakpoints') + endif + let line = getline('.') + " Stack trace with valid location (filename and possible line number) + let parsed = matchlist(line, '^> \+__ .* ___ \(.*\) __ \%(line \(\d\+\)\)\?$') + if len(parsed) && parsed[2] !=# '' + call vimspector#SetLineBreakpoint(parsed[1], str2nr(parsed[2])) + return s:utils.log.emphasize('Break point set') + endif + let test = s:utils.findTest() + if !has_key(test, 'stacktrace') + return s:utils.log.emphasize('No breakpoints added') + endif + let bps = filter(copy(test.stacktrace), + \ "has_key(v:val, 'filename') && has_key(v:val, 'lnum')") + for bp in bps + call vimspector#SetLineBreakpoint(bp.filename, bp.lnum) + endfor + let n = len(bps) + let message = printf('%d break point%s set', n, n == 1 ? '' : 's') + return s:utils.log.emphasize(message) +endfunction + + +function! OmniSharp#testrunner#SetTests(bufferTests) abort + let hasNew = v:false + for buffer in a:bufferTests + let [sln, assembly, key] = s:utils.getProject(buffer.bufnr) + if !has_key(s:tests, key) || !s:tests[key].visible + let hasNew = v:true + endif + let project = get(s:tests, key, { 'files': {}, 'errors': [] }) + let project.visible = 1 + let s:tests[key] = project + let filename = fnamemodify(bufname(buffer.bufnr), ':p') + let testfile = get(project.files, filename, { 'tests': {} }) + if !get(testfile, 'visible', 0) + let hasNew = v:true + endif + let testfile.visible = 1 + let project.files[filename] = testfile + for buffertest in buffer.tests + let name = buffertest.name + if !has_key(testfile.tests, name) + let hasNew = v:true + endif + let test = get(testfile.tests, name, { 'state': 'Not run' }) + let testfile.tests[name] = test + let test.name = name + let test.filename = filename + let test.assembly = assembly + let test.sln = sln + let test.framework = buffertest.framework + let test.lnum = buffertest.nameRange.Start.Line + endfor + endfor + if !get(g:, 'OmniSharp_testrunner', 1) | return | endif + let winid = win_getid() + if hasNew + call s:Open() + call win_gotoid(winid) + elseif s:buffer.focus() + for buffer in a:bufferTests + let filename = fnamemodify(bufname(buffer.bufnr), ':p') + let pattern = '^ ' . substitute(filename, '/', '\\/', 'g') + call search(pattern, 'cw') + normal! 5zo + endfor + call win_gotoid(winid) + endif +endfunction + + +function! s:UpdateState(bufnr, state, ...) abort + let opts = a:0 ? a:1 : {} + let [sln, assembly, key] = s:utils.getProject(a:bufnr) + let s:tests[key].errors = get(opts, 'errors', []) + let filename = fnamemodify(bufname(a:bufnr), ':p') + let tests = s:tests[key].files[filename].tests + for testname in get(opts, 'testnames', s:current.testnames[a:bufnr]) + if has_key(tests, testname) + let stacktrace = [] + for st in get(opts, 'stacktrace', []) + let parsed = matchlist(st, 'at \(.\+\) in \([^:]\+\)\(:line \(\d\+\)\)\?') + if len(parsed) + call add(stacktrace, { + \ 'text': parsed[1], + \ 'filename': parsed[2], + \ 'lnum': str2nr(parsed[4]) + \}) + else + let parsed = matchlist(st, 'at \(.\+\)') + if len(parsed) + call add(stacktrace, {'text': parsed[1]}) + else + call add(stacktrace, {'text': st}) + endif + endif + endfor + let test = tests[testname] + let test.state = a:state + let test.message = get(opts, 'message', []) + let test.stacktrace = stacktrace + let test.output = get(opts, 'output', []) + call s:buffer.repaintproject(key) + call s:buffer.repainttest(filename, testname, test) + endif + endfor + if !get(g:, 'OmniSharp_testrunner', 1) | return | endif + let winid = win_getid() + if s:buffer.focus() + syn sync fromstart + call win_gotoid(winid) + endif +endfunction + +function! OmniSharp#testrunner#StateComplete(location) abort + if get(a:location, 'type', '') ==# 'E' + let state = 'Failed' + elseif get(a:location, 'type', '') ==# 'W' + let state = 'Not run' + else + let state = 'Passed' + endif + call s:UpdateState(a:location.bufnr, state, { + \ 'testnames': [a:location.fullname], + \ 'message': get(a:location, 'message', []), + \ 'stacktrace': get(a:location, 'stacktrace', []), + \ 'output': get(a:location, 'output', []) + \}) +endfunction + +function! OmniSharp#testrunner#StateError(bufnr, messages) abort + call s:UpdateState(a:bufnr, 'Not run', {'errors': a:messages}) +endfunction + +function! OmniSharp#testrunner#StateRunning(bufnr, testnames) abort + let testnames = type(a:testnames) == type([]) ? a:testnames : [a:testnames] + let s:current.testnames[a:bufnr] = testnames + call s:UpdateState(a:bufnr, 'Running', {'testnames': testnames}) +endfunction + +function! OmniSharp#testrunner#StateSkipped(bufnr) abort + call s:UpdateState(a:bufnr, 'Not run') +endfunction + + +function! OmniSharp#testrunner#ToggleBanner() abort + let g:OmniSharp_testrunner_banner = 1 - get(g:, 'OmniSharp_testrunner_banner', 1) + if s:buffer.focus() + let displayed = getline(1) =~# '`' + call setbufvar(s:runner.bufnr, '&modifiable', 1) + if g:OmniSharp_testrunner_banner && !displayed + call appendbufline(s:runner.bufnr, 0, s:buffer.paintbanner()) + elseif !g:OmniSharp_testrunner_banner && displayed + call deletebufline(s:runner.bufnr, 1, len(s:buffer.paintbanner())) + endif + call setbufvar(s:runner.bufnr, '&modifiable', 0) + call setbufvar(s:runner.bufnr, '&modified', 0) + endif +endfunction + + +let s:spinner = {} +let s:spinner.steps_ascii = [ +\ '<*---->', +\ '<-*--->', +\ '<--*-->', +\ '<---*->', +\ '<----*>', +\ '<---*->', +\ '<--*-->', +\ '<-*--->' +\] +let s:spinner.steps_utf8 = [ +\ '∙∙∙', +\ '●∙∙', +\ '∙●∙', +\ '∙∙●', +\ '∙∙∙' +\] + +function! s:spinner.spin(test, lnum, timer) abort + if s:utils.state2char[a:test.state] !=# '-' + call timer_stop(a:timer) + endif + let lnum = a:lnum + (get(g:, 'OmniSharp_testrunner_banner', 1) ? 8 : 0) + let lines = getbufline(s:runner.bufnr, lnum) + if len(lines) == 0 + call timer_stop(a:timer) + return + endif + " TODO: find the test by name, instead of line number + let line = lines[0] + let steps = get(g:, 'OmniSharp_testrunner_spinnersteps', + \ get(g:, 'OmniSharp_testrunner_spinner_ascii') + \ ? self.steps_ascii : self.steps_utf8) + if !has_key(a:test.spinner, 'index') + " Starting + let line .= ' -- ' . steps[0] + let a:test.spinner.index = 0 + elseif s:utils.state2char[a:test.state] !=# '-' + " Stopping + let line = substitute(line, ' -- .*$', '', '') + else + " Stepping + let a:test.spinner.index += 1 + if a:test.spinner.index >= len(steps) + let a:test.spinner.index = 0 + endif + let step = steps[a:test.spinner.index] + let line = substitute(line, ' -- \zs.*$', step, '') + endif + call setbufvar(s:runner.bufnr, '&modifiable', 1) + call setbufline(s:runner.bufnr, lnum, line) + call setbufvar(s:runner.bufnr, '&modifiable', 0) + call setbufvar(s:runner.bufnr, '&modified', 0) +endfunction + +function! s:spinner.start(test, lnum) abort + if !get(g:, 'OmniSharp_testrunner_spinner', 1) | return | endif + let lnum = a:lnum - (get(g:, 'OmniSharp_testrunner_banner', 1) ? 8 : 0) + let a:test.spinner = {} + let a:test.spinner.timer = timer_start(300, + \ funcref('s:spinner.spin', [a:test, lnum], self), + \ {'repeat': -1}) +endfunction + + +let s:utils = {} +let s:utils.log = {} + +let s:utils.state2char = { +\ 'Not run': '|', +\ 'Running': '-', +\ 'Passed': '*', +\ 'Failed': '!' +\} + +function! s:utils.findTest() abort + if &filetype !=# 'omnisharptest' | return {} | endif + let testpattern = '[-|*!] \S' + let line = getline('.') + if line =~# testpattern + let testline = line('.') + else + let testline = search(testpattern, 'bcnWz') + endif + if testline > 0 + let line = getline(testline) + let testname = matchlist(line, '[-|*!] \%(|| .\{-} || \)\?\zs\S*')[0] + let projectline = search('^;', 'bcnWz') + let projectkey = matchlist(getline(projectline), '^\S\+')[0] + let fileline = search('^ \f', 'bcnWz') + let filename = matchlist(getline(fileline), '^ \zs.*$')[0] + return s:tests[projectkey].files[filename].tests[testname] + endif + return {} +endfunction + +function! s:utils.getProject(bufnr) abort + let host = OmniSharp#GetHost(a:bufnr) + let msbuildproject = get(host.project, 'MsBuildProject', {}) + let sln = host.sln_or_dir + let assembly = get(msbuildproject, 'AssemblyName', '_Default') + return [sln, assembly, printf(';%s;%s;', assembly, sln)] +endfunction + +function! s:utils.log.echo(highlightGroup, message) abort + let messageLines = type(a:message) == type([]) ? a:message : [a:message] + execute 'echohl' a:highlightGroup + for messageLine in messageLines + echomsg messageLine + endfor + echohl None +endfunction + +function! s:utils.log.emphasize(message) abort + call self.echo('Title', a:message) + return 1 +endfunction + +function! s:utils.log.warn(message) abort + call self.echo('WarningMsg', a:message) + return 0 +endfunction + +let &cpoptions = s:save_cpo +unlet s:save_cpo + +" vim:et:sw=2:sts=2 diff --git a/autoload/OmniSharp/util.vim b/autoload/OmniSharp/util.vim index 897af5c8e..ada389f3c 100644 --- a/autoload/OmniSharp/util.vim +++ b/autoload/OmniSharp/util.vim @@ -46,7 +46,11 @@ function! OmniSharp#util#AwaitParallel(Funcs, OnAllComplete) abort \ 'OnAllComplete': a:OnAllComplete \} for Func in a:Funcs - call Func(function('s:AwaitFuncComplete', [state])) + " If the Func has been declared as a dictionary function, then it must be + " called as a dictionary function: + " function s:run.test() + let dict = { 'f': Func } + call dict.f(function('s:AwaitFuncComplete', [state])) endfor endfunction @@ -64,9 +68,12 @@ function! OmniSharp#util#AwaitSequence(Funcs, OnAllComplete, ...) abort \} endif - let Func = remove(a:Funcs, 0) + " If the Func has been declared as a dictionary function, then it must be + " called as a dictionary function: + " function s:run.test() + let dict = { 'f': remove(a:Funcs, 0) } let state.OnComplete = function('OmniSharp#util#AwaitSequence', [a:Funcs, a:OnAllComplete]) - call Func(function('s:AwaitFuncComplete', [state])) + call dict.f(function('s:AwaitFuncComplete', [state])) endfunction function! s:AwaitFuncComplete(state, ...) abort diff --git a/doc/omnisharp-vim.txt b/doc/omnisharp-vim.txt index 79fe7dbbe..c736230bb 100644 --- a/doc/omnisharp-vim.txt +++ b/doc/omnisharp-vim.txt @@ -20,12 +20,15 @@ CONTENTS *omnisharp-contents 3.2 Diagnostics ......................... |omnisharp-diagnostic-options| 3.3 Highlights .......................... |omnisharp-highlight-options| 3.4 Popups .............................. |omnisharp-popup-options| - 3.5 Tests ............................... |omnisharp-test-options| 3.6 Integrations ........................ |omnisharp-integration-options| 3.7 Miscellaneous ....................... |omnisharp-miscellaneous-options| - 4. Commands ............................... |omnisharp-commands| + 4. General Commands ....................... |omnisharp-commands| 5. Autocmds ............................... |omnisharp-autocmds| - 6. Integrations ........................... |omnisharp-integrations| + 6. Test runner ............................ |omnisharp-testrunner| + 6.1 Options ............................. |omnisharp-test-options| + 6.1 Commands ............................ |omnisharp-testrunner-commands| + 6.2 Mappings ............................ |omnisharp-testrunner-mappings| + 7. Integrations ........................... |omnisharp-integrations| =============================================================================== 1. DEPENDENCIES *omnisharp-dependencies* @@ -43,7 +46,7 @@ Optional:~ 2. USAGE *omnisharp-usage* Opening a `*.cs` file will automatically start an instance of omnisharp-server -(if using vim 8.0+, neovim or vim-dispatch). Symantic completions are triggered +(if using vim 8.0+, neovim or vim-dispatch). Semantic completions are triggered using omni-completion (CTRL-X_CTRL-O in insert mode), or using an autocompletion plugin such as asyncomplete or Deoplete. @@ -371,26 +374,7 @@ Default: atcursor > let g:OmniSharp_popup_position = 'peek' < ------------------------------------------------------------------------------- -3.5 TESTS *omnisharp-test-options* - - *g:OmniSharp_runtests_echo_output* -When running unit tests with |:OmniSharpRunTest| and |:OmniSharpRunTestsInFile|, -echo all test runner output to the |message-history|, so it can be viewed with -|:messages|. -Default: 1 - - *g:OmniSharp_runtests_parallel* -When running multiple unit test files with |:OmniSharpRunTestsInFile|, run -tests in all files simultaneously - this is the fastest way to run multiple -test files. The disadvantage is that when |g:OmniSharp_runtests_echo_output| -is set to 1, tests from multiple files will be interspersed, and therefore -difficult to read. Set |g:OmniSharp_runtests_parallel| to 0 in order to run -test files in sequence instead, resulting in readable output in the -|message-history|. -Default: 1 - -------------------------------------------------------------------------------- -3.6 INTEGRATIONS *omnisharp-integration-options* +3.5 INTEGRATIONS *omnisharp-integration-options* *g:OmniSharp_selector_ui* Use this option to specify a selector UI for choosing code actions and @@ -451,7 +435,7 @@ Use this option to enable syntastic integration > let g:syntastic_cs_checkers = ['code_checker'] < ------------------------------------------------------------------------------- -3.7 MISCELLANEOUS *omnisharp-miscellaneous-options* +3.6 MISCELLANEOUS *omnisharp-miscellaneous-options* *g:OmniSharp_filename_modifiers* File paths returned from the server are normalized using Vim @@ -499,7 +483,7 @@ the |:OmniSharpDocumentation| command. Default: 1 > let g:omnicomplete_fetch_full_documentation = 1 < =============================================================================== -4. COMMANDS *omnisharp-commands* +4. GENERAL COMMANDS *omnisharp-commands* Most of the OmniSharp-vim commands have associated plug mappings defined, for convenient user re-mapping. These can be used like so: > @@ -517,8 +501,8 @@ convenient user re-mapping. These can be used like so: > :OmniSharpGotoDefinition vsplit :OmniSharpGotoDefinition tabedit < - *:OmniSharpGotoTypeDefinition* - *(omnisharp_go_to_type_definition)* + *:OmniSharpGotoTypeDefinition* + *(omnisharp_go_to_type_definition)* :OmniSharpGotoTypeDefinition [{cmd}] Navigates to the type definition of the symbol under the cursor. By default the definition is opened in the current window. To open it in a @@ -603,44 +587,6 @@ convenient user re-mapping. These can be used like so: > :OmniSharpNavigateDown Navigates to next method or class - *:OmniSharpRunTest* - *(omnisharp_run_test)* - *(omnisharp_run_test_no_build)* -:OmniSharpRunTest[!] - Run the current unit test. The cursor can be anywhere in the test method. - If the test fails, the failure message and location will be displayed in - the quickfix list, along with the error stack trace, if one exists. - - When called with ! or the |(omnisharp_run_test_no_build)| mapping, the - project is not built before running the test. - - *:OmniSharpDebugTest* - *(omnisharp_debug_test)* - *(omnisharp_debug_test_no_build)* -:OmniSharpDebugTest[!] - Debug the current unit test with Vimspector. The cursor can be anywhere in - the test method. The quickfix list is not populated with the test result. - - When called with ! or the |(omnisharp_debug_test_no_build)| mapping, - the project is not built before starting the debugger. - - *:OmniSharpRunTestsInFile* - *(omnisharp_run_tests_in_file)* - *(omnisharp_run_tests_in_file_no_build)* -:OmniSharpRunTestsInFile[!] - Run all unit tests in the current file. The quickfix list will be - populated with the test results of all tests. - Optionally accepts one or more filenames of files to run tests for. -> - " Run all unit tests in the current file - :OmniSharpRunTestsInFile - - " Run all unit tests in the current file, and file `tests/test1.cs` - :OmniSharpRunTestsInFile % tests/test1.cs -< - When called with ! or the |(omnisharp_run_tests_in_file_no_build)| - mapping, the project is not built before running the tests. - *:OmniSharpOpenLog* *(omnisharp_open_log)* :OmniSharpOpenLog [{cmd}] @@ -794,7 +740,199 @@ OmniSharpStopped Fired when the server process is stopped. =============================================================================== -6. INTEGRATIONS *omnisharp-integrations* +6. TEST RUNNER *omnisharp-testrunner* + +The test runner provides an overview of unit tests which have been run, along +with the test status (running/passed/failed/not run) and any outputs that may +be produced (exceptions and Console output). + +Open the test runner window by either running a test (with e.g. +|:OmniSharpRunTest|) or with the |:OmniSharpTestRunner| command. + +Any time a new test is run, it (along with all other tests in the same file) +is added to the runner. Tests remain visible in the test runner window until +they are manually removed. + +Tests are grouped under the files they are contained in, which are in turn +grouped by project. Folds are used to collapse and expand sections, so a +project/file/output section can be hidden or displayed using standard vim +|folding-commands|. + +To disable the testrunner and instead use the quickfix list to navigate and +display test results, use the following settings: > + let g:OmniSharp_testrunner = 0 + let g:OmniSharp_runtests_quickfix = 1 +< +------------------------------------------------------------------------------- +6.1 OPTIONS *omnisharp-test-options* + + *g:OmniSharp_runtests_echo_output* +When running unit tests with |:OmniSharpRunTest| and |:OmniSharpRunTestsInFile| +or from the |omnisharp-testrunner|, echo all test runner output to the +|message-history|, so it can be viewed with |:messages|. +Default: 0 + + *g:OmniSharp_runtests_parallel* +When running multiple unit test files with |:OmniSharpRunTestsInFile|, run +tests in all files simultaneously - this is the fastest way to run multiple +test files. The disadvantage is that when |g:OmniSharp_runtests_echo_output| +is set to 1, tests from multiple files will be interspersed, and therefore +difficult to read. Set |g:OmniSharp_runtests_parallel| to 0 in order to run +test files in sequence instead, resulting in readable output in the +|message-history|. +Default: 1 + + *g:OmniSharp_runtests_quickfix* +When running unit tests with |:OmniSharpRunTest| and |:OmniSharpRunTestsInFile| +or from the |omnisharp-testrunner|, populate the quickfix list with test +results. +When a single test is run and an exception occurs, the quickfix will be +populated with the exception stack trace. +When multiple tests are run then each test will get a quickfix location: +failed test locations point to the failed assertion; successful test locations +point to the test method declaration. +Default: 0 + + *g:OmniSharp_testrunner* +Open the |omnisharp-testrunner| automatically when running tests. +Default: 1 + + *g:OmniSharp_testrunner_banner* +Display the |omnisharp-testrunner| help/introduction banner when opening the +testrunner. When this option is set to 0, the banner may still be toggled with +the (default) toggle mapping. +Default: 1 + + *g:OmniSharp_testrunner_glyph* +Display a passed/failed "glyph" string beside completed test results in the +|omnisharp-testrunner|. +Default: 1 + + *g:OmniSharp_testrunner_glyph_failed* +The "glyph" string to display beside failed completed tests in the testrunner. +Default: ✘ + + *g:OmniSharp_testrunner_glyph_passed* +The "glyph" string to display beside passed completed tests in the testrunner. +Default: ✔ + + *g:OmniSharp_testrunner_loglevel* +The type of build and test logs to output in the |omnisharp-testrunner|. + + all All build output and test runner output is displayed. + + error When a build error occurs, the error message and stack + trace are output. + + none Only build error messages will be output. + +Default: error > + let g:OmniSharp_testrunner_loglevel = 'all' +< + *g:OmniSharp_testrunner_spinner* +Display a "running" spinner animation when running tests in the testrunner. +Default: 1 + + *g:OmniSharp_testrunner_spinnersteps* +A list of "step" strings to be displayed as the |omnisharp-testrunner| +"spinner" animation. +Default: ['∙∙∙', '●∙∙', '∙●∙', '∙∙●', '∙∙∙'] + +------------------------------------------------------------------------------- +6.2 COMMANDS *omnisharp-testrunner-commands* + + *:OmniSharpRunTest* + *(omnisharp_run_test)* + *(omnisharp_run_test_no_build)* +:OmniSharpRunTest[!] + Run the current unit test. The cursor can be anywhere in the test method. + The |omnisharp-testrunner| window will be opened to display the test + status and results. + If the test fails, the failure message and location will be displayed in + the testrunner, or optionally (see |g:OmniSharp_runtests_quickfix|) in the + quickfix list, along with the error stack trace if one exists. + + When called with ! or the |(omnisharp_run_test_no_build)| mapping, the + project is not built before running the test. + + *:OmniSharpDebugTest* + *(omnisharp_debug_test)* + *(omnisharp_debug_test_no_build)* +:OmniSharpDebugTest[!] + Debug the current unit test with Vimspector. The cursor can be anywhere in + the test method. + + When called with ! or the |(omnisharp_debug_test_no_build)| mapping, + the project is not built before starting the debugger. + + *:OmniSharpRunTestsInFile* + *(omnisharp_run_tests_in_file)* + *(omnisharp_run_tests_in_file_no_build)* +:OmniSharpRunTestsInFile[!] + Run all unit tests in the current file. When |g:OmniSharp_runtests_quickfix| + is enabled, the quickfix list will be populated with the results of all + tests. + Optionally accepts one or more filenames of files to run tests for. +> + " Run all unit tests in the current file + :OmniSharpRunTestsInFile + + " Run all unit tests in the current file, and file `tests/test1.cs` + :OmniSharpRunTestsInFile % tests/test1.cs +< + When called with ! or the |(omnisharp_run_tests_in_file_no_build)| + mapping, the project is not built before running the tests. + + *:OmniSharpTestRunner* + Open the |omnisharp-testrunner| window + + *:OmniSharpTestRunnerReset* + Forget all test history and clear the test runner window + +------------------------------------------------------------------------------- +6.3 MAPPINGS *omnisharp-testrunner-mappings* + +The following || mappings and associated default recursive mappings are +provided in the test runner window. + + *(omnisharp_testrunner_navigate)* + Default: +Navigate to the test under the cursor. When the used on a error stack trace +location in a failed test result, navigate to the stack location. + + *(omnisharp_testrunner_togglebanner)* + Default: +Toggle display of the help/intro banner. Note that the banner can be hidden +initially using the |g:OmniSharp_testrunner_banner| setting. + + *(omnisharp_testrunner_run)* + Default: +Run the test(s) under the cursor. When the cursor is on a line representing a +single test (the test itself or part of its output) then just that single test +is run. When the cursor is on a file name, then all tests in the file are run. +When the cursor is on a project name, all previously run tests in the project +will be run. +Note: Running tests in a project only runs previous run tests. This command +does not automatically discover tests in the project. + + *(omnisharp_testrunner_debug)* + Default: +Debug the test under the cursor in Vimspector. + + *(omnisharp_testrunner_set_breakpoints)* + Default: +When used on a failed test resulting in an error stack, the stack trace +locations are set in Vimspector as breakpoints. + + *(omnisharp_testrunner_remove)* + Default: dd +Remove the item under the cursor from the test runner. Can be used at the +test, file or project level. Note however that individual tests cannot be +completely removed: subsequent runs of tests in the containing file/project +will still run the removed test. + +=============================================================================== +7. INTEGRATIONS *omnisharp-integrations* 6.1 fzf, vim-clap, CtrlP, unite.vim~ diff --git a/ftplugin/omnisharptest/OmniSharp.vim b/ftplugin/omnisharptest/OmniSharp.vim new file mode 100644 index 000000000..554198d2e --- /dev/null +++ b/ftplugin/omnisharptest/OmniSharp.vim @@ -0,0 +1,31 @@ +set bufhidden=hide +set noswapfile +set conceallevel=3 +set concealcursor=nv +set foldlevel=2 +set foldmethod=syntax +set signcolumn=no +set synmaxcol=3000 + +nnoremap (omnisharp_testrunner_togglebanner) :call OmniSharp#testrunner#ToggleBanner() +nnoremap (omnisharp_testrunner_run) :call OmniSharp#testrunner#Run() +nnoremap (omnisharp_testrunner_debug) :call OmniSharp#testrunner#Debug() +nnoremap (omnisharp_testrunner_set_breakpoints) :call OmniSharp#testrunner#SetBreakpoints() +nnoremap (omnisharp_testrunner_navigate) :call OmniSharp#testrunner#Navigate() +nnoremap (omnisharp_testrunner_remove) :call OmniSharp#testrunner#Remove() + +function! s:map(mode, lhs, plug) abort + let l:rhs = '(' . a:plug . ')' + if !hasmapto(l:rhs, substitute(a:mode, 'x', 'v', '')) + execute a:mode . 'map ' a:lhs l:rhs + endif +endfunction + +call s:map('n', '', 'omnisharp_testrunner_togglebanner') +call s:map('n', '', 'omnisharp_testrunner_run') +call s:map('n', '', 'omnisharp_testrunner_debug') +call s:map('n', '', 'omnisharp_testrunner_set_breakpoints') +call s:map('n', '', 'omnisharp_testrunner_navigate') +call s:map('n', 'dd', 'omnisharp_testrunner_remove') + +set foldtext=OmniSharp#testrunner#FoldText() diff --git a/plugin/OmniSharp.vim b/plugin/OmniSharp.vim index 77e04a50f..a088ff573 100644 --- a/plugin/OmniSharp.vim +++ b/plugin/OmniSharp.vim @@ -39,7 +39,7 @@ let g:OmniSharp_loglevel = get(g:, 'OmniSharp_loglevel', defaultlevel) let g:OmniSharp_diagnostic_listen = get(g:, 'OmniSharp_diagnostic_listen', 2) let g:OmniSharp_runtests_parallel = get(g:, 'OmniSharp_runtests_parallel', 1) -let g:OmniSharp_runtests_echo_output = get(g:, 'OmniSharp_runtests_echo_output', 1) +let g:OmniSharp_runtests_echo_output = get(g:, 'OmniSharp_runtests_echo_output', 0) " Set to 1 when ultisnips is installed let g:OmniSharp_want_snippet = get(g:, 'OmniSharp_want_snippet', 0) @@ -55,6 +55,8 @@ let g:omnicomplete_fetch_full_documentation = get(g:, 'omnicomplete_fetch_full_d command! -bar -nargs=? OmniSharpInstall call OmniSharp#Install() command! -bar -nargs=? OmniSharpOpenLog call OmniSharp#log#Open() command! -bar -bang OmniSharpStatus call OmniSharp#Status(0) +command! -bar OmniSharpTestRunner call OmniSharp#testrunner#Open() +command! -bar OmniSharpTestRunnerReset call OmniSharp#testrunner#Reset() " Preserve backwards compatibility with older version g:OmniSharp_highlight_types let g:OmniSharp_highlighting = get(g:, 'OmniSharp_highlighting', get(g:, 'OmniSharp_highlight_types', 2)) diff --git a/syntax/omnisharptest.vim b/syntax/omnisharptest.vim new file mode 100644 index 000000000..357fd7677 --- /dev/null +++ b/syntax/omnisharptest.vim @@ -0,0 +1,99 @@ +if exists('b:current_syntax') + finish +endif + +let s:save_cpo = &cpoptions +set cpoptions&vim + +syn region ostBanner start="\%1l" end="\%8l$" contains=ostBannerDelim,ostBannerTitle,ostBannerHelp transparent keepend +syn match ostBannerHelp "^` .*$" contained contains=ostBannerMap,ostBannerLink,ostBannerPrefix +syn match ostBannerMap "^` \S\+" contained contains=ostBannerPrefix +syn match ostBannerLink ":help [[:alnum:]-]\+" contained +syn match ostBannerTitle "\%2l^`.\+$" contained contains=ostBannerPrefix +syn match ostBannerDelim "\%1l^`.*$" contained contains=ostBannerPrefix +syn match ostBannerDelim "\%3l^`.*$" contained contains=ostBannerPrefix +syn match ostBannerDelim "\%8l^`.*$" contained contains=ostBannerPrefix +syn match ostBannerPrefix "^`" conceal contained + +syn match ostProjectKey ";[^;]*;[^;]*;.*" contains=ostSolution,ostAssembly,ostProjectDelimiter,ostProjectError +syn match ostSolution "\%(^;[^;]\+;\)\@<=[^;]\+" contained conceal +syn match ostAssembly "\%(^;\)\@<=[^;]\+\ze;[^;]\+;" contained +syn match ostProjectDelimiter ";" contained conceal +syn match ostProjectError "ERROR$" contained +syn region ostProject start="^;" end="^$"me=s-1 contains=TOP transparent fold + +syn region ostError start="^<" end="^[^<]"me=s-1 contains=ostErrorPrefix,ostStackFile,ostStackFileNoLoc fold +syn match ostErrorPrefix "^<" conceal contained +syn match ostFileName "^ \S.*" contains=ostFilePath,ostFileExt +syn match ostFilePath "\%(^ \)\@<=\f\{-}\ze[^/\\]\+\.csx\?$" conceal contained +syn match ostFileExt "\.csx\?$" conceal contained +syn region ostFile start="^ \S.*" end="^__$"me=s-1 contains=TOP transparent fold +syn match ostFileDivider "^__$" conceal + +syn match ostStateNotRun "^|.*" contains=ostStatePrefix,ostTestNamespace +syn match ostStateRunning "^-.*" contains=ostStatePrefix,ostTestNamespace,ostRunningSuffix +syn match ostStatePassed "^\*.*" contains=ostStatePrefix,ostTestNamespace,ostStatePassedGlyph,ostCompletePrefixDivider +syn match ostStateFailed "^!.*" contains=ostStatePrefix,ostTestNamespace,ostStateFailedGlyph,ostCompletePrefixDivider +syn match ostStatePrefix "^[|\*!-]" conceal contained +syn match ostTestNamespace "\%(\w\+\.\)*\ze\w\+" conceal contained + +syn match ostRunningSuffix " -- .*" contained contains=ostRunningSpinner,ostRunningSuffixDivider +syn match ostRunningSuffixDivider " \zs--" conceal contained +syn match ostRunningSpinner " -- \zs.*" contained + +syn match ostStatePassedGlyph "\%(|| \)\@<=.\{-}\ze || " contained +syn match ostStateFailedGlyph "\%(|| \)\@<=.\{-}\ze || " contained +syn match ostCompletePrefixDivider "|| " conceal contained + +syn region ostFailure start="^>" end="^[^>]"me=s-1 contains=ostFailurePrefix,ostStackLoc,ostStackNoLoc fold +syn match ostFailurePrefix "^>" conceal contained +syn region ostStackLoc start=" __ "hs=e+1 end=" __ "he=e-1 contains=ostStackFile,ostStackDelimiter,ostStackNamespace contained keepend +syn region ostStackFile start=" ___ " end=" __ "he=e-1 contains=ostStackFileDelimiter,ostStackDelimiter conceal contained +syn match ostStackDelimiter " __ "he=e-1 conceal contained +syn match ostStackFileDelimiter " ___ " conceal contained +syn region ostStackNoLoc start=" _._ "hs=e+1 end=" _._" contains=ostStackNoLocDelimiter,ostStackNamespace contained keepend +syn match ostStackNoLocDelimiter " _._" conceal contained +syn match ostStackNamespace "\%(\w\+\.\)*\ze\w\+\.\w\+(" conceal contained +syn region ostOutput start="^//" end="^[^/]"me=s-1 contains=ostOutputPrefix fold +syn match ostOutputPrefix "^//" conceal contained + +hi def link ostBannerDelim PreProc +hi def link ostBannerTitle Normal +hi def link ostBannerHelp Comment +hi def link ostBannerMap PreProc +hi def link ostBannerLink Identifier +hi def link ostAssembly Identifier +hi def link ostSolution Normal +hi def link ostProjectError WarningMsg +hi def link ostFileName TypeDef +hi def link ostStateNotRun Comment +hi def link ostStateRunning Identifier +hi def link ostRunningSpinner Normal +hi def link ostStatePassed Title +hi def link ostStatePassedGlyph Title +hi def link ostStateFailed WarningMsg +hi def link ostStateFailedGlyph WarningMsg +hi def link ostStackLoc Identifier +hi def link ostOutput Comment + +" Highlights for normally concealed elements +hi def link ostBannerPrefix NonText +hi def link ostProjectDelimiter NonText +hi def link ostErrorPrefix NonText +hi def link ostFileDivider NonText +hi def link ostStatePrefix NonText +hi def link ostFailurePrefix NonText +hi def link ostRunningSuffixDivider NonText +hi def link ostCompletePrefixDivider NonText +hi def link ostStackDelimiter NonText +hi def link ostStackFileDelimiter NonText +hi def link ostStackNoLocDelimiter NonText +hi def link ostOutputPrefix NonText +hi def link ostStackFile WarningMsg + +let b:current_syntax = 'omnisharptest' + +let &cpoptions = s:save_cpo +unlet s:save_cpo + +" vim:et:sw=2:sts=2