162 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
		
		
			
		
	
	
			162 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
|   | """prompt-toolkit utilities | ||
|  | 
 | ||
|  | Everything in this module is a private API, | ||
|  | not to be used outside yap_ipython. | ||
|  | """ | ||
|  | 
 | ||
|  | # Copyright (c) yap_ipython Development Team. | ||
|  | # Distributed under the terms of the Modified BSD License. | ||
|  | 
 | ||
|  | import unicodedata | ||
|  | from wcwidth import wcwidth | ||
|  | 
 | ||
|  | from yap_ipython.core.completer import ( | ||
|  |     provisionalcompleter, cursor_to_position, | ||
|  |     _deduplicate_completions) | ||
|  | from prompt_toolkit.completion import Completer, Completion | ||
|  | from prompt_toolkit.layout.lexers import Lexer | ||
|  | from prompt_toolkit.layout.lexers import PygmentsLexer | ||
|  | 
 | ||
|  | import pygments.lexers as pygments_lexers | ||
|  | 
 | ||
|  | _completion_sentinel = object() | ||
|  | 
 | ||
|  | def _elide(string, *, min_elide=30): | ||
|  |     """ | ||
|  |     If a string is long enough, and has at least 2 dots, | ||
|  |     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 | ||
|  | 
 | ||
|  |     parts = string.split('.') | ||
|  | 
 | ||
|  |     if len(parts) <= 3: | ||
|  |         return string | ||
|  | 
 | ||
|  |     return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(parts[0], parts[1][0], parts[-2][-1], parts[-1]) | ||
|  | 
 | ||
|  | 
 | ||
|  | def _adjust_completion_text_based_on_context(text, body, offset): | ||
|  |     if text.endswith('=') and len(body) > offset and body[offset] is '=': | ||
|  |         return text[:-1] | ||
|  |     else: | ||
|  |         return text | ||
|  | 
 | ||
|  | 
 | ||
|  | class IPythonPTCompleter(Completer): | ||
|  |     """Adaptor to provide yap_ipython completions to prompt_toolkit""" | ||
|  |     def __init__(self, ipy_completer=None, shell=None, patch_stdout=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 | ||
|  |         if patch_stdout is None: | ||
|  |             raise TypeError("Please pass patch_stdout") | ||
|  |         self.patch_stdout = patch_stdout | ||
|  | 
 | ||
|  |     @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 | ||
|  |         # 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. | ||
|  | 
 | ||
|  |         with self.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) | ||
|  |             yield from self._get_completions(body, offset, cursor_position, self.ipy_completer) | ||
|  | 
 | ||
|  |     @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: | ||
|  |                 # Guard against completion machinery giving us an empty string. | ||
|  |                 continue | ||
|  |             text = unicodedata.normalize('NFC', c.text) | ||
|  |             # 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. | ||
|  |             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) | ||
|  | 
 | ||
|  |                     # Yield the modified completion instead, if this worked. | ||
|  |                     if wcwidth(text[0:1]) == 1: | ||
|  |                         yield Completion(fixed_text, start_position=c.start - offset - 1) | ||
|  |                         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) | ||
|  |             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+'()'), display_meta=c.type+c.signature) | ||
|  |             else: | ||
|  |                 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text), 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), | ||
|  |             'prolog': PygmentsLexer(l.PrologLexer), | ||
|  |         } | ||
|  | 
 | ||
|  |     def lex_document(self, cli, 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(cli, document) |