diff --git a/autoload/maktaba/syscall.vim b/autoload/maktaba/syscall.vim index 5786e70..8fdd38f 100644 --- a/autoload/maktaba/syscall.vim +++ b/autoload/maktaba/syscall.vim @@ -89,6 +89,23 @@ function! s:CurrentEnv() endfunction +"" +" Helper to call {Callback} with and without legacy {env_data} arg. +" Tries {Callback}({return_data}) first and falls back to +" {Callback}({env_dict}, {return_data}). +function! s:TriggerAsyncCallback(Callback, env_data, return_data) abort + try + " Try prototype callback({result_dict}). + call maktaba#function#Call(a:Callback, [a:return_data]) + catch /E119:/ + " Not enough arguments. + " Fall back to legacy prototype callback({env_dict}, {result_dict}). + " TODO(#180): Deprecate and shout an error for this case. + call maktaba#function#Call(a:Callback, [a:env_data, a:return_data]) + endtry +endfunction + + "" " @private " @dict Syscall @@ -336,16 +353,7 @@ function! maktaba#syscall#CallAsync(Callback, allow_sync_fallback) abort dict \ self.GetCommand()) let l:return_data = self.Call(0) let l:return_data.status = v:shell_error - try - " Try prototype callback({result_dict}). - call maktaba#function#Call(self.callback, [l:return_data]) - catch /E119:/ - " Not enough arguments. - " Fall back to legacy prototype callback({env_dict}, {result_dict}). - " TODO(#180): Deprecate and shout an error for this case. - call maktaba#function#Call( - \ self.callback, [s:CurrentEnv(), l:return_data]) - endtry + call s:TriggerAsyncCallback(self.callback, s:CurrentEnv(), l:return_data) return else " Neither async nor sync is available. Throw error with salient reason. @@ -452,24 +460,23 @@ endfunction " callback(env_dict, result_dict). The latter will be deprecated and stop " working in future versions of maktaba. function! maktaba#syscall#AsyncDone(stdout_file, stderr_file, exit_code) - let l:callback_info = s:callbacks[a:stdout_file] - let l:return_data = {} - let l:return_data.status = a:exit_code - let l:return_data.stdout = join(readfile(a:stdout_file), "\n") - if filereadable(a:stderr_file) - let l:return_data.stderr = join(readfile(a:stderr_file), "\n") - call delete(a:stderr_file) - endif - unlet s:callbacks[a:stdout_file] - call delete(a:stdout_file) try - " Try prototype callback({result_dict}). - call maktaba#function#Call(l:callback_info.function, [l:return_data]) - catch /E119:/ - " Not enough arguments. - " Fall back to legacy prototype callback({env_dict}, {result_dict}). - " TODO(#180): Deprecate and shout an error for this case. - call maktaba#function#Call( - \ l:callback_info.function, [l:callback_info.env, l:return_data]) + let l:callback_info = s:callbacks[a:stdout_file] + let l:return_data = {} + let l:return_data.status = a:exit_code + let l:return_data.stdout = join(readfile(a:stdout_file), "\n") + if filereadable(a:stderr_file) + let l:return_data.stderr = join(readfile(a:stderr_file), "\n") + call delete(a:stderr_file) + endif + unlet s:callbacks[a:stdout_file] + call delete(a:stdout_file) + call s:TriggerAsyncCallback( + \ l:callback_info.function, l:callback_info.env, l:return_data) + catch + " Uncaught errors from here would be sent back to the --remote-expr command + " line, but vim can't do anything useful with them from there. Catch and + " shout them here instead. + call maktaba#error#Shout('Error from CallAsync callback: %s', v:exception) endtry endfunction diff --git a/vroom/system.vroom b/vroom/system.vroom index 83180a1..e2f0e33 100644 --- a/vroom/system.vroom +++ b/vroom/system.vroom @@ -369,33 +369,42 @@ maktaba#system#Or to chain commands. To execute system calls asynchronously, use CallAsync. + :function AsyncWait(delay) abort + | let l:deadline = localtime() + a:delay " wait for at most N seconds + | while !exists('g:callback_exit_code') && localtime() < l:deadline + | endwhile + | call maktaba#ensure#IsTrue(exists('g:callback_exit_code'), + | 'Async callback was expected to complete within %d seconds', a:delay) + |endfunction :let g:syscall = maktaba#syscall#Create(['echo', 'hi']) :function Callback(return_data) abort | let g:callback_stdout = a:return_data.stdout | let g:callback_exit_code = a:return_data.status |endfunction - :call g:syscall.CallAsync('Callback', 0) + :call g:syscall.CallAsync('Callback', 0) | call AsyncWait(2) ! .*echo hi; vim --servername .* --remote-expr "maktaba#syscall#AsyncDone.* - :let g:deadline = localtime() + 2 " wait for at most 2 seconds - :while !exists('g:callback_exit_code') && localtime() < g:deadline - :endwhile - :call maktaba#ensure#IsTrue(exists('g:callback_exit_code'), - | 'Async callback was expected to complete within 2 seconds') :echomsg g:callback_stdout ~ hi +Async calls can't throw uncaught errors up to the original caller, so they're +just shouted to the user directly. + + :function ThrowErrorCallback(return_data) abort + | let g:callback_exit_code = a:return_data.status + | throw 'BOOM' + |endfunction + :unlet g:callback_exit_code + :call g:syscall.CallAsync('ThrowErrorCallback', 0) | call AsyncWait(2) + ! .*echo hi; .* + ~ Error from CallAsync callback: BOOM + Asynchronous calls can also take a stdin parameter. :unlet g:callback_stdout :unlet g:callback_exit_code :let g:syscall = maktaba#syscall#Create(['cat']).WithStdin("Hello") - :call g:syscall.CallAsync('Callback', 0) + :call g:syscall.CallAsync('Callback', 0) | call AsyncWait(2) ! .*cat; vim --servername .* --remote-expr "maktaba#syscall#AsyncDone.* - :let g:deadline = localtime() + 2 " wait for at most 2 seconds - :while !exists('g:callback_exit_code') && localtime() < g:deadline - :endwhile - :call maktaba#ensure#IsTrue(exists('g:callback_exit_code'), - | 'Async callback was expected to complete within 2 seconds') :echomsg g:callback_stdout ~ Hello