2024-03-12 12:32:57 +08:00
"=============================================================================
" neoformat.vim --- A Neovim plugin for formatting
" Copyright (c) 2016-2021 Steve Dignam
" Copyright (c) 2022 Eric Wong
" Author: Eric Wong < wsdjeg@outlook.com >
" URL: https://spacevim.org
" License: GPLv3
"=============================================================================
2021-11-11 15:00:17 +08:00
" Set global flag to allow checking in custom user config
let g :neoformat = 1
2024-03-12 12:32:57 +08:00
" @todo update `:h neoformat`
" https://github.com/sbdchd/neoformat/blob/f1b6cd506b72be0a2aaf529105320ec929683920/doc/neoformat.txt
""
" @section Introduction, intro
" @library
" @order intro commands config adding-new-formatter managing-undo-history supported-filetypes
" A [Neovim](https://neovim.io) and Vim8 plugin for formatting code.
"
" *Neoformat* uses a variety of formatters for many filetypes. Currently, Neoformat
" will run a formatter using the current buffer data, and on success it will
" update the current buffer with the formatted text. On a formatter failure,
" Neoformat will try the next formatter defined for the filetype.
"
" By using `getbufline()` to read from the current buffer instead of file,
" Neoformat is able to format your buffer without you having to `:w` your file first.
" Also, by using `setline()`, marks, jumps, etc. are all maintained after formatting.
"
" Neoformat supports both sending buffer data to formatters via stdin, and also
" writing buffer data to `/tmp/` for formatters to read that do not support input
" via stdin.
""
" @section MANAGING UNDO HISTORY, managing-undo-history
" If you use an |autocmd| to run Neoformat on save, and you have your editor
" configured to save automatically on |CursorHold| then you might run into
" problems reverting changes. Pressing |u| will undo the last change made by
" Neoformat instead of the change that you made yourself - and then Neoformat
" will run again redoing the change that you just reverted. To avoid this
" problem you can run Neoformat with the Vim |undojoin| command to put changes
" made by Neoformat into the same |undo-block| with the preceding change. For
" example:
"
" >
" augroup fmt
" autocmd!
" autocmd BufWritePre * undojoin | Neoformat
" augroup END
" <
"
" When |undojoin| is used this way pressing |u| will "skip over" the Neoformat
" changes - it will revert both the changes made by Neoformat and the change
" that caused Neoformat to be invoked.
""
" @section ADDING A NEW FORMATTER, adding-new-formatter
" Note: you should replace everything `{{ }}` accordingly
"
" 1. Create a file in `autoload/neoformat/formatters/{{ filetype }}.vim` if it does not
" already exist for your filetype.
"
" 2. Follow the following format
"
" See Config above for options
" >
" function! neoformat#formatters#{{ filetype }}#enabled() abort
" return ['{{ formatter name }}', '{{ other formatter name for filetype }}']
" endfunction
"
" function! neoformat#formatters#{{ filetype }}#{{ formatter name }}() abort
" return {
" \ 'exe': '{{ formatter name }}',
" \ 'args': ['-s 4', '-q'],
" \ 'stdin': 1
" \ }
" endfunction
"
" function! neoformat#formatters#{{ filetype }}#{{ other formatter name }}() abort
" return {'exe': {{ other formatter name }}
" endfunction
" <
" 3. Update `README.md` and `doc/neoformat.txt`
""
" @section Configuration, config
" Define custom formatters.
"
" Options:
"
" | `exe` | the name the formatter executable in the path | required
" | `args` | list of arguments | default: [] | optional
" | `replace` | overwrite the file, instead of updating the buffer | default: 0 | optional
" | `stdin` | send data to the stdin of the formatter | default 0 | optional
" | `stderr` | used to specify whether stderr output should be read along with
" the stdin, otherwise redirects stderr to `stderr.log` file in neoformat's
" temporary directory | default 0 | optional
" | `no_append` | do not append the `path` of the file to the formatter command,
" used when the `path` is in the middle of a command | default: 0 |
" optional
" | `env` | list of environment variables to prepend to the command | default: [] | optional
"
" | `valid_exit_codes` | list of valid exit codes for formatters who do not respect common unix practices | \[0] | optional
" | `try_node_exe` | attempt to find `exe` in a `node_modules/.bin` directory in the current working directory or one of its parents (requires setting `g:neoformat_try_node_exe`) | default: 0 | optional
"
" Example:
"
" Define custom formatters.
" >
" let g:neoformat_python_autopep8 = {
" \ 'exe': 'autopep8',
" \ 'args': ['-s 4', '-E'],
" \ 'replace': 1 " replace the file, instead of updating buffer (default: 0),
" \ 'stdin': 1, " send data to stdin of formatter (default: 0)
" \ 'valid_exit_codes': [0, 23],
" \ 'no_append': 1,
" \ }
"
" let g:neoformat_enabled_python = ['autopep8']
" <
" Have Neoformat use &formatprg as a formatter
" >
" let g:neoformat_try_formatprg = 1
" <
" Enable basic formatting when a filetype is not found. Disabled by default.
" >
" " Enable alignment globally
" let g:neoformat_basic_format_align = 1
"
" " Enable tab to spaces conversion globally
" let g:neoformat_basic_format_retab = 1
"
" " Enable trimmming of trailing whitespace globally
" let g:neoformat_basic_format_trim = 1
"
" Run all enabled formatters (by default Neoformat stops after the first
" formatter succeeds)
"
" let g:neoformat_run_all_formatters = 1
"
" Above options can be activated or deactivated per buffer. For example:
"
" " runs all formatters for current buffer without tab to spaces conversion
" let b:neoformat_run_all_formatters = 1
" let b:neoformat_basic_format_retab = 0
"
" Have Neoformat only msg when there is an error
" >
" let g:neoformat_only_msg_on_error = 1
" <
" When debugging, you can enable either of following variables for extra logging.
" >
" let g:neoformat_verbose = 1 " only affects the verbosity of Neoformat
" " Or
" let &verbose = 1 " also increases verbosity of the editor as a whole
" <
" Have Neoformat look for a formatter executable in the `node_modules/.bin`
" directory in the current working directory or one of its parents (only applies
" to formatters with `try_node_exe` set to `1`):
" >
" let g:neoformat_try_node_exe = 1
" <
2020-06-13 14:06:35 +08:00
function ! neoformat #Neoformat ( bang , user_input , start_line , end_line ) abort
let view = winsaveview ( )
2021-06-18 22:42:55 +08:00
let search = @/
2020-06-13 14:06:35 +08:00
let original_filetype = &filetype
call s :neoformat ( a :bang , a :user_input , a :start_line , a :end_line )
2021-06-18 22:42:55 +08:00
" Setting &filetype might destroy existing folds, so only do that
" if the filetype got changed (which can only be possible when
" invoking with a bang)
if a :bang && &filetype ! = original_filetype
let &filetype = original_filetype
endif
2020-06-13 14:06:35 +08:00
let @/ = search
call winrestview ( view )
endfunction
function ! s :neoformat ( bang , user_input , start_line , end_line ) abort
if ! &modifiable
return neoformat #utils #warn ( 'buffer not modifiable' )
endif
let using_visual_selection = a :start_line ! = 1 | | a :end_line ! = line ( '$' )
let inputs = split ( a :user_input )
if a :bang
let &filetype = len ( inputs ) > 1 ? inputs [0 ] : a :user_input
endif
let filetype = s :split_filetypes ( &filetype )
let using_user_passed_formatter = ( ! empty ( a :user_input ) && ! a :bang )
\ | | ( len ( inputs ) > 1 && a :bang )
if using_user_passed_formatter
let formatters = len ( inputs ) > 1 ? [inputs [1 ]] : [a :user_input ]
else
let formatters = s :get_enabled_formatters ( filetype )
if formatters = = []
call neoformat #utils #msg ( 'formatter not defined for ' . filetype . ' filetype' )
return s :basic_format ( )
endif
endif
let formatters_failed = []
let formatters_changed = []
for formatter in formatters
if &formatprg ! = '' && split ( &formatprg ) [0 ] = = # formatter
\ && neoformat #utils #var ( 'neoformat_try_formatprg' )
call neoformat #utils #log ( 'using formatprg' )
let fmt_prg_def = split ( &formatprg )
let definition = {
\ 'exe' : fmt_prg_def [0 ],
\ 'args' : fmt_prg_def [1 :],
\ 'stdin' : 1 ,
\ }
elseif exists ( 'b:neoformat_' . filetype . '_' . formatter )
let definition = b :neoformat_ {filetype }_ {formatter }
elseif exists ( 'g:neoformat_' . filetype . '_' . formatter )
let definition = g :neoformat_ {filetype }_ {formatter }
elseif s :autoload_func_exists ( 'neoformat#formatters#' . filetype . '#' . formatter )
let definition = neoformat #formatters #{filetype }#{formatter }( )
else
call neoformat #utils #log ( 'definition not found for formatter: ' . formatter )
if using_user_passed_formatter
call neoformat #utils #msg ( 'formatter definition for ' . a :user_input . ' not found' )
return s :basic_format ( )
endif
continue
endif
let cmd = s :generate_cmd ( definition , filetype )
if cmd = = {}
if using_user_passed_formatter
return neoformat #utils #warn ( 'formatter ' . a :user_input . ' failed' )
endif
continue
endif
let stdin = getbufline ( bufnr ( '%' ) , a :start_line , a :end_line )
let original_buffer = getbufline ( bufnr ( '%' ) , 1 , '$' )
call neoformat #utils #log ( stdin )
call neoformat #utils #log ( cmd .exe )
if cmd .stdin
call neoformat #utils #log ( 'using stdin' )
let stdin_str = join ( stdin , "\n" )
let stdout = split ( system ( cmd .exe , stdin_str ) , '\n' )
else
call neoformat #utils #log ( 'using tmp file' )
2022-10-26 15:47:48 +08:00
if empty ( cmd .tmp_file_path )
call neoformat #utils #log ( 'tmp file name is empty, skipped!' )
return
else
call writefile ( stdin , cmd .tmp_file_path )
let stdout = split ( system ( cmd .exe ) , '\n' )
endif
2020-06-13 14:06:35 +08:00
endif
" read from /tmp file if formatter replaces file on format
2021-06-18 22:42:55 +08:00
if cmd .replace
2020-06-13 14:06:35 +08:00
let stdout = readfile ( cmd .tmp_file_path )
endif
call neoformat #utils #log ( stdout )
call neoformat #utils #log ( cmd .valid_exit_codes )
call neoformat #utils #log ( v :shell_error )
let process_ran_succesfully = index ( cmd .valid_exit_codes , v :shell_error ) ! = -1
if cmd .stderr_log ! = ''
call neoformat #utils #log ( 'stderr output redirected to file' . cmd .stderr_log )
call neoformat #utils #log_file_content ( cmd .stderr_log )
endif
if process_ran_succesfully
" 1. append the lines that are before and after the formatterd content
let lines_after = getbufline ( bufnr ( '%' ) , a :end_line + 1 , '$' )
let lines_before = getbufline ( bufnr ( '%' ) , 1 , a :start_line - 1 )
let new_buffer = lines_before + stdout + lines_after
if new_buffer ! = # original_buffer
call s :deletelines ( len ( new_buffer ) , line ( '$' ) )
2024-03-12 11:13:53 +08:00
if cmd .output_encode ! = 'utf-8'
let new_buffer = map ( new_buffer , 'iconv(v:val, "cp936", "utf-8")' )
endif
2020-06-13 14:06:35 +08:00
call setline ( 1 , new_buffer )
call add ( formatters_changed , cmd .name )
let endmsg = cmd .name . ' formatted buffer'
else
let endmsg = 'no change necessary with ' . cmd .name
endif
if ! neoformat #utils #var ( 'neoformat_run_all_formatters' )
return neoformat #utils #msg ( endmsg )
endif
call neoformat #utils #log ( 'running next formatter' )
else
call add ( formatters_failed , cmd .name )
call neoformat #utils #log ( v :shell_error )
call neoformat #utils #log ( 'trying next formatter' )
endif
endfor
if len ( formatters_failed ) > 0
2021-11-11 15:00:17 +08:00
call neoformat #utils #msg ( 'formatters ' . join ( formatters_failed , ", " ) . ' failed to run' )
2020-06-13 14:06:35 +08:00
endif
if len ( formatters_changed ) > 0
2021-11-11 15:00:17 +08:00
call neoformat #utils #msg ( join ( formatters_changed , ", " ) . ' formatted buffer' )
2020-06-13 14:06:35 +08:00
elseif len ( formatters_failed ) = = 0
call neoformat #utils #msg ( 'no change necessary' )
endif
endfunction
function ! s :get_enabled_formatters ( filetype ) abort
if &formatprg ! = '' && neoformat #utils #var ( 'neoformat_try_formatprg' )
call neoformat #utils #log ( 'adding formatprg to enabled formatters' )
let format_prg_exe = [split ( &formatprg ) [0 ]]
else
let format_prg_exe = []
endif
" Note: we append format_prg_exe to ever return as it will either be
" [], or it will be a formatter that we want to try first
if exists ( 'b:neoformat_enabled_' . a :filetype )
return format_prg_exe + b :neoformat_enabled_ {a :filetype }
elseif exists ( 'g:neoformat_enabled_' . a :filetype )
return format_prg_exe + g :neoformat_enabled_ {a :filetype }
elseif s :autoload_func_exists ( 'neoformat#formatters#' . a :filetype . '#enabled' )
return format_prg_exe + neoformat #formatters #{a :filetype }#enabled ( )
endif
return format_prg_exe
endfunction
function ! s :deletelines ( start , end ) abort
silent ! execute a :start . ',' . a :end . 'delete _'
endfunction
function ! neoformat #CompleteFormatters ( ArgLead , CmdLine , CursorPos ) abort
if a :CmdLine = ~ '!'
" 1. user inputting formatter :Neoformat! css <here>
if a :CmdLine = ~ # 'Neoformat! \S*\s'
let filetype = split ( a :CmdLine ) [1 ]
return filter ( s :get_enabled_formatters ( filetype ) ,
\ "v:val =~? '^" . a :ArgLead ."'" )
endif
" 2. user inputting filetype :Neoformat! <here>
" https://github.com/junegunn/fzf.vim/pull/110
" 1. globpath (find) all filetype files in neoformat
" 2. split at new lines
" 3. map ~/.config/nvim/plugged/neoformat/autoload/neoformat/formatters/xml.vim --> xml
" 4. sort & uniq to eliminate dupes
" 5. filter for input
return filter ( uniq ( sort ( map ( split ( globpath ( &runtimepath ,
\ 'plugged/neoformat/autoload/neoformat/formatters/*.vim' ) , '\n' ) ,
\ "fnamemodify(v:val, ':t:r')" ) ) ) ,
\ "v:val =~? '^" . a :ArgLead . "'" )
endif
2021-11-11 15:00:17 +08:00
if a :ArgLead = ~ '[^A-Za-z0-9]'
2020-06-13 14:06:35 +08:00
return []
endif
let filetype = s :split_filetypes ( &filetype )
return filter ( s :get_enabled_formatters ( filetype ) ,
\ "v:val =~? '^" . a :ArgLead ."'" )
endfunction
function ! s :autoload_func_exists ( func_name ) abort
try
call eval ( a :func_name . '()' )
catch /^Vim\%((\a\+)\)\=:E/
return 0
endtry
return 1
endfunction
function ! s :split_filetypes ( filetype ) abort
if a :filetype = = ''
return ''
endif
return split ( a :filetype , '\.' ) [0 ]
endfunction
2021-11-11 15:00:17 +08:00
function ! s :get_node_exe ( exe ) abort
let node_exe = findfile ( 'node_modules/.bin/' . a :exe , getcwd ( ) . ';' )
if ! empty ( node_exe ) && executable ( node_exe )
return node_exe
endif
return a :exe
endfunction
2020-06-13 14:06:35 +08:00
function ! s :generate_cmd ( definition , filetype ) abort
let executable = get ( a :definition , 'exe' , '' )
2024-03-12 11:13:53 +08:00
let output_encode = get ( a :definition , 'output_encode' , 'utf-8' )
2020-06-13 14:06:35 +08:00
if executable = = ''
call neoformat #utils #log ( 'no exe field in definition' )
return {}
endif
2021-06-18 22:42:55 +08:00
2021-11-11 15:00:17 +08:00
if exists ( 'g:neoformat_try_node_exe' )
\ && g :neoformat_try_node_exe
\ && get ( a :definition , 'try_node_exe' , 0 )
let executable = s :get_node_exe ( executable )
endif
if &shell = ~ '\v%(powershell|pwsh)'
if system ( '[bool](Get-Command ' . executable . ' -ErrorAction SilentlyContinue)' ) ! ~ 'True'
2021-06-18 22:42:55 +08:00
call neoformat #utils #log ( 'executable: ' . executable . ' is not a cmdlet, function, script file, or an executable program' )
return {}
endif
elseif ! executable ( executable )
2020-06-13 14:06:35 +08:00
call neoformat #utils #log ( 'executable: ' . executable . ' is not an executable' )
return {}
endif
let args = get ( a :definition , 'args' , [])
let args_expanded = []
for a in args
let args_expanded = add ( args_expanded , s :expand_fully ( a ) )
endfor
let no_append = get ( a :definition , 'no_append' , 0 )
let using_stdin = get ( a :definition , 'stdin' , 0 )
let using_stderr = get ( a :definition , 'stderr' , 0 )
let stderr_log = ''
let filename = expand ( '%:t' )
2021-11-11 15:00:17 +08:00
let tmp_dir = has ( 'win32' ) ? expand ( '$TEMP/neoformat' ) :
\ exists ( '$TMPDIR' ) ? expand ( '$TMPDIR/neoformat' ) :
2020-06-13 14:06:35 +08:00
\ '/tmp/neoformat'
if ! isdirectory ( tmp_dir )
call mkdir ( tmp_dir , 'p' )
endif
if get ( a :definition , 'replace' , 0 )
2021-11-11 15:00:17 +08:00
let path = ! using_stdin ? expand ( tmp_dir . '/' . fnameescape ( filename ) ) : ''
2020-06-13 14:06:35 +08:00
else
let path = ! using_stdin ? tempname ( ) : ''
endif
let inline_environment = get ( a :definition , 'env' , [])
let _fullcmd = join ( inline_environment , ' ' ) . ' ' . executable . ' ' . join ( args_expanded ) . ' ' . ( no_append ? '' : path )
" make sure there aren't any double spaces in the cmd
let fullcmd = join ( split ( _fullcmd ) )
if ! using_stderr
if neoformat #utils #should_be_verbose ( )
let stderr_log = expand ( tmp_dir . '/stderr.log' )
let fullcmd = fullcmd . ' 2> ' . stderr_log
else
if ( has ( 'win32' ) | | has ( 'win64' ) )
let stderr_log = ''
let fullcmd = fullcmd . ' 2> ' . 'NUL'
else
let stderr_log = ''
let fullcmd = fullcmd . ' 2> ' . '/dev/null'
endif
endif
endif
return {
\ 'exe' : fullcmd ,
\ 'stdin' : using_stdin ,
2024-03-12 11:13:53 +08:00
\ 'output_encode' : output_encode ,
2020-06-13 14:06:35 +08:00
\ 'stderr_log' : stderr_log ,
\ 'name' : a :definition .exe ,
\ 'replace' : get ( a :definition , 'replace' , 0 ) ,
\ 'tmp_file_path' : path ,
\ 'valid_exit_codes' : get ( a :definition , 'valid_exit_codes' , [0 ]) ,
\ }
endfunction
function ! s :expand_fully ( string ) abort
return substitute ( a :string , '%\(:[a-z]\)*' , '\=expand(submatch(0))' , 'g' )
endfunction
function ! s :basic_format ( ) abort
call neoformat #utils #log ( 'running basic format' )
if ! exists ( 'g:neoformat_basic_format_align' )
let g :neoformat_basic_format_align = 0
endif
if ! exists ( 'g:neoformat_basic_format_retab' )
let g :neoformat_basic_format_retab = 0
endif
if ! exists ( 'g:neoformat_basic_format_trim' )
let g :neoformat_basic_format_trim = 0
endif
if neoformat #utils #var ( 'neoformat_basic_format_align' )
call neoformat #utils #log ( 'aligning with basic formatter' )
let v = winsaveview ( )
silent ! execute 'normal gg=G'
call winrestview ( v )
endif
if neoformat #utils #var ( 'neoformat_basic_format_retab' )
call neoformat #utils #log ( 'converting tabs with basic formatter' )
retab
endif
if neoformat #utils #var ( 'neoformat_basic_format_trim' )
call neoformat #utils #log ( 'trimming whitespace with basic formatter' )
" http://stackoverflow.com/q/356126
let search = @/
let view = winsaveview ( )
" vint: -ProhibitCommandRelyOnUser -ProhibitCommandWithUnintendedSideEffect
silent ! %s /\s\+$/ /e
" vint: +ProhibitCommandRelyOnUser +ProhibitCommandWithUnintendedSideEffect
let @/= search
call winrestview ( view )
endif
endfunction
2024-03-12 12:32:57 +08:00
""
" @section Supported filetypes, supported-filetypes
" This is a list of default formatters.