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

Try to use formatters for subfiletypes #380

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
192 changes: 107 additions & 85 deletions autoload/neoformat.vim
Original file line number Diff line number Diff line change
Expand Up @@ -31,117 +31,131 @@ function! s:neoformat(bang, user_input, start_line, end_line) abort
let &filetype = len(inputs) > 1 ? inputs[0] : a:user_input
endif

let filetype = s:split_filetypes(&filetype)

let filetypes = 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]
let filetype = s:split_filetype(&filetype)
let formatters = len(inputs) > 1 ? [[filetype, [inputs[1]]]] : [[filetype, [a:user_input]]]
else
let formatters = s:get_enabled_formatters(filetype)
if formatters == []
call neoformat#utils#msg('formatter not defined for ' . filetype . ' filetype')
let slice = 0
let use_subfiletype_formatters = neoformat#utils#var('neoformat_all_subfiletypes_formatters')
if use_subfiletype_formatters
let slice = -1
endif
let formatters = []
let formatters_len = 0
for filetype in filetypes[:slice]
let filetype_formatters = s:get_enabled_formatters(filetype)
let formatters_len += len(filetype_formatters)
call add(formatters, [filetype, filetype_formatters])
endfor
if formatters_len == 0
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')
for filetype_formatters in formatters
let filetype = filetype_formatters[0]
Copy link
Owner

Choose a reason for hiding this comment

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

I'm wondering if we should enable multiple file formatters conditionally

My concern is that someone could have a filetype like typescript.jsx or something and the first one works but the second one fails

or maybe the two formatters are incompatible and there is fighting between them

I'm curious what typical multi-file filetypes are common in the ecosystem

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's hard to tell. Generally I would expect that if filetype is typescript.jsx then formatters for all filetypes should work correctly. In the same vein, it's uncertain whether all typescript formatters can handle jsx within file (or any other subfiletype that I might have invented), and yet currently NeoFormat will treat it as valid typescript file, which it might be not. I think it's generally a good assumption.

I think NeoFormat already has mechanisms to fulfil most common scenarios. User can specify per filetype whether one or more formatters should be used, and it is also possible to disable formatter. If I added option to disable search for subfiletype formatters with global or buffer level variable, combination of these three variables should be flexible enough to enable or disable this feature for specific filetypes. It could be disabled by default to maintain existing behaviour.
It's worth considering if NeoFormat should make additional checks like g:neoformat_enabled_typescript_jsx = ['prettier']. I think that is not necessary, it's very much possible to achieve that by setting b:neoformat_enabled_filetype in filetype plugin.

I wonder whether formatters should be de-duplicated. Let's stick to typescript.jsx, and suppose both filetypes have prettier definition. Should I run only first formatter, or run prettier twice? Perhaps prettier definition for jsx is different, e.g. different flags are passed to the command, or user has different settings for jsx prettier formatter, let say they wish different indentation width. This scenario could also be handled with proper setting enabled formatters in filetype plugin, though.

In conclusion, I think a variable to enable this behaviour should be sufficient. Disabling it by default will prevent users from stumbling upon weird combinations that wouldn't work without some manual tweaking.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sbdchd So, what do you think about that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, so I added option to enable using formatters for all subfiletypes. By default, Neoformat works as before, only first subfiletype is being considered.

for formatter in filetype_formatters[1]

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()
return s:basic_format()
endif
continue
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')
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
continue
endif

let stdin = getbufline(bufnr('%'), a:start_line, a:end_line)
let original_buffer = getbufline(bufnr('%'), 1, '$')
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(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')
call writefile(stdin, cmd.tmp_file_path)
let stdout = split(system(cmd.exe), '\n')
endif
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')
call writefile(stdin, cmd.tmp_file_path)
let stdout = split(system(cmd.exe), '\n')
endif

" read from /tmp file if formatter replaces file on format
if cmd.replace
let stdout = readfile(cmd.tmp_file_path)
endif
" read from /tmp file if formatter replaces file on format
if cmd.replace
let stdout = readfile(cmd.tmp_file_path)
endif

call neoformat#utils#log(stdout)
call neoformat#utils#log(stdout)

call neoformat#utils#log(cmd.valid_exit_codes)
call neoformat#utils#log(v:shell_error)
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
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)
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
let new_buffer = lines_before + stdout + lines_after
if new_buffer !=# original_buffer

call s:deletelines(len(new_buffer), line('$'))
call s:deletelines(len(new_buffer), line('$'))

call setline(1, new_buffer)
call setline(1, new_buffer)

call add(formatters_changed, cmd.name)
let endmsg = cmd.name . ' formatted buffer'
else
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)
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
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
endfor
if len(formatters_failed) > 0
call neoformat#utils#msg('formatters ' . join(formatters_failed, ", ") . ' failed to run')
Expand Down Expand Up @@ -202,7 +216,7 @@ function! neoformat#CompleteFormatters(ArgLead, CmdLine, CursorPos) abort
if a:ArgLead =~ '[^A-Za-z0-9]'
return []
endif
let filetype = s:split_filetypes(&filetype)
let filetype = s:split_filetype(&filetype)
return filter(s:get_enabled_formatters(filetype),
\ "v:val =~? '^" . a:ArgLead ."'")
endfunction
Expand All @@ -216,11 +230,19 @@ function! s:autoload_func_exists(func_name) abort
return 1
endfunction

function! s:split_filetype(filetype) abort
let filetypes = s:split_filetypes(a:filetype)
if filetypes == []
return ''
endif
return filetypes[0]
endfunction

function! s:split_filetypes(filetype) abort
if a:filetype == ''
return ''
return []
endif
return split(a:filetype, '\.')[0]
return split(a:filetype, '\.')
endfunction

function! s:get_node_exe(exe) abort
Expand Down
10 changes: 9 additions & 1 deletion doc/neoformat.txt
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,19 @@ formatter succeeds)

let g:neoformat_run_all_formatters = 1

Search for formatter definitions for all subfiletypes (by default Neoformat
searches only for the first subfiletype, e.g. `typescript` if `filetype`
is `typescript.jsx`)

let g:neoformat_all_subfiletypes_formatters = 1

Above options can be activated or deactivated per buffer. For example:

" runs all formatters for current buffer without tab to spaces conversion
" runs all formatters for all subfitypes for current buffer without tab to
" spaces conversion
let b:neoformat_run_all_formatters = 1
let b:neoformat_basic_format_retab = 0
let b:neoformat_all_subfiletypes_formatters = 1

Have Neoformat only msg when there is an error
>
Expand Down