coverage.vim 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. let s:toggle = 0
  2. " Buffer creates a new cover profile with 'go test -coverprofile' and changes
  3. " the current buffers highlighting to show covered and uncovered sections of
  4. " the code. If run again it clears the annotation.
  5. function! go#coverage#BufferToggle(bang, ...) abort
  6. if s:toggle
  7. call go#coverage#Clear()
  8. return
  9. endif
  10. if a:0 == 0
  11. return call(function('go#coverage#Buffer'), [a:bang])
  12. endif
  13. return call(function('go#coverage#Buffer'), [a:bang] + a:000)
  14. endfunction
  15. " Buffer creates a new cover profile with 'go test -coverprofile' and changes
  16. " the current buffers highlighting to show covered and uncovered sections of
  17. " the code. Calling it again reruns the tests and shows the last updated
  18. " coverage.
  19. function! go#coverage#Buffer(bang, ...) abort
  20. " we use matchaddpos() which was introduce with 7.4.330, be sure we have
  21. " it: http://ftp.vim.org/vim/patches/7.4/7.4.330
  22. if !exists("*matchaddpos")
  23. call go#util#EchoError("GoCoverage is supported with Vim version 7.4-330 or later")
  24. return -1
  25. endif
  26. " check if there is any test file, if not we just return
  27. let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd '
  28. let dir = getcwd()
  29. try
  30. execute cd . fnameescape(expand("%:p:h"))
  31. if empty(glob("*_test.go"))
  32. call go#util#EchoError("no test files available")
  33. return
  34. endif
  35. finally
  36. execute cd . fnameescape(dir)
  37. endtry
  38. let s:toggle = 1
  39. let l:tmpname = tempname()
  40. if get(g:, 'go_echo_command_info', 1)
  41. echon "vim-go: " | echohl Identifier | echon "testing ..." | echohl None
  42. endif
  43. if go#util#has_job()
  44. call s:coverage_job({
  45. \ 'cmd': ['go', 'test', '-coverprofile', l:tmpname] + a:000,
  46. \ 'complete': function('s:coverage_callback', [l:tmpname]),
  47. \ 'bang': a:bang,
  48. \ 'for': 'GoTest',
  49. \ })
  50. return
  51. endif
  52. let args = [a:bang, 0, "-coverprofile", l:tmpname]
  53. if a:0
  54. call extend(args, a:000)
  55. endif
  56. let disabled_term = 0
  57. if get(g:, 'go_term_enabled')
  58. let disabled_term = 1
  59. let g:go_term_enabled = 0
  60. endif
  61. let id = call('go#test#Test', args)
  62. if disabled_term
  63. let g:go_term_enabled = 1
  64. endif
  65. if has('nvim')
  66. call go#jobcontrol#AddHandler(function('s:coverage_handler'))
  67. let s:coverage_handler_jobs[id] = l:tmpname
  68. return
  69. endif
  70. if go#util#ShellError() == 0
  71. call go#coverage#overlay(l:tmpname)
  72. endif
  73. call delete(l:tmpname)
  74. endfunction
  75. " Clear clears and resets the buffer annotation matches
  76. function! go#coverage#Clear() abort
  77. call clearmatches()
  78. if exists("s:toggle") | let s:toggle = 0 | endif
  79. " remove the autocmd we defined
  80. augroup vim-go-coverage
  81. autocmd!
  82. augroup end
  83. endfunction
  84. " Browser creates a new cover profile with 'go test -coverprofile' and opens
  85. " a new HTML coverage page from that profile in a new browser
  86. function! go#coverage#Browser(bang, ...) abort
  87. let l:tmpname = tempname()
  88. if go#util#has_job()
  89. call s:coverage_job({
  90. \ 'cmd': ['go', 'test', '-coverprofile', l:tmpname],
  91. \ 'complete': function('s:coverage_browser_callback', [l:tmpname]),
  92. \ 'bang': a:bang,
  93. \ 'for': 'GoTest',
  94. \ })
  95. return
  96. endif
  97. let args = [a:bang, 0, "-coverprofile", l:tmpname]
  98. if a:0
  99. call extend(args, a:000)
  100. endif
  101. let id = call('go#test#Test', args)
  102. if has('nvim')
  103. call go#jobcontrol#AddHandler(function('s:coverage_browser_handler'))
  104. let s:coverage_browser_handler_jobs[id] = l:tmpname
  105. return
  106. endif
  107. if go#util#ShellError() == 0
  108. let openHTML = 'go tool cover -html='.l:tmpname
  109. call go#tool#ExecuteInDir(openHTML)
  110. endif
  111. call delete(l:tmpname)
  112. endfunction
  113. " Parses a single line from the cover file generated via go test -coverprofile
  114. " and returns a single coverage profile block.
  115. function! go#coverage#parsegocoverline(line) abort
  116. " file:startline.col,endline.col numstmt count
  117. let mx = '\([^:]\+\):\(\d\+\)\.\(\d\+\),\(\d\+\)\.\(\d\+\)\s\(\d\+\)\s\(\d\+\)'
  118. let tokens = matchlist(a:line, mx)
  119. let ret = {}
  120. let ret.file = tokens[1]
  121. let ret.startline = str2nr(tokens[2])
  122. let ret.startcol = str2nr(tokens[3])
  123. let ret.endline = str2nr(tokens[4])
  124. let ret.endcol = str2nr(tokens[5])
  125. let ret.numstmt = tokens[6]
  126. let ret.cnt = tokens[7]
  127. return ret
  128. endfunction
  129. " Generates matches to be added to matchaddpos for the given coverage profile
  130. " block
  131. function! go#coverage#genmatch(cov) abort
  132. let color = 'goCoverageCovered'
  133. if a:cov.cnt == 0
  134. let color = 'goCoverageUncover'
  135. endif
  136. let matches = []
  137. " if start and end are the same, also specify the byte length
  138. " example: foo.go:92.2,92.65 1 0
  139. if a:cov.startline == a:cov.endline
  140. call add(matches, {
  141. \ 'group': color,
  142. \ 'pos': [[a:cov.startline, a:cov.startcol, a:cov.endcol - a:cov.startcol]],
  143. \ 'priority': 2,
  144. \ })
  145. return matches
  146. endif
  147. " add start columns. Because we don't know the length of the of
  148. " the line, we assume it is at maximum 200 bytes. I know this is hacky,
  149. " but that's only way of fixing the issue
  150. call add(matches, {
  151. \ 'group': color,
  152. \ 'pos': [[a:cov.startline, a:cov.startcol, 200]],
  153. \ 'priority': 2,
  154. \ })
  155. " and then the remaining lines
  156. let start_line = a:cov.startline
  157. while start_line < a:cov.endline
  158. let start_line += 1
  159. call add(matches, {
  160. \ 'group': color,
  161. \ 'pos': [[start_line]],
  162. \ 'priority': 2,
  163. \ })
  164. endwhile
  165. " finally end columns
  166. call add(matches, {
  167. \ 'group': color,
  168. \ 'pos': [[a:cov.endline, a:cov.endcol-1]],
  169. \ 'priority': 2,
  170. \ })
  171. return matches
  172. endfunction
  173. " Reads the given coverprofile file and annotates the current buffer
  174. function! go#coverage#overlay(file) abort
  175. if !filereadable(a:file)
  176. return
  177. endif
  178. let lines = readfile(a:file)
  179. " cover mode, by default it's 'set'. Just here for debugging purposes
  180. let mode = lines[0]
  181. " contains matches for matchaddpos()
  182. let matches = []
  183. " first mark all lines as goCoverageNormalText. We use a custom group to not
  184. " interfere with other buffers highlightings. Because the priority is
  185. " lower than the cover and uncover matches, it'll be overridden.
  186. let cnt = 1
  187. while cnt <= line('$')
  188. call add(matches, {'group': 'goCoverageNormalText', 'pos': [cnt], 'priority': 1})
  189. let cnt += 1
  190. endwhile
  191. let fname = expand('%')
  192. " when called for a _test.go file, run the coverage for the actuall file
  193. " file
  194. if fname =~# '^\f\+_test\.go$'
  195. let l:root = split(fname, '_test.go$')[0]
  196. let fname = l:root . ".go"
  197. if !filereadable(fname)
  198. call go#util#EchoError("couldn't find ".fname)
  199. return
  200. endif
  201. " open the alternate file to show the coverage
  202. exe ":edit ". fnamemodify(fname, ":p")
  203. endif
  204. " cov.file includes only the filename itself, without full path
  205. let fname = fnamemodify(fname, ":t")
  206. for line in lines[1:]
  207. let cov = go#coverage#parsegocoverline(line)
  208. " TODO(arslan): for now only include the coverage for the current
  209. " buffer
  210. if fname != fnamemodify(cov.file, ':t')
  211. continue
  212. endif
  213. call extend(matches, go#coverage#genmatch(cov))
  214. endfor
  215. " clear the matches if we leave the buffer
  216. augroup vim-go-coverage
  217. autocmd!
  218. autocmd BufWinLeave <buffer> call go#coverage#Clear()
  219. augroup end
  220. for m in matches
  221. call matchaddpos(m.group, m.pos)
  222. endfor
  223. endfunction
  224. " ---------------------
  225. " | Vim job callbacks |
  226. " ---------------------
  227. "
  228. function s:coverage_job(args)
  229. " autowrite is not enabled for jobs
  230. call go#cmd#autowrite()
  231. let status_dir = expand('%:p:h')
  232. let Complete = a:args.complete
  233. function! s:complete(job, exit_status, data) closure
  234. let status = {
  235. \ 'desc': 'last status',
  236. \ 'type': "coverage",
  237. \ 'state': "finished",
  238. \ }
  239. if a:exit_status
  240. let status.state = "failed"
  241. endif
  242. call go#statusline#Update(status_dir, status)
  243. return Complete(a:job, a:exit_status, a:data)
  244. endfunction
  245. let a:args.complete = funcref('s:complete')
  246. let callbacks = go#job#Spawn(a:args)
  247. let start_options = {
  248. \ 'callback': callbacks.callback,
  249. \ 'exit_cb': callbacks.exit_cb,
  250. \ 'close_cb': callbacks.close_cb,
  251. \ }
  252. " pre start
  253. let dir = getcwd()
  254. let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd '
  255. let jobdir = fnameescape(expand("%:p:h"))
  256. execute cd . jobdir
  257. call go#statusline#Update(status_dir, {
  258. \ 'desc': "current status",
  259. \ 'type': "coverage",
  260. \ 'state': "started",
  261. \})
  262. call job_start(a:args.cmd, start_options)
  263. " post start
  264. execute cd . fnameescape(dir)
  265. endfunction
  266. " coverage_callback is called when the coverage execution is finished
  267. function! s:coverage_callback(coverfile, job, exit_status, data)
  268. if a:exit_status == 0
  269. call go#coverage#overlay(a:coverfile)
  270. endif
  271. call delete(a:coverfile)
  272. endfunction
  273. function! s:coverage_browser_callback(coverfile, job, exit_status, data)
  274. if a:exit_status == 0
  275. let openHTML = 'go tool cover -html='.a:coverfile
  276. call go#tool#ExecuteInDir(openHTML)
  277. endif
  278. call delete(a:coverfile)
  279. endfunction
  280. " -----------------------
  281. " | Neovim job handlers |
  282. " -----------------------
  283. let s:coverage_handler_jobs = {}
  284. let s:coverage_browser_handler_jobs = {}
  285. function! s:coverage_handler(job, exit_status, data) abort
  286. if !has_key(s:coverage_handler_jobs, a:job.id)
  287. return
  288. endif
  289. let l:tmpname = s:coverage_handler_jobs[a:job.id]
  290. if a:exit_status == 0
  291. call go#coverage#overlay(l:tmpname)
  292. endif
  293. call delete(l:tmpname)
  294. unlet s:coverage_handler_jobs[a:job.id]
  295. endfunction
  296. function! s:coverage_browser_handler(job, exit_status, data) abort
  297. if !has_key(s:coverage_browser_handler_jobs, a:job.id)
  298. return
  299. endif
  300. let l:tmpname = s:coverage_browser_handler_jobs[a:job.id]
  301. if a:exit_status == 0
  302. let openHTML = 'go tool cover -html='.l:tmpname
  303. call go#tool#ExecuteInDir(openHTML)
  304. endif
  305. call delete(l:tmpname)
  306. unlet s:coverage_browser_handler_jobs[a:job.id]
  307. endfunction
  308. " vim: sw=2 ts=2 et