├── nfasd └── __init__.py ├── MANIFEST.in ├── .gitignore ├── setup_cheatsheet.txt ├── setup.py ├── README.rst └── bin ├── register-python-argcomplete-menu └── nfasd /nfasd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules. 2 | *.pyc 3 | 4 | # Setuptools distribution folder. 5 | /dist/ 6 | 7 | # Python egg metadata, regenerated from source files by setuptools. 8 | /*.egg-info 9 | 10 | build/ 11 | -------------------------------------------------------------------------------- /setup_cheatsheet.txt: -------------------------------------------------------------------------------- 1 | requirement 2 | ----------- 3 | python3 -m pip install --upgrade setuptools wheel twine 4 | 5 | upload to pypi 6 | -------------- 7 | update version in setup.py 8 | python setup.py sdist bdist_wheel 9 | twine upload --repository-url https://upload.pypi.org/legacy/ dist/* 10 | 11 | install locally 12 | -------------- 13 | python setup.py develop 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | # read the contents of your README file 3 | from os import path 4 | this_directory = path.abspath(path.dirname(__file__)) 5 | with open(path.join(this_directory, 'README.rst'), encoding='utf-8') as f: 6 | long_description = f.read() 7 | 8 | setup(name='nfasd', 9 | version='2.0', 10 | description='fasd for neovim', 11 | keywords='fasd neovim nvim nyaovim jumplist', 12 | url='http://github.com/haifengkao/nfasd', 13 | author='Hai Feng Kao', 14 | author_email='haifeng@cocoaspice.in', 15 | license='MIT', 16 | long_description=long_description, 17 | long_description_content_type="text/x-rst", 18 | packages=['nfasd'], 19 | scripts=['bin/nfasd', 'bin/register-python-argcomplete-menu'], 20 | install_requires=[ 21 | 'argcomplete', 22 | 'msgpack-python', 23 | ], 24 | classifiers=[ 25 | 'Development Status :: 3 - Alpha', 26 | 'Environment :: Console', 27 | 'Environment :: MacOS X', 28 | 'Intended Audience :: Developers', 29 | 'Intended Audience :: System Administrators', 30 | 'Intended Audience :: End Users/Desktop', 31 | 'Intended Audience :: Information Technology', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Operating System :: MacOS', 34 | 'Operating System :: Unix', 35 | 'Operating System :: POSIX', 36 | 'Programming Language :: Python', 37 | 'Topic :: System :: Shells', 38 | 'Topic :: Utilities', 39 | ], 40 | zip_safe=False) 41 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | nfasd 2 | ===== 3 | 4 | Autocomplete nvim recent files in command line 5 | 6 | Installation 7 | ------------ 8 | Install the python package 9 | 10 | .. code:: bash 11 | 12 | pip install nfasd 13 | 14 | Add the following line in your :code:`~/.bashrc` 15 | 16 | .. code:: bash 17 | 18 | eval "$(register-python-argcomplete-menu nfasd)" 19 | 20 | # use TAB to cycle through all possible matches 21 | # optional but highly recommended 22 | [[ $- = *i* ]] && bind TAB:menu-complete 23 | 24 | Alternatively, if you use zsh, add the following to :code:`~/.zshrc` 25 | 26 | .. code:: bash 27 | 28 | eval "$(register-python-argcomplete-menu nfasd)" 29 | # stop shell from beeping for every complete 30 | # optional but highly recommended 31 | setopt NO_LIST_BEEP 32 | 33 | For fish shell, you need to install 34 | 35 | .. code:: bash 36 | 37 | pip install argcomplete 38 | register-python-argcomplete --shell fish my-favourite-script.py > ~/.config/fish/my-favourite-script.py.fish 39 | 40 | Configuration 41 | ------------- 42 | 43 | Add the following to :code:`~/.bashrc` or :code:`~/.zshrc` 44 | 45 | .. code:: bash 46 | 47 | alias n='nfasd -e nvim' 48 | alias ny='nfasd -e nyaovim' 49 | 50 | Then you can press :code:`n myPro` 51 | to get :code:`n ~/myProject` 52 | 53 | `-e` specifies which executable to open the file 54 | 55 | If you want to exclude certain file patterns, 56 | use the `--exclude` option, e.g. 57 | 58 | .. code:: bash 59 | 60 | alias n=`nfasd -e nvim --exclude tmp` 61 | 62 | Changelog 63 | ------------- 64 | 2.0 for nvim 0.6 new shada format 65 | 66 | 1.0 for python3 67 | 68 | 0.19 for python2 69 | 70 | Tips 71 | ---- 72 | To increase the number of recent files to 1000, add the following line to your `~/.config/nvim/init.vim` 73 | 74 | .. code:: bash 75 | 76 | set shada=!,'1000,<50,s10,h 77 | 78 | Special Thanks 79 | -------------- 80 | `fasd `_ : the awesome command line tool 81 | -------------------------------------------------------------------------------- /bin/register-python-argcomplete-menu: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # PYTHON_ARGCOMPLETE_OK 3 | 4 | # Copyright 2012-2013, Andrey Kislyuk and argcomplete contributors. 5 | # Licensed under the Apache License. See https://github.com/kislyuk/argcomplete for more info. 6 | 7 | ''' 8 | Register a Python executable for use with the argcomplete module. 9 | 10 | To perform the registration, source the output of this script in your bash shell (quote the output to avoid interpolation). 11 | 12 | Example: 13 | 14 | $ eval "$(register-python-argcomplete-menu my-favorite-script.py)" 15 | ''' 16 | 17 | import sys 18 | import argparse 19 | 20 | shellcode = ''' 21 | _python_argcomplete_lst() { 22 | 23 | COMPREPLY=( $(IFS="$IFS" \ 24 | COMP_LINE="$COMP_LINE" \ 25 | COMP_POINT="$COMP_POINT" \ 26 | _ARGCOMPLETE_COMP_WORDBREAKS="$COMP_WORDBREAKS" \ 27 | _ARGCOMPLETE=1 \ 28 | "$1" 8>&1 9>&2 1>/dev/null 2>/dev/null) ) 29 | 30 | # bash use echo to return results 31 | echo $COMPREPLY 32 | } 33 | 34 | _python_argcomplete_zsh() { 35 | # need different IFS for bash and zsh. why? 36 | local IFS='\n' 37 | 38 | # use tab to cycle throught all possible matches 39 | compstate[insert]=menu # no expand 40 | 41 | local ret=1 42 | local -a suf matches 43 | local -x COMP_POINT COMP_CWORD 44 | local -a COMP_WORDS COMPREPLY BASH_VERSINFO 45 | local -x COMP_LINE="$words" 46 | local -A savejobstates savejobtexts 47 | 48 | # copied from zsh/Completion/bashcompinit 49 | (( COMP_POINT = 1 + ${#${(j. .)words[1,CURRENT-1]}} + $#QIPREFIX + $#IPREFIX + $#PREFIX )) 50 | (( COMP_CWORD = CURRENT - 1)) 51 | COMP_WORDS=( $words ) 52 | BASH_VERSINFO=( 2 05b 0 1 release ) 53 | 54 | savejobstates=( ${(kv)jobstates} ) 55 | savejobtexts=( ${(kv)jobtexts} ) 56 | 57 | [[ ${argv[${argv[(I)nospace]:-0}-1]} = -o ]] && suf=( -S '' ) 58 | 59 | # debuf messages 60 | # echo "====log====" 61 | # echo $COMP_CWORD 62 | # echo $COMP_POINT 63 | # echo $COMP_TYPE 64 | # echo $words 65 | # echo "====end====" 66 | 67 | # get result from python script 68 | val=`_python_argcomplete_lst %(executable)s` 69 | 70 | # here we have to use bash's IFS (\013) to separate the val string into an array, why? 71 | matches=("${(@s/\013/)val}") 72 | 73 | if [ ${#matches[@]} -le "1" ]; then 74 | # if there is only one match, a strange space will be put at the end of the line 75 | # remove it by http://stackoverflow.com/questions/369758/how-to-trim-whitespace-from-a-bash-variable 76 | matches=("${val//[[:space:]]/}") 77 | fi 78 | 79 | if [[ -n $matches ]]; then 80 | if [[ ${argv[${argv[(I)filenames]:-0}-1]} = -o ]]; then 81 | compset -P '*/' && matches=( ${matches##*/} ) 82 | compset -S '/*' && matches=( ${matches%%/*} ) 83 | compadd -U -Q -f "${suf[@]}" -a matches && ret=0 84 | else 85 | # comadd reference http://zsh.sourceforge.net/Doc/Release/Completion-Widgets.html#Completion-Widgets 86 | count=0 87 | for item in "${matches[@]}" 88 | do 89 | # -V to preserve the completion orders http://stackoverflow.com/questions/15140396/zsh-completion-order 90 | # -S to remove the trailing space after the completion. equal to 'complete -o nospace' in bash 91 | # TODO: allow user to specify the complete_opts ? 92 | compadd -U -S '' -V $count $item 93 | (( count++ )) 94 | done 95 | ret=0 96 | 97 | fi 98 | fi 99 | 100 | if (( ret )); then 101 | if [[ ${argv[${argv[(I)default]:-0}-1]} = -o ]]; then 102 | _default "${suf[@]}" && ret=0 103 | elif [[ ${argv[${argv[(I)dirnames]:-0}-1]} = -o ]]; then 104 | _directories "${suf[@]}" && ret=0 105 | fi 106 | fi 107 | 108 | return ret 109 | } 110 | 111 | _python_argcomplete() { 112 | local IFS='\013' 113 | 114 | COMPREPLY=`_python_argcomplete_lst %(executable)s` 115 | if [[ $? != 0 ]]; then 116 | unset COMPREPLY 117 | fi 118 | } 119 | 120 | if type compdef >/dev/null 2>/dev/null; then 121 | # zsh 122 | compdef _python_argcomplete_zsh "%(executable)s" 123 | else if type complete >/dev/null 2>/dev/null; then 124 | # bash 125 | complete %(complete_opts)s -F _python_argcomplete "%(executable)s" 126 | fi; fi 127 | ''' 128 | 129 | parser = argparse.ArgumentParser( 130 | description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) 131 | 132 | parser.add_argument( 133 | '--no-defaults', 134 | dest='use_defaults', action='store_false', default=True, 135 | help='When no matches are generated, do not fallback to readline\'s default completion') 136 | parser.add_argument( 137 | '--complete-arguments', 138 | nargs=argparse.REMAINDER, 139 | help='arguments to call complete with; use of this option discards default options') 140 | 141 | parser.add_argument("executable") 142 | 143 | if len(sys.argv) == 1: 144 | parser.print_help() 145 | sys.exit(1) 146 | 147 | args = parser.parse_args() 148 | 149 | if args.complete_arguments is None: 150 | complete_options = '-o nospace -o default' if args.use_defaults else '-o nospace' 151 | else: 152 | complete_options = " ".join(args.complete_arguments) 153 | 154 | sys.stdout.write(shellcode % dict( 155 | complete_opts=complete_options, 156 | executable=args.executable 157 | )) 158 | -------------------------------------------------------------------------------- /bin/nfasd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # PYTHON_ARGCOMPLETE_OK 4 | import argcomplete, argparse 5 | import os # to execute nvim 6 | import platform # to find XDG_DATA_HOME 7 | import operator 8 | import msgpack # to parse shada 9 | import re 10 | # from argcomplete import warn # logging 11 | 12 | def shada_path(): 13 | # Unix: ~/.local/share/nvim/ 14 | # Windows: ~/AppData/Local/nvim-data/ 15 | key = 'XDG_DATA_HOME' 16 | path = None 17 | if key in os.environ: 18 | path = os.environ[key] 19 | if not path: 20 | if platform.system() == 'Windows': 21 | path = '~/AppData/Local' 22 | else: 23 | path = '~/.local/share' 24 | if platform.system() == 'Windows': 25 | path = os.path.join(path, 'nvim-data') 26 | else: 27 | path = os.path.join(path, 'nvim') 28 | path = os.path.join(path, 'shada', 'main.shada') 29 | return path 30 | 31 | # return an array of v:oldfiles 32 | def nvim_oldfiles(shada_path): 33 | shada_path = os.path.expanduser(shada_path) 34 | f = open(shada_path, 'rb') 35 | unpacker = msgpack.Unpacker(f) 36 | 37 | # see https://neovim.io/doc/user/starting.html#shada-format 38 | 39 | # 1. First goes type of the entry. Object type must be an unsigned integer. 40 | # Object type must not be equal to zero. 41 | # 2. Second goes entry timestamp. It must also be an unsigned integer. 42 | # 3. Third goes the length of the fourth entry. Unsigned integer as well, used 43 | # for fast skipping without parsing. 44 | # 4. Fourth is actual entry data. All currently used ShaDa entries use 45 | # containers to hold data: either map or array. Exact format depends on the 46 | # entry type: 47 | 48 | # 7 (GlobalMark) 49 | # 8 (Jump) 50 | # 10 (LocalMark) 51 | # 11 (Change) 52 | valid_types = [7, 8, 10, 11] 53 | 54 | file_set = set() 55 | recent_files = [] 56 | 57 | try: 58 | while True: 59 | type = unpacker.unpack() 60 | 61 | unpacker.skip() # timestamp 62 | unpacker.skip() # data_length in bytes 63 | if not type in valid_types: 64 | unpacker.skip() 65 | else: 66 | data = unpacker.unpack() 67 | file_name = data['f'] 68 | if not file_name in file_set: 69 | file_set.add(file_name) 70 | recent_files.append(file_name) 71 | except msgpack.exceptions.OutOfData as e: 72 | # all data are processed 73 | pass 74 | 75 | # convert byte string to utf8 76 | recent_files_utf8 = [] 77 | for filename in recent_files: 78 | recent_files_utf8.append(filename.decode('utf-8')) 79 | return recent_files_utf8 80 | 81 | 82 | # convert string into a (char -> list) map 83 | # the list contains the indices of char position in string 84 | # we use this map to find the location of corresponding char from user_input 85 | def index_map(user_input, string): 86 | indices = {} 87 | for ch in user_input: 88 | if ch not in indices: 89 | indices[ch] = [] 90 | 91 | for i, ch in enumerate(string): 92 | if ch in indices: 93 | index = indices[ch] 94 | index.append(i) 95 | 96 | return indices 97 | 98 | # for file name partial matching 99 | # objective: find the largest chunk of keyword 100 | # we want to give substring the lowest score (the lower the better) 101 | # 0: 'abc', '123abc456' <= exact substring 102 | # 1: 'abc', '12a34bc56' <= two substring 'a', 'bc' 103 | # 2: 'abc', '1a34b5c6' <= three substring 'a', 'b', 'c' 104 | # None: 'abc', '123ab456' <= 'abc' is not a subsequence of '123ab456' 105 | def match_fuzzy_substring(user_input, string): 106 | if len(user_input) <= 0: 107 | # are you kidding me? 108 | return 0 109 | 110 | max_cost = len(user_input) + 1 111 | indices = index_map(user_input, string) 112 | 113 | # first line 114 | distances = [] 115 | for i in indices[user_input[0]]: 116 | distances.append((0, [i])) # cost, indices 117 | 118 | for ch in user_input[1:]: 119 | if len(distances) <= 0: 120 | # user_input is not a subsequence of string 121 | break 122 | distances_ = [] 123 | for i in indices[ch]: 124 | min_cost = max_cost 125 | min_indices = [] 126 | for t in distances: 127 | # TODO: binary search? 128 | if (t[1][-1] >= i): 129 | continue 130 | 131 | if t[1][-1] != i - 1: 132 | # non-consecutive matching 133 | # cost++ 134 | # e.g. compare 'aa' against 'aba' 135 | cost = t[0] + 1 136 | else: 137 | # consecutive matching 138 | # e.g. compare 'aa' against 'aa' or 'baa' or 'aab' 139 | cost = t[0] 140 | if cost < min_cost: 141 | min_cost = cost 142 | min_indices = t[1] 143 | 144 | if min_cost != max_cost: # at least we got something 145 | distances_.append((min_cost, min_indices+[i])) 146 | distances = distances_ 147 | 148 | if len(distances) > 0: 149 | return sorted(distances)[0] # return tuple (final cost, an array of last matching indices) 150 | else: 151 | return None # user_input is not a subsequence of string 152 | 153 | # for file name partial matching 154 | def fuzzyfinder(user_input, collection, is_case_sensitive = True): 155 | # reverse matching favors the matched char near the end 156 | # in our case, it prefers the result that matches files name 157 | is_reverse_matching = True 158 | if not is_case_sensitive: 159 | user_input = user_input.lower() 160 | if is_reverse_matching: 161 | user_input = user_input[::-1] 162 | 163 | suggestions = [] 164 | for i, item in enumerate(collection): 165 | matching_item = item 166 | if not is_case_sensitive: 167 | matching_item = item.lower() 168 | if is_reverse_matching: 169 | matching_item = matching_item[::-1] 170 | 171 | res = match_fuzzy_substring(user_input, matching_item) # Checks if user_input is a subsequence of theItem 172 | if res != None: 173 | score = res[0] 174 | 175 | # first_matching_index example: 176 | # 1: 'abc', '1a34b5c6' 177 | # 3: 'abc', '123abc456' 178 | # if is_reverse_matching is True, the first_matching_index is index the last match char in the reversed string 179 | first_matching_index = res[1][0] 180 | # print (score, item) 181 | suggestions.append((score, first_matching_index, i, item)) # usually user will type filename instead of path. last_matching_index will give filename higher weight 182 | 183 | # sort the result based on score and i 184 | # smaller i means more recent files 185 | return [x for _, _, _, x in sorted(suggestions, key = operator.itemgetter(0, 1, 2))] 186 | 187 | # no need to validate 188 | def novalidator(completion, prefix): 189 | # return TRUE 190 | return completion.startwith(prefix) 191 | 192 | # only return existed files 193 | def exist_filter(files, max_count): 194 | res = [] 195 | for i, path in enumerate(files): 196 | if os.path.exists(path): 197 | res.append(path) 198 | if len(res) >= max_count: 199 | break 200 | 201 | return res 202 | 203 | # returns the files in current folder which matches prefix 204 | def file_prefix_completer(prefix, cwd, is_case_sensitive, prefix_only=True): 205 | res = [] 206 | 207 | # if user typed a complete dir name, search for the files in it instead 208 | prefix = os.path.join(prefix, '') if os.path.isdir(prefix) else prefix 209 | 210 | (prefix_path, last_item) = os.path.split(prefix) 211 | 212 | target_dir = os.path.join(cwd, prefix_path) 213 | 214 | if not os.path.isdir(target_dir): 215 | return [] 216 | 217 | if not is_case_sensitive: 218 | last_item = last_item.lower() 219 | 220 | for i in os.listdir(target_dir): 221 | filename = i if is_case_sensitive else i.lower() 222 | 223 | if prefix_only and filename.startswith(last_item): 224 | # prefix matching 225 | res.append(os.path.join(target_dir, i)) 226 | elif not prefix_only and last_item in filename: 227 | # substring matching 228 | res.append(os.path.join(target_dir, i)) 229 | 230 | return res 231 | 232 | def nvim_recent_files(prefix, parsed_args, **kwargs): 233 | MAX_OUTPUT_NUMBER = 8 # limit choices to 8, otherwise the screen might be cluttered with lines 234 | # handle ~/my_file or ~abc cases 235 | prefix = os.path.expanduser(os.path.normpath(prefix)) 236 | 237 | # get nvim recent files 238 | history = nvim_oldfiles(shada_path()) 239 | 240 | cwd = os.getcwd() 241 | 242 | is_case_sensitive = False 243 | 244 | # case insensitive is better in all scenarios 245 | # if prefix == prefix.lower(): 246 | # smart case: no uppercase char, so we just ignore cases 247 | # is_case_sensitive = False 248 | 249 | # search for local files 250 | files = file_prefix_completer(prefix, cwd, is_case_sensitive) 251 | 252 | matched = [] 253 | if len(prefix) and len(files) < MAX_OUTPUT_NUMBER: # don't need to do expensive fuzzy matching if we already got enough data 254 | # fuzzy file matching, will match infix or suffix substring 255 | fuzzy_files = file_prefix_completer(prefix, cwd, is_case_sensitive, False) 256 | matched = fuzzyfinder(prefix, fuzzy_files + history, is_case_sensitive) 257 | else: 258 | matched = history 259 | 260 | # give local files higher priority over nvim history (my intuition?) 261 | collection = files + matched 262 | 263 | # remove duplicated files 264 | seen = {} 265 | collection = [seen.setdefault(x, x) for x in collection if x not in seen] 266 | 267 | if parsed_args is not None and parsed_args.exclude is not None: 268 | exclude = re.compile(parsed_args.exclude) 269 | collection = [x for x in collection if not exclude.search(x)] 270 | 271 | res = exist_filter(collection, MAX_OUTPUT_NUMBER) 272 | 273 | # if the result is a subdir of current working dir 274 | # remove the common prefix if it is not current working directory ('./' looks weird in terminal) 275 | res = [os.path.relpath(x, cwd) if x.startswith(cwd) and x != cwd else x for x in res] 276 | 277 | # add trailing slash for directories 278 | res = [os.path.join(x, '') if os.path.isdir(x) else x for x in res] 279 | 280 | return res 281 | 282 | parser = argparse.ArgumentParser() 283 | parser.add_argument("-e", help='the executable name of neovim') 284 | 285 | # the text is copied from man grep 286 | parser.add_argument("--exclude", help='If specified, it excludes files matching the given filename pattern. Patterns are matched to the full path specified, not only to the filename component. The pattern is matched by python re module') 287 | parser.add_argument("filepath", help='the path of the recent file').completer = nvim_recent_files 288 | 289 | def my_validator(current_input, keyword_to_check_against): 290 | # warn('my_validator'+current_input+keyword_to_check_against) 291 | # Pass through ALL options even if they don't all start with 'current_input' 292 | return True 293 | 294 | argcomplete.autocomplete(parser, validator=my_validator, always_complete_options=False) # don't complete the annoying --help 295 | 296 | # args = parser.parse_args() 297 | args, unknown = parser.parse_known_args() 298 | 299 | # print args.filepath 300 | # print args.e 301 | # print unknown 302 | 303 | if len(unknown): 304 | path = unknown[0] 305 | else: 306 | path = args.filepath 307 | 308 | if not os.path.exists(path): 309 | # the user might forget to press tab (like nfasd -e nvim mysomet...) 310 | # help the user press "the tab" 311 | res = nvim_recent_files(path, None) 312 | 313 | if len(res): 314 | path = res[0] 315 | 316 | # don't do anything when the file doesn't exist 317 | # we don't want user to accidentally create some file with strange partial file name 318 | if os.path.exists(path): 319 | absPath = os.path.abspath(path) 320 | exe_name = 'nvim' 321 | if args.e and len(args.e): 322 | exe_name = args.e # use the user-defined executable name 323 | exec_str = exe_name + ' "' + absPath + '"' 324 | 325 | os.system(exec_str) 326 | 327 | # if __name__ == "__main__": 328 | # print(match_fuzzy_substring('drop', 'Dropbox/p')) # =>None 329 | # print(match_fuzzy_substring('ber', u"Hi!Übersicht")) # =>None 330 | # print(match_fuzzy_substring('drop', 'dropbox/p')) # =>(3, 0) 331 | # print(match_fuzzy_substring('aa', 'aa')) # =>(1, 0) 332 | # print(match_fuzzy_substring('aa', 'aab')) # =>(1, 0) 333 | # print(match_fuzzy_substring('aa', 'baa')) # =>(2, 0) 334 | # print(match_fuzzy_substring('aa', 'bbaa')) # =>(3, 0) 335 | # print(match_fuzzy_substring('aa', 'aabb')) # =>(1, 0) 336 | # print(match_fuzzy_substring('aa', 'abba')) # =>(3, 1) 337 | # print(match_fuzzy_substring('aaa', 'ababa')) # =>(4, 2) 338 | # print(match_fuzzy_substring('init', 'ing/install.sh')) # =>(7, 2) 339 | # print(match_fuzzy_substring('init', 'ing/nvim/init.vim')) # =>(12, 0) 340 | # print(match_fuzzy_substring('init', 'ing/insitall.sh')) # =>(8, 1) 341 | # print(match_fuzzy_substring("foo", "foobar")) # =>(2, 0) 342 | # print(match_fuzzy_substring("foo", "FooBar")) # =>None 343 | # print(match_fuzzy_substring("fb", "foobar")) # =>(3, 1) 344 | # print(match_fuzzy_substring("", "foo bar")) # =>0 345 | # print(match_fuzzy_substring("", "")) # =>0 346 | # print(match_fuzzy_substring("f", "")) # =>None 347 | 348 | --------------------------------------------------------------------------------