Skip to content
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

fmt: format with gopls #2729

Merged
merged 4 commits into from
Feb 23, 2020
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
62 changes: 28 additions & 34 deletions autoload/go/fmt.vim
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ set cpo&vim
" this and have VimL experience, please look at the function for
" improvements, patches are welcome :)
function! go#fmt#Format(withGoimport) abort
let l:bin_name = go#config#FmtCommand()
if a:withGoimport == 1
let l:bin_name = "goimports"
endif

if l:bin_name == 'gopls'
call go#lsp#Format()
return
endif

if go#config#FmtExperimental()
" Using winsaveview to save/restore cursor state has the problem of
" closing folds on save:
Expand Down Expand Up @@ -52,20 +62,15 @@ function! go#fmt#Format(withGoimport) abort
let l:tmpname = tr(l:tmpname, '\', '/')
endif

let bin_name = go#config#FmtCommand()
if a:withGoimport == 1
let bin_name = "goimports"
endif

let current_col = col('.')
let [l:out, l:err] = go#fmt#run(bin_name, l:tmpname, expand('%'))
let [l:out, l:err] = go#fmt#run(l:bin_name, l:tmpname, expand('%'))
let diff_offset = len(readfile(l:tmpname)) - line('$')

if l:err == 0
call go#fmt#update_file(l:tmpname, expand('%'))
elseif !go#config#FmtFailSilently()
let errors = s:parse_errors(expand('%'), out)
call s:show_errors(errors)
let l:errors = s:replace_filename(expand('%'), out)
call go#fmt#ShowErrors(l:errors)
endif

" We didn't use the temp file, so clean up
Expand Down Expand Up @@ -162,38 +167,27 @@ function! s:fmt_cmd(bin_name, source, target)
return cmd
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\+\):\(\d\+\)\s*\(.*\)')
if !empty(tokens)
call add(errors,{
\"filename": a:filename,
\"lnum": tokens[2],
\"col": tokens[3],
\"text": tokens[4],
\ })
endif
endfor
" replace_filename replaces the filename on each line of content with
" a:filename.
function! s:replace_filename(filename, content) abort
let l:errors = split(a:content, '\n')

return errors
let l:errors = map(l:errors, printf('substitute(v:val, ''^.\{-}:'', ''%s:'', '''')', a:filename))
return join(l:errors, "\n")
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
" show_errors opens a location list and shows the given errors. If errors is
" empty, it closes the the location list.
function! go#fmt#ShowErrors(errors) abort
let l:errorformat = '%f:%l:%c:\ %m'
let l:listtype = go#list#Type("GoFmt")
if !empty(a:errors)
call go#list#Populate(l:listtype, a:errors, 'Format')
endif

call go#list#ParseFormat(l:listtype, l:errorformat, a:errors, 'Format')
let l:errors = go#list#Get(l:listtype)

" 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))
" it if there are any.
call go#list#Window(l:listtype, len(l:errors))
endfunction

function! go#fmt#ToggleFmtAutoSave() abort
Expand Down
65 changes: 65 additions & 0 deletions autoload/go/fmt_test.vim
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,71 @@ func! Test_goimports() abort
call assert_equal(expected, actual)
endfunc

func! Test_run_fmt_gopls() abort
try
let g:go_fmt_command = 'gopls'

let actual_file = tempname()
call writefile(readfile("test-fixtures/fmt/hello.go"), actual_file)

let expected = join(readfile("test-fixtures/fmt/hello_golden.go"), "\n")

" run our code
call go#fmt#run("gofmt", actual_file, "test-fixtures/fmt/hello.go")

" this should now contain the formatted code
let actual = join(readfile(actual_file), "\n")

call assert_equal(expected, actual)
finally
unlet g:go_fmt_command
endtry
endfunc

func! Test_update_file_gopls() abort
try
let g:go_fmt_command = 'gopls'

let expected = join(readfile("test-fixtures/fmt/hello_golden.go"), "\n")
let source_file = tempname()
call writefile(readfile("test-fixtures/fmt/hello_golden.go"), source_file)

let target_file = tempname()
call writefile([""], target_file)

" update_file now
call go#fmt#update_file(source_file, target_file)

" this should now contain the formatted code
let actual = join(readfile(target_file), "\n")

call assert_equal(expected, actual)
finally
unlet g:go_fmt_command
endtry
endfunc

func! Test_goimports_gopls() abort
try
let g:go_fmt_command = 'gopls'

let $GOPATH = 'test-fixtures/fmt/'
let actual_file = tempname()
call writefile(readfile("test-fixtures/fmt/src/imports/goimports.go"), actual_file)

let expected = join(readfile("test-fixtures/fmt/src/imports/goimports_golden.go"), "\n")

" run our code
call go#fmt#run("goimports", actual_file, "test-fixtures/fmt/src/imports/goimports.go")

" this should now contain the formatted code
let actual = join(readfile(actual_file), "\n")

call assert_equal(expected, actual)
finally
unlet g:go_fmt_command
endtry
endfunc
" restore Vi compatibility settings
let &cpo = s:cpo_save
unlet s:cpo_save
Expand Down
109 changes: 109 additions & 0 deletions autoload/go/lsp.vim
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,115 @@ function! go#lsp#ClearDiagnosticHighlights() abort
call go#util#ClearHighlights('goDiagnosticWarning')
endfunction

" Format formats the current buffer.
function! go#lsp#Format() abort
let l:fname = expand('%:p')
" send the current file so that TextEdits will be relative to the current
" state of the buffer.
call go#lsp#DidChange(l:fname)

let l:lsp = s:lspfactory.get()

let l:state = s:newHandlerState('format')
let l:formatHandler = go#promise#New(function('s:formatHandler', [], l:state), 10000, '')
let l:state.handleResult = l:formatHandler.wrapper
let l:state.error = l:formatHandler.wrapper
let l:state.handleError = function('s:handleFormatError', [l:fname], l:state)
let l:msg = go#lsp#message#Format(l:fname)
call l:lsp.sendMessage(l:msg, l:state)

" await the result to avoid any race conditions among autocmds (e.g.
" BufWritePre and BufWritePost)
call formatHandler.await()
endfunction

function! s:formatHandler(msg) abort dict
if a:msg is v:null
return
endif

if type(a:msg) is type('')
call self.handleError(a:msg)
return
endif

" process the TextEdit list in reverse order, because the positions are
" based on the current line numbers; processing in forward order would
" require keeping track of how the proper position of each TextEdit would be
" affected by all the TextEdits that came before.
call reverse(sort(a:msg, function('s:textEditLess')))
for l:msg in a:msg
let l:startline = l:msg.range.start.line+1
let l:endline = l:msg.range.end.line+1
let l:text = l:msg.newText

" handle the deletion of whole lines
if len(l:text) == 0 && l:msg.range.start.character == 0 && l:msg.range.end.character == 0 && l:startline < l:endline
call deletebufline('', l:startline, l:endline-1)
continue
endif

let l:startcontent = getline(l:startline)
let l:preSliceEnd = 0
if l:msg.range.start.character > 0
let l:preSliceEnd = go#lsp#lsp#PositionOf(l:startcontent, l:msg.range.start.character-1) - 1
endif

let l:endcontent = getline(l:endline)
let l:postSliceStart = 0
if l:msg.range.end.character > 0
let l:postSliceStart = go#lsp#lsp#PositionOf(l:endcontent, l:msg.range.end.character-1)
endif

" There isn't an easy way to replace the text in a byte or character
" range, so append any text on l:endline starting from l:tailidx to l:text,
" prepend any text on l:startline prior to l:prelen to l:text, and
" finally replace the lines with a delete followed by and append.
let l:text = printf('%s%s%s', l:startcontent[:l:preSliceEnd], l:text, l:endcontent[(l:postSliceStart):])

" TODO(bc): deal with the undo file
" TODO(bc): deal with folds

call execute(printf('%d,%d d_', l:startline, l:endline))
call append(l:startline-1, l:text)
endfor

call go#lsp#DidChange(expand('%:p'))
return
endfunction

function! s:handleFormatError(filename, msg) abort dict
if !go#config#FmtFailSilently()
let l:errors = split(a:msg, '\n')
let l:errors = map(l:errors, printf('substitute(v:val, ''^'', ''%s:'', '''')', a:filename))
let l:errors = join(l:errors, "\n")
call go#fmt#ShowErrors(l:errors)
endif
endfunction

