diff --git a/autoload/go/cmd.vim b/autoload/go/cmd.vim index d3ce7b0446..87312806d9 100644 --- a/autoload/go/cmd.vim +++ b/autoload/go/cmd.vim @@ -14,24 +14,30 @@ endfunction " default it tries to call simply 'go build', but it first tries to get all " dependent files for the current folder and passes it to go build. function! go#cmd#Build(bang, ...) - let default_makeprg = &makeprg + " expand all wildcards(i.e: '%' to the current file name) + let goargs = map(copy(a:000), "expand(v:val)") - let old_gopath = $GOPATH - let $GOPATH = go#path#Detect() + " escape all shell arguments before we pass it to make + let goargs = go#util#Shelllist(goargs, 1) - let l:tmpname = tempname() + " create our command arguments. go build discards any results when it + " compiles multiple packages. So we pass the `errors` package just as an + " placeholder with the current folder (indicated with '.') + let args = ["build"] + goargs + [".", "errors"] - if v:shell_error - let &makeprg = "go build . errors" - else - " :make expands '%' and '#' wildcards, so they must also be escaped - let goargs = go#util#Shelljoin(map(copy(a:000), "expand(v:val)"), 1) - let gofiles = go#util#Shelljoin(go#tool#Files(), 1) - let &makeprg = "go build -o " . l:tmpname . ' ' . goargs . ' ' . gofiles + " if we have nvim, call it asynchronously and return early ;) + if has('nvim') + call go#jobcontrol#Spawn("build", args) + return endif - echon "vim-go: " | echohl Identifier | echon "building ..."| echohl None + let old_gopath = $GOPATH + let $GOPATH = go#path#Detect() + let default_makeprg = &makeprg + let &makeprg = "go " . join(args, ' ') + if g:go_dispatch_enabled && exists(':Make') == 2 + call go#util#EchoProgress("building dispatched ...") silent! exe 'Make' else silent! exe 'lmake!' @@ -42,25 +48,35 @@ function! go#cmd#Build(bang, ...) let errors = go#list#Get() call go#list#Window(len(errors)) - if !empty(errors) + if !empty(errors) if !a:bang call go#list#JumpToFirst() endif else - redraws! | echon "vim-go: " | echohl Function | echon "[build] SUCCESS"| echohl None + call go#util#EchoSuccess("[build] SUCCESS") endif - - call delete(l:tmpname) let &makeprg = default_makeprg let $GOPATH = old_gopath endfunction + +" Run runs the current file (and their dependencies if any) in a new terminal. +function! go#cmd#RunTerm(mode) + let cmd = "go run ". go#util#Shelljoin(go#tool#Files()) + call go#term#newmode(cmd, a:mode) +endfunction + " Run runs the current file (and their dependencies if any) and outputs it. " This is intented to test small programs and play with them. It's not " suitable for long running apps, because vim is blocking by default and " calling long running apps will block the whole UI. function! go#cmd#Run(bang, ...) + if has('nvim') + call go#cmd#RunTerm('') + return + endif + let old_gopath = $GOPATH let $GOPATH = go#path#Detect() @@ -90,27 +106,8 @@ function! go#cmd#Run(bang, ...) exe 'lmake!' endif - " Remove any nonvalid filename from the location list to avoid opening an - " empty buffer. See https://github.com/fatih/vim-go/issues/287 for - " details. let items = go#list#Get() - let errors = [] - let is_readable = {} - - for item in items - let filename = bufname(item.bufnr) - if !has_key(is_readable, filename) - let is_readable[filename] = filereadable(filename) - endif - if is_readable[filename] - call add(errors, item) - endif - endfor - - for k in keys(filter(is_readable, '!v:val')) - echo "vim-go: " | echohl Identifier | echon "[run] Dropped " | echohl Constant | echon '"' . k . '"' - echohl Identifier | echon " from location list (nonvalid filename)" | echohl None - endfor + let errors = go#tool#FilterValids(items) call go#list#Populate(errors) call go#list#Window(len(errors)) @@ -149,33 +146,51 @@ endfunction " compile the tests instead of running them (useful to catch errors in the " test files). Any other argument is appendend to the final `go test` command function! go#cmd#Test(bang, compile, ...) - let command = "go test " + let args = ["test"] " don't run the test, only compile it. Useful to capture and fix errors or " to create a test binary. if a:compile - let command .= "-c " + call add(args, "-c") endif if a:0 - let command .= go#util#Shelljoin(map(copy(a:000), "expand(v:val)")) + " expand all wildcards(i.e: '%' to the current file name) + let goargs = map(copy(a:000), "expand(v:val)") + + " escape all shell arguments before we pass it to test + call extend(args, go#util#Shelllist(goargs, 1)) else " only add this if no custom flags are passed let timeout = get(g:, 'go_test_timeout', '10s') - let command .= "-timeout=" . timeout . " " + call add(args, printf("-timeout=%s", timeout)) + endif + + if has('nvim') + if get(g:, 'go_term_enabled', 0) + call go#term#new(["go"] + args) + else + call go#jobcontrol#Spawn("test", args) + endif + return endif - call go#cmd#autowrite() if a:compile echon "vim-go: " | echohl Identifier | echon "compiling tests ..." | echohl None else echon "vim-go: " | echohl Identifier | echon "testing ..." | echohl None endif + call go#cmd#autowrite() redraw + + let command = "go " . join(args, ' ') + let out = go#tool#ExecuteInDir(command) if v:shell_error let errors = go#tool#ParseErrors(split(out, '\n')) + let errors = go#tool#FilterValids(errors) + call go#list#Populate(errors) call go#list#Window(len(errors)) if !empty(errors) && !a:bang diff --git a/autoload/go/fmt.vim b/autoload/go/fmt.vim index ee9525a92a..9ef9a2ebe2 100644 --- a/autoload/go/fmt.vim +++ b/autoload/go/fmt.vim @@ -43,6 +43,8 @@ if !exists("g:go_fmt_experimental") let g:go_fmt_experimental = 0 endif +let s:got_fmt_error = 0 + " we have those problems : " http://stackoverflow.com/questions/12741977/prevent-vim-from-updating-its-undo-tree " http://stackoverflow.com/questions/18532692/golang-formatter-and-vim-how-to-destroy-history-record?rq=1 @@ -117,9 +119,12 @@ function! go#fmt#Format(withGoimport) let &fileformat = old_fileformat let &syntax = &syntax - " clean up previous location list - call go#list#Clean() - call go#list#Window() + " clean up previous location list, but only if it's due fmt + if s:got_fmt_error + let s:got_fmt_error = 0 + call go#list#Clean() + call go#list#Window() + endif elseif g:go_fmt_fail_silently == 0 let splitted = split(out, '\n') "otherwise get the errors and put them to location list @@ -141,6 +146,7 @@ function! go#fmt#Format(withGoimport) echohl Error | echomsg "Gofmt returned error" | echohl None endif + let s:got_fmt_error = 1 call go#list#Window(len(errors)) " We didn't use the temp file, so clean up diff --git a/autoload/go/jobcontrol.vim b/autoload/go/jobcontrol.vim new file mode 100644 index 0000000000..f8576a2c45 --- /dev/null +++ b/autoload/go/jobcontrol.vim @@ -0,0 +1,168 @@ +" s:jobs is a global reference to all jobs started with Spawn() or with the +" internal function s:spawn +let s:jobs = {} + +" Spawn is a wrapper around s:spawn. It can be executed by other files and +" scripts if needed. Desc defines the description for printing the status +" during the job execution (useful for statusline integration). +function! go#jobcontrol#Spawn(desc, args) + " autowrite is not enabled for jobs + call go#cmd#autowrite() + + let job = s:spawn(a:desc, a:args) + return job.id +endfunction + +" Statusline returns the current status of the job +function! go#jobcontrol#Statusline() abort + if empty(s:jobs) + return '' + endif + + let import_path = go#package#ImportPath(expand('%:p:h')) + + for job in values(s:jobs) + if job.importpath != import_path + continue + endif + + if job.state == "SUCCESS" + return '' + endif + + return printf("%s ... [%s]", job.desc, job.state) + endfor + + return '' +endfunction + +" spawn spawns a go subcommand with the name and arguments with jobstart. Once +" a job is started a reference will be stored inside s:jobs. spawn changes the +" GOPATH when g:go_autodetect_gopath is enabled. The job is started inside the +" current files folder. +function! s:spawn(desc, args) + let job = { + \ 'desc': a:desc, + \ 'winnr': winnr(), + \ 'importpath': go#package#ImportPath(expand('%:p:h')), + \ 'state': "RUNNING", + \ 'stderr' : [], + \ 'stdout' : [], + \ 'on_stdout': function('s:on_stdout'), + \ 'on_stderr': function('s:on_stderr'), + \ 'on_exit' : function('s:on_exit'), + \ } + + " modify GOPATH if needed + let old_gopath = $GOPATH + let $GOPATH = go#path#Detect() + + " execute go build in the files directory + let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' + + " cleanup previous jobs for this file + for jb in values(s:jobs) + if jb.importpath == job.importpath + unlet s:jobs[jb.id] + endif + endfor + + let dir = getcwd() + + execute cd . fnameescape(expand("%:p:h")) + + " append the subcommand, such as 'build' + let argv = ['go'] + a:args + + " run, forrest, run! + let id = jobstart(argv, job) + let job.id = id + let s:jobs[id] = job + + execute cd . fnameescape(dir) + + " restore back GOPATH + let $GOPATH = old_gopath + + return job +endfunction + +" on_exit is the exit handler for jobstart(). It handles cleaning up the job +" references and also displaying errors in the quickfix window collected by +" on_stderr handler. If there are no errors and a quickfix window is open, +" it'll be closed. +function! s:on_exit(job_id, data) + let std_combined = self.stderr + self.stdout + if empty(std_combined) + call go#list#Clean() + call go#list#Window() + + let self.state = "SUCCESS" + return + endif + + let errors = go#tool#ParseErrors(std_combined) + let errors = go#tool#FilterValids(errors) + + if !len(errors) + " no errors could be past, just return + call go#list#Clean() + call go#list#Window() + + let self.state = "SUCCESS" + return + endif + + let self.state = "FAILED" + + " if we are still in the same windows show the list + if self.winnr == winnr() + call go#list#Populate(errors) + call go#list#Window(len(errors)) + call go#list#JumpToFirst() + endif +endfunction + +" on_stdout is the stdout handler for jobstart(). It collects the output of +" stderr and stores them to the jobs internal stdout list. +function! s:on_stdout(job_id, data) + call extend(self.stdout, a:data) +endfunction + +" on_stderr is the stderr handler for jobstart(). It collects the output of +" stderr and stores them to the jobs internal stderr list. +function! s:on_stderr(job_id, data) + call extend(self.stderr, a:data) +endfunction + +" abort_all aborts all current jobs created with s:spawn() +function! s:abort_all() + if empty(s:jobs) + return + endif + + for id in keys(s:jobs) + if id > 0 + silent! call jobstop(id) + endif + endfor + + let s:jobs = {} +endfunction + +" abort aborts the job with the given name, where name is the first argument +" passed to s:spawn() +function! s:abort(path) + if empty(s:jobs) + return + endif + + for job in values(s:jobs) + if job.importpath == path && job.id > 0 + silent! call jobstop(job.id) + unlet s:jobs['job.id'] + endif + endfor +endfunction + +" vim:ts=2:sw=2:et diff --git a/autoload/go/list.vim b/autoload/go/list.vim index 366c558903..9582d788ce 100644 --- a/autoload/go/list.vim +++ b/autoload/go/list.vim @@ -36,6 +36,10 @@ function! go#list#Populate(items) call setloclist(0, a:items, 'r') endfunction +function! go#list#PopulateWin(winnr, items) + call setloclist(a:winnr, a:items, 'r') +endfunction + " Parse parses the given items based on the specified errorformat nad " populates the location list. function! go#list#ParseFormat(errformat, items) diff --git a/autoload/go/term.vim b/autoload/go/term.vim new file mode 100644 index 0000000000..9d0b13a936 --- /dev/null +++ b/autoload/go/term.vim @@ -0,0 +1,108 @@ +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(cmd) + call go#term#newmode(a:cmd, g:go_term_mode) +endfunction + +" new creates a new terminal with the given command and window mode. +function! go#term#newmode(cmd, mode) + let mode = a:mode + if empty(mode) + let mode = g:go_term_mode + endif + + execute mode.' __go_term__' + + setlocal filetype=goterm + setlocal bufhidden=delete + setlocal winfixheight + setlocal noswapfile + setlocal nobuflisted + + let job = { + \ 'stderr' : [], + \ 'stdout' : [], + \ 'on_stdout': function('s:on_stdout'), + \ 'on_stderr': function('s:on_stderr'), + \ 'on_exit' : function('s:on_exit'), + \ } + + let id = termopen(a:cmd, job) + let job.id = id + 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 vertical we don't change + " the height. The below command resizes the buffer + if a:mode == "split" + exe 'resize ' . height + elseif a:mode == "vertical" + exe 'vertical resize ' . width + endif + + " we also need to resize the pty, so there you go... + call jobresize(id, width, height) + + let s:jobs[id] = job + return id +endfunction + +function! s:on_stdout(job_id, data) + 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) + if !has_key(s:jobs, a:job_id) + return + endif + let job = s:jobs[a:job_id] + + call extend(job.stderr, a:data) +endfunction + +function! s:on_exit(job_id, data) + if !has_key(s:jobs, a:job_id) + return + endif + let job = s:jobs[a:job_id] + + " usually there is always output so never branch into this clause + if empty(job.stdout) + call go#list#Clean() + call go#list#Window() + else + let errors = go#tool#ParseErrors(job.stdout) + let errors = go#tool#FilterValids(errors) + if !empty(errors) + " close terminal we don't need it + close + + call go#list#Populate(errors) + call go#list#Window(len(errors)) + call go#list#JumpToFirst() + else + call go#list#Clean() + call go#list#Window() + endif + + endif + + unlet s:jobs[a:job_id] +endfunction + +" vim:ts=2:sw=2:et diff --git a/autoload/go/tool.vim b/autoload/go/tool.vim index ed339ab7fa..2ad184c82e 100644 --- a/autoload/go/tool.vim +++ b/autoload/go/tool.vim @@ -50,9 +50,14 @@ function! go#tool#ParseErrors(lines) if !empty(fatalerrors) call add(errors, {"text": fatalerrors[1]}) elseif !empty(tokens) - call add(errors, {"filename" : fnamemodify(tokens[1], ':p'), - \"lnum": tokens[2], - \"text": tokens[3]}) + " strip endlines of form ^M + let out=substitute(tokens[3], '\r$', '', '') + + call add(errors, { + \ "filename" : fnamemodify(tokens[1], ':p'), + \ "lnum" : tokens[2], + \ "text" : out, + \ }) elseif !empty(errors) " Preserve indented lines. " This comes up especially with multi-line test output. @@ -65,6 +70,41 @@ function! go#tool#ParseErrors(lines) return errors endfunction +"FilterValids filters the given items with only items that have a valid +"filename. Any non valid filename is filtered out. +function! go#tool#FilterValids(items) + " Remove any nonvalid filename from the location list to avoid opening an + " empty buffer. See https://github.com/fatih/vim-go/issues/287 for + " details. + let filtered = [] + let is_readable = {} + + for item in a:items + if has_key(item, 'bufnr') + let filename = bufname(item.bufnr) + elseif has_key(item, 'filename') + let filename = item.filename + else + echohl Identifier | echon "no filename available" | echohl None + continue + endif + + if !has_key(is_readable, filename) + let is_readable[filename] = filereadable(filename) + endif + if is_readable[filename] + call add(filtered, item) + endif + endfor + + for k in keys(filter(is_readable, '!v:val')) + echo "vim-go: " | echohl Identifier | echon "[run] Dropped " | echohl Constant | echon '"' . k . '"' + echohl Identifier | echon " from location list (nonvalid filename)" | echohl None + endfor + + return filtered +endfunction + function! go#tool#ExecuteInDir(cmd) abort let old_gopath = $GOPATH let $GOPATH = go#path#Detect() diff --git a/autoload/go/util.vim b/autoload/go/util.vim index ce34d15e0c..85ab9331a7 100644 --- a/autoload/go/util.vim +++ b/autoload/go/util.vim @@ -53,7 +53,37 @@ endfunction function! go#util#Shelljoin(arglist, ...) if a:0 return join(map(copy(a:arglist), 'shellescape(v:val, ' . a:1 . ')'), ' ') - else - return join(map(copy(a:arglist), 'shellescape(v:val)'), ' ') endif + + return join(map(copy(a:arglist), 'shellescape(v:val)'), ' ') +endfunction + +" Shelljoin returns a shell-safe representation of the items in the given +" arglist. The {special} argument of shellescape() may optionally be passed. +function! go#util#Shelllist(arglist, ...) + if a:0 + return map(copy(a:arglist), 'shellescape(v:val, ' . a:1 . ')') + endif + return map(copy(a:arglist), 'shellescape(v:val)') +endfunction + +" TODO(arslan): I couldn't parameterize the highlight types. Check if we can +" simplify the following functions + +function! go#util#EchoSuccess(msg) + redraws! | echon "vim-go: " | echohl Function | echon a:msg | echohl None endfunction + +function! go#util#EchoError(msg) + redraws! | echon "vim-go: " | echohl ErrorMsg | echon a:msg | echohl None +endfunction + +function! go#util#EchoWarning(msg) + redraws! | echon "vim-go: " | echohl WarningMsg | echon a:msg | echohl None +endfunction + +function! go#util#EchoProgress(msg) + redraws! | echon "vim-go: " | echohl Identifier | echon a:msg | echohl None +endfunction + +" vim:ts=4:sw=4:et diff --git a/ftplugin/go/mappings.vim b/ftplugin/go/mappings.vim index 2b11c4c8e9..9e243543d7 100644 --- a/ftplugin/go/mappings.vim +++ b/ftplugin/go/mappings.vim @@ -11,6 +11,13 @@ endif " Some handy plug mappings nnoremap (go-run) :call go#cmd#Run(!g:go_jump_to_error, '%') + +if has("nvim") + nnoremap (go-run-vertical) :call go#cmd#RunTerm('vsplit') + nnoremap (go-run-split) :call go#cmd#RunTerm('split') + nnoremap (go-run-tab) :call go#cmd#RunTerm('tab') +endif + nnoremap (go-build) :call go#cmd#Build(!g:go_jump_to_error) nnoremap (go-generate) :call go#cmd#Generate(!g:go_jump_to_error) nnoremap (go-install) :call go#cmd#Install(!g:go_jump_to_error) diff --git a/plugin/go.vim b/plugin/go.vim index 3337b59dcc..43295ad756 100644 --- a/plugin/go.vim +++ b/plugin/go.vim @@ -137,7 +137,6 @@ augroup vim-go if get(g:, "go_metalinter_autosave", 0) autocmd BufWritePost *.go call go#lint#Gometa(1) endif - augroup END