diff --git a/README.md b/README.md index 8566224ace..d2113761a9 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ This plugin adds Go language support for Vim, with the following main features: with `:GoTest`. Run a single tests with `:GoTestFunc`). * Quickly execute your current file(s) with `:GoRun`. * Improved syntax highlighting and folding. +* Debug programs with integrated `delve` support with `:GoDebugStart`. * Completion support via `gocode`. * `gofmt` or `goimports` on save keeps the cursor position and undo history. * Go to symbol/declaration with `:GoDef`. * Look up documentation with `:GoDoc` or `:GoDocBrowser`. * Easily import packages via `:GoImport`, remove them via `:GoDrop`. -* Automatic `GOPATH` detection which works with `gb` and `godep`. Change or - display `GOPATH` with `:GoPath`. +* Precise type-safe renaming of identifiers with `:GoRename`. * See which code is covered by tests with `:GoCoverage`. * Add or remove tags on struct fields with `:GoAddTags` and `:GoRemoveTags`. * Call `gometalinter` with `:GoMetaLinter` to invoke all possible linters @@ -28,7 +28,6 @@ This plugin adds Go language support for Vim, with the following main features: errors, or make sure errors are checked with `:GoErrCheck`. * Advanced source analysis tools utilizing `guru`, such as `:GoImplements`, `:GoCallees`, and `:GoReferrers`. -* Precise type-safe renaming of identifiers with `:GoRename`. * ... and many more! Please see [doc/vim-go.txt](doc/vim-go.txt) for more information. diff --git a/autoload/go/debug.vim b/autoload/go/debug.vim new file mode 100644 index 0000000000..a03bd9e1bf --- /dev/null +++ b/autoload/go/debug.vim @@ -0,0 +1,880 @@ +scriptencoding utf-8 + +if !exists('g:go_debug_windows') + let g:go_debug_windows = { + \ 'stack': 'leftabove 20vnew', + \ 'out': 'botright 10new', + \ 'vars': 'leftabove 30vnew', + \ } +endif + +if !exists('g:go_debug_address') + let g:go_debug_address = '127.0.0.1:8181' +endif + +if !exists('s:state') + let s:state = { + \ 'rpcid': 1, + \ 'breakpoint': {}, + \ 'currentThread': {}, + \ 'localVars': {}, + \ 'functionArgs': {}, + \ 'message': [], + \} + + if go#util#HasDebug('debugger-state') + let g:go_debug_diag = s:state + endif +endif + +if !exists('s:start_args') + let s:start_args = [] +endif + +function! s:groutineID() abort + return s:state['currentThread'].goroutineID +endfunction + +function! s:exit(job, status) abort + if has_key(s:state, 'job') + call remove(s:state, 'job') + endif + call s:clearState() + if a:status > 0 + call go#util#EchoError(s:state['message']) + endif +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 + +function! s:call_jsonrpc(method, ...) abort + if go#util#HasDebug('debugger-commands') + if !exists('g:go_debug_commands') + let g:go_debug_commands = [] + endif + echom 'sending to dlv ' . a:method + endif + + if len(a:000) > 0 && type(a:000[0]) == v:t_func + let Cb = a:000[0] + let args = a:000[1:] + else + let Cb = v:none + let args = a:000 + endif + let s:state['rpcid'] += 1 + let req_json = json_encode({ + \ 'id': s:state['rpcid'], + \ 'method': a:method, + \ 'params': args, + \}) + + try + " Use callback + if type(Cb) == v:t_func + let s:ch = ch_open('127.0.0.1:8181', {'mode': 'nl', 'callback': Cb}) + call ch_sendraw(s:ch, req_json) + + if go#util#HasDebug('debugger-commands') + let g:go_debug_commands = add(g:go_debug_commands, { + \ 'request': req_json, + \ 'response': Cb, + \ }) + endif + return + endif + + let ch = ch_open('127.0.0.1:8181', {'mode': 'nl', 'timeout': 20000}) + call ch_sendraw(ch, req_json) + let resp_json = ch_readraw(ch) + + if go#util#HasDebug('debugger-commands') + let g:go_debug_commands = add(g:go_debug_commands, { + \ 'request': req_json, + \ 'response': resp_json, + \ }) + endif + + let obj = json_decode(resp_json) + if type(obj) == v:t_dict && has_key(obj, 'error') && !empty(obj.error) + throw obj.error + endif + return obj + catch + throw substitute(v:exception, '^Vim', '', '') + endtry +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) ==# v:t_none + 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 + silent! sign unplace 9999 + silent! exe 'sign place 9999 line=' . linenr . ' name=godebugcurline file=' . filename +endfunction + +" Populate the stacktrace window. +function! s:show_stacktrace(res) abort + if !has_key(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] + 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['currentThread'] = {} + let s:state['localVars'] = {} + let s:state['functionArgs'] = {} + silent! sign unplace 9999 +endfunction + +function! s:stop() abort + call s:clearState() + if has_key(s:state, 'job') + call job_stop(s:state['job']) + call remove(s:state, 'job') + endif +endfunction + +function! go#debug#Stop() abort + for k in keys(s:state['breakpoint']) + let bt = s:state['breakpoint'][k] + if bt.id >= 0 + silent exe 'sign unplace ' . bt.id + endif + endfor + + for k in filter(map(split(execute('command GoDebug'), "\n")[1:], 'matchstr(v:val,"^\\s*\\zs\\S\\+")'), 'v:val!="GoDebugStart"') + exe 'delcommand' k + endfor + for k in map(split(execute('map (go-debug-'), "\n")[1:], 'matchstr(v:val,"^n\\s\\+\\zs\\S\\+")') + exe 'unmap' k + endfor + + command! -nargs=* -complete=customlist,go#package#Complete GoDebugStart call go#debug#Start() + + 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' + + set noballooneval + set balloonexpr= +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(ch, json) abort + let res = json_decode(a:json) + if type(res) == v:t_dict && has_key(res, 'error') && !empty(res.error) + throw res.error + endif + if empty(res) || !has_key(res, 'result') + return + endif + for bt in res.result.Breakpoints + if bt.id >= 0 + let s:state['breakpoint'][bt.id] = bt + exe 'sign place '. bt.id .' line=' . bt.line . ' name=godebugbreakpoint file=' . bt.file + endif + endfor + + let oldbuf = bufnr('%') + silent! only! + + let winnum = bufwinnr(bufnr('__GODEBUG_STACKTRACE__')) + if winnum != -1 + return + endif + + if exists('g:go_debug_windows["stack"]') && g:go_debug_windows['stack'] != '' + exe 'silent ' . g:go_debug_windows['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 exists('g:go_debug_windows["out"]') && g:go_debug_windows['out'] != '' + exe 'silent ' . g:go_debug_windows['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 + + if exists('g:go_debug_windows["vars"]') && g:go_debug_windows['vars'] != '' + exe 'silent ' . g:go_debug_windows['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 + + silent! delcommand GoDebugStart + command! -nargs=0 GoDebugContinue call go#debug#Stack('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('')) + + nmap (go-debug-continue) + nmap (go-debug-print) + nmap (go-debug-breakpoint) + nmap (go-debug-next) + nmap (go-debug-step) + + set balloonexpr=go#debug#BalloonExpr() + set ballooneval + + exe bufwinnr(oldbuf) 'wincmd w' +endfunction + +function! s:starting(ch, msg) abort + call go#util#EchoProgress(a:msg) + let s:state['message'] += [a:msg] + if stridx(a:msg, g:go_debug_address) != -1 + call ch_setoptions(a:ch, { + \ 'out_cb': function('s:logger', ['OUT: ']), + \ 'err_cb': function('s:logger', ['ERR: ']), + \}) + + " Tell dlv about the breakpoints that the user added before delve started. + let l:breaks = copy(s:state.breakpoint) + let s:state['breakpoint'] = {} + for l:bt in values(l:breaks) + call go#debug#Breakpoint(bt.line) + endfor + + call s:call_jsonrpc('RPCServer.ListBreakpoints', function('s:start_cb')) + endif +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(...) abort + if has('nvim') + call go#util#EchoError('This feature only works in Vim for now; Neovim is not (yet) supported. Sorry :-(') + return + endif + if !go#util#has_job() + call go#util#EchoError('This feature requires Vim 8.0.0087 or newer with +job.') + return + endif + + " It's already running. + if has_key(s:state, 'job') && job_status(s:state['job']) == 'run' + return + endif + + let s:start_args = a:000 + + if go#util#HasDebug('debugger-state') + let g:go_debug_diag = s:state + endif + + let l:is_test = bufname('')[-8:] is# '_test.go' + + let dlv = go#path#CheckBinPath("dlv") + if empty(dlv) + return + endif + + try + if len(a:000) > 0 + let l:pkgname = a:1 + " Expand .; otherwise this won't work from a tmp dir. + if l:pkgname[0] == '.' + let l:pkgname = go#package#FromPath(getcwd()) . l:pkgname[1:] + endif + else + let l:pkgname = go#package#FromPath(getcwd()) + endif + + let l:args = [] + if len(a:000) > 1 + let l:args = ['--'] + a:000[1:] + endif + + let l:cmd = [ + \ dlv, + \ (l:is_test ? 'test' : 'debug'), + \ '--output', tempname(), + \ '--headless', + \ '--api-version', '2', + \ '--log', + \ '--listen', g:go_debug_address, + \ '--accept-multiclient', + \] + if get(g:, 'go_build_tags', '') isnot '' + let l:cmd += ['--build-flags', '--tags=' . g:go_build_tags] + endif + let l:cmd += l:args + + call go#util#EchoProgress('Starting GoDebug...') + let s:state['message'] = [] + let s:state['job'] = job_start(l:cmd, { + \ 'out_cb': function('s:starting'), + \ 'err_cb': function('s:starting'), + \ 'exit_cb': function('s:exit'), + \ 'stoponexit': 'kill', + \}) + catch + call go#util#EchoError(v:exception) + endtry +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 res = s:call_jsonrpc('RPCServer.State') + let goroutineID = res.result.State.currentThread.goroutineID + let res = s:call_jsonrpc('RPCServer.Eval', { + \ 'expr': a:arg, + \ 'scope': {'GoroutineID': goroutineID} + \ }) + return s:eval_tree(res.result.Variable, 0) + catch + call go#util#EchoError(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(v:exception) + 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:groutineID()}, + \ 'cfg': {'MaxStringLen': 20, 'MaxArrayValues': 20} + \ } + + try + let res = s:call_jsonrpc('RPCServer.ListLocalVars', l:cfg) + let s:state['localVars'] = res.result['Variables'] + catch + call go#util#EchoError(v:exception) + endtry + + try + let res = s:call_jsonrpc('RPCServer.ListFunctionArgs', l:cfg) + let s:state['functionArgs'] = res.result['Args'] + catch + call go#util#EchoError(v:exception) + endtry + + call s:show_variables() +endfunction + +function! go#debug#Set(symbol, value) abort + try + let res = s:call_jsonrpc('RPCServer.State') + let goroutineID = res.result.State.currentThread.goroutineID + call s:call_jsonrpc('RPCServer.Set', { + \ 'symbol': a:symbol, + \ 'value': a:value, + \ 'scope': {'GoroutineID': goroutineID} + \ }) + catch + call go#util#EchoError(v:exception) + endtry + + call s:update_variables() +endfunction + +function! s:update_stacktrace() abort + try + let res = s:call_jsonrpc('RPCServer.Stacktrace', {'id': s:groutineID(), 'depth': 5}) + call s:show_stacktrace(res) + catch + call go#util#EchoError(v:exception) + endtry +endfunction + +function! s:stack_cb(ch, json) abort + let s:stack_name = '' + let res = json_decode(a:json) + if type(res) == v:t_dict && has_key(res, 'error') && !empty(res.error) + call go#util#EchoError(res.error) + call s:clearState() + call go#debug#Restart() + return + endif + + if empty(res) || !has_key(res, 'result') + return + endif + call s:update_breakpoint(res) + call s:update_stacktrace() + call s:update_variables() +endfunction + +" Send a command change the cursor location to Delve. +" +" a:name must be one of continue, next, step, or stepOut. +function! go#debug#Stack(name) abort + let name = a:name + + " Run continue if the program hasn't started yet. + if s:state['rpcid'] <= 2 + let name = 'continue' + endif + + " Add a breakpoint to the main.Main if the user didn't define any. + if len(s:state['breakpoint']) is 0 + try + let res = s:call_jsonrpc('RPCServer.FindLocation', {'loc': 'main.main'}) + let res = s:call_jsonrpc('RPCServer.CreateBreakpoint', {'Breakpoint':{'addr': res.result.Locations[0].pc}}) + let bt = res.result.Breakpoint + let s:state['breakpoint'][bt.id] = bt + catch + call go#util#EchoError(v:exception) + endtry + endif + + try + if name is# 'next' && get(s:, 'stack_name', '') is# 'next' + call s:call_jsonrpc('RPCServer.CancelNext') + endif + let s:stack_name = name + call s:call_jsonrpc('RPCServer.Command', function('s:stack_cb'), {'name': name}) + catch + call go#util#EchoError(v:exception) + endtry +endfunction + +function! go#debug#Restart() abort + try + call job_stop(s:state['job']) + while has_key(s:state, 'job') && job_status(s:state['job']) is# 'run' + sleep 50m + endwhile + + let l:breaks = s:state['breakpoint'] + let s:state = { + \ 'rpcid': 1, + \ 'breakpoint': {}, + \ 'currentThread': {}, + \ 'localVars': {}, + \ 'functionArgs': {}, + \ 'message': [], + \} + + " Preserve breakpoints. + for bt in values(l:breaks) + " TODO: should use correct filename + exe 'sign unplace '. bt.id .' file=' . bt.file + call go#debug#Breakpoint(bt.line) + endfor + call call('go#debug#Start', s:start_args) + catch + call go#util#EchoError(v:exception) + endtry +endfunction + +" Report if debugger mode is active. +function! s:isActive() + return len(s:state['message']) > 0 +endfunction + +" Toggle breakpoint. +function! go#debug#Breakpoint(...) abort + let filename = fnamemodify(expand('%'), ':p:gs!\\!/!') + + if len(a:000) > 0 + let linenr = str2nr(a:1) + if linenr is 0 + call go#util#EchoError('not a number: ' . a:1) + return + endif + else + let linenr = line('.') + endif + + try + " Check if we already have a breakpoint for this line. + let found = v:none + for k in keys(s:state.breakpoint) + let bt = s:state.breakpoint[k] + if bt.file == filename && bt.line == linenr + let found = bt + break + endif + endfor + + " Remove breakpoint. + if type(found) == v:t_dict + call remove(s:state['breakpoint'], bt.id) + exe 'sign unplace '. found.id .' file=' . found.file + if s:isActive() + let res = s:call_jsonrpc('RPCServer.ClearBreakpoint', {'id': found.id}) + endif + " Add breakpoint. + else + if s:isActive() + let res = s:call_jsonrpc('RPCServer.CreateBreakpoint', {'Breakpoint':{'file': filename, 'line': linenr}}) + let bt = res.result.Breakpoint + exe 'sign place '. bt.id .' line=' . bt.line . ' name=godebugbreakpoint file=' . bt.file + let s:state['breakpoint'][bt.id] = bt + else + let id = len(s:state['breakpoint']) + 1 + let s:state['breakpoint'][id] = {'id': id, 'file': filename, 'line': linenr} + exe 'sign place '. id .' line=' . linenr . ' name=godebugbreakpoint file=' . filename + endif + endif + catch + call go#util#EchoError(v:exception) + endtry +endfunction + +sign define godebugbreakpoint text=> texthl=GoDebugBreakpoint +sign define godebugcurline text== linehl=GoDebugCurrent texthl=GoDebugCurrent + +fun! s:hi() + hi GoDebugBreakpoint term=standout ctermbg=117 ctermfg=0 guibg=#BAD4F5 guifg=Black + hi GoDebugCurrent term=reverse ctermbg=12 ctermfg=7 guibg=DarkBlue guifg=White +endfun +augroup vim-go-breakpoint + autocmd! + autocmd ColorScheme * call s:hi() +augroup end +call s:hi() + +" vim: sw=2 ts=2 et diff --git a/autoload/go/util.vim b/autoload/go/util.vim index bf72daddf8..9c620bce4c 100644 --- a/autoload/go/util.vim +++ b/autoload/go/util.vim @@ -362,7 +362,6 @@ function! go#util#archive() return expand("%:p:gs!\\!/!") . "\n" . strlen(l:buffer) . "\n" . l:buffer endfunction - " Make a named temporary directory which starts with "prefix". " " Unfortunately Vim's tempname() is not portable enough across various systems; @@ -384,7 +383,7 @@ function! go#util#tempdir(prefix) abort endfor if l:dir == '' - echoerr 'Unable to find directory to store temporary directory in' + call go#util#EchoError('Unable to find directory to store temporary directory in') return endif @@ -395,4 +394,9 @@ function! go#util#tempdir(prefix) abort return l:tmp endfunction +" Report if the user enabled a debug flag in g:go_debug. +function! go#util#HasDebug(flag) + return index(get(g:, 'go_debug', []), a:flag) >= 0 +endfunction + " vim: sw=2 ts=2 et diff --git a/doc/vim-go.txt b/doc/vim-go.txt index f2bedc954f..4227b52520 100644 --- a/doc/vim-go.txt +++ b/doc/vim-go.txt @@ -22,10 +22,11 @@ CONTENTS *go-contents* 6. Functions....................................|go-functions| 7. Settings.....................................|go-settings| 8. Syntax highlighting..........................|go-syntax| - 9. FAQ/Troubleshooting..........................|go-troubleshooting| - 10. Development..................................|go-development| - 11. Donation.....................................|go-donation| - 12. Credits......................................|go-credits| + 9. Debugger.....................................|go-debug| + 10. FAQ/Troubleshooting..........................|go-troubleshooting| + 11. Development..................................|go-development| + 12. Donation.....................................|go-donation| + 13. Credits......................................|go-credits| ============================================================================== INTRO *go-intro* @@ -40,13 +41,13 @@ tools developed by the Go community to provide a seamless Vim experience. test it with |:GoTest|. Run a single tests with |:GoTestFunc|). * Quickly execute your current file(s) with |:GoRun|. * Improved syntax highlighting and folding. + * Debug programs with integrated `delve` support with |:GoDebugStart|. * Completion support via `gocode`. * `gofmt` or `goimports` on save keeps the cursor position and undo history. * Go to symbol/declaration with |:GoDef|. * Look up documentation with |:GoDoc| or |:GoDocBrowser|. * Easily import packages via |:GoImport|, remove them via |:GoDrop|. - * Automatic `GOPATH` detection which works with `gb` and `godep`. Change or - display `GOPATH` with |:GoPath|. + * Precise type-safe renaming of identifiers with |:GoRename|. * See which code is covered by tests with |:GoCoverage|. * Add or remove tags on struct fields with |:GoAddTags| and |:GoRemoveTags|. * Call `gometalinter` with |:GoMetaLinter| to invoke all possible linters @@ -56,7 +57,8 @@ tools developed by the Go community to provide a seamless Vim experience. static errors, or make sure errors are checked with |:GoErrCheck|. * Advanced source analysis tools utilizing `guru`, such as |:GoImplements|, |:GoCallees|, and |:GoReferrers|. - * Precise type-safe renaming of identifiers with |:GoRename|. + * Automatic `GOPATH` detection which works with `gb` and `godep`. Change or + display `GOPATH` with |:GoPath|. * Integrated and improved snippets, supporting `ultisnips`, `neosnippet`, and `vim-minisnip`. * Share your current code to play.golang.org with |:GoPlay|. @@ -1658,6 +1660,18 @@ By default "snakecase" is used. Current values are: ["snakecase", > let g:go_addtags_transform = 'snakecase' < + *'g:go_debug'* + +A list of options to debug; useful for development and/or reporting bugs. + +Currently accepted values: + + debugger-state Expose debugger state in 'g:go_debug_diag'. + debugger-commands Echo communication between vim-go and `dlv`; requests and + responses are recorded in `g:go_debug_commands`. +> + let g:go_debug = [] +< ============================================================================== SYNTAX HIGHLIGHTING *ft-go-syntax* *go-syntax* @@ -1815,6 +1829,207 @@ The `gohtmltmpl` filetype is automatically set for `*.tmpl` files; the `gotexttmpl` is never automatically set and needs to be set manually. +============================================================================== +DEBUGGER *go-debug* + +Vim-go comes with a special "debugger mode". This starts a `dlv` process in +the background and provides various commands to communicate with it. + +This debugger is similar to Visual Studio or Eclipse and has the following +features: + + * Show stack trace and jumps. + * List local variables. + * List function arguments. + * Expand values of struct or array/slice. + * Show balloon on the symbol. + * Show output of stdout/stderr. + * Toggle breakpoint. + * Stack operation continue/next/step out. + +This feature requires Vim 8.0.0087 or newer with the |+job| feature. Neovim +does _not_ work (yet). +This requires Delve 1.0.0 or newer, and it is recommended to use Go 1.10 or +newer, as its new caching will speed up recompiles. + + *go-debug-intro* +GETTING STARTED WITH THE DEBUGGER~ + +Use |:GoDebugStart| to start the debugger. The first argument is the package +name, and any arguments after that will be passed on to the program; for +example: +> + :GoDebugStart . -someflag value +< +This may take few seconds. After the code is compiled you'll see three new +windows: the stack trace on left side, the variable list on the bottom-left, +and program output at the bottom. + +You can add breakpoints with |:GoDebugBreakpoint| () and run your program +with |:GoDebugContinue| (). + +The program will halt on the breakpoint, at which point you can inspect the +program state. You can go to the next line with |:GoDebugNext| () or step +in with |:GoDebugStep| (). + +The variable window in the bottom left (`GODEBUG_VARIABLES`) will display all +local variables. Struct values are displayed as `{...}`, array/slices as +`[4]`. Use on the variable name to expand the values. + +The `GODEBUG_OUTPUT` window displays output from the program and the Delve +debugger. + +The `GODEBUG_STACKTRACE` window can be used to jump to different places in the +call stack. + +When you're done use |:GoDebugStop| to close the debugging windows and halt +the `dlv` process, or |:GoDebugRestart| to recompile the code. + + *go-debug-commands* +DEBUGGER COMMANDS~ + +Only |:GoDebugStart| and |:GoDebugBreakpoint| are available by default; the +rest of the commands and mappings become available after starting debug mode. + + *:GoDebugStart* +:GoDebugStart [pkg] [program-args] + + Start the debug mode for [pkg]; this does several things: + + * Start `dlv debug` for [pkg], or `dlv test` if the current buffer's + filename ends with `_test.go`. + * Setup the debug windows according to |'g:go_debug_windows'|. + * Make the `:GoDebug*` commands and `(go-debug-*)` mappings available. + + The current directory is used if [pkg] is empty. Any other arguments will + be passed to the program. + + Use `-test.flag` to pass flags to `go test` when debugging a test; for + example `-test.v` or `-test.run TestFoo` + + Use |:GoDebugStop| to stop `dlv` and exit debugging mode. + + *:GoDebugRestart* +:GoDebugRestart + + Stop the program (if running) and restart `dlv` to recompile the package. + The current window layout and breakpoints will be left intact. + + *:GoDebugStop* + *(go-debug-stop)* +:GoDebugStop + + Stop `dlv` and remove all debug-specific commands, mappings, and windows. + + *:GoDebugBreakpoint* + *(go-debug-breakpoint)* +:GoDebugBreakpoint [linenr] + + Toggle breakpoint for the [linenr]. [linenr] defaults to the current line + if it is omitted. A line with a breakpoint will have the + {godebugbreakpoint} |:sign| placed on it. The line the program is + currently halted on will have the {godebugcurline} sign. + + *hl-GoDebugCurrent* *hl-GoDebugBreakpoint* + A line with a breakpoint will be highlighted with the {GoDebugBreakpoint} + group; the line the program is currently halted on will be highlighted + with {GoDebugCurrent}. + + Mapped to by default. + + *:GoDebugContinue* + *(go-debug-continue)* +:GoDebugContinue + + Continue execution until breakpoint or program termination. It will start + the program if it hasn't been started yet. + + Mapped to by default. + + *:GoDebugNext* + *(go-debug-next)* +:GoDebugNext + + Advance execution by one line, also called "step over" by some other + debuggers. + It will behave as |:GoDebugContinue| if the program isn't started. + + Mapped to by default. + + *:GoDebugStep* + *(go-debug-step)* +:GoDebugStep + + Advance execution by one step, stopping at the next line of code that will + be executed (regardless of location). + It will behave as |:GoDebugContinue| if the program isn't started. + + Mapped to by default. + + *:GoDebugStepOut* + *(go-debug-stepout)* + +:GoDebugStepOut + + Run all the code in the current function and halt when the function + returns ("step out of the current function"). + It will behave as |:GoDebugContinue| if the program isn't started. + + *:GoDebugSet* +:GoDebugSet {var} {value} + + Set the variable {var} to {value}. Example: +> + :GoDebugSet truth 42 +< + This only works for `float`, `int` and variants, `uint` and variants, + `bool`, and pointers (this is a `delve` limitation, not a vim-go + limitation). + + *:GoDebugPrint* + *(go-debug-print)* +:GoDebugPrint {expr} + + Print the result of a Go expression. +> + :GoDebugPrint truth == 42 + truth == 42 true +< + Mapped to by default, which will evaluate the under the + cursor. + + *go-debug-settings* +DEBUGGER SETTINGS~ + + *'g:go_debug_windows'* + +Controls the window layout for debugging mode. This is a |dict| with three +possible keys: "stack", "out", and "vars"; the windows will created in that +order with the commands in the value. +A window will not be created if a key is missing or empty. + +Defaults: +> + let g:go_debug_windows = { + \ 'stack': 'leftabove 20vnew', + \ 'out': 'botright 10new', + \ 'vars': 'leftabove 30vnew', + \ } +< +Show only variables on the right-hand side: > + + let g:go_debug_windows = { + \ 'vars': 'rightbelow 60vnew', + \ } +< + *'g:go_debug_address'* + +Server address `dlv` will listen on; must be in `hostname:port` format. +Defaults to `127.0.0.1:8181`: +> + let g:go_debug_address = '127.0.0.1:8181' +< + ============================================================================== FAQ TROUBLESHOOTING *go-troubleshooting* diff --git a/ftplugin/go/commands.vim b/ftplugin/go/commands.vim index 9b983ef56f..fbe22385ad 100644 --- a/ftplugin/go/commands.vim +++ b/ftplugin/go/commands.vim @@ -98,4 +98,10 @@ command! -nargs=0 GoKeyify call go#keyify#Keyify() " -- fillstruct command! -nargs=0 GoFillStruct call go#fillstruct#FillStruct() +" -- debug +if !exists(':GoDebugStart') + command! -nargs=* -complete=customlist,go#package#Complete GoDebugStart call go#debug#Start() + command! -nargs=? GoDebugBreakpoint call go#debug#Breakpoint() +endif + " vim: sw=2 ts=2 et diff --git a/plugin/go.vim b/plugin/go.vim index bb88608f4f..513ce36623 100644 --- a/plugin/go.vim +++ b/plugin/go.vim @@ -31,6 +31,7 @@ endif " needed by the user with GoInstallBinaries let s:packages = { \ 'asmfmt': ['github.com/klauspost/asmfmt/cmd/asmfmt'], + \ 'dlv': ['github.com/derekparker/delve/cmd/dlv'], \ 'errcheck': ['github.com/kisielk/errcheck'], \ 'fillstruct': ['github.com/davidrjenni/reftools/cmd/fillstruct'], \ 'gocode': ['github.com/nsf/gocode', {'windows': '-ldflags -H=windowsgui'}], diff --git a/syntax/godebugoutput.vim b/syntax/godebugoutput.vim new file mode 100644 index 0000000000..b8e6f5ffe2 --- /dev/null +++ b/syntax/godebugoutput.vim @@ -0,0 +1,13 @@ +if exists("b:current_syntax") + finish +endif + +syn match godebugOutputErr '^ERR:.*' +syn match godebugOutputOut '^OUT:.*' + +let b:current_syntax = "godebugoutput" + +hi def link godebugOutputErr Comment +hi def link godebugOutputOut Normal + +" vim: sw=2 ts=2 et diff --git a/syntax/godebugstacktrace.vim b/syntax/godebugstacktrace.vim new file mode 100644 index 0000000000..b0c5372581 --- /dev/null +++ b/syntax/godebugstacktrace.vim @@ -0,0 +1,11 @@ +if exists("b:current_syntax") + finish +endif + +syn match godebugStacktrace '^\S\+' + +let b:current_syntax = "godebugoutput" + +hi def link godebugStacktrace SpecialKey + +" vim: sw=2 ts=2 et diff --git a/syntax/godebugvariables.vim b/syntax/godebugvariables.vim new file mode 100644 index 0000000000..791705ba0e --- /dev/null +++ b/syntax/godebugvariables.vim @@ -0,0 +1,23 @@ +if exists("b:current_syntax") + finish +endif + +syn match godebugTitle '^#.*' +syn match godebugVariables '^\s*\S\+\ze:' + +syn keyword goType chan map bool string error +syn keyword goSignedInts int int8 int16 int32 int64 rune +syn keyword goUnsignedInts byte uint uint8 uint16 uint32 uint64 uintptr +syn keyword goFloats float32 float64 +syn keyword goComplexes complex64 complex128 + +syn keyword goBoolean true false + +let b:current_syntax = "godebugvariables" + +hi def link godebugTitle Underlined +hi def link godebugVariables Statement +hi def link goType Type +hi def link goBoolean Boolean + +" vim: sw=2 ts=2 et