vim-codefmt/autoload/codefmt.vim
Ari Archer d8b28f0bb4
Merge https://github.com/google/vim-codefmt/pull/154
Signed-off-by: Ari Archer <truncateddinosour@gmail.com>
2022-01-15 21:01:11 +02:00

311 lines
11 KiB
VimL

" Copyright 2017 Google Inc. All rights reserved.
"
" Licensed under the Apache License, Version 2.0 (the "License");
" you may not use this file except in compliance with the License.
" You may obtain a copy of the License at
"
" http://www.apache.org/licenses/LICENSE-2.0
"
" Unless required by applicable law or agreed to in writing, software
" distributed under the License is distributed on an "AS IS" BASIS,
" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
" See the License for the specific language governing permissions and
" limitations under the License.
let s:plugin = maktaba#plugin#Get('codefmt')
let s:registry = s:plugin.GetExtensionRegistry()
""
" @dict Formatter
" Interface for applying formatting to lines of code. Formatters are
" registered with codefmt using maktaba's standard extension registry:
" >
" let l:codefmt_registry = maktaba#extension#GetRegistry('codefmt')
" call l:codefmt_registry.AddExtension(l:formatter)
" <
"
" Formatters define these fields:
" * name (string): The formatter name that will be exposed to users.
" * setup_instructions (string, optional): A string explaining to users how to
" make the plugin available if not already available.
" and these functions:
" * IsAvailable() -> boolean: Whether the formatter is fully functional with
" all dependencies available. Returns 0 only if setup_instructions have not
" been followed.
" * AppliesToBuffer() -> boolean: Whether the current buffer is of a type
" normally formatted by this formatter. Normally based on 'filetype', but
" could depend on buffer name or other properties.
" and should implement at least one of the following functions:
" * Format(): Formats the current buffer directly.
" * FormatRange({startline}, {endline}): Formats the current buffer, focusing
" on the range of lines from {startline} to {endline}.
" * FormatRanges({ranges}): Formats the current buffer, focusing on the given
" ranges of lines. Each range should be a 2-item list of
" [startline,endline].
" Formatters should implement the most specific format method that is supported.
""
" @private
" Ensures that {formatter} is a valid formatter, and then prepares it for use by
" codefmt. See @dict(Formatter) for the API {formatter} must implement.
" @throws BadValue if {formatter} is missing required fields.
" Returns the fully prepared formatter.
function! codefmt#EnsureFormatter(formatter) abort
let l:required_fields = ['name', 'IsAvailable', 'AppliesToBuffer']
" Throw BadValue if any required fields are missing.
let l:missing_fields =
\ filter(copy(l:required_fields), '!has_key(a:formatter, v:val)')
if !empty(l:missing_fields)
throw maktaba#error#BadValue('a:formatter is missing fields: ' .
\ join(l:missing_fields, ', '))
endif
" Throw BadValue if the wrong number of format functions are provided.
let l:available_format_functions = ['Format', 'FormatRange', 'FormatRanges']
let l:format_functions = filter(copy(l:available_format_functions),
\ 'has_key(a:formatter, v:val)')
if empty(l:format_functions)
throw maktaba#error#BadValue('Formatter ' . a:formatter.name .
\ ' has no format functions. It must have at least one of ' .
\ join(l:available_format_functions, ', '))
endif
" TODO(dbarnett): Check types.
endfunction
""
" Checks whether {formatter} is available.
" NOTE: If IsAvailable checks are disabled via
" @function(#SetWhetherToPerformIsAvailableChecksForTesting), skips the
" IsAvailable check and always returns true.
function! s:IsAvailable(formatter) abort
if !codefmt#ShouldPerformIsAvailableChecks()
return 1
endif
return a:formatter.IsAvailable()
endfunction
" Checks whether {formatter} is available, safely handling errors by logging
" an error and returning 0.
function! s:IsAvailableSafe(formatter) abort
try
return s:IsAvailable(a:formatter)
catch /.*/
call maktaba#error#Shout(
\ 'Failed to evaluate whether formatter %s is available: %s',
\ a:formatter.name,
\ v:exception)
return 0
endtry
endfunction
""
" Detects whether a formatter has been defined for the current buffer/filetype.
function! codefmt#IsFormatterAvailable() abort
if !empty(get(b:, 'codefmt_formatter'))
return 1
endif
for l:formatter in s:registry.GetExtensions()
if l:formatter.AppliesToBuffer() && s:IsAvailableSafe(l:formatter)
return 1
endif
endfor
return 0
endfunction
function! s:GetSetupInstructions(formatter) abort
let l:error = 'Formatter "'. a:formatter.name . '" is not available.'
if has_key(a:formatter, 'setup_instructions')
let l:error .= ' Setup instructions: ' . a:formatter.setup_instructions
endif
return l:error
endfunction
""
" Get formatter based on [name], @setting(b:codefmt_formatter), and defaults.
" If no formatter is available, shout error and return 0.
function! s:GetFormatter(...) abort
if a:0 >= 1
let l:explicit_name = a:1
elseif !empty(get(b:, 'codefmt_formatter'))
let l:explicit_name = b:codefmt_formatter
endif
let l:formatters = s:registry.GetExtensions()
if exists('l:explicit_name')
" Explicit name passed.
let l:selected_formatters = filter(
\ copy(l:formatters), 'v:val.name == l:explicit_name')
if empty(l:selected_formatters)
" No such formatter.
call maktaba#error#Shout(
\ '"%s" is not a supported formatter.', l:explicit_name)
return
endif
let l:formatter = l:selected_formatters[0]
try
let l:formatter_is_available = s:IsAvailable(l:formatter)
catch /.*/
call maktaba#error#Shout(
\ 'Error checking if formatter %s is available: %s',
\ l:formatter.name,
\ v:exception)
return
endtry
if !l:formatter_is_available
call maktaba#error#Shout(s:GetSetupInstructions(l:formatter))
return
endif
else
" No explicit name, use default.
let l:applicable_formatters = filter(
\ copy(l:formatters), 'v:val.AppliesToBuffer()')
let l:default_formatters = filter(
\ copy(l:applicable_formatters), 's:IsAvailableSafe(v:val)')
if !empty(l:default_formatters)
let l:formatter = l:default_formatters[0]
else
" Check if we have formatters that are not available for some reason.
" Report a better error message in that case.
if !empty(l:applicable_formatters)
let l:error = join(map(copy(l:applicable_formatters),
\ 's:GetSetupInstructions(v:val)'), "\n")
else
let l:error = 'Not available. codefmt doesn''t have a default ' .
\ 'formatter for this buffer. Using vim formatting instead.'
silent norm gg=G``
silent execute '%retab'
endif
call maktaba#error#Shout(l:error)
return
endif
endif
return l:formatter
endfunction
""
" Applies [formatter] to the current buffer.
function! codefmt#FormatBuffer(...) abort
let l:formatter = a:0 >= 1 ? s:GetFormatter(a:1) : s:GetFormatter()
if l:formatter is# 0
return
endif
try
if has_key(l:formatter, 'Format')
call l:formatter.Format()
elseif has_key(l:formatter, 'FormatRange')
call l:formatter.FormatRange(1, line('$'))
elseif has_key(l:formatter, 'FormatRanges')
call l:formatter.FormatRanges([[1, line('$')]])
endif
catch
call maktaba#error#Shout('Error formatting file: %s', v:exception)
endtry
endfunction
""
" Applies [formatter] to buffer lines from {startline} to {endline}.
function! codefmt#FormatLines(startline, endline, ...) abort
call maktaba#ensure#IsNumber(a:startline)
call maktaba#ensure#IsNumber(a:endline)
let l:formatter = a:0 >= 1 ? s:GetFormatter(a:1) : s:GetFormatter()
if l:formatter is# 0
return
endif
try
if has_key(l:formatter, 'FormatRange')
call l:formatter.FormatRange(a:startline, a:endline)
elseif has_key(l:formatter, 'FormatRanges')
call l:formatter.FormatRanges([[a:startline, a:endline]])
elseif has_key(l:formatter, 'Format')
if a:startline is# 1 && a:endline is# line('$')
" Allow formatting 1,$ as non-range if range formatting isn't supported.
call l:formatter.Format()
else
call maktaba#error#Shout(
\ 'Range formatting not supported for %s', l:formatter.name)
endif
endif
catch
call maktaba#error#Shout('Error formatting file: %s', v:exception)
endtry
endfunction
""
" @public
" Suitable for use as 'operatorfunc'; see |g@| for details.
" The type is ignored since formatting only works on complete lines.
function! codefmt#FormatMap(type) range abort
call codefmt#FormatLines(line("'["), line("']"))
endfunction
""
" @public
" To map the builtin |gq| command to invoke codefmt, set 'formatexpr' to call
" this function. Example: >
" set formatexpr=codefmt#FormatExpr()
" <
function! codefmt#FormatExpr() abort
call codefmt#FormatLines(v:lnum, v:lnum + v:count)
endfunction
""
" Generate the completion for supported formatters. Lists available formatters
" that apply to the current buffer first, then unavailable formatters that
" apply, then everything else.
function! codefmt#GetSupportedFormatters(ArgLead, CmdLine, CursorPos) abort
let l:groups = [[], [], []]
for l:formatter in s:registry.GetExtensions()
let l:key = l:formatter.AppliesToBuffer() ? (
\ s:IsAvailable(l:formatter) ? 0 : 1) : 2
call add(l:groups[l:key], l:formatter.name)
endfor
return join(l:groups[0] + l:groups[1] + l:groups[2], "\n")
endfunction
""
" @public
" Returns whether there is a default formatter available for the current
" buffer.
function! codefmt#AvailableInCurrrentBuffer() abort
let l:formatters = s:registry.GetExtensions()
if !empty(get(b:, 'codefmt_formatter'))
let l:Predicate = {f -> f.name ==# b:codefmt_formatter}
else
let l:Predicate = {f -> f.AppliesToBuffer() && s:IsAvailable(f)}
endif
for l:formatter in s:registry.GetExtensions()
if l:Predicate(l:formatter)
return 1
endif
endfor
return 0
endfunction
""
" @private
" Returns whether to perform Availability checks, which is normall set for
" testing. Defaults to 1 (enable availablity checks).
function! codefmt#ShouldPerformIsAvailableChecks() abort
return get(s:, 'check_formatters_available', 1)
endfunction
""
" @private
" Configures whether codefmt should bypass FORMATTER.IsAvailable checks and
" assume every formatter is available to avoid checking for executables on the
" path. By default, of course, checks are enabled. If {enable} is 0, they will
" be disabled. If 1, normal behavior with IsAvailable checking is restored.
function! codefmt#SetWhetherToPerformIsAvailableChecksForTesting(enable) abort
let s:check_formatters_available = a:enable
endfunction