├── .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 |
--------------------------------------------------------------------------------