1"""
2`Fish-style <http://fishshell.com/>`_  like auto-suggestion.
3
4While a user types input in a certain buffer, suggestions are generated
5(asynchronously.) Usually, they are displayed after the input. When the cursor
6presses the right arrow and the cursor is at the end of the input, the
7suggestion will be inserted.
8
9If you want the auto suggestions to be asynchronous (in a background thread),
10because they take too much time, and could potentially block the event loop,
11then wrap the :class:`.AutoSuggest` instance into a
12:class:`.ThreadedAutoSuggest`.
13"""
14from __future__ import unicode_literals
15
16from abc import ABCMeta, abstractmethod
17
18from six import with_metaclass
19
20from .eventloop import Future, run_in_executor
21from .filters import to_filter
22
23__all__ = [
24    'Suggestion',
25    'AutoSuggest',
26    'ThreadedAutoSuggest',
27    'DummyAutoSuggest',
28    'AutoSuggestFromHistory',
29    'ConditionalAutoSuggest',
30    'DynamicAutoSuggest',
31]
32
33
34class Suggestion(object):
35    """
36    Suggestion returned by an auto-suggest algorithm.
37
38    :param text: The suggestion text.
39    """
40    def __init__(self, text):
41        self.text = text
42
43    def __repr__(self):
44        return 'Suggestion(%s)' % self.text
45
46
47class AutoSuggest(with_metaclass(ABCMeta, object)):
48    """
49    Base class for auto suggestion implementations.
50    """
51    @abstractmethod
52    def get_suggestion(self, buffer, document):
53        """
54        Return `None` or a :class:`.Suggestion` instance.
55
56        We receive both :class:`~prompt_toolkit.buffer.Buffer` and
57        :class:`~prompt_toolkit.document.Document`. The reason is that auto
58        suggestions are retrieved asynchronously. (Like completions.) The
59        buffer text could be changed in the meantime, but ``document`` contains
60        the buffer document like it was at the start of the auto suggestion
61        call. So, from here, don't access ``buffer.text``, but use
62        ``document.text`` instead.
63
64        :param buffer: The :class:`~prompt_toolkit.buffer.Buffer` instance.
65        :param document: The :class:`~prompt_toolkit.document.Document` instance.
66        """
67
68    def get_suggestion_future(self, buff, document):
69        """
70        Return a :class:`.Future` which is set when the suggestions are ready.
71        This function can be overloaded in order to provide an asynchronous
72        implementation.
73        """
74        return Future.succeed(self.get_suggestion(buff, document))
75
76
77class ThreadedAutoSuggest(AutoSuggest):
78    """
79    Wrapper that runs auto suggestions in a thread.
80    (Use this to prevent the user interface from becoming unresponsive if the
81    generation of suggestions takes too much time.)
82    """
83    def __init__(self, auto_suggest):
84        assert isinstance(auto_suggest, AutoSuggest)
85        self.auto_suggest = auto_suggest
86
87    def get_suggestion(self, buff, document):
88        return self.auto_suggest.get_suggestion(buff, document)
89
90    def get_suggestion_future(self, buff, document):
91        """
92        Run the `get_suggestion` function in a thread.
93        """
94        def run_get_suggestion_thread():
95            return self.get_suggestion(buff, document)
96        f = run_in_executor(run_get_suggestion_thread)
97        return f
98
99
100class DummyAutoSuggest(AutoSuggest):
101    """
102    AutoSuggest class that doesn't return any suggestion.
103    """
104    def get_suggestion(self, buffer, document):
105        return  # No suggestion
106
107
108class AutoSuggestFromHistory(AutoSuggest):
109    """
110    Give suggestions based on the lines in the history.
111    """
112    def get_suggestion(self, buffer, document):
113        history = buffer.history
114
115        # Consider only the last line for the suggestion.
116        text = document.text.rsplit('\n', 1)[-1]
117
118        # Only create a suggestion when this is not an empty line.
119        if text.strip():
120            # Find first matching line in history.
121            for string in reversed(list(history.get_strings())):
122                for line in reversed(string.splitlines()):
123                    if line.startswith(text):
124                        return Suggestion(line[len(text):])
125
126
127class ConditionalAutoSuggest(AutoSuggest):
128    """
129    Auto suggest that can be turned on and of according to a certain condition.
130    """
131    def __init__(self, auto_suggest, filter):
132        assert isinstance(auto_suggest, AutoSuggest)
133
134        self.auto_suggest = auto_suggest
135        self.filter = to_filter(filter)
136
137    def get_suggestion(self, buffer, document):
138        if self.filter():
139            return self.auto_suggest.get_suggestion(buffer, document)
140
141
142class DynamicAutoSuggest(AutoSuggest):
143    """
144    Validator class that can dynamically returns any Validator.
145
146    :param get_validator: Callable that returns a :class:`.Validator` instance.
147    """
148    def __init__(self, get_auto_suggest):
149        assert callable(get_auto_suggest)
150        self.get_auto_suggest = get_auto_suggest
151
152    def get_suggestion(self, buff, document):
153        auto_suggest = self.get_auto_suggest() or DummyAutoSuggest()
154        assert isinstance(auto_suggest, AutoSuggest)
155        return auto_suggest.get_suggestion(buff, document)
156
157    def get_suggestion_future(self, buff, document):
158        auto_suggest = self.get_auto_suggest() or DummyAutoSuggest()
159        assert isinstance(auto_suggest, AutoSuggest)
160        return auto_suggest.get_suggestion_future(buff, document)
161