| | """prompt-toolkit utilities |
| | |
| | Everything in this module is a private API, |
| | not to be used outside IPython. |
| | """ |
| |
|
| | |
| | |
| |
|
| | import unicodedata |
| | from wcwidth import wcwidth |
| |
|
| | from IPython.core.completer import ( |
| | provisionalcompleter, cursor_to_position, |
| | _deduplicate_completions) |
| | from prompt_toolkit.completion import Completer, Completion |
| | from prompt_toolkit.lexers import Lexer |
| | from prompt_toolkit.lexers import PygmentsLexer |
| | from prompt_toolkit.patch_stdout import patch_stdout |
| |
|
| | import pygments.lexers as pygments_lexers |
| | import os |
| | import sys |
| | import traceback |
| |
|
| | _completion_sentinel = object() |
| |
|
| | def _elide_point(string:str, *, min_elide=30)->str: |
| | """ |
| | 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, |
| | replace the middle part with ellipses. |
| | |
| | If three consecutive dots, or two consecutive dots are encountered these are |
| | replaced by the equivalents HORIZONTAL ELLIPSIS or TWO DOT LEADER unicode |
| | equivalents |
| | """ |
| | string = string.replace('...','\N{HORIZONTAL ELLIPSIS}') |
| | string = string.replace('..','\N{TWO DOT LEADER}') |
| | if len(string) < min_elide: |
| | return string |
| |
|
| | object_parts = string.split('.') |
| | file_parts = string.split(os.sep) |
| | if file_parts[-1] == '': |
| | file_parts.pop() |
| |
|
| | if len(object_parts) > 3: |
| | return "{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}".format( |
| | object_parts[0], |
| | object_parts[1][:1], |
| | object_parts[-2][-1:], |
| | object_parts[-1], |
| | ) |
| |
|
| | elif len(file_parts) > 3: |
| | return ("{}" + os.sep + "{}\N{HORIZONTAL ELLIPSIS}{}" + os.sep + "{}").format( |
| | file_parts[0], file_parts[1][:1], file_parts[-2][-1:], file_parts[-1] |
| | ) |
| |
|
| | return string |
| |
|
| | def _elide_typed(string:str, typed:str, *, min_elide:int=30)->str: |
| | """ |
| | 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 |
| | if cut_how_much < 7: |
| | return string |
| | if string.startswith(typed) and len(string)> len(typed): |
| | return f"{string[:3]}\N{HORIZONTAL ELLIPSIS}{string[cut_how_much:]}" |
| | return string |
| |
|
| | def _elide(string:str, typed:str, min_elide=30)->str: |
| | return _elide_typed( |
| | _elide_point(string, min_elide=min_elide), |
| | typed, min_elide=min_elide) |
| |
|
| |
|
| |
|
| | def _adjust_completion_text_based_on_context(text, body, offset): |
| | if text.endswith('=') and len(body) > offset and body[offset] == '=': |
| | return text[:-1] |
| | else: |
| | return text |
| |
|
| |
|
| | class IPythonPTCompleter(Completer): |
| | """Adaptor to provide IPython completions to prompt_toolkit""" |
| | def __init__(self, ipy_completer=None, shell=None): |
| | if shell is None and ipy_completer is None: |
| | raise TypeError("Please pass shell=an InteractiveShell instance.") |
| | self._ipy_completer = ipy_completer |
| | self.shell = shell |
| |
|
| | @property |
| | def ipy_completer(self): |
| | if self._ipy_completer: |
| | return self._ipy_completer |
| | else: |
| | return self.shell.Completer |
| |
|
| | def get_completions(self, document, complete_event): |
| | if not document.current_line.strip(): |
| | return |
| | |
| | |
| | |
| |
|
| | with patch_stdout(), provisionalcompleter(): |
| | 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) |
| | try: |
| | yield from self._get_completions(body, offset, cursor_position, self.ipy_completer) |
| | except Exception as e: |
| | 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') |
| |
|
| | @staticmethod |
| | def _get_completions(body, offset, cursor_position, ipyc): |
| | """ |
| | Private equivalent of get_completions() use only for unit_testing. |
| | """ |
| | debug = getattr(ipyc, 'debug', False) |
| | completions = _deduplicate_completions( |
| | body, ipyc.completions(body, offset)) |
| | for c in completions: |
| | if not c.text: |
| | |
| | continue |
| | text = unicodedata.normalize('NFC', c.text) |
| | |
| | |
| | |
| | |
| | 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) |
| |
|
| | |
| | if wcwidth(text[0:1]) == 1: |
| | yield Completion(fixed_text, start_position=c.start - offset - 1) |
| | continue |
| |
|
| | |
| | |
| | |
| | |
| | |
| | display_text = c.text |
| |
|
| | adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset) |
| | if c.type == 'function': |
| | yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text+'()', body[c.start:c.end]), display_meta=c.type+c.signature) |
| | else: |
| | yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text, body[c.start:c.end]), display_meta=c.type) |
| |
|
| | class IPythonPTLexer(Lexer): |
| | """ |
| | Wrapper around PythonLexer and BashLexer. |
| | """ |
| | def __init__(self): |
| | l = pygments_lexers |
| | self.python_lexer = PygmentsLexer(l.Python3Lexer) |
| | 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), |
| | } |
| |
|
| | def lex_document(self, document): |
| | 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 |
| |
|
| | return lexer.lex_document(document) |
| |
|