From ef6ac42cfccac7e546ae03ee4acb07d3ab10c1e0 Mon Sep 17 00:00:00 2001 From: Tim Pope Date: Mon, 1 Apr 2013 19:52:40 -0400 Subject: [PATCH] dispatch.vim 1.0 --- .gitignore | 1 + CONTRIBUTING.markdown | 1 + README.markdown | 143 +++++++++++ autoload/dispatch.vim | 448 +++++++++++++++++++++++++++++++++ autoload/dispatch/headless.vim | 27 ++ autoload/dispatch/iterm.vim | 67 +++++ autoload/dispatch/screen.vim | 32 +++ autoload/dispatch/tmux.vim | 96 +++++++ autoload/dispatch/windows.vim | 50 ++++ autoload/dispatch/x11.vim | 28 +++ doc/dispatch.txt | 128 ++++++++++ plugin/dispatch.vim | 37 +++ 12 files changed, 1058 insertions(+) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.markdown create mode 100644 README.markdown create mode 100644 autoload/dispatch.vim create mode 100644 autoload/dispatch/headless.vim create mode 100644 autoload/dispatch/iterm.vim create mode 100644 autoload/dispatch/screen.vim create mode 100644 autoload/dispatch/tmux.vim create mode 100644 autoload/dispatch/windows.vim create mode 100644 autoload/dispatch/x11.vim create mode 100644 doc/dispatch.txt create mode 100644 plugin/dispatch.vim diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a56e3f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/doc/tags diff --git a/CONTRIBUTING.markdown b/CONTRIBUTING.markdown new file mode 100644 index 0000000..b3f0095 --- /dev/null +++ b/CONTRIBUTING.markdown @@ -0,0 +1 @@ +See the [contribution guidelines for pathogen.vim](https://github.com/tpope/vim-pathogen/blob/master/CONTRIBUTING.markdown). diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..8f71d41 --- /dev/null +++ b/README.markdown @@ -0,0 +1,143 @@ +# dispatch.vim + +Leverage the power of Vim's compiler plugins without being bound by +synchronicity. Kick off builds and test suites using one of several +asynchronous adapters (including tmux, screen, iTerm, Windows, and a headless +mode), and when the job completes, errors will be loaded and parsed +automatically. + +If that doesn't excite you, then perhaps [this video][teaser] will change your +mind. + +[teaser]: http://vimeo.com/tpope/vim-dispatch-teaser + +## Installation + +If you don't have a preferred installation method, I recommend +installing [pathogen.vim](https://github.com/tpope/vim-pathogen), and +then simply copy and paste: + + cd ~/.vim/bundle + git clone git://github.com/tpope/vim-dispatch.git + +## Usage + +The core of Vim's compiler system is `:make`, a command similar to `:grep` +that runs a build tool and parses the resulting errors. The default build +tool is of course `make`, but you can switch it (and the associated error +parser) with `:compiler`. There are lots of built-in compilers, and they do +more than just compile things. Plus you can make your own. + +We'll start by looking at dispatch.vim's `:make` wrapper `:Make`, and then +move on to higher abstractions. + +### Foreground builds + +Kick off quick tasks with `:Make`. What happens next depends on which adapter +takes charge. + +* If you're in tmux, a small split will be opened at the bottom. +* If you have iTerm running, a new tab is opened. +* On Windows, a minimized cmd.exe window is spawned. +* Otherwise, you get a plain old `:make invocation`. + +When the task completes, the window closes, the errors are loaded and parsed, +and the quickfix window automatically opens. At no point will your focus be +stolen. + +### Background builds + +Use `:Make!` for longer running tasks, like "run the entire test suite". + +* If you're in tmux or GNU screen, a new window is created in the background. +* If you have iTerm running, a new tab is opened but not selected. +* Windows still spawns a minimized cmd.exe window. +* Otherwise, you get a headless invocation. You can't see it, but it's + running in the background. + +You won't be interrupted with a quickfix window for a background build. +Instead, open it at your leisure with `:Copen`. + +You can also use `:Copen` on a build that's still running to retrieve and +parse any errors that have already happened. + +### Compiler switching + +As hinted earlier, it's easy to switch compilers. + + :compiler rubyunit + :make test/models/user_test.rb + +Wait, that's still twice as many commands as it needs to be. Plus, it +requires you to make the leap from `testrb` (the executable) to `rubyunit` +(the compiler plugin). The `:Dispatch` command looks for a compiler for an +executable and sets it up automatically. + + :Dispatch testrb test/models/user_test.rb + +If no compiler plugin is found, `:Dispatch` simply captures all output. + + :Dispatch bundle install + +As with `:make`, you can use `%` expansions for the current filename. + + :Dispatch rspec % + +The `:Dispatch` command switches the compiler back afterwards, so you can pick +a primary compiler for `:Make`, and use `:Dispatch` for secondary concerns. + +### Default dispatch + +With no arguments, `:Dispatch` looks for a `b:dispatch` variable. You +can set it interactively, or in an autocommand: + + autocmd FileType java let b:dispatch = 'javac %' + +If no `b:dispatch` is found, it falls back to `:Make`. + +`:Dispatch` makes a great map: + + nnoremap :Dispatch + +### Focusing + +Use `:FocusDispatch` (or just `:Focus`) to temporarily, globally override the +default dispatch: + + :Focus rake spec:models + +Now every bare call to `:Dispatch` will call `:Dispatch rake spec:models`. +You'll be getting a lot of mileage out of that `:Dispatch` map. + +Use `:Focus!` to reset back to the default. + +### Spawning interactive processes + +Sometimes you just want to kick off a process without any output capturing or +error parsing. That's what `:Start` is for: + + :Start lein repl + +Unlike `:Make`, the new window will be in focus, since the idea is that you +want to interact with it. Use `:Start!` to launch it in the background. + +### Plugin support + +Using dispatch.vim from a plugin is a simple matter of checking for and using +`:Make` and `:Start` if they're available instead of `:make` and `:!`. Your +favorite plugin already supports it, assuming your favorite plugin is +[rails.vim](https://github.com/tpope/rails). + +## Self-Promotion + +Like dispatch.vim? Follow the repository on +[GitHub](https://github.com/tpope/vim-dispatch) and vote for it on +[vim.org](http://www.vim.org/scripts/script.php?script_id=4504). And if +you're feeling especially charitable, follow [tpope](http://tpo.pe/) on +[Twitter](http://twitter.com/tpope) and +[GitHub](https://github.com/tpope). + +## License + +Copyright © Tim Pope. Distributed under the same terms as Vim itself. +See `:help license`. diff --git a/autoload/dispatch.vim b/autoload/dispatch.vim new file mode 100644 index 0000000..335f5fc --- /dev/null +++ b/autoload/dispatch.vim @@ -0,0 +1,448 @@ +" autoload/dispatch.vim +if exists('g:autoloaded_dispatch') + finish +endif + +let g:autoloaded_dispatch = 1 + +" Utility {{{1 + +function! dispatch#uniq(list) abort + let i = 0 + let seen = {} + while i < len(a:list) + if (a:list[i] ==# '' && exists('empty')) || has_key(seen,a:list[i]) + call remove(a:list,i) + elseif a:list[i] ==# '' + let i += 1 + let empty = 1 + else + let seen[a:list[i]] = 1 + let i += 1 + endif + endwhile + return a:list +endfunction + +function! dispatch#shellescape(...) abort + let args = [] + for arg in a:000 + if arg =~ '^[A-Za-z0-9_/.-]\+$' + let args += [arg] + else + let args += [shellescape(arg)] + endif + endfor + return join(args, ' ') +endfunction + +let s:flags = '\%(:[p8~.htre]\|:g\=s\(\.\).\{-\}\1.\{-\}\1\)*' +let s:expandable = '\\*\%(<\w\+>\|%\|#\d*\)' . s:flags +function! dispatch#expand(string) abort + return substitute(a:string, s:expandable, '\=s:expand(submatch(0))', 'g') +endfunction + +function! s:expand(string) + let slashes = len(matchstr(a:string, '^\%(\\\\\)*')) + return repeat('\', slashes/2) . expand(a:string[slashes : -1]) +endfunction + +function! dispatch#slash() abort + return !exists("+shellslash") || &shellslash ? '/' : '\' +endfunction + +function! dispatch#shellpipe(file) abort + if &shellpipe =~# '%s' + return ' ' . printf(&shellpipe, dispatch#shellescape(a:file)) + else + return ' ' . &shellpipe . ' ' . dispatch#shellescape(a:file) + endif +endfunction + +function! dispatch#vim_executable() abort + if !exists('s:vim') + if has('win32') + let root = fnamemodify($VIMRUNTIME, ':8') . dispatch#slash() + elseif has('gui_macvim') + let root = fnamemodify($VIM, ':h:h') . '/MacOS/' + else + let root = fnamemodify($VIM, ':h:h') . '/bin/' + endif + if executable(root . v:progname) + let s:vim = root . v:progname + elseif executable(v:progname) + let s:vim = v:progname + else + let s:vim = 'vim' + endif + endif + return s:vim +endfunction + +function! dispatch#callback(request) abort + if !empty(v:servername) + return dispatch#shellescape(dispatch#vim_executable()) . + \ ' --servername ' . dispatch#shellescape(v:servername) . + \ ' --remote-expr "' . 'DispatchComplete(' . s:request(a:request).id . ')' . '"' + endif + return '' +endfunction + +function! dispatch#prepare_make(request, ...) abort + let exec = 'echo $$ > ' . a:request.file . '.pid; ' + if executable('perl') + let exec .= 'perl -e "select(undef,undef,undef,0.1)"; ' + else + let exec .= 'sleep 1; ' + endif + let exec = a:0 ? a:1 : (a:request.expanded . dispatch#shellpipe(a:request.file)) + + let after = 'rm -f ' . a:request.file . '.pid; ' . + \ 'touch ' . a:request.file . '.complete; ' . + \ dispatch#callback(a:request) + if &shellpipe =~# '2>&1' + return 'trap ' . shellescape(after) . ' EXIT INT TERM; ' . exec + else + " csh + return exec . '; ' . after + endif +endfunction + +function! s:set_current_compiler(name) + if empty(a:name) + unlet! b:current_compiler + else + let b:current_compiler = a:name + endif +endfunction + +function! s:dispatch(request) abort + for handler in g:dispatch_handlers + let response = call('dispatch#'.handler.'#handle', [a:request]) + if !empty(response) + redraw + echo ':!'.a:request.expanded . ' ('.handler.')' + return 1 + endif + endfor + return 0 +endfunction + +" }}}1 +" :Start {{{1 + +function! dispatch#start(command, ...) abort + let title = matchstr(a:command, '-title=\zs\%(\\.\|\S\)*') + if !empty(title) + let command = a:command[strlen(title) + 8 : -1] + else + let command = a:command + endif + if empty(command) + let command = &shell + endif + if empty(title) + let title = fnamemodify(matchstr(command, '\%(\\.\|\S\)\+'), ':t:r') + endif + let title = substitute(title, '\\\(\s\)', '\1', 'g') + let request = extend({ + \ 'action': 'start', + \ 'background': 0, + \ 'command': command, + \ 'directory': getcwd(), + \ 'expanded': dispatch#expand(command), + \ 'title': title, + \ }, a:0 ? a:1 : {}) + if !s:dispatch(request) + execute '!' . request.command + endif + return '' +endfunction + +" }}}1 +" :Dispatch, :Make {{{1 + +function! dispatch#compiler_for_program(program) abort + if a:program ==# 'make' + return 'make' + endif + for plugin in reverse(split(globpath(escape(&rtp, ' '), 'compiler/*.vim', 1), "\n")) + for line in readfile(plugin, '', 100) + if matchstr(line, '\' + return [':Make' . compiler[1:-1], why] + elseif compiler =~# '^!' + return [':Start ' . compiler[1:-1], why] + else + return [':Dispatch ' . compiler, why] + endif +endfunction + +function! dispatch#focus_command(bang, args) abort + if empty(a:args) && a:bang + unlet! w:dispatch t:dispatch g:dispatch + let [what, why] = dispatch#focus() + echo 'Reverted default to ' . what + elseif empty(a:args) + let [what, why] = dispatch#focus() + echo printf('%s is %s', why, what) + elseif a:bang + let w:dispatch = escape(dispatch#expand(a:args), '#%') + let [what, why] = dispatch#focus() + echo 'Set window local focus to ' . what + else + unlet! w:dispatch t:dispatch + let g:dispatch = escape(dispatch#expand(a:args), '#%') + let [what, why] = dispatch#focus() + echo 'Set global focus to ' . what + endif + return '' +endfunction + +" }}}1 +" Requests {{{1 + +function! s:file(request) abort + if type(a:request) == type('') + return a:request + elseif type(a:request) == type({}) + return get(a:request, 'file', '') + else + return get(get(s:makes, a:request-1, {}), 'file', '') + endif +endfunction + +function! s:request(request) abort + if type(a:request) == type({}) + return a:request + elseif type(a:request) == type(0) + return get(s:makes, a:request-1, {}) + else + return get(s:files, a:request, {}) + endif +endfunction + +function! dispatch#completed(request) abort + return get(s:request(a:request), 'completed', 0) +endfunction + +function! dispatch#complete(file, ...) abort + if !dispatch#completed(a:file) + let request = s:request(a:file) + let request.completed = 1 + if !a:0 + if has_key(request, 'args') + echo 'Finished :Make' request.args + else + echo 'Finished :Dispatch' request.command + endif + endif + if !request.background + let all = get(request, 'compiler', '') ==# 'run' + call s:cgetfile(request, all, 0) + endif + endif + return '' +endfunction + +" }}}1 +" Quickfix window {{{1 + +function! dispatch#copen(bang) abort + if empty(s:makes) + return 'echoerr ' . string('No dispatches yet') + endif + let request = s:makes[-1] + if !dispatch#completed(request) && filereadable(request.file . '.complete') + let request.completed = 1 + endif + call s:cgetfile(request, a:bang, 1) +endfunction + +function! s:cgetfile(request, all, copen) abort + let request = s:request(a:request) + let efm = &l:efm + let makeprg = &l:makeprg + let compiler = get(b:, 'current_compiler', '') + let cd = haslocaldir() ? 'lcd' : 'cd' + let dir = getcwd() + try + call s:set_current_compiler(get(request, 'compiler', '')) + exe cd request.directory + if a:all + let &l:efm = '%+G%.%#' + else + let &l:efm = request.format + endif + let &l:makeprg = request.command + execute 'cgetfile '.fnameescape(request.file) + catch '^E40:' + return v:exception + finally + exe cd dir + let &l:efm = efm + let &l:makeprg = makeprg + call s:set_current_compiler(compiler) + endtry + call s:open_quickfix(request, a:copen) +endfunction + +function! s:open_quickfix(request, copen) abort + let was_qf = &buftype ==# 'quickfix' + execute (!empty(getqflist()) || a:copen) ? 'copen' : 'cwindow' + if &buftype ==# 'quickfix' && !was_qf && !a:copen + wincmd p + endif + for winnr in range(1, winnr('$')) + if getwinvar(winnr, '&buftype') ==# 'quickfix' + call setwinvar(winnr, 'quickfix_title', ':' . a:request.expanded) + let bufnr = winbufnr(winnr) + call setbufvar(bufnr, '&efm', a:request.format) + call setbufvar(bufnr, 'dispatch', escape(a:request.expanded, '%#')) + if has_key(a:request, 'program') + call setbufvar(bufnr, '&makeprg', a:request.program) + endif + if has_key(a:request, 'compiler') + call setbufvar(bufnr, 'current_compiler', a:request.compiler) + endif + endif + endfor +endfunction + +" }}}1 diff --git a/autoload/dispatch/headless.vim b/autoload/dispatch/headless.vim new file mode 100644 index 0000000..fd7414e --- /dev/null +++ b/autoload/dispatch/headless.vim @@ -0,0 +1,27 @@ +" dispatch.vim headless strategy + +if exists('g:autoloaded_dispatch_headless') + finish +endif +let g:autoloaded_dispatch_headless = 1 + + +function! dispatch#headless#handle(request) abort + if !a:request.background || &shell !~# 'sh' + return 0 + endif + if a:request.action ==# 'make' + let command = dispatch#prepare_make(a:request) + elseif a:request.action ==# 'start' + let command = a:request.command + else + return 0 + endif + if &shellredir =~# '%s' + let redir = printf(&shellredir, '/dev/null') + else + let redir = &shellredir . ' ' . '/dev/null' + endif + call system('('.command.') ' . redir . ' &') + return !v:shell_error +endfunction diff --git a/autoload/dispatch/iterm.vim b/autoload/dispatch/iterm.vim new file mode 100644 index 0000000..8a2b551 --- /dev/null +++ b/autoload/dispatch/iterm.vim @@ -0,0 +1,67 @@ +" dispatch.vim iTerm strategy + +if exists('g:autoloaded_dispatch_iterm') + finish +endif +let g:autoloaded_dispatch_iterm = 1 + +function! dispatch#iterm#handle(request) abort + if $TERM_PROGRAM !=# 'iTerm.app' && !(has('gui_macvim') && has('gui_running')) + return 0 + endif + if a:request.action ==# 'make' + if !get(a:request, 'background', 0) && !has('gui_running') + return 0 + endif + let exec = dispatch#prepare_make(a:request) + return dispatch#iterm#spawn(exec, a:request, 0) + elseif a:request.action ==# 'start' + return dispatch#iterm#spawn(a:request.expanded, a:request, !a:request.background) + endif +endfunction + +function! dispatch#iterm#printf_title(request) + return 'printf ' . shellescape('\033]1;%s\007\033]2;%s\007') . + \ ' ' . shellescape(a:request.title) . + \ ' ' . shellescape(a:request.expanded) +endfunction + +function! dispatch#iterm#spawn(command, request, activate) abort + let temp = tempname() + let command = ['cd ' . shellescape(a:request.directory)] + for line in split(system('env'), "\n") + let var = matchstr(line, '^\w\+\ze=') + if !empty(var) && var !=# '_' + if &shell =~# 'csh' + let command += ['setenv '.var.' '.shellescape(eval('$'.var))] + else + let command += ['export '.var.'='.shellescape(eval('$'.var))] + endif + endif + endfor + let command += [dispatch#iterm#printf_title(a:request)] + let command += [a:command] + call writefile(command, temp) + return s:osascript( + \ 'tell application "iTerm"', + \ 'tell the current terminal', + \ 'set oldsession to the current session', + \ 'tell (make new session)', + \ 'set name to '.s:escape(a:request.title), + \ 'set title to '.s:escape(a:request.command), + \ 'exec command ' . s:escape(&shell . ' -l ' . temp), + \ a:request.background ? 'select oldsession' : '', + \ 'end tell', + \ 'end tell', + \ a:activate ? 'activate' : '', + \ 'end tell') +endfunction + +function! s:osascript(...) abort + call system('osascript'.join(map(copy(a:000), '" -e ".shellescape(v:val)'), '')) + return !v:shell_error +endfunction + +function! s:escape(string) + return '"'.escape(a:string, '"\').'"' +endfunction diff --git a/autoload/dispatch/screen.vim b/autoload/dispatch/screen.vim new file mode 100644 index 0000000..e36f758 --- /dev/null +++ b/autoload/dispatch/screen.vim @@ -0,0 +1,32 @@ +" dispatch.vim GNU Screen strategy + +if exists('g:autoloaded_dispatch_screen') + finish +endif +let g:autoloaded_dispatch_screen = 1 + +function! dispatch#screen#handle(request) abort + if empty($STY) || !executable('screen') + return 0 + endif + if a:request.action ==# 'make' + if !get(a:request, 'background', 0) && empty(v:servername) + return 0 + endif + return dispatch#screen#spawn(dispatch#prepare_make(a:request), a:request) + elseif a:request.action ==# 'start' + return dispatch#screen#spawn(a:request.expanded, a:request) + endif +endfunction + +function! dispatch#screen#spawn(command, request) abort + let command = 'screen -ln -fn -t '.dispatch#shellescape( + \ a:request.title, &shell, &shellcmdflag, + \ 'cd "' . a:request.directory . '"; ' . a:command) + if a:request.background + call system(command) + else + silent execute '!' . escape(command, '!#%') + endif + return 1 +endfunction diff --git a/autoload/dispatch/tmux.vim b/autoload/dispatch/tmux.vim new file mode 100644 index 0000000..3018919 --- /dev/null +++ b/autoload/dispatch/tmux.vim @@ -0,0 +1,96 @@ +" dispatch.vim tmux strategy + +if exists('g:autoloaded_dispatch_tmux') + finish +endif +let g:autoloaded_dispatch_tmux = 1 + +let s:waiting = {} +let s:make_pane = tempname() + +function! dispatch#tmux#handle(request) abort + let session = get(g:, 'tmux_session', '') + if empty($TMUX) && empty(session) || !executable('tmux') + return 0 + endif + if !empty(system('tmux has-session -t '.shellescape(session))[0:-2]) + return '' + endif + + if a:request.action ==# 'make' + return dispatch#tmux#make(a:request) + elseif a:request.action ==# 'start' + let command = 'tmux new-window -t '.shellescape(session.':') + let command .= ' -n '.shellescape(a:request.title) + if a:request.background + let command .= ' -d' + endif + let command .= ' ' . shellescape('cd '.shellescape(getcwd()).'; '.a:request.expanded) + call system(command) + return 1 + endif +endfunction + +function! dispatch#tmux#make(request) abort + let session = get(g:, 'tmux_session', '') + let exec = dispatch#prepare_make(a:request, 'cd "'.a:request.directory.'"; '.a:request.command) + + let title = shellescape(get(a:request, 'compiler', 'make')) + if get(a:request, 'background', 0) + let cmd = 'new-window -d -n '.title + elseif has('gui_running') || empty($TMUX) || (!empty(session) && session !=# system('tmux display-message -p "#S"')[0:-2]) + let cmd = 'new-window -n '.title + else + let cmd = 'split-window -l 10 -d' + endif + + let cmd .= ' ' . dispatch#shellescape('-P', '-t', session.':', exec) + + let filter = 'sed' + let uname = system('uname')[0:-2] + if uname ==# 'Darwin' + let filter .= ' -l' + elseif uname ==# 'Linux' + let filter .= ' -u' + endif + let filter .= " -e \"s/\r//g\" -e \"s/\e[[0-9;]*m//g\" > ".a:request.file + silent execute '!tmux ' . cmd . '|tee ' . s:make_pane . '|xargs -I {} tmux pipe-pane -t {} '.shellescape(filter) + + let pane = get(readfile(s:make_pane, '', 1), 0, '') + return s:record(pane, a:request) +endfunction + +function! s:record(pane, request) + if a:pane =~# '\.\d\+$' + let [window, index] = split(a:pane, '\.\%(\d\+$\)\@=') + let out = system('tmux list-panes -F "#P #{pane_id}" -t '.shellescape(window)) + let id = matchstr("\n".out, '\n'.index.' \+\zs%\d\+') + else + let id = system('tmux list-panes -F "#{pane_id}" -t '.shellescape(a:pane))[0:-2] + endif + + if empty(id) + return 0 + endif + let s:waiting[id] = a:request + return 1 + +endfunction + +function! dispatch#tmux#poll() abort + if empty(s:waiting) + return + endif + let panes = split(system('tmux list-panes -a -F "#{pane_id}"'), "\n") + for [pane, request] in items(s:waiting) + if index(panes, pane) < 0 + call remove(s:waiting, pane) + call dispatch#complete(request) + endif + endfor +endfunction + +augroup dispatch_tmux + autocmd! + autocmd VimResized * if !has('gui_running') | call dispatch#tmux#poll() | endif +augroup END diff --git a/autoload/dispatch/windows.vim b/autoload/dispatch/windows.vim new file mode 100644 index 0000000..3b89db6 --- /dev/null +++ b/autoload/dispatch/windows.vim @@ -0,0 +1,50 @@ +" dispatch.vim Windows strategy + +if exists('g:autoloaded_dispatch_windows') + finish +endif +let g:autoloaded_dispatch_windows = 1 + +function! s:escape(str) + if &shellxquote ==# '"' + return '"' . substitute(a:str, '"', '""', 'g') . '"' + else + let esc = exists('+shellxescape') ? &shellxescape : '"&|<>()@^' + return &shellquote . + \ substitute(a:str, '['.esc.']', '^&', 'g') . + \ get({'(': ')', '"(': ')"'}, &shellquote, &shellquote) + endif +endfunction + +function! dispatch#windows#handle(request) abort + if !has('win32') || empty(v:servername) + return 0 + endif + if a:request.action ==# 'make' + return dispatch#windows#make(a:request) + elseif a:request.action ==# 'start' + let title = get(a:request, 'title', matchstr(a:request.command, '\S\+')) + return dispatch#windows#spawn(title, a:request.command, a:request.background) + endif +endfunction + +function! dispatch#windows#spawn(title, exec, background) abort + let extra = a:background ? ' /min' : '' + silent execute '!start /min cmd.exe /cstart ' . + \ '"' . substitute(a:title, '"', '', 'g') . '"' . extra . ' ' . + \ &shell . ' ' . &shellcmdflag . ' ' . s:escape(a:exec) + return 1 +endfunction + +function! dispatch#windows#make(request) abort + if &shellxquote ==# '"' + let exec = dispatch#prepare_make(a:request) + else + let exec = escape(a:request.expanded, '%#!') . + \ ' ' . dispatch#shellpipe(a:request.file) . + \ ' & cd . > ' . a:request.file . '.complete' . + \ ' & ' . dispatch#callback(a:request) + endif + + return dispatch#windows#spawn(a:request.title, exec, 1) +endfunction diff --git a/autoload/dispatch/x11.vim b/autoload/dispatch/x11.vim new file mode 100644 index 0000000..860ed52 --- /dev/null +++ b/autoload/dispatch/x11.vim @@ -0,0 +1,28 @@ +" dispatch.vim X11 strategy + +if exists('g:autoloaded_dispatch_x11') + finish +endif +let g:autoloaded_dispatch_x11 = 1 + +function! dispatch#x11#handle(request) abort + if $DISPLAY !~# '^:' || a:request.action !=# 'start' || a:request.background + return 0 + endif + if !empty($TERMINAL) + let terminal = $TERMINAL + elseif executable('x-terminal-emulator') + let terminal = 'x-terminal-emulator' + elseif executable('xterm') + let terminal = 'xterm' + else + return 0 + endif + let command = dispatch#shellescape('printf', + \ '\033]1;%s\007\033]2;%s\007', + \ a:request.title, + \ a:request.expanded) . '; ' + \ . a:request.expanded + call system(dispatch#shellescape(terminal, '-e', &shell, &shellcmdflag, command). ' &') + return 1 +endfunction diff --git a/doc/dispatch.txt b/doc/dispatch.txt new file mode 100644 index 0000000..0c64396 --- /dev/null +++ b/doc/dispatch.txt @@ -0,0 +1,128 @@ +*dispatch.txt* Asynchronous build and test dispatcher + +Author: Tim Pope +Repo: https://github.com/tpope/vim-dispatch +License: Same terms as Vim itself (see |license|) + +INTRODUCTION *dispatch* + +Leverage the power of Vim's compiler system without being constrained by +synchronicity. + +COMMANDS *dispatch-commands* + + *dispatch-:Make* +:Make [arguments] Using the current |:compiler| settings, dispatch a + build in the foreground. Adapter strategies vary, but + the goal is visibility without stealing focus. When + the build is complete, load the results into the + |quickfix| list and call |:cwindow|. This command is + preferred for shorter tasks like "build this file." + +:Make! [arguments] Using the current compiler settings, dispatch a build + in the background. Use |:Copen| to load the results. + This command is preferred for longer tasks like "run + the entire test suite." + + *dispatch-:Copen* +:Copen Load the latest build into the quickfix list and open + it with |:copen|. You may call this before the + process is finished. + +:Copen! Load the latest build into the quickfix list using a + catch-all parser. This is useful when you can't tell + what went wrong. + + *dispatch-:Dispatch* +:Dispatch[!] {program} [arguments] + Find a compiler plugin that sets 'makeprg' to + {command} and use its 'errorformat' to dispatch a + |:Make| for the given {command} and [arguments]. If + no compiler plugin is found, the generic format + %+G%.%# is used. + + :Dispatch picks a compiler by looking for + CompilerSet makeprg={program} in compiler plugins. + To force a given {program} to use a given {compiler}, + create ~/.vim/after/compiler/{compiler}.vim and add to + it a line like the following: > + + " CompilerSet makeprg={program} +< + *b:dispatch* +:Dispatch[!] Invoke |:Dispatch| with the command and arguments + found in b:dispatch. When absent, equivalent to + |:Make|. + + *dispatch-:FocusDispatch* +:FocusDispatch {program} [arguments] + Set a global default command for |:Dispatch| with no + arguments. Overrides |b:dispatch|. + +:FocusDispatch! {program} [arguments] + Set a window local default command for |:Dispatch| + with no arguments. Overrides |b:dispatch| and the + global default. + +:FocusDispatch! Clear the global and window local defaults for + |:Dispatch|. + +:FocusDispatch Show the task that would run when calling |:Dispatch| + with no arguments. + + *dispatch-:Start* +:Start {command} Start a process in a new, focused window. + +:Start! {command} Start a process in a new, unfocused window. + +:Start[!] -title={title} {command} + Start a process in a window titled {title}. This is + not normally used interactively. + +STRATEGIES *dispatch-strategies* + +Strategies are listed in order of precedence. The first available one is +used. Some strategies only provide for a subset of tasks. + +Tmux ~ + +Foreground makes open in a small split at the bottom. The closure of the +pane triggers a |VimResized| event which loads the results into the quickfix +list. (All other strategies use the GUI subsystem to get this to work.) + +The tmux strategy can be used from the GUI as well. Either start Vim from +inside of tmux or assign g:tmux_session. This will use a new window for +foreground makes rather than a split. + +GNU Screen ~ + +Not used for foreground |:Make| invocations unless you're in GUI Vim. + +Windows ~ + +You can use either the standard cmd.exe or a cygwin shell. Both foreground +and background |:Make| invocations are started minimized to prevent focus +stealing. + +iTerm ~ + +This strategy fires if you're in MacVim with at least one iTerm window open, +or if Vim is running in iTerm itself. In the latter case, you can't use it +for foreground |:Make| invocations. + +X11 ~ + +Uses $TERMINAL, x-terminal-emulator, or xterm. Used only for |:Start|, as its +insistence on stealing focus makes it infuriating for much else. + +Headless ~ + +Forks tasks into the background. It's working, you just can't see it. Don't +forget to check |:Copen|. The presence of this strategy means that |:Make!| +and |:Start!| will never block Vim. + +Synchronous ~ + +When all else fails, a vanilla |:make| or |:!| is performed. + + vim:tw=78:et:ft=help:norl: diff --git a/plugin/dispatch.vim b/plugin/dispatch.vim new file mode 100644 index 0000000..96f07b9 --- /dev/null +++ b/plugin/dispatch.vim @@ -0,0 +1,37 @@ +" dispatch.vim - Asynchronous build and test dispatcher +" Maintainer: Tim Pope +" Version: 1.0 + +if exists("g:loaded_dispatch") || v:version < 700 || &cp + finish +endif +let g:loaded_dispatch = 1 + +command! -bang -nargs=* -complete=custom,dispatch#command_complete Dispatch + \ execute dispatch#compile_command(0, ) + +command! -bang -nargs=* -complete=custom,dispatch#command_complete FocusDispatch + \ execute dispatch#focus_command(0, ) + +command! -bang -nargs=* -complete=file Make + \ Dispatch _ + +command! -bang -nargs=* -complete=custom,dispatch#command_complete Start + \ call dispatch#start(, {'background': 0}) + +command! -bang Copen call dispatch#copen(0) + +function! DispatchComplete(id) + return dispatch#complete(a:id) +endfunction + +if !exists('g:dispatch_handlers') + let g:dispatch_handlers = [ + \ 'tmux', + \ 'screen', + \ 'windows', + \ 'iterm', + \ 'x11', + \ 'headless', + \ ] +endif