" don't spam the user when Vim is started in Vi compatibility mode let s:cpo_save = &cpo set cpo&vim " Spawn starts an asynchronous job. See the description of go#job#Options to " understand the args parameter. " " Spawn returns a job. function! go#job#Spawn(cmd, args) let l:options = go#job#Options(a:args) return go#job#Start(a:cmd, l:options) endfunction " Options returns callbacks to be used with job_start. It is abstracted to be " used with various go commands, such as build, test, install, etc.. This " allows us to avoid writing the same callback over and over for some " commands. It's fully customizable so each command can change it to its own " logic. " " args is a dictionary with the these keys: " 'bang': " Set to 0 to jump to the first error in the error list. " Defaults to 0. " 'statustype': " The status type to use when updating the status. " See statusline.vim. " 'for': " The g:go_list_type_command key to use to get the error list type to use. " Errors will not be handled when the value is '_'. " Defaults to '_job' " 'errorformat': " The errorformat string to use when parsing errors. Defaults to " &errorformat. " See :help 'errorformat'. " 'complete': " A function to call after the job exits and the channel is closed. The " function will be passed three arguments: the job, its exit code, and the " list of messages received from the channel. The default is a no-op. A " custom value can modify the messages before they are processed by the " returned exit_cb and close_cb callbacks. When the function is called, " the current window will be the window that was hosting the buffer when " the job was started. After it returns, the current window will be " restored to what it was before the function was called. " 'preserveerrors': " A function that will be passed one value, the list type. It should " return a boolean value that indicates whether any errors encountered " should be consider additive to the existing set of errors. This is " mostly useful for a set of commands that are run via autocmds. " " The return value is a dictionary with these keys: " 'callback': " A function suitable to be passed as a job callback handler. See " job-callback. " 'exit_cb': " A function suitable to be passed as a job exit_cb handler. See " job-exit_cb. " 'close_cb': " A function suitable to be passed as a job close_cb handler. See " job-close_cb. " 'cwd': " The path to the directory which contains the current buffer. The " callbacks are configured to expect this directory is the working " directory for the job; it should not be modified by callers. function! go#job#Options(args) let cbs = {} let state = { \ 'winid': win_getid(winnr()), \ 'dir': getcwd(), \ 'jobdir': expand("%:p:h"), \ 'messages': [], \ 'bang': 0, \ 'for': "_job", \ 'exited': 0, \ 'exit_status': 0, \ 'closed': 0, \ 'errorformat': &errorformat, \ 'statustype' : '', \ } let cbs.cwd = state.jobdir if has_key(a:args, 'bang') let state.bang = a:args.bang endif if has_key(a:args, 'for') let state.for = a:args.for endif if has_key(a:args, 'statustype') let state.statustype = a:args.statustype endif if has_key(a:args, 'errorformat') let state.errorformat = a:args.errorformat endif if has_key(a:args, 'preserveerrors') let state.preserveerrors = a:args.preserveerrors endif function state.complete(job, exit_status, data) if has_key(self, 'custom_complete') let l:winid = win_getid(winnr()) " Always set the active window to the window that was active when the job " was started. Among other things, this makes sure that the correct " window's location list will be populated when the list type is " 'location' and the user has moved windows since starting the job. call win_gotoid(self.winid) call self.custom_complete(a:job, a:exit_status, a:data) call win_gotoid(l:winid) endif call self.show_errors(a:job, a:exit_status, a:data) endfunction function state.show_status(job, exit_status) dict if self.statustype == '' return endif if go#config#EchoCommandInfo() let prefix = '[' . self.statustype . '] ' if a:exit_status == 0 call go#util#EchoSuccess(prefix . "SUCCESS") else call go#util#EchoError(prefix . "FAIL") endif endif let status = { \ 'desc': 'last status', \ 'type': self.statustype, \ 'state': "success", \ } if a:exit_status let status.state = "failed" endif if has_key(self, 'started_at') let elapsed_time = reltimestr(reltime(self.started_at)) " strip whitespace let elapsed_time = substitute(elapsed_time, '^\s*\(.\{-}\)\s*$', '\1', '') let status.state .= printf(" (%ss)", elapsed_time) endif call go#statusline#Update(self.jobdir, status) endfunction if has_key(a:args, 'complete') let state.custom_complete = a:args.complete endif " explicitly bind _start to state so that within it, self will " always refer to state. See :help Partial for more information. " " _start is intended only for internal use and should not be referenced " outside of this file. let cbs._start = function('s:start', [''], state) " explicitly bind callback to state so that within it, self will " always refer to state. See :help Partial for more information. let cbs.callback = function('s:callback', [], state) " explicitly bind exit_cb to state so that within it, self will always refer " to state. See :help Partial for more information. let cbs.exit_cb = function('s:exit_cb', [], state) " explicitly bind close_cb to state so that within it, self will " always refer to state. See :help Partial for more information. let cbs.close_cb = function('s:close_cb', [], state) function state.show_errors(job, exit_status, data) if self.for == '_' return endif let l:winid = win_getid(winnr()) " Always set the active window to the window that was active when the job " was started. Among other things, this makes sure that the correct " window's location list will be populated when the list type is " 'location' and the user has moved windows since starting the job. call win_gotoid(self.winid) let l:listtype = go#list#Type(self.for) let l:preserveerrors = 0 if has_key(self, 'preserveerrors') let l:preserveerrors = self.preserveerrors(l:listtype) endif if a:exit_status == 0 if !l:preserveerrors call go#list#Clean(l:listtype) call win_gotoid(l:winid) endif return endif let l:listtype = go#list#Type(self.for) if len(a:data) == 0 if !l:preserveerrors call go#list#Clean(l:listtype) call win_gotoid(l:winid) endif return endif let out = join(self.messages, "\n") let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' try " parse the errors relative to self.jobdir execute l:cd fnameescape(self.jobdir) call go#list#ParseFormat(l:listtype, self.errorformat, out, self.for, l:preserveerrors) let errors = go#list#Get(l:listtype) finally execute l:cd fnameescape(self.dir) endtry if empty(errors) " failed to parse errors, output the original content call go#util#EchoError([self.dir] + self.messages) call win_gotoid(l:winid) return endif " only open the error window if user was still in the window from which " the job was started. if self.winid == l:winid call go#list#Window(l:listtype, len(errors)) if self.bang call win_gotoid(l:winid) else call go#list#JumpToFirst(l:listtype) endif endif endfunction return cbs endfunction function! s:start(args) dict if go#config#EchoCommandInfo() && self.statustype != "" let prefix = '[' . self.statustype . '] ' call go#util#EchoSuccess(prefix . "dispatched") endif if self.statustype != '' let status = { \ 'desc': 'current status', \ 'type': self.statustype, \ 'state': "started", \ } call go#statusline#Update(self.jobdir, status) endif let self.started_at = reltime() endfunction function! s:callback(chan, msg) dict call add(self.messages, a:msg) endfunction function! s:exit_cb(job, exitval) dict let self.exit_status = a:exitval let self.exited = 1 call self.show_status(a:job, a:exitval) if self.closed || has('nvim') call self.complete(a:job, self.exit_status, self.messages) endif endfunction function! s:close_cb(ch) dict let self.closed = 1 if self.exited let job = ch_getjob(a:ch) call self.complete(job, self.exit_status, self.messages) endif endfunction " go#job#Start runs a job. The options are expected to be the options " suitable for Vim8 jobs. When called from Neovim, Vim8 options will be " transformed to their Neovim equivalents. function! go#job#Start(cmd, options) let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' let l:options = copy(a:options) if has('nvim') let l:options = s:neooptions(l:options) endif " Verify that the working directory for the job actually exists. Return " early if the directory does not exist. This helps avoid errors when " working with plugins that use virtual files that don't actually exist on " the file system. let l:filedir = expand("%:p:h") if has_key(l:options, 'cwd') && !isdirectory(l:options.cwd) return elseif !isdirectory(l:filedir) return endif let l:manualcd = 0 if !has_key(l:options, 'cwd') " pre start let l:manualcd = 1 let dir = getcwd() execute l:cd fnameescape(filedir) endif if has_key(l:options, '_start') call l:options._start() " remove _start to play nicely with vim (when vim encounters an unexpected " job option it reports an "E475: invalid argument" error). unlet l:options._start endif " noblock was added in 8.1.350; remove it if it's not supported. if has_key(l:options, 'noblock') && (has('nvim') || !has("patch-8.1.350")) call remove(l:options, 'noblock') endif if go#util#HasDebug('shell-commands') call go#util#EchoInfo('job command: ' . string(a:cmd)) endif if has('nvim') let l:input = [] if has_key(a:options, 'in_io') && a:options.in_io ==# 'file' && !empty(a:options.in_name) let l:input = readfile(a:options.in_name, "b") endif let job = jobstart(a:cmd, l:options) if len(l:input) > 0 call chansend(job, l:input) " close stdin to signal that no more bytes will be sent. call chanclose(job, 'stdin') endif else let l:cmd = a:cmd if go#util#IsWin() let l:cmd = join(map(copy(a:cmd), function('s:winjobarg')), " ") endif let job = job_start(l:cmd, l:options) endif if l:manualcd " post start execute l:cd fnameescape(l:dir) endif return job endfunction " s:neooptions returns a dictionary of job options suitable for use by Neovim " based on a dictionary of job options suitable for Vim8. function! s:neooptions(options) let l:options = {} let l:options['stdout_buf'] = '' let l:options['stderr_buf'] = '' let l:err_mode = get(a:options, 'err_mode', get(a:options, 'mode', '')) let l:out_mode = get(a:options, 'out_mode', get(a:options, 'mode', '')) for key in keys(a:options) if key == 'cwd' let l:options['cwd'] = a:options['cwd'] continue endif if key == 'callback' let l:options['callback'] = a:options['callback'] if !has_key(a:options, 'out_cb') let l:options['on_stdout'] = function('s:callback2on_stdout', [l:out_mode], l:options) endif if !has_key(a:options, 'err_cb') let l:options['on_stderr'] = function('s:callback2on_stderr', [l:err_mode], l:options) endif continue endif if key == 'out_cb' let l:options['out_cb'] = a:options['out_cb'] let l:options['on_stdout'] = function('s:on_stdout', [l:out_mode], l:options) continue endif if key == 'err_cb' let l:options['err_cb'] = a:options['err_cb'] let l:options['on_stderr'] = function('s:on_stderr', [l:err_mode], l:options) continue endif if key == 'exit_cb' let l:options['exit_cb'] = a:options['exit_cb'] let l:options['on_exit'] = function('s:on_exit', [], l:options) continue endif if key == 'close_cb' continue endif if key == 'stoponexit' if a:options['stoponexit'] == '' let l:options['detach'] = 1 endif continue endif endfor return l:options endfunction function! s:callback2on_stdout(mode, ch, data, event) dict let self.stdout_buf = s:neocb(a:mode, a:ch, self.stdout_buf, a:data, self.callback) endfunction function! s:callback2on_stderr(mode, ch, data, event) dict let self.stderr_buf = s:neocb(a:mode, a:ch, self.stderr_buf, a:data, self.callback) endfunction function! s:on_stdout(mode, ch, data, event) dict let self.stdout_buf = s:neocb(a:mode, a:ch, self.stdout_buf, a:data, self.out_cb) endfunction function! s:on_stderr(mode, ch, data, event) dict let self.stderr_buf = s:neocb(a:mode, a:ch, self.stderr_buf, a:data, self.err_cb ) endfunction function! s:on_exit(jobid, exitval, event) dict call self.exit_cb(a:jobid, a:exitval) endfunction function! go#job#Stop(job) abort if has('nvim') call jobstop(a:job) return endif call job_stop(a:job) call go#job#Wait(a:job) return endfunction function! go#job#Wait(job) abort if has('nvim') call jobwait([a:job]) return endif while job_status(a:job) is# 'run' sleep 50m endwhile endfunction function! s:winjobarg(idx, val) abort if empty(a:val) return '""' endif return a:val endfunction function! s:neocb(mode, ch, buf, data, callback) " dealing with the channel lines of Neovim is awful. The docs (:help " channel-lines) say: " stream event handlers may receive partial (incomplete) lines. For a " given invocation of on_stdout etc, `a:data` is not guaranteed to end " with a newline. " - `abcdefg` may arrive as `['abc']`, `['defg']`. " - `abc\nefg` may arrive as `['abc', '']`, `['efg']` or `['abc']`, " `['','efg']`, or even `['ab']`, `['c','efg']`. " " Thankfully, though, this is explained a bit better in an issue: " https://github.com/neovim/neovim/issues/3555. Specifically in these two " comments: " * https://github.com/neovim/neovim/issues/3555#issuecomment-152290804 " * https://github.com/neovim/neovim/issues/3555#issuecomment-152588749 " " The key is " Every item in the list passed to job control callbacks represents a " string after a newline(Except the first, of course). If the program " outputs: "hello\nworld" the corresponding list is ["hello", "world"]. " If the program outputs "hello\nworld\n", the corresponding list is " ["hello", "world", ""]. In other words, you can always determine if " the last line received is complete or not. " and " for every list you receive in a callback, all items except the first " represent newlines. let l:buf = '' " A single empty string means EOF was reached. The first item will never be " an empty string except for when it's the only item and is signaling that " EOF was reached. if len(a:data) == 1 && a:data[0] == '' " when there's nothing buffered, return early so that an " erroneous message will not be added. if a:buf == '' return '' endif let l:data = [a:buf] else let l:data = copy(a:data) let l:data[0] = a:buf . l:data[0] " The last element may be a partial line; save it for next time. if a:mode != 'raw' let l:buf = l:data[-1] let l:data = l:data[:-2] endif endif let l:i = 0 let l:last = len(l:data) - 1 while l:i <= l:last let l:msg = l:data[l:i] if a:mode == 'raw' && l:i < l:last let l:msg = l:msg . "\n" endif call a:callback(a:ch, l:msg) let l:i += 1 endwhile return l:buf endfunction " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save " vim: sw=2 ts=2 et