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