├── demo.gif ├── LICENSE ├── README.md ├── doc └── mix-format.txt └── ftplugin └── elixir.vim /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhinz/vim-mix-format/HEAD/demo.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present, Marco Hinz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vim-mix-format 2 | 3 | Elixir 1.6 introduced the formatter: `mix format`. This plugin makes it easy to 4 | run the formatter asynchronously from within Vim 8 and Neovim. 5 | 6 | ![demo](demo.gif) 7 | 8 | ## Installation 9 | 10 | Use your [favorite plugin manager](https://github.com/mhinz/vim-galore#managing-plugins), e.g. 11 | [vim-plug](https://github.com/junegunn/vim-plug): 12 | 13 | Plug 'mhinz/vim-mix-format' 14 | 15 | ## Commands 16 | 17 | * To format the current file, use `:MixFormat`. Use `:verb MixFormat` to see the 18 | exact shell command used. 19 | 20 | * The formatter is not perfect yet, so `:MixFormatDiff` will open a diff window 21 | that can be used for previewing the changes or picking only those that seem 22 | reasonable. 23 | 24 | `dp` pushes changes from the diff window to the source file. `q` closes the diff 25 | window. `]c` and `[c` jump between the changes. 26 | 27 | If you're not used to Vim's diff mode, [watch this 28 | screencast](http://vimcasts.org/episodes/comparing-buffers-with-vimdiff). 29 | 30 | ## Options 31 | 32 | * Automatically format on saving. 33 | 34 | ```vim 35 | let g:mix_format_on_save = 1 36 | ``` 37 | 38 | * Set options for the formatter. See `mix help format` in the shell. 39 | 40 | ```vim 41 | let g:mix_format_options = '--check-equivalent' 42 | ``` 43 | 44 | * By default this plugin opens a window containing the stacktrace on errors. 45 | With this option enabled, there will be just a short message in the 46 | command-line bar. The stacktrace can still be looked up via `:messages`. 47 | 48 | ```vim 49 | let g:mix_format_silent_errors = 1 50 | ``` 51 | 52 | * If you're not using Elixir 1.6 in your project, but want to use the formatter 53 | anyway, you can specify the bin directory of an alternative Elixir installation: 54 | 55 | ```vim 56 | let g:mix_format_elixir_bin_path = '~/repo/elixir/bin' 57 | ``` 58 | 59 | ## Customization 60 | 61 | When using `:MixFormatDiff`, a new diff window will be opened and an user event 62 | is emitted. It can be used to set different settings or switch back to the 63 | source window: 64 | 65 | ```vim 66 | autocmd User MixFormatDiff wincmd p 67 | ``` 68 | 69 | ## Feedback 70 | 71 | If you like this plugin, star it! It helps me deciding which projects to spend 72 | more time on. 73 | -------------------------------------------------------------------------------- /doc/mix-format.txt: -------------------------------------------------------------------------------- 1 | *mix-format.txt* Elixir formatter integration. 2 | *mix-format* 3 | 4 | by Marco Hinz~ 5 | 6 | Twitter: https://twitter.com/_mhinz_ 7 | Github: http://github.com/mhinz 8 | > 9 | If you use any of my plugins, please star them on github. It's a great way 10 | of getting feedback and gives me the kick to put more time into their 11 | development. 12 | < 13 | ============================================================================== 14 | COMMANDS *mix-format-commands* 15 | 16 | * To format the current file, use `:MixFormat`. 17 | 18 | * The formatter is not perfect yet, so `:MixFormatDiff` will open a diff 19 | window that can be used for previewing the changes or picking only those 20 | that seem reasonable. 21 | 22 | |dp| pushes changes from the diff window to the source file. `q` closes the 23 | diff window. |]c| and |[c| jump between the changes. 24 | 25 | If you're not used to Vim's diff mode, watch this screencast: 26 | 27 | http://vimcasts.org/episodes/comparing-buffers-with-vimdiff 28 | 29 | ============================================================================== 30 | OPTIONS *mix-format-options* 31 | 32 | * Automatically format on saving. 33 | > 34 | let g:mix_format_on_save = 1 35 | < 36 | * Set options for the formatter. See `mix help format` in the shell. 37 | > 38 | let g:mix_format_options = '--check-equivalent' 39 | < 40 | * By default this plugin creates a new |quickfix| list on errors. 41 | With this option enabled, there will be just a short message in the 42 | command-line bar. The stacktrace can still be looked up via `:messages`. 43 | > 44 | let g:mix_format_silent_errors = 1 45 | < 46 | * If you're not using Elixir 1.6 in your project, but want to use the 47 | formatter anyway, you can specify the bin directory of an alternative Elixir 48 | installation: 49 | > 50 | let g:mix_format_elixir_bin_path = '~/repo/elixir/bin' 51 | < 52 | ============================================================================== 53 | CUSTOMIZATION *mix-format-customization* 54 | 55 | When using `:MixFormatDiff`, a new diff window will be opened and an user event 56 | is emitted. It can be used to set different settings or switch back to the 57 | source window: 58 | > 59 | autocmd User MixFormatDiff wincmd p 60 | < 61 | ============================================================================== 62 | vim: tw=78 63 | -------------------------------------------------------------------------------- /ftplugin/elixir.vim: -------------------------------------------------------------------------------- 1 | if exists('b:loaded_mix_format') 2 | \ || &filetype != 'elixir' 3 | \ || &compatible 4 | finish 5 | endif 6 | 7 | " Is 'cwd' key for job_start() options available? 8 | let s:has_cwd = has('nvim') || has('patch-8.0.902') 9 | 10 | if !exists('g:mix_format_env_cmd') 11 | " Workaround for https://github.com/mhinz/vim-mix-format/issues/15 12 | let g:mix_format_env_cmd = executable('env') ? ['env', '-u', 'MIX_ENV'] : [] 13 | endif 14 | 15 | function! s:msg(show, msg) abort 16 | if a:show 17 | echomsg 'MixFormat: '. a:msg 18 | endif 19 | endfunction 20 | 21 | function! s:on_stdout_nvim(_job, data, _event) dict abort 22 | if empty(a:data[-1]) 23 | " Second-last item is the last complete line in a:data. 24 | let self.stdout += self.stdoutbuf + a:data[:-2] 25 | let self.stdoutbuf = [] 26 | else 27 | if empty(self.stdoutbuf) 28 | " Last item in a:data is an incomplete line. Put into buffer. 29 | let self.stdoutbuf = [remove(a:data, -1)] 30 | let self.stdout += a:data 31 | else 32 | " Last item in a:data is an incomplete line. Append to buffer. 33 | let self.stdoutbuf = self.stdoutbuf[:-2] 34 | \ + [self.stdoutbuf[-1] . get(a:data, 0, '')] 35 | \ + a:data[1:] 36 | endif 37 | endif 38 | endfunction 39 | 40 | function! s:on_stdout_vim(_job, data) dict abort 41 | let self.stdout += [a:data] 42 | endfunction 43 | 44 | function! s:on_exit(_job, exitval, ...) dict abort 45 | let source_win_id = win_getid() 46 | call win_gotoid(self.win_id) 47 | 48 | if !s:has_cwd 49 | call s:msg(self.verbose, 'Changing to: '. self.origdir) 50 | execute 'cd' fnameescape(self.origdir) 51 | endif 52 | 53 | if filereadable(self.undofile) 54 | execute 'silent rundo' self.undofile 55 | call s:msg(self.verbose, 'Deleting undo file: '. self.undofile) 56 | call delete(self.undofile) 57 | endif 58 | 59 | if a:exitval && get(g:, 'mix_format_silent_errors') 60 | for line in self.stdout 61 | echomsg line 62 | endfor 63 | redraw | echohl ErrorMsg | echo 'Formatting failed. Check :messages.' | echohl NONE 64 | return 65 | end 66 | 67 | let old_efm = &errorformat 68 | let &errorformat = '%-Gmix format failed%.%#' 69 | let &errorformat .= ',** (%.%#) %f:%l: %m' 70 | lgetexpr self.stdout 71 | let &errorformat = old_efm 72 | lwindow 73 | if &buftype == 'quickfix' 74 | let w:quickfix_title = s:build_cmd(fnamemodify(self.origfile, ':.')) 75 | endif 76 | 77 | if a:exitval 78 | redraw | echohl ErrorMsg | echo 'Formatting failed.' | echohl NONE 79 | return 80 | endif 81 | 82 | if self.diffmode 83 | call system(printf('diff %s %s', self.origfile, self.difffile)) 84 | if !v:shell_error 85 | echomsg 'No formatting issues found.' 86 | if +get(g:, 'mix_format_diff_win_id') 87 | let winnr = win_id2win(g:mix_format_diff_win_id) 88 | if winnr 89 | execute winnr 'close' 90 | endif 91 | endif 92 | return 93 | endif 94 | else 95 | let [sol, ur] = [&startofline, &undoreload] 96 | let [&startofline, &undoreload] = [0, 10000] 97 | mkview 98 | try 99 | silent edit! 100 | finally 101 | let [&startofline, &undoreload] = [sol, ur] 102 | loadview 103 | endtry 104 | call win_gotoid(source_win_id) 105 | return 106 | end 107 | 108 | diffthis 109 | 110 | if +get(g:, 'mix_format_diff_win_id') && win_gotoid(g:mix_format_diff_win_id) 111 | %delete 112 | else 113 | rightbelow vnew 114 | let g:mix_format_diff_win_id = win_getid() 115 | set buftype=nofile nobuflisted bufhidden=wipe 116 | runtime syntax/elixir.vim 117 | endif 118 | 119 | execute 'silent read' fnameescape(self.difffile) 120 | call s:msg(self.verbose, 'Deleting diff file: '. self.difffile) 121 | silent! call delete(self.difffile) 122 | silent 0delete _ 123 | diffthis 124 | normal! ]c 125 | 126 | nnoremap q :close 127 | augroup mix_format_diff 128 | autocmd! 129 | autocmd BufWipeout silent diffoff! 130 | augroup END 131 | 132 | if exists('#User#MixFormatDiff') 133 | doautocmd User MixFormatDiff 134 | endif 135 | endfunction 136 | 137 | function! s:get_cmd_from_file(filename) abort 138 | let cmd = s:build_cmd(a:filename) 139 | if has('win32') && &shell =~ 'cmd' 140 | return 'cmd /c '. cmd 141 | endif 142 | return g:mix_format_env_cmd + ['sh', '-c', cmd] 143 | endfunction 144 | 145 | function! s:build_cmd(filename) abort 146 | let elixir_bin_path = get(g:, 'mix_format_elixir_bin_path') 147 | let options = get(g:, 'mix_format_options', '--check-equivalent') 148 | 149 | let [shellslash, &shellslash] = [&shellslash, 0] 150 | let dot_formatter = findfile('.formatter.exs', expand('%:p:h').';') 151 | if !empty(dot_formatter) 152 | let options .= ' --dot-formatter '. shellescape(fnamemodify(dot_formatter, ':p')) 153 | endif 154 | let filename = shellescape(a:filename) 155 | let &shellslash = shellslash 156 | 157 | if empty(elixir_bin_path) 158 | return printf('mix format %s %s', options, filename) 159 | endif 160 | 161 | return printf('%s %s %s %s', 162 | \ elixir_bin_path .'/elixir', 163 | \ elixir_bin_path .'/mix format', 164 | \ options, 165 | \ filename) 166 | endfunction 167 | 168 | function! s:mix_format(diffmode) abort 169 | if &modified 170 | redraw | echohl WarningMsg | echo 'Unsaved buffer. Quitting.' | echohl NONE 171 | return 172 | endif 173 | 174 | let origdir = getcwd() 175 | 176 | let mixfile = findfile('mix.exs', expand('%:p:h').';') 177 | if empty(mixfile) 178 | call s:msg(&verbose, 'No mix project found.') 179 | else 180 | let mixroot = fnamemodify(mixfile, ':h') 181 | if !s:has_cwd 182 | call s:msg(&verbose, 'Changing to: '. mixroot) 183 | execute 'cd' fnameescape(mixroot) 184 | endif 185 | endif 186 | 187 | let origfile = expand('%:p') 188 | 189 | if a:diffmode 190 | let difffile = tempname() 191 | call s:msg(&verbose, 'Creating diff file: '. difffile) 192 | execute 'silent write' fnameescape(difffile) 193 | else 194 | let difffile = origfile 195 | endif 196 | let cmd = s:get_cmd_from_file(difffile) 197 | 198 | let undofile = tempname() 199 | call s:msg(&verbose, 'Creating undo file: '. undofile) 200 | execute 'silent wundo!' undofile 201 | 202 | let options = { 203 | \ 'cmd': type(cmd) == type([]) ? join(cmd) : cmd, 204 | \ 'diffmode': a:diffmode, 205 | \ 'origdir': origdir, 206 | \ 'origfile': origfile, 207 | \ 'difffile': difffile, 208 | \ 'undofile': undofile, 209 | \ 'win_id': win_getid(), 210 | \ 'verbose': &verbose, 211 | \ 'stdout': [], 212 | \ 'stdoutbuf': [], 213 | \ } 214 | 215 | if s:has_cwd && exists('mixroot') 216 | let options.cwd = mixroot 217 | endif 218 | 219 | call s:msg(&verbose, type(cmd) == type([]) ? string(cmd) : cmd) 220 | 221 | if has('nvim') 222 | silent! call jobstop(s:id) 223 | let s:id = jobstart(cmd, extend(options, { 224 | \ 'on_stdout': function('s:on_stdout_nvim'), 225 | \ 'on_stderr': function('s:on_stdout_nvim'), 226 | \ 'on_exit': function('s:on_exit'), 227 | \ 'detach': !has('nvim-0.3.6'), 228 | \ })) 229 | else 230 | silent! call job_stop(s:id) 231 | let s:id = job_start(cmd, extend({ 232 | \ 'in_io': 'null', 233 | \ 'err_io': 'out', 234 | \ 'out_cb': function('s:on_stdout_vim', options), 235 | \ 'exit_cb': function('s:on_exit', options), 236 | \ }, has_key(options, 'cwd') ? {'cwd': options.cwd} : {})) 237 | endif 238 | endfunction 239 | 240 | command! -buffer -bar MixFormat call mix_format(0+'diffmode') 241 | command! -buffer -bar MixFormatDiff call mix_format(1+'diffmode') 242 | 243 | if get(g:, 'mix_format_on_save') 244 | augroup mix_format 245 | autocmd BufWritePre noautocmd silent update | call s:mix_format(0+'diffmode') 246 | augroup END 247 | endif 248 | 249 | let b:loaded_mix_format = 1 250 | --------------------------------------------------------------------------------