##// END OF EJS Templates
forgotten sys
Matthias Bussonnier -
Show More
@@ -1,195 +1,196 b''
1 1 """prompt-toolkit utilities
2 2
3 3 Everything in this module is a private API,
4 4 not to be used outside IPython.
5 5 """
6 6
7 7 # Copyright (c) IPython Development Team.
8 8 # Distributed under the terms of the Modified BSD License.
9 9
10 10 import unicodedata
11 11 from wcwidth import wcwidth
12 12
13 13 from IPython.core.completer import (
14 14 provisionalcompleter, cursor_to_position,
15 15 _deduplicate_completions)
16 16 from prompt_toolkit.completion import Completer, Completion
17 17 from prompt_toolkit.lexers import Lexer
18 18 from prompt_toolkit.lexers import PygmentsLexer
19 19 from prompt_toolkit.patch_stdout import patch_stdout
20 20
21 21 import pygments.lexers as pygments_lexers
22 22 import os
23 import sys
23 24
24 25 _completion_sentinel = object()
25 26
26 27 def _elide_point(string:str, *, min_elide=30)->str:
27 28 """
28 29 If a string is long enough, and has at least 3 dots,
29 30 replace the middle part with ellipses.
30 31
31 32 If a string naming a file is long enough, and has at least 3 slashes,
32 33 replace the middle part with ellipses.
33 34
34 35 If three consecutive dots, or two consecutive dots are encountered these are
35 36 replaced by the equivalents HORIZONTAL ELLIPSIS or TWO DOT LEADER unicode
36 37 equivalents
37 38 """
38 39 string = string.replace('...','\N{HORIZONTAL ELLIPSIS}')
39 40 string = string.replace('..','\N{TWO DOT LEADER}')
40 41 if len(string) < min_elide:
41 42 return string
42 43
43 44 object_parts = string.split('.')
44 45 file_parts = string.split(os.sep)
45 46 if file_parts[-1] == '':
46 47 file_parts.pop()
47 48
48 49 if len(object_parts) > 3:
49 50 return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(object_parts[0], object_parts[1][0], object_parts[-2][-1], object_parts[-1])
50 51
51 52 elif len(file_parts) > 3:
52 53 return ('{}' + os.sep + '{}\N{HORIZONTAL ELLIPSIS}{}' + os.sep + '{}').format(file_parts[0], file_parts[1][0], file_parts[-2][-1], file_parts[-1])
53 54
54 55 return string
55 56
56 57 def _elide_typed(string:str, typed:str, *, min_elide:int=30)->str:
57 58 """
58 59 Elide the middle of a long string if the beginning has already been typed.
59 60 """
60 61
61 62 if len(string) < min_elide:
62 63 return string
63 64 cut_how_much = len(typed)-3
64 65 if cut_how_much < 7:
65 66 return string
66 67 if string.startswith(typed) and len(string)> len(typed):
67 68 return f"{string[:3]}\N{HORIZONTAL ELLIPSIS}{string[cut_how_much:]}"
68 69 return string
69 70
70 71 def _elide(string:str, typed:str, min_elide=30)->str:
71 72 return _elide_typed(
72 73 _elide_point(string, min_elide=min_elide),
73 74 typed, min_elide=min_elide)
74 75
75 76
76 77
77 78 def _adjust_completion_text_based_on_context(text, body, offset):
78 79 if text.endswith('=') and len(body) > offset and body[offset] == '=':
79 80 return text[:-1]
80 81 else:
81 82 return text
82 83
83 84
84 85 class IPythonPTCompleter(Completer):
85 86 """Adaptor to provide IPython completions to prompt_toolkit"""
86 87 def __init__(self, ipy_completer=None, shell=None):
87 88 if shell is None and ipy_completer is None:
88 89 raise TypeError("Please pass shell=an InteractiveShell instance.")
89 90 self._ipy_completer = ipy_completer
90 91 self.shell = shell
91 92
92 93 @property
93 94 def ipy_completer(self):
94 95 if self._ipy_completer:
95 96 return self._ipy_completer
96 97 else:
97 98 return self.shell.Completer
98 99
99 100 def get_completions(self, document, complete_event):
100 101 if not document.current_line.strip():
101 102 return
102 103 # Some bits of our completion system may print stuff (e.g. if a module
103 104 # is imported). This context manager ensures that doesn't interfere with
104 105 # the prompt.
105 106
106 107 with patch_stdout(), provisionalcompleter():
107 108 body = document.text
108 109 cursor_row = document.cursor_position_row
109 110 cursor_col = document.cursor_position_col
110 111 cursor_position = document.cursor_position
111 112 offset = cursor_to_position(body, cursor_row, cursor_col)
112 113 try:
113 114 yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
114 115 except Exception as e:
115 116 try:
116 117 exc_type, exc_value, exc_tb = sys.exc_info()
117 118 traceback.print_exception(exc_type, exc_value, exc_tb)
118 119 except AttributeError:
119 120 print('Unrecoverable Error in completions')
120 121
121 122 @staticmethod
122 123 def _get_completions(body, offset, cursor_position, ipyc):
123 124 """
124 125 Private equivalent of get_completions() use only for unit_testing.
125 126 """
126 127 debug = getattr(ipyc, 'debug', False)
127 128 completions = _deduplicate_completions(
128 129 body, ipyc.completions(body, offset))
129 130 for c in completions:
130 131 if not c.text:
131 132 # Guard against completion machinery giving us an empty string.
132 133 continue
133 134 text = unicodedata.normalize('NFC', c.text)
134 135 # When the first character of the completion has a zero length,
135 136 # then it's probably a decomposed unicode character. E.g. caused by
136 137 # the "\dot" completion. Try to compose again with the previous
137 138 # character.
138 139 if wcwidth(text[0]) == 0:
139 140 if cursor_position + c.start > 0:
140 141 char_before = body[c.start - 1]
141 142 fixed_text = unicodedata.normalize(
142 143 'NFC', char_before + text)
143 144
144 145 # Yield the modified completion instead, if this worked.
145 146 if wcwidth(text[0:1]) == 1:
146 147 yield Completion(fixed_text, start_position=c.start - offset - 1)
147 148 continue
148 149
149 150 # TODO: Use Jedi to determine meta_text
150 151 # (Jedi currently has a bug that results in incorrect information.)
151 152 # meta_text = ''
152 153 # yield Completion(m, start_position=start_pos,
153 154 # display_meta=meta_text)
154 155 display_text = c.text
155 156
156 157 adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset)
157 158 if c.type == 'function':
158 159 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()', body[c.start:c.end]), display_meta=c.type+c.signature)
159 160 else:
160 161 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text, body[c.start:c.end]), display_meta=c.type)
161 162
162 163 class IPythonPTLexer(Lexer):
163 164 """
164 165 Wrapper around PythonLexer and BashLexer.
165 166 """
166 167 def __init__(self):
167 168 l = pygments_lexers
168 169 self.python_lexer = PygmentsLexer(l.Python3Lexer)
169 170 self.shell_lexer = PygmentsLexer(l.BashLexer)
170 171
171 172 self.magic_lexers = {
172 173 'HTML': PygmentsLexer(l.HtmlLexer),
173 174 'html': PygmentsLexer(l.HtmlLexer),
174 175 'javascript': PygmentsLexer(l.JavascriptLexer),
175 176 'js': PygmentsLexer(l.JavascriptLexer),
176 177 'perl': PygmentsLexer(l.PerlLexer),
177 178 'ruby': PygmentsLexer(l.RubyLexer),
178 179 'latex': PygmentsLexer(l.TexLexer),
179 180 }
180 181
181 182 def lex_document(self, document):
182 183 text = document.text.lstrip()
183 184
184 185 lexer = self.python_lexer
185 186
186 187 if text.startswith('!') or text.startswith('%%bash'):
187 188 lexer = self.shell_lexer
188 189
189 190 elif text.startswith('%%'):
190 191 for magic, l in self.magic_lexers.items():
191 192 if text.startswith('%%' + magic):
192 193 lexer = l
193 194 break
194 195
195 196 return lexer.lex_document(document)
General Comments 0
You need to be logged in to leave comments. Login now