function! s:textEditLess(left, right) abort
" TextEdits in a TextEdit[] never overlap and Vim's sort() is stable.
if a:left.range.start.line < a:right.range.start.line
return -1
endif

if a:left.range.start.line > a:right.range.start.line
return 1
endif

if a:left.range.start.line == a:right.range.start.line
if a:left.range.start.character < a:right.range.start.character
return -1
endif

if a:left.range.start.character > a:right.range.start.character
return 1
endif
endif

" return 0, because a:left an a:right refer to the same position.
return 0
endfunction
" restore Vi compatibility settings
let &cpo = s:cpo_save
unlet s:cpo_save
Expand Down
15 changes: 15 additions & 0 deletions autoload/go/lsp/message.vim
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ function! go#lsp#message#Shutdown() abort
\ }
endfunction

function! go#lsp#message#Format(file) abort
return {
\ 'notification': 0,
\ 'method': 'textDocument/formatting',
\ 'params': {
\ 'textDocument': {
\ 'uri': go#path#ToURI(a:file)
\ },
\ 'options': {
\ 'insertSpaces': v:false,
\ },
\ }
\ }
endfunction

function! go#lsp#message#Exit() abort
return {
\ 'notification': 1,
Expand Down
8 changes: 5 additions & 3 deletions doc/vim-go.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1342,8 +1342,8 @@ Use this option to auto |:GoFmt| on save. By default it's enabled >
<
*'g:go_fmt_command'*

Use this option to define which tool is used to gofmt. By default `gofmt` is
used.
Use this option to define which tool is used to gofmt. Valid options are
`gofmt`, `goimports`, and `gopls`. By default `gofmt` is used.
>

let g:go_fmt_command = "gofmt"
Expand Down Expand Up @@ -1393,7 +1393,9 @@ fails. By default the location list is shown. >
Use this option to enable fmt's experimental mode. This experimental mode is
superior to the current mode as it fully saves the undo history, so undo/redo
doesn't break. However, it's slow (creates/deletes a file for every save) and
it's causing problems on some Vim versions. By default it's disabled. >
it's causing problems on some Vim versions. This has no effect if
`g:go_fmt_command` is set to `gopls`. By default it's disabled.
>

let g:go_fmt_experimental = 0

Expand Down