1"""prompt-toolkit utilities 2 3Everything in this module is a private API, 4not to be used outside IPython. 5""" 6 7# Copyright (c) IPython Development Team. 8# Distributed under the terms of the Modified BSD License. 9 10import unicodedata 11from wcwidth import wcwidth 12 13from IPython.utils.py3compat import PY3 14 15from IPython.core.completer import IPCompleter 16from prompt_toolkit.completion import Completer, Completion 17from prompt_toolkit.layout.lexers import Lexer 18from prompt_toolkit.layout.lexers import PygmentsLexer 19 20import pygments.lexers as pygments_lexers 21 22 23class IPythonPTCompleter(Completer): 24 """Adaptor to provide IPython completions to prompt_toolkit""" 25 def __init__(self, ipy_completer=None, shell=None, patch_stdout=None): 26 if shell is None and ipy_completer is None: 27 raise TypeError("Please pass shell=an InteractiveShell instance.") 28 self._ipy_completer = ipy_completer 29 self.shell = shell 30 if patch_stdout is None: 31 raise TypeError("Please pass patch_stdout") 32 self.patch_stdout = patch_stdout 33 34 @property 35 def ipy_completer(self): 36 if self._ipy_completer: 37 return self._ipy_completer 38 else: 39 return self.shell.Completer 40 41 def get_completions(self, document, complete_event): 42 if not document.current_line.strip(): 43 return 44 45 # Some bits of our completion system may print stuff (e.g. if a module 46 # is imported). This context manager ensures that doesn't interfere with 47 # the prompt. 48 with self.patch_stdout(): 49 used, matches = self.ipy_completer.complete( 50 line_buffer=document.current_line, 51 cursor_pos=document.cursor_position_col 52 ) 53 start_pos = -len(used) 54 for m in matches: 55 if not m: 56 # Guard against completion machinery giving us an empty string. 57 continue 58 59 m = unicodedata.normalize('NFC', m) 60 61 # When the first character of the completion has a zero length, 62 # then it's probably a decomposed unicode character. E.g. caused by 63 # the "\dot" completion. Try to compose again with the previous 64 # character. 65 if wcwidth(m[0]) == 0: 66 if document.cursor_position + start_pos > 0: 67 char_before = document.text[document.cursor_position + start_pos - 1] 68 m = unicodedata.normalize('NFC', char_before + m) 69 70 # Yield the modified completion instead, if this worked. 71 if wcwidth(m[0:1]) == 1: 72 yield Completion(m, start_position=start_pos - 1) 73 continue 74 75 # TODO: Use Jedi to determine meta_text 76 # (Jedi currently has a bug that results in incorrect information.) 77 # meta_text = '' 78 # yield Completion(m, start_position=start_pos, 79 # display_meta=meta_text) 80 yield Completion(m, start_position=start_pos) 81 82class IPythonPTLexer(Lexer): 83 """ 84 Wrapper around PythonLexer and BashLexer. 85 """ 86 def __init__(self): 87 l = pygments_lexers 88 self.python_lexer = PygmentsLexer(l.Python3Lexer if PY3 else l.PythonLexer) 89 self.shell_lexer = PygmentsLexer(l.BashLexer) 90 91 self.magic_lexers = { 92 'HTML': PygmentsLexer(l.HtmlLexer), 93 'html': PygmentsLexer(l.HtmlLexer), 94 'javascript': PygmentsLexer(l.JavascriptLexer), 95 'js': PygmentsLexer(l.JavascriptLexer), 96 'perl': PygmentsLexer(l.PerlLexer), 97 'ruby': PygmentsLexer(l.RubyLexer), 98 'latex': PygmentsLexer(l.TexLexer), 99 } 100 101 def lex_document(self, cli, document): 102 text = document.text.lstrip() 103 104 lexer = self.python_lexer 105 106 if text.startswith('!') or text.startswith('%%bash'): 107 lexer = self.shell_lexer 108 109 elif text.startswith('%%'): 110 for magic, l in self.magic_lexers.items(): 111 if text.startswith('%%' + magic): 112 lexer = l 113 break 114 115 return lexer.lex_document(cli, document) 116