rust.vim (16975B)
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 5 function! rust#Load() 6 " Utility call to get this script loaded, for debugging 7 endfunction 8 9 function! 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 18 endfunction 19 20 " Include expression {{{1 21 22 function! 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 55 endfunction 56 57 " Jump {{{1 58 59 function! rust#Jump(mode, function) range 60 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 72 endfunction 73 74 function! s:Jump_Back() 75 call search('{', 'b') 76 keepjumps normal! w99[{ 77 endfunction 78 79 function! s:Jump_Forward() 80 normal! j0 81 call search('{', 'b') 82 keepjumps normal! w99[{% 83 call search('{') 84 endfunction 85 86 " Run {{{1 87 88 function! rust#Run(bang, args) 89 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 102 103 let b:rust_last_rustc_args = l:rustc_args 104 let b:rust_last_args = l:args 105 106 call s:WithPath(function("s:Run"), rustc_args, args) 107 endfunction 108 109 function! s:Run(dict, rustc_args, args) 110 let exepath = a:dict.tmpdir.'/'.fnamemodify(a:dict.path, ':t:r') 111 if has('win32') 112 let exepath .= '.exe' 113 endif 114 115 let relpath = get(a:dict, 'tmpdir_relpath', a:dict.path) 116 let rustc_args = [relpath, '-o', exepath] + a:rustc_args 117 118 let rustc = exists("g:rustc_path") ? g:rustc_path : "rustc" 119 120 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 130 endfunction 131 132 " Expand {{{1 133 134 function! rust#Expand(bang, args) 135 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) 142 endfunction 143 144 function! s:Expand(dict, pretty, args) 145 try 146 let rustc = exists("g:rustc_path") ? g:rustc_path : "rustc" 147 148 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 186 endfunction 187 188 function! rust#CompleteExpand(lead, line, pos) 189 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 197 198 return glob(escape(a:lead, "*?[") . '*', 0, 1) 199 endfunction 200 201 " Emit {{{1 202 203 function! rust#Emit(type, args) 204 let args = s:ShellTokenize(a:args) 205 call s:WithPath(function("s:Emit"), a:type, args) 206 endfunction 207 208 function! s:Emit(dict, type, args) 209 try 210 let output_path = a:dict.tmpdir.'/output' 211 212 let rustc = exists("g:rustc_path") ? g:rustc_path : "rustc" 213 214 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 256 endfunction 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. 272 function! s:WithPath(func, ...) 273 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) 281 282 " Always create a tmpdir in case the wrapped command wants it 283 let dict.tmpdir = tempname() 284 call mkdir(dict.tmpdir) 285 286 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 296 297 let saved.mod = &modified 298 set nomodified 299 300 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 308 309 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 319 endfunction 320 321 function! rust#AppendCmdLine(text) 322 call setcmdpos(getcmdpos()) 323 let cmd = getcmdline() . a:text 324 return cmd 325 endfunction 326 327 " Tokenize the string according to sh parsing rules 328 function! s:ShellTokenize(text) 329 " 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 390 endfunction 391 392 function! s:RmDir(path) 393 " 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 411 endfunction 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. 415 function! s:system(pwd, cmd) 416 let cmd = a:cmd 417 if !empty(a:pwd) 418 let cmd = 'cd ' . shellescape(a:pwd) . ' && ' . cmd 419 endif 420 return system(cmd) 421 endfunction 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 427 function! s:has_webapi() 428 if !exists("*webapi#http#post") 429 try 430 call webapi#http#post() 431 catch 432 endtry 433 endif 434 return exists("*webapi#http#post") 435 endfunction 436 437 function! 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() 444 echohl ErrorMsg | echomsg ':RustPlay depends on webapi.vim (https://github.com/mattn/webapi-vim)' | echohl None 445 return 446 endif 447 448 let bufname = bufname('%') 449 if a:count < 1 450 let content = join(getline(a:line1, a:line2), "\n") 451 else 452 let save_regcont = @" 453 let save_regtype = getregtype('"') 454 silent! normal! gvy 455 let content = @" 456 call setreg('"', save_regcont, save_regtype) 457 endif 458 459 let url = l:rust_playpen_url."?code=".webapi#http#encodeURI(content) 460 461 if strlen(url) > 5000 462 echohl ErrorMsg | echomsg 'Buffer too large, max 5000 encoded characters ('.strlen(url).')' | echohl None 463 return 464 endif 465 466 let payload = "format=simple&url=".webapi#http#encodeURI(url) 467 let res = webapi#http#post(l:rust_shortener_url.'create.php', payload, {}) 468 if res.status[0] ==# '2' 469 let url = res.content 470 endif 471 472 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 480 endfunction 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. 486 function! 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*') 516 endfunction 517 518 function! 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 566 endfunction 567 568 " }}}1 569 570 " vim: set et sw=4 sts=4 ts=8: