|
|
"""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.
|
|
|
|
|
|
import unicodedata
|
|
|
from wcwidth import wcwidth
|
|
|
|
|
|
from IPython.utils.py3compat import PY3
|
|
|
|
|
|
from IPython.core.completer import IPCompleter
|
|
|
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
|
|
|
|
|
|
|
|
|
class IPythonPTCompleter(Completer):
|
|
|
"""Adaptor to provide 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():
|
|
|
used, matches = self.ipy_completer.complete(
|
|
|
line_buffer=document.current_line,
|
|
|
cursor_pos=document.cursor_position_col
|
|
|
)
|
|
|
start_pos = -len(used)
|
|
|
for m in matches:
|
|
|
if not m:
|
|
|
# Guard against completion machinery giving us an empty string.
|
|
|
continue
|
|
|
|
|
|
m = unicodedata.normalize('NFC', m)
|
|
|
|
|
|
# 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(m[0]) == 0:
|
|
|
if document.cursor_position + start_pos > 0:
|
|
|
char_before = document.text[document.cursor_position + start_pos - 1]
|
|
|
m = unicodedata.normalize('NFC', char_before + m)
|
|
|
|
|
|
# Yield the modified completion instead, if this worked.
|
|
|
if wcwidth(m[0:1]) == 1:
|
|
|
yield Completion(m, start_position=start_pos - 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)
|
|
|
yield Completion(m, start_position=start_pos)
|
|
|
|
|
|
class IPythonPTLexer(Lexer):
|
|
|
"""
|
|
|
Wrapper around PythonLexer and BashLexer.
|
|
|
"""
|
|
|
def __init__(self):
|
|
|
l = pygments_lexers
|
|
|
self.python_lexer = PygmentsLexer(l.Python3Lexer if PY3 else l.PythonLexer)
|
|
|
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, 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)
|
|
|
|