rust.vim 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. " Description: Helper functions for Rust commands/mappings
  2. " Last Modified: 2023-09-11
  3. " For bugs, patches and license go to https://github.com/rust-lang/rust.vim
  4. function! rust#Load()
  5. " Utility call to get this script loaded, for debugging
  6. endfunction
  7. function! rust#GetConfigVar(name, default)
  8. " Local buffer variable with same name takes predeence over global
  9. if has_key(b:, a:name)
  10. return get(b:, a:name)
  11. endif
  12. if has_key(g:, a:name)
  13. return get(g:, a:name)
  14. endif
  15. return a:default
  16. endfunction
  17. " Include expression {{{1
  18. function! rust#IncludeExpr(fname) abort
  19. " Remove leading 'crate::' to deal with 2018 edition style 'use'
  20. " statements
  21. let l:fname = substitute(a:fname, '^crate::', '', '')
  22. " Remove trailing colons arising from lines like
  23. "
  24. " use foo::{Bar, Baz};
  25. let l:fname = substitute(l:fname, ':\+$', '', '')
  26. " Replace '::' with '/'
  27. let l:fname = substitute(l:fname, '::', '/', 'g')
  28. " When we have
  29. "
  30. " use foo::bar::baz;
  31. "
  32. " we can't tell whether baz is a module or a function; and we can't tell
  33. " which modules correspond to files.
  34. "
  35. " So we work our way up, trying
  36. "
  37. " foo/bar/baz.rs
  38. " foo/bar.rs
  39. " foo.rs
  40. while l:fname !=# '.'
  41. let l:path = findfile(l:fname)
  42. if !empty(l:path)
  43. return l:fname
  44. endif
  45. let l:fname = fnamemodify(l:fname, ':h')
  46. endwhile
  47. return l:fname
  48. endfunction
  49. " Jump {{{1
  50. function! rust#Jump(mode, function) range
  51. let cnt = v:count1
  52. normal! m'
  53. if a:mode ==# 'v'
  54. norm! gv
  55. endif
  56. let foldenable = &foldenable
  57. set nofoldenable
  58. while cnt > 0
  59. execute "call <SID>Jump_" . a:function . "()"
  60. let cnt = cnt - 1
  61. endwhile
  62. let &foldenable = foldenable
  63. endfunction
  64. function! s:Jump_Back()
  65. call search('{', 'b')
  66. keepjumps normal! w99[{
  67. endfunction
  68. function! s:Jump_Forward()
  69. normal! j0
  70. call search('{', 'b')
  71. keepjumps normal! w99[{%
  72. call search('{')
  73. endfunction
  74. " Run {{{1
  75. function! rust#Run(bang, args)
  76. let args = s:ShellTokenize(a:args)
  77. if a:bang
  78. let idx = index(l:args, '--')
  79. if idx != -1
  80. let rustc_args = idx == 0 ? [] : l:args[:idx-1]
  81. let args = l:args[idx+1:]
  82. else
  83. let rustc_args = l:args
  84. let args = []
  85. endif
  86. else
  87. let rustc_args = []
  88. endif
  89. let b:rust_last_rustc_args = l:rustc_args
  90. let b:rust_last_args = l:args
  91. call s:WithPath(function("s:Run"), rustc_args, args)
  92. endfunction
  93. function! s:Run(dict, rustc_args, args)
  94. let exepath = a:dict.tmpdir.'/'.fnamemodify(a:dict.path, ':t:r')
  95. if has('win32')
  96. let exepath .= '.exe'
  97. endif
  98. let relpath = get(a:dict, 'tmpdir_relpath', a:dict.path)
  99. let rustc_args = [relpath, '-o', exepath] + a:rustc_args
  100. let rustc = exists("g:rustc_path") ? g:rustc_path : "rustc"
  101. let pwd = a:dict.istemp ? a:dict.tmpdir : ''
  102. let output = s:system(pwd, shellescape(rustc) . " " . join(map(rustc_args, 'shellescape(v:val)')))
  103. if output !=# ''
  104. echohl WarningMsg
  105. echo output
  106. echohl None
  107. endif
  108. if !v:shell_error
  109. exe '!' . shellescape(exepath) . " " . join(map(a:args, 'shellescape(v:val)'))
  110. endif
  111. endfunction
  112. " Expand {{{1
  113. function! rust#Expand(bang, args)
  114. let args = s:ShellTokenize(a:args)
  115. if a:bang && !empty(l:args)
  116. let pretty = remove(l:args, 0)
  117. else
  118. let pretty = "expanded"
  119. endif
  120. call s:WithPath(function("s:Expand"), pretty, args)
  121. endfunction
  122. function! s:Expand(dict, pretty, args)
  123. try
  124. let rustc = exists("g:rustc_path") ? g:rustc_path : "rustc"
  125. if a:pretty =~? '^\%(everybody_loops$\|flowgraph=\)'
  126. let flag = '--xpretty'
  127. else
  128. let flag = '--pretty'
  129. endif
  130. let relpath = get(a:dict, 'tmpdir_relpath', a:dict.path)
  131. let args = [relpath, '-Z', 'unstable-options', l:flag, a:pretty] + a:args
  132. let pwd = a:dict.istemp ? a:dict.tmpdir : ''
  133. let output = s:system(pwd, shellescape(rustc) . " " . join(map(args, 'shellescape(v:val)')))
  134. if v:shell_error
  135. echohl WarningMsg
  136. echo output
  137. echohl None
  138. else
  139. new
  140. silent put =output
  141. 1
  142. d
  143. setl filetype=rust
  144. setl buftype=nofile
  145. setl bufhidden=hide
  146. setl noswapfile
  147. " give the buffer a nice name
  148. let suffix = 1
  149. let basename = fnamemodify(a:dict.path, ':t:r')
  150. while 1
  151. let bufname = basename
  152. if suffix > 1 | let bufname .= ' ('.suffix.')' | endif
  153. let bufname .= '.pretty.rs'
  154. if bufexists(bufname)
  155. let suffix += 1
  156. continue
  157. endif
  158. exe 'silent noautocmd keepalt file' fnameescape(bufname)
  159. break
  160. endwhile
  161. endif
  162. endtry
  163. endfunction
  164. function! rust#CompleteExpand(lead, line, pos)
  165. if a:line[: a:pos-1] =~# '^RustExpand!\s*\S*$'
  166. " first argument and it has a !
  167. let list = ["normal", "expanded", "typed", "expanded,identified", "flowgraph=", "everybody_loops"]
  168. if !empty(a:lead)
  169. call filter(list, "v:val[:len(a:lead)-1] == a:lead")
  170. endif
  171. return list
  172. endif
  173. return glob(escape(a:lead, "*?[") . '*', 0, 1)
  174. endfunction
  175. " Emit {{{1
  176. function! rust#Emit(type, args)
  177. let args = s:ShellTokenize(a:args)
  178. call s:WithPath(function("s:Emit"), a:type, args)
  179. endfunction
  180. function! s:Emit(dict, type, args)
  181. try
  182. let output_path = a:dict.tmpdir.'/output'
  183. let rustc = exists("g:rustc_path") ? g:rustc_path : "rustc"
  184. let relpath = get(a:dict, 'tmpdir_relpath', a:dict.path)
  185. let args = [relpath, '--emit', a:type, '-o', output_path] + a:args
  186. let pwd = a:dict.istemp ? a:dict.tmpdir : ''
  187. let output = s:system(pwd, shellescape(rustc) . " " . join(map(args, 'shellescape(v:val)')))
  188. if output !=# ''
  189. echohl WarningMsg
  190. echo output
  191. echohl None
  192. endif
  193. if !v:shell_error
  194. new
  195. exe 'silent keepalt read' fnameescape(output_path)
  196. 1
  197. d
  198. if a:type ==# "llvm-ir"
  199. setl filetype=llvm
  200. let extension = 'll'
  201. elseif a:type ==# "asm"
  202. setl filetype=asm
  203. let extension = 's'
  204. endif
  205. setl buftype=nofile
  206. setl bufhidden=hide
  207. setl noswapfile
  208. if exists('l:extension')
  209. " give the buffer a nice name
  210. let suffix = 1
  211. let basename = fnamemodify(a:dict.path, ':t:r')
  212. while 1
  213. let bufname = basename
  214. if suffix > 1 | let bufname .= ' ('.suffix.')' | endif
  215. let bufname .= '.'.extension
  216. if bufexists(bufname)
  217. let suffix += 1
  218. continue
  219. endif
  220. exe 'silent noautocmd keepalt file' fnameescape(bufname)
  221. break
  222. endwhile
  223. endif
  224. endif
  225. endtry
  226. endfunction
  227. " Utility functions {{{1
  228. " Invokes func(dict, ...)
  229. " Where {dict} is a dictionary with the following keys:
  230. " 'path' - The path to the file
  231. " 'tmpdir' - The path to a temporary directory that will be deleted when the
  232. " function returns.
  233. " 'istemp' - 1 if the path is a file inside of {dict.tmpdir} or 0 otherwise.
  234. " If {istemp} is 1 then an additional key is provided:
  235. " 'tmpdir_relpath' - The {path} relative to the {tmpdir}.
  236. "
  237. " {dict.path} may be a path to a file inside of {dict.tmpdir} or it may be the
  238. " existing path of the current buffer. If the path is inside of {dict.tmpdir}
  239. " then it is guaranteed to have a '.rs' extension.
  240. function! s:WithPath(func, ...)
  241. let buf = bufnr('')
  242. let saved = {}
  243. let dict = {}
  244. try
  245. let saved.write = &write
  246. set write
  247. let dict.path = expand('%')
  248. let pathisempty = empty(dict.path)
  249. " Always create a tmpdir in case the wrapped command wants it
  250. let dict.tmpdir = tempname()
  251. call mkdir(dict.tmpdir)
  252. if pathisempty || !saved.write
  253. let dict.istemp = 1
  254. " if we're doing this because of nowrite, preserve the filename
  255. if !pathisempty
  256. let filename = expand('%:t:r').".rs"
  257. else
  258. let filename = 'unnamed.rs'
  259. endif
  260. let dict.tmpdir_relpath = filename
  261. let dict.path = dict.tmpdir.'/'.filename
  262. let saved.mod = &modified
  263. set nomodified
  264. silent exe 'keepalt write! ' . fnameescape(dict.path)
  265. if pathisempty
  266. silent keepalt 0file
  267. endif
  268. else
  269. let dict.istemp = 0
  270. update
  271. endif
  272. call call(a:func, [dict] + a:000)
  273. finally
  274. if bufexists(buf)
  275. for [opt, value] in items(saved)
  276. silent call setbufvar(buf, '&'.opt, value)
  277. unlet value " avoid variable type mismatches
  278. endfor
  279. endif
  280. if has_key(dict, 'tmpdir') | silent call s:RmDir(dict.tmpdir) | endif
  281. endtry
  282. endfunction
  283. function! rust#AppendCmdLine(text)
  284. call setcmdpos(getcmdpos())
  285. let cmd = getcmdline() . a:text
  286. return cmd
  287. endfunction
  288. " Tokenize the string according to sh parsing rules
  289. function! s:ShellTokenize(text)
  290. " states:
  291. " 0: start of word
  292. " 1: unquoted
  293. " 2: unquoted backslash
  294. " 3: double-quote
  295. " 4: double-quoted backslash
  296. " 5: single-quote
  297. let l:state = 0
  298. let l:current = ''
  299. let l:args = []
  300. for c in split(a:text, '\zs')
  301. if l:state == 0 || l:state == 1 " unquoted
  302. if l:c ==# ' '
  303. if l:state == 0 | continue | endif
  304. call add(l:args, l:current)
  305. let l:current = ''
  306. let l:state = 0
  307. elseif l:c ==# '\'
  308. let l:state = 2
  309. elseif l:c ==# '"'
  310. let l:state = 3
  311. elseif l:c ==# "'"
  312. let l:state = 5
  313. else
  314. let l:current .= l:c
  315. let l:state = 1
  316. endif
  317. elseif l:state == 2 " unquoted backslash
  318. if l:c !=# "\n" " can it even be \n?
  319. let l:current .= l:c
  320. endif
  321. let l:state = 1
  322. elseif l:state == 3 " double-quote
  323. if l:c ==# '\'
  324. let l:state = 4
  325. elseif l:c ==# '"'
  326. let l:state = 1
  327. else
  328. let l:current .= l:c
  329. endif
  330. elseif l:state == 4 " double-quoted backslash
  331. if stridx('$`"\', l:c) >= 0
  332. let l:current .= l:c
  333. elseif l:c ==# "\n" " is this even possible?
  334. " skip it
  335. else
  336. let l:current .= '\'.l:c
  337. endif
  338. let l:state = 3
  339. elseif l:state == 5 " single-quoted
  340. if l:c ==# "'"
  341. let l:state = 1
  342. else
  343. let l:current .= l:c
  344. endif
  345. endif
  346. endfor
  347. if l:state != 0
  348. call add(l:args, l:current)
  349. endif
  350. return l:args
  351. endfunction
  352. function! s:RmDir(path)
  353. " sanity check; make sure it's not empty, /, or $HOME
  354. if empty(a:path)
  355. echoerr 'Attempted to delete empty path'
  356. return 0
  357. elseif a:path ==# '/' || a:path ==# $HOME
  358. let l:path = expand(a:path)
  359. if l:path ==# '/' || l:path ==# $HOME
  360. echoerr 'Attempted to delete protected path: ' . a:path
  361. return 0
  362. endif
  363. endif
  364. if !isdirectory(a:path)
  365. return 0
  366. endif
  367. " delete() returns 0 when removing file successfully
  368. return delete(a:path, 'rf') == 0
  369. endfunction
  370. " Executes {cmd} with the cwd set to {pwd}, without changing Vim's cwd.
  371. " If {pwd} is the empty string then it doesn't change the cwd.
  372. function! s:system(pwd, cmd)
  373. let cmd = a:cmd
  374. if !empty(a:pwd)
  375. let cmd = 'cd ' . shellescape(a:pwd) . ' && ' . cmd
  376. endif
  377. return system(cmd)
  378. endfunction
  379. " Playpen Support {{{1
  380. " Parts of gist.vim by Yasuhiro Matsumoto <mattn.jp@gmail.com> reused
  381. " gist.vim available under the BSD license, available at
  382. " http://github.com/mattn/gist-vim
  383. function! s:has_webapi()
  384. if !exists("*webapi#http#post")
  385. try
  386. call webapi#http#post()
  387. catch
  388. endtry
  389. endif
  390. return exists("*webapi#http#post")
  391. endfunction
  392. function! rust#Play(count, line1, line2, ...) abort
  393. redraw
  394. let l:rust_playpen_url = get(g:, 'rust_playpen_url', 'https://play.rust-lang.org/')
  395. let l:rust_shortener_url = get(g:, 'rust_shortener_url', 'https://is.gd/')
  396. if !s:has_webapi()
  397. echohl ErrorMsg | echomsg ':RustPlay depends on webapi.vim (https://github.com/mattn/webapi-vim)' | echohl None
  398. return
  399. endif
  400. let bufname = bufname('%')
  401. if a:count < 1
  402. let content = join(getline(a:line1, a:line2), "\n")
  403. else
  404. let save_regcont = @"
  405. let save_regtype = getregtype('"')
  406. silent! normal! gvy
  407. let content = @"
  408. call setreg('"', save_regcont, save_regtype)
  409. endif
  410. let url = l:rust_playpen_url."?code=".webapi#http#encodeURI(content)
  411. if strlen(url) > 5000
  412. echohl ErrorMsg | echomsg 'Buffer too large, max 5000 encoded characters ('.strlen(url).')' | echohl None
  413. return
  414. endif
  415. let payload = "format=simple&url=".webapi#http#encodeURI(url)
  416. let res = webapi#http#post(l:rust_shortener_url.'create.php', payload, {})
  417. if res.status[0] ==# '2'
  418. let url = res.content
  419. endif
  420. let footer = ''
  421. if exists('g:rust_clip_command')
  422. call system(g:rust_clip_command, url)
  423. if !v:shell_error
  424. let footer = ' (copied to clipboard)'
  425. endif
  426. endif
  427. redraw | echomsg 'Done: '.url.footer
  428. endfunction
  429. " Run a test under the cursor or all tests {{{1
  430. " Finds a test function name under the cursor. Returns empty string when a
  431. " test function is not found.
  432. function! s:SearchTestFunctionNameUnderCursor() abort
  433. let cursor_line = line('.')
  434. " Find #[test] attribute
  435. if search('\m\C#\[test\]', 'bcW') is 0
  436. return ''
  437. endif
  438. " Move to an opening brace of the test function
  439. let test_func_line = search('\m\C^\s*fn\s\+\h\w*\s*(.\+{$', 'eW')
  440. if test_func_line is 0
  441. return ''
  442. endif
  443. " Search the end of test function (closing brace) to ensure that the
  444. " cursor position is within function definition
  445. if maparg('<Plug>(MatchitNormalForward)') ==# ''
  446. keepjumps normal! %
  447. else
  448. " Prefer matchit.vim official plugin to native % since the plugin
  449. " provides better behavior than original % (#391)
  450. " To load the plugin, run:
  451. " :packadd matchit
  452. execute 'keepjumps' 'normal' "\<Plug>(MatchitNormalForward)"
  453. endif
  454. if line('.') < cursor_line
  455. return ''
  456. endif
  457. return matchstr(getline(test_func_line), '\m\C^\s*fn\s\+\zs\h\w*')
  458. endfunction
  459. function! rust#Test(mods, winsize, all, options) abort
  460. let manifest = findfile('Cargo.toml', expand('%:p:h') . ';')
  461. if manifest ==# ''
  462. return rust#Run(1, '--test ' . a:options)
  463. endif
  464. " <count> defaults to 0, but we prefer an empty string
  465. let winsize = a:winsize ? a:winsize : ''
  466. if has('terminal')
  467. if has('patch-8.0.910')
  468. let cmd = printf('%s noautocmd %snew | terminal ++curwin ', a:mods, winsize)
  469. else
  470. let cmd = printf('%s terminal ', a:mods)
  471. endif
  472. elseif has('nvim')
  473. let cmd = printf('%s noautocmd %snew | terminal ', a:mods, winsize)
  474. else
  475. let cmd = '!'
  476. let manifest = shellescape(manifest)
  477. endif
  478. if a:all
  479. if a:options ==# ''
  480. execute cmd . 'cargo test --manifest-path' manifest
  481. else
  482. execute cmd . 'cargo test --manifest-path' manifest a:options
  483. endif
  484. return
  485. endif
  486. let saved = getpos('.')
  487. try
  488. let func_name = s:SearchTestFunctionNameUnderCursor()
  489. finally
  490. call setpos('.', saved)
  491. endtry
  492. if func_name ==# ''
  493. echohl ErrorMsg
  494. echomsg 'No test function was found under the cursor. Please add ! to command if you want to run all tests'
  495. echohl None
  496. return
  497. endif
  498. if a:options ==# ''
  499. execute cmd . 'cargo test --manifest-path' manifest func_name
  500. else
  501. execute cmd . 'cargo test --manifest-path' manifest func_name a:options
  502. endif
  503. endfunction
  504. " }}}1
  505. " vim: set et sw=4 sts=4 ts=8: