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