" don't spam the user when Vim is started in Vi compatibility mode let s:cpo_save = &cpo set cpo&vim scriptencoding utf-8 if !exists('s:state') let s:state = { \ 'rpcid': 1, \ 'running': 0, \ 'currentThread': {}, \ 'localVars': {}, \ 'functionArgs': {}, \ 'message': [], \ 'resultHandlers': {}, \ 'kill_on_detach': v:true, \ } if go#util#HasDebug('debugger-state') call go#config#SetDebugDiag(s:state) endif endif if !exists('s:start_args') let s:start_args = [] endif function! s:goroutineID() abort return s:state['currentThread'].goroutineID endfunction function! s:complete(job, exit_status, data) abort let l:gotready = get(s:state, 'ready', 0) " copy messages to a:data _only_ when dlv exited non-zero and it was never " detected as ready (e.g. there was a compiler error). if a:exit_status > 0 && !l:gotready " copy messages to data so that vim-go's usual handling of errors from " async jobs will occur. call extend(a:data, s:state['message']) endif " return early instead of clearing any variables when the current job is not " a:job if has_key(s:state, 'job') && s:state['job'] != a:job return endif if has_key(s:state, 'job') call remove(s:state, 'job') endif if has_key(s:state, 'ready') call remove(s:state, 'ready') endif if has_key(s:state, 'ch') call remove(s:state, 'ch') endif call s:clearState() endfunction function! s:logger(prefix, ch, msg) abort let l:cur_win = bufwinnr('') let l:log_win = bufwinnr(bufnr('__GODEBUG_OUTPUT__')) if l:log_win == -1 return endif exe l:log_win 'wincmd w' try setlocal modifiable if getline(1) == '' call setline('$', a:prefix . a:msg) else call append('$', a:prefix . a:msg) endif normal! G setlocal nomodifiable finally exe l:cur_win 'wincmd w' endtry endfunction " 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': l:reqid, \ 'method': a:method, \ 'params': l:args, \}) try let l:ch = s:state['ch'] if has('nvim') call chansend(l:ch, l:req_json) else call ch_sendraw(l:ch, req_json) endif 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, \ }) endif redraw catch throw substitute(v:exception, '^Vim', '', '') endtry endfunction function! s:exited(res) abort if type(a:res) ==# type(v:null) return 0 endif let state = a:res.result.State return state.exited == v:true endfunction " Update the location of the current breakpoint or line we're halted on based on " response from dlv. function! s:update_breakpoint(res) abort if type(a:res) ==# type(v:null) return endif let state = a:res.result.State if !has_key(state, 'currentThread') return endif let s:state['currentThread'] = state.currentThread let bufs = filter(map(range(1, winnr('$')), '[v:val,bufname(winbufnr(v:val))]'), 'v:val[1]=~"\.go$"') if len(bufs) == 0 return endif exe bufs[0][0] 'wincmd w' let filename = state.currentThread.file let linenr = state.currentThread.line let oldfile = fnamemodify(expand('%'), ':p:gs!\\!/!') if oldfile != filename silent! exe 'edit' filename endif silent! exe 'norm!' linenr.'G' silent! normal! zvzz " TODO(bc): convert to use s:sign_unplace() silent! sign unplace 9999 " TODO(bc): convert to use s:sign_place() silent! exe 'sign place 9999 line=' . linenr . ' name=godebugcurline file=' . filename call s:warn_when_stale(fnamemodify(l:filename, ':p')) endfunction " Populate the stacktrace window. 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 let l:stack_win = bufwinnr(bufnr('__GODEBUG_STACKTRACE__')) if l:stack_win == -1 return endif let l:cur_win = bufwinnr('') exe l:stack_win 'wincmd w' try setlocal modifiable silent %delete _ for i in range(len(a:res.result.Locations)) let loc = a:res.result.Locations[i] if loc.file is# '?' || !has_key(loc, 'function') continue endif call setline(i+1, printf('%s - %s:%d', loc.function.name, fnamemodify(loc.file, ':p'), loc.line)) endfor finally setlocal nomodifiable exe l:cur_win 'wincmd w' endtry endfunction " Populate the variable window. function! s:show_variables() abort let l:var_win = bufwinnr(bufnr('__GODEBUG_VARIABLES__')) if l:var_win == -1 return endif let l:cur_win = bufwinnr('') exe l:var_win 'wincmd w' try setlocal modifiable silent %delete _ let v = [] let v += ['# Local Variables'] if type(get(s:state, 'localVars', [])) is type([]) for c in s:state['localVars'] let v += split(s:eval_tree(c, 0), "\n") endfor endif let v += [''] let v += ['# Function Arguments'] if type(get(s:state, 'functionArgs', [])) is type([]) for c in s:state['functionArgs'] let v += split(s:eval_tree(c, 0), "\n") endfor endif call setline(1, v) finally setlocal nomodifiable exe l:cur_win 'wincmd w' endtry endfunction function! s:clearState() abort let s:state['running'] = 0 let s:state['currentThread'] = {} let s:state['localVars'] = {} let s:state['functionArgs'] = {} let s:state['message'] = [] silent! sign unplace 9999 endfunction function! s:stop() abort call s:call_jsonrpc(function('s:noop'), 'RPCServer.Detach', {'kill': s:state['kill_on_detach']}) if has_key(s:state, 'job') call go#job#Wait(s:state['job']) " while waiting, the s:complete may have already removed job from s:state. if has_key(s:state, 'job') call remove(s:state, 'job') endif endif if has_key(s:state, 'ready') call remove(s:state, 'ready') endif if has_key(s:state, 'ch') call remove(s:state, 'ch') endif call s:clearState() endfunction function! go#debug#Stop() abort " Remove all commands and add back the default commands. for k in map(split(execute('command GoDebug'), "\n")[1:], 'matchstr(v:val, "^\\s*\\zs\\S\\+")') exe 'delcommand' k endfor command! -nargs=* -complete=customlist,go#package#Complete GoDebugStart call go#debug#Start('debug', ) command! -nargs=* -complete=customlist,go#package#Complete GoDebugTest call go#debug#Start('test', ) command! -nargs=1 GoDebugAttach call go#debug#Start('attach', ) command! -nargs=? GoDebugBreakpoint call go#debug#Breakpoint() " Remove all mappings. for k in map(split(execute('map (go-debug-'), "\n")[1:], 'matchstr(v:val, "^n\\s\\+\\zs\\S\\+")') exe 'unmap' k endfor call s:stop() let bufs = filter(map(range(1, winnr('$')), '[v:val,bufname(winbufnr(v:val))]'), 'v:val[1]=~"\.go$"') if len(bufs) > 0 exe bufs[0][0] 'wincmd w' else wincmd p endif silent! exe bufwinnr(bufnr('__GODEBUG_STACKTRACE__')) 'wincmd c' silent! exe bufwinnr(bufnr('__GODEBUG_VARIABLES__')) 'wincmd c' silent! exe bufwinnr(bufnr('__GODEBUG_OUTPUT__')) 'wincmd c' silent! exe bufwinnr(bufnr('__GODEBUG_GOROUTINES__')) 'wincmd c' if has('balloon_eval') let &ballooneval=s:ballooneval let &balloonexpr=s:balloonexpr endif augroup vim-go-debug autocmd! augroup END augroup! vim-go-debug endfunction function! s:goto_file() abort let m = matchlist(getline('.'), ' - \(.*\):\([0-9]\+\)$') if m[1] == '' return endif let bufs = filter(map(range(1, winnr('$')), '[v:val,bufname(winbufnr(v:val))]'), 'v:val[1]=~"\.go$"') if len(bufs) == 0 return endif exe bufs[0][0] 'wincmd w' let filename = m[1] let linenr = m[2] let oldfile = fnamemodify(expand('%'), ':p:gs!\\!/!') if oldfile != filename silent! exe 'edit' filename endif silent! exe 'norm!' linenr.'G' silent! normal! zvzz endfunction function! s:delete_expands() let nr = line('.') while 1 let l = getline(nr+1) if empty(l) || l =~ '^\S' return endif silent! exe (nr+1) . 'd _' endwhile silent! exe 'norm!' nr.'G' endfunction function! s:expand_var() abort " Get name from struct line. let name = matchstr(getline('.'), '^[^:]\+\ze: \*\?[a-zA-Z0-9-_/\.]\+\({\.\.\.}\)\?$') " Anonymous struct if name == '' let name = matchstr(getline('.'), '^[^:]\+\ze: \*\?struct {.\{-}}$') endif if name != '' setlocal modifiable let not_open = getline(line('.')+1) !~ '^ ' let l = line('.') call s:delete_expands() if not_open call append(l, split(s:eval(name), "\n")[1:]) endif silent! exe 'norm!' l.'G' setlocal nomodifiable return endif " Expand maps let m = matchlist(getline('.'), '^[^:]\+\ze: map.\{-}\[\(\d\+\)\]$') if len(m) > 0 && m[1] != '' setlocal modifiable let not_open = getline(line('.')+1) !~ '^ ' let l = line('.') call s:delete_expands() if not_open " TODO: Not sure how to do this yet... Need to get keys of the map. " let vs = '' " for i in range(0, min([10, m[1]-1])) " let vs .= ' ' . s:eval(printf("%s[%s]", m[0], )) " endfor " call append(l, split(vs, "\n")) endif silent! exe 'norm!' l.'G' setlocal nomodifiable return endif " Expand string. let m = matchlist(getline('.'), '^\([^:]\+\)\ze: \(string\)\[\([0-9]\+\)\]\(: .\{-}\)\?$') if len(m) > 0 && m[1] != '' setlocal modifiable let not_open = getline(line('.')+1) !~ '^ ' let l = line('.') call s:delete_expands() if not_open let vs = '' for i in range(0, min([10, m[3]-1])) let vs .= ' ' . s:eval(m[1] . '[' . i . ']') endfor call append(l, split(vs, "\n")) endif silent! exe 'norm!' l.'G' setlocal nomodifiable return endif " Expand slice. let m = matchlist(getline('.'), '^\([^:]\+\)\ze: \(\[\]\w\{-}\)\[\([0-9]\+\)\]$') if len(m) > 0 && m[1] != '' setlocal modifiable let not_open = getline(line('.')+1) !~ '^ ' let l = line('.') call s:delete_expands() if not_open let vs = '' for i in range(0, min([10, m[3]-1])) let vs .= ' ' . s:eval(m[1] . '[' . i . ']') endfor call append(l, split(vs, "\n")) endif silent! exe 'norm!' l.'G' setlocal nomodifiable return endif endfunction function! s:start_cb() abort let l:winid = win_getid() silent! only! let winnum = bufwinnr(bufnr('__GODEBUG_STACKTRACE__')) if winnum != -1 return endif let debugwindows = go#config#DebugWindows() if has_key(debugwindows, "vars") && debugwindows['vars'] != '' exe 'silent ' . debugwindows['vars'] silent file `='__GODEBUG_VARIABLES__'` setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline setlocal filetype=godebugvariables call append(0, ["# Local Variables", "", "# Function Arguments"]) nmap :call expand_var() nmap q (go-debug-stop) endif if has_key(debugwindows, "stack") && debugwindows['stack'] != '' exe 'silent ' . debugwindows['stack'] silent file `='__GODEBUG_STACKTRACE__'` setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline setlocal filetype=godebugstacktrace nmap :call goto_file() nmap q (go-debug-stop) endif if has_key(debugwindows, "goroutines") && debugwindows['goroutines'] != '' exe 'silent ' . debugwindows['goroutines'] silent file `='__GODEBUG_GOROUTINES__'` setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline setlocal filetype=godebugvariables call append(0, ["# Goroutines"]) nmap :call go#debug#Goroutine() endif if has_key(debugwindows, "out") && debugwindows['out'] != '' exe 'silent ' . debugwindows['out'] silent file `='__GODEBUG_OUTPUT__'` setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline setlocal filetype=godebugoutput nmap q (go-debug-stop) endif call win_gotoid(l:winid) silent! delcommand GoDebugStart silent! delcommand GoDebugTest silent! delcommand GoDebugAttach 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=* GoDebugSet call go#debug#Set() command! -nargs=1 GoDebugPrint call go#debug#Print() command! -nargs=0 GoDebugHalt call go#debug#Stack('halt') 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-print) :call go#debug#Print(expand('')) nnoremap (go-debug-halt) :call go#debug#Stack('halt') if has('balloon_eval') let s:balloonexpr=&balloonexpr let s:ballooneval=&ballooneval set balloonexpr=go#debug#BalloonExpr() set ballooneval endif augroup vim-go-debug autocmd! * autocmd FileType go nmap (go-debug-continue) autocmd FileType go nmap (go-debug-print) autocmd FileType go nmap (go-debug-breakpoint) autocmd FileType go nmap (go-debug-next) autocmd FileType go nmap (go-debug-step) autocmd FileType go nmap (go-debug-halt) augroup END doautocmd vim-go-debug FileType go endfunction function! s:err_cb(ch, msg) abort if get(s:state, 'ready', 0) != 0 call s:logger('ERR: ', a:ch, a:msg) return endif let s:state['message'] += [a:msg] endfunction function! s:out_cb(ch, msg) abort if get(s:state, 'ready', 0) != 0 call s:logger('OUT: ', a:ch, a:msg) return endif let s:state['message'] += [a:msg] if stridx(a:msg, go#config#DebugAddress()) != -1 let s:state['data'] = [] let l:state = {'databuf': ''} " explicitly bind callback to state so that within it, self will " always refer to state. See :help Partial for more information. let l:state.on_data = function('s:on_data', [], l:state) if has('nvim') let l:ch = sockconnect('tcp', go#config#DebugAddress(), {'on_data': l:state.on_data, 'state': l:state}) if l:ch == 0 call go#util#EchoError("could not connect to debugger") call go#job#Stop(s:state['job']) return endif else let l:ch = ch_open(go#config#DebugAddress(), {'mode': 'raw', 'timeout': 20000, 'callback': l:state.on_data}) if ch_status(l:ch) !=# 'open' call go#util#EchoError("could not connect to debugger") call go#job#Stop(s:state['job']) return endif endif let s:state['ch'] = l:ch " After this block executes, Delve will be running with all the " breakpoints setup, so this callback doesn't have to run again; just log " future messages. let s:state['ready'] = 1 " replace all the breakpoints set before delve started so that the ids won't overlap. for l:bt in s:list_breakpoints() call s:sign_unplace(l:bt.id, l:bt.file) call go#debug#Breakpoint(l:bt.line, l:bt.file) endfor call s:start_cb() endif endfunction " s:on_data's third optional argument is provided, but not used, so that the " same function can be used for Vim's 'callback' and Neovim's 'data'. function! s:on_data(ch, data, ...) dict abort let l:data = s:message(self.databuf, a:data) 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 if has('nvim') " 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 endif return printf('%s%s', a:buf, a:data) endfunction " s:check_errors 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:check_errors(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:check_errors value so that the result handler can call s:check_errors " without passing any arguments to check whether the response is an error " response. call call(s:state.resultHandlers[l:id], [function('s:check_errors', [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(mode, ...) abort call go#cmd#autowrite() if !go#util#has_job() call go#util#EchoError('This feature requires either Vim 8.0.0087 or newer with +job or Neovim.') return endif " It's already running. if has_key(s:state, 'job') return s:state['job'] endif let s:start_args = [a:mode] + a:000 if go#util#HasDebug('debugger-state') call go#config#SetDebugDiag(s:state) endif let dlv = go#path#CheckBinPath("dlv") if empty(dlv) return endif try let l:cmd = [dlv, a:mode] let s:state['kill_on_detach'] = v:true if a:mode is 'debug' || a:mode is 'test' let l:cmd = extend(l:cmd, s:package(a:000)) let l:cmd = extend(l:cmd, ['--output', tempname()]) elseif a:mode is 'attach' let l:cmd = add(l:cmd, a:1) let s:state['kill_on_detach'] = v:false else call go#util#EchoError('Unknown dlv command') endif let l:cmd += [ \ '--headless', \ '--api-version', '2', \ '--listen', go#config#DebugAddress(), \] let l:debugLogOutput = go#config#DebugLogOutput() if l:debugLogOutput != '' let cmd += ['--log', '--log-output', l:debugLogOutput] endif let l:buildtags = go#config#BuildTags() if buildtags isnot '' let l:cmd += ['--build-flags', '--tags=' . buildtags] endif if len(a:000) > 1 let l:cmd += ['--'] + a:000[1:] endif let s:state['message'] = [] let l:opts = { \ 'for': 'GoDebug', \ 'statustype': 'debug', \ 'complete': function('s:complete'), \ } let l:opts = go#job#Options(l:opts) let l:opts.out_cb = function('s:out_cb') let l:opts.err_cb = function('s:err_cb') let l:opts.stoponexit = 'kill' let s:state['job'] = go#job#Start(l:cmd, l:opts) catch call go#util#EchoError(printf('could not start debugger: %s', v:exception)) endtry return s:state['job'] endfunction " s:package returns the import path of package name of a :GoDebug(Start|Test) " call as a list so that the package can be appended to a command list using " extend(). args is expected to be a (potentially empty_ list. The first " element in args (if there are any) is expected to be a package path. An " emnpty list is returned when either args is an empty list or the import path " cannot be determined. function! s:package(args) if len(a:args) == 0 return [] endif " append the package when it's given. let l:pkgname = a:args[0] if l:pkgname[0] == '.' let l:pkgabspath = fnamemodify(l:pkgname, ':p') let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' let l:dir = getcwd() let l:dir = go#util#Chdir(expand('%:p:h')) try let l:pkgname = go#package#FromPath(l:pkgabspath) if type(l:pkgname) == type(0) call go#util#EchoError('could not determine package name') return [] endif finally call go#util#Chdir(l:dir) endtry endif return [l:pkgname] endfunction " Translate a reflect kind constant to a human string. function! s:reflect_kind(k) " Kind constants from Go's reflect package. return [ \ 'Invalid Kind', \ 'Bool', \ 'Int', \ 'Int8', \ 'Int16', \ 'Int32', \ 'Int64', \ 'Uint', \ 'Uint8', \ 'Uint16', \ 'Uint32', \ 'Uint64', \ 'Uintptr', \ 'Float32', \ 'Float64', \ 'Complex64', \ 'Complex128', \ 'Array', \ 'Chan', \ 'Func', \ 'Interface', \ 'Map', \ 'Ptr', \ 'Slice', \ 'String', \ 'Struct', \ 'UnsafePointer', \ ][a:k] endfunction function! s:eval_tree(var, nest) abort if a:var.name =~ '^\~' return '' endif let nest = a:nest let v = '' let kind = s:reflect_kind(a:var.kind) if !empty(a:var.name) let v .= repeat(' ', nest) . a:var.name . ': ' if kind == 'Bool' let v .= printf("%s\n", a:var.value) elseif kind == 'Struct' " Anonymous struct if a:var.type[:8] == 'struct { ' let v .= printf("%s\n", a:var.type) else let v .= printf("%s{...}\n", a:var.type) endif elseif kind == 'String' let v .= printf("%s[%d]%s\n", a:var.type, a:var.len, \ len(a:var.value) > 0 ? ': ' . a:var.value : '') elseif kind == 'Slice' || kind == 'String' || kind == 'Map' || kind == 'Array' let v .= printf("%s[%d]\n", a:var.type, a:var.len) elseif kind == 'Chan' || kind == 'Func' || kind == 'Interface' let v .= printf("%s\n", a:var.type) elseif kind == 'Ptr' " TODO: We can do something more useful here. let v .= printf("%s\n", a:var.type) elseif kind == 'Complex64' || kind == 'Complex128' let v .= printf("%s%s\n", a:var.type, a:var.value) " Int, Float else let v .= printf("%s(%s)\n", a:var.type, a:var.value) endif else let nest -= 1 endif if index(['Chan', 'Complex64', 'Complex128'], kind) == -1 && a:var.type != 'error' for c in a:var.children let v .= s:eval_tree(c, nest+1) endfor endif return v endfunction function! s:eval(arg) abort try 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)) return '' endtry endfunction function! go#debug#BalloonExpr() abort silent! let l:v = s:eval(v:beval_text) return l:v endfunction function! go#debug#Print(arg) abort try echo substitute(s:eval(a:arg), "\n$", "", 0) catch call go#util#EchoError(printf('could not print: %s', v:exception)) endtry 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 call a:check_errors() let l:currentGoroutineID = 0 try 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 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 function! s:show_goroutines(currentGoroutineID, res) abort let l:goroutines_winid = bufwinid('__GODEBUG_GOROUTINES__') if l:goroutines_winid == -1 return endif let l:winid = win_getid() call win_gotoid(l:goroutines_winid) try setlocal modifiable silent %delete _ let v = ['# Goroutines'] if type(a:res) isnot type({}) || !has_key(a:res, 'result') || empty(a:res['result']) call setline(1, v) return endif let l:goroutines = a:res["result"]["Goroutines"] if len(l:goroutines) == 0 call go#util#EchoWarning("No Goroutines Running Now...") call setline(1, v) return endif for l:idx in range(len(l:goroutines)) let l:goroutine = l:goroutines[l:idx] let l:goroutineType = "" let l:loc = 0 if l:goroutine.startLoc.file != "" let l:loc = l:goroutine.startLoc let l:goroutineType = "Start" endif if l:goroutine.goStatementLoc.file != "" let l:loc = l:goroutine.goStatementLoc let l:goroutineType = "Go" endif if l:goroutine.currentLoc.file != "" let l:loc = l:goroutine.currentLoc let l:goroutineType = "Runtime" endif if l:goroutine.userCurrentLoc.file != "" let l:loc=l:goroutine.userCurrentLoc let l:goroutineType = "User" endif " The current goroutine can be changed by pressing enter on one of the " lines listing a non-active goroutine. If the format of either of these " lines is modified, then make sure that go#debug#Goroutine is also " changed if needed. if l:goroutine.id == a:currentGoroutineID let l:g = printf("* Goroutine %s - %s: %s:%s %s (thread: %s)", l:goroutine.id, l:goroutineType, l:loc.file, l:loc.line, l:loc.function.name, l:goroutine.threadID) else let l:g = printf(" Goroutine %s - %s: %s:%s %s (thread: %s)", l:goroutine.id, l:goroutineType, l:loc.file, l:loc.line, l:loc.function.name, l:goroutine.threadID) endif let v += [l:g] endfor call setline(1, v) finally setlocal nomodifiable call win_gotoid(l:winid) endtry endfunction function! s:update_variables() abort " FollowPointers requests pointers to be automatically dereferenced. " MaxVariableRecurse is how far to recurse when evaluating nested types. " MaxStringLen is the maximum number of bytes read from a string " MaxArrayValues is the maximum number of elements read from an array, a slice or a map. " MaxStructFields is the maximum number of fields read from a struct, -1 will read all fields. let l:cfg = { \ 'scope': {'GoroutineID': s:goroutineID()}, \ 'cfg': {'MaxStringLen': 20, 'MaxArrayValues': 20} \ } try 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 let s:state['localVars'] = {} try call a:check_errors() 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#EchoWarning(printf('could not list variables: %s', v:exception)) endtry call s:show_variables() endfunction function! s:handle_list_function_args(check_errors, res) abort let s:state['functionArgs'] = {} try call a:check_errors() 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#EchoWarning(printf('could not list function arguments: %s', v:exception)) endtry call s:show_variables() endfunction function! go#debug#Set(symbol, value) abort try 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} \ }) catch call go#util#EchoError(printf('could not set symbol value: %s', v:exception)) endtry 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 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 endfunction function! s:stack_cb(res) abort let s:stack_name = '' if type(a:res) isnot type({}) || !has_key(a:res, 'result') || empty(a:res.result) return endif if s:exited(a:res) call go#debug#Stop() return endif call s:update_breakpoint(a:res) call s:update_goroutines() call s:update_stacktrace() call s:update_variables() endfunction " Send a command to change the cursor location to Delve. " " a:name must be one of continue, next, step, or stepOut. function! go#debug#Stack(name) abort let l:name = a:name " Run continue if the program hasn't started yet. 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 return endif endif try " s:stack_name is reset in s:stack_cb(). While its value is 'next', the " current operation being performed by delve is a next operation and it " must be cancelled before another next operation can start. See " 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' " 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 silent! sign unplace 9999 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() call go#util#EchoInfo('restarting debugger') call go#debug#Restart() endtry catch call go#util#EchoError(printf('CancelNext RPC call failed: %s', v:exception)) 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 let l:w = 0 while l:w < 1 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. 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 endwhile catch throw v:exception endtry endfunction function! go#debug#Restart() abort call go#cmd#autowrite() try call s:stop() let s:state = { \ 'rpcid': 1, \ 'running': 0, \ 'currentThread': {}, \ 'localVars': {}, \ 'functionArgs': {}, \ 'message': [], \ 'resultHandlers': {}, \ 'kill_on_detach': s:state['kill_on_detach'], \ } call call('go#debug#Start', s:start_args) catch call go#util#EchoError(printf('restart failed: %s', v:exception)) endtry endfunction " Report if debugger mode is active. function! s:isActive() return len(s:state['message']) > 0 endfunction " Change Goroutine function! go#debug#Goroutine() abort let l:goroutineID = str2nr(substitute(getline('.'), '^ Goroutine \(.\{-1,\}\) - .*', '\1', 'g')) if l:goroutineID <= 0 return endif try 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 call go#util#EchoError(printf('could not switch goroutine: %s', v:exception)) endtry endfunction " Toggle breakpoint. Returns 0 on success and 1 on failure. function! go#debug#Breakpoint(...) abort let l:filename = fnamemodify(expand('%'), ':p:gs!\\!/!') let l:linenr = line('.') " Get line number from argument. if len(a:000) > 0 let l:linenr = str2nr(a:1) if l:linenr is 0 call go#util#EchoError('not a number: ' . a:1) return 0 endif if len(a:000) > 1 let l:filename = a:2 endif endif try " Check if we already have a breakpoint for this line. let l:found = {} for l:bt in s:list_breakpoints() if l:bt.file is# l:filename && l:bt.line is# l:linenr let l:found = l:bt break endif endfor " Remove breakpoint. if type(l:found) == v:t_dict && !empty(l:found) call s:sign_unplace(l:found.id, l:found.file) if s:isActive() 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: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 call s:sign_place(l:id, l:filename, l:linenr) endif endif catch call go#util#EchoError(printf('could not toggle breakpoint: %s', v:exception)) return 1 endtry return 0 endfunction function! s:sign_unplace(id, file) abort if !exists('*sign_unplace') exe 'sign unplace ' . a:id .' file=' . a:file return endif call sign_unplace('vim-go-debug', {'buffer': a:file, 'id': a:id}) endfunction function! s:sign_place(id, expr, lnum) abort if !exists('*sign_place') exe 'sign place ' . a:id . ' line=' . a:lnum . ' name=godebugbreakpoint file=' . a:expr return endif call sign_place(a:id, 'vim-go-debug', 'godebugbreakpoint', a:expr, {'lnum': a:lnum}) endfunction function! s:list_breakpoints() let l:breakpoints = [] let l:signs = s:sign_getplaced() for l:item in l:signs let l:file = fnamemodify(bufname(l:item.bufnr), ':p') for l:sign in l:item.signs call add(l:breakpoints, { \ 'id': l:sign.id, \ 'file': l:file, \ 'line': l:sign.lnum, \ }) endfor endfor return l:breakpoints endfunction function! s:sign_getplaced() abort if !exists('*sign_getplaced') " sign_getplaced was introduced in Vim 8.1.0614 " :sign place " --- Signs --- " Signs for a.go: " line=15 id=2 name=godebugbreakpoint " line=16 id=1 name=godebugbreakpoint " Signs for a_test.go: " line=6 id=3 name=godebugbreakpoint " l:signs should be the same sam form as the return value for " sign_getplaced(), a list with the following entries: " * bufnr - number of the buffer with the sign " * signs = list of signs placed in bufnr let l:signs = [] let l:file = '' for l:line in split(execute('sign place'), '\n')[1:] if l:line =~# '^Signs for ' let l:file = l:line[10:-2] continue else " sign place's output may end with Signs instead of starting with Signs. " See " https://github.com/fatih/vim-go/issues/2920#issuecomment-644885774. let l:idx = match(l:line, '\.go .* Signs:$') if l:idx >= 0 let l:file = l:line[0:l:idx+2] continue endif endif if l:line !~# 'name=godebugbreakpoint' continue endif let l:sign = matchlist(l:line, '\vline\=(\d+) +id\=(\d+)') call add(l:signs, { \ 'bufnr': bufnr(l:file), \ 'signs': [{ \ 'id': str2nr(l:sign[2]), \ 'lnum': str2nr(l:sign[1]), \ }], \ }) endfor 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'})})) let l:bufinfo = getbufinfo() let l:listed = [] for l:info in l:bufinfo if l:info.listed let l:listed = add(l:listed, l:info) endif endfor let l:signs = [] for l:buf in l:listed let l:signs = add(l:signs, sign_getplaced(l:buf.bufnr, {'group': 'vim-go-debug', 'name': 'godebugbreakpoint'})[0]) endfor return l:signs 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 function! s:warn_when_stale(filename) abort let l:bufinfo = getbufinfo(a:filename) if len(l:bufinfo) == 0 return endif if l:bufinfo[0].changed call s:warn_stale() return endif call s:call_jsonrpc(function('s:handle_staleness_check_response', [fnamemodify(a:filename, ':p')]), 'RPCServer.LastModified') endfunction function! s:handle_staleness_check_response(filename, check_errors, res) abort try call a:check_errors() catch " swallow any errors return endtry let l:ftime = strftime('%Y-%m-%dT%H:%M:%S', getftime(a:filename)) if l:ftime < a:res.result.Time[0:(len(l:ftime) - 1)] return endif call s:warn_stale(a:filename) endfunction function! s:warn_stale(filename) abort call go#util#EchoWarning(printf('file locations may be incorrect, because %s has changed since debugging started', a:filename)) endfunction " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save " vim: sw=2 ts=2 et