guru.vim 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. " guru.vim -- Vim integration for the Go guru.
  2. " guru_cmd returns a dict that contains the command to execute guru. args
  3. " is dict with following options:
  4. " mode : guru mode, such as 'implements'
  5. " format : output format, either 'plain' or 'json'
  6. " needs_scope : if 1, adds the current package to the scope
  7. " selected : if 1, means it's a range of selection, otherwise it picks up the
  8. " offset under the cursor
  9. " example output:
  10. " {'cmd' : ['guru', '-json', 'implements', 'demo/demo.go:#66']}
  11. function! s:guru_cmd(args) range abort
  12. let mode = a:args.mode
  13. let format = a:args.format
  14. let needs_scope = a:args.needs_scope
  15. let selected = a:args.selected
  16. let result = {}
  17. let pkg = go#package#ImportPath()
  18. " this is important, check it!
  19. if pkg == -1 && needs_scope
  20. return {'err': "current directory is not inside of a valid GOPATH"}
  21. endif
  22. "return with a warning if the binary doesn't exist
  23. let bin_path = go#path#CheckBinPath("guru")
  24. if empty(bin_path)
  25. return {'err': "bin path not found"}
  26. endif
  27. " start constructing the command
  28. let cmd = [bin_path]
  29. let filename = fnamemodify(expand("%"), ':p:gs?\\?/?')
  30. if &modified
  31. let result.stdin_content = go#util#archive()
  32. call add(cmd, "-modified")
  33. endif
  34. " enable outputting in json format
  35. if format == "json"
  36. call add(cmd, "-json")
  37. endif
  38. " check for any tags
  39. if exists('g:go_build_tags')
  40. let tags = get(g:, 'go_build_tags')
  41. call extend(cmd, ["-tags", tags])
  42. let result.tags = tags
  43. endif
  44. " some modes require scope to be defined (such as callers). For these we
  45. " choose a sensible setting, which is using the current file's package
  46. let scopes = []
  47. if needs_scope
  48. let scopes = [pkg]
  49. endif
  50. " check for any user defined scope setting. users can define the scope,
  51. " in package pattern form. examples:
  52. " golang.org/x/tools/cmd/guru # a single package
  53. " golang.org/x/tools/... # all packages beneath dir
  54. " ... # the entire workspace.
  55. if exists('g:go_guru_scope')
  56. " check that the setting is of type list
  57. if type(get(g:, 'go_guru_scope')) != type([])
  58. return {'err' : "go_guru_scope should of type list"}
  59. endif
  60. let scopes = get(g:, 'go_guru_scope')
  61. endif
  62. " now add the scope to our command if there is any
  63. if !empty(scopes)
  64. " strip trailing slashes for each path in scoped. bug:
  65. " https://github.com/golang/go/issues/14584
  66. let scopes = go#util#StripTrailingSlash(scopes)
  67. " create shell-safe entries of the list
  68. if !has("nvim") && !go#util#has_job() | let scopes = go#util#Shelllist(scopes) | endif
  69. " guru expect a comma-separated list of patterns, construct it
  70. let l:scope = join(scopes, ",")
  71. let result.scope = l:scope
  72. call extend(cmd, ["-scope", l:scope])
  73. endif
  74. let pos = printf("#%s", go#util#OffsetCursor())
  75. if selected != -1
  76. " means we have a range, get it
  77. let pos1 = go#util#Offset(line("'<"), col("'<"))
  78. let pos2 = go#util#Offset(line("'>"), col("'>"))
  79. let pos = printf("#%s,#%s", pos1, pos2)
  80. endif
  81. let filename .= ':'.pos
  82. call extend(cmd, [mode, filename])
  83. let result.cmd = cmd
  84. return result
  85. endfunction
  86. " sync_guru runs guru in sync mode with the given arguments
  87. function! s:sync_guru(args) abort
  88. let result = s:guru_cmd(a:args)
  89. if has_key(result, 'err')
  90. call go#util#EchoError(result.err)
  91. return -1
  92. endif
  93. if !has_key(a:args, 'disable_progress')
  94. if a:args.needs_scope
  95. call go#util#EchoProgress("analysing with scope ". result.scope .
  96. \ " (see ':help go-guru-scope' if this doesn't work)...")
  97. elseif a:args.mode !=# 'what'
  98. " the query might take time, let us give some feedback
  99. call go#util#EchoProgress("analysing ...")
  100. endif
  101. endif
  102. " run, forrest run!!!
  103. let command = join(result.cmd, " ")
  104. if has_key(result, 'stdin_content')
  105. let out = go#util#System(command, result.stdin_content)
  106. else
  107. let out = go#util#System(command)
  108. endif
  109. if has_key(a:args, 'custom_parse')
  110. call a:args.custom_parse(go#util#ShellError(), out, a:args.mode)
  111. else
  112. call s:parse_guru_output(go#util#ShellError(), out, a:args.mode)
  113. endif
  114. return out
  115. endfunc
  116. " use vim or neovim job api as appropriate
  117. function! s:job_start(cmd, start_options) abort
  118. if go#util#has_job()
  119. return job_start(a:cmd, a:start_options)
  120. endif
  121. let opts = {'stdout_buffered': v:true, 'stderr_buffered': v:true}
  122. function opts.on_stdout(job_id, data, event) closure
  123. call a:start_options.callback(a:job_id, join(a:data, "\n"))
  124. endfunction
  125. function opts.on_stderr(job_id, data, event) closure
  126. call a:start_options.callback(a:job_id, join(a:data, "\n"))
  127. endfunction
  128. function opts.on_exit(job_id, exit_code, event) closure
  129. call a:start_options.exit_cb(a:job_id, a:exit_code)
  130. call a:start_options.close_cb(a:job_id)
  131. endfunction
  132. " use a shell for input redirection if needed
  133. let cmd = a:cmd
  134. if has_key(a:start_options, 'in_io') && a:start_options.in_io ==# 'file' && !empty(a:start_options.in_name)
  135. let cmd = ['/bin/sh', '-c', join(a:cmd, ' ') . ' <' . a:start_options.in_name]
  136. endif
  137. return jobstart(cmd, opts)
  138. endfunction
  139. " async_guru runs guru in async mode with the given arguments
  140. function! s:async_guru(args) abort
  141. let result = s:guru_cmd(a:args)
  142. if has_key(result, 'err')
  143. call go#util#EchoError(result.err)
  144. return
  145. endif
  146. if !has_key(a:args, 'disable_progress')
  147. if a:args.needs_scope
  148. call go#util#EchoProgress("analysing with scope " . result.scope .
  149. \ " (see ':help go-guru-scope' if this doesn't work)...")
  150. endif
  151. endif
  152. let state = {
  153. \ 'status_dir': expand('%:p:h'),
  154. \ 'statusline_type': printf("%s", a:args.mode),
  155. \ 'mode': a:args.mode,
  156. \ 'status': {},
  157. \ 'exitval': 0,
  158. \ 'closed': 0,
  159. \ 'exited': 0,
  160. \ 'messages': [],
  161. \ 'parse' : get(a:args, 'custom_parse', funcref("s:parse_guru_output"))
  162. \ }
  163. function! s:callback(chan, msg) dict
  164. call add(self.messages, a:msg)
  165. endfunction
  166. function! s:exit_cb(job, exitval) dict
  167. let self.exited = 1
  168. let status = {
  169. \ 'desc': 'last status',
  170. \ 'type': self.statusline_type,
  171. \ 'state': "finished",
  172. \ }
  173. if a:exitval
  174. let self.exitval = a:exitval
  175. let status.state = "failed"
  176. endif
  177. call go#statusline#Update(self.status_dir, status)
  178. if self.closed
  179. call self.complete()
  180. endif
  181. endfunction
  182. function! s:close_cb(ch) dict
  183. let self.closed = 1
  184. if self.exited
  185. call self.complete()
  186. endif
  187. endfunction
  188. function state.complete() dict
  189. let out = join(self.messages, "\n")
  190. call self.parse(self.exitval, out, self.mode)
  191. endfunction
  192. " explicitly bind the callbacks to state so that self within them always
  193. " refers to state. See :help Partial for more information.
  194. let start_options = {
  195. \ 'callback': function('s:callback', [], state),
  196. \ 'exit_cb': function('s:exit_cb', [], state),
  197. \ 'close_cb': function('s:close_cb', [], state)
  198. \ }
  199. if has_key(result, 'stdin_content')
  200. let l:tmpname = tempname()
  201. call writefile(split(result.stdin_content, "\n"), l:tmpname, "b")
  202. let l:start_options.in_io = "file"
  203. let l:start_options.in_name = l:tmpname
  204. endif
  205. call go#statusline#Update(state.status_dir, {
  206. \ 'desc': "current status",
  207. \ 'type': state.statusline_type,
  208. \ 'state': "analysing",
  209. \})
  210. return s:job_start(result.cmd, start_options)
  211. endfunc
  212. " run_guru runs the given guru argument
  213. function! s:run_guru(args) abort
  214. if has('nvim') || go#util#has_job()
  215. let res = s:async_guru(a:args)
  216. else
  217. let res = s:sync_guru(a:args)
  218. endif
  219. return res
  220. endfunction
  221. " Show 'implements' relation for selected package
  222. function! go#guru#Implements(selected) abort
  223. let args = {
  224. \ 'mode': 'implements',
  225. \ 'format': 'plain',
  226. \ 'selected': a:selected,
  227. \ 'needs_scope': 1,
  228. \ }
  229. call s:run_guru(args)
  230. endfunction
  231. " Report the possible constants, global variables, and concrete types that may
  232. " appear in a value of type error
  233. function! go#guru#Whicherrs(selected) abort
  234. let args = {
  235. \ 'mode': 'whicherrs',
  236. \ 'format': 'plain',
  237. \ 'selected': a:selected,
  238. \ 'needs_scope': 1,
  239. \ }
  240. " TODO(arslan): handle empty case for both sync/async
  241. " if empty(out.out)
  242. " call go#util#EchoSuccess("no error variables found. Try to change the scope with :GoGuruScope")
  243. " return
  244. " endif
  245. call s:run_guru(args)
  246. endfunction
  247. " Describe selected syntax: definition, methods, etc
  248. function! go#guru#Describe(selected) abort
  249. let args = {
  250. \ 'mode': 'describe',
  251. \ 'format': 'plain',
  252. \ 'selected': a:selected,
  253. \ 'needs_scope': 1,
  254. \ }
  255. call s:run_guru(args)
  256. endfunction
  257. function! go#guru#DescribeInfo() abort
  258. " json_encode() and friends are introduced with this patch (7.4.1304)
  259. " vim: https://groups.google.com/d/msg/vim_dev/vLupTNhQhZ8/cDGIk0JEDgAJ
  260. " nvim: https://github.com/neovim/neovim/pull/4131
  261. if !exists("*json_decode")
  262. call go#util#EchoError("requires 'json_decode'. Update your Vim/Neovim version.")
  263. return
  264. endif
  265. function! s:info(exit_val, output, mode)
  266. if a:exit_val != 0
  267. return
  268. endif
  269. if a:output[0] !=# '{'
  270. return
  271. endif
  272. if empty(a:output) || type(a:output) != type("")
  273. return
  274. endif
  275. let result = json_decode(a:output)
  276. if type(result) != type({})
  277. call go#util#EchoError(printf("malformed output from guru: %s", a:output))
  278. return
  279. endif
  280. if !has_key(result, 'detail')
  281. " if there is no detail check if there is a description and print it
  282. if has_key(result, "desc")
  283. call go#util#EchoInfo(result["desc"])
  284. return
  285. endif
  286. call go#util#EchoError("detail key is missing. Please open a bug report on vim-go repo.")
  287. return
  288. endif
  289. let detail = result['detail']
  290. let info = ""
  291. " guru gives different information based on the detail mode. Let try to
  292. " extract the most useful information
  293. if detail == "value"
  294. if !has_key(result, 'value')
  295. call go#util#EchoError("value key is missing. Please open a bug report on vim-go repo.")
  296. return
  297. endif
  298. let val = result["value"]
  299. if !has_key(val, 'type')
  300. call go#util#EchoError("type key is missing (value.type). Please open a bug report on vim-go repo.")
  301. return
  302. endif
  303. let info = val["type"]
  304. elseif detail == "type"
  305. if !has_key(result, 'type')
  306. call go#util#EchoError("type key is missing. Please open a bug report on vim-go repo.")
  307. return
  308. endif
  309. let type = result["type"]
  310. if !has_key(type, 'type')
  311. call go#util#EchoError("type key is missing (type.type). Please open a bug report on vim-go repo.")
  312. return
  313. endif
  314. let info = type["type"]
  315. elseif detail == "package"
  316. if !has_key(result, 'package')
  317. call go#util#EchoError("package key is missing. Please open a bug report on vim-go repo.")
  318. return
  319. endif
  320. let package = result["package"]
  321. if !has_key(package, 'path')
  322. call go#util#EchoError("path key is missing (package.path). Please open a bug report on vim-go repo.")
  323. return
  324. endif
  325. let info = printf("package %s", package["path"])
  326. elseif detail == "unknown"
  327. let info = result["desc"]
  328. else
  329. call go#util#EchoError(printf("unknown detail mode found '%s'. Please open a bug report on vim-go repo", detail))
  330. return
  331. endif
  332. call go#util#EchoInfo(info)
  333. endfunction
  334. let args = {
  335. \ 'mode': 'describe',
  336. \ 'format': 'json',
  337. \ 'selected': -1,
  338. \ 'needs_scope': 0,
  339. \ 'custom_parse': function('s:info'),
  340. \ 'disable_progress': 1,
  341. \ }
  342. call s:run_guru(args)
  343. endfunction
  344. " Show possible targets of selected function call
  345. function! go#guru#Callees(selected) abort
  346. let args = {
  347. \ 'mode': 'callees',
  348. \ 'format': 'plain',
  349. \ 'selected': a:selected,
  350. \ 'needs_scope': 1,
  351. \ }
  352. call s:run_guru(args)
  353. endfunction
  354. " Show possible callers of selected function
  355. function! go#guru#Callers(selected) abort
  356. let args = {
  357. \ 'mode': 'callers',
  358. \ 'format': 'plain',
  359. \ 'selected': a:selected,
  360. \ 'needs_scope': 1,
  361. \ }
  362. call s:run_guru(args)
  363. endfunction
  364. " Show path from callgraph root to selected function
  365. function! go#guru#Callstack(selected) abort
  366. let args = {
  367. \ 'mode': 'callstack',
  368. \ 'format': 'plain',
  369. \ 'selected': a:selected,
  370. \ 'needs_scope': 1,
  371. \ }
  372. call s:run_guru(args)
  373. endfunction
  374. " Show free variables of selection
  375. function! go#guru#Freevars(selected) abort
  376. " Freevars requires a selection
  377. if a:selected == -1
  378. call go#util#EchoError("GoFreevars requires a selection (range) of code")
  379. return
  380. endif
  381. let args = {
  382. \ 'mode': 'freevars',
  383. \ 'format': 'plain',
  384. \ 'selected': 1,
  385. \ 'needs_scope': 0,
  386. \ }
  387. call s:run_guru(args)
  388. endfunction
  389. " Show send/receive corresponding to selected channel op
  390. function! go#guru#ChannelPeers(selected) abort
  391. let args = {
  392. \ 'mode': 'peers',
  393. \ 'format': 'plain',
  394. \ 'selected': a:selected,
  395. \ 'needs_scope': 1,
  396. \ }
  397. call s:run_guru(args)
  398. endfunction
  399. " Show all refs to entity denoted by selected identifier
  400. function! go#guru#Referrers(selected) abort
  401. let args = {
  402. \ 'mode': 'referrers',
  403. \ 'format': 'plain',
  404. \ 'selected': a:selected,
  405. \ 'needs_scope': 0,
  406. \ }
  407. call s:run_guru(args)
  408. endfunction
  409. function! go#guru#SameIds() abort
  410. " we use matchaddpos() which was introduce with 7.4.330, be sure we have
  411. " it: http://ftp.vim.org/vim/patches/7.4/7.4.330
  412. if !exists("*matchaddpos")
  413. call go#util#EchoError("GoSameIds requires 'matchaddpos'. Update your Vim/Neovim version.")
  414. return
  415. endif
  416. " json_encode() and friends are introduced with this patch (7.4.1304)
  417. " vim: https://groups.google.com/d/msg/vim_dev/vLupTNhQhZ8/cDGIk0JEDgAJ
  418. " nvim: https://github.com/neovim/neovim/pull/4131
  419. if !exists("*json_decode")
  420. call go#util#EchoError("GoSameIds requires 'json_decode'. Update your Vim/Neovim version.")
  421. return
  422. endif
  423. let args = {
  424. \ 'mode': 'what',
  425. \ 'format': 'json',
  426. \ 'selected': -1,
  427. \ 'needs_scope': 0,
  428. \ 'custom_parse': function('s:same_ids_highlight'),
  429. \ }
  430. call s:run_guru(args)
  431. endfunction
  432. function! s:same_ids_highlight(exit_val, output, mode) abort
  433. call go#guru#ClearSameIds() " run after calling guru to reduce flicker.
  434. if a:output[0] !=# '{'
  435. if !get(g:, 'go_auto_sameids', 0)
  436. call go#util#EchoError(a:output)
  437. endif
  438. return
  439. endif
  440. let result = json_decode(a:output)
  441. if type(result) != type({}) && !get(g:, 'go_auto_sameids', 0)
  442. call go#util#EchoError("malformed output from guru")
  443. return
  444. endif
  445. if !has_key(result, 'sameids')
  446. if !get(g:, 'go_auto_sameids', 0)
  447. call go#util#EchoError("no same_ids founds for the given identifier")
  448. endif
  449. return
  450. endif
  451. let poslen = 0
  452. for enclosing in result['enclosing']
  453. if enclosing['desc'] == 'identifier'
  454. let poslen = enclosing['end'] - enclosing['start']
  455. break
  456. endif
  457. endfor
  458. " return when there's no identifier to highlight.
  459. if poslen == 0
  460. return
  461. endif
  462. let same_ids = result['sameids']
  463. " highlight the lines
  464. for item in same_ids
  465. let pos = split(item, ':')
  466. call matchaddpos('goSameId', [[str2nr(pos[-2]), str2nr(pos[-1]), str2nr(poslen)]])
  467. endfor
  468. if get(g:, "go_auto_sameids", 0)
  469. " re-apply SameIds at the current cursor position at the time the buffer
  470. " is redisplayed: e.g. :edit, :GoRename, etc.
  471. augroup vim-go-sameids
  472. autocmd!
  473. autocmd BufWinEnter <buffer> nested call go#guru#SameIds()
  474. augroup end
  475. endif
  476. endfunction
  477. " ClearSameIds returns 0 when it removes goSameId groups and non-zero if no
  478. " goSameId groups are found.
  479. function! go#guru#ClearSameIds() abort
  480. let l:cleared = 0
  481. let m = getmatches()
  482. for item in m
  483. if item['group'] == 'goSameId'
  484. call matchdelete(item['id'])
  485. let l:cleared = 1
  486. endif
  487. endfor
  488. if !l:cleared
  489. return 1
  490. endif
  491. " remove the autocmds we defined
  492. augroup vim-go-sameids
  493. autocmd!
  494. augroup end
  495. return 0
  496. endfunction
  497. function! go#guru#ToggleSameIds() abort
  498. if go#guru#ClearSameIds() != 0
  499. call go#guru#SameIds()
  500. endif
  501. endfunction
  502. function! go#guru#AutoToogleSameIds() abort
  503. if get(g:, "go_auto_sameids", 0)
  504. call go#util#EchoProgress("sameids auto highlighting disabled")
  505. call go#guru#ClearSameIds()
  506. let g:go_auto_sameids = 0
  507. return
  508. endif
  509. call go#util#EchoSuccess("sameids auto highlighting enabled")
  510. let g:go_auto_sameids = 1
  511. endfunction
  512. """"""""""""""""""""""""""""""""""""""""
  513. "" HELPER FUNCTIONS
  514. """"""""""""""""""""""""""""""""""""""""
  515. " This uses Vim's errorformat to parse the output from Guru's 'plain output
  516. " and put it into location list. I believe using errorformat is much more
  517. " easier to use. If we need more power we can always switch back to parse it
  518. " via regex. Match two possible styles of errorformats:
  519. "
  520. " 'file:line.col-line2.col2: message'
  521. " 'file:line:col: message'
  522. "
  523. " We discard line2 and col2 for the first errorformat, because it's not
  524. " useful and location only has the ability to show one line and column
  525. " number
  526. function! s:parse_guru_output(exit_val, output, title) abort
  527. if a:exit_val
  528. call go#util#EchoError(a:output)
  529. return
  530. endif
  531. let errformat = "%f:%l.%c-%[%^:]%#:\ %m,%f:%l:%c:\ %m"
  532. let l:listtype = go#list#Type("_guru")
  533. call go#list#ParseFormat(l:listtype, errformat, a:output, a:title)
  534. let errors = go#list#Get(l:listtype)
  535. call go#list#Window(l:listtype, len(errors))
  536. endfun
  537. function! go#guru#Scope(...) abort
  538. if a:0
  539. if a:0 == 1 && a:1 == '""'
  540. unlet g:go_guru_scope
  541. call go#util#EchoSuccess("guru scope is cleared")
  542. else
  543. let g:go_guru_scope = a:000
  544. call go#util#EchoSuccess("guru scope changed to: ". join(a:000, ","))
  545. endif
  546. return
  547. endif
  548. if !exists('g:go_guru_scope')
  549. call go#util#EchoError("guru scope is not set")
  550. else
  551. call go#util#EchoSuccess("current guru scope: ". join(g:go_guru_scope, ","))
  552. endif
  553. endfunction
  554. " vim: sw=2 ts=2 et