466 lines
16 KiB
VimL
466 lines
16 KiB
VimL
|
" autoload/editorconfig_core/fnmatch.vim: Globbing for
|
||
|
" editorconfig-vim. Ported from the Python core's fnmatch.py.
|
||
|
|
||
|
" Copyright (c) 2012-2019 EditorConfig Team {{{1
|
||
|
" All rights reserved.
|
||
|
"
|
||
|
" Redistribution and use in source and binary forms, with or without
|
||
|
" modification, are permitted provided that the following conditions are met:
|
||
|
"
|
||
|
" 1. Redistributions of source code must retain the above copyright notice,
|
||
|
" this list of conditions and the following disclaimer.
|
||
|
" 2. Redistributions in binary form must reproduce the above copyright notice,
|
||
|
" this list of conditions and the following disclaimer in the documentation
|
||
|
" and/or other materials provided with the distribution.
|
||
|
"
|
||
|
" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||
|
" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||
|
" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||
|
" ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||
|
" LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||
|
" CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||
|
" SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||
|
" INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||
|
" CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||
|
" ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||
|
" POSSIBILITY OF SUCH DAMAGE. }}}1
|
||
|
|
||
|
"Filename matching with shell patterns.
|
||
|
"
|
||
|
"fnmatch(FILENAME, PATH, PATTERN) matches according to the local convention.
|
||
|
"fnmatchcase(FILENAME, PATH, PATTERN) always takes case in account.
|
||
|
"
|
||
|
"The functions operate by translating the pattern into a regular
|
||
|
"expression. They cache the compiled regular expressions for speed.
|
||
|
"
|
||
|
"The function translate(PATTERN) returns a regular expression
|
||
|
"corresponding to PATTERN. (It does not compile it.)
|
||
|
|
||
|
let s:saved_cpo = &cpo
|
||
|
set cpo&vim
|
||
|
|
||
|
" variables {{{1
|
||
|
if !exists('g:editorconfig_core_vimscript_debug')
|
||
|
let g:editorconfig_core_vimscript_debug = 0
|
||
|
endif
|
||
|
" }}}1
|
||
|
" === Regexes =========================================================== {{{1
|
||
|
let s:LEFT_BRACE = '\v%(^|[^\\])\{'
|
||
|
"LEFT_BRACE = re.compile(
|
||
|
" r"""
|
||
|
"
|
||
|
" (?: ^ | [^\\] ) # Beginning of string or a character besides "\"
|
||
|
"
|
||
|
" \{ # "{"
|
||
|
"
|
||
|
" """, re.VERBOSE
|
||
|
")
|
||
|
|
||
|
let s:RIGHT_BRACE = '\v%(^|[^\\])\}'
|
||
|
"RIGHT_BRACE = re.compile(
|
||
|
" r"""
|
||
|
"
|
||
|
" (?: ^ | [^\\] ) # Beginning of string or a character besides "\"
|
||
|
"
|
||
|
" \} # "}"
|
||
|
"
|
||
|
" """, re.VERBOSE
|
||
|
")
|
||
|
|
||
|
let s:NUMERIC_RANGE = '\v([+-]?\d+)' . '\.\.' . '([+-]?\d+)'
|
||
|
"NUMERIC_RANGE = re.compile(
|
||
|
" r"""
|
||
|
" ( # Capture a number
|
||
|
" [+-] ? # Zero or one "+" or "-" characters
|
||
|
" \d + # One or more digits
|
||
|
" )
|
||
|
"
|
||
|
" \.\. # ".."
|
||
|
"
|
||
|
" ( # Capture a number
|
||
|
" [+-] ? # Zero or one "+" or "-" characters
|
||
|
" \d + # One or more digits
|
||
|
" )
|
||
|
" """, re.VERBOSE
|
||
|
")
|
||
|
|
||
|
" }}}1
|
||
|
" === Internal functions ================================================ {{{1
|
||
|
|
||
|
" Dump the bytes of a:text. For debugging use.
|
||
|
function! s:dump_bytes(text)
|
||
|
let l:idx=0
|
||
|
while l:idx < strlen(a:text)
|
||
|
let l:byte_val = char2nr(a:text[l:idx])
|
||
|
echom printf('%10s%-5d%02x %s', '', l:idx, l:byte_val,
|
||
|
\ a:text[l:idx])
|
||
|
let l:idx+=1
|
||
|
endwhile
|
||
|
endfunction "s:dump_bytes
|
||
|
|
||
|
" Dump the characters of a:text and their codepoints. For debugging use.
|
||
|
function! s:dump_chars(text)
|
||
|
let l:chars = split(a:text, '\zs')
|
||
|
let l:idx = 0
|
||
|
let l:out1 = ''
|
||
|
let l:out2 = ''
|
||
|
while l:idx < len(l:chars)
|
||
|
let l:char = l:chars[l:idx]
|
||
|
let l:out1 .= printf('%5s', l:char)
|
||
|
let l:out2 .= printf('%5x', char2nr(l:char))
|
||
|
let l:idx+=1
|
||
|
endwhile
|
||
|
|
||
|
echom l:out1
|
||
|
echom l:out2
|
||
|
endfunction "s:dump_chars
|
||
|
|
||
|
" }}}1
|
||
|
" === Translating globs to patterns ===================================== {{{1
|
||
|
|
||
|
" Used by s:re_escape: backslash-escape any character below U+0080;
|
||
|
" replace all others with a %U escape.
|
||
|
" See https://vi.stackexchange.com/a/19617/1430 by yours truly
|
||
|
" (https://vi.stackexchange.com/users/1430/cxw).
|
||
|
unlockvar s:replacement_expr
|
||
|
let s:replacement_expr =
|
||
|
\ '\=' .
|
||
|
\ '((char2nr(submatch(1)) >= 128) ? ' .
|
||
|
\ 'printf("%%U%08x", char2nr(submatch(1))) : ' .
|
||
|
\ '("\\" . submatch(1))' .
|
||
|
\ ')'
|
||
|
lockvar s:replacement_expr
|
||
|
|
||
|
" Escaper for very-magic regexes
|
||
|
function! s:re_escape(text)
|
||
|
return substitute(a:text, '\v([^0-9a-zA-Z_])', s:replacement_expr, 'g')
|
||
|
endfunction
|
||
|
|
||
|
"def translate(pat, nested=0):
|
||
|
" Translate a shell PATTERN to a regular expression.
|
||
|
" There is no way to quote meta-characters.
|
||
|
function! editorconfig_core#fnmatch#translate(pat, ...)
|
||
|
let l:nested = 0
|
||
|
if a:0
|
||
|
let l:nested = a:1
|
||
|
endif
|
||
|
|
||
|
if g:editorconfig_core_vimscript_debug
|
||
|
echom '- fnmatch#translate: pattern ' . a:pat
|
||
|
echom printf(
|
||
|
\ '- %d chars', strlen(substitute(a:pat, ".", "x", "g")))
|
||
|
call s:dump_chars(a:pat)
|
||
|
endif
|
||
|
|
||
|
let l:pat = a:pat " TODO remove if we wind up not needing this
|
||
|
|
||
|
" Note: the Python sets MULTILINE and DOTALL, but Vim has \_.
|
||
|
" instead of DOTALL, and \_^ / \_$ instead of MULTILINE.
|
||
|
|
||
|
let l:is_escaped = 0
|
||
|
|
||
|
" Find out whether the pattern has balanced braces.
|
||
|
let l:left_braces=[]
|
||
|
let l:right_braces=[]
|
||
|
call substitute(l:pat, s:LEFT_BRACE, '\=add(l:left_braces, 1)', 'g')
|
||
|
call substitute(l:pat, s:RIGHT_BRACE, '\=add(l:right_braces, 1)', 'g')
|
||
|
" Thanks to http://jeromebelleman.gitlab.io/posts/productivity/vimsub/
|
||
|
let l:matching_braces = (len(l:left_braces) == len(l:right_braces))
|
||
|
|
||
|
" Unicode support (#2). Indexing l:pat[l:index] returns bytes, per
|
||
|
" https://github.com/neovim/neovim/issues/68#issue-28114985 .
|
||
|
" Instead, use split() per vimdoc to break the input string into an
|
||
|
" array of *characters*, and process that.
|
||
|
let l:characters = split(l:pat, '\zs')
|
||
|
|
||
|
let l:index = 0 " character index
|
||
|
let l:length = len(l:characters)
|
||
|
let l:brace_level = 0
|
||
|
let l:in_brackets = 0
|
||
|
|
||
|
let l:result = ''
|
||
|
let l:numeric_groups = []
|
||
|
while l:index < l:length
|
||
|
let l:current_char = l:characters[l:index]
|
||
|
let l:index += 1
|
||
|
|
||
|
" if g:editorconfig_core_vimscript_debug
|
||
|
" echom ' - fnmatch#translate: ' . l:current_char . '@' .
|
||
|
" \ (l:index-1) . '; result ' . l:result
|
||
|
" endif
|
||
|
|
||
|
if l:current_char ==# '*'
|
||
|
let l:pos = l:index
|
||
|
if l:pos < l:length && l:characters[l:pos] ==# '*'
|
||
|
let l:result .= '\_.*'
|
||
|
let l:index += 1 " skip the second star
|
||
|
else
|
||
|
let l:result .= '[^/]*'
|
||
|
endif
|
||
|
|
||
|
elseif l:current_char ==# '?'
|
||
|
let l:result .= '\_[^/]'
|
||
|
|
||
|
elseif l:current_char ==# '['
|
||
|
if l:in_brackets
|
||
|
let l:result .= '\['
|
||
|
else
|
||
|
let l:pos = l:index
|
||
|
let l:has_slash = 0
|
||
|
while l:pos < l:length && l:characters[l:pos] != ']'
|
||
|
if l:characters[l:pos] ==# '/' && l:characters[l:pos-1] !=# '\'
|
||
|
let has_slash = 1
|
||
|
break
|
||
|
endif
|
||
|
let l:pos += 1
|
||
|
endwhile
|
||
|
if l:has_slash
|
||
|
" POSIX IEEE 1003.1-2017 sec. 2.13.3: '/' cannot occur
|
||
|
" in a bracket expression, so [/] matches a literal
|
||
|
" three-character string '[' . '/' . ']'.
|
||
|
let l:result .= '\['
|
||
|
\ . s:re_escape(join(l:characters[l:index : l:pos-1], ''))
|
||
|
\ . '\/'
|
||
|
" escape the slash
|
||
|
let l:index = l:pos + 1
|
||
|
" resume after the slash
|
||
|
else
|
||
|
if l:index < l:length && l:characters[l:index] =~# '\v%(\^|\!)'
|
||
|
let l:index += 1
|
||
|
let l:result .= '[^'
|
||
|
else
|
||
|
let l:result .= '['
|
||
|
endif
|
||
|
let l:in_brackets = 1
|
||
|
endif
|
||
|
endif
|
||
|
|
||
|
elseif l:current_char ==# '-'
|
||
|
if l:in_brackets
|
||
|
let l:result .= l:current_char
|
||
|
else
|
||
|
let l:result .= '\' . l:current_char
|
||
|
endif
|
||
|
|
||
|
elseif l:current_char ==# ']'
|
||
|
if l:in_brackets && !l:is_escaped
|
||
|
let l:result .= ']'
|
||
|
let l:in_brackets = 0
|
||
|
elseif l:is_escaped
|
||
|
let l:result .= '\]'
|
||
|
let l:is_escaped = 0
|
||
|
else
|
||
|
let l:result .= '\]'
|
||
|
endif
|
||
|
|
||
|
elseif l:current_char ==# '{'
|
||
|
let l:pos = l:index
|
||
|
let l:has_comma = 0
|
||
|
while l:pos < l:length && (l:characters[l:pos] !=# '}' || l:is_escaped)
|
||
|
if l:characters[l:pos] ==# ',' && ! l:is_escaped
|
||
|
let l:has_comma = 1
|
||
|
break
|
||
|
endif
|
||
|
let l:is_escaped = l:characters[l:pos] ==# '\' && ! l:is_escaped
|
||
|
let l:pos += 1
|
||
|
endwhile
|
||
|
if ! l:has_comma && l:pos < l:length
|
||
|
let l:num_range =
|
||
|
\ matchlist(join(l:characters[l:index : l:pos-1], ''),
|
||
|
\ s:NUMERIC_RANGE)
|
||
|
if len(l:num_range) > 0 " Remember the ranges
|
||
|
call add(l:numeric_groups, [ 0+l:num_range[1], 0+l:num_range[2] ])
|
||
|
let l:result .= '([+-]?\d+)'
|
||
|
else
|
||
|
let l:inner_xlat = editorconfig_core#fnmatch#translate(
|
||
|
\ join(l:characters[l:index : l:pos-1], ''), 1)
|
||
|
let l:inner_result = l:inner_xlat[0]
|
||
|
let l:inner_groups = l:inner_xlat[1]
|
||
|
let l:result .= '\{' . l:inner_result . '\}'
|
||
|
let l:numeric_groups += l:inner_groups
|
||
|
endif
|
||
|
let l:index = l:pos + 1
|
||
|
elseif l:matching_braces
|
||
|
let l:result .= '%('
|
||
|
let l:brace_level += 1
|
||
|
else
|
||
|
let l:result .= '\{'
|
||
|
endif
|
||
|
|
||
|
elseif l:current_char ==# ','
|
||
|
if l:brace_level > 0 && ! l:is_escaped
|
||
|
let l:result .= '|'
|
||
|
else
|
||
|
let l:result .= '\,'
|
||
|
endif
|
||
|
|
||
|
elseif l:current_char ==# '}'
|
||
|
if l:brace_level > 0 && ! l:is_escaped
|
||
|
let l:result .= ')'
|
||
|
let l:brace_level -= 1
|
||
|
else
|
||
|
let l:result .= '\}'
|
||
|
endif
|
||
|
|
||
|
elseif l:current_char ==# '/'
|
||
|
if join(l:characters[l:index : (l:index + 2)], '') ==# '**/'
|
||
|
let l:result .= '%(/|/\_.*/)'
|
||
|
let l:index += 3
|
||
|
else
|
||
|
let l:result .= '\/'
|
||
|
endif
|
||
|
|
||
|
elseif l:current_char != '\'
|
||
|
let l:result .= s:re_escape(l:current_char)
|
||
|
endif
|
||
|
|
||
|
if l:current_char ==# '\'
|
||
|
if l:is_escaped
|
||
|
let l:result .= s:re_escape(l:current_char)
|
||
|
endif
|
||
|
let l:is_escaped = ! l:is_escaped
|
||
|
else
|
||
|
let l:is_escaped = 0
|
||
|
endif
|
||
|
|
||
|
endwhile
|
||
|
|
||
|
if ! l:nested
|
||
|
let l:result .= '\_$'
|
||
|
endif
|
||
|
|
||
|
return [l:result, l:numeric_groups]
|
||
|
endfunction " #editorconfig_core#fnmatch#translate
|
||
|
|
||
|
let s:_cache = {}
|
||
|
function! s:cached_translate(pat)
|
||
|
if ! has_key(s:_cache, a:pat)
|
||
|
"regex = re.compile(res)
|
||
|
let s:_cache[a:pat] =
|
||
|
\ editorconfig_core#fnmatch#translate(a:pat)
|
||
|
" we don't compile the regex
|
||
|
endif
|
||
|
return s:_cache[a:pat]
|
||
|
endfunction " cached_translate
|
||
|
|
||
|
" }}}1
|
||
|
" === Matching functions ================================================ {{{1
|
||
|
|
||
|
function! editorconfig_core#fnmatch#fnmatch(name, path, pattern)
|
||
|
"def fnmatch(name, pat):
|
||
|
" """Test whether FILENAME matches PATH/PATTERN.
|
||
|
"
|
||
|
" Patterns are Unix shell style:
|
||
|
"
|
||
|
" - ``*`` matches everything except path separator
|
||
|
" - ``**`` matches everything
|
||
|
" - ``?`` matches any single character
|
||
|
" - ``[seq]`` matches any character in seq
|
||
|
" - ``[!seq]`` matches any char not in seq
|
||
|
" - ``{s1,s2,s3}`` matches any of the strings given (separated by commas)
|
||
|
"
|
||
|
" An initial period in FILENAME is not special.
|
||
|
" Both FILENAME and PATTERN are first case-normalized
|
||
|
" if the operating system requires it.
|
||
|
" If you don't want this, use fnmatchcase(FILENAME, PATTERN).
|
||
|
" """
|
||
|
"
|
||
|
" Note: This throws away the backslash in '\.txt' on Cygwin, but that
|
||
|
" makes sense since it's Windows under the hood.
|
||
|
" We don't care about shellslash since we're going to change backslashes
|
||
|
" to slashes in just a moment anyway.
|
||
|
let l:localname = fnamemodify(a:name, ':p')
|
||
|
|
||
|
if editorconfig_core#util#is_win() " normalize
|
||
|
let l:localname = substitute(tolower(l:localname), '\v\\', '/', 'g')
|
||
|
let l:path = substitute(tolower(a:path), '\v\\', '/', 'g')
|
||
|
let l:pattern = tolower(a:pattern)
|
||
|
else
|
||
|
let l:localname = l:localname
|
||
|
let l:path = a:path
|
||
|
let l:pattern = a:pattern
|
||
|
endif
|
||
|
|
||
|
if g:editorconfig_core_vimscript_debug
|
||
|
echom '- fnmatch#fnmatch testing <' . l:localname . '> against <' .
|
||
|
\ l:pattern . '> wrt <' . l:path . '>'
|
||
|
endif
|
||
|
|
||
|
return editorconfig_core#fnmatch#fnmatchcase(l:localname, l:path, l:pattern)
|
||
|
endfunction " fnmatch
|
||
|
|
||
|
function! editorconfig_core#fnmatch#fnmatchcase(name, path, pattern)
|
||
|
"def fnmatchcase(name, pat):
|
||
|
" """Test whether FILENAME matches PATH/PATTERN, including case.
|
||
|
"
|
||
|
" This is a version of fnmatch() which doesn't case-normalize
|
||
|
" its arguments.
|
||
|
" """
|
||
|
"
|
||
|
let [regex, num_groups] = s:cached_translate(a:pattern)
|
||
|
|
||
|
let l:escaped_path = s:re_escape(a:path)
|
||
|
let l:regex = '\v' . l:escaped_path . l:regex
|
||
|
|
||
|
if g:editorconfig_core_vimscript_debug
|
||
|
echom '- fnmatch#fnmatchcase: regex ' . l:regex
|
||
|
call s:dump_chars(l:regex)
|
||
|
echom '- fnmatch#fnmatchcase: checking ' . a:name
|
||
|
call s:dump_chars(a:name)
|
||
|
endif
|
||
|
|
||
|
let l:match_groups = matchlist(a:name, l:regex)[1:] " [0] = full match
|
||
|
|
||
|
if g:editorconfig_core_vimscript_debug
|
||
|
echom printf(' Got %d matches', len(l:match_groups))
|
||
|
endif
|
||
|
|
||
|
if len(l:match_groups) == 0
|
||
|
return 0
|
||
|
endif
|
||
|
|
||
|
" Check numeric ranges
|
||
|
let pattern_matched = 1
|
||
|
for l:idx in range(0,len(l:match_groups))
|
||
|
let l:num = l:match_groups[l:idx]
|
||
|
if l:num ==# ''
|
||
|
break
|
||
|
endif
|
||
|
|
||
|
let [min_num, max_num] = num_groups[l:idx]
|
||
|
if (min_num > (0+l:num)) || ((0+l:num) > max_num)
|
||
|
let pattern_matched = 0
|
||
|
break
|
||
|
endif
|
||
|
|
||
|
" Reject leading zeros without sign. This is very odd ---
|
||
|
" see editorconfig/editorconfig#371.
|
||
|
if match(l:num, '\v^0') != -1
|
||
|
let pattern_matched = 0
|
||
|
break
|
||
|
endif
|
||
|
endfor
|
||
|
|
||
|
if g:editorconfig_core_vimscript_debug
|
||
|
echom '- fnmatch#fnmatchcase: ' . (pattern_matched ? 'matched' : 'did not match')
|
||
|
endif
|
||
|
|
||
|
return pattern_matched
|
||
|
endfunction " fnmatchcase
|
||
|
|
||
|
" }}}1
|
||
|
" === Copyright notices ================================================= {{{1
|
||
|
" Based on code from fnmatch.py file distributed with Python 2.6.
|
||
|
" Portions Copyright (c) 2001-2010 Python Software Foundation;
|
||
|
" All Rights Reserved. Licensed under PSF License (see LICENSE.PSF file).
|
||
|
"
|
||
|
" Changes to original fnmatch:
|
||
|
"
|
||
|
" - translate function supports ``*`` and ``**`` similarly to fnmatch C library
|
||
|
" }}}1
|
||
|
|
||
|
let &cpo = s:saved_cpo
|
||
|
unlet! s:saved_cpo
|
||
|
|
||
|
" vi: set fdm=marker:
|