ptutils.py
204 lines
| 7.3 KiB
| text/x-python
|
PythonLexer
Min RK
|
r22768 | """prompt-toolkit utilities | ||
Everything in this module is a private API, | ||||
not to be used outside IPython. | ||||
""" | ||||
# Copyright (c) IPython Development Team. | ||||
# Distributed under the terms of the Modified BSD License. | ||||
Thomas Kluyver
|
r22388 | import unicodedata | ||
from wcwidth import wcwidth | ||||
Matthias Bussonnier
|
r23358 | from IPython.core.completer import ( | ||
Matthias Bussonnier
|
r23467 | provisionalcompleter, cursor_to_position, | ||
Matthias Bussonnier
|
r23358 | _deduplicate_completions) | ||
Thomas Kluyver
|
r22388 | from prompt_toolkit.completion import Completer, Completion | ||
Jonathan Slenders
|
r24376 | from prompt_toolkit.lexers import Lexer | ||
from prompt_toolkit.lexers import PygmentsLexer | ||||
from prompt_toolkit.patch_stdout import patch_stdout | ||||
Thomas Kluyver
|
r22388 | |||
Jonathan Slenders
|
r22512 | import pygments.lexers as pygments_lexers | ||
Elliott Morgan Jobson
|
r24827 | import os | ||
Matthias Bussonnier
|
r25872 | import sys | ||
Matthias Bussonnier
|
r25873 | import traceback | ||
Jonathan Slenders
|
r22512 | |||
Matthias Bussonnier
|
r23284 | _completion_sentinel = object() | ||
Matthias Bussonnier
|
r25690 | def _elide_point(string:str, *, min_elide=30)->str: | ||
Matthias Bussonnier
|
r23467 | """ | ||
Elliott Morgan Jobson
|
r24816 | If a string is long enough, and has at least 3 dots, | ||
replace the middle part with ellipses. | ||||
If a string naming a file is long enough, and has at least 3 slashes, | ||||
Matthias Bussonnier
|
r23467 | replace the middle part with ellipses. | ||
Matthias Bussonnier
|
r23591 | If three consecutive dots, or two consecutive dots are encountered these are | ||
replaced by the equivalents HORIZONTAL ELLIPSIS or TWO DOT LEADER unicode | ||||
equivalents | ||||
Matthias Bussonnier
|
r23467 | """ | ||
Matthias Bussonnier
|
r23591 | string = string.replace('...','\N{HORIZONTAL ELLIPSIS}') | ||
string = string.replace('..','\N{TWO DOT LEADER}') | ||||
Matthias Bussonnier
|
r23467 | if len(string) < min_elide: | ||
return string | ||||
Elliott Morgan Jobson
|
r24816 | object_parts = string.split('.') | ||
Elliott Morgan Jobson
|
r24827 | file_parts = string.split(os.sep) | ||
Matthias Bussonnier
|
r25486 | if file_parts[-1] == '': | ||
file_parts.pop() | ||||
Matthias Bussonnier
|
r23467 | |||
David Dorfman
|
r27740 | if len(object_parts) > 3: | ||
David Dorfman
|
r27741 | return "{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}".format( | ||
object_parts[0], | ||||
object_parts[1][:1], | ||||
object_parts[-2][-1:], | ||||
object_parts[-1], | ||||
) | ||||
Elliott Morgan Jobson
|
r24816 | |||
David Dorfman
|
r27740 | elif len(file_parts) > 3: | ||
David Dorfman
|
r27741 | return ("{}" + os.sep + "{}\N{HORIZONTAL ELLIPSIS}{}" + os.sep + "{}").format( | ||
file_parts[0], file_parts[1][:1], file_parts[-2][-1:], file_parts[-1] | ||||
) | ||||
Matthias Bussonnier
|
r23467 | |||
Elliott Morgan Jobson
|
r24816 | return string | ||
Matthias Bussonnier
|
r23467 | |||
Matthias Bussonnier
|
r25690 | def _elide_typed(string:str, typed:str, *, min_elide:int=30)->str: | ||
Matthias Bussonnier
|
r25689 | """ | ||
Elide the middle of a long string if the beginning has already been typed. | ||||
""" | ||||
if len(string) < min_elide: | ||||
return string | ||||
cut_how_much = len(typed)-3 | ||||
Matthias Bussonnier
|
r25690 | if cut_how_much < 7: | ||
return string | ||||
Matthias Bussonnier
|
r25689 | if string.startswith(typed) and len(string)> len(typed): | ||
return f"{string[:3]}\N{HORIZONTAL ELLIPSIS}{string[cut_how_much:]}" | ||||
return string | ||||
Matthias Bussonnier
|
r25690 | def _elide(string:str, typed:str, min_elide=30)->str: | ||
Matthias Bussonnier
|
r25689 | return _elide_typed( | ||
_elide_point(string, min_elide=min_elide), | ||||
typed, min_elide=min_elide) | ||||
Matthias Bussonnier
|
r23284 | |||
Steve Bartz
|
r23622 | def _adjust_completion_text_based_on_context(text, body, offset): | ||
Matthias Bussonnier
|
r24924 | if text.endswith('=') and len(body) > offset and body[offset] == '=': | ||
Steve Bartz
|
r23622 | return text[:-1] | ||
else: | ||||
return text | ||||
Matthias Bussonnier
|
r23284 | |||
Thomas Kluyver
|
r22388 | |||
class IPythonPTCompleter(Completer): | ||||
"""Adaptor to provide IPython completions to prompt_toolkit""" | ||||
Jonathan Slenders
|
r24376 | def __init__(self, ipy_completer=None, shell=None): | ||
Min RK
|
r22768 | if shell is None and ipy_completer is None: | ||
raise TypeError("Please pass shell=an InteractiveShell instance.") | ||||
self._ipy_completer = ipy_completer | ||||
Min RK
|
r22765 | self.shell = shell | ||
Min RK
|
r22767 | |||
Min RK
|
r22765 | @property | ||
def ipy_completer(self): | ||||
Min RK
|
r22768 | if self._ipy_completer: | ||
return self._ipy_completer | ||||
else: | ||||
return self.shell.Completer | ||||
Thomas Kluyver
|
r22388 | |||
def get_completions(self, document, complete_event): | ||||
if not document.current_line.strip(): | ||||
return | ||||
Thomas Kluyver
|
r23171 | # Some bits of our completion system may print stuff (e.g. if a module | ||
# is imported). This context manager ensures that doesn't interfere with | ||||
# the prompt. | ||||
Thomas Kluyver
|
r22445 | |||
Jonathan Slenders
|
r24376 | with patch_stdout(), provisionalcompleter(): | ||
Matthias Bussonnier
|
r23284 | body = document.text | ||
cursor_row = document.cursor_position_row | ||||
cursor_col = document.cursor_position_col | ||||
cursor_position = document.cursor_position | ||||
offset = cursor_to_position(body, cursor_row, cursor_col) | ||||
Matthias Bussonnier
|
r25689 | try: | ||
yield from self._get_completions(body, offset, cursor_position, self.ipy_completer) | ||||
except Exception as e: | ||||
Matthias Bussonnier
|
r25870 | try: | ||
exc_type, exc_value, exc_tb = sys.exc_info() | ||||
traceback.print_exception(exc_type, exc_value, exc_tb) | ||||
except AttributeError: | ||||
print('Unrecoverable Error in completions') | ||||
Thomas Kluyver
|
r22388 | |||
Matthias Bussonnier
|
r23284 | @staticmethod | ||
def _get_completions(body, offset, cursor_position, ipyc): | ||||
""" | ||||
Private equivalent of get_completions() use only for unit_testing. | ||||
""" | ||||
debug = getattr(ipyc, 'debug', False) | ||||
Matthias Bussonnier
|
r23358 | completions = _deduplicate_completions( | ||
body, ipyc.completions(body, offset)) | ||||
Matthias Bussonnier
|
r23284 | for c in completions: | ||
if not c.text: | ||||
# Guard against completion machinery giving us an empty string. | ||||
continue | ||||
text = unicodedata.normalize('NFC', c.text) | ||||
Thomas Kluyver
|
r22388 | # When the first character of the completion has a zero length, | ||
# then it's probably a decomposed unicode character. E.g. caused by | ||||
# the "\dot" completion. Try to compose again with the previous | ||||
# character. | ||||
Matthias Bussonnier
|
r23284 | if wcwidth(text[0]) == 0: | ||
if cursor_position + c.start > 0: | ||||
char_before = body[c.start - 1] | ||||
fixed_text = unicodedata.normalize( | ||||
'NFC', char_before + text) | ||||
Thomas Kluyver
|
r22388 | |||
# Yield the modified completion instead, if this worked. | ||||
Matthias Bussonnier
|
r23284 | if wcwidth(text[0:1]) == 1: | ||
yield Completion(fixed_text, start_position=c.start - offset - 1) | ||||
Thomas Kluyver
|
r22388 | continue | ||
# TODO: Use Jedi to determine meta_text | ||||
# (Jedi currently has a bug that results in incorrect information.) | ||||
# meta_text = '' | ||||
# yield Completion(m, start_position=start_pos, | ||||
# display_meta=meta_text) | ||||
Matthias Bussonnier
|
r23467 | display_text = c.text | ||
Steve Bartz
|
r23622 | adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset) | ||
Matthias Bussonnier
|
r23467 | if c.type == 'function': | ||
Matthias Bussonnier
|
r25689 | yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()', body[c.start:c.end]), display_meta=c.type+c.signature) | ||
Matthias Bussonnier
|
r23753 | else: | ||
Matthias Bussonnier
|
r25689 | yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text, body[c.start:c.end]), display_meta=c.type) | ||
Thomas Kluyver
|
r22388 | |||
class IPythonPTLexer(Lexer): | ||||
""" | ||||
Wrapper around PythonLexer and BashLexer. | ||||
""" | ||||
def __init__(self): | ||||
Jonathan Slenders
|
r22512 | l = pygments_lexers | ||
Srinivas Reddy Thatiparthy
|
r23108 | self.python_lexer = PygmentsLexer(l.Python3Lexer) | ||
Jonathan Slenders
|
r22512 | self.shell_lexer = PygmentsLexer(l.BashLexer) | ||
self.magic_lexers = { | ||||
'HTML': PygmentsLexer(l.HtmlLexer), | ||||
'html': PygmentsLexer(l.HtmlLexer), | ||||
'javascript': PygmentsLexer(l.JavascriptLexer), | ||||
'js': PygmentsLexer(l.JavascriptLexer), | ||||
'perl': PygmentsLexer(l.PerlLexer), | ||||
'ruby': PygmentsLexer(l.RubyLexer), | ||||
'latex': PygmentsLexer(l.TexLexer), | ||||
} | ||||
Thomas Kluyver
|
r22388 | |||
Jonathan Slenders
|
r24376 | def lex_document(self, document): | ||
Jonathan Slenders
|
r22512 | text = document.text.lstrip() | ||
lexer = self.python_lexer | ||||
if text.startswith('!') or text.startswith('%%bash'): | ||||
lexer = self.shell_lexer | ||||
elif text.startswith('%%'): | ||||
for magic, l in self.magic_lexers.items(): | ||||
if text.startswith('%%' + magic): | ||||
lexer = l | ||||
break | ||||
Jonathan Slenders
|
r24376 | return lexer.lex_document(document) | ||