diff --git a/autoload/go/debug.vim b/autoload/go/debug.vim index ca9b8adeb0..2b3a4e0662 100644 --- a/autoload/go/debug.vim +++ b/autoload/go/debug.vim @@ -6,13 +6,14 @@ scriptencoding utf-8 if !exists('s:state') let s:state = { - \ 'rpcid': 1, - \ 'running': 0, - \ 'currentThread': {}, - \ 'localVars': {}, - \ 'functionArgs': {}, - \ 'message': [], - \} + \ 'rpcid': 1, + \ 'running': 0, + \ 'currentThread': {}, + \ 'localVars': {}, + \ 'functionArgs': {}, + \ 'message': [], + \ 'resultHandlers': {}, + \ } if go#util#HasDebug('debugger-state') call go#config#SetDebugDiag(s:state) @@ -80,15 +81,23 @@ function! s:logger(prefix, ch, msg) abort endtry endfunction -function! s:call_jsonrpc(method, ...) abort +" s:call_jsonrpc will call method, passing all of s:call_jsonrpc's optional +" arguments in the rpc request's params field. + +" The first argument to s:call_jsonrpc should be a function that takes two +" arguments. The first argument will be a function that takes no arguments and will +" throw an exception if the response to the request is an error response. The +" second argument is the response itself. +function! s:call_jsonrpc(handle_result, method, ...) abort if go#util#HasDebug('debugger-commands') call go#util#EchoInfo('sending to dlv ' . a:method) endif let l:args = a:000 let s:state['rpcid'] += 1 + let l:reqid = s:state['rpcid'] let l:req_json = json_encode({ - \ 'id': s:state['rpcid'], + \ 'id': l:reqid, \ 'method': a:method, \ 'params': l:args, \}) @@ -101,26 +110,13 @@ function! s:call_jsonrpc(method, ...) abort call ch_sendraw(l:ch, req_json) endif - while len(s:state.data) == 0 - sleep 50m - if get(s:state, 'ready', 0) == 0 - return - endif - endwhile - let resp_json = s:state.data[0] - let s:state.data = s:state.data[1:] + let s:state.resultHandlers[l:reqid] = a:handle_result if go#util#HasDebug('debugger-commands') let g:go_debug_commands = add(go#config#DebugCommands(), { \ 'request': l:req_json, - \ 'response': l:resp_json, \ }) endif - - if type(l:resp_json) == v:t_dict && has_key(l:resp_json, 'error') && !empty(l:resp_json.error) - throw l:resp_json.error - endif - return l:resp_json catch throw substitute(v:exception, '^Vim', '', '') endtry @@ -169,7 +165,14 @@ function! s:update_breakpoint(res) abort endfunction " Populate the stacktrace window. -function! s:show_stacktrace(res) abort +function! s:show_stacktrace(check_errors, res) abort + try + call a:check_errors() + catch + call go#util#EchoError(printf('could not update stack: %s', v:exception)) + return + endtry + if type(a:res) isnot type({}) || !has_key(a:res, 'result') || empty(a:res.result) return endif @@ -242,7 +245,7 @@ function! s:clearState() abort endfunction function! s:stop() abort - let l:res = s:call_jsonrpc('RPCServer.Detach', {'kill': v:true}) + call s:call_jsonrpc(function('s:noop'), 'RPCServer.Detach', {'kill': v:true}) if has_key(s:state, 'job') call go#job#Wait(s:state['job']) @@ -468,21 +471,33 @@ function! s:start_cb() abort silent! delcommand GoDebugStart silent! delcommand GoDebugTest + command! -nargs=0 GoDebugContinue call go#debug#Stack('continue') + command! -nargs=0 GoDebugStop call go#debug#Stop() + + nnoremap (go-debug-breakpoint) :call go#debug#Breakpoint() + nnoremap (go-debug-continue) :call go#debug#Stack('continue') + nnoremap (go-debug-stop) :call go#debug#Stop() + + augroup vim-go-debug + autocmd! * + autocmd FileType go nmap (go-debug-continue) + autocmd FileType go nmap (go-debug-breakpoint) + augroup END + doautocmd vim-go-debug FileType go +endfunction + +function! s:continue() command! -nargs=0 GoDebugNext call go#debug#Stack('next') command! -nargs=0 GoDebugStep call go#debug#Stack('step') command! -nargs=0 GoDebugStepOut call go#debug#Stack('stepOut') command! -nargs=0 GoDebugRestart call go#debug#Restart() - command! -nargs=0 GoDebugStop call go#debug#Stop() command! -nargs=* GoDebugSet call go#debug#Set() command! -nargs=1 GoDebugPrint call go#debug#Print() - nnoremap (go-debug-breakpoint) :call go#debug#Breakpoint() nnoremap (go-debug-next) :call go#debug#Stack('next') nnoremap (go-debug-step) :call go#debug#Stack('step') nnoremap (go-debug-stepout) :call go#debug#Stack('stepOut') - nnoremap (go-debug-continue) :call go#debug#Stack('continue') - nnoremap (go-debug-stop) :call go#debug#Stop() nnoremap (go-debug-print) :call go#debug#Print(expand('')) if has('balloon_eval') @@ -569,23 +584,66 @@ endfunction function! s:on_data(ch, data, ...) dict abort let l:data = s:message(self.databuf, a:data) - try - let l:res = json_decode(l:data) - let s:state['data'] = add(s:state['data'], l:res) - let self.databuf = '' - catch - " there isn't a complete message in databuf: buffer l:data and try - " again when more data comes in. - let self.databuf = l:data - finally - endtry + let l:messages = split(l:data, "\n") + for l:msg in l:messages + let l:data = l:messages[0] + try + let l:res = json_decode(l:data) + " remove the decoded message + call remove(l:messages, 0) + catch + return + finally + " Rejoin messages and assign to databuf so that any messages that come + " in if s:handleRPCResult sleeps will be appended correctly. + " + " Because the current message is removed in the try immediately after + " decoding, that l:messages contains all the messages that have not + " yet been decoded including the current message if decoding it + " failed. + let self.databuf = join(l:messages, "\n") + endtry + + if go#util#HasDebug('debugger-commands') + let g:go_debug_commands = add(go#config#DebugCommands(), { + \ 'response': l:data, + \ }) + endif + call s:handleRPCResult(l:res) + endfor endfunction function! s:message(buf, data) abort - let l:data = a:buf if has('nvim') - for l:msg in a:data - let l:data .= l:msg + " 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:data = printf('%s%s', a:buf, a:data[0]) + for l:msg in a:data[1:] + let l:data = printf("%s\n%s", l:data, l:msg) endfor return l:data @@ -594,6 +652,33 @@ function! s:message(buf, data) abort return printf('%s%s', a:buf, a:data) endfunction +" s:error_check will be curried and injected into rpc result handlers so that +" those result handlers can consistently check for errors in the response by +" catching exceptions and handling the error appropriately. +function! s:error_check(resp_json) abort + if type(a:resp_json) == v:t_dict && has_key(a:resp_json, 'error') && !empty(a:resp_json.error) + throw a:resp_json.error + endif +endfunction + +function! s:handleRPCResult(resp) abort + try + let l:id = a:resp.id + " call the result handler with its first argument set to a curried + " s:error_check value so that the the handle can call s:error_check + " without passing any arguments to check whether the response is an error + " response. + call call(s:state.resultHandlers[l:id], [function('s:error_check', [a:resp]), a:resp]) + catch + throw v:exception + finally + if has_key(s:state.resultHandlers, l:id) + call remove(s:state.resultHandlers, l:id) + endif + endtry +endfunction + + " Start the debug mode. The first argument is the package name to compile and " debug, anything else will be passed to the running program. function! go#debug#Start(is_test, ...) abort @@ -779,11 +864,17 @@ endfunction function! s:eval(arg) abort try - let l:res = s:call_jsonrpc('RPCServer.State') - let l:res = s:call_jsonrpc('RPCServer.Eval', { + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.State') + let l:res = l:promise.await() + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.Eval', { \ 'expr': a:arg, \ 'scope': {'GoroutineID': l:res.result.State.currentThread.goroutineID} \ }) + + let l:res = l:promise.await() + return s:eval_tree(l:res.result.Variable, 0) catch call go#util#EchoError(printf('evaluation failed: %s', v:exception)) @@ -791,6 +882,7 @@ function! s:eval(arg) abort endtry endfunction + function! go#debug#BalloonExpr() abort silent! let l:v = s:eval(v:beval_text) return l:v @@ -805,23 +897,36 @@ function! go#debug#Print(arg) abort endfunction function! s:update_goroutines() abort + call s:call_jsonrpc(function('s:update_goroutines_state_handler'), 'RPCServer.State') +endfunction + +function! s:update_goroutines_state_handler(check_errors, res) abort try - let l:res = s:call_jsonrpc('RPCServer.State') + call a:check_errors() + let l:currentGoroutineID = 0 try - if type(l:res) is type({}) && has_key(l:res, 'result') && !empty(l:res['result']) - let l:currentGoroutineID = l:res["result"]["State"]["currentGoroutine"]["id"] + if type(a:res) is type({}) && has_key(a:res, 'result') && !empty(a:res['result']) + let l:currentGoroutineID = a:res["result"]["State"]["currentGoroutine"]["id"] endif catch call go#util#EchoWarning("current goroutine not found...") endtry - let l:res = s:call_jsonrpc('RPCServer.ListGoroutines') - call s:show_goroutines(l:currentGoroutineID, l:res) + call s:call_jsonrpc(function('s:list_goroutines_handler', [l:currentGoroutineID]), 'RPCServer.ListGoroutines') + catch + call go#util#EchoError(printf('could not list goroutines: %s', v:exception)) + endtry +endfunction + +function s:list_goroutines_handler(currentGoroutineID, check_errors, res) abort + try + call a:check_errors() + call s:show_goroutines(a:currentGoroutineID, a:res) catch call go#util#EchoError(printf('could not show goroutines: %s', v:exception)) endtry - endfunction +endfunction function! s:show_goroutines(currentGoroutineID, res) abort let l:goroutines_winid = bufwinid('__GODEBUG_GOROUTINES__') @@ -902,21 +1007,39 @@ function! s:update_variables() abort \ } try - let res = s:call_jsonrpc('RPCServer.ListLocalVars', l:cfg) + call s:call_jsonrpc(function('s:handle_list_local_vars'), 'RPCServer.ListLocalVars', l:cfg) + catch + call go#util#EchoError(printf('could not list variables: %s', v:exception)) + endtry + + try + call s:call_jsonrpc(function('s:handle_list_function_args'), 'RPCServer.ListFunctionArgs', l:cfg) + catch + call go#util#EchoError(printf('could not list function arguments: %s', v:exception)) + endtry +endfunction + +function! s:handle_list_local_vars(check_errors, res) abort + try + call a:check_errors() let s:state['localVars'] = {} - if type(l:res) is type({}) && has_key(l:res, 'result') && !empty(l:res.result) - let s:state['localVars'] = l:res.result['Variables'] + if type(a:res) is type({}) && has_key(a:res, 'result') && !empty(a:res.result) + let s:state['localVars'] = a:res.result['Variables'] endif catch call go#util#EchoError(printf('could not list variables: %s', v:exception)) endtry + call s:show_variables() +endfunction + +function! s:handle_list_function_args(check_errors, res) abort try - let res = s:call_jsonrpc('RPCServer.ListFunctionArgs', l:cfg) + call a:check_errors() let s:state['functionArgs'] = {} - if type(l:res) is type({}) && has_key(l:res, 'result') && !empty(l:res.result) - let s:state['functionArgs'] = res.result['Args'] + if type(a:res) is type({}) && has_key(a:res, 'result') && !empty(a:res.result) + let s:state['functionArgs'] = a:res.result['Args'] endif catch call go#util#EchoError(printf('could not list function arguments: %s', v:exception)) @@ -927,8 +1050,11 @@ endfunction function! go#debug#Set(symbol, value) abort try - let l:res = s:call_jsonrpc('RPCServer.State') - call s:call_jsonrpc('RPCServer.Set', { + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.State') + let l:res = l:promise.await() + + call s:call_jsonrpc(function('s:handle_set'), 'RPCServer.Set', { \ 'symbol': a:symbol, \ 'value': a:value, \ 'scope': {'GoroutineID': l:res.result.State.currentThread.goroutineID} @@ -940,10 +1066,19 @@ function! go#debug#Set(symbol, value) abort call s:update_variables() endfunction +function! s:handle_set(check_errors, res) abort + try + call a:check_errors() + catch + call go#util#EchoError(printf('could not set symbol value: %s', v:exception)) + endtry + + call s:update_variables() +endfunction + function! s:update_stacktrace() abort try - let l:res = s:call_jsonrpc('RPCServer.Stacktrace', {'id': s:goroutineID(), 'depth': 5}) - call s:show_stacktrace(l:res) + call s:call_jsonrpc(function('s:show_stacktrace'), 'RPCServer.Stacktrace', {'id': s:goroutineID(), 'depth': 5}) catch call go#util#EchoError(printf('could not update stack: %s', v:exception)) endtry @@ -976,9 +1111,11 @@ function! go#debug#Stack(name) abort if s:state.running is 0 let s:state.running = 1 let l:name = 'continue' + call s:continue() endif " Add a breakpoint to the main.Main if the user didn't define any. + " TODO(bc): actually set set the breakpoint in main.Main if len(s:list_breakpoints()) is 0 if go#debug#Breakpoint() isnot 0 let s:state.running = 0 @@ -993,16 +1130,15 @@ function! go#debug#Stack(name) abort " https://github.com/go-delve/delve/blob/ab5713d3ec5d12754f4b2edf85e4b36a08b67c48/Documentation/api/ClientHowto.md#special-continue-commands-and-asynchronous-breakpoints " for more information. if l:name is# 'next' && get(s:, 'stack_name', '') is# 'next' - call s:call_jsonrpc('RPCServer.CancelNext') + " use s:rpc_response so that the any errors will be checked instead of + " completely discarding the result with s:noop. + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.CancelNext') + call l:promise.await() endif let s:stack_name = l:name try - let l:res = s:call_jsonrpc('RPCServer.Command', {'name': l:name}) - - if l:name is# 'next' - call s:handleNextInProgress(l:res) - endif - call s:stack_cb(l:res) + call s:call_jsonrpc(function('s:handle_stack_response', [l:name]), 'RPCServer.Command', {'name': l:name}) catch call go#util#EchoError(printf('rpc failure: %s', v:exception)) call s:clearState() @@ -1014,6 +1150,23 @@ function! go#debug#Stack(name) abort endtry endfunction +function! s:handle_stack_response(command, check_errors, res) abort + try + call a:check_errors() + + if a:command is# 'next' + call s:handleNextInProgress(a:res) + endif + + call s:stack_cb(a:res) + catch + call go#util#EchoError(printf('rpc failure: %s', v:exception)) + call s:clearState() + call go#util#EchoInfo('restarting debugger') + call go#debug#Restart() + endtry +endfunction + function! s:handleNextInProgress(res) try let l:res = a:res @@ -1022,8 +1175,9 @@ function! s:handleNextInProgress(res) if l:res.result.State.NextInProgress == v:true " TODO(bc): message the user that a breakpoint was hit in a different " goroutine while trying to resume. - " was hit. - let l:res = s:call_jsonrpc('RPCServer.Command', {'name': 'continue'}) + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.Command', {'name': 'continue'}) + let l:res = l:promise.await() else return endif @@ -1040,13 +1194,14 @@ function! go#debug#Restart() abort call s:stop() let s:state = { - \ 'rpcid': 1, - \ 'running': 0, - \ 'currentThread': {}, - \ 'localVars': {}, - \ 'functionArgs': {}, - \ 'message': [], - \} + \ 'rpcid': 1, + \ 'running': 0, + \ 'currentThread': {}, + \ 'localVars': {}, + \ 'functionArgs': {}, + \ 'message': [], + \ 'resultHandlers': {}, + \ } call call('go#debug#Start', s:start_args) catch @@ -1068,7 +1223,9 @@ function! go#debug#Goroutine() abort endif try - let l:res = s:call_jsonrpc('RPCServer.Command', {'Name': 'switchGoroutine', 'GoroutineID': l:goroutineID}) + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.Command', {'Name': 'switchGoroutine', 'GoroutineID': l:goroutineID}) + let l:res = l:promise.await() call s:stack_cb(l:res) call go#util#EchoInfo("Switched goroutine to: " . l:goroutineID) catch @@ -1107,12 +1264,16 @@ function! go#debug#Breakpoint(...) abort if type(l:found) == v:t_dict && !empty(l:found) call s:sign_unplace(l:found.id, l:found.file) if s:isActive() - let res = s:call_jsonrpc('RPCServer.ClearBreakpoint', {'id': l:found.id}) + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.ClearBreakpoint', {'id': l:found.id}) + let res = l:promise.await() endif else " Add breakpoint if s:isActive() - let l:res = s:call_jsonrpc('RPCServer.CreateBreakpoint', {'Breakpoint': {'file': l:filename, 'line': l:linenr}}) - let l:bt = res.result.Breakpoint + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.CreateBreakpoint', {'Breakpoint': {'file': l:filename, 'line': l:linenr}}) + let l:res = l:promise.await() + let l:bt = l:res.result.Breakpoint call s:sign_place(l:bt.id, l:bt.file, l:bt.line) else let l:id = len(s:list_breakpoints()) + 1 @@ -1210,7 +1371,6 @@ function! s:sign_getplaced() abort return l:signs endif - " it would be nice to use lambda's here, but vim-vimparser currently fails " to parse lamdas as map() arguments. " TODO(bc): return flatten(map(filter(copy(getbufinfo()), { _, val -> val.listed }), { _, val -> sign_getplaced(val.bufnr, {'group': 'vim-go-debug', 'name': 'godebugbreakpoint'})})) @@ -1232,6 +1392,18 @@ endfunction exe 'sign define godebugbreakpoint text='.go#config#DebugBreakpointSignText().' texthl=GoDebugBreakpoint' sign define godebugcurline text== texthl=GoDebugCurrent linehl=GoDebugCurrent +" s:rpc_response is a convenience function to check for errors and return +" a:res when a:res is not an error response. +function! s:rpc_response(check_errors, res) abort + call a:check_errors() + return a:res +endfunction + +" s:noop is a noop function. It takes any number of arguments and does +" nothing. +function s:noop(...) abort +endfunction + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/autoload/go/promise.vim b/autoload/go/promise.vim index 76c2f7c3bd..4782883a56 100644 --- a/autoload/go/promise.vim +++ b/autoload/go/promise.vim @@ -19,13 +19,18 @@ function! go#promise#New(fn, timeout, default) abort " explicitly bind to state so that within l:promise's methods, self will " always refer to state. See :help Partial for more information. return { - \ 'wrapper': function('s:wrapper', [a:fn], l:state), + \ 'wrapper': function('s:wrapper', [a:fn, a:default], l:state), \ 'await': function('s:await', [a:timeout, a:default], l:state), \ } endfunction -function! s:wrapper(fn, ...) dict - let self.retval = call(a:fn, a:000) +function! s:wrapper(fn, default, ...) dict + try + let self.retval = call(a:fn, a:000) + catch + let self.retval = substitute(v:exception, '^Vim', '', '') + let self.exception = 1 + endtry return self.retval endfunction @@ -36,6 +41,9 @@ function! s:await(timeout, default) dict endwhile call timer_stop(l:timer) + if get(self, 'exception', 0) + throw self.retval + endif return self.retval endfunction