ptshell.py
451 lines
| 16.6 KiB
| text/x-python
|
PythonLexer
Thomas Kluyver
|
r21930 | """IPython terminal interface using prompt_toolkit in place of readline""" | ||
from __future__ import print_function | ||||
Thomas Kluyver
|
r22128 | import os | ||
Thomas Kluyver
|
r21950 | import sys | ||
Jonathan Slenders
|
r22194 | import signal | ||
Jonathan Slenders
|
r22227 | import unicodedata | ||
Thomas Kluyver
|
r22204 | from warnings import warn | ||
Jonathan Slenders
|
r22227 | from wcwidth import wcwidth | ||
Thomas Kluyver
|
r21950 | |||
Thomas Kluyver
|
r22204 | from IPython.core.error import TryNext | ||
Thomas Kluyver
|
r21911 | from IPython.core.interactiveshell import InteractiveShell | ||
Thomas Kluyver
|
r22107 | from IPython.utils.py3compat import PY3, cast_unicode_py2, input | ||
Min RK
|
r22124 | from IPython.utils.terminal import toggle_set_term_title, set_term_title | ||
from IPython.utils.process import abbrev_cwd | ||||
Matthias Bussonnier
|
r22276 | from traitlets import Bool, CBool, Unicode, Dict, Integer | ||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r21916 | from prompt_toolkit.completion import Completer, Completion | ||
Gil Forsyth
|
r22241 | from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER | ||
Thomas Kluyver
|
r22136 | from prompt_toolkit.filters import HasFocus, HasSelection, Condition | ||
Thomas Kluyver
|
r21914 | from prompt_toolkit.history import InMemoryHistory | ||
Matthias Bussonnier
|
r22277 | from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout | ||
Thomas Kluyver
|
r21912 | from prompt_toolkit.interface import CommandLineInterface | ||
from prompt_toolkit.key_binding.manager import KeyBindingManager | ||||
Thomas Kluyver
|
r21928 | from prompt_toolkit.key_binding.vi_state import InputMode | ||
from prompt_toolkit.key_binding.bindings.vi import ViStateFilter | ||||
Thomas Kluyver
|
r21912 | from prompt_toolkit.keys import Keys | ||
Jonathan Slenders
|
r22197 | from prompt_toolkit.layout.lexers import Lexer | ||
Thomas Kluyver
|
r21911 | from prompt_toolkit.layout.lexers import PygmentsLexer | ||
Matthias Bussonnier
|
r22277 | from prompt_toolkit.styles import PygmentsStyle, DynamicStyle | ||
Thomas Kluyver
|
r21911 | |||
Matthias Bussonnier
|
r22277 | from pygments.styles import get_style_by_name, get_all_styles | ||
Jonathan Slenders
|
r22197 | from pygments.lexers import Python3Lexer, BashLexer, PythonLexer | ||
Thomas Kluyver
|
r21911 | from pygments.token import Token | ||
Thomas Kluyver
|
r21934 | from .pt_inputhooks import get_inputhook_func | ||
Thomas Kluyver
|
r22128 | from .interactiveshell import get_default_editor, TerminalMagics | ||
Thomas Kluyver
|
r21934 | |||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r21916 | class IPythonPTCompleter(Completer): | ||
"""Adaptor to provide IPython completions to prompt_toolkit""" | ||||
def __init__(self, ipy_completer): | ||||
self.ipy_completer = ipy_completer | ||||
def get_completions(self, document, complete_event): | ||||
Thomas Kluyver
|
r21922 | if not document.current_line.strip(): | ||
return | ||||
Thomas Kluyver
|
r21916 | 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: | ||||
Jonathan Slenders
|
r22227 | 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 | ||||
Thomas Kluyver
|
r21916 | yield Completion(m, start_position=start_pos) | ||
Jonathan Slenders
|
r22197 | |||
class IPythonPTLexer(Lexer): | ||||
""" | ||||
Wrapper around PythonLexer and BashLexer. | ||||
""" | ||||
def __init__(self): | ||||
self.python_lexer = PygmentsLexer(Python3Lexer if PY3 else PythonLexer) | ||||
self.shell_lexer = PygmentsLexer(BashLexer) | ||||
def lex_document(self, cli, document): | ||||
if document.text.startswith('!'): | ||||
return self.shell_lexer.lex_document(cli, document) | ||||
else: | ||||
return self.python_lexer.lex_document(cli, document) | ||||
Thomas Kluyver
|
r22112 | class TerminalInteractiveShell(InteractiveShell): | ||
Thomas Kluyver
|
r21920 | colors_force = True | ||
Matthias Bussonnier
|
r22280 | space_for_menu = Integer(6, config=True, help='Number of line at the bottom of the screen ' | ||
'to reserve for the completion menu') | ||||
Matthias Bussonnier
|
r22276 | |||
Matthias Bussonnier
|
r22277 | def _space_for_menu_changed(self, old, new): | ||
Matthias Bussonnier
|
r22279 | self._update_layout() | ||
Matthias Bussonnier
|
r22277 | |||
Thomas Kluyver
|
r21911 | pt_cli = None | ||
Thomas Kluyver
|
r22204 | autoedit_syntax = CBool(False, config=True, | ||
help="auto editing of files with syntax errors.") | ||||
Thomas Kluyver
|
r22186 | confirm_exit = CBool(True, config=True, | ||
help=""" | ||||
Set to confirm when you try to exit IPython with an EOF (Control-D | ||||
in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit', | ||||
you can force a direct exit without any confirmation.""", | ||||
) | ||||
Thomas Kluyver
|
r21928 | vi_mode = Bool(False, config=True, | ||
help="Use vi style keybindings at the prompt", | ||||
) | ||||
Thomas Kluyver
|
r22055 | mouse_support = Bool(False, config=True, | ||
help="Enable mouse support in the prompt" | ||||
) | ||||
Matthias Bussonnier
|
r22277 | highlighting_style = Unicode('default', config=True, | ||
help="The name of a Pygments style to use for syntax highlighting: \n %s" % ', '.join(get_all_styles()) | ||||
Thomas Kluyver
|
r21929 | ) | ||
Matthias Bussonnier
|
r22277 | def _highlighting_style_changed(self, old, new): | ||
self._style = self._make_style_from_name(self.highlighting_style) | ||||
Thomas Kluyver
|
r21929 | highlighting_style_overrides = Dict(config=True, | ||
help="Override highlighting format for specific tokens" | ||||
) | ||||
Thomas Kluyver
|
r21969 | editor = Unicode(get_default_editor(), config=True, | ||
help="Set the editor used by IPython (default to $EDITOR/vi/notepad)." | ||||
) | ||||
Min RK
|
r22124 | |||
term_title = Bool(True, config=True, | ||||
help="Automatically set the terminal title" | ||||
) | ||||
def _term_title_changed(self, name, new_value): | ||||
self.init_term_title() | ||||
def init_term_title(self): | ||||
# Enable or disable the terminal title. | ||||
if self.term_title: | ||||
toggle_set_term_title(True) | ||||
set_term_title('IPython: ' + abbrev_cwd()) | ||||
else: | ||||
toggle_set_term_title(False) | ||||
Thomas Kluyver
|
r21969 | |||
Thomas Kluyver
|
r21911 | def get_prompt_tokens(self, cli): | ||
return [ | ||||
(Token.Prompt, 'In ['), | ||||
Thomas Kluyver
|
r21918 | (Token.PromptNum, str(self.execution_count)), | ||
Thomas Kluyver
|
r21911 | (Token.Prompt, ']: '), | ||
] | ||||
Thomas Kluyver
|
r21933 | def get_continuation_tokens(self, cli, width): | ||
return [ | ||||
Matthias Bussonnier
|
r22122 | (Token.Prompt, (' ' * (width - 5)) + '...: '), | ||
Thomas Kluyver
|
r21933 | ] | ||
Thomas Kluyver
|
r21911 | def init_prompt_toolkit_cli(self): | ||
Thomas Kluyver
|
r22132 | if ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or not sys.stdin.isatty(): | ||
# Fall back to plain non-interactive output for tests. | ||||
# This is very limited, and only accepts a single line. | ||||
Thomas Kluyver
|
r22107 | def prompt(): | ||
return cast_unicode_py2(input('In [%d]: ' % self.execution_count)) | ||||
self.prompt_for_code = prompt | ||||
return | ||||
Thomas Kluyver
|
r21928 | kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode) | ||
insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT) | ||||
Thomas Kluyver
|
r21923 | # Ctrl+J == Enter, seemingly | ||
@kbmanager.registry.add_binding(Keys.ControlJ, | ||||
Thomas Kluyver
|
r21928 | filter=(HasFocus(DEFAULT_BUFFER) | ||
& ~HasSelection() | ||||
& insert_mode | ||||
)) | ||||
Thomas Kluyver
|
r21912 | def _(event): | ||
b = event.current_buffer | ||||
Thomas Kluyver
|
r22012 | d = b.document | ||
if not (d.on_last_line or d.cursor_position_row >= d.line_count | ||||
- d.empty_line_count_at_the_end()): | ||||
Thomas Kluyver
|
r21912 | b.newline() | ||
return | ||||
Thomas Kluyver
|
r22012 | status, indent = self.input_splitter.check_complete(d.text) | ||
Thomas Kluyver
|
r21912 | |||
if (status != 'incomplete') and b.accept_action.is_returnable: | ||||
b.accept_action.validate_and_handle(event.cli, b) | ||||
else: | ||||
Thomas Kluyver
|
r21914 | b.insert_text('\n' + (' ' * (indent or 0))) | ||
Thomas Kluyver
|
r22162 | @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(DEFAULT_BUFFER)) | ||
Thomas Kluyver
|
r21915 | def _(event): | ||
event.current_buffer.reset() | ||||
Gil Forsyth
|
r22241 | @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(SEARCH_BUFFER)) | ||
def _(event): | ||||
if event.current_buffer.document.text: | ||||
event.current_buffer.reset() | ||||
else: | ||||
event.cli.push_focus(DEFAULT_BUFFER) | ||||
Jonathan Slenders
|
r22194 | supports_suspend = Condition(lambda cli: hasattr(signal, 'SIGTSTP')) | ||
@kbmanager.registry.add_binding(Keys.ControlZ, filter=supports_suspend) | ||||
def _(event): | ||||
event.cli.suspend_to_background() | ||||
Thomas Kluyver
|
r22136 | @Condition | ||
def cursor_in_leading_ws(cli): | ||||
before = cli.application.buffer.document.current_line_before_cursor | ||||
return (not before) or before.isspace() | ||||
# Ctrl+I == Tab | ||||
@kbmanager.registry.add_binding(Keys.ControlI, | ||||
filter=(HasFocus(DEFAULT_BUFFER) | ||||
& ~HasSelection() | ||||
& insert_mode | ||||
& cursor_in_leading_ws | ||||
)) | ||||
def _(event): | ||||
event.current_buffer.insert_text(' ' * 4) | ||||
Thomas Kluyver
|
r21914 | # Pre-populate history from IPython's history database | ||
history = InMemoryHistory() | ||||
last_cell = u"" | ||||
for _, _, cell in self.history_manager.get_tail(self.history_load_length, | ||||
include_latest=True): | ||||
# Ignore blank lines and consecutive duplicates | ||||
cell = cell.rstrip() | ||||
if cell and (cell != last_cell): | ||||
history.append(cell) | ||||
Thomas Kluyver
|
r21912 | |||
Matthias Bussonnier
|
r22277 | self._style = self._make_style_from_name(self.highlighting_style) | ||
style = DynamicStyle(lambda: self._style) | ||||
self._app = create_prompt_application( | ||||
key_bindings_registry=kbmanager.registry, | ||||
history=history, | ||||
completer=IPythonPTCompleter(self.Completer), | ||||
enable_history_search=True, | ||||
style=style, | ||||
mouse_support=self.mouse_support, | ||||
**self._layout_options() | ||||
) | ||||
self.pt_cli = CommandLineInterface(self._app, | ||||
eventloop=create_eventloop(self.inputhook)) | ||||
def _make_style_from_name(self, name): | ||||
""" | ||||
Small wrapper that make an IPython compatible style from a style name | ||||
We need that to add style for prompt ... etc. | ||||
""" | ||||
style_cls = get_style_by_name(name) | ||||
Thomas Kluyver
|
r21929 | style_overrides = { | ||
Matthias Bussonnier
|
r22278 | Token.Prompt: style_cls.styles.get( Token.Keyword, '#009900'), | ||
Token.PromptNum: style_cls.styles.get( Token.Literal.Number, '#00ff00 bold') | ||||
Thomas Kluyver
|
r21929 | } | ||
Matthias Bussonnier
|
r22277 | if name is 'default': | ||
Thomas Kluyver
|
r21929 | style_cls = get_style_by_name('default') | ||
Thomas Kluyver
|
r21939 | # The default theme needs to be visible on both a dark background | ||
# and a light background, because we can't tell what the terminal | ||||
# looks like. These tweaks to the default theme help with that. | ||||
Thomas Kluyver
|
r21929 | style_overrides.update({ | ||
Token.Number: '#007700', | ||||
Token.Operator: 'noinherit', | ||||
Token.String: '#BB6622', | ||||
Thomas Kluyver
|
r21939 | Token.Name.Function: '#2080D0', | ||
Token.Name.Class: 'bold #2080D0', | ||||
Token.Name.Namespace: 'bold #2080D0', | ||||
Thomas Kluyver
|
r21929 | }) | ||
style_overrides.update(self.highlighting_style_overrides) | ||||
style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls, | ||||
style_dict=style_overrides) | ||||
Thomas Kluyver
|
r21918 | |||
Matthias Bussonnier
|
r22277 | return style | ||
def _layout_options(self): | ||||
""" | ||||
Return the current layout option for the current Terminal InteractiveShell | ||||
""" | ||||
return { | ||||
'lexer':IPythonPTLexer(), | ||||
'reserve_space_for_menu':self.space_for_menu, | ||||
'get_prompt_tokens':self.get_prompt_tokens, | ||||
'get_continuation_tokens':self.get_continuation_tokens, | ||||
Matthias Bussonnier
|
r22283 | 'multiline':True, | ||
Matthias Bussonnier
|
r22277 | } | ||
Matthias Bussonnier
|
r22279 | def _update_layout(self): | ||
Matthias Bussonnier
|
r22277 | """ | ||
Ask for a re computation of the application layout, if for example , | ||||
some configuration options have changed. | ||||
""" | ||||
self._app.layout = create_prompt_layout(**self._layout_options()) | ||||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r22107 | def prompt_for_code(self): | ||
document = self.pt_cli.run(pre_run=self.pre_prompt) | ||||
return document.text | ||||
Thomas Kluyver
|
r21950 | def init_io(self): | ||
if sys.platform not in {'win32', 'cli'}: | ||||
return | ||||
import colorama | ||||
colorama.init() | ||||
# For some reason we make these wrappers around stdout/stderr. | ||||
# For now, we need to reset them so all output gets coloured. | ||||
# https://github.com/ipython/ipython/issues/8669 | ||||
from IPython.utils import io | ||||
io.stdout = io.IOStream(sys.stdout) | ||||
io.stderr = io.IOStream(sys.stderr) | ||||
Thomas Kluyver
|
r22128 | def init_magics(self): | ||
super(TerminalInteractiveShell, self).init_magics() | ||||
self.register_magics(TerminalMagics) | ||||
def init_alias(self): | ||||
# The parent class defines aliases that can be safely used with any | ||||
# frontend. | ||||
super(TerminalInteractiveShell, self).init_alias() | ||||
# Now define aliases that only make sense on the terminal, because they | ||||
# need direct access to the console in a way that we can't emulate in | ||||
# GUI or web frontend | ||||
if os.name == 'posix': | ||||
for cmd in ['clear', 'more', 'less', 'man']: | ||||
self.alias_manager.soft_define_alias(cmd, cmd) | ||||
Thomas Kluyver
|
r21911 | def __init__(self, *args, **kwargs): | ||
Thomas Kluyver
|
r22112 | super(TerminalInteractiveShell, self).__init__(*args, **kwargs) | ||
Thomas Kluyver
|
r21911 | self.init_prompt_toolkit_cli() | ||
Min RK
|
r22124 | self.init_term_title() | ||
Thomas Kluyver
|
r21911 | self.keep_running = True | ||
def ask_exit(self): | ||||
self.keep_running = False | ||||
Thomas Kluyver
|
r21948 | rl_next_input = None | ||
def pre_prompt(self): | ||||
if self.rl_next_input: | ||||
Thomas Kluyver
|
r22011 | self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input) | ||
Thomas Kluyver
|
r21948 | self.rl_next_input = None | ||
Thomas Kluyver
|
r21911 | def interact(self): | ||
while self.keep_running: | ||||
Thomas Kluyver
|
r21921 | print(self.separate_in, end='') | ||
Thomas Kluyver
|
r21948 | |||
Thomas Kluyver
|
r21915 | try: | ||
Thomas Kluyver
|
r22107 | code = self.prompt_for_code() | ||
Thomas Kluyver
|
r21915 | except EOFError: | ||
Thomas Kluyver
|
r22186 | if (not self.confirm_exit) \ | ||
or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'): | ||||
Thomas Kluyver
|
r21915 | self.ask_exit() | ||
else: | ||||
Thomas Kluyver
|
r22107 | if code: | ||
self.run_cell(code, store_history=True) | ||||
Thomas Kluyver
|
r22204 | if self.autoedit_syntax and self.SyntaxTB.last_syntax_error: | ||
self.edit_syntax_error() | ||||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r22054 | def mainloop(self): | ||
# An extra layer of protection in case someone mashing Ctrl-C breaks | ||||
# out of our internal code. | ||||
while True: | ||||
try: | ||||
self.interact() | ||||
break | ||||
except KeyboardInterrupt: | ||||
print("\nKeyboardInterrupt escaped interact()\n") | ||||
Thomas Kluyver
|
r21934 | _inputhook = None | ||
def inputhook(self, context): | ||||
if self._inputhook is not None: | ||||
self._inputhook(context) | ||||
def enable_gui(self, gui=None): | ||||
if gui: | ||||
self._inputhook = get_inputhook_func(gui) | ||||
else: | ||||
self._inputhook = None | ||||
Thomas Kluyver
|
r21911 | |||
Thomas Kluyver
|
r22204 | # Methods to support auto-editing of SyntaxErrors: | ||
def edit_syntax_error(self): | ||||
"""The bottom half of the syntax error handler called in the main loop. | ||||
Loop until syntax error is fixed or user cancels. | ||||
""" | ||||
while self.SyntaxTB.last_syntax_error: | ||||
# copy and clear last_syntax_error | ||||
err = self.SyntaxTB.clear_err_state() | ||||
if not self._should_recompile(err): | ||||
return | ||||
try: | ||||
# may set last_syntax_error again if a SyntaxError is raised | ||||
self.safe_execfile(err.filename, self.user_ns) | ||||
except: | ||||
self.showtraceback() | ||||
else: | ||||
try: | ||||
Thomas Kluyver
|
r22205 | with open(err.filename) as f: | ||
Thomas Kluyver
|
r22204 | # This should be inside a display_trap block and I | ||
# think it is. | ||||
sys.displayhook(f.read()) | ||||
except: | ||||
self.showtraceback() | ||||
def _should_recompile(self, e): | ||||
"""Utility routine for edit_syntax_error""" | ||||
if e.filename in ('<ipython console>', '<input>', '<string>', | ||||
'<console>', '<BackgroundJob compilation>', | ||||
None): | ||||
return False | ||||
try: | ||||
if (self.autoedit_syntax and | ||||
not self.ask_yes_no( | ||||
'Return to editor to correct syntax error? ' | ||||
'[Y/n] ', 'y')): | ||||
return False | ||||
except EOFError: | ||||
return False | ||||
def int0(x): | ||||
try: | ||||
return int(x) | ||||
except TypeError: | ||||
return 0 | ||||
# always pass integer line and offset values to editor hook | ||||
try: | ||||
self.hooks.fix_error_editor(e.filename, | ||||
int0(e.lineno), int0(e.offset), | ||||
e.msg) | ||||
except TryNext: | ||||
warn('Could not open editor') | ||||
return False | ||||
return True | ||||
Thomas Kluyver
|
r22239 | # Run !system commands directly, not through pipes, so terminal programs | ||
# work correctly. | ||||
system = InteractiveShell.system_raw | ||||
Thomas Kluyver
|
r21911 | if __name__ == '__main__': | ||
Thomas Kluyver
|
r22112 | TerminalInteractiveShell.instance().interact() | ||