├── .github └── FUNDING.yml ├── .gitignore ├── CONTRIBUTING.markdown ├── README.markdown ├── compiler └── heroku.vim ├── doc └── heroku.txt └── plugin └── heroku.vim /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tpope 2 | custom: ["https://www.paypal.me/vimpope"] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.markdown: -------------------------------------------------------------------------------- 1 | See the [contribution guidelines for pathogen.vim](https://github.com/tpope/vim-pathogen/blob/master/CONTRIBUTING.markdown). 2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # heroku.vim 2 | 3 | A Vim plugin for interacting with Heroku. Yes really. Provided is a 4 | `:Heroku` command that wraps the [Heroku CLI][], featuring some pretty wicked 5 | tab complete. Plus it adds a command wrapper for each Heroku remote in your 6 | Git config, so `:Staging console` is only a few keystrokes away. 7 | 8 | [Heroku CLI]: https://devcenter.heroku.com/articles/heroku-cli 9 | 10 | ## Installation 11 | 12 | Install using your favorite package manager, or use Vim's built-in package 13 | support: 14 | 15 | mkdir -p ~/.vim/pack/tpope/start 16 | cd ~/.vim/pack/tpope/start 17 | git clone https://tpope.io/vim/heroku.git 18 | vim -u NONE -c "helptags heroku/doc" -c q 19 | 20 | You may also want to install [dispatch.vim][] and [fugitive.vim][] for 21 | asynchronous command execution and better Git repository detection, 22 | respectively. 23 | 24 | [dispatch.vim]: https://github.com/tpope/vim-dispatch 25 | [fugitive.vim]: https://github.com/tpope/vim-fugitive 26 | 27 | ## License 28 | 29 | Copyright © Tim Pope. Distributed under the same terms as Vim itself. 30 | See `:help license`. 31 | -------------------------------------------------------------------------------- /compiler/heroku.vim: -------------------------------------------------------------------------------- 1 | " Vim compiler file 2 | 3 | if exists("current_compiler") 4 | finish 5 | endif 6 | 7 | let current_compiler = "heroku" 8 | 9 | CompilerSet makeprg=heroku 10 | CompilerSet errorformat=%-G%\\e[?25h, 11 | \%\\&terminal=%\\C%\\%%(run%\\\|console%\\\|psql%\\\|pg:psql%\\\|local%\\\|local:start%\\)%\\>:%\\@!%\\ze%.%#, 12 | \%+I%.%# 13 | -------------------------------------------------------------------------------- /doc/heroku.txt: -------------------------------------------------------------------------------- 1 | *heroku.txt* Heroku CLI and hk wrapper 2 | 3 | Author: Tim Pope 4 | Repo: https://github.com/tpope/vim-heroku 5 | License: Same terms as Vim itself (see |license|) 6 | 7 | USAGE *heroku* *:Heroku* *:Hk* 8 | 9 | :Heroku [args] Run heroku with the given [args] using |:Dispatch| (or 10 | |:make| if unavailable). 11 | 12 | :Heroku run [args] Identical to :Heroku above, but uses |:Start| (or |:!| 13 | :Heroku console [args] if unavailable). 14 | :Heroku pg:psql [args] 15 | :Heroku local [args] 16 | 17 | When editing a file in a Git repository, every Heroku remote from that 18 | repository is camel cased and turned into a :Heroku command wrapper that 19 | passes the appropriate app argument. For example, if you have a remote named 20 | staging pointed at git@heroku.com:myapp-staging.git, calling :Staging restart 21 | is equivalent to calling :Heroku restart -a myapp-staging. 22 | 23 | vim:tw=78:et:ft=help:norl: 24 | -------------------------------------------------------------------------------- /plugin/heroku.vim: -------------------------------------------------------------------------------- 1 | " Location: plugin/heroku.vim 2 | " Maintainer: Tim Pope 3 | " Version: 1.0 4 | 5 | if exists("g:loaded_heroku") || v:version < 700 || &cp 6 | finish 7 | endif 8 | let g:loaded_heroku = 1 9 | 10 | function! s:heroku_json(args, default) abort 11 | if !executable('heroku') 12 | return a:default 13 | endif 14 | let output = system('heroku '.a:args.' --json') 15 | let string = matchstr(output, '[[{].*') 16 | if v:shell_error || empty(string) 17 | throw substitute(output, "\n$", '', '') 18 | endif 19 | if exists('*json_decode') 20 | try 21 | return json_decode(string) 22 | catch 23 | endtry 24 | else 25 | let [null, false, true] = ['', 0, 1] 26 | let stripped = substitute(string, '\C"\(\\.\|[^"\\]\)*"', '', 'g') 27 | if stripped !~# "[^,:{}\\[\\]0-9.\\-+Eaeflnr-u \n\r\t]" 28 | try 29 | return eval(substitute(string,"[\r\n]"," ",'g')) 30 | catch 31 | endtry 32 | endif 33 | endif 34 | throw "invalid JSON: ".string 35 | endfunction 36 | 37 | function! s:extract_app(args) abort 38 | let args = substitute(a:args, ' -- .*', '', '') 39 | let app = matchstr(args, '\s-\%(a\s*\|-app[= ]\s*\)\zs\S\+') 40 | if !empty(app) 41 | return app 42 | endif 43 | let remote = matchstr(args, '\s-\%(r\s*\|-remote[= ]\s*\)\zs\S\+') 44 | if has_key(get(b:, 'heroku_remotes', {}), remote) 45 | return b:heroku_remotes[remote] 46 | endif 47 | return '' 48 | endfunction 49 | 50 | function! s:prepare(args, app) abort 51 | let args = a:args 52 | let name = matchstr(args, '\a\S*') 53 | let command = s:command(name) 54 | if !empty(a:app) && !empty(name) && empty(s:extract_app(args)) && 55 | \ (type(get(command, 'flags')) != type({}) || 56 | \ has_key(command.flags, 'app')) 57 | let args = substitute(args, '\S\@<=\S\@!', ' -a '.a:app, '') 58 | endif 59 | return args 60 | endfunction 61 | 62 | function! s:dispatch(dir, app, bang, args) abort 63 | if a:args ==# '&' 64 | let s:complete_app = a:app 65 | return 66 | endif 67 | 68 | let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' 69 | let cwd = getcwd() 70 | let [mp, efm, cc] = [&l:mp, &l:efm, get(b:, 'current_compiler', '')] 71 | try 72 | let args = s:prepare(a:args, a:app) 73 | if a:args =~# '^\v\s*%(run|console|%(pg:)?psql|local%(:start)?)>:@!' && substitute(a:args, '-- .*', '', '') !~# ' -d\>' 74 | execute cd fnameescape(a:dir) 75 | let title = empty(a:app) ? 'heroku' : a:app 76 | let title .= ' '.matchstr(a:args, '^\s*\%(run\s\+\)\=\%(-a\s\+\S\+\s\+\)\=\zs\S\+') 77 | if exists(':Start') 78 | execute 'Start'.a:bang '-title='.escape(title, ' ') 'heroku' args 79 | else 80 | execute '!heroku' args 81 | endif 82 | else 83 | compiler heroku 84 | execute cd fnameescape(a:dir) 85 | execute (exists(':Make') == 2 ? 'Make'.a:bang : 'make!') args 86 | endif 87 | finally 88 | let [&l:mp, &l:efm, b:current_compiler] = [mp, efm, cc] 89 | if empty(cc) | unlet! b:current_compiler | endif 90 | execute cd fnameescape(cwd) 91 | endtry 92 | endfunction 93 | 94 | unlet! s:commands 95 | function! s:commands() abort 96 | if !exists('s:commands') 97 | let s:commands = {} 98 | try 99 | let list = s:heroku_json('commands', []) 100 | for command in type(list) == type({}) ? get(list, 'commands', []) : list 101 | let id = get(command, 'id', substitute(get(command, 'topic', '').':'.get(command, 'command', ''), ':$', '', '')) 102 | if !empty(id) 103 | let s:commands[id] = command 104 | endif 105 | for alias in get(command, 'aliases', []) 106 | let s:commands[alias] = command 107 | endfor 108 | endfor 109 | if !has_key(s:commands, 'local') 110 | let s:commands['local'] = get(s:commands, 'local:start', {}) 111 | endif 112 | catch 113 | if &verbose 114 | echomsg "Could not determine Heroku commands" 115 | endif 116 | endtry 117 | lockvar s:commands 118 | endif 119 | return s:commands 120 | endfunction 121 | 122 | function! s:command(name, ...) abort 123 | let command = empty(a:name) ? {} : get(s:commands(), a:name, {}) 124 | if a:0 125 | return get(command, a:1, a:0 > 1 ? a:2 : '') 126 | else 127 | return command 128 | endif 129 | endfunction 130 | 131 | let s:completers = {} 132 | let s:app_completers = {} 133 | 134 | function! s:completers.app(...) abort 135 | return map(s:heroku_json('apps -A', []), 'v:val.name') 136 | endfunction 137 | let s:completers.confirm = s:completers.app 138 | 139 | function! s:completers.org(...) abort 140 | return map(s:heroku_json('teams', []), 'v:val.name') 141 | endfunction 142 | 143 | function! s:completers.plan(arg, ...) abort 144 | return map(s:heroku_json('addons:plans '.matchstr(a:arg, '.*\ze:'), []), 'v:val.name') 145 | endfunction 146 | 147 | function! s:completers.region(...) abort 148 | return map(s:heroku_json('regions', []), 'v:val.name') 149 | endfunction 150 | 151 | function! s:app_completers.release(app, ...) abort 152 | return map(s:heroku_json('releases -a '.a:app, []), '"v".v:val.version') 153 | endfunction 154 | 155 | function! s:completers.remote(...) abort 156 | return keys(get(b:, 'heroku_remotes', {})) 157 | endfunction 158 | 159 | function! s:completers.service(...) abort 160 | return map(s:heroku_json('addons:services', []), 'v:val.name') 161 | endfunction 162 | 163 | function! s:completers.space(...) abort 164 | return map(s:heroku_json('spaces', []), 'v:val.name') 165 | endfunction 166 | 167 | function! s:completers.topic(...) abort 168 | return sort(keys(s:commands())) 169 | endfunction 170 | 171 | function! s:app_completers.addon(app, ...) abort 172 | return map(s:heroku_json('addons -a '.a:app, []), 'v:val.name') 173 | endfunction 174 | 175 | function! s:completers.addon(...) abort 176 | return map(s:heroku_json('addons --all', []), 'v:val.name') 177 | endfunction 178 | 179 | function! s:completion_for(type, app, arg) abort 180 | let type = a:type 181 | if type =~ ':' 182 | let type = matchstr(type, '[^:]\+' . (a:arg =~ ':' ? '$' : '')) 183 | endif 184 | if !empty(a:app) && has_key(s:app_completers, type) 185 | return s:app_completers[type](a:app, a:arg) 186 | elseif has_key(s:completers, type) 187 | return s:completers[type](a:arg) 188 | else 189 | return [] 190 | endif 191 | endfunction 192 | 193 | function! s:complete_command(cmd, app, A, L, P) abort 194 | let opt = matchstr(strpart(a:L, 0, a:P), ' \zs\%(-[a-z]\|--[[:alnum:]-]\+[ =]\@=\)\ze\%(=\|\s*\)\S*$') 195 | let command = s:command(a:cmd) 196 | if empty(command) 197 | return [] 198 | endif 199 | let flags = {} 200 | if type(get(command, 'flags')) ==# type({}) 201 | for flag in values(command.flags) 202 | let desc = flag.type ==# 'boolean' ? '' : flag.name 203 | let flags['--'.flag.name] = desc 204 | if !empty(get(flag, 'char', '')) 205 | let flags['-'.flag.char] = desc 206 | endif 207 | endfor 208 | endif 209 | if !empty(get(flags, opt)) 210 | return s:completion_for(flags[opt], a:app, a:A) 211 | endif 212 | let options = [] 213 | for arg in type(get(command, 'args')) ==# type([]) ? command.args : [] 214 | if type(get(arg, 'name')) == type('') 215 | let options += s:completion_for(arg.name, a:app, a:A) 216 | endif 217 | endfor 218 | return options + sort(keys(flags)) 219 | endfunction 220 | 221 | function! s:completion_filter(results, A) abort 222 | return join(a:results, "\n") 223 | endfunction 224 | 225 | function! s:Complete(A, L, P) abort 226 | silent! execute matchstr(a:L, '\u\a*') '&' 227 | return CompilerComplete_heroku(a:A, a:L, a:P, s:complete_app) 228 | endfunction 229 | 230 | function! CompilerComplete_heroku(A, L, P, ...) abort 231 | let app = s:extract_app(a:L) 232 | let app = len(app) ? app : (a:0 ? a:1 : '') 233 | let cmd = matchstr(strpart(a:L, 0, a:P), '[! ]\zs\(\S\+\)\ze\s\+') 234 | if !empty(cmd) && cmd !=# 'help' 235 | let results = s:complete_command(cmd, app, a:A, a:L, a:P) 236 | if !empty(app) 237 | call filter(results, 'v:val !~# "^-\\%([ar]\\|-app\\|-remote\\)$"') 238 | endif 239 | return s:completion_filter(results, a:A) 240 | endif 241 | return s:completion_filter(s:completers.topic(), a:A) 242 | endfunction 243 | 244 | function! s:Detect(git_dir) abort 245 | if empty(a:git_dir) 246 | return 247 | endif 248 | let b:heroku_remotes = {} 249 | if filereadable(a:git_dir.'/config') 250 | for line in readfile(a:git_dir.'/config') 251 | let remote = matchstr(line, '^\s*\[\s*remote\s\+"\zs.*\ze"\s*\]\s*$') 252 | if !empty(remote) 253 | let alias = remote 254 | endif 255 | let app = matchstr(line, '^\s*url\s*=.*heroku.com[:/]\zs.*\ze\.git\s*$') 256 | if !empty(app) 257 | let b:heroku_remotes[alias] = app 258 | endif 259 | endfor 260 | endif 261 | for [remote, app] in items(b:heroku_remotes) 262 | let command = substitute(remote, '\%(^\|[-_]\+\)\(\w\)', '\u\1', 'g') 263 | execute 'command! -bar -bang -buffer -nargs=? -complete=custom,s:Complete' command 264 | \ 'call s:dispatch(' . string(fnamemodify(a:git_dir, ':h')) . ', ' . string(app) . ', "", )' 265 | endfor 266 | endfunction 267 | 268 | function! Heroku_db_canonicalize(url) abort 269 | if a:url !~# '^heroku:[[:alnum:]-]*\%(#[[:alnum:]_]*\)\=$' 270 | throw 'DB: Invalid Heroku app '.string(a:url) 271 | endif 272 | let app = matchstr(a:url, ':\zs[^#]*') 273 | let cmd = 'heroku config:get '.(empty(app) ? '' : '-a '.app.' ') 274 | let var = matchstr(a:url, '#\zs.*') 275 | let var = empty(var) ? 'DATABASE_URL' : var 276 | if !executable('heroku') 277 | throw 'DB: No heroku command' 278 | endif 279 | let out = get(split(system(cmd.var), "\n"), -1) 280 | if empty(out) 281 | let out = get(split(system(cmd.var.'_URL'), "\n"), -1) 282 | endif 283 | if out =~# '^[A-Z_]\+$' 284 | let out = get(split(system(cmd.out), "\n"), 0) 285 | endif 286 | if !empty(out) && !v:shell_error && out =~# '^\w\+:' && out !~# '^heroku:' 287 | return out 288 | endif 289 | throw v:shell_error ? 'DB: '.out : 'DB: could not find database URL for Heroku app' 290 | endfunction 291 | 292 | function! Heroku_db_complete_opaque(url) abort 293 | if executable('heroku') 294 | return filter(map(split(system('heroku apps -A'), "\n"), 295 | \ 'matchstr(v:val, "^\\w\\S*")'), 'len(v:val)') 296 | endif 297 | return '' 298 | endfunction 299 | 300 | function! Heroku_db_complete_fragment(url, ...) abort 301 | let app = matchstr(a:url, ':\zs[^#]*') 302 | if executable('heroku') 303 | let env = split(system('heroku config' . (empty(app) ? '' : ' -a '.app)), "\n") 304 | if !v:shell_error 305 | let filter = '^\w\+:\s*\%([A-Z][A-Z0-9_]*_URL$\|[a-z0-9-+.]\+://\)' 306 | return map(filter(env, 'v:val =~# filter'), 'matchstr(v:val, "^.\\{-\\}\\ze\\%(_URL\\)\\=:")') 307 | endif 308 | endif 309 | return '' 310 | endfunction 311 | 312 | let g:db_adapter_heroku = 'Heroku_db_' 313 | 314 | augroup heroku 315 | autocmd! 316 | autocmd BufNewFile,BufReadPost * 317 | \ if exists('*FugitiveCommonDir') && exists('*FugitiveConfigGetRegexp') | 318 | \ call s:Detect(FugitiveCommonDir(+expand(''))) | 319 | \ elseif exists('*FugitiveExtractGitDir') | 320 | \ call s:Detect(FugitiveExtractGitDir(expand(':p'))) | 321 | \ else | 322 | \ call s:Detect(finddir('.git', '.;')) | 323 | \ endif 324 | augroup END 325 | 326 | command! -bar -bang -nargs=? -complete=custom,CompilerComplete_heroku 327 | \ Hk call s:dispatch(getcwd(), '', '', ) 328 | 329 | command! -bar -bang -nargs=? -complete=custom,CompilerComplete_heroku 330 | \ Heroku call s:dispatch(getcwd(), '', '', ) 331 | --------------------------------------------------------------------------------