├── LICENSE ├── README.md ├── demo.gif └── plugin └── crates.vim /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present, Marco Hinz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-crates 2 | 3 | When maintaining Rust projects, this plugin helps with updating the dependencies 4 | in `Cargo.toml` files. It uses the [crates.io](https://crates.io) API to get all 5 | available versions of a crate and caches them. 6 | 7 | _[curl](https://curl.haxx.se) needs to be installed._ 8 | 9 | - **Insert completion** 10 | 11 | If the cursor is on a [version requirement](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies) 12 | and in insert mode, use `` (hold Ctrl and hit 13 | x then u) to open a completion menu with all available 14 | versions (see `:h i_CTRL-X_CTRL-U`). 15 | 16 | - **:CratesUp** 17 | 18 | Update the current dependency to the latest non-prerelease version. 19 | 20 | - **:CratesToggle** 21 | 22 | For each dependency that is out-of-date, indicate the latest version as virtual 23 | text after the end of the line. Use it again to remove all indicators. This is 24 | a [Nvim](https://github.com/neovim/neovim/)-only feature. 25 | 26 | Customize the colors of the indicators like this: 27 | 28 | ```vim 29 | highlight Crates ctermfg=green ctermbg=NONE cterm=NONE 30 | " or link it to another highlight group 31 | highlight link Crates WarningMsg 32 | ``` 33 | Use `:verb CratesToggle` to see debug messages. 34 | 35 | Inspired by [serayuzgur/crates](https://github.com/serayuzgur/crates). 36 | 37 | Happy 🦀 everyone! 38 | 39 | ## Configuration 40 | 41 | Automatically run `:CratesToggle` when opening a `Cargo.toml` file: 42 | 43 | ```vim 44 | if has('nvim') 45 | autocmd BufRead Cargo.toml call crates#toggle() 46 | endif 47 | ``` 48 | 49 | ## Demo 50 | 51 | ![](https://raw.githubusercontent.com/mhinz/vim-crates/master/demo.gif) 52 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhinz/vim-crates/f6f13113997495654a58f27d7169532c0d125214/demo.gif -------------------------------------------------------------------------------- /plugin/crates.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_crates') 2 | finish 3 | endif 4 | 5 | " curl -s https://crates.io/api/v1/crates/cargo_metadata/versions | jq '.versions[].num' 6 | 7 | let s:api = 'https://crates.io/api/v1' 8 | 9 | highlight default Crates 10 | \ ctermfg=white ctermbg=198 cterm=NONE 11 | \ guifg=#ffffff guibg=#fc3790 gui=NONE 12 | 13 | " @return [crate, version] 14 | function! s:cargo_file_parse_line(line, lnum) abort 15 | if a:line =~# '^version' 16 | " [dependencies.my-crate] 17 | " version = "1.2.3" 18 | let vers = matchstr(a:line, '^version = "\zs[0-9.\*]\+\ze') 19 | if empty(vers) 20 | break 21 | endif 22 | for lnum in reverse(range(1, a:lnum)) 23 | let crate = matchstr(getline(lnum), '^\[.*dependencies\.\zs.*\ze]$') 24 | if !empty(crate) 25 | return [crate, vers] 26 | endif 27 | endfor 28 | elseif a:line =~ '^[[:alnum:]\-_]* = "' 29 | " my-crate = "1.2.3" 30 | return matchlist(a:line, '^\([[:alnum:]\-_]\+\) = "\([0-9.\*]\+\)"')[1:2] 31 | elseif a:line =~# 'version' 32 | " my-crate = { version = "1.2.3" } 33 | return matchlist(a:line, '^\([[:alnum:]\-_]\+\) = {.*version = "\([0-9.\*]\+\)"')[1:2] 34 | endif 35 | if &verbose 36 | echomsg 'Skipped:' a:line 37 | endif 38 | return ['', -1] 39 | endfunction 40 | 41 | function! s:job_callback_nvim_stdout(_job_id, data, _event) dict abort 42 | let self.stdoutbuf[-1] .= a:data[0] 43 | call extend(self.stdoutbuf, a:data[1:]) 44 | endfunction 45 | 46 | function! s:job_callback_nvim_exit(_job_id, exitval, _event) dict abort 47 | call self.callback(a:exitval) 48 | endfunction 49 | 50 | function! s:callback_show_latest_version(exitval) dict abort 51 | if a:exitval 52 | echomsg "D'oh! Got ". a:exitval 53 | return 54 | endif 55 | let data = json_decode(self.stdoutbuf[0]) 56 | if !has_key(data, 'versions') 57 | if self.verbose 58 | echomsg self.crate .': '. string(data) 59 | endif 60 | return 61 | endif 62 | let b:crates[self.crate] = map(data.versions, 'v:val.num') 63 | call s:virttext_add_version(self.lnum, self.vers, s:cache(self.crate)) 64 | endfunction 65 | 66 | function! s:virttext_add_version(lnum, vers_current, vers_latest) 67 | if s:show(a:vers_current, a:vers_latest) 68 | call nvim_buf_set_virtual_text(bufnr(''), nvim_create_namespace('crates'), 69 | \ a:lnum, [[' '. a:vers_latest .' ', 'Crates']], {}) 70 | endif 71 | endfunction 72 | 73 | function! s:crates_io_cmd(crate, async) abort 74 | let url = printf('%s/crates/%s/versions', s:api, a:crate) 75 | let useragent = 'vim-crates (https://github.com/mhinz/vim-crates)' 76 | if !a:async 77 | let useragent = shellescape(useragent) 78 | endif 79 | return ['curl', '-sLA', useragent, url] 80 | endfunction 81 | 82 | function! s:make_request_sync(crate) 83 | let result = system(join(s:crates_io_cmd(a:crate, 0))) 84 | if v:shell_error 85 | return v:shell_error 86 | endif 87 | let b:crates[a:crate] = map(json_decode(result).versions, 'v:val.num') 88 | return 0 89 | endfunction 90 | 91 | function! s:make_request_async(cmd, crate, vers, lnum, callback) abort 92 | call jobstart(a:cmd, { 93 | \ 'crate': a:crate, 94 | \ 'vers': a:vers, 95 | \ 'lnum': a:lnum, 96 | \ 'callback': a:callback, 97 | \ 'verbose': &verbose, 98 | \ 'stdoutbuf': [''], 99 | \ 'on_stdout': function('s:job_callback_nvim_stdout'), 100 | \ 'on_exit': function('s:job_callback_nvim_exit'), 101 | \ }) 102 | endfunction 103 | 104 | " Show latest version if it's outside of the given requirement. 105 | function! s:show(a, b) abort 106 | let has_wildcard = match(a:a, '\*') != -1 107 | let a = split(a:a, '\.') 108 | let b = split(a:b, '\.') 109 | return has_wildcard ? s:show_wildcard(a, b) : s:show_caret(a, b) 110 | endfunction 111 | 112 | " Handle caret requirements. 113 | " ^1.2.3 := >=1.2.3 <2.0.0 114 | " ^1.2 := >=1.2.0 <2.0.0 115 | " ^1 := >=1.0.0 <2.0.0 116 | " ^0.2.3 := >=0.2.3 <0.3.0 117 | " ^0.2 := >=0.2.0 <0.3.0 118 | " ^0.0.3 := >=0.0.3 <0.0.4 119 | " ^0.0 := >=0.0.0 <0.1.0 120 | " ^0 := >=0.0.0 <1.0.0 121 | function! s:show_caret(a, b) abort 122 | for i in range(min([len(a:a), 3])) 123 | let a = str2nr(a:a[i]) 124 | let b = str2nr(a:b[i]) 125 | if a == 0 && b == 0 | continue | endif 126 | return a < b ? 1 : 0 127 | endfor 128 | return 0 129 | endfunction 130 | 131 | " Handle wildcard requirements. 132 | " * := >=0.0.0 133 | " 1.* := >=1.0.0 <2.0.0 134 | " 1.2.* := >=1.2.0 <1.3.0 135 | function! s:show_wildcard(a, b) abort 136 | for i in range(min([len(a:a), 3])) 137 | if a:a[i] == '*' | break | endif 138 | let a = str2nr(a:a[i]) 139 | let b = str2nr(a:b[i]) 140 | if a < b | return 1 | endif 141 | endfor 142 | return 0 143 | endfunction 144 | 145 | function! s:cache(crate) abort 146 | return filter(copy(b:crates[a:crate]), 'v:val !~ "\\a"')[0] 147 | endfunction 148 | 149 | function! g:CratesComplete(findstart, base) 150 | if a:findstart 151 | let line = getline('.') 152 | let start = col('.') - 1 153 | while start > 0 && line[start - 1] =~ '[0-9.]' 154 | let start -= 1 155 | endwhile 156 | return start 157 | else 158 | let crate = matchstr(getline('.'), '^[a-z\-_0-9]\+') 159 | if !exists('b:crates') 160 | let b:crates = {} 161 | endif 162 | if !has_key(b:crates, crate) 163 | if s:make_request_sync(crate) != 0 164 | return [] 165 | endif 166 | endif 167 | return filter(copy(b:crates[crate]), 'v:val =~ "^'.a:base.'"') 168 | endif 169 | endfunction 170 | 171 | function! s:crates() abort 172 | if !has('nvim') 173 | echomsg 'Sorry, this is a Nvim-only feature.' 174 | return 175 | endif 176 | call s:virttext_clear('crates') 177 | if !exists('b:crates') 178 | let b:crates = {} 179 | endif 180 | let lnum = 0 181 | let in_dep_section = 0 182 | 183 | for line in getline(1, '$') 184 | if line =~# '^\[.*dependencies.*\]$' 185 | let in_dep_section = 1 186 | elseif line[0] == '[' 187 | let in_dep_section = 0 188 | elseif line[0] == '#' 189 | elseif empty(line) 190 | elseif in_dep_section 191 | try 192 | let [crate, vers] = s:cargo_file_parse_line(line, lnum) 193 | catch 194 | if &verbose 195 | echomsg 'Failed parsing line:' line 196 | endif 197 | let lnum += 1 198 | continue 199 | endtry 200 | if !empty(crate) 201 | if has_key(b:crates, crate) 202 | call s:virttext_add_version(lnum, vers, s:cache(crate)) 203 | else 204 | call s:make_request_async(s:crates_io_cmd(crate, 1), crate, vers, lnum, 205 | \ function('s:callback_show_latest_version')) 206 | endif 207 | endif 208 | endif 209 | let lnum += 1 210 | endfor 211 | endfunction 212 | 213 | function! s:virttext_clear(ns) abort 214 | call nvim_buf_clear_namespace(bufnr(''), nvim_create_namespace(a:ns), 0, -1) 215 | endfunction 216 | 217 | function! crates#toggle() abort 218 | if !exists('b:crates_toggle') 219 | let b:crates_toggle = 0 220 | endif 221 | if b:crates_toggle == 0 222 | call s:crates() 223 | " By now all the latest versions are cached, so updating the virttext on 224 | " each save is a very fast operation. 225 | augroup crates_toggle 226 | autocmd BufWritePost call s:crates() 227 | augroup END 228 | else 229 | call s:virttext_clear('crates') 230 | autocmd! crates_toggle 231 | endif 232 | let b:crates_toggle = !b:crates_toggle 233 | endfunction 234 | 235 | function! crates#up() abort 236 | if !exists('b:crates') 237 | let b:crates = {} 238 | endif 239 | let lnum = line('.') 240 | let line = getline('.') 241 | let [crate, vers] = s:cargo_file_parse_line(line, lnum) 242 | if empty(crate) 243 | echomsg 'No version on this line.' 244 | return 245 | endif 246 | if !has_key(b:crates, crate) && s:make_request_sync(crate) != 0 247 | echomsg 'curl failed!' 248 | return 249 | endif 250 | let vers_latest = s:cache(crate) 251 | if line =~# 'version\s*=' 252 | let line = substitute(line, 'version\s*=\s*"\zs[0-9\.\*]\+\ze"', vers_latest, '') 253 | else 254 | let line = substitute(line, '"\zs[0-9\.\*]\+\ze"', vers_latest, '') 255 | endif 256 | call setline(lnum, line) 257 | call nvim_buf_clear_namespace(bufnr(''), nvim_create_namespace('crates'), 258 | \ line('.')-1, line('.')) 259 | endfunction 260 | 261 | function! s:setup() abort 262 | setlocal completefunc=CratesComplete 263 | command! -bar CratesToggle call crates#toggle() 264 | command! -bar CratesUp call crates#up() 265 | endfunction 266 | 267 | augroup crates 268 | autocmd BufRead Cargo.toml call s:setup() 269 | augroup END 270 | 271 | let g:loaded_crates = 1 272 | --------------------------------------------------------------------------------