diff options
| author | Devtools Arcadia <[email protected]> | 2022-02-07 18:08:42 +0300 | 
|---|---|---|
| committer | Devtools Arcadia <[email protected]> | 2022-02-07 18:08:42 +0300 | 
| commit | 1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch) | |
| tree | e26c9fed0de5d9873cce7e00bc214573dc2195b7 /contrib/python/prompt-toolkit/py3/prompt_toolkit/completion | |
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'contrib/python/prompt-toolkit/py3/prompt_toolkit/completion')
7 files changed, 1002 insertions, 0 deletions
| diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/__init__.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/__init__.py new file mode 100644 index 00000000000..270c74337c8 --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/__init__.py @@ -0,0 +1,41 @@ +from .base import ( +    CompleteEvent, +    Completer, +    Completion, +    ConditionalCompleter, +    DummyCompleter, +    DynamicCompleter, +    ThreadedCompleter, +    get_common_complete_suffix, +    merge_completers, +) +from .deduplicate import DeduplicateCompleter +from .filesystem import ExecutableCompleter, PathCompleter +from .fuzzy_completer import FuzzyCompleter, FuzzyWordCompleter +from .nested import NestedCompleter +from .word_completer import WordCompleter + +__all__ = [ +    # Base. +    "Completion", +    "Completer", +    "ThreadedCompleter", +    "DummyCompleter", +    "DynamicCompleter", +    "CompleteEvent", +    "ConditionalCompleter", +    "merge_completers", +    "get_common_complete_suffix", +    # Filesystem. +    "PathCompleter", +    "ExecutableCompleter", +    # Fuzzy +    "FuzzyCompleter", +    "FuzzyWordCompleter", +    # Nested. +    "NestedCompleter", +    # Word completer. +    "WordCompleter", +    # Deduplicate +    "DeduplicateCompleter", +] diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/base.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/base.py new file mode 100644 index 00000000000..3bfde97d10d --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/base.py @@ -0,0 +1,398 @@ +""" +""" +from abc import ABCMeta, abstractmethod +from typing import AsyncGenerator, Callable, Iterable, Optional, Sequence + +from prompt_toolkit.document import Document +from prompt_toolkit.eventloop import generator_to_async_generator +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples + +__all__ = [ +    "Completion", +    "Completer", +    "ThreadedCompleter", +    "DummyCompleter", +    "DynamicCompleter", +    "CompleteEvent", +    "ConditionalCompleter", +    "merge_completers", +    "get_common_complete_suffix", +] + + +class Completion: +    """ +    :param text: The new string that will be inserted into the document. +    :param start_position: Position relative to the cursor_position where the +        new text will start. The text will be inserted between the +        start_position and the original cursor position. +    :param display: (optional string or formatted text) If the completion has +        to be displayed differently in the completion menu. +    :param display_meta: (Optional string or formatted text) Meta information +        about the completion, e.g. the path or source where it's coming from. +        This can also be a callable that returns a string. +    :param style: Style string. +    :param selected_style: Style string, used for a selected completion. +        This can override the `style` parameter. +    """ + +    def __init__( +        self, +        text: str, +        start_position: int = 0, +        display: Optional[AnyFormattedText] = None, +        display_meta: Optional[AnyFormattedText] = None, +        style: str = "", +        selected_style: str = "", +    ) -> None: + +        from prompt_toolkit.formatted_text import to_formatted_text + +        self.text = text +        self.start_position = start_position +        self._display_meta = display_meta + +        if display is None: +            display = text + +        self.display = to_formatted_text(display) + +        self.style = style +        self.selected_style = selected_style + +        assert self.start_position <= 0 + +    def __repr__(self) -> str: +        if isinstance(self.display, str) and self.display == self.text: +            return "%s(text=%r, start_position=%r)" % ( +                self.__class__.__name__, +                self.text, +                self.start_position, +            ) +        else: +            return "%s(text=%r, start_position=%r, display=%r)" % ( +                self.__class__.__name__, +                self.text, +                self.start_position, +                self.display, +            ) + +    def __eq__(self, other: object) -> bool: +        if not isinstance(other, Completion): +            return False +        return ( +            self.text == other.text +            and self.start_position == other.start_position +            and self.display == other.display +            and self._display_meta == other._display_meta +        ) + +    def __hash__(self) -> int: +        return hash((self.text, self.start_position, self.display, self._display_meta)) + +    @property +    def display_text(self) -> str: +        "The 'display' field as plain text." +        from prompt_toolkit.formatted_text import fragment_list_to_text + +        return fragment_list_to_text(self.display) + +    @property +    def display_meta(self) -> StyleAndTextTuples: +        "Return meta-text. (This is lazy when using a callable)." +        from prompt_toolkit.formatted_text import to_formatted_text + +        return to_formatted_text(self._display_meta or "") + +    @property +    def display_meta_text(self) -> str: +        "The 'meta' field as plain text." +        from prompt_toolkit.formatted_text import fragment_list_to_text + +        return fragment_list_to_text(self.display_meta) + +    def new_completion_from_position(self, position: int) -> "Completion": +        """ +        (Only for internal use!) +        Get a new completion by splitting this one. Used by `Application` when +        it needs to have a list of new completions after inserting the common +        prefix. +        """ +        assert position - self.start_position >= 0 + +        return Completion( +            text=self.text[position - self.start_position :], +            display=self.display, +            display_meta=self._display_meta, +        ) + + +class CompleteEvent: +    """ +    Event that called the completer. + +    :param text_inserted: When True, it means that completions are requested +        because of a text insert. (`Buffer.complete_while_typing`.) +    :param completion_requested: When True, it means that the user explicitly +        pressed the `Tab` key in order to view the completions. + +    These two flags can be used for instance to implement a completer that +    shows some completions when ``Tab`` has been pressed, but not +    automatically when the user presses a space. (Because of +    `complete_while_typing`.) +    """ + +    def __init__( +        self, text_inserted: bool = False, completion_requested: bool = False +    ) -> None: +        assert not (text_inserted and completion_requested) + +        #: Automatic completion while typing. +        self.text_inserted = text_inserted + +        #: Used explicitly requested completion by pressing 'tab'. +        self.completion_requested = completion_requested + +    def __repr__(self) -> str: +        return "%s(text_inserted=%r, completion_requested=%r)" % ( +            self.__class__.__name__, +            self.text_inserted, +            self.completion_requested, +        ) + + +class Completer(metaclass=ABCMeta): +    """ +    Base class for completer implementations. +    """ + +    @abstractmethod +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        """ +        This should be a generator that yields :class:`.Completion` instances. + +        If the generation of completions is something expensive (that takes a +        lot of time), consider wrapping this `Completer` class in a +        `ThreadedCompleter`. In that case, the completer algorithm runs in a +        background thread and completions will be displayed as soon as they +        arrive. + +        :param document: :class:`~prompt_toolkit.document.Document` instance. +        :param complete_event: :class:`.CompleteEvent` instance. +        """ +        while False: +            yield + +    async def get_completions_async( +        self, document: Document, complete_event: CompleteEvent +    ) -> AsyncGenerator[Completion, None]: +        """ +        Asynchronous generator for completions. (Probably, you won't have to +        override this.) + +        Asynchronous generator of :class:`.Completion` objects. +        """ +        for item in self.get_completions(document, complete_event): +            yield item + + +class ThreadedCompleter(Completer): +    """ +    Wrapper that runs the `get_completions` generator in a thread. + +    (Use this to prevent the user interface from becoming unresponsive if the +    generation of completions takes too much time.) + +    The completions will be displayed as soon as they are produced. The user +    can already select a completion, even if not all completions are displayed. +    """ + +    def __init__(self, completer: Completer) -> None: +        self.completer = completer + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        return self.completer.get_completions(document, complete_event) + +    async def get_completions_async( +        self, document: Document, complete_event: CompleteEvent +    ) -> AsyncGenerator[Completion, None]: +        """ +        Asynchronous generator of completions. +        """ +        async for completion in generator_to_async_generator( +            lambda: self.completer.get_completions(document, complete_event) +        ): +            yield completion + +    def __repr__(self) -> str: +        return "ThreadedCompleter(%r)" % (self.completer,) + + +class DummyCompleter(Completer): +    """ +    A completer that doesn't return any completion. +    """ + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        return [] + +    def __repr__(self) -> str: +        return "DummyCompleter()" + + +class DynamicCompleter(Completer): +    """ +    Completer class that can dynamically returns any Completer. + +    :param get_completer: Callable that returns a :class:`.Completer` instance. +    """ + +    def __init__(self, get_completer: Callable[[], Optional[Completer]]) -> None: +        self.get_completer = get_completer + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        completer = self.get_completer() or DummyCompleter() +        return completer.get_completions(document, complete_event) + +    async def get_completions_async( +        self, document: Document, complete_event: CompleteEvent +    ) -> AsyncGenerator[Completion, None]: +        completer = self.get_completer() or DummyCompleter() + +        async for completion in completer.get_completions_async( +            document, complete_event +        ): +            yield completion + +    def __repr__(self) -> str: +        return "DynamicCompleter(%r -> %r)" % (self.get_completer, self.get_completer()) + + +class ConditionalCompleter(Completer): +    """ +    Wrapper around any other completer that will enable/disable the completions +    depending on whether the received condition is satisfied. + +    :param completer: :class:`.Completer` instance. +    :param filter: :class:`.Filter` instance. +    """ + +    def __init__(self, completer: Completer, filter: FilterOrBool) -> None: +        self.completer = completer +        self.filter = to_filter(filter) + +    def __repr__(self) -> str: +        return "ConditionalCompleter(%r, filter=%r)" % (self.completer, self.filter) + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        # Get all completions in a blocking way. +        if self.filter(): +            for c in self.completer.get_completions(document, complete_event): +                yield c + +    async def get_completions_async( +        self, document: Document, complete_event: CompleteEvent +    ) -> AsyncGenerator[Completion, None]: + +        # Get all completions in a non-blocking way. +        if self.filter(): +            async for item in self.completer.get_completions_async( +                document, complete_event +            ): +                yield item + + +class _MergedCompleter(Completer): +    """ +    Combine several completers into one. +    """ + +    def __init__(self, completers: Sequence[Completer]) -> None: +        self.completers = completers + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        # Get all completions from the other completers in a blocking way. +        for completer in self.completers: +            for c in completer.get_completions(document, complete_event): +                yield c + +    async def get_completions_async( +        self, document: Document, complete_event: CompleteEvent +    ) -> AsyncGenerator[Completion, None]: + +        # Get all completions from the other completers in a non-blocking way. +        for completer in self.completers: +            async for item in completer.get_completions_async(document, complete_event): +                yield item + + +def merge_completers( +    completers: Sequence[Completer], deduplicate: bool = False +) -> Completer: +    """ +    Combine several completers into one. + +    :param deduplicate: If `True`, wrap the result in a `DeduplicateCompleter` +        so that completions that would result in the same text will be +        deduplicated. +    """ +    if deduplicate: +        from .deduplicate import DeduplicateCompleter + +        return DeduplicateCompleter(_MergedCompleter(completers)) + +    return _MergedCompleter(completers) + + +def get_common_complete_suffix( +    document: Document, completions: Sequence[Completion] +) -> str: +    """ +    Return the common prefix for all completions. +    """ +    # Take only completions that don't change the text before the cursor. +    def doesnt_change_before_cursor(completion: Completion) -> bool: +        end = completion.text[: -completion.start_position] +        return document.text_before_cursor.endswith(end) + +    completions2 = [c for c in completions if doesnt_change_before_cursor(c)] + +    # When there is at least one completion that changes the text before the +    # cursor, don't return any common part. +    if len(completions2) != len(completions): +        return "" + +    # Return the common prefix. +    def get_suffix(completion: Completion) -> str: +        return completion.text[-completion.start_position :] + +    return _commonprefix([get_suffix(c) for c in completions2]) + + +def _commonprefix(strings: Iterable[str]) -> str: +    # Similar to os.path.commonprefix +    if not strings: +        return "" + +    else: +        s1 = min(strings) +        s2 = max(strings) + +        for i, c in enumerate(s1): +            if c != s2[i]: +                return s1[:i] + +        return s1 diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/deduplicate.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/deduplicate.py new file mode 100644 index 00000000000..6ef95224a6c --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/deduplicate.py @@ -0,0 +1,43 @@ +from typing import Iterable, Set + +from prompt_toolkit.document import Document + +from .base import CompleteEvent, Completer, Completion + +__all__ = ["DeduplicateCompleter"] + + +class DeduplicateCompleter(Completer): +    """ +    Wrapper around a completer that removes duplicates. Only the first unique +    completions are kept. + +    Completions are considered to be a duplicate if they result in the same +    document text when they would be applied. +    """ + +    def __init__(self, completer: Completer) -> None: +        self.completer = completer + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        # Keep track of the document strings we'd get after applying any completion. +        found_so_far: Set[str] = set() + +        for completion in self.completer.get_completions(document, complete_event): +            text_if_applied = ( +                document.text[: document.cursor_position + completion.start_position] +                + completion.text +                + document.text[document.cursor_position :] +            ) + +            if text_if_applied == document.text: +                # Don't include completions that don't have any effect at all. +                continue + +            if text_if_applied in found_so_far: +                continue + +            found_so_far.add(text_if_applied) +            yield completion diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/filesystem.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/filesystem.py new file mode 100644 index 00000000000..719eac6097d --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/filesystem.py @@ -0,0 +1,117 @@ +import os +from typing import Callable, Iterable, List, Optional + +from prompt_toolkit.completion import CompleteEvent, Completer, Completion +from prompt_toolkit.document import Document + +__all__ = [ +    "PathCompleter", +    "ExecutableCompleter", +] + + +class PathCompleter(Completer): +    """ +    Complete for Path variables. + +    :param get_paths: Callable which returns a list of directories to look into +                      when the user enters a relative path. +    :param file_filter: Callable which takes a filename and returns whether +                        this file should show up in the completion. ``None`` +                        when no filtering has to be done. +    :param min_input_len: Don't do autocompletion when the input string is shorter. +    """ + +    def __init__( +        self, +        only_directories: bool = False, +        get_paths: Optional[Callable[[], List[str]]] = None, +        file_filter: Optional[Callable[[str], bool]] = None, +        min_input_len: int = 0, +        expanduser: bool = False, +    ) -> None: + +        self.only_directories = only_directories +        self.get_paths = get_paths or (lambda: ["."]) +        self.file_filter = file_filter or (lambda _: True) +        self.min_input_len = min_input_len +        self.expanduser = expanduser + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        text = document.text_before_cursor + +        # Complete only when we have at least the minimal input length, +        # otherwise, we can too many results and autocompletion will become too +        # heavy. +        if len(text) < self.min_input_len: +            return + +        try: +            # Do tilde expansion. +            if self.expanduser: +                text = os.path.expanduser(text) + +            # Directories where to look. +            dirname = os.path.dirname(text) +            if dirname: +                directories = [ +                    os.path.dirname(os.path.join(p, text)) for p in self.get_paths() +                ] +            else: +                directories = self.get_paths() + +            # Start of current file. +            prefix = os.path.basename(text) + +            # Get all filenames. +            filenames = [] +            for directory in directories: +                # Look for matches in this directory. +                if os.path.isdir(directory): +                    for filename in os.listdir(directory): +                        if filename.startswith(prefix): +                            filenames.append((directory, filename)) + +            # Sort +            filenames = sorted(filenames, key=lambda k: k[1]) + +            # Yield them. +            for directory, filename in filenames: +                completion = filename[len(prefix) :] +                full_name = os.path.join(directory, filename) + +                if os.path.isdir(full_name): +                    # For directories, add a slash to the filename. +                    # (We don't add them to the `completion`. Users can type it +                    # to trigger the autocompletion themselves.) +                    filename += "/" +                elif self.only_directories: +                    continue + +                if not self.file_filter(full_name): +                    continue + +                yield Completion( +                    text=completion, +                    start_position=0, +                    display=filename, +                ) +        except OSError: +            pass + + +class ExecutableCompleter(PathCompleter): +    """ +    Complete only executable files in the current path. +    """ + +    def __init__(self) -> None: +        super().__init__( +            only_directories=False, +            min_input_len=1, +            get_paths=lambda: os.environ.get("PATH", "").split(os.pathsep), +            file_filter=lambda name: os.access(name, os.X_OK), +            expanduser=True, +        ), diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/fuzzy_completer.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/fuzzy_completer.py new file mode 100644 index 00000000000..4f7c3ab5d68 --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/fuzzy_completer.py @@ -0,0 +1,201 @@ +import re +from typing import Callable, Dict, Iterable, List, NamedTuple, Optional, Tuple, Union + +from prompt_toolkit.document import Document +from prompt_toolkit.filters import FilterOrBool, to_filter +from prompt_toolkit.formatted_text import AnyFormattedText, StyleAndTextTuples + +from .base import CompleteEvent, Completer, Completion +from .word_completer import WordCompleter + +__all__ = [ +    "FuzzyCompleter", +    "FuzzyWordCompleter", +] + + +class FuzzyCompleter(Completer): +    """ +    Fuzzy completion. +    This wraps any other completer and turns it into a fuzzy completer. + +    If the list of words is: ["leopard" , "gorilla", "dinosaur", "cat", "bee"] +    Then trying to complete "oar" would yield "leopard" and "dinosaur", but not +    the others, because they match the regular expression 'o.*a.*r'. +    Similar, in another application "djm" could expand to "django_migrations". + +    The results are sorted by relevance, which is defined as the start position +    and the length of the match. + +    Notice that this is not really a tool to work around spelling mistakes, +    like what would be possible with difflib. The purpose is rather to have a +    quicker or more intuitive way to filter the given completions, especially +    when many completions have a common prefix. + +    Fuzzy algorithm is based on this post: +    https://blog.amjith.com/fuzzyfinder-in-10-lines-of-python + +    :param completer: A :class:`~.Completer` instance. +    :param WORD: When True, use WORD characters. +    :param pattern: Regex pattern which selects the characters before the +        cursor that are considered for the fuzzy matching. +    :param enable_fuzzy: (bool or `Filter`) Enabled the fuzzy behavior. For +        easily turning fuzzyness on or off according to a certain condition. +    """ + +    def __init__( +        self, +        completer: Completer, +        WORD: bool = False, +        pattern: Optional[str] = None, +        enable_fuzzy: FilterOrBool = True, +    ): + +        assert pattern is None or pattern.startswith("^") + +        self.completer = completer +        self.pattern = pattern +        self.WORD = WORD +        self.pattern = pattern +        self.enable_fuzzy = to_filter(enable_fuzzy) + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        if self.enable_fuzzy(): +            return self._get_fuzzy_completions(document, complete_event) +        else: +            return self.completer.get_completions(document, complete_event) + +    def _get_pattern(self) -> str: +        if self.pattern: +            return self.pattern +        if self.WORD: +            return r"[^\s]+" +        return "^[a-zA-Z0-9_]*" + +    def _get_fuzzy_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: + +        word_before_cursor = document.get_word_before_cursor( +            pattern=re.compile(self._get_pattern()) +        ) + +        # Get completions +        document2 = Document( +            text=document.text[: document.cursor_position - len(word_before_cursor)], +            cursor_position=document.cursor_position - len(word_before_cursor), +        ) + +        completions = list(self.completer.get_completions(document2, complete_event)) + +        fuzzy_matches: List[_FuzzyMatch] = [] + +        pat = ".*?".join(map(re.escape, word_before_cursor)) +        pat = "(?=({0}))".format(pat)  # lookahead regex to manage overlapping matches +        regex = re.compile(pat, re.IGNORECASE) +        for compl in completions: +            matches = list(regex.finditer(compl.text)) +            if matches: +                # Prefer the match, closest to the left, then shortest. +                best = min(matches, key=lambda m: (m.start(), len(m.group(1)))) +                fuzzy_matches.append( +                    _FuzzyMatch(len(best.group(1)), best.start(), compl) +                ) + +        def sort_key(fuzzy_match: "_FuzzyMatch") -> Tuple[int, int]: +            "Sort by start position, then by the length of the match." +            return fuzzy_match.start_pos, fuzzy_match.match_length + +        fuzzy_matches = sorted(fuzzy_matches, key=sort_key) + +        for match in fuzzy_matches: +            # Include these completions, but set the correct `display` +            # attribute and `start_position`. +            yield Completion( +                text=match.completion.text, +                start_position=match.completion.start_position +                - len(word_before_cursor), +                display_meta=match.completion.display_meta, +                display=self._get_display(match, word_before_cursor), +                style=match.completion.style, +            ) + +    def _get_display( +        self, fuzzy_match: "_FuzzyMatch", word_before_cursor: str +    ) -> AnyFormattedText: +        """ +        Generate formatted text for the display label. +        """ +        m = fuzzy_match +        word = m.completion.text + +        if m.match_length == 0: +            # No highlighting when we have zero length matches (no input text). +            # In this case, use the original display text (which can include +            # additional styling or characters). +            return m.completion.display + +        result: StyleAndTextTuples = [] + +        # Text before match. +        result.append(("class:fuzzymatch.outside", word[: m.start_pos])) + +        # The match itself. +        characters = list(word_before_cursor) + +        for c in word[m.start_pos : m.start_pos + m.match_length]: +            classname = "class:fuzzymatch.inside" +            if characters and c.lower() == characters[0].lower(): +                classname += ".character" +                del characters[0] + +            result.append((classname, c)) + +        # Text after match. +        result.append( +            ("class:fuzzymatch.outside", word[m.start_pos + m.match_length :]) +        ) + +        return result + + +class FuzzyWordCompleter(Completer): +    """ +    Fuzzy completion on a list of words. + +    (This is basically a `WordCompleter` wrapped in a `FuzzyCompleter`.) + +    :param words: List of words or callable that returns a list of words. +    :param meta_dict: Optional dict mapping words to their meta-information. +    :param WORD: When True, use WORD characters. +    """ + +    def __init__( +        self, +        words: Union[List[str], Callable[[], List[str]]], +        meta_dict: Optional[Dict[str, str]] = None, +        WORD: bool = False, +    ) -> None: + +        self.words = words +        self.meta_dict = meta_dict or {} +        self.WORD = WORD + +        self.word_completer = WordCompleter( +            words=self.words, WORD=self.WORD, meta_dict=self.meta_dict +        ) + +        self.fuzzy_completer = FuzzyCompleter(self.word_completer, WORD=self.WORD) + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        return self.fuzzy_completer.get_completions(document, complete_event) + + +_FuzzyMatch = NamedTuple( +    "_FuzzyMatch", +    [("match_length", int), ("start_pos", int), ("completion", Completion)], +) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/nested.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/nested.py new file mode 100644 index 00000000000..8b0978ca32e --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/nested.py @@ -0,0 +1,109 @@ +""" +Nestedcompleter for completion of hierarchical data structures. +""" +from typing import Any, Dict, Iterable, Mapping, Optional, Set, Union + +from prompt_toolkit.completion import CompleteEvent, Completer, Completion +from prompt_toolkit.completion.word_completer import WordCompleter +from prompt_toolkit.document import Document + +__all__ = ["NestedCompleter"] + +# NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]] +NestedDict = Mapping[str, Union[Any, Set[str], None, Completer]] + + +class NestedCompleter(Completer): +    """ +    Completer which wraps around several other completers, and calls any the +    one that corresponds with the first word of the input. + +    By combining multiple `NestedCompleter` instances, we can achieve multiple +    hierarchical levels of autocompletion. This is useful when `WordCompleter` +    is not sufficient. + +    If you need multiple levels, check out the `from_nested_dict` classmethod. +    """ + +    def __init__( +        self, options: Dict[str, Optional[Completer]], ignore_case: bool = True +    ) -> None: + +        self.options = options +        self.ignore_case = ignore_case + +    def __repr__(self) -> str: +        return "NestedCompleter(%r, ignore_case=%r)" % (self.options, self.ignore_case) + +    @classmethod +    def from_nested_dict(cls, data: NestedDict) -> "NestedCompleter": +        """ +        Create a `NestedCompleter`, starting from a nested dictionary data +        structure, like this: + +        .. code:: + +            data = { +                'show': { +                    'version': None, +                    'interfaces': None, +                    'clock': None, +                    'ip': {'interface': {'brief'}} +                }, +                'exit': None +                'enable': None +            } + +        The value should be `None` if there is no further completion at some +        point. If all values in the dictionary are None, it is also possible to +        use a set instead. + +        Values in this data structure can be a completers as well. +        """ +        options: Dict[str, Optional[Completer]] = {} +        for key, value in data.items(): +            if isinstance(value, Completer): +                options[key] = value +            elif isinstance(value, dict): +                options[key] = cls.from_nested_dict(value) +            elif isinstance(value, set): +                options[key] = cls.from_nested_dict({item: None for item in value}) +            else: +                assert value is None +                options[key] = None + +        return cls(options) + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        # Split document. +        text = document.text_before_cursor.lstrip() +        stripped_len = len(document.text_before_cursor) - len(text) + +        # If there is a space, check for the first term, and use a +        # subcompleter. +        if " " in text: +            first_term = text.split()[0] +            completer = self.options.get(first_term) + +            # If we have a sub completer, use this for the completions. +            if completer is not None: +                remaining_text = text[len(first_term) :].lstrip() +                move_cursor = len(text) - len(remaining_text) + stripped_len + +                new_document = Document( +                    remaining_text, +                    cursor_position=document.cursor_position - move_cursor, +                ) + +                for c in completer.get_completions(new_document, complete_event): +                    yield c + +        # No space in the input: behave exactly like `WordCompleter`. +        else: +            completer = WordCompleter( +                list(self.options.keys()), ignore_case=self.ignore_case +            ) +            for c in completer.get_completions(document, complete_event): +                yield c diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/word_completer.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/word_completer.py new file mode 100644 index 00000000000..28c133e921b --- /dev/null +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/completion/word_completer.py @@ -0,0 +1,93 @@ +from typing import Callable, Iterable, List, Mapping, Optional, Pattern, Union + +from prompt_toolkit.completion import CompleteEvent, Completer, Completion +from prompt_toolkit.document import Document +from prompt_toolkit.formatted_text import AnyFormattedText + +__all__ = [ +    "WordCompleter", +] + + +class WordCompleter(Completer): +    """ +    Simple autocompletion on a list of words. + +    :param words: List of words or callable that returns a list of words. +    :param ignore_case: If True, case-insensitive completion. +    :param meta_dict: Optional dict mapping words to their meta-text. (This +        should map strings to strings or formatted text.) +    :param WORD: When True, use WORD characters. +    :param sentence: When True, don't complete by comparing the word before the +        cursor, but by comparing all the text before the cursor. In this case, +        the list of words is just a list of strings, where each string can +        contain spaces. (Can not be used together with the WORD option.) +    :param match_middle: When True, match not only the start, but also in the +                         middle of the word. +    :param pattern: Optional compiled regex for finding the word before +        the cursor to complete. When given, use this regex pattern instead of +        default one (see document._FIND_WORD_RE) +    """ + +    def __init__( +        self, +        words: Union[List[str], Callable[[], List[str]]], +        ignore_case: bool = False, +        display_dict: Optional[Mapping[str, AnyFormattedText]] = None, +        meta_dict: Optional[Mapping[str, AnyFormattedText]] = None, +        WORD: bool = False, +        sentence: bool = False, +        match_middle: bool = False, +        pattern: Optional[Pattern[str]] = None, +    ) -> None: + +        assert not (WORD and sentence) + +        self.words = words +        self.ignore_case = ignore_case +        self.display_dict = display_dict or {} +        self.meta_dict = meta_dict or {} +        self.WORD = WORD +        self.sentence = sentence +        self.match_middle = match_middle +        self.pattern = pattern + +    def get_completions( +        self, document: Document, complete_event: CompleteEvent +    ) -> Iterable[Completion]: +        # Get list of words. +        words = self.words +        if callable(words): +            words = words() + +        # Get word/text before cursor. +        if self.sentence: +            word_before_cursor = document.text_before_cursor +        else: +            word_before_cursor = document.get_word_before_cursor( +                WORD=self.WORD, pattern=self.pattern +            ) + +        if self.ignore_case: +            word_before_cursor = word_before_cursor.lower() + +        def word_matches(word: str) -> bool: +            """True when the word before the cursor matches.""" +            if self.ignore_case: +                word = word.lower() + +            if self.match_middle: +                return word_before_cursor in word +            else: +                return word.startswith(word_before_cursor) + +        for a in words: +            if word_matches(a): +                display = self.display_dict.get(a, a) +                display_meta = self.meta_dict.get(a, "") +                yield Completion( +                    text=a, +                    start_position=-len(word_before_cursor), +                    display=display, +                    display_meta=display_meta, +                ) | 
