diff --git a/autoload/go/config.vim b/autoload/go/config.vim index 8e72b1a83a..1f7260a4dd 100644 --- a/autoload/go/config.vim +++ b/autoload/go/config.vim @@ -278,6 +278,14 @@ function! go#config#SetAsmfmtAutosave(value) abort let g:go_asmfmt_autosave = a:value endfunction +function! go#config#ModFmtAutosave() abort + return get(g:, "go_mod_fmt_autosave", 1) +endfunction + +function! go#config#SetModFmtAutosave(value) abort + let g:go_mod_fmt_autosave = a:value +endfunction + function! go#config#DocMaxHeight() abort return get(g:, "go_doc_max_height", 20) endfunction diff --git a/autoload/go/mod.vim b/autoload/go/mod.vim new file mode 100644 index 0000000000..0f4b5a85d7 --- /dev/null +++ b/autoload/go/mod.vim @@ -0,0 +1,140 @@ +let s:go_major_version = "" + +function! go#mod#Format() abort + " go mod only exists in `v1.11` + if empty(s:go_major_version) + let tokens = matchlist(go#util#System("go version"), '\d\+.\(\d\+\) ') + let s:go_major_version = str2nr(tokens[1]) + endif + + if s:go_major_version < "11" + call go#util#EchoError("Go v1.11 is required to format go.mod file") + return + endif + + let fname = fnamemodify(expand("%"), ':p:gs?\\?/?') + + " Save cursor position and many other things. + let l:curw = winsaveview() + + " Write current unsaved buffer to a temp file + let l:tmpname = tempname() . '.mod' + call writefile(go#util#GetLines(), l:tmpname) + if go#util#IsWin() + let l:tmpname = tr(l:tmpname, '\', '/') + endif + + let current_col = col('.') + let l:args = ['go', 'mod', 'edit', '--fmt', l:tmpname] + let [l:out, l:err] = go#util#Exec(l:args) + let diff_offset = len(readfile(l:tmpname)) - line('$') + + if l:err == 0 + call go#mod#update_file(l:tmpname, fname) + else + let errors = s:parse_errors(fname, l:out) + call s:show_errors(errors) + endif + + " We didn't use the temp file, so clean up + call delete(l:tmpname) + + " Restore our cursor/windows positions. + call winrestview(l:curw) + + " be smart and jump to the line the new statement was added/removed + call cursor(line('.') + diff_offset, current_col) + + " Syntax highlighting breaks less often. + syntax sync fromstart +endfunction + +" update_file updates the target file with the given formatted source +function! go#mod#update_file(source, target) + " remove undo point caused via BufWritePre + try | silent undojoin | catch | endtry + + let old_fileformat = &fileformat + if exists("*getfperm") + " save file permissions + let original_fperm = getfperm(a:target) + endif + + call rename(a:source, a:target) + + " restore file permissions + if exists("*setfperm") && original_fperm != '' + call setfperm(a:target , original_fperm) + endif + + " reload buffer to reflect latest changes + silent edit! + + let &fileformat = old_fileformat + let &syntax = &syntax + + let l:listtype = go#list#Type("GoModFmt") + + " the title information was introduced with 7.4-2200 + " https://github.com/vim/vim/commit/d823fa910cca43fec3c31c030ee908a14c272640 + if has('patch-7.4.2200') + " clean up previous list + if l:listtype == "quickfix" + let l:list_title = getqflist({'title': 1}) + else + let l:list_title = getloclist(0, {'title': 1}) + endif + else + " can't check the title, so assume that the list was for go fmt. + let l:list_title = {'title': 'Format'} + endif + + if has_key(l:list_title, "title") && l:list_title['title'] == "Format" + call go#list#Clean(l:listtype) + endif +endfunction + +" parse_errors parses the given errors and returns a list of parsed errors +function! s:parse_errors(filename, content) abort + let splitted = split(a:content, '\n') + + " list of errors to be put into location list + let errors = [] + for line in splitted + let tokens = matchlist(line, '^\(.\{-}\):\(\d\+\):\s*\(.*\)') + if !empty(tokens) + call add(errors,{ + \"filename": a:filename, + \"lnum": tokens[2], + \"text": tokens[3], + \ }) + endif + endfor + + return errors +endfunction + +" show_errors opens a location list and shows the given errors. If the given +" errors is empty, it closes the the location list +function! s:show_errors(errors) abort + let l:listtype = go#list#Type("GoModFmt") + if !empty(a:errors) + call go#list#Populate(l:listtype, a:errors, 'Format') + call go#util#EchoError("GoModFmt returned error") + endif + + " this closes the window if there are no errors or it opens + " it if there is any + call go#list#Window(l:listtype, len(a:errors)) +endfunction + +function! go#mod#ToggleModFmtAutoSave() abort + if go#config#ModFmtAutosave() + call go#config#SetModFmtAutosave(0) + call go#util#EchoProgress("auto mod fmt disabled") + return + end + + call go#config#SetModFmtAutosave(1) + call go#util#EchoProgress("auto mod fmt enabled") +endfunction diff --git a/doc/vim-go.txt b/doc/vim-go.txt index 33015f40bb..31a3a76e61 100644 --- a/doc/vim-go.txt +++ b/doc/vim-go.txt @@ -806,6 +806,11 @@ CTRL-t Toggles |'g:go_fmt_autosave'|. + *:GoModFmtAutoSaveToggle* +:GoModFmtAutoSaveToggle + + Toggles |'g:go_mod_fmt_autosave'|. + *:GoAsmFmtAutoSaveToggle* :GoAsmFmtAutoSaveToggle @@ -880,6 +885,13 @@ CTRL-t } } < + *:GoModFmt* +:GoModFmt + + Filter the current go.mod buffer through "go mod edit -fmt" command. It + tries to preserve cursor position and avoids replacing the buffer with + stderr output. + ============================================================================== MAPPINGS *go-mappings* @@ -1097,6 +1109,10 @@ Calls `:GoImport` for the current package Generate if err != nil { return ... } automatically which infer the type of return values and the numbers. + *(go-mod-fmt)* + +Calls |:GoModFmt| for the current buffer + ============================================================================== TEXT OBJECTS *go-text-objects* @@ -1287,7 +1303,15 @@ doesn't break. However it's slows (creates/deletes a file for every save) and it's causing problems on some Vim versions. By default it's disabled. > let g:go_fmt_experimental = 0 + < + *'g:go_mod_fmt_autosave'* + +Use this option to auto |:GoModFmt| on save. By default it's enabled > + + let g:go_mod_fmt_autosave = 1 +< + *'g:go_doc_keywordprg_enabled'* Use this option to run `godoc` on words under the cursor with |K|; this will @@ -1497,10 +1521,10 @@ that was called. Supported values are "", "quickfix", and "locationlist". 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", "GoMetaLinterAutoSave", "GoModifyTags" (used for both -:GoAddTags and :GoRemoveTags), "GoRename", "GoRun", and "GoTest". Supported -values for each command are "quickfix" and "locationlist". +Supported keys are "GoBuild", "GoErrCheck", "GoFmt", "GoModFmt", "GoInstall", +"GoLint", "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 = {} < @@ -1874,6 +1898,13 @@ filetype. The `gohtmltmpl` filetype is automatically set for `*.tmpl` files; the `gotexttmpl` is never automatically set and needs to be set manually. +============================================================================== + *gomod* *ft-gomod-syntax* +go.mod file syntax~ + +The `gomod` 'filetype' provides syntax highlighting for Go's module file +`go.mod` + ============================================================================== DEBUGGER *go-debug* diff --git a/ftdetect/gofiletype.vim b/ftdetect/gofiletype.vim index d3662f4be1..acb061d7db 100644 --- a/ftdetect/gofiletype.vim +++ b/ftdetect/gofiletype.vim @@ -31,4 +31,9 @@ au BufReadPost *.s call s:gofiletype_post() au BufRead,BufNewFile *.tmpl set filetype=gohtmltmpl +" make sure we explicitly look for a `go.mod` and the `module` starts from the +" beginning +au BufNewFile,BufRead go.mod + \ if getline(1) =~ '^module.*' | set filetype=gomod | endif + " vim: sw=2 ts=2 et diff --git a/ftplugin/go/commands.vim b/ftplugin/go/commands.vim index ba29c59c07..99d81cf424 100644 --- a/ftplugin/go/commands.vim +++ b/ftplugin/go/commands.vim @@ -23,6 +23,9 @@ command! -range=0 GoSameIdsAutoToggle call go#guru#AutoToogleSameIds() command! -nargs=* -range GoAddTags call go#tags#Add(, , , ) command! -nargs=* -range GoRemoveTags call go#tags#Remove(, , , ) +" -- mod +command! -nargs=0 -range GoModFmt call go#mod#Format() + " -- tool command! -nargs=* -complete=customlist,go#tool#ValidFiles GoFiles echo go#tool#Files() command! -nargs=0 GoDeps echo go#tool#Deps() diff --git a/ftplugin/gomod.vim b/ftplugin/gomod.vim new file mode 100644 index 0000000000..5f3dc412a9 --- /dev/null +++ b/ftplugin/gomod.vim @@ -0,0 +1,15 @@ +" gomod.vim: Vim filetype plugin for Go assembler. + +if exists("b:did_ftplugin") + finish +endif +let b:did_ftplugin = 1 + +let b:undo_ftplugin = "setl fo< com< cms<" + +setlocal formatoptions-=t + +setlocal comments=s1:/*,mb:*,ex:*/,:// +setlocal commentstring=//\ %s + +" vim: sw=2 ts=2 et diff --git a/ftplugin/gomod/commands.vim b/ftplugin/gomod/commands.vim new file mode 100644 index 0000000000..d6a7285fce --- /dev/null +++ b/ftplugin/gomod/commands.vim @@ -0,0 +1,3 @@ +command! -nargs=0 -range GoModFmt call go#mod#Format() + +command! -nargs=0 GoModFmtAutoSaveToggle call go#mod#ToggleModFmtAutoSave() diff --git a/ftplugin/gomod/mappings.vim b/ftplugin/gomod/mappings.vim new file mode 100644 index 0000000000..c8664f648b --- /dev/null +++ b/ftplugin/gomod/mappings.vim @@ -0,0 +1 @@ +nnoremap (go-mod-fmt) :call go#mod#Format() diff --git a/plugin/go.vim b/plugin/go.vim index 01e19b7d93..78f7e5b570 100644 --- a/plugin/go.vim +++ b/plugin/go.vim @@ -226,6 +226,13 @@ function! s:asmfmt_autosave() endif endfunction +function! s:modfmt_autosave() + " go.mod code formatting on save + if get(g:, "go_mod_fmt_autosave", 1) + call go#mod#Format() + endif +endfunction + function! s:metalinter_autosave() " run gometalinter on save if get(g:, "go_metalinter_autosave", 0) @@ -253,6 +260,7 @@ augroup vim-go endif autocmd BufWritePre *.go call s:fmt_autosave() + autocmd BufWritePre *.mod call s:modfmt_autosave() autocmd BufWritePre *.s call s:asmfmt_autosave() autocmd BufWritePost *.go call s:metalinter_autosave() autocmd BufNewFile *.go call s:template_autocreate() diff --git a/syntax/gomod.vim b/syntax/gomod.vim new file mode 100644 index 0000000000..3679e2cdf5 --- /dev/null +++ b/syntax/gomod.vim @@ -0,0 +1,45 @@ +" gomod.vim: Vim syntax file for go.mod file +" +" Quit when a (custom) syntax file was already loaded +if exists("b:current_syntax") + finish +endif + +syntax case match + +" match keywords +syntax keyword gomodModule module +syntax keyword gomodRequire require +syntax keyword gomodExclude exclude +syntax keyword gomodReplace replace + +" require, exclude and replace can be also grouped into block +syntax region gomodRequire start='require (' end=')' transparent contains=gomodRequire,gomodVersion +syntax region gomodExclude start='exclude (' end=')' transparent contains=gomodExclude,gomodVersion +syntax region gomodReplace start='replace (' end=')' transparent contains=gomodReplace,gomodVersion + +" set highlights +highlight default link gomodModule Keyword +highlight default link gomodRequire Keyword +highlight default link gomodExclude Keyword +highlight default link gomodReplace Keyword + +" comments are always in form of // ... +syntax region gomodComment start="//" end="$" contains=@Spell +highlight default link gomodComment Comment + +" make sure quoted import paths are higlighted +syntax region gomodString start=+"+ skip=+\\\\\|\\"+ end=+"+ +highlight default link gomodString String + +" replace operator is in the form of '=>' +syntax match gomodReplaceOperator "\v\=\>" +highlight default link gomodReplaceOperator Operator + + +" highlight semver, note that this is very simple. But it works for now +syntax match gomodVersion "v\d\+\.\d\+\.\d\+" +syntax match gomodVersion "v\d\+\.\d\+\.\d\+-.*" +highlight default link gomodVersion Identifier + +let b:current_syntax = "gomod"