Skip to content

go.mod file support #1931

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions autoload/go/config.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
140 changes: 140 additions & 0 deletions autoload/go/mod.vim
Original file line number Diff line number Diff line change
@@ -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')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing in the parsed errors and calling go#list#Populate manually, can you use an errorformat string and call go#list#ParseFormat?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried it, but I think it'll not work because we run the command on a temporary file first (because go mod edit does an in-place edit). This means that the initial %f is the temporary filename. This is copied from fmt.vim and it's the same issue. I'll check if I can do something ( I would prefer ParseFormat indeed)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm... If you know the temporary filename, then it should be easy enough to do a substitute(...) on a:errors before passing to ParseFormat.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think at this point, this is too much a hack. We're having a working version and it really works fine. Not sure if it's worth doing it (same applies for gofmt).

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
39 changes: 35 additions & 4 deletions doc/vim-go.txt
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,11 @@ CTRL-t

Toggles |'g:go_fmt_autosave'|.

*:GoModFmtAutoSaveToggle*
:GoModFmtAutoSaveToggle

Toggles |'g:go_mod_fmt_autosave'|.

*:GoAsmFmtAutoSaveToggle*
:GoAsmFmtAutoSaveToggle

Expand Down Expand Up @@ -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*

Expand Down Expand Up @@ -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*

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {}
<
Expand Down Expand Up @@ -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*
Expand Down
5 changes: 5 additions & 0 deletions ftdetect/gofiletype.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions ftplugin/go/commands.vim
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ command! -range=0 GoSameIdsAutoToggle call go#guru#AutoToogleSameIds()
command! -nargs=* -range GoAddTags call go#tags#Add(<line1>, <line2>, <count>, <f-args>)
command! -nargs=* -range GoRemoveTags call go#tags#Remove(<line1>, <line2>, <count>, <f-args>)

" -- mod
command! -nargs=0 -range GoModFmt call go#mod#Format()

" -- tool
command! -nargs=* -complete=customlist,go#tool#ValidFiles GoFiles echo go#tool#Files(<f-args>)
command! -nargs=0 GoDeps echo go#tool#Deps()
Expand Down
15 changes: 15 additions & 0 deletions ftplugin/gomod.vim
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions ftplugin/gomod/commands.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
command! -nargs=0 -range GoModFmt call go#mod#Format()

command! -nargs=0 GoModFmtAutoSaveToggle call go#mod#ToggleModFmtAutoSave()
1 change: 1 addition & 0 deletions ftplugin/gomod/mappings.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nnoremap <silent> <Plug>(go-mod-fmt) :<C-u>call go#mod#Format()<CR>
8 changes: 8 additions & 0 deletions plugin/go.vim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
45 changes: 45 additions & 0 deletions syntax/gomod.vim
Original file line number Diff line number Diff line change
@@ -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"