1"""
2Processors are little transformation blocks that transform the token list from
3a buffer before the BufferControl will render it to the screen.
4
5They can insert tokens before or after, or highlight fragments by replacing the
6token types.
7"""
8from __future__ import unicode_literals
9from abc import ABCMeta, abstractmethod
10from six import with_metaclass
11from six.moves import range
12
13from prompt_toolkit.cache import SimpleCache
14from prompt_toolkit.document import Document
15from prompt_toolkit.enums import SEARCH_BUFFER
16from prompt_toolkit.filters import to_cli_filter, ViInsertMultipleMode
17from prompt_toolkit.layout.utils import token_list_to_text
18from prompt_toolkit.reactive import Integer
19from prompt_toolkit.token import Token
20
21from .utils import token_list_len, explode_tokens
22
23import re
24
25__all__ = (
26    'Processor',
27    'Transformation',
28
29    'HighlightSearchProcessor',
30    'HighlightSelectionProcessor',
31    'PasswordProcessor',
32    'HighlightMatchingBracketProcessor',
33    'DisplayMultipleCursors',
34    'BeforeInput',
35    'AfterInput',
36    'AppendAutoSuggestion',
37    'ConditionalProcessor',
38    'ShowLeadingWhiteSpaceProcessor',
39    'ShowTrailingWhiteSpaceProcessor',
40    'TabsProcessor',
41)
42
43
44class Processor(with_metaclass(ABCMeta, object)):
45    """
46    Manipulate the tokens for a given line in a
47    :class:`~prompt_toolkit.layout.controls.BufferControl`.
48    """
49    @abstractmethod
50    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
51        """
52        Apply transformation.  Returns a :class:`.Transformation` instance.
53
54        :param cli: :class:`.CommandLineInterface` instance.
55        :param lineno: The number of the line to which we apply the processor.
56        :param source_to_display: A function that returns the position in the
57            `tokens` for any position in the source string. (This takes
58            previous processors into account.)
59        :param tokens: List of tokens that we can transform. (Received from the
60            previous processor.)
61        """
62        return Transformation(tokens)
63
64    def has_focus(self, cli):
65        """
66        Processors can override the focus.
67        (Used for the reverse-i-search prefix in DefaultPrompt.)
68        """
69        return False
70
71
72class Transformation(object):
73    """
74    Transformation result, as returned by :meth:`.Processor.apply_transformation`.
75
76    Important: Always make sure that the length of `document.text` is equal to
77               the length of all the text in `tokens`!
78
79    :param tokens: The transformed tokens. To be displayed, or to pass to the
80        next processor.
81    :param source_to_display: Cursor position transformation from original string to
82        transformed string.
83    :param display_to_source: Cursor position transformed from source string to
84        original string.
85    """
86    def __init__(self, tokens, source_to_display=None, display_to_source=None):
87        self.tokens = tokens
88        self.source_to_display = source_to_display or (lambda i: i)
89        self.display_to_source = display_to_source or (lambda i: i)
90
91
92class HighlightSearchProcessor(Processor):
93    """
94    Processor that highlights search matches in the document.
95    Note that this doesn't support multiline search matches yet.
96
97    :param preview_search: A Filter; when active it indicates that we take
98        the search text in real time while the user is typing, instead of the
99        last active search state.
100    """
101    def __init__(self, preview_search=False, search_buffer_name=SEARCH_BUFFER,
102                 get_search_state=None):
103        self.preview_search = to_cli_filter(preview_search)
104        self.search_buffer_name = search_buffer_name
105        self.get_search_state = get_search_state or (lambda cli: cli.search_state)
106
107    def _get_search_text(self, cli):
108        """
109        The text we are searching for.
110        """
111        # When the search buffer has focus, take that text.
112        if self.preview_search(cli) and cli.buffers[self.search_buffer_name].text:
113            return cli.buffers[self.search_buffer_name].text
114        # Otherwise, take the text of the last active search.
115        else:
116            return self.get_search_state(cli).text
117
118    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
119        search_text = self._get_search_text(cli)
120        searchmatch_current_token = (':', ) + Token.SearchMatch.Current
121        searchmatch_token = (':', ) + Token.SearchMatch
122
123        if search_text and not cli.is_returning:
124            # For each search match, replace the Token.
125            line_text = token_list_to_text(tokens)
126            tokens = explode_tokens(tokens)
127
128            flags = re.IGNORECASE if cli.is_ignoring_case else 0
129
130            # Get cursor column.
131            if document.cursor_position_row == lineno:
132                cursor_column = source_to_display(document.cursor_position_col)
133            else:
134                cursor_column = None
135
136            for match in re.finditer(re.escape(search_text), line_text, flags=flags):
137                if cursor_column is not None:
138                    on_cursor = match.start() <= cursor_column < match.end()
139                else:
140                    on_cursor = False
141
142                for i in range(match.start(), match.end()):
143                    old_token, text = tokens[i]
144                    if on_cursor:
145                        tokens[i] = (old_token + searchmatch_current_token, tokens[i][1])
146                    else:
147                        tokens[i] = (old_token + searchmatch_token, tokens[i][1])
148
149        return Transformation(tokens)
150
151
152class HighlightSelectionProcessor(Processor):
153    """
154    Processor that highlights the selection in the document.
155    """
156    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
157        selected_token = (':', ) + Token.SelectedText
158
159        # In case of selection, highlight all matches.
160        selection_at_line = document.selection_range_at_line(lineno)
161
162        if selection_at_line:
163            from_, to = selection_at_line
164            from_ = source_to_display(from_)
165            to = source_to_display(to)
166
167            tokens = explode_tokens(tokens)
168
169            if from_ == 0 and to == 0 and len(tokens) == 0:
170                # When this is an empty line, insert a space in order to
171                # visualiase the selection.
172                return Transformation([(Token.SelectedText, ' ')])
173            else:
174                for i in range(from_, to + 1):
175                    if i < len(tokens):
176                        old_token, old_text = tokens[i]
177                        tokens[i] = (old_token + selected_token, old_text)
178
179        return Transformation(tokens)
180
181
182class PasswordProcessor(Processor):
183    """
184    Processor that turns masks the input. (For passwords.)
185
186    :param char: (string) Character to be used. "*" by default.
187    """
188    def __init__(self, char='*'):
189        self.char = char
190
191    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
192        tokens = [(token, self.char * len(text)) for token, text in tokens]
193        return Transformation(tokens)
194
195
196class HighlightMatchingBracketProcessor(Processor):
197    """
198    When the cursor is on or right after a bracket, it highlights the matching
199    bracket.
200
201    :param max_cursor_distance: Only highlight matching brackets when the
202        cursor is within this distance. (From inside a `Processor`, we can't
203        know which lines will be visible on the screen. But we also don't want
204        to scan the whole document for matching brackets on each key press, so
205        we limit to this value.)
206    """
207    _closing_braces = '])}>'
208
209    def __init__(self, chars='[](){}<>', max_cursor_distance=1000):
210        self.chars = chars
211        self.max_cursor_distance = max_cursor_distance
212
213        self._positions_cache = SimpleCache(maxsize=8)
214
215    def _get_positions_to_highlight(self, document):
216        """
217        Return a list of (row, col) tuples that need to be highlighted.
218        """
219        # Try for the character under the cursor.
220        if document.current_char and document.current_char in self.chars:
221            pos = document.find_matching_bracket_position(
222                    start_pos=document.cursor_position - self.max_cursor_distance,
223                    end_pos=document.cursor_position + self.max_cursor_distance)
224
225        # Try for the character before the cursor.
226        elif (document.char_before_cursor and document.char_before_cursor in
227              self._closing_braces and document.char_before_cursor in self.chars):
228            document = Document(document.text, document.cursor_position - 1)
229
230            pos = document.find_matching_bracket_position(
231                    start_pos=document.cursor_position - self.max_cursor_distance,
232                    end_pos=document.cursor_position + self.max_cursor_distance)
233        else:
234            pos = None
235
236        # Return a list of (row, col) tuples that need to be highlighted.
237        if pos:
238            pos += document.cursor_position  # pos is relative.
239            row, col = document.translate_index_to_position(pos)
240            return [(row, col), (document.cursor_position_row, document.cursor_position_col)]
241        else:
242            return []
243
244    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
245        # Get the highlight positions.
246        key = (cli.render_counter, document.text, document.cursor_position)
247        positions = self._positions_cache.get(
248            key, lambda: self._get_positions_to_highlight(document))
249
250        # Apply if positions were found at this line.
251        if positions:
252            for row, col in positions:
253                if row == lineno:
254                    col = source_to_display(col)
255                    tokens = explode_tokens(tokens)
256                    token, text = tokens[col]
257
258                    if col == document.cursor_position_col:
259                        token += (':', ) + Token.MatchingBracket.Cursor
260                    else:
261                        token += (':', ) + Token.MatchingBracket.Other
262
263                    tokens[col] = (token, text)
264
265        return Transformation(tokens)
266
267
268class DisplayMultipleCursors(Processor):
269    """
270    When we're in Vi block insert mode, display all the cursors.
271    """
272    _insert_multiple =  ViInsertMultipleMode()
273
274    def __init__(self, buffer_name):
275        self.buffer_name = buffer_name
276
277    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
278        buff = cli.buffers[self.buffer_name]
279
280        if self._insert_multiple(cli):
281            positions = buff.multiple_cursor_positions
282            tokens = explode_tokens(tokens)
283
284            # If any cursor appears on the current line, highlight that.
285            start_pos = document.translate_row_col_to_index(lineno, 0)
286            end_pos = start_pos + len(document.lines[lineno])
287
288            token_suffix = (':', ) + Token.MultipleCursors.Cursor
289
290            for p in positions:
291                if start_pos <= p < end_pos:
292                    column = source_to_display(p - start_pos)
293
294                    # Replace token.
295                    token, text = tokens[column]
296                    token += token_suffix
297                    tokens[column] = (token, text)
298                elif p == end_pos:
299                    tokens.append((token_suffix, ' '))
300
301            return Transformation(tokens)
302        else:
303            return Transformation(tokens)
304
305
306class BeforeInput(Processor):
307    """
308    Insert tokens before the input.
309
310    :param get_tokens: Callable that takes a
311        :class:`~prompt_toolkit.interface.CommandLineInterface` and returns the
312        list of tokens to be inserted.
313    """
314    def __init__(self, get_tokens):
315        assert callable(get_tokens)
316        self.get_tokens = get_tokens
317
318    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
319        if lineno == 0:
320            tokens_before = self.get_tokens(cli)
321            tokens = tokens_before + tokens
322
323            shift_position = token_list_len(tokens_before)
324            source_to_display = lambda i: i + shift_position
325            display_to_source = lambda i: i - shift_position
326        else:
327            source_to_display = None
328            display_to_source = None
329
330        return Transformation(tokens, source_to_display=source_to_display,
331                              display_to_source=display_to_source)
332
333    @classmethod
334    def static(cls, text, token=Token):
335        """
336        Create a :class:`.BeforeInput` instance that always inserts the same
337        text.
338        """
339        def get_static_tokens(cli):
340            return [(token, text)]
341        return cls(get_static_tokens)
342
343    def __repr__(self):
344        return '%s(get_tokens=%r)' % (
345            self.__class__.__name__, self.get_tokens)
346
347
348class AfterInput(Processor):
349    """
350    Insert tokens after the input.
351
352    :param get_tokens: Callable that takes a
353        :class:`~prompt_toolkit.interface.CommandLineInterface` and returns the
354        list of tokens to be appended.
355    """
356    def __init__(self, get_tokens):
357        assert callable(get_tokens)
358        self.get_tokens = get_tokens
359
360    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
361        # Insert tokens after the last line.
362        if lineno == document.line_count - 1:
363            return Transformation(tokens=tokens + self.get_tokens(cli))
364        else:
365            return Transformation(tokens=tokens)
366
367    @classmethod
368    def static(cls, text, token=Token):
369        """
370        Create a :class:`.AfterInput` instance that always inserts the same
371        text.
372        """
373        def get_static_tokens(cli):
374            return [(token, text)]
375        return cls(get_static_tokens)
376
377    def __repr__(self):
378        return '%s(get_tokens=%r)' % (
379            self.__class__.__name__, self.get_tokens)
380
381
382class AppendAutoSuggestion(Processor):
383    """
384    Append the auto suggestion to the input.
385    (The user can then press the right arrow the insert the suggestion.)
386
387    :param buffer_name: The name of the buffer from where we should take the
388        auto suggestion. If not given, we take the current buffer.
389    """
390    def __init__(self, buffer_name=None, token=Token.AutoSuggestion):
391        self.buffer_name = buffer_name
392        self.token = token
393
394    def _get_buffer(self, cli):
395        if self.buffer_name:
396            return cli.buffers[self.buffer_name]
397        else:
398            return cli.current_buffer
399
400    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
401        # Insert tokens after the last line.
402        if lineno == document.line_count - 1:
403            buffer = self._get_buffer(cli)
404
405            if buffer.suggestion and buffer.document.is_cursor_at_the_end:
406                suggestion = buffer.suggestion.text
407            else:
408                suggestion = ''
409
410            return Transformation(tokens=tokens + [(self.token, suggestion)])
411        else:
412            return Transformation(tokens=tokens)
413
414
415class ShowLeadingWhiteSpaceProcessor(Processor):
416    """
417    Make leading whitespace visible.
418
419    :param get_char: Callable that takes a :class:`CommandLineInterface`
420        instance and returns one character.
421    :param token: Token to be used.
422    """
423    def __init__(self, get_char=None, token=Token.LeadingWhiteSpace):
424        assert get_char is None or callable(get_char)
425
426        if get_char is None:
427            def get_char(cli):
428                if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?':
429                    return '.'
430                else:
431                    return '\xb7'
432
433        self.token = token
434        self.get_char = get_char
435
436    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
437        # Walk through all te tokens.
438        if tokens and token_list_to_text(tokens).startswith(' '):
439            t = (self.token, self.get_char(cli))
440            tokens = explode_tokens(tokens)
441
442            for i in range(len(tokens)):
443                if tokens[i][1] == ' ':
444                    tokens[i] = t
445                else:
446                    break
447
448        return Transformation(tokens)
449
450
451class ShowTrailingWhiteSpaceProcessor(Processor):
452    """
453    Make trailing whitespace visible.
454
455    :param get_char: Callable that takes a :class:`CommandLineInterface`
456        instance and returns one character.
457    :param token: Token to be used.
458    """
459    def __init__(self, get_char=None, token=Token.TrailingWhiteSpace):
460        assert get_char is None or callable(get_char)
461
462        if get_char is None:
463            def get_char(cli):
464                if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?':
465                    return '.'
466                else:
467                    return '\xb7'
468
469        self.token = token
470        self.get_char = get_char
471
472
473    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
474        if tokens and tokens[-1][1].endswith(' '):
475            t = (self.token, self.get_char(cli))
476            tokens = explode_tokens(tokens)
477
478            # Walk backwards through all te tokens and replace whitespace.
479            for i in range(len(tokens) - 1, -1, -1):
480                char = tokens[i][1]
481                if char == ' ':
482                    tokens[i] = t
483                else:
484                    break
485
486        return Transformation(tokens)
487
488
489class TabsProcessor(Processor):
490    """
491    Render tabs as spaces (instead of ^I) or make them visible (for instance,
492    by replacing them with dots.)
493
494    :param tabstop: (Integer) Horizontal space taken by a tab.
495    :param get_char1: Callable that takes a `CommandLineInterface` and return a
496        character (text of length one). This one is used for the first space
497        taken by the tab.
498    :param get_char2: Like `get_char1`, but for the rest of the space.
499    """
500    def __init__(self, tabstop=4, get_char1=None, get_char2=None, token=Token.Tab):
501        assert isinstance(tabstop, Integer)
502        assert get_char1 is None or callable(get_char1)
503        assert get_char2 is None or callable(get_char2)
504
505        self.get_char1 = get_char1 or get_char2 or (lambda cli: '|')
506        self.get_char2 = get_char2 or get_char1 or (lambda cli: '\u2508')
507        self.tabstop = tabstop
508        self.token = token
509
510    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
511        tabstop = int(self.tabstop)
512        token = self.token
513
514        # Create separator for tabs.
515        separator1 = self.get_char1(cli)
516        separator2 = self.get_char2(cli)
517
518        # Transform tokens.
519        tokens = explode_tokens(tokens)
520
521        position_mappings = {}
522        result_tokens = []
523        pos = 0
524
525        for i, token_and_text in enumerate(tokens):
526            position_mappings[i] = pos
527
528            if token_and_text[1] == '\t':
529                # Calculate how many characters we have to insert.
530                count = tabstop - (pos % tabstop)
531                if count == 0:
532                    count = tabstop
533
534                # Insert tab.
535                result_tokens.append((token, separator1))
536                result_tokens.append((token, separator2 * (count - 1)))
537                pos += count
538            else:
539                result_tokens.append(token_and_text)
540                pos += 1
541
542        position_mappings[len(tokens)] = pos
543
544        def source_to_display(from_position):
545            " Maps original cursor position to the new one. "
546            return position_mappings[from_position]
547
548        def display_to_source(display_pos):
549            " Maps display cursor position to the original one. "
550            position_mappings_reversed = dict((v, k) for k, v in position_mappings.items())
551
552            while display_pos >= 0:
553                try:
554                    return position_mappings_reversed[display_pos]
555                except KeyError:
556                    display_pos -= 1
557            return 0
558
559        return Transformation(
560            result_tokens,
561            source_to_display=source_to_display,
562            display_to_source=display_to_source)
563
564
565class ConditionalProcessor(Processor):
566    """
567    Processor that applies another processor, according to a certain condition.
568    Example::
569
570        # Create a function that returns whether or not the processor should
571        # currently be applied.
572        def highlight_enabled(cli):
573            return true_or_false
574
575        # Wrapt it in a `ConditionalProcessor` for usage in a `BufferControl`.
576        BufferControl(input_processors=[
577            ConditionalProcessor(HighlightSearchProcessor(),
578                                 Condition(highlight_enabled))])
579
580    :param processor: :class:`.Processor` instance.
581    :param filter: :class:`~prompt_toolkit.filters.CLIFilter` instance.
582    """
583    def __init__(self, processor, filter):
584        assert isinstance(processor, Processor)
585
586        self.processor = processor
587        self.filter = to_cli_filter(filter)
588
589    def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
590        # Run processor when enabled.
591        if self.filter(cli):
592            return self.processor.apply_transformation(
593                cli, document, lineno, source_to_display, tokens)
594        else:
595            return Transformation(tokens)
596
597    def has_focus(self, cli):
598        if self.filter(cli):
599            return self.processor.has_focus(cli)
600        else:
601            return False
602
603    def __repr__(self):
604        return '%s(processor=%r, filter=%r)' % (
605            self.__class__.__name__, self.processor, self.filter)
606