1from __future__ import annotations 
    2 
    3import os 
    4from typing import Callable, Iterable 
    5 
    6from prompt_toolkit.completion import CompleteEvent, Completer, Completion 
    7from prompt_toolkit.document import Document 
    8 
    9__all__ = [ 
    10    "PathCompleter", 
    11    "ExecutableCompleter", 
    12] 
    13 
    14 
    15class PathCompleter(Completer): 
    16    """ 
    17    Complete for Path variables. 
    18 
    19    :param get_paths: Callable which returns a list of directories to look into 
    20                      when the user enters a relative path. 
    21    :param file_filter: Callable which takes a filename and returns whether 
    22                        this file should show up in the completion. ``None`` 
    23                        when no filtering has to be done. 
    24    :param min_input_len: Don't do autocompletion when the input string is shorter. 
    25    """ 
    26 
    27    def __init__( 
    28        self, 
    29        only_directories: bool = False, 
    30        get_paths: Callable[[], list[str]] | None = None, 
    31        file_filter: Callable[[str], bool] | None = None, 
    32        min_input_len: int = 0, 
    33        expanduser: bool = False, 
    34    ) -> None: 
    35        self.only_directories = only_directories 
    36        self.get_paths = get_paths or (lambda: ["."]) 
    37        self.file_filter = file_filter or (lambda _: True) 
    38        self.min_input_len = min_input_len 
    39        self.expanduser = expanduser 
    40 
    41    def get_completions( 
    42        self, document: Document, complete_event: CompleteEvent 
    43    ) -> Iterable[Completion]: 
    44        text = document.text_before_cursor 
    45 
    46        # Complete only when we have at least the minimal input length, 
    47        # otherwise, we can too many results and autocompletion will become too 
    48        # heavy. 
    49        if len(text) < self.min_input_len: 
    50            return 
    51 
    52        try: 
    53            # Do tilde expansion. 
    54            if self.expanduser: 
    55                text = os.path.expanduser(text) 
    56 
    57            # Directories where to look. 
    58            dirname = os.path.dirname(text) 
    59            if dirname: 
    60                directories = [ 
    61                    os.path.dirname(os.path.join(p, text)) for p in self.get_paths() 
    62                ] 
    63            else: 
    64                directories = self.get_paths() 
    65 
    66            # Start of current file. 
    67            prefix = os.path.basename(text) 
    68 
    69            # Get all filenames. 
    70            filenames = [] 
    71            for directory in directories: 
    72                # Look for matches in this directory. 
    73                if os.path.isdir(directory): 
    74                    for filename in os.listdir(directory): 
    75                        if filename.startswith(prefix): 
    76                            filenames.append((directory, filename)) 
    77 
    78            # Sort 
    79            filenames = sorted(filenames, key=lambda k: k[1]) 
    80 
    81            # Yield them. 
    82            for directory, filename in filenames: 
    83                completion = filename[len(prefix) :] 
    84                full_name = os.path.join(directory, filename) 
    85 
    86                if os.path.isdir(full_name): 
    87                    # For directories, add a slash to the filename. 
    88                    # (We don't add them to the `completion`. Users can type it 
    89                    # to trigger the autocompletion themselves.) 
    90                    filename += "/" 
    91                elif self.only_directories: 
    92                    continue 
    93 
    94                if not self.file_filter(full_name): 
    95                    continue 
    96 
    97                yield Completion( 
    98                    text=completion, 
    99                    start_position=0, 
    100                    display=filename, 
    101                ) 
    102        except OSError: 
    103            pass 
    104 
    105 
    106class ExecutableCompleter(PathCompleter): 
    107    """ 
    108    Complete only executable files in the current path. 
    109    """ 
    110 
    111    def __init__(self) -> None: 
    112        super().__init__( 
    113            only_directories=False, 
    114            min_input_len=1, 
    115            get_paths=lambda: os.environ.get("PATH", "").split(os.pathsep), 
    116            file_filter=lambda name: os.access(name, os.X_OK), 
    117            expanduser=True, 
    118        )