1"""prompt-toolkit utilities
2
3Everything in this module is a private API,
4not to be used outside IPython.
5"""
6
7# Copyright (c) IPython Development Team.
8# Distributed under the terms of the Modified BSD License.
9
10import unicodedata
11from wcwidth import wcwidth
12
13from IPython.utils.py3compat import PY3
14
15from IPython.core.completer import IPCompleter
16from prompt_toolkit.completion import Completer, Completion
17from prompt_toolkit.layout.lexers import Lexer
18from prompt_toolkit.layout.lexers import PygmentsLexer
19
20import pygments.lexers as pygments_lexers
21
22
23class IPythonPTCompleter(Completer):
24    """Adaptor to provide IPython completions to prompt_toolkit"""
25    def __init__(self, ipy_completer=None, shell=None, patch_stdout=None):
26        if shell is None and ipy_completer is None:
27            raise TypeError("Please pass shell=an InteractiveShell instance.")
28        self._ipy_completer = ipy_completer
29        self.shell = shell
30        if patch_stdout is None:
31            raise TypeError("Please pass patch_stdout")
32        self.patch_stdout = patch_stdout
33
34    @property
35    def ipy_completer(self):
36        if self._ipy_completer:
37            return self._ipy_completer
38        else:
39            return self.shell.Completer
40
41    def get_completions(self, document, complete_event):
42        if not document.current_line.strip():
43            return
44
45        # Some bits of our completion system may print stuff (e.g. if a module
46        # is imported). This context manager ensures that doesn't interfere with
47        # the prompt.
48        with self.patch_stdout():
49            used, matches = self.ipy_completer.complete(
50                                line_buffer=document.current_line,
51                                cursor_pos=document.cursor_position_col
52            )
53        start_pos = -len(used)
54        for m in matches:
55            if not m:
56                # Guard against completion machinery giving us an empty string.
57                continue
58
59            m = unicodedata.normalize('NFC', m)
60
61            # When the first character of the completion has a zero length,
62            # then it's probably a decomposed unicode character. E.g. caused by
63            # the "\dot" completion. Try to compose again with the previous
64            # character.
65            if wcwidth(m[0]) == 0:
66                if document.cursor_position + start_pos > 0:
67                    char_before = document.text[document.cursor_position + start_pos - 1]
68                    m = unicodedata.normalize('NFC', char_before + m)
69
70                    # Yield the modified completion instead, if this worked.
71                    if wcwidth(m[0:1]) == 1:
72                        yield Completion(m, start_position=start_pos - 1)
73                        continue
74
75            # TODO: Use Jedi to determine meta_text
76            # (Jedi currently has a bug that results in incorrect information.)
77            # meta_text = ''
78            # yield Completion(m, start_position=start_pos,
79            #                  display_meta=meta_text)
80            yield Completion(m, start_position=start_pos)
81
82class IPythonPTLexer(Lexer):
83    """
84    Wrapper around PythonLexer and BashLexer.
85    """
86    def __init__(self):
87        l = pygments_lexers
88        self.python_lexer = PygmentsLexer(l.Python3Lexer if PY3 else l.PythonLexer)
89        self.shell_lexer = PygmentsLexer(l.BashLexer)
90
91        self.magic_lexers = {
92            'HTML': PygmentsLexer(l.HtmlLexer),
93            'html': PygmentsLexer(l.HtmlLexer),
94            'javascript': PygmentsLexer(l.JavascriptLexer),
95            'js': PygmentsLexer(l.JavascriptLexer),
96            'perl': PygmentsLexer(l.PerlLexer),
97            'ruby': PygmentsLexer(l.RubyLexer),
98            'latex': PygmentsLexer(l.TexLexer),
99        }
100
101    def lex_document(self, cli, document):
102        text = document.text.lstrip()
103
104        lexer = self.python_lexer
105
106        if text.startswith('!') or text.startswith('%%bash'):
107            lexer = self.shell_lexer
108
109        elif text.startswith('%%'):
110            for magic, l in self.magic_lexers.items():
111                if text.startswith('%%' + magic):
112                    lexer = l
113                    break
114
115        return lexer.lex_document(cli, document)
116