def Matches(self, comp): # type: (Api) -> Iterator[Union[Iterator, Iterator[str]]] to_complete = comp.to_complete # Problem: .. and ../.. don't complete /. # TODO: Set display_pos before fixing this. #import os #to_complete = os.path.normpath(to_complete) dirname, basename = os_path.split(to_complete) if dirname == '': # We're completing in this directory to_list = '.' else: # We're completing in some other directory to_list = dirname if 0: log('basename %r', basename) log('to_list %r', to_list) log('dirname %r', dirname) try: names = posix.listdir(to_list) except OSError as e: return # nothing for name in names: path = os_path.join(dirname, name) if path.startswith(to_complete): if self.dirs_only: # add_slash not used here # NOTE: There is a duplicate isdir() check later to add a trailing # slash. Consolidate the checks for fewer stat() ops. This is hard # because all the completion actions must obey the same interface. # We could have another type like candidate = File | Dir | # OtherString ? if path_stat.isdir(path): yield path continue if self.exec_only: # TODO: Handle exception if file gets deleted in between listing and # check? if not posix.access(path, posix.X_OK_): continue if self.add_slash and path_stat.isdir(path): yield path + '/' else: yield path
def Matches(self, comp): to_complete = comp.to_complete i = to_complete.rfind('/') if i == -1: # it looks like 'foo' to_list = '.' base = '' elif i == 0: # it's an absolute path to_complete like / or /b to_list = '/' base = '/' else: to_list = to_complete[:i] base = to_list #log('to_list %r', to_list) try: names = posix.listdir(to_list) except OSError as e: return # nothing for name in names: path = os_path.join(base, name) if path.startswith(to_complete): if self.dirs_only: # add_slash not used here # NOTE: There is a duplicate isdir() check later to add a trailing # slash. Consolidate the checks for fewer stat() ops. This is hard # because all the completion actions must obey the same interface. # We could have another type like candidate = File | Dir | # OtherString ? if path_stat.isdir(path): yield path continue if self.exec_only: # TODO: Handle exception if file gets deleted in between listing and # check? if not posix.access(path, posix.X_OK): continue if self.add_slash and path_stat.isdir(path): yield path + '/' else: yield path
def _PostProcess( self, base_opts, # type: Dict dynamic_opts, # type: Dict user_spec, # type: UserSpec comp, # type: Api ): # type: (...) -> Iterator[Union[Iterator, Iterator[str]]] """ Add trailing spaces / slashes to completion candidates, and time them. NOTE: This post-processing MUST go here, and not in UserSpec, because it's in READLINE in bash. compgen doesn't see it. """ self.debug_f.log('Completing %r ... (Ctrl-C to cancel)', comp.line) start_time = time.time() # TODO: dedupe candidates? You can get two 'echo' in bash, which is dumb. i = 0 for candidate, is_fs_action in user_spec.Matches(comp): # SUBTLE: dynamic_opts is part of compopt_state, which ShellFuncAction # can mutate! So we don't want to pull this out of the loop. # # TODO: The candidates from each actions shouldn't be flattened. # for action in user_spec.Actions(): # if action.IsFileSystem(): # this returns is_dir too # # action.Run() # might set dynamic opts # opt_nospace = base_opts... # if 'nospace' in dynamic_opts: # opt_nosspace = dynamic_opts['nospace'] # for candidate in action.Matches(): # add space or / # and do escaping too # # Or maybe you can request them on demand? Most actions are EAGER. # While the ShellacAction is LAZY? And you should be able to cancel it! # NOTE: User-defined plugins (and the -P flag) can REWRITE what the user # already typed. So # # $ echo 'dir with spaces'/f<TAB> # # can be rewritten to: # # $ echo dir\ with\ spaces/foo line_until_tab = self.comp_ui_state.line_until_tab line_until_word = line_until_tab[:self.comp_ui_state.display_pos] opt_filenames = base_opts.get('filenames', False) if 'filenames' in dynamic_opts: opt_filenames = dynamic_opts['filenames'] # compopt -o filenames is for user-defined actions. Or any # FileSystemAction needs it. if is_fs_action or opt_filenames: if path_stat.isdir(candidate): # TODO: test coverage yield line_until_word + ShellQuoteB(candidate) + '/' continue opt_nospace = base_opts.get('nospace', False) if 'nospace' in dynamic_opts: opt_nospace = dynamic_opts['nospace'] sp = '' if opt_nospace else ' ' yield line_until_word + ShellQuoteB(candidate) + sp # NOTE: Can't use %.2f in production build! i += 1 elapsed_ms = (time.time() - start_time) * 1000.0 plural = '' if i == 1 else 'es' # TODO: Show this in the UI if it takes too long! if 0: self.debug_f.log( '... %d match%s for %r in %d ms (Ctrl-C to cancel)', i, plural, comp.line, elapsed_ms) elapsed_ms = (time.time() - start_time) * 1000.0 plural = '' if i == 1 else 'es' self.debug_f.log('Found %d match%s for %r in %d ms', i, plural, comp.line, elapsed_ms)
def _PostProcess(self, base_opts, dynamic_opts, user_spec, comp): """ Add trailing spaces / slashes to completion candidates, and time them. NOTE: This post-processing MUST go here, and not in UserSpec, because it's in READLINE in bash. compgen doesn't see it. """ self.progress_f.Write('Completing %r ... (Ctrl-C to cancel)', comp.line) start_time = time.time() # TODO: dedupe candidates? You can get two 'echo' in bash, which is dumb. i = 0 for m, is_fs_action in user_spec.Matches(comp): # - Do shell QUOTING here. Not just for filenames, but for everything! # User-defined functions can't emit $var, only \$var # Problem: COMP_WORDBREAKS messes things up! How can I account for that? # '_tmp/spam\ ' # it stops at the first ' ' char. # # I guess you can add a COMP_WORDBREAKS suffix? # Or should you get rid of completion_delims altogether? # Then you would be constantly completing the beginning of the line? # TODO: write a terminal program to show that #m = util.BackslashEscape(m, SHELL_META_CHARS) #self.debug_f.log('after shell escaping: %s', m) # SUBTLE: dynamic_opts is part of comp_state, which ShellFuncAction can # mutate! So we don't want to pull this out of the loop. opt_filenames = False if 'filenames' in dynamic_opts: opt_filenames = dynamic_opts['filenames'] if 'filenames' in base_opts: opt_filenames = base_opts['filenames'] # compopt -o filenames is for user-defined actions. Or any # FileSystemAction needs it. if is_fs_action or opt_filenames: if path_stat.isdir(m): # TODO: test coverage yield m + '/' continue opt_nospace = False if 'nospace' in dynamic_opts: opt_nospace = dynamic_opts['nospace'] if 'nospace' in base_opts: opt_nospace = base_opts['nospace'] if opt_nospace: yield m else: yield m + ' ' # NOTE: Can't use %.2f in production build! i += 1 elapsed_ms = (time.time() - start_time) * 1000.0 plural = '' if i == 1 else 'es' self.progress_f.Write( '... %d match%s for %r in %d ms (Ctrl-C to cancel)', i, plural, comp.line, elapsed_ms) elapsed_ms = (time.time() - start_time) * 1000.0 plural = '' if i == 1 else 'es' self.progress_f.Write('Found %d match%s for %r in %d ms', i, plural, comp.line, elapsed_ms)