fmt.vim 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. " Copyright 2011 The Go Authors. All rights reserved.
  2. " Use of this source code is governed by a BSD-style
  3. " license that can be found in the LICENSE file.
  4. "
  5. " fmt.vim: Vim command to format Go files with gofmt (and gofmt compatible
  6. " toorls, such as goimports).
  7. if !exists("g:go_fmt_command")
  8. let g:go_fmt_command = "gofmt"
  9. endif
  10. if !exists('g:go_fmt_options')
  11. let g:go_fmt_options = ''
  12. endif
  13. if !exists('g:go_fmt_fail_silently')
  14. let g:go_fmt_fail_silently = 0
  15. endif
  16. if !exists("g:go_fmt_experimental")
  17. let g:go_fmt_experimental = 0
  18. endif
  19. " we have those problems :
  20. " http://stackoverflow.com/questions/12741977/prevent-vim-from-updating-its-undo-tree
  21. " http://stackoverflow.com/questions/18532692/golang-formatter-and-vim-how-to-destroy-history-record?rq=1
  22. "
  23. " The below function is an improved version that aims to fix all problems.
  24. " it doesn't undo changes and break undo history. If you are here reading
  25. " this and have VimL experience, please look at the function for
  26. " improvements, patches are welcome :)
  27. function! go#fmt#Format(withGoimport) abort
  28. if g:go_fmt_experimental == 1
  29. " Using winsaveview to save/restore cursor state has the problem of
  30. " closing folds on save:
  31. " https://github.com/fatih/vim-go/issues/502
  32. " One fix is to use mkview instead. Unfortunately, this sometimes causes
  33. " other bad side effects:
  34. " https://github.com/fatih/vim-go/issues/728
  35. " and still closes all folds if foldlevel>0:
  36. " https://github.com/fatih/vim-go/issues/732
  37. let l:curw = {}
  38. try
  39. mkview!
  40. catch
  41. let l:curw = winsaveview()
  42. endtry
  43. " save our undo file to be restored after we are done. This is needed to
  44. " prevent an additional undo jump due to BufWritePre auto command and also
  45. " restore 'redo' history because it's getting being destroyed every
  46. " BufWritePre
  47. let tmpundofile = tempname()
  48. exe 'wundo! ' . tmpundofile
  49. else
  50. " Save cursor position and many other things.
  51. let l:curw = winsaveview()
  52. endif
  53. " Write current unsaved buffer to a temp file
  54. let l:tmpname = tempname() . '.go'
  55. call writefile(go#util#GetLines(), l:tmpname)
  56. if go#util#IsWin()
  57. let l:tmpname = tr(l:tmpname, '\', '/')
  58. endif
  59. let bin_name = g:go_fmt_command
  60. if a:withGoimport == 1
  61. let bin_name = "goimports"
  62. endif
  63. let current_col = col('.')
  64. let out = go#fmt#run(bin_name, l:tmpname, expand('%'))
  65. let diff_offset = len(readfile(l:tmpname)) - line('$')
  66. if go#util#ShellError() == 0
  67. call go#fmt#update_file(l:tmpname, expand('%'))
  68. elseif g:go_fmt_fail_silently == 0
  69. let errors = s:parse_errors(expand('%'), out)
  70. call s:show_errors(errors)
  71. endif
  72. " We didn't use the temp file, so clean up
  73. call delete(l:tmpname)
  74. if g:go_fmt_experimental == 1
  75. " restore our undo history
  76. silent! exe 'rundo ' . tmpundofile
  77. call delete(tmpundofile)
  78. " Restore our cursor/windows positions, folds, etc.
  79. if empty(l:curw)
  80. silent! loadview
  81. else
  82. call winrestview(l:curw)
  83. endif
  84. else
  85. " Restore our cursor/windows positions.
  86. call winrestview(l:curw)
  87. endif
  88. " be smart and jump to the line the new statement was added/removed
  89. call cursor(line('.') + diff_offset, current_col)
  90. " Syntax highlighting breaks less often.
  91. syntax sync fromstart
  92. endfunction
  93. " update_file updates the target file with the given formatted source
  94. function! go#fmt#update_file(source, target)
  95. " remove undo point caused via BufWritePre
  96. try | silent undojoin | catch | endtry
  97. let old_fileformat = &fileformat
  98. if exists("*getfperm")
  99. " save file permissions
  100. let original_fperm = getfperm(a:target)
  101. endif
  102. call rename(a:source, a:target)
  103. " restore file permissions
  104. if exists("*setfperm") && original_fperm != ''
  105. call setfperm(a:target , original_fperm)
  106. endif
  107. " reload buffer to reflect latest changes
  108. silent edit!
  109. let &fileformat = old_fileformat
  110. let &syntax = &syntax
  111. let l:listtype = go#list#Type("GoFmt")
  112. " the title information was introduced with 7.4-2200
  113. " https://github.com/vim/vim/commit/d823fa910cca43fec3c31c030ee908a14c272640
  114. if has('patch-7.4.2200')
  115. " clean up previous list
  116. if l:listtype == "quickfix"
  117. let l:list_title = getqflist({'title': 1})
  118. else
  119. let l:list_title = getloclist(0, {'title': 1})
  120. endif
  121. else
  122. " can't check the title, so assume that the list was for go fmt.
  123. let l:list_title = {'title': 'Format'}
  124. endif
  125. if has_key(l:list_title, "title") && l:list_title['title'] == "Format"
  126. call go#list#Clean(l:listtype)
  127. endif
  128. endfunction
  129. " run runs the gofmt/goimport command for the given source file and returns
  130. " the output of the executed command. Target is the real file to be formatted.
  131. function! go#fmt#run(bin_name, source, target)
  132. let cmd = s:fmt_cmd(a:bin_name, a:source, a:target)
  133. if empty(cmd)
  134. return
  135. endif
  136. let command = join(cmd, " ")
  137. " execute our command...
  138. let out = go#util#System(command)
  139. return out
  140. endfunction
  141. " fmt_cmd returns a dict that contains the command to execute gofmt (or
  142. " goimports). args is dict with
  143. function! s:fmt_cmd(bin_name, source, target)
  144. " check if the user has installed command binary.
  145. " For example if it's goimports, let us check if it's installed,
  146. " if not the user get's a warning via go#path#CheckBinPath()
  147. let bin_path = go#path#CheckBinPath(a:bin_name)
  148. if empty(bin_path)
  149. return []
  150. endif
  151. " start constructing the command
  152. let bin_path = go#util#Shellescape(bin_path)
  153. let cmd = [bin_path]
  154. call add(cmd, "-w")
  155. " add the options for binary (if any). go_fmt_options was by default of type
  156. " string, however to allow customization it's now a dictionary of binary
  157. " name mapping to options.
  158. let opts = g:go_fmt_options
  159. if type(g:go_fmt_options) == type({})
  160. let opts = has_key(g:go_fmt_options, a:bin_name) ? g:go_fmt_options[a:bin_name] : ""
  161. endif
  162. call extend(cmd, split(opts, " "))
  163. if a:bin_name == "goimports"
  164. " lazy check if goimports support `-srcdir`. We should eventually remove
  165. " this in the future
  166. if !exists('b:goimports_vendor_compatible')
  167. let out = go#util#System(bin_path . " --help")
  168. if out !~ "-srcdir"
  169. call go#util#EchoWarning(printf("vim-go: goimports (%s) does not support srcdir. Update with: :GoUpdateBinaries", bin_path))
  170. else
  171. let b:goimports_vendor_compatible = 1
  172. endif
  173. endif
  174. if exists('b:goimports_vendor_compatible') && b:goimports_vendor_compatible
  175. let ssl_save = &shellslash
  176. set noshellslash
  177. " use the filename without the fully qualified name if the tree is
  178. " symlinked into the GOPATH, goimports won't work properly.
  179. call extend(cmd, ["-srcdir", shellescape(a:target)])
  180. let &shellslash = ssl_save
  181. endif
  182. endif
  183. call add(cmd, a:source)
  184. return cmd
  185. endfunction
  186. " parse_errors parses the given errors and returns a list of parsed errors
  187. function! s:parse_errors(filename, content) abort
  188. let splitted = split(a:content, '\n')
  189. " list of errors to be put into location list
  190. let errors = []
  191. for line in splitted
  192. let tokens = matchlist(line, '^\(.\{-}\):\(\d\+\):\(\d\+\)\s*\(.*\)')
  193. if !empty(tokens)
  194. call add(errors,{
  195. \"filename": a:filename,
  196. \"lnum": tokens[2],
  197. \"col": tokens[3],
  198. \"text": tokens[4],
  199. \ })
  200. endif
  201. endfor
  202. return errors
  203. endfunction
  204. " show_errors opens a location list and shows the given errors. If the given
  205. " errors is empty, it closes the the location list
  206. function! s:show_errors(errors) abort
  207. let l:listtype = go#list#Type("GoFmt")
  208. if !empty(a:errors)
  209. call go#list#Populate(l:listtype, a:errors, 'Format')
  210. echohl Error | echomsg "Gofmt returned error" | echohl None
  211. endif
  212. " this closes the window if there are no errors or it opens
  213. " it if there is any
  214. call go#list#Window(l:listtype, len(a:errors))
  215. endfunction
  216. function! go#fmt#ToggleFmtAutoSave() abort
  217. if get(g:, "go_fmt_autosave", 1)
  218. let g:go_fmt_autosave = 0
  219. call go#util#EchoProgress("auto fmt disabled")
  220. return
  221. end
  222. let g:go_fmt_autosave = 1
  223. call go#util#EchoProgress("auto fmt enabled")
  224. endfunction
  225. " vim: sw=2 ts=2 et