diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..e9062d5 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,12 @@ +--- +coverage: + status: + project: + default: + target: auto + threshold: 1 + base: auto +comment: false +ignore: + - "!autoload/go/*.vim$" + - "autoload/go/*_test.vim$" diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d9b46..ac5d8d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,106 @@ ## unplanned +## 1.17 - (March 27, 2018) + +FEATURES: + +* **Debugger support!** Add integrated support for the + [`delve`](https://github.com/derekparker/delve) debugger. Use + `:GoInstallBinaries` to install `dlv`, and see `:help go-debug` to get + started. + [[GH-1390]](https://github.com/fatih/vim-go/pull/1390) + +IMPROVEMENTS: + +* Add descriptions to neosnippet abbrevations. + [[GH-1639]](https://github.com/fatih/vim-go/pull/1639) +* Show messages in the location list instead of the quickfix list when + `gometalinter` is run automatically when saving a buffer. Whether the + location list or quickfix list is used can be customized in the usual ways. + [[GH-1652]](https://github.com/fatih/vim-go/pull/1652) +* Redraw the screen before executing blocking calls to gocode. + [[GH-1671]](https://github.com/fatih/vim-go/pull/1671) +* Add `fe` -> `fmt.Errorf()` snippet for NeoSnippet and UltiSnippets. + [[GH-1677]](https://github.com/fatih/vim-go/pull/1677) +* Use the async api when calling guru from neovim. + [[GH-1678]](https://github.com/fatih/vim-go/pull/1678) +* Use the async api when calling gocode to get type info. + [[GH-1697]](https://github.com/fatih/vim-go/pull/1697) +* Cache import path lookups to improve responsiveness. + [[GH-1713]](https://github.com/fatih/vim-go/pull/1713) + +BUG FIXES: + +* Create quickfix list correctly when tests timeout. + [[GH-1633]](https://github.com/fatih/vim-go/pull/1633) +* Apply `g:go_test_timeout` when running `:GoTestFunc`. + [[GH-1631]](https://github.com/fatih/vim-go/pull/1631) +* The user's configured `g:go_doc_url` variable wasn't working correctly in the + case when the "gogetdoc" command isn't installed. + [[GH-1629]](https://github.com/fatih/vim-go/pull/1629) +* Highlight format specifiers with an index (e.g. `%[2]d`). + [[GH-1634]](https://github.com/fatih/vim-go/pull/1634) +* Respect `g:go_test_show_name` change for `:GoTest` when it changes during a + Vim session. + [[GH-1641]](https://github.com/fatih/vim-go/pull/1641) +* Show `g:go_test_show_name` value for `:GoTest` failures if it's available. + [[GH-1641]](https://github.com/fatih/vim-go/pull/1641) +* Make sure linter errors for the file being saved are shown in vim74 and nvim. + [[GH-1640]](https://github.com/fatih/vim-go/pull/1640) +* Make sure only linter errors for the file being saved are shown in vim8. + Previously, all linter errors for all files in the current file's directory + were being shown. + [[GH-1640]](https://github.com/fatih/vim-go/pull/1640) +* Make sure gometalinter is run on the given directories when arguments are + given to :GoMetaLinter. + [[GH-1640]](https://github.com/fatih/vim-go/pull/1640) +* Do not run disabled linters with `gometalinter`. + [[GH-1648]](https://github.com/fatih/vim-go/pull/1648) +* Do not prompt user to press enter after when `gometalinter` is called in + autosave mode. + [[GH-1654]](https://github.com/fatih/vim-go/pull/1654) +* Fix potential race conditions when using vim8 jobs. + [[GH-1656]](https://github.com/fatih/vim-go/pull/1656) +* Treat `'autowriteall'` the same as `'autowrite'` when determining whether to + write a buffer before calling some commands. + [[GH-1653]](https://github.com/fatih/vim-go/pull/1653) +* Show the file location of test errors when the message is empty or begins + with a newline. + [[GH-1664]](https://github.com/fatih/vim-go/pull/1664) +* Fix minisnip on Windows. + [[GH-1698]](https://github.com/fatih/vim-go/pull/1698) +* Keep alternate filename when loading an autocreate template. + [[GH-1675]](https://github.com/fatih/vim-go/pull/1675) +* Parse the column number in errors correctly in vim8 and neovim. + [[GH-1716]](https://github.com/fatih/vim-go/pull/1716) +* Fix race conditions in the terminal handling for neovim. + [[GH-1721]](https://github.com/fatih/vim-go/pull/1721) +* Put the user back in the original window regardless of the value of + `splitright` after starting a neovim terminal window. + [[GH-1725]](https://github.com/fatih/vim-go/pull/1725) + +BACKWARDS INCOMPATIBILITIES: + +* Highlighting function and method declarations/calls is fixed. To fix it we + had to remove the meaning of the previous settings. The following setting is + removed: + + * `go_highlight_methods` + + in favor of the following settings and changes: + + * `go_highlight_functions`: This highlights now all function and method + declarations (whereas previously it would also highlight function and + method calls, not anymore) + * `go_highlight_function_calls`: This higlights now all all function and + method calls. + [[GH-1557]](https://github.com/fatih/vim-go/pull/1557) +* Rename g`g:go_metalinter_excludes` to `g:go_metalinter_disabled`. + [[GH-1648]](https://github.com/fatih/vim-go/pull/1648) +* `:GoBuild` doesn't append the `-i` flag anymore due the recent Go 1.10 + changes that introduced a build cache. + [[GH-1701]](https://github.com/fatih/vim-go/pull/1701) + ## 1.16 - (December 29, 2017) FEATURES: diff --git a/README.md b/README.md index 8566224..d211376 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/assets/screenshot.png b/assets/screenshot.png deleted file mode 100644 index 8b2923d..0000000 Binary files a/assets/screenshot.png and /dev/null differ diff --git a/autoload/go/cmd.vim b/autoload/go/cmd.vim index a867607..569572d 100644 --- a/autoload/go/cmd.vim +++ b/autoload/go/cmd.vim @@ -1,5 +1,5 @@ function! go#cmd#autowrite() abort - if &autowrite == 1 + if &autowrite == 1 || &autowriteall == 1 silent! wall endif endfunction @@ -17,7 +17,7 @@ function! go#cmd#Build(bang, ...) abort let args = \ ["build"] + \ map(copy(a:000), "expand(v:val)") + - \ ["-i", ".", "errors"] + \ [".", "errors"] " Vim async. if go#util#has_job() @@ -269,7 +269,7 @@ function s:cmd_job(args) abort " autowrite is not enabled for jobs call go#cmd#autowrite() - function! s:error_info_cb(job, exit_status, data) closure abort + function! s:complete(job, exit_status, data) closure abort let status = { \ 'desc': 'last status', \ 'type': a:args.cmd[1], @@ -288,12 +288,13 @@ function s:cmd_job(args) abort call go#statusline#Update(status_dir, status) endfunction - let a:args.error_info_cb = funcref('s:error_info_cb') + let a:args.complete = funcref('s:complete') let callbacks = go#job#Spawn(a:args) let start_options = { \ 'callback': callbacks.callback, \ 'exit_cb': callbacks.exit_cb, + \ 'close_cb': callbacks.close_cb, \ } " pre start diff --git a/autoload/go/cmd_test.vim b/autoload/go/cmd_test.vim new file mode 100644 index 0000000..ef39110 --- /dev/null +++ b/autoload/go/cmd_test.vim @@ -0,0 +1,30 @@ +func! Test_GoBuildErrors() + try + let l:filename = 'cmd/bad.go' + let l:tmp = gotest#load_fixture(l:filename) + exe 'cd ' . l:tmp . '/src/cmd' + + " set the compiler type so that the errorformat option will be set + " correctly. + compiler go + + let expected = [{'lnum': 4, 'bufnr': bufnr('%'), 'col': 2, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'undefined: notafunc'}] + " clear the quickfix lists + call setqflist([], 'r') + + call go#cmd#Build(1) + + let actual = getqflist() + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getqflist() + endwhile + + call gotest#assert_quickfix(actual, l:expected) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +" vim: sw=2 ts=2 et diff --git a/autoload/go/complete.vim b/autoload/go/complete.vim index 32ac360..8013f00 100644 --- a/autoload/go/complete.vim +++ b/autoload/go/complete.vim @@ -1,58 +1,51 @@ let s:sock_type = (has('win32') || has('win64')) ? 'tcp' : 'unix' -function! s:gocodeCurrentBuffer() abort - let file = tempname() - call writefile(go#util#GetLines(), file) - return file -endfunction - -function! s:gocodeCommand(cmd, preargs, args) abort - for i in range(0, len(a:args) - 1) - let a:args[i] = go#util#Shellescape(a:args[i]) - endfor - for i in range(0, len(a:preargs) - 1) - let a:preargs[i] = go#util#Shellescape(a:preargs[i]) - endfor - +function! s:gocodeCommand(cmd, args) abort let bin_path = go#path#CheckBinPath("gocode") if empty(bin_path) - return + return [] endif + let socket_type = get(g:, 'go_gocode_socket_type', s:sock_type) + + let cmd = [bin_path] + let cmd = extend(cmd, ['-sock', socket_type]) + let cmd = extend(cmd, ['-f', 'vim']) + let cmd = extend(cmd, [a:cmd]) + let cmd = extend(cmd, a:args) + + return cmd +endfunction + +function! s:sync_gocode(cmd, args, input) abort " We might hit cache problems, as gocode doesn't handle different GOPATHs " well. See: https://github.com/nsf/gocode/issues/239 let old_goroot = $GOROOT let $GOROOT = go#util#env("goroot") try - let socket_type = get(g:, 'go_gocode_socket_type', s:sock_type) - let cmd = printf('%s -sock %s %s %s %s', - \ go#util#Shellescape(bin_path), - \ socket_type, - \ join(a:preargs), - \ go#util#Shellescape(a:cmd), - \ join(a:args) - \ ) - - let result = go#util#System(cmd) + let cmd = s:gocodeCommand(a:cmd, a:args) + " gocode can sometimes be slow, so redraw now to avoid waiting for gocode + " to return before redrawing automatically. + redraw + + let [l:result, l:err] = go#util#Exec(cmd, a:input) finally let $GOROOT = old_goroot endtry - if go#util#ShellError() != 0 - return "[\"0\", []]" - else - if &encoding != 'utf-8' - let result = iconv(result, 'utf-8', &encoding) - endif - return result + if l:err != 0 + return "[0, []]" + endif + + if &encoding != 'utf-8' + let l:result = iconv(l:result, 'utf-8', &encoding) endif -endfunction -function! s:gocodeCurrentBufferOpt(filename) abort - return '-in=' . a:filename + return l:result endfunction +" TODO(bc): reset when gocode isn't running let s:optionsEnabled = 0 function! s:gocodeEnableOptions() abort if s:optionsEnabled @@ -78,62 +71,161 @@ endfunction function! s:gocodeAutocomplete() abort call s:gocodeEnableOptions() - let filename = s:gocodeCurrentBuffer() - let result = s:gocodeCommand('autocomplete', - \ [s:gocodeCurrentBufferOpt(filename), '-f=vim'], - \ [expand('%:p'), go#util#OffsetCursor()]) - call delete(filename) - return result + " use the offset as is, because the cursor position is the position for + " which autocomplete candidates are needed. + return s:sync_gocode('autocomplete', + \ [expand('%:p'), go#util#OffsetCursor()], + \ go#util#GetLines()) endfunction +" go#complete#GoInfo returns the description of the identifier under the +" cursor. function! go#complete#GetInfo() abort + return s:sync_info(0) +endfunction + +function! go#complete#Info(auto) abort + if go#util#has_job() + return s:async_info(a:auto) + else + return s:sync_info(a:auto) + endif +endfunction + +function! s:async_info(auto) + if exists("s:async_info_job") + call job_stop(s:async_info_job) + unlet s:async_info_job + endif + + let state = { + \ 'exited': 0, + \ 'exit_status': 0, + \ 'closed': 0, + \ 'messages': [], + \ 'auto': a:auto + \ } + + function! s:callback(chan, msg) dict + let l:msg = a:msg + if &encoding != 'utf-8' + let l:msg = iconv(l:msg, 'utf-8', &encoding) + endif + call add(self.messages, l:msg) + endfunction + + function! s:exit_cb(job, exitval) dict + let self.exit_status = a:exitval + let self.exited = 1 + + if self.closed + call self.complete() + endif + endfunction + + function! s:close_cb(ch) dict + let self.closed = 1 + if self.exited + call self.complete() + endif + endfunction + + function state.complete() dict + if self.exit_status != 0 + return + endif + + let result = s:info_filter(self.auto, join(self.messages, "\n")) + call s:info_complete(self.auto, result) + endfunction + + " add 1 to the offset, so that the position at the cursor will be included + " in gocode's search let offset = go#util#OffsetCursor()+1 - let filename = s:gocodeCurrentBuffer() - let result = s:gocodeCommand('autocomplete', - \ [s:gocodeCurrentBufferOpt(filename), '-f=godit'], + + " We might hit cache problems, as gocode doesn't handle different GOPATHs + " well. See: https://github.com/nsf/gocode/issues/239 + let env = { + \ "GOROOT": go#util#env("goroot") + \ } + + let cmd = s:gocodeCommand('autocomplete', \ [expand('%:p'), offset]) - call delete(filename) - " first line is: Charcount,,NumberOfCandidates, i.e: 8,,1 - " following lines are candiates, i.e: func foo(name string),,foo( - let out = split(result, '\n') + " TODO(bc): Don't write the buffer to a file; pass the buffer directrly to + " gocode's stdin. It shouldn't be necessary to use {in_io: 'file', in_name: + " s:gocodeFile()}, but unfortunately {in_io: 'buffer', in_buf: bufnr('%')} + " should work. + let options = { + \ 'env': env, + \ 'in_io': 'file', + \ 'in_name': s:gocodeFile(), + \ 'callback': funcref("s:callback", [], state), + \ 'exit_cb': funcref("s:exit_cb", [], state), + \ 'close_cb': funcref("s:close_cb", [], state) + \ } - " no candidates are found - if len(out) == 1 + let s:async_info_job = job_start(cmd, options) +endfunction + +function! s:gocodeFile() + let file = tempname() + call writefile(go#util#GetLines(), file) + return file +endfunction + +function! s:sync_info(auto) + " auto is true if we were called by g:go_auto_type_info's autocmd + + " add 1 to the offset, so that the position at the cursor will be included + " in gocode's search + let offset = go#util#OffsetCursor()+1 + + let result = s:sync_gocode('autocomplete', + \ [expand('%:p'), offset], + \ go#util#GetLines()) + + let result = s:info_filter(a:auto, result) + call s:info_complete(a:auto, result) +endfunction + +function! s:info_filter(auto, result) abort + if empty(a:result) return "" endif - " only one candidate is found - if len(out) == 2 - return split(out[1], ',,')[0] + let l:result = eval(a:result) + if len(l:result) != 2 + return "" endif - " to many candidates are available, pick one that maches the word under the - " cursor - let infos = [] - for info in out[1:] - call add(infos, split(info, ',,')[0]) - endfor + let l:candidates = l:result[1] + if len(l:candidates) == 1 + " When gocode panics in vim mode, it returns + " [0, [{'word': 'PANIC', 'abbr': 'PANIC PANIC PANIC', 'info': 'PANIC PANIC PANIC'}]] + if a:auto && l:candidates[0].info ==# "PANIC PANIC PANIC" + return "" + endif + + return l:candidates[0].info + endif + let filtered = [] let wordMatch = '\<' . expand("") . '\>' " escape single quotes in wordMatch before passing it to filter let wordMatch = substitute(wordMatch, "'", "''", "g") - let filtered = filter(infos, "v:val =~ '".wordMatch."'") + let filtered = filter(l:candidates, "v:val.info =~ '".wordMatch."'") - if len(filtered) == 1 - return filtered[0] + if len(l:filtered) != 1 + return "" endif - return "" + return l:filtered[0].info endfunction -function! go#complete#Info(auto) abort - " auto is true if we were called by g:go_auto_type_info's autocmd - let result = go#complete#GetInfo() - if !empty(result) - " if auto, and the result is a PANIC by gocode, hide it - if a:auto && result ==# 'PANIC PANIC PANIC' | return | endif - echo "vim-go: " | echohl Function | echon result | echohl None +function! s:info_complete(auto, result) abort + if !empty(a:result) + echo "vim-go: " | echohl Function | echon a:result | echohl None endif endfunction @@ -142,20 +234,22 @@ function! s:trim_bracket(val) abort return a:val endfunction +let s:completions = "" function! go#complete#Complete(findstart, base) abort "findstart = 1 when we need to get the text length if a:findstart == 1 - execute "silent let g:gocomplete_completions = " . s:gocodeAutocomplete() - return col('.') - g:gocomplete_completions[0] - 1 + execute "silent let s:completions = " . s:gocodeAutocomplete() + return col('.') - s:completions[0] - 1 "findstart = 0 when we need to return the list of completions else let s = getline(".")[col('.') - 1] if s =~ '[(){}\{\}]' - return map(copy(g:gocomplete_completions[1]), 's:trim_bracket(v:val)') + return map(copy(s:completions[1]), 's:trim_bracket(v:val)') endif - return g:gocomplete_completions[1] + + return s:completions[1] endif -endf +endfunction function! go#complete#ToggleAutoTypeInfo() abort if get(g:, "go_auto_type_info", 0) @@ -168,5 +262,4 @@ function! go#complete#ToggleAutoTypeInfo() abort call go#util#EchoProgress("auto type info enabled") endfunction - " vim: sw=2 ts=2 et diff --git a/autoload/go/coverage.vim b/autoload/go/coverage.vim index 882f819..7f362ca 100644 --- a/autoload/go/coverage.vim +++ b/autoload/go/coverage.vim @@ -45,13 +45,13 @@ function! go#coverage#Buffer(bang, ...) abort let l:tmpname = tempname() if get(g:, 'go_echo_command_info', 1) - echon "vim-go: " | echohl Identifier | echon "testing ..." | echohl None + call go#util#EchoProgress("testing...") endif if go#util#has_job() call s:coverage_job({ \ 'cmd': ['go', 'test', '-coverprofile', l:tmpname] + a:000, - \ 'custom_cb': function('s:coverage_callback', [l:tmpname]), + \ 'complete': function('s:coverage_callback', [l:tmpname]), \ 'bang': a:bang, \ 'for': 'GoTest', \ }) @@ -107,7 +107,7 @@ function! go#coverage#Browser(bang, ...) abort if go#util#has_job() call s:coverage_job({ \ 'cmd': ['go', 'test', '-coverprofile', l:tmpname], - \ 'custom_cb': function('s:coverage_browser_callback', [l:tmpname]), + \ 'complete': function('s:coverage_browser_callback', [l:tmpname]), \ 'bang': a:bang, \ 'for': 'GoTest', \ }) @@ -278,7 +278,8 @@ function s:coverage_job(args) call go#cmd#autowrite() let status_dir = expand('%:p:h') - function! s:error_info_cb(job, exit_status, data) closure + let Complete = a:args.complete + function! s:complete(job, exit_status, data) closure let status = { \ 'desc': 'last status', \ 'type': "coverage", @@ -290,14 +291,16 @@ function s:coverage_job(args) endif call go#statusline#Update(status_dir, status) + return Complete(a:job, a:exit_status, a:data) endfunction - let a:args.error_info_cb = funcref('s:error_info_cb') + let a:args.complete = funcref('s:complete') let callbacks = go#job#Spawn(a:args) let start_options = { \ 'callback': callbacks.callback, \ 'exit_cb': callbacks.exit_cb, + \ 'close_cb': callbacks.close_cb, \ } " pre start diff --git a/autoload/go/debug.vim b/autoload/go/debug.vim new file mode 100644 index 0000000..64e4f8d --- /dev/null +++ b/autoload/go/debug.vim @@ -0,0 +1,904 @@ +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, + \ 'running': 0, + \ 'breakpoint': {}, + \ 'currentThread': {}, + \ 'localVars': {}, + \ 'functionArgs': {}, + \ 'message': [], + \ 'is_test': 0, + \} + + 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'] = {} + let s:state['message'] = [] + 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 + " Remove signs. + 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 + + " 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(0, ) + command! -nargs=* -complete=customlist,go#package#Complete GoDebugTest call go#debug#Start(1, ) + 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' + + 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 + silent! delcommand GoDebugTest + 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:err_cb(ch, msg) abort + call go#util#EchoError(a:msg) + let s:state['message'] += [a:msg] +endfunction + +function! s:out_cb(ch, msg) abort + call go#util#EchoProgress(a:msg) + let s:state['message'] += [a:msg] + + " TODO: why do this in this callback? + 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(is_test, ...) 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 + + " cd in to test directory; this is also what running "go test" does. + if a:is_test + lcd %:p:h + endif + + let s:state.is_test = a:is_test + + 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, + \ (a: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:out_cb'), + \ 'err_cb': function('s:err_cb'), + \ '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 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' + endif + + " Add a breakpoint to the main.Main if the user didn't define any. + if len(s:state['breakpoint']) is 0 + if go#debug#Breakpoint() isnot 0 + let s:state.running = 0 + return + endif + endif + + try + " TODO: document why this is needed. + if l:name is# 'next' && get(s:, 'stack_name', '') is# 'next' + call s:call_jsonrpc('RPCServer.CancelNext') + endif + let s:stack_name = l:name + call s:call_jsonrpc('RPCServer.Command', function('s:stack_cb'), {'name': l: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, + \ 'running': 0, + \ '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. Returns 0 on success and 1 on failure. +function! go#debug#Breakpoint(...) abort + let l:filename = fnamemodify(expand('%'), ':p:gs!\\!/!') + + " Get line number from argument. + if len(a:000) > 0 + let linenr = str2nr(a:1) + if linenr is 0 + call go#util#EchoError('not a number: ' . a:1) + return 0 + 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 == l: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': l: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': l:filename, 'line': linenr} + exe 'sign place '. id .' line=' . linenr . ' name=godebugbreakpoint file=' . l:filename + endif + endif + catch + call go#util#EchoError(v:exception) + return 1 + endtry + + return 0 +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/def.vim b/autoload/go/def.vim index f2383c9..7b5c86d 100644 --- a/autoload/go/def.vim +++ b/autoload/go/def.vim @@ -53,7 +53,7 @@ function! go#def#Jump(mode) abort if go#util#has_job() let l:spawn_args = { \ 'cmd': cmd, - \ 'custom_cb': function('s:jump_to_declaration_cb', [a:mode, bin_name]), + \ 'complete': function('s:jump_to_declaration_cb', [a:mode, bin_name]), \ } if &modified @@ -292,16 +292,12 @@ function! go#def#Stack(...) abort endfunction function s:def_job(args) abort - function! s:error_info_cb(job, exit_status, data) closure - " do not print anything during async definition search&jump - endfunction - - let a:args.error_info_cb = funcref('s:error_info_cb') let callbacks = go#job#Spawn(a:args) let start_options = { \ 'callback': callbacks.callback, \ 'exit_cb': callbacks.exit_cb, + \ 'close_cb': callbacks.close_cb, \ } if &modified diff --git a/autoload/go/doc.vim b/autoload/go/doc.vim index 7c2f53d..e06a790 100644 --- a/autoload/go/doc.vim +++ b/autoload/go/doc.vim @@ -29,18 +29,7 @@ function! go#doc#OpenBrowser(...) abort let name = out["name"] let decl = out["decl"] - let godoc_url = get(g:, 'go_doc_url', 'https://godoc.org') - if godoc_url isnot 'https://godoc.org' - " strip last '/' character if available - let last_char = strlen(godoc_url) - 1 - if godoc_url[last_char] == '/' - let godoc_url = strpart(godoc_url, 0, last_char) - endif - - " custom godoc installations expects it - let godoc_url .= "/pkg" - endif - + let godoc_url = s:custom_godoc_url() let godoc_url .= "/" . import if decl !~ "^package" let godoc_url .= "#" . name @@ -61,7 +50,7 @@ function! go#doc#OpenBrowser(...) abort let exported_name = pkgs[1] " example url: https://godoc.org/github.com/fatih/set#Set - let godoc_url = "https://godoc.org/" . pkg . "#" . exported_name + let godoc_url = s:custom_godoc_url() . "/" . pkg . "#" . exported_name call go#tool#OpenBrowser(godoc_url) endfunction @@ -217,13 +206,18 @@ function! s:godocWord(args) abort return [pkg, exported_name] endfunction -function! s:godocNotFound(content) abort - if len(a:content) == 0 - return 1 +function! s:custom_godoc_url() abort + let godoc_url = get(g:, 'go_doc_url', 'https://godoc.org') + if godoc_url isnot 'https://godoc.org' + " strip last '/' character if available + let last_char = strlen(godoc_url) - 1 + if godoc_url[last_char] == '/' + let godoc_url = strpart(godoc_url, 0, last_char) + endif + " custom godoc installations expect /pkg before package names + let godoc_url .= "/pkg" endif - - return a:content =~# '^.*: no such file or directory\n$' + return godoc_url endfunction - " vim: sw=2 ts=2 et diff --git a/autoload/go/fmt.vim b/autoload/go/fmt.vim index 42d0419..08fc3b3 100644 --- a/autoload/go/fmt.vim +++ b/autoload/go/fmt.vim @@ -148,7 +148,6 @@ function! go#fmt#update_file(source, target) if has_key(l:list_title, "title") && l:list_title['title'] == "Format" call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) endif endfunction diff --git a/autoload/go/guru.vim b/autoload/go/guru.vim index c252090..3e735ec 100644 --- a/autoload/go/guru.vim +++ b/autoload/go/guru.vim @@ -78,7 +78,7 @@ function! s:guru_cmd(args) range abort let scopes = go#util#StripTrailingSlash(scopes) " create shell-safe entries of the list - if !go#util#has_job() | let scopes = go#util#Shelllist(scopes) | endif + if !has("nvim") && !go#util#has_job() | let scopes = go#util#Shelllist(scopes) | endif " guru expect a comma-separated list of patterns, construct it let l:scope = join(scopes, ",") @@ -129,7 +129,7 @@ function! s:sync_guru(args) abort endif if has_key(a:args, 'custom_parse') - call a:args.custom_parse(go#util#ShellError(), out) + call a:args.custom_parse(go#util#ShellError(), out, a:args.mode) else call s:parse_guru_output(go#util#ShellError(), out, a:args.mode) endif @@ -137,6 +137,33 @@ function! s:sync_guru(args) abort return out endfunc +" use vim or neovim job api as appropriate +function! s:job_start(cmd, start_options) abort + if go#util#has_job() + return job_start(a:cmd, a:start_options) + endif + + let opts = {'stdout_buffered': v:true, 'stderr_buffered': v:true} + function opts.on_stdout(job_id, data, event) closure + call a:start_options.callback(a:job_id, join(a:data, "\n")) + endfunction + function opts.on_stderr(job_id, data, event) closure + call a:start_options.callback(a:job_id, join(a:data, "\n")) + endfunction + function opts.on_exit(job_id, exit_code, event) closure + call a:start_options.exit_cb(a:job_id, a:exit_code) + call a:start_options.close_cb(a:job_id) + endfunction + + " use a shell for input redirection if needed + let cmd = a:cmd + if has_key(a:start_options, 'in_io') && a:start_options.in_io ==# 'file' && !empty(a:start_options.in_name) + let cmd = ['/bin/sh', '-c', join(a:cmd, ' ') . ' <' . a:start_options.in_name] + endif + + return jobstart(cmd, opts) +endfunction + " async_guru runs guru in async mode with the given arguments function! s:async_guru(args) abort let result = s:guru_cmd(a:args) @@ -145,8 +172,6 @@ function! s:async_guru(args) abort return endif - let status_dir = expand('%:p:h') - let statusline_type = printf("%s", a:args.mode) if !has_key(a:args, 'disable_progress') if a:args.needs_scope @@ -155,44 +180,64 @@ function! s:async_guru(args) abort endif endif - let messages = [] - function! s:callback(chan, msg) closure - call add(messages, a:msg) + let state = { + \ 'status_dir': expand('%:p:h'), + \ 'statusline_type': printf("%s", a:args.mode), + \ 'mode': a:args.mode, + \ 'status': {}, + \ 'exitval': 0, + \ 'closed': 0, + \ 'exited': 0, + \ 'messages': [], + \ 'parse' : get(a:args, 'custom_parse', funcref("s:parse_guru_output")) + \ } + + function! s:callback(chan, msg) dict + call add(self.messages, a:msg) endfunction - let status = {} - let exitval = 0 + function! s:exit_cb(job, exitval) dict + let self.exited = 1 - function! s:exit_cb(job, exitval) closure let status = { \ 'desc': 'last status', - \ 'type': statusline_type, + \ 'type': self.statusline_type, \ 'state': "finished", \ } if a:exitval - let exitval = a:exitval + let self.exitval = a:exitval let status.state = "failed" endif - call go#statusline#Update(status_dir, status) + call go#statusline#Update(self.status_dir, status) + + if self.closed + call self.complete() + endif endfunction - function! s:close_cb(ch) closure - let out = join(messages, "\n") + function! s:close_cb(ch) dict + let self.closed = 1 - if has_key(a:args, 'custom_parse') - call a:args.custom_parse(exitval, out) - else - call s:parse_guru_output(exitval, out, a:args.mode) + if self.exited + call self.complete() endif endfunction + function state.complete() dict + let out = join(self.messages, "\n") + + call self.parse(self.exitval, out, self.mode) + endfunction + + " explicitly bind the callbacks to state so that self within them always + " refers to state. See :help Partial for more information. let start_options = { - \ 'callback': funcref("s:callback"), - \ 'exit_cb': funcref("s:exit_cb"), - \ 'close_cb': funcref("s:close_cb"), - \ } + \ 'callback': function('s:callback', [], state), + \ 'exit_cb': function('s:exit_cb', [], state), + \ 'close_cb': function('s:close_cb', [], state) + \ } if has_key(result, 'stdin_content') let l:tmpname = tempname() @@ -201,18 +246,18 @@ function! s:async_guru(args) abort let l:start_options.in_name = l:tmpname endif - call go#statusline#Update(status_dir, { + call go#statusline#Update(state.status_dir, { \ 'desc': "current status", - \ 'type': statusline_type, + \ 'type': state.statusline_type, \ 'state': "analysing", \}) - return job_start(result.cmd, start_options) + return s:job_start(result.cmd, start_options) endfunc " run_guru runs the given guru argument function! s:run_guru(args) abort - if go#util#has_job() + if has('nvim') || go#util#has_job() let res = s:async_guru(a:args) else let res = s:sync_guru(a:args) @@ -273,7 +318,7 @@ function! go#guru#DescribeInfo() abort return endif - function! s:info(exit_val, output) + function! s:info(exit_val, output, mode) if a:exit_val != 0 return endif @@ -448,10 +493,6 @@ function! go#guru#Referrers(selected) abort call s:run_guru(args) endfunction -function! go#guru#SameIdsTimer() abort - call timer_start(200, function('go#guru#SameIds'), {'repeat': -1}) -endfunction - function! go#guru#SameIds() abort " we use matchaddpos() which was introduce with 7.4.330, be sure we have " it: http://ftp.vim.org/vim/patches/7.4/7.4.330 @@ -479,7 +520,7 @@ function! go#guru#SameIds() abort call s:run_guru(args) endfunction -function! s:same_ids_highlight(exit_val, output) abort +function! s:same_ids_highlight(exit_val, output, mode) abort call go#guru#ClearSameIds() " run after calling guru to reduce flicker. if a:output[0] !=# '{' diff --git a/autoload/go/job.vim b/autoload/go/job.vim index 965f20d..62214b4 100644 --- a/autoload/go/job.vim +++ b/autoload/go/job.vim @@ -1,9 +1,38 @@ -" Spawn returns callbacks to be used with job_start. It's abstracted to be -" used with various go command, such as build, test, install, etc.. This avoid -" us to write the same callback over and over for some commands. It's fully -" customizable so each command can change it to it's own logic. +" Spawn returns callbacks to be used with job_start. It is abstracted to be +" used with various go commands, such as build, test, install, etc.. This +" allows us to avoid writing the same callback over and over for some +" commands. It's fully customizable so each command can change it to it's own +" logic. +" +" args is a dictionary with the these keys: +" 'cmd': +" The value to pass to job_start(). +" 'bang': +" Set to 0 to jump to the first error in the error list. +" Defaults to 0. +" 'for': +" The g:go_list_type_command key to use to get the error list type to use. +" Defaults to '_job' +" 'complete': +" A function to call after the job exits and the channel is closed. The +" function will be passed three arguments: the job, its exit code, and the +" list of messages received from the channel. The default value will +" process the messages and manage the error list after the job exits and +" the channel is closed. + +" The return value is a dictionary with these keys: +" 'callback': +" A function suitable to be passed as a job callback handler. See +" job-callback. +" 'exit_cb': +" A function suitable to be passed as a job exit_cb handler. See +" job-exit_cb. +" 'close_cb': +" A function suitable to be passed as a job close_cb handler. See +" job-close_cb. function go#job#Spawn(args) - let cbs = { + let cbs = {} + let state = { \ 'winnr': winnr(), \ 'dir': getcwd(), \ 'jobdir': fnameescape(expand("%:p:h")), @@ -11,34 +40,38 @@ function go#job#Spawn(args) \ 'args': a:args.cmd, \ 'bang': 0, \ 'for': "_job", - \ } + \ 'exited': 0, + \ 'exit_status': 0, + \ 'closed': 0, + \ 'errorformat': &errorformat + \ } if has_key(a:args, 'bang') - let cbs.bang = a:args.bang + let state.bang = a:args.bang endif if has_key(a:args, 'for') - let cbs.for = a:args.for + let state.for = a:args.for endif - " add final callback to be called if async job is finished - " The signature should be in form: func(job, exit_status, messages) - if has_key(a:args, 'custom_cb') - let cbs.custom_cb = a:args.custom_cb - endif + " do nothing in state.complete by default. + function state.complete(job, exit_status, data) + endfunction - if has_key(a:args, 'error_info_cb') - let cbs.error_info_cb = a:args.error_info_cb + if has_key(a:args, 'complete') + let state.complete = a:args.complete endif - function cbs.callback(chan, msg) dict + function! s:callback(chan, msg) dict call add(self.messages, a:msg) endfunction + " explicitly bind callback to state so that within it, self will + " always refer to state. See :help Partial for more information. + let cbs.callback = function('s:callback', [], state) - function cbs.exit_cb(job, exitval) dict - if has_key(self, 'error_info_cb') - call self.error_info_cb(a:job, a:exitval, self.messages) - endif + function! s:exit_cb(job, exitval) dict + let self.exit_status = a:exitval + let self.exited = 1 if get(g:, 'go_echo_command_info', 1) if a:exitval == 0 @@ -48,55 +81,69 @@ function go#job#Spawn(args) endif endif - if has_key(self, 'custom_cb') - call self.custom_cb(a:job, a:exitval, self.messages) + if self.closed + call self.complete(a:job, self.exit_status, self.messages) + call self.show_errors(a:job, self.exit_status, self.messages) + endif + endfunction + " explicitly bind exit_cb to state so that within it, self will always refer + " to state. See :help Partial for more information. + let cbs.exit_cb = function('s:exit_cb', [], state) + + function! s:close_cb(ch) dict + let self.closed = 1 + + if self.exited + let job = ch_getjob(a:ch) + call self.complete(job, self.exit_status, self.messages) + call self.show_errors(job, self.exit_status, self.messages) endif + endfunction + " explicitly bind close_cb to state so that within it, self will + " always refer to state. See :help Partial for more information. + let cbs.close_cb = function('s:close_cb', [], state) + function state.show_errors(job, exit_status, data) let l:listtype = go#list#Type(self.for) - if a:exitval == 0 + if a:exit_status == 0 call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) return endif - call self.show_errors(l:listtype) - endfunction + let l:listtype = go#list#Type(self.for) + if len(a:data) == 0 + call go#list#Clean(l:listtype) + return + endif + + let out = join(self.messages, "\n") - function cbs.show_errors(listtype) dict let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' try + " parse the errors relative to self.jobdir execute cd self.jobdir - let errors = go#tool#ParseErrors(self.messages) - let errors = go#tool#FilterValids(errors) + call go#list#ParseFormat(l:listtype, self.errorformat, out, self.for) + let errors = go#list#Get(l:listtype) finally execute cd . fnameescape(self.dir) endtry - if !len(errors) + + if empty(errors) " failed to parse errors, output the original content call go#util#EchoError(self.messages + [self.dir]) return endif if self.winnr == winnr() - call go#list#Populate(a:listtype, errors, join(self.args)) - call go#list#Window(a:listtype, len(errors)) - if !empty(errors) && !self.bang - call go#list#JumpToFirst(a:listtype) + call go#list#Window(l:listtype, len(errors)) + if !self.bang + call go#list#JumpToFirst(l:listtype) endif endif endfunction - " override callback handler if user provided it - if has_key(a:args, 'callback') - let cbs.callback = a:args.callback - endif - - " override exit callback handler if user provided it - if has_key(a:args, 'exit_cb') - let cbs.exit_cb = a:args.exit_cb - endif - return cbs endfunction + " vim: sw=2 ts=2 et diff --git a/autoload/go/jobcontrol.vim b/autoload/go/jobcontrol.vim index 57eab32..2550581 100644 --- a/autoload/go/jobcontrol.vim +++ b/autoload/go/jobcontrol.vim @@ -62,6 +62,7 @@ function! s:spawn(bang, desc, for, args) abort \ 'status_dir' : status_dir, \ 'started_at' : started_at, \ 'for' : a:for, + \ 'errorformat': &errorformat, \ } " execute go build in the files directory @@ -125,7 +126,6 @@ function! s:on_exit(job_id, exit_status, event) dict abort let l:listtype = go#list#Type(self.for) if a:exit_status == 0 call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) let self.state = "SUCCESS" @@ -143,8 +143,9 @@ function! s:on_exit(job_id, exit_status, event) dict abort call go#util#EchoError("[" . self.status_type . "] FAILED") endif - let errors = go#tool#ParseErrors(std_combined) - let errors = go#tool#FilterValids(errors) + " parse the errors relative to self.jobdir + call go#list#ParseFormat(l:listtype, self.errorformat, std_combined, self.for) + let errors = go#list#Get(l:listtype) execute cd . fnameescape(dir) @@ -156,7 +157,6 @@ function! s:on_exit(job_id, exit_status, event) dict abort " if we are still in the same windows show the list if self.winnr == winnr() - call go#list#Populate(l:listtype, errors, self.desc) call go#list#Window(l:listtype, len(errors)) if !empty(errors) && !self.bang call go#list#JumpToFirst(l:listtype) diff --git a/autoload/go/lint.vim b/autoload/go/lint.vim index 5bf4edb..4204694 100644 --- a/autoload/go/lint.vim +++ b/autoload/go/lint.vim @@ -10,8 +10,8 @@ if !exists("g:go_metalinter_enabled") let g:go_metalinter_enabled = ['vet', 'golint', 'errcheck'] endif -if !exists("g:go_metalinter_excludes") - let g:go_metalinter_excludes = [] +if !exists("g:go_metalinter_disabled") + let g:go_metalinter_disabled = [] endif if !exists("g:go_golint_bin") @@ -24,9 +24,9 @@ endif function! go#lint#Gometa(autosave, ...) abort if a:0 == 0 - let goargs = shellescape(expand('%:p:h')) + let goargs = [expand('%:p:h')] else - let goargs = go#util#Shelljoin(a:000) + let goargs = a:000 endif let bin_path = go#path#CheckBinPath("gometalinter") @@ -44,8 +44,8 @@ function! go#lint#Gometa(autosave, ...) abort let cmd += ["--enable=".linter] endfor - for exclude in g:go_metalinter_excludes - let cmd += ["--exclude=".exclude] + for linter in g:go_metalinter_disabled + let cmd += ["--disable=".linter] endfor " gometalinter has a --tests flag to tell its linters whether to run @@ -54,14 +54,20 @@ function! go#lint#Gometa(autosave, ...) abort " test files. One example of a linter that will not run against tests if " we do not specify this flag is errcheck. let cmd += ["--tests"] - - " path - let cmd += [expand('%:p:h')] else " the user wants something else, let us use it. let cmd += split(g:go_metalinter_command, " ") endif + if a:autosave + " redraw so that any messages that were displayed while writing the file + " will be cleared + redraw + + " Include only messages for the active buffer for autosave. + let cmd += [printf('--include=^%s:.*$', fnamemodify(expand('%:p'), ":."))] + endif + " gometalinter has a default deadline of 5 seconds. " " For async mode (s:lint_job), we want to override the default deadline only @@ -78,29 +84,27 @@ function! go#lint#Gometa(autosave, ...) abort let cmd += ["--deadline=" . deadline] endif - call s:lint_job({'cmd': cmd}) + let cmd += goargs + + call s:lint_job({'cmd': cmd}, a:autosave) return endif " We're calling gometalinter synchronously. - let cmd += ["--deadline=" . get(g:, 'go_metalinter_deadline', "5s")] - if a:autosave - " include only messages for the active buffer - let cmd += ["--include='^" . expand('%:p') . ".*$'"] - endif + let cmd += goargs + let [l:out, l:err] = go#util#Exec(cmd) - let meta_command = join(cmd, " ") - - let out = go#util#System(meta_command) + if a:autosave + let l:listtype = go#list#Type("GoMetaLinterAutoSave") + else + let l:listtype = go#list#Type("GoMetaLinter") + endif - let l:listtype = go#list#Type("GoMetaLinter") - if go#util#ShellError() == 0 - redraw | echo + if l:err == 0 call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) echon "vim-go: " | echohl Function | echon "[metalinter] PASS" | echohl None else " GoMetaLinter can output one of the two, so we look for both: @@ -142,7 +146,7 @@ function! go#lint#Golint(...) abort endif let l:listtype = go#list#Type("GoLint") - call go#list#Parse(l:listtype, out) + call go#list#Parse(l:listtype, out, "GoLint") let errors = go#list#Get(l:listtype) call go#list#Window(l:listtype, len(errors)) call go#list#JumpToFirst(l:listtype) @@ -161,8 +165,9 @@ function! go#lint#Vet(bang, ...) abort let l:listtype = go#list#Type("GoVet") if go#util#ShellError() != 0 - let errors = go#tool#ParseErrors(split(out, '\n')) - call go#list#Populate(l:listtype, errors, 'Vet') + let errorformat="%-Gexit status %\\d%\\+," . &errorformat + call go#list#ParseFormat(l:listtype, l:errorformat, out, "GoVet") + let errors = go#list#Get(l:listtype) call go#list#Window(l:listtype, len(errors)) if !empty(errors) && !a:bang call go#list#JumpToFirst(l:listtype) @@ -170,7 +175,6 @@ function! go#lint#Vet(bang, ...) abort echon "vim-go: " | echohl ErrorMsg | echon "[vet] FAIL" | echohl None else call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) redraw | echon "vim-go: " | echohl Function | echon "[vet] PASS" | echohl None endif endfunction @@ -223,7 +227,6 @@ function! go#lint#Errcheck(...) abort endif else call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) echon "vim-go: " | echohl Function | echon "[errcheck] PASS" | echohl None endif @@ -240,11 +243,19 @@ function! go#lint#ToggleMetaLinterAutoSave() abort call go#util#EchoProgress("auto metalinter enabled") endfunction -function s:lint_job(args) - let status_dir = expand('%:p:h') - let started_at = reltime() - - call go#statusline#Update(status_dir, { +function! s:lint_job(args, autosave) + let state = { + \ 'status_dir': expand('%:p:h'), + \ 'started_at': reltime(), + \ 'messages': [], + \ 'exited': 0, + \ 'closed': 0, + \ 'exit_status': 0, + \ 'winnr': winnr(), + \ 'autosave': a:autosave + \ } + + call go#statusline#Update(state.status_dir, { \ 'desc': "current status", \ 'type': "gometalinter", \ 'state': "analysing", @@ -253,32 +264,20 @@ function s:lint_job(args) " autowrite is not enabled for jobs call go#cmd#autowrite() - let l:listtype = go#list#Type("GoMetaLinter") - let l:errformat = '%f:%l:%c:%t%*[^:]:\ %m,%f:%l::%t%*[^:]:\ %m' - - function! s:callback(chan, msg) closure - let old_errorformat = &errorformat - let &errorformat = l:errformat - try - if l:listtype == "locationlist" - lad a:msg - elseif l:listtype == "quickfix" - caddexpr a:msg - endif - finally - let &errorformat = old_errorformat - endtry - - " TODO(jinleileiking): give a configure to jump or not - let l:winnr = winnr() - - let errors = go#list#Get(l:listtype) - call go#list#Window(l:listtype, len(errors)) + if a:autosave + let state.listtype = go#list#Type("GoMetaLinterAutoSave") + else + let state.listtype = go#list#Type("GoMetaLinter") + endif - exe l:winnr . "wincmd w" + function! s:callback(chan, msg) dict closure + call add(self.messages, a:msg) endfunction - function! s:exit_cb(job, exitval) closure + function! s:exit_cb(job, exitval) dict + let self.exited = 1 + let self.exit_status = a:exitval + let status = { \ 'desc': 'last status', \ 'type': "gometaliner", @@ -289,22 +288,50 @@ function s:lint_job(args) let status.state = "failed" endif - let elapsed_time = reltimestr(reltime(started_at)) + let elapsed_time = reltimestr(reltime(self.started_at)) " strip whitespace let elapsed_time = substitute(elapsed_time, '^\s*\(.\{-}\)\s*$', '\1', '') let status.state .= printf(" (%ss)", elapsed_time) - call go#statusline#Update(status_dir, status) + call go#statusline#Update(self.status_dir, status) - let errors = go#list#Get(l:listtype) - if empty(errors) - call go#list#Window(l:listtype, len(errors)) - elseif has("patch-7.4.2200") - if l:listtype == 'quickfix' - call setqflist([], 'a', {'title': 'GoMetaLinter'}) - else - call setloclist(0, [], 'a', {'title': 'GoMetaLinter'}) - endif + if self.closed + call self.show_errors() + endif + endfunction + + function! s:close_cb(ch) dict + let self.closed = 1 + + if self.exited + call self.show_errors() + endif + endfunction + + + function state.show_errors() + let l:winnr = winnr() + + " make sure the current window is the window from which gometalinter was + " run when the listtype is locationlist so that the location list for the + " correct window will be populated. + if self.listtype == 'locationlist' + exe self.winnr . "wincmd w" + endif + + let l:errorformat = '%f:%l:%c:%t%*[^:]:\ %m,%f:%l::%t%*[^:]:\ %m' + call go#list#ParseFormat(self.listtype, l:errorformat, self.messages, 'GoMetaLinter') + + let errors = go#list#Get(self.listtype) + call go#list#Window(self.listtype, len(errors)) + + " move to the window that was active before processing the errors, because + " the user may have moved around within the window or even moved to a + " different window since saving. Moving back to current window as of the + " start of this function avoids the perception that the quickfix window + " steals focus when linting takes a while. + if self.autosave + exe l:winnr . "wincmd w" endif if get(g:, 'go_echo_command_info', 1) @@ -312,15 +339,16 @@ function s:lint_job(args) endif endfunction + " explicitly bind the callbacks to state so that self within them always + " refers to state. See :help Partial for more information. let start_options = { - \ 'callback': funcref("s:callback"), - \ 'exit_cb': funcref("s:exit_cb"), + \ 'callback': funcref("s:callback", [], state), + \ 'exit_cb': funcref("s:exit_cb", [], state), + \ 'close_cb': funcref("s:close_cb", [], state), \ } call job_start(a:args.cmd, start_options) - call go#list#Clean(l:listtype) - if get(g:, 'go_echo_command_info', 1) call go#util#EchoProgress("linting started ...") endif diff --git a/autoload/go/lint_test.vim b/autoload/go/lint_test.vim new file mode 100644 index 0000000..141d57c --- /dev/null +++ b/autoload/go/lint_test.vim @@ -0,0 +1,131 @@ +func! Test_Gometa() abort + let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint' + silent exe 'e ' . $GOPATH . '/src/lint/lint.go' + + let expected = [ + \ {'lnum': 5, 'bufnr': bufnr('%')+1, 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'pattern': '', 'text': 'exported function MissingFooDoc should have comment or be unexported (golint)'} + \ ] + + " clear the quickfix lists + call setqflist([], 'r') + + " call go#lint#ToggleMetaLinterAutoSave from lint.vim so that the file will + " be autoloaded and the default for g:go_metalinter_enabled will be set so + " we can capture it to restore it after the test is run. + call go#lint#ToggleMetaLinterAutoSave() + " And restore it back to its previous value + call go#lint#ToggleMetaLinterAutoSave() + + let orig_go_metalinter_enabled = g:go_metalinter_enabled + let g:go_metalinter_enabled = ['golint'] + + call go#lint#Gometa(0, $GOPATH . '/src/foo') + + let actual = getqflist() + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getqflist() + endwhile + + call gotest#assert_quickfix(actual, expected) + let g:go_metalinter_enabled = orig_go_metalinter_enabled +endfunc + +func! Test_GometaWithDisabled() abort + let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint' + silent exe 'e ' . $GOPATH . '/src/lint/lint.go' + + let expected = [ + \ {'lnum': 5, 'bufnr': bufnr('%')+1, 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'pattern': '', 'text': 'exported function MissingFooDoc should have comment or be unexported (golint)'} + \ ] + + " clear the quickfix lists + call setqflist([], 'r') + + " call go#lint#ToggleMetaLinterAutoSave from lint.vim so that the file will + " be autoloaded and the default for g:go_metalinter_disabled will be set so + " we can capture it to restore it after the test is run. + call go#lint#ToggleMetaLinterAutoSave() + " And restore it back to its previous value + call go#lint#ToggleMetaLinterAutoSave() + + let orig_go_metalinter_disabled = g:go_metalinter_disabled + let g:go_metalinter_disabled = ['vet'] + + call go#lint#Gometa(0, $GOPATH . '/src/foo') + + let actual = getqflist() + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getqflist() + endwhile + + call gotest#assert_quickfix(actual, expected) + let g:go_metalinter_disabled = orig_go_metalinter_disabled +endfunc + +func! Test_GometaAutoSave() abort + let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint' + silent exe 'e ' . $GOPATH . '/src/lint/lint.go' + + let expected = [ + \ {'lnum': 5, 'bufnr': bufnr('%'), 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'pattern': '', 'text': 'exported function MissingDoc should have comment or be unexported (golint)'} + \ ] + + let winnr = winnr() + + " clear the location lists + call setloclist(l:winnr, [], 'r') + + " call go#lint#ToggleMetaLinterAutoSave from lint.vim so that the file will + " be autoloaded and the default for g:go_metalinter_autosave_enabled will be + " set so we can capture it to restore it after the test is run. + call go#lint#ToggleMetaLinterAutoSave() + " And restore it back to its previous value + call go#lint#ToggleMetaLinterAutoSave() + + let orig_go_metalinter_autosave_enabled = g:go_metalinter_autosave_enabled + let g:go_metalinter_autosave_enabled = ['golint'] + + call go#lint#Gometa(1) + + let actual = getloclist(l:winnr) + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getloclist(l:winnr) + endwhile + + call gotest#assert_quickfix(actual, expected) + let g:go_metalinter_autosave_enabled = orig_go_metalinter_autosave_enabled +endfunc + +func! Test_Vet() + let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint' + silent exe 'e ' . $GOPATH . '/src/vet/vet.go' + compiler go + + let expected = [ + \ {'lnum': 7, 'bufnr': bufnr('%'), 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'arg str for printf verb %d of wrong type: string'} + \ ] + + let winnr = winnr() + + " clear the location lists + call setqflist([], 'r') + + call go#lint#Vet(1) + + let actual = getqflist() + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getqflist() + endwhile + + call gotest#assert_quickfix(actual, expected) +endfunc + +" vim: sw=2 ts=2 et diff --git a/autoload/go/list.vim b/autoload/go/list.vim index 472e0d3..23b0cbd 100644 --- a/autoload/go/list.vim +++ b/autoload/go/list.vim @@ -18,14 +18,7 @@ function! go#list#Window(listtype, ...) abort " location list increases/decreases, cwindow will not resize when a new " updated height is passed. lopen in the other hand resizes the screen. if !a:0 || a:1 == 0 - let autoclose_window = get(g:, 'go_list_autoclose', 1) - if autoclose_window - if a:listtype == "locationlist" - lclose - else - cclose - endif - endif + call go#list#Close(a:listtype) return endif @@ -79,13 +72,7 @@ function! go#list#ParseFormat(listtype, errformat, items, title) abort " parse and populate the location list let &errorformat = a:errformat try - if a:listtype == "locationlist" - lgetexpr a:items - if has("patch-7.4.2200") | call setloclist(0, [], 'a', {'title': a:title}) | endif - else - cgetexpr a:items - if has("patch-7.4.2200") | call setqflist([], 'a', {'title': a:title}) | endif - endif + call go#list#Parse(a:listtype, a:items, a:title) finally "restore back let &errorformat = old_errorformat @@ -94,11 +81,13 @@ endfunction " Parse parses the given items based on the global errorformat and " populates the list. -function! go#list#Parse(listtype, items) abort +function! go#list#Parse(listtype, items, title) abort if a:listtype == "locationlist" lgetexpr a:items + if has("patch-7.4.2200") | call setloclist(0, [], 'a', {'title': a:title}) | endif else cgetexpr a:items + if has("patch-7.4.2200") | call setqflist([], 'a', {'title': a:title}) | endif endif endfunction @@ -111,13 +100,29 @@ function! go#list#JumpToFirst(listtype) abort endif endfunction -" Clean cleans the location list +" Clean cleans and closes the location list function! go#list#Clean(listtype) abort if a:listtype == "locationlist" lex [] else cex [] endif + + call go#list#Close(a:listtype) +endfunction + +" Close closes the location list +function! go#list#Close(listtype) abort + let autoclose_window = get(g:, 'go_list_autoclose', 1) + if !autoclose_window + return + endif + + if a:listtype == "locationlist" + lclose + else + cclose + endif endfunction function! s:listtype(listtype) abort @@ -137,21 +142,22 @@ endfunction " single file or buffer. Keys that begin with an underscore are not supported " in g:go_list_type_commands. let s:default_list_type_commands = { - \ "GoBuild": "quickfix", - \ "GoErrCheck": "quickfix", - \ "GoFmt": "locationlist", - \ "GoGenerate": "quickfix", - \ "GoInstall": "quickfix", - \ "GoLint": "quickfix", - \ "GoMetaLinter": "quickfix", - \ "GoModifyTags": "locationlist", - \ "GoRename": "quickfix", - \ "GoRun": "quickfix", - \ "GoTest": "quickfix", - \ "GoVet": "quickfix", - \ "_guru": "locationlist", - \ "_term": "locationlist", - \ "_job": "locationlist", + \ "GoBuild": "quickfix", + \ "GoErrCheck": "quickfix", + \ "GoFmt": "locationlist", + \ "GoGenerate": "quickfix", + \ "GoInstall": "quickfix", + \ "GoLint": "quickfix", + \ "GoMetaLinter": "quickfix", + \ "GoMetaLinterAutoSave": "locationlist", + \ "GoModifyTags": "locationlist", + \ "GoRename": "quickfix", + \ "GoRun": "quickfix", + \ "GoTest": "quickfix", + \ "GoVet": "quickfix", + \ "_guru": "locationlist", + \ "_term": "locationlist", + \ "_job": "locationlist", \ } function! go#list#Type(for) abort diff --git a/autoload/go/package.vim b/autoload/go/package.vim index 940423f..7cf7c5c 100644 --- a/autoload/go/package.vim +++ b/autoload/go/package.vim @@ -54,8 +54,14 @@ function! go#package#Paths() abort return dirs endfunction +let s:import_paths = {} " ImportPath returns the import path in the current directory it was executed function! go#package#ImportPath() abort + let dir = expand("%:p:h") + if has_key(s:import_paths, dir) + return s:import_paths[dir] + endif + let out = go#tool#ExecuteInDir("go list") if go#util#ShellError() != 0 return -1 @@ -69,6 +75,8 @@ function! go#package#ImportPath() abort return -1 endif + let s:import_paths[dir] = import_path + return import_path endfunction diff --git a/autoload/go/path.vim b/autoload/go/path.vim index 8d91f26..49d647c 100644 --- a/autoload/go/path.vim +++ b/autoload/go/path.vim @@ -45,9 +45,9 @@ function! go#path#Default() abort return $GOPATH endfunction -" HasPath checks whether the given path exists in GOPATH environment variable +" s:HasPath checks whether the given path exists in GOPATH environment variable " or not -function! go#path#HasPath(path) abort +function! s:HasPath(path) abort let go_paths = split(go#path#Default(), go#util#PathListSep()) let last_char = strlen(a:path) - 1 @@ -94,11 +94,11 @@ function! go#path#Detect() abort " gb vendor plugin " (https://github.com/constabulary/gb/tree/master/cmd/gb-vendor) let gb_vendor_root = src_path . "vendor" . go#util#PathSep() - if isdirectory(gb_vendor_root) && !go#path#HasPath(gb_vendor_root) + if isdirectory(gb_vendor_root) && !s:HasPath(gb_vendor_root) let gopath = gb_vendor_root . go#util#PathListSep() . gopath endif - if !go#path#HasPath(src_path) + if !s:HasPath(src_path) let gopath = src_path . go#util#PathListSep() . gopath endif endif @@ -108,7 +108,7 @@ function! go#path#Detect() abort if !empty(godeps_root) let godeps_path = join([fnamemodify(godeps_root, ':p:h:h'), "Godeps", "_workspace" ], go#util#PathSep()) - if !go#path#HasPath(godeps_path) + if !s:HasPath(godeps_path) let gopath = godeps_path . go#util#PathListSep() . gopath endif endif @@ -164,7 +164,7 @@ function! go#path#CheckBinPath(binpath) abort let $PATH = old_path if go#util#IsUsingCygwinShell() == 1 - return go#path#CygwinPath(binpath) + return s:CygwinPath(binpath) endif return binpath @@ -183,13 +183,13 @@ function! go#path#CheckBinPath(binpath) abort let $PATH = old_path if go#util#IsUsingCygwinShell() == 1 - return go#path#CygwinPath(a:binpath) + return s:CygwinPath(a:binpath) endif return go_bin_path . go#util#PathSep() . basename endfunction -function! go#path#CygwinPath(path) +function! s:CygwinPath(path) return substitute(a:path, '\\', '/', "g") endfunction diff --git a/autoload/go/rename.vim b/autoload/go/rename.vim index 3af9dcf..095462f 100644 --- a/autoload/go/rename.vim +++ b/autoload/go/rename.vim @@ -72,14 +72,23 @@ function! go#rename#Rename(bang, ...) abort endfunction function s:rename_job(args) - let messages = [] - function! s:callback(chan, msg) closure - call add(messages, a:msg) + let state = { + \ 'exited': 0, + \ 'closed': 0, + \ 'exitval': 0, + \ 'messages': [], + \ 'status_dir': expand('%:p:h'), + \ 'bang': a:args.bang + \ } + + function! s:callback(chan, msg) dict + call add(self.messages, a:msg) endfunction - let status_dir = expand('%:p:h') + function! s:exit_cb(job, exitval) dict + let self.exited = 1 + let self.exitval = a:exitval - function! s:exit_cb(job, exitval) closure let status = { \ 'desc': 'last status', \ 'type': "gorename", @@ -90,17 +99,30 @@ function s:rename_job(args) let status.state = "failed" endif - call go#statusline#Update(status_dir, status) + call go#statusline#Update(self.status_dir, status) - call s:parse_errors(a:exitval, a:args.bang, messages) + if self.closed + call s:parse_errors(self.exitval, self.bang, self.messages) + endif + endfunction + + function! s:close_cb(ch) dict + let self.closed = 1 + + if self.exited + call s:parse_errors(self.exitval, self.bang, self.messages) + endif endfunction + " explicitly bind the callbacks to state so that self within them always + " refers to state. See :help Partial for more information. let start_options = { - \ 'callback': funcref("s:callback"), - \ 'exit_cb': funcref("s:exit_cb"), + \ 'callback': funcref("s:callback", [], state), + \ 'exit_cb': funcref("s:exit_cb", [], state), + \ 'close_cb': funcref("s:close_cb", [], state), \ } - call go#statusline#Update(status_dir, { + call go#statusline#Update(state.status_dir, { \ 'desc': "current status", \ 'type': "gorename", \ 'state': "started", @@ -138,7 +160,6 @@ function s:parse_errors(exit_val, bang, out) " strip out newline on the end that gorename puts. If we don't remove, it " will trigger the 'Hit ENTER to continue' prompt call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) call go#util#EchoSuccess(a:out[0]) " refresh the buffer so we can see the new content diff --git a/autoload/go/template.vim b/autoload/go/template.vim index 11a91ae..edb26a6 100644 --- a/autoload/go/template.vim +++ b/autoload/go/template.vim @@ -24,7 +24,7 @@ function! go#template#create() abort let l:template_file = get(g:, 'go_template_file', "hello_world.go") endif let l:template_path = go#util#Join(l:root_dir, "templates", l:template_file) - silent exe '0r ' . fnameescape(l:template_path) + silent exe 'keepalt 0r ' . fnameescape(l:template_path) elseif l:package_name == -1 && l:go_template_use_pkg == 1 " cwd is now the dir of the package let l:path = fnamemodify(getcwd(), ':t') diff --git a/autoload/go/term.vim b/autoload/go/term.vim index 23ee18c..1e085a3 100644 --- a/autoload/go/term.vim +++ b/autoload/go/term.vim @@ -2,9 +2,6 @@ if has('nvim') && !exists("g:go_term_mode") let g:go_term_mode = 'vsplit' endif -" s:jobs is a global reference to all jobs started with new() -let s:jobs = {} - " new creates a new terminal with the given command. Mode is set based on the " global variable g:go_term_mode, which is by default set to :vsplit function! go#term#new(bang, cmd) abort @@ -18,8 +15,14 @@ function! go#term#newmode(bang, cmd, mode) abort let mode = g:go_term_mode endif + let state = { + \ 'cmd': a:cmd, + \ 'bang' : a:bang, + \ 'winid': win_getid(winnr()), + \ 'stdout': [] + \ } + " execute go build in the files directory - let l:winnr = winnr() let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' let dir = getcwd() @@ -33,30 +36,27 @@ function! go#term#newmode(bang, cmd, mode) abort setlocal noswapfile setlocal nobuflisted + " explicitly bind callbacks to state so that within them, self will always + " refer to state. See :help Partial for more information. + " + " Don't set an on_stderr, because it will be passed the same data as + " on_stdout. See https://github.com/neovim/neovim/issues/2836 let job = { - \ 'stderr' : [], - \ 'stdout' : [], - \ 'bang' : a:bang, - \ 'on_stdout': function('s:on_stdout'), - \ 'on_stderr': function('s:on_stderr'), - \ 'on_exit' : function('s:on_exit'), - \ } + \ 'on_stdout': function('s:on_stdout', [], state), + \ 'on_exit' : function('s:on_exit', [], state), + \ } - let id = termopen(a:cmd, job) + let state.id = termopen(a:cmd, job) + let state.termwinid = win_getid(winnr()) execute cd . fnameescape(dir) - let job.id = id - let job.cmd = a:cmd - startinsert - " resize new term if needed. let height = get(g:, 'go_term_height', winheight(0)) let width = get(g:, 'go_term_width', winwidth(0)) - " we are careful how to resize. for example it's vsplit we don't change - " the height. The below command resizes the buffer - + " Adjust the window width or height depending on whether it's a vertical or + " horizontal split. if mode =~ "vertical" || mode =~ "vsplit" || mode =~ "vnew" exe 'vertical resize ' . width elseif mode =~ "split" || mode =~ "new" @@ -64,77 +64,56 @@ function! go#term#newmode(bang, cmd, mode) abort endif " we also need to resize the pty, so there you go... - call jobresize(id, width, height) + call jobresize(state.id, width, height) - let s:jobs[id] = job - stopinsert + call win_gotoid(state.winid) - if l:winnr !=# winnr() - exe l:winnr . "wincmd w" - endif - - return id + return state.id endfunction function! s:on_stdout(job_id, data, event) dict abort - if !has_key(s:jobs, a:job_id) - return - endif - let job = s:jobs[a:job_id] - - call extend(job.stdout, a:data) -endfunction - -function! s:on_stderr(job_id, data, event) dict abort - if !has_key(s:jobs, a:job_id) - return - endif - let job = s:jobs[a:job_id] - - call extend(job.stderr, a:data) + call extend(self.stdout, a:data) endfunction function! s:on_exit(job_id, exit_status, event) dict abort - if !has_key(s:jobs, a:job_id) - return - endif - let job = s:jobs[a:job_id] - let l:listtype = go#list#Type("_term") " usually there is always output so never branch into this clause - if empty(job.stdout) - call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) - unlet s:jobs[a:job_id] + if empty(self.stdout) + call s:cleanlist(self.winid, l:listtype) return endif - let errors = go#tool#ParseErrors(job.stdout) + let errors = go#tool#ParseErrors(self.stdout) let errors = go#tool#FilterValids(errors) if !empty(errors) - " close terminal we don't need it anymore + " close terminal; we don't need it anymore + call win_gotoid(self.termwinid) close - call go#list#Populate(l:listtype, errors, job.cmd) + call win_gotoid(self.winid) + + call go#list#Populate(l:listtype, errors, self.cmd) call go#list#Window(l:listtype, len(errors)) if !self.bang call go#list#JumpToFirst(l:listtype) endif - unlet s:jobs[a:job_id] + return endif - " tests are passing clean the list and close the list. But we only can - " close them from a normal view, so jump back, close the list and then - " again jump back to the terminal - wincmd p - call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) - wincmd p + call s:cleanlist(self.winid, l:listtype) +endfunction - unlet s:jobs[a:job_id] +function! s:cleanlist(winid, listtype) abort + " There are no errors. Clean and close the list. Jump to the window to which + " the location list is attached, close the list, and then jump back to the + " current window. + let winid = win_getid(winnr()) + call win_gotoid(a:winid) + call go#list#Clean(a:listtype) + call win_gotoid(l:winid) endfunction " vim: sw=2 ts=2 et diff --git a/autoload/go/term_test.vim b/autoload/go/term_test.vim new file mode 100644 index 0000000..7a630ee --- /dev/null +++ b/autoload/go/term_test.vim @@ -0,0 +1,50 @@ +func! Test_GoTermNewMode() + if !has('nvim') + return + endif + + try + let l:filename = 'term/term.go' + let l:tmp = gotest#load_fixture(l:filename) + exe 'cd ' . l:tmp . '/src/term' + + let expected = expand('%:p') + + let cmd = "go run ". go#util#Shelljoin(go#tool#Files()) + + set nosplitright + call go#term#newmode(0, cmd, '') + let actual = expand('%:p') + call assert_equal(actual, l:expected) + + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +func! Test_GoTermNewMode_SplitRight() + if !has('nvim') + return + endif + + try + let l:filename = 'term/term.go' + let l:tmp = gotest#load_fixture(l:filename) + exe 'cd ' . l:tmp . '/src/term' + + let expected = expand('%:p') + + let cmd = "go run ". go#util#Shelljoin(go#tool#Files()) + + set splitright + call go#term#newmode(0, cmd, '') + let actual = expand('%:p') + call assert_equal(actual, l:expected) + + finally + call delete(l:tmp, 'rf') + set nosplitright + endtry +endfunc + +" vim: sw=2 ts=2 et diff --git a/autoload/go/test-fixtures/cmd/bad.go b/autoload/go/test-fixtures/cmd/bad.go new file mode 100644 index 0000000..a1cc46e --- /dev/null +++ b/autoload/go/test-fixtures/cmd/bad.go @@ -0,0 +1,5 @@ +package main + +func main() { + notafunc() +} diff --git a/autoload/go/test-fixtures/lint/src/foo/foo.go b/autoload/go/test-fixtures/lint/src/foo/foo.go new file mode 100644 index 0000000..594af18 --- /dev/null +++ b/autoload/go/test-fixtures/lint/src/foo/foo.go @@ -0,0 +1,7 @@ +package foo + +import "fmt" + +func MissingFooDoc() { + fmt.Println("missing doc") +} diff --git a/autoload/go/test-fixtures/lint/src/lint/lint.go b/autoload/go/test-fixtures/lint/src/lint/lint.go new file mode 100644 index 0000000..52446f5 --- /dev/null +++ b/autoload/go/test-fixtures/lint/src/lint/lint.go @@ -0,0 +1,7 @@ +package lint + +import "fmt" + +func MissingDoc() { + fmt.Println("missing doc") +} diff --git a/autoload/go/test-fixtures/lint/src/lint/quux.go b/autoload/go/test-fixtures/lint/src/lint/quux.go new file mode 100644 index 0000000..85c411d --- /dev/null +++ b/autoload/go/test-fixtures/lint/src/lint/quux.go @@ -0,0 +1,7 @@ +package lint + +import "fmt" + +func AlsoMissingDoc() { + fmt.Println("missing doc") +} diff --git a/autoload/go/test-fixtures/lint/src/vet/vet.go b/autoload/go/test-fixtures/lint/src/vet/vet.go new file mode 100644 index 0000000..d3a8e04 --- /dev/null +++ b/autoload/go/test-fixtures/lint/src/vet/vet.go @@ -0,0 +1,8 @@ +package main + +import "fmt" + +func main() { + str := "hello world!" + fmt.Printf("%d\n", str) +} diff --git a/autoload/go/test-fixtures/term/term.go b/autoload/go/test-fixtures/term/term.go new file mode 100644 index 0000000..73d83e6 --- /dev/null +++ b/autoload/go/test-fixtures/term/term.go @@ -0,0 +1,5 @@ +package main + +func main() { + println("hello, world") +} diff --git a/autoload/go/test-fixtures/test/src/play/play_test.go b/autoload/go/test-fixtures/test/src/play/play_test.go index ed71ef3..0270e89 100644 --- a/autoload/go/test-fixtures/test/src/play/play_test.go +++ b/autoload/go/test-fixtures/test/src/play/play_test.go @@ -18,6 +18,7 @@ func TestTopSubHelper(t *testing.T) { func TestMultiline(t *testing.T) { t.Error("this is an error\nand a second line, too") + t.Error("\nthis is another error") } func TestSub(t *testing.T) { diff --git a/autoload/go/test-fixtures/test/src/showname/showname_test.go b/autoload/go/test-fixtures/test/src/showname/showname_test.go new file mode 100644 index 0000000..b1290ce --- /dev/null +++ b/autoload/go/test-fixtures/test/src/showname/showname_test.go @@ -0,0 +1,11 @@ +package main + +import "testing" + +func TestHelloWorld(t *testing.T) { + t.Error("so long") + + t.Run("sub", func(t *testing.T) { + t.Error("thanks for all the fish") + }) +} diff --git a/autoload/go/test-fixtures/test/src/timeout/timeout_test.go b/autoload/go/test-fixtures/test/src/timeout/timeout_test.go new file mode 100644 index 0000000..502c39e --- /dev/null +++ b/autoload/go/test-fixtures/test/src/timeout/timeout_test.go @@ -0,0 +1,47 @@ +// Run a few parallel tests, all in parallel, using multiple techniques for +// causing the test to take a while so that the stacktraces resulting from a +// test timeout will contain several goroutines to avoid giving a false sense +// of confidence or creating error formats that don't account for the more +// complex scenarios that can occur with timeouts. + +package main + +import ( + "testing" + "time" +) + +func TestSleep(t *testing.T) { + t.Parallel() + time.Sleep(15 * time.Second) + t.Log("expected panic if run with timeout < 15s") +} + +func TestRunning(t *testing.T) { + t.Parallel() + c := time.After(15 * time.Second) +Loop: + for { + select { + case <-c: + break Loop + default: + } + } + + t.Log("expected panic if run with timeout < 15s") +} + +func TestRunningAlso(t *testing.T) { + t.Parallel() + c := time.After(15 * time.Second) +Loop: + for { + select { + case <-c: + break Loop + default: + } + } + t.Log("expected panic if run with timeout < 15s") +} diff --git a/autoload/go/test.vim b/autoload/go/test.vim index 51e9a5d..c62f508 100644 --- a/autoload/go/test.vim +++ b/autoload/go/test.vim @@ -94,7 +94,6 @@ function! go#test#Test(bang, compile, ...) abort call go#util#EchoError("[test] FAIL") else call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) if a:compile call go#util#EchoSuccess("[test] SUCCESS") @@ -129,15 +128,16 @@ function! go#test#Func(bang, ...) abort if a:0 call extend(args, a:000) + else + " only add this if no custom flags are passed + let timeout = get(g:, 'go_test_timeout', '10s') + call add(args, printf("-timeout=%s", timeout)) endif call call('go#test#Test', args) endfunction function! s:test_job(args) abort - let status_dir = expand('%:p:h') - let started_at = reltime() - let status = { \ 'desc': 'current status', \ 'type': "test", @@ -148,24 +148,37 @@ function! s:test_job(args) abort let status.state = "compiling" endif - call go#statusline#Update(status_dir, status) - " autowrite is not enabled for jobs call go#cmd#autowrite() - let messages = [] - function! s:callback(chan, msg) closure - call add(messages, a:msg) + let state = { + \ 'exited': 0, + \ 'closed': 0, + \ 'exitval': 0, + \ 'messages': [], + \ 'args': a:args, + \ 'compile_test': a:args.compile_test, + \ 'status_dir': expand('%:p:h'), + \ 'started_at': reltime() + \ } + + call go#statusline#Update(state.status_dir, status) + + function! s:callback(chan, msg) dict + call add(self.messages, a:msg) endfunction - function! s:exit_cb(job, exitval) closure + function! s:exit_cb(job, exitval) dict + let self.exited = 1 + let self.exitval = a:exitval + let status = { \ 'desc': 'last status', \ 'type': "test", \ 'state': "pass", \ } - if a:args.compile_test + if self.compile_test let status.state = "success" endif @@ -175,7 +188,7 @@ function! s:test_job(args) abort if get(g:, 'go_echo_command_info', 1) if a:exitval == 0 - if a:args.compile_test + if self.compile_test call go#util#EchoSuccess("[test] SUCCESS") else call go#util#EchoSuccess("[test] PASS") @@ -185,29 +198,33 @@ function! s:test_job(args) abort endif endif - let elapsed_time = reltimestr(reltime(started_at)) + let elapsed_time = reltimestr(reltime(self.started_at)) " strip whitespace let elapsed_time = substitute(elapsed_time, '^\s*\(.\{-}\)\s*$', '\1', '') let status.state .= printf(" (%ss)", elapsed_time) - call go#statusline#Update(status_dir, status) + call go#statusline#Update(self.status_dir, status) - let l:listtype = go#list#Type("GoTest") - if a:exitval == 0 - call go#list#Clean(l:listtype) - call go#list#Window(l:listtype) - return + if self.closed + call s:show_errors(self.args, self.exitval, self.messages) endif + endfunction - " TODO(bc): When messages is JSON, the JSON should be run through a - " filter to produce lines that are more easily described by errorformat. - call s:show_errors(a:args, a:exitval, messages) + function! s:close_cb(ch) dict + let self.closed = 1 + + if self.exited + call s:show_errors(self.args, self.exitval, self.messages) + endif endfunction + " explicitly bind the callbacks to state so that self within them always + " refers to state. See :help Partial for more information. let start_options = { - \ 'callback': funcref("s:callback"), - \ 'exit_cb': funcref("s:exit_cb"), - \ } + \ 'callback': funcref("s:callback", [], state), + \ 'exit_cb': funcref("s:exit_cb", [], state), + \ 'close_cb': funcref("s:close_cb", [], state) + \ } " pre start let dir = getcwd() @@ -225,6 +242,15 @@ endfunction " a quickfix compatible list of errors. It's intended to be used only for go " test output. function! s:show_errors(args, exit_val, messages) abort + let l:listtype = go#list#Type("GoTest") + if a:exit_val == 0 + call go#list#Clean(l:listtype) + return + endif + + " TODO(bc): When messages is JSON, the JSON should be run through a + " filter to produce lines that are more easily described by errorformat. + let l:listtype = go#list#Type("GoTest") let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' @@ -253,6 +279,7 @@ endfunction let s:efm= "" +let s:go_test_show_name=0 function! s:errorformat() abort " NOTE(arslan): once we get JSON output everything will be easier :). @@ -261,14 +288,16 @@ function! s:errorformat() abort " https://github.com/golang/go/issues/2981. let goroot = go#util#goroot() - if s:efm != "" + let show_name=get(g:, 'go_test_show_name', 0) + if s:efm != "" && s:go_test_show_name == show_name return s:efm endif + let s:go_test_show_name = show_name - " each level of test indents the test output 4 spaces. - " TODO(bc): figure out how to use 0 or more groups of four spaces for the - " indentation. '%\\( %\\)%#' should work, but doesn't. - let indent = " %#" + " each level of test indents the test output 4 spaces. Capturing groups + " (e.g. \(\)) cannot be used in an errorformat, but non-capturing groups can + " (e.g. \%(\)). + let indent = '%\\%( %\\)%#' " match compiler errors let format = "%f:%l:%c: %m" @@ -285,8 +314,8 @@ function! s:errorformat() abort " " e.g.: " '--- FAIL: TestSomething (0.00s)' - if get(g:, 'go_test_show_name', 0) - let format .= ",%+G" . indent . "--- FAIL: %.%#" + if show_name + let format .= ",%G" . indent . "--- FAIL: %m (%.%#)" else let format .= ",%-G" . indent . "--- FAIL: %.%#" endif @@ -298,6 +327,12 @@ function! s:errorformat() abort " message. e.g.: " '\ttime_test.go:30: Likely problem: the time zone files have not been installed.' let format .= ",%A" . indent . "%\\t%\\+%f:%l: %m" + " also match lines that don't have a message (i.e. the message begins with a + " newline or is the empty string): + " e.g.: + " t.Errorf("\ngot %v; want %v", actual, expected) + " t.Error("") + let format .= ",%A" . indent . "%\\t%\\+%f:%l: " " Match the 2nd and later lines of multi-line output. These lines are " indented the number of spaces for the level of nesting of the test, @@ -314,6 +349,10 @@ function! s:errorformat() abort " set the format for panics. + " handle panics from test timeouts + let format .= ",%+Gpanic: test timed out after %.%\\+" + + " handle non-timeout panics " In addition to 'panic', check for 'fatal error' to support older versions " of Go that used 'fatal error'. " diff --git a/autoload/go/test_test.vim b/autoload/go/test_test.vim index e9ab162..d0abc3c 100644 --- a/autoload/go/test_test.vim +++ b/autoload/go/test_test.vim @@ -6,18 +6,19 @@ func! Test_GoTest() abort \ {'lnum': 16, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'helper badness'}, \ {'lnum': 20, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'this is an error'}, \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'and a second line, too'}, - \ {'lnum': 25, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'this is a sub-test error'}, + \ {'lnum': 21, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': ''}, + \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'this is another error'}, + \ {'lnum': 26, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'this is a sub-test error'}, \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'and a second line, too'}, \ {'lnum': 6, 'bufnr': 3, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'another package badness'}, - \ {'lnum': 42, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'panic: worst ever [recovered]'} + \ {'lnum': 43, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'panic: worst ever [recovered]'} \ ] call s:test('play/play_test.go', expected) - endfunc func! Test_GoTestConcurrentPanic() let expected = [ - \ {'lnum': 49, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'panic: concurrent fail'} + \ {'lnum': 50, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'panic: concurrent fail'} \ ] call s:test('play/play_test.go', expected, "-run", "TestConcurrentPanic") endfunc @@ -30,11 +31,13 @@ func! Test_GoTestVerbose() abort \ {'lnum': 16, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'helper badness'}, \ {'lnum': 20, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'this is an error'}, \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'and a second line, too'}, - \ {'lnum': 25, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'this is a sub-test error'}, + \ {'lnum': 21, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': ''}, + \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'this is another error'}, + \ {'lnum': 26, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'this is a sub-test error'}, \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'and a second line, too'}, - \ {'lnum': 31, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'goodness'}, + \ {'lnum': 32, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'goodness'}, \ {'lnum': 6, 'bufnr': 3, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'another package badness'}, - \ {'lnum': 42, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'panic: worst ever [recovered]'} + \ {'lnum': 43, 'bufnr': 2, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'panic: worst ever [recovered]'} \ ] call s:test('play/play_test.go', expected, "-v") endfunc @@ -43,10 +46,32 @@ func! Test_GoTestCompilerError() abort let expected = [ \ {'lnum': 6, 'bufnr': 6, 'col': 22, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'syntax error: unexpected newline, expecting comma or )'} \ ] - call s:test('compilerror/compilerror_test.go', expected) endfunc +func! Test_GoTestTimeout() abort + let expected = [ + \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'panic: test timed out after 500ms'} + \ ] + + let g:go_test_timeout="500ms" + call s:test('timeout/timeout_test.go', expected) + unlet g:go_test_timeout +endfunc + +func! Test_GoTestShowName() abort + let expected = [ + \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'TestHelloWorld'}, + \ {'lnum': 6, 'bufnr': 9, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'so long'}, + \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'TestHelloWorld/sub'}, + \ {'lnum': 9, 'bufnr': 9, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'thanks for all the fish'}, + \ ] + + let g:go_test_show_name=1 + call s:test('showname/showname_test.go', expected) + let g:go_test_show_name=0 +endfunc + func! s:test(file, expected, ...) abort if has('nvim') " nvim mostly shows test errors correctly, but the the expected errors are @@ -57,7 +82,7 @@ func! s:test(file, expected, ...) abort " the tests will run for Neovim, too. return endif - let $GOPATH = fnameescape(expand("%:p:h")) . '/test-fixtures/test' + let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/test' silent exe 'e ' . $GOPATH . '/src/' . a:file " clear the quickfix lists @@ -78,42 +103,15 @@ func! s:test(file, expected, ...) abort let actual = getqflist() endwhile - " for some reason, when run headless, the quickfix lists includes a line - " that should have been filtered out; remove it manually. The line is not - " present when run manually. - let i = 0 - while i < len(actual) - if actual[i].text =~# '^=== RUN .*' - call remove(actual, i) - endif - let i += 1 - endwhile + for item in actual + let item.text = s:normalize_durations(item.text) + endfor - call assert_equal(len(a:expected), len(actual), "number of errors") - if len(a:expected) != len(actual) - return - endif + for item in a:expected + let item.text = s:normalize_durations(item.text) + endfor - let i = 0 - while i < len(a:expected) - let expected_item = a:expected[i] - let actual_item = actual[i] - let i += 1 - - call assert_equal(expected_item.bufnr, actual_item.bufnr, "bufnr") - call assert_equal(expected_item.lnum, actual_item.lnum, "lnum") - call assert_equal(expected_item.col, actual_item.col, "col") - call assert_equal(expected_item.vcol, actual_item.vcol, "vcol") - call assert_equal(expected_item.nr, actual_item.nr, "nr") - call assert_equal(expected_item.pattern, actual_item.pattern, "pattern") - - let expected_text = s:normalize_durations(expected_item.text) - let actual_text = s:normalize_durations(actual_item.text) - - call assert_equal(expected_text, actual_text, "text") - call assert_equal(expected_item.type, actual_item.type, "type") - call assert_equal(expected_item.valid, actual_item.valid, "valid") - endwhile + call gotest#assert_quickfix(actual, a:expected) endfunc func! s:normalize_durations(str) abort diff --git a/autoload/go/textobj.vim b/autoload/go/textobj.vim index 62a7042..e8a23d3 100644 --- a/autoload/go/textobj.vim +++ b/autoload/go/textobj.vim @@ -17,6 +17,7 @@ endif " < > " t for tag +" Select a function in visual mode. function! go#textobj#Function(mode) abort let offset = go#util#OffsetCursor() @@ -98,23 +99,8 @@ function! go#textobj#Function(mode) abort call cursor(info.rbrace.line-1, 1) endfunction -function! go#textobj#FunctionJump(mode, direction) abort - " get count of the motion. This should be done before all the normal - " expressions below as those reset this value(because they have zero - " count!). We abstract -1 because the index starts from 0 in motion. - let l:cnt = v:count1 - 1 - - " set context mark so we can jump back with '' or `` - normal! m' - - " select already previously selected visual content and continue from there. - " If it's the first time starts with the visual mode. This is needed so - " after selecting something in visual mode, every consecutive motion - " continues. - if a:mode == 'v' - normal! gv - endif - +" Get the location of the previous or next function. +function! go#textobj#FunctionLocation(direction, cnt) abort let offset = go#util#OffsetCursor() let fname = shellescape(expand("%:p")) @@ -131,7 +117,7 @@ function! go#textobj#FunctionJump(mode, direction) abort endif let command = printf("%s -format vim -file %s -offset %s", bin_path, fname, offset) - let command .= ' -shift ' . l:cnt + let command .= ' -shift ' . a:cnt if a:direction == 'next' let command .= ' -mode next' @@ -154,9 +140,33 @@ function! go#textobj#FunctionJump(mode, direction) abort call delete(l:tmpname) endif - " convert our string dict representation into native Vim dictionary type - let result = eval(out) - if type(result) != 4 || !has_key(result, 'fn') + let l:result = json_decode(out) + if type(l:result) != 4 || !has_key(l:result, 'fn') + return 0 + endif + + return l:result +endfunction + +function! go#textobj#FunctionJump(mode, direction) abort + " get count of the motion. This should be done before all the normal + " expressions below as those reset this value(because they have zero + " count!). We abstract -1 because the index starts from 0 in motion. + let l:cnt = v:count1 - 1 + + " set context mark so we can jump back with '' or `` + normal! m' + + " select already previously selected visual content and continue from there. + " If it's the first time starts with the visual mode. This is needed so + " after selecting something in visual mode, every consecutive motion + " continues. + if a:mode == 'v' + normal! gv + endif + + let l:result = go#textobj#FunctionLocation(a:direction, l:cnt) + if l:result is 0 return endif diff --git a/autoload/go/util.vim b/autoload/go/util.vim index bf72dad..7b43460 100644 --- a/autoload/go/util.vim +++ b/autoload/go/util.vim @@ -330,6 +330,7 @@ function! go#util#EchoWarning(msg) call s:echo(a:msg, 'WarningMsg') endfunction function! go#util#EchoProgress(msg) + redraw call s:echo(a:msg, 'Identifier') endfunction function! go#util#EchoInfo(msg) @@ -362,7 +363,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 +384,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 +395,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/autoload/gotest.vim b/autoload/gotest.vim index 199df1e..8053b4b 100644 --- a/autoload/gotest.vim +++ b/autoload/gotest.vim @@ -102,4 +102,29 @@ fun! gotest#assert_fixture(path) abort call gotest#assert_buffer(0, l:want) endfun +func! gotest#assert_quickfix(got, want) abort + call assert_equal(len(a:want), len(a:got), "number of errors") + if len(a:want) != len(a:got) + call assert_equal(a:want, a:got) + return + endif + + let i = 0 + while i < len(a:want) + let want_item = a:want[i] + let got_item = a:got[i] + let i += 1 + + call assert_equal(want_item.bufnr, got_item.bufnr, "bufnr") + call assert_equal(want_item.lnum, got_item.lnum, "lnum") + call assert_equal(want_item.col, got_item.col, "col") + call assert_equal(want_item.vcol, got_item.vcol, "vcol") + call assert_equal(want_item.nr, got_item.nr, "nr") + call assert_equal(want_item.pattern, got_item.pattern, "pattern") + call assert_equal(want_item.text, got_item.text, "text") + call assert_equal(want_item.type, got_item.type, "type") + call assert_equal(want_item.valid, got_item.valid, "valid") + endwhile +endfunc + " vim: sw=2 ts=2 et diff --git a/doc/vim-go.txt b/doc/vim-go.txt index 9adb896..ae63eba 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|. @@ -797,6 +799,9 @@ CTRL-t Toggles |'g:go_metalinter_autosave'|. + By default, `gometalinter` messages will be shown in the |location-list| + window. The list to use can be set using |'g:go_list_type_commands'|. + *:GoTemplateAutoCreateToggle* :GoTemplateAutoCreateToggle @@ -1097,7 +1102,7 @@ cleaned for each package after `60` seconds. This can be changed with the *go#complete#GetInfo()* Returns the description of the identifer under the cursor. Can be used to plug -into the statusline. This function is also used for |'g:go_auto_type_info'|. +into the statusline. ============================================================================== SETTINGS *go-settings* @@ -1250,7 +1255,7 @@ Maximum height for the GoDoc window created with |:GoDoc|. Default is 20. > *'g:go_doc_url'* godoc server URL used when |:GoDocBrowser| is used. Change if you want to use -a private internal service. Default is 'https://godoc.org'. +a private internal service. Default is 'https://godoc.org'. > let g:go_doc_url = 'https://godoc.org' < @@ -1371,8 +1376,12 @@ function when using the `af` text object. By default it's enabled. > *'g:go_metalinter_autosave'* Use this option to auto |:GoMetaLinter| on save. Only linter messages for -the active buffer will be shown. By default it's disabled > +the active buffer will be shown. + +By default, `gometalinter` messages will be shown in the |location-list| +window. The list to use can be set using |'g:go_list_type_commands'|. + By default it's disabled > let g:go_metalinter_autosave = 0 < *'g:go_metalinter_autosave_enabled'* @@ -1384,17 +1393,17 @@ default it's using `vet` and `golint`. < *'g:go_metalinter_enabled'* -Specifies the currently enabled linters for the |:GoMetaLinter| command. By -default it's using `vet`, `golint` and `errcheck`. +Specifies the linters to enable for the |:GoMetaLinter| command. By default +it's using `vet`, `golint` and `errcheck`. > let g:go_metalinter_enabled = ['vet', 'golint', 'errcheck'] < - *'g:go_metalinter_excludes'* + *'g:go_metalinter_disabled'* -Specifies the linters to be excluded from the |:GoMetaLinter| command. By -default it's empty +Specifies the linters to disable for the |:GoMetaLinter| command. By default +it's empty > - let g:go_metalinter_excludes = [] + let g:go_metalinter_disabled = [] < *'g:go_metalinter_command'* @@ -1437,9 +1446,9 @@ Specifies the type of list to use for command outputs (such as errors from builds, results from static analysis commands, etc...). When an expected key is not present in the dictionary, |'g:go_list_type'| will be used instead. Supported keys are "GoBuild", "GoErrCheck", "GoFmt", "GoInstall", "GoLint", -"GoMetaLinter", "GoModifyTags" (used for both :GoAddTags and :GoRemoveTags), -"GoRename", "GoRun", and "GoTest". Supported values for each command are -"quickfix" and "locationlist". +"GoMetaLinter", "GoMetaLinterAutoSave", "GoModifyTags" (used for both +:GoAddTags and :GoRemoveTags), "GoRename", "GoRun", and "GoTest". Supported +values for each command are "quickfix" and "locationlist". > let g:go_list_type_commands = {} < @@ -1651,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* @@ -1725,7 +1746,7 @@ Highlight operators such as `:=` , `==`, `-=`, etc. < *'g:go_highlight_functions'* -Highlight function names. +Highlight function and method declarations. > let g:go_highlight_functions = 0 < @@ -1737,11 +1758,11 @@ declarations. Setting this implies the functionality from > let g:go_highlight_function_arguments = 0 < - *'g:go_highlight_methods'* + *'g:go_highlight_function_calls'* -Highlight method names. +Highlight function and method calls. > - let g:go_highlight_methods = 0 + let g:go_highlight_function_calls = 0 < *'g:go_highlight_types'* @@ -1808,6 +1829,212 @@ 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| or |:GoDebugTest| 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: + + * 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 |:GoDebugStop| to stop `dlv` and exit debugging mode. + + *:GoDebugTest* +:GoDebugTest [pkg] [program-args] + + Behaves the same as |:GoDebugStart| but runs `dlv test` instead of + `dlv debug` so you can debug tests. + + Use `-test.flag` to pass flags to `go test` when debugging a test; for + example `-test.v` or `-test.run TestFoo` + + + *: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/ftdetect/gofiletype.vim b/ftdetect/gofiletype.vim index bc47776..d3662f4 100644 --- a/ftdetect/gofiletype.vim +++ b/ftdetect/gofiletype.vim @@ -1,3 +1,5 @@ +" vint: -ProhibitAutocmdWithNoGroup + " We take care to preserve the user's fileencodings and fileformats, " because those settings are global (not buffer local), yet we want " to override them for loading Go files, which are defined to be UTF-8. @@ -18,17 +20,15 @@ function! s:gofiletype_post() let &g:fileencodings = s:current_fileencodings endfunction -augroup vim-go-filetype - autocmd! - au BufNewFile *.go setfiletype go | setlocal fileencoding=utf-8 fileformat=unix - au BufRead *.go call s:gofiletype_pre("go") - au BufReadPost *.go call s:gofiletype_post() +" Note: should not use augroup in ftdetect (see :help ftdetect) +au BufNewFile *.go setfiletype go | setlocal fileencoding=utf-8 fileformat=unix +au BufRead *.go call s:gofiletype_pre("go") +au BufReadPost *.go call s:gofiletype_post() - au BufNewFile *.s setfiletype asm | setlocal fileencoding=utf-8 fileformat=unix - au BufRead *.s call s:gofiletype_pre("asm") - au BufReadPost *.s call s:gofiletype_post() +au BufNewFile *.s setfiletype asm | setlocal fileencoding=utf-8 fileformat=unix +au BufRead *.s call s:gofiletype_pre("asm") +au BufReadPost *.s call s:gofiletype_post() - au BufRead,BufNewFile *.tmpl set filetype=gohtmltmpl -augroup end +au BufRead,BufNewFile *.tmpl set filetype=gohtmltmpl " vim: sw=2 ts=2 et diff --git a/ftplugin/go/commands.vim b/ftplugin/go/commands.vim index 9b983ef..8b74ba6 100644 --- a/ftplugin/go/commands.vim +++ b/ftplugin/go/commands.vim @@ -98,4 +98,11 @@ 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(0, ) + command! -nargs=* -complete=customlist,go#package#Complete GoDebugTest call go#debug#Start(1, ) + command! -nargs=? GoDebugBreakpoint call go#debug#Breakpoint() +endif + " vim: sw=2 ts=2 et diff --git a/ftplugin/go/snippets.vim b/ftplugin/go/snippets.vim index 33a3cd5..f53de41 100644 --- a/ftplugin/go/snippets.vim +++ b/ftplugin/go/snippets.vim @@ -40,7 +40,7 @@ function! s:GoMinisnip() abort endif if exists('g:minisnip_dir') - let g:minisnip_dir .= ':' . globpath(&rtp, 'gosnippets/minisnip') + let g:minisnip_dir .= go#util#PathListSep() . globpath(&rtp, 'gosnippets/minisnip') else let g:minisnip_dir = globpath(&rtp, 'gosnippets/minisnip') endif diff --git a/gosnippets/UltiSnips/go.snippets b/gosnippets/UltiSnips/go.snippets index 1a78a04..9a9f546 100644 --- a/gosnippets/UltiSnips/go.snippets +++ b/gosnippets/UltiSnips/go.snippets @@ -252,6 +252,11 @@ snippet fn "fmt.Println(...)" fmt.Println("${1:${VISUAL}}") endsnippet +# Fmt Errorf debug +snippet fe "fmt.Errorf(...)" +fmt.Errorf("${1:${VISUAL}}") +endsnippet + # log printf snippet lf "log.Printf(...)" log.Printf("${1:${VISUAL}} = %+v\n", $1) diff --git a/gosnippets/snippets/go.snip b/gosnippets/snippets/go.snip index 9a480b0..7a22cd2 100644 --- a/gosnippets/snippets/go.snip +++ b/gosnippets/snippets/go.snip @@ -128,21 +128,21 @@ abbr if err := ...; err != nil { ... } # error snippet snippet errn -abbr if err != nil { ... } +abbr if err != nil { return err } if err != nil { return err } ${0} # error snippet in TestFunc snippet errt -abbr if err != nil { ... } +abbr if err != nil { t.Fatal(err) } if err != nil { t.Fatal(err) } # error snippet in log.Fatal snippet errl -abbr if err != nil { ... } +abbr if err != nil { log.Fatal(err) } if err != nil { log.Fatal(err) } @@ -157,7 +157,7 @@ abbr if err != nil { return [...], err } # error snippet handle and return snippet errh -abbr if err != nil { return } +abbr if err != nil { ... return } if err != nil { ${1} return @@ -166,7 +166,7 @@ abbr if err != nil { return } # error snippet with panic snippet errp -abbr if err != nil { ... } +abbr if err != nil { panic(...) } if err != nil { panic(${1}) } @@ -219,6 +219,10 @@ abbr fmt.Printf(...) snippet fn abbr fmt.Println(...) fmt.Println("${1}") +# Fmt Errorf +snippet fe +abbr fmt.Errorf(...) + fmt.Errorf("${1}") # log printf snippet lf abbr log.Printf(...) diff --git a/plugin/go.vim b/plugin/go.vim index bb88608..513ce36 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/go.vim b/syntax/go.vim index 8e7df12..8769dff 100644 --- a/syntax/go.vim +++ b/syntax/go.vim @@ -42,8 +42,8 @@ if !exists("g:go_highlight_function_arguments") let g:go_highlight_function_arguments = 0 endif -if !exists("g:go_highlight_methods") - let g:go_highlight_methods = 0 +if !exists("g:go_highlight_function_calls") + let g:go_highlight_function_calls = 0 endif if !exists("g:go_highlight_fields") @@ -202,7 +202,19 @@ else endif if g:go_highlight_format_strings != 0 - syn match goFormatSpecifier /\([^%]\(%%\)*\)\@<=%[-#0 +]*\%(\*\|\d\+\)\=\%(\.\%(\*\|\d\+\)\)*[vTtbcdoqxXUeEfgGsp]/ contained containedin=goString + " [n] notation is valid for specifying explicit argument indexes + " 1. Match a literal % not preceded by a %. + " 2. Match any number of -, #, 0, space, or + + " 3. Match * or [n]* or any number or nothing before a . + " 4. Match * or [n]* or any number or nothing after a . + " 5. Match [n] or nothing before a verb + " 6. Match a formatting verb + syn match goFormatSpecifier /\ + \([^%]\(%%\)*\)\ + \@<=%[-#0 +]*\ + \%(\%(\%(\[\d\+\]\)\=\*\)\|\d\+\)\=\ + \%(\.\%(\%(\%(\[\d\+\]\)\=\*\)\|\d\+\)\=\)\=\ + \%(\[\d\+\]\)\=[vTtbcdoqxXUeEfFgGsp]/ contained containedin=goString,goRawString hi def link goFormatSpecifier goSpecialString endif @@ -348,7 +360,6 @@ hi def link goOperator Operator " Functions; if g:go_highlight_functions isnot 0 || g:go_highlight_function_arguments isnot 0 - syn match goFunctionCall /\w\+\ze(/ contains=goBuiltins,goDeclaration syn match goDeclaration /\/ nextgroup=goReceiver,goFunction,goSimpleArguments skipwhite skipnl syn match goReceiverVar /\w\+\ze\s\+\(\w\|\*\)/ nextgroup=goPointerOperator,goReceiverType skipwhite skipnl contained syn match goPointerOperator /\*/ nextgroup=goReceiverType contained skipwhite skipnl @@ -367,13 +378,12 @@ else syn keyword goDeclaration func endif hi def link goFunction Function -hi def link goFunctionCall Type -" Methods; -if g:go_highlight_methods != 0 - syn match goMethodCall /\.\w\+\ze(/hs=s+1 +" Function calls; +if g:go_highlight_function_calls != 0 + syn match goFunctionCall /\w\+\ze(/ contains=goBuiltins,goDeclaration endif -hi def link goMethodCall Type +hi def link goFunctionCall Type " Fields; if g:go_highlight_fields != 0 @@ -443,7 +453,7 @@ if g:go_highlight_build_constraints != 0 || s:fold_package_comment \ . ' contains=@goCommentGroup,@Spell' \ . (s:fold_package_comment ? ' fold' : '') exe 'syn region goPackageComment start=/\v\/\*.*\n(.*\n)*\s*\*\/\npackage/' - \ . ' end=/\v\n\s*package/he=e-7,me=e-7,re=e-7' + \ . ' end=/\v\*\/\n\s*package/he=e-7,me=e-7,re=e-7' \ . ' contains=@goCommentGroup,@Spell' \ . (s:fold_package_comment ? ' fold' : '') hi def link goPackageComment Comment diff --git a/syntax/godebugoutput.vim b/syntax/godebugoutput.vim new file mode 100644 index 0000000..b8e6f5f --- /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 0000000..b0c5372 --- /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 0000000..791705b --- /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