From a5b5b9acf5efbde545199c22c468c8e308bcbfeb Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 19 Dec 2017 12:30:25 -0500 Subject: [PATCH] Support ptk 2.0 --- xonsh/ptk/key_bindings.py | 42 ++++++----- xonsh/ptk/shell.py | 142 ++++++++++++++++++++++++++++++++++++-- xonsh/ptk/shortcuts.py | 61 +++++++++++++++- xonsh/shell.py | 2 +- 4 files changed, 217 insertions(+), 30 deletions(-) diff --git a/xonsh/ptk/key_bindings.py b/xonsh/ptk/key_bindings.py index d26b39017..9a5cef2a9 100644 --- a/xonsh/ptk/key_bindings.py +++ b/xonsh/ptk/key_bindings.py @@ -6,6 +6,7 @@ from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import (Condition, IsMultiline, HasSelection, EmacsInsertMode, ViInsertMode) from prompt_toolkit.keys import Keys +from prompt_toolkit.application.current import get_app from xonsh.aliases import xonsh_exit from xonsh.tools import check_for_partial_string, get_line_continuation @@ -84,34 +85,36 @@ def can_compile(src): @Condition -def tab_insert_indent(cli): +def tab_insert_indent(): """Check if should insert indent instead of starting autocompletion. Checks if there are only whitespaces before the cursor - if so indent should be inserted, otherwise autocompletion. """ - before_cursor = cli.current_buffer.document.current_line_before_cursor + before_cursor = get_app( + ).current_buffer.document.current_line_before_cursor return bool(before_cursor.isspace()) @Condition -def beginning_of_line(cli): +def beginning_of_line(): """Check if cursor is at beginning of a line other than the first line in a multiline document """ - before_cursor = cli.current_buffer.document.current_line_before_cursor + app = get_app() + before_cursor = app.current_buffer.document.current_line_before_cursor return bool(len(before_cursor) == 0 and - not cli.current_buffer.document.on_first_line) + not app.current_buffer.document.on_first_line) @Condition -def end_of_line(cli): +def end_of_line(): """Check if cursor is at the end of a line other than the last line in a multiline document """ - d = cli.current_buffer.document + d = get_app().current_buffer.document at_end = d.is_cursor_at_the_end_of_line last_line = d.is_cursor_at_the_end @@ -119,56 +122,57 @@ def end_of_line(cli): @Condition -def should_confirm_completion(cli): +def should_confirm_completion(): """Check if completion needs confirmation""" return (builtins.__xonsh_env__.get('COMPLETIONS_CONFIRM') and - cli.current_buffer.complete_state) + get_app().current_buffer.complete_state) # Copied from prompt-toolkit's key_binding/bindings/basic.py @Condition -def ctrl_d_condition(cli): +def ctrl_d_condition(): """Ctrl-D binding is only active when the default buffer is selected and empty. """ if builtins.__xonsh_env__.get("IGNOREEOF"): raise EOFError else: - return (cli.current_buffer_name == DEFAULT_BUFFER and - not cli.current_buffer.text) + app = get_app() + return (app.current_buffer_name == DEFAULT_BUFFER and + not app.current_buffer.text) @Condition -def autopair_condition(cli): +def autopair_condition(): """Check if XONSH_AUTOPAIR is set""" return builtins.__xonsh_env__.get("XONSH_AUTOPAIR", False) @Condition -def whitespace_or_bracket_before(cli): +def whitespace_or_bracket_before(): """Check if there is whitespace or an opening bracket to the left of the cursor""" - d = cli.current_buffer.document + d = get_app().current_buffer.document return bool(d.cursor_position == 0 or d.char_before_cursor.isspace() or d.char_before_cursor in '([{') @Condition -def whitespace_or_bracket_after(cli): +def whitespace_or_bracket_after(): """Check if there is whitespace or a closing bracket to the right of the cursor""" - d = cli.current_buffer.document + d = get_app().current_buffer.document return bool(d.is_cursor_at_the_end_of_line or d.current_char.isspace() or d.current_char in ')]}') -def load_xonsh_bindings(key_bindings_manager): +def load_xonsh_bindings(key_bindings): """ Load custom key bindings. """ - handle = key_bindings_manager.registry.add_binding + handle = key_bindings.add has_selection = HasSelection() insert_mode = ViInsertMode() | EmacsInsertMode() diff --git a/xonsh/ptk/shell.py b/xonsh/ptk/shell.py index 1e5d1ec65..da2b46fec 100644 --- a/xonsh/ptk/shell.py +++ b/xonsh/ptk/shell.py @@ -3,24 +3,37 @@ import sys import builtins -from prompt_toolkit.key_binding.manager import KeyBindingManager from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.layout.lexers import PygmentsLexer -from prompt_toolkit.shortcuts import print_tokens -from prompt_toolkit.styles import PygmentsStyle, style_from_dict +from xonsh.platform import ptk_version_info from xonsh.base_shell import BaseShell from xonsh.tools import print_exception, carriage_return from xonsh.ptk.completer import PromptToolkitCompleter from xonsh.ptk.history import PromptToolkitHistory from xonsh.ptk.key_bindings import load_xonsh_bindings -from xonsh.ptk.shortcuts import Prompter +from xonsh.ptk.shortcuts import get_prompter from xonsh.events import events from xonsh.shell import transform_command from xonsh.platform import HAS_PYGMENTS, ON_WINDOWS from xonsh.style_tools import partial_color_tokenize, _TokenType, DEFAULT_STYLE_DICT from xonsh.lazyimps import pygments, pyghooks, winutils + +if ptk_version_info()[:2] < (2, 0): + from prompt_toolkit.key_binding.manager import KeyBindingManager + from prompt_toolkit.shortcuts import print_tokens as ptk_print + from prompt_toolkit.styles import style_from_dict + from prompt_toolkit.styles import PygmentsStyle as style_from_pygments +else: # ptk 2.0 + from prompt_toolkit.key_binding import KeyBindings + from prompt_toolkit.shortcuts import print_formatted_text as ptk_print + from prompt_toolkit.shortcuts import CompleteStyle + from prompt_toolkit.formatted_text import PygmentsTokens + from prompt_toolkit.styles.pygments import ( + style_from_pygments, Style, pygments_token_to_classname) + + Token = _TokenType() events.transmogrify('on_ptk_create', 'LoadEvent') @@ -39,7 +52,7 @@ class PromptToolkitShell(BaseShell): if ON_WINDOWS: winutils.enable_virtual_terminal_processing() self._first_prompt = True - self.prompter = Prompter() + self.prompter = get_prompter() self.history = PromptToolkitHistory() self.pt_completer = PromptToolkitCompleter(self.completer, self.ctx, self) key_bindings_manager_args = { @@ -270,10 +283,10 @@ class PromptToolkitShell(BaseShell): if HAS_PYGMENTS: env = builtins.__xonsh_env__ self.styler.style_name = env.get('XONSH_COLOR_STYLE') - proxy_style = PygmentsStyle(pyghooks.xonsh_style_proxy(self.styler)) + proxy_style = style_from_pygments(pyghooks.xonsh_style_proxy(self.styler)) else: proxy_style = style_from_dict(DEFAULT_STYLE_DICT) - print_tokens(tokens, style=proxy_style) + ptk_print(tokens, style=proxy_style) def color_style_names(self): """Returns an iterable of all available style names.""" @@ -312,3 +325,118 @@ class PromptToolkitShell(BaseShell): # if not ON_POSIX: # return # sys.stdout.write('\033[9999999C\n') + + +class PromptToolkitShell2(PromptToolkitShell): + """The xonsh shell for ptk 2.0. + """ + def __init__(self, **kwargs): + super(PromptToolkitShell, self).__init__(**kwargs) + if ON_WINDOWS: + winutils.enable_virtual_terminal_processing() + self._first_prompt = True + self.prompter = get_prompter() + self.history = PromptToolkitHistory() + self.pt_completer = PromptToolkitCompleter( + self.completer, self.ctx, self) + self.key_bindings = KeyBindings() + load_xonsh_bindings(self.key_bindings) + # This assumes that PromptToolkitShell is a singleton + events.on_ptk_create.fire( + prompter=self.prompter, + history=self.history, + completer=self.pt_completer, + bindings=self.key_bindings, + ) + + def singleline(self, store_in_history=True, auto_suggest=None, + enable_history_search=True, multiline=True, **kwargs): + """Reads a single line of input from the shell. The store_in_history + kwarg flags whether the input should be stored in PTK's in-memory + history. + """ + events.on_pre_prompt.fire() + env = builtins.__xonsh_env__ + mouse_support = env.get('MOUSE_SUPPORT') + if store_in_history: + history = self.history + else: + history = None + enable_history_search = False + auto_suggest = auto_suggest if env.get('AUTO_SUGGEST') else None + completions_display = env.get('COMPLETIONS_DISPLAY') + if completions_display == 'multi': + complete_style = CompleteStyle.MULTI_COLUMN + complete_while_typing = env.get('UPDATE_COMPLETIONS_ON_KEYPRESS') + if complete_while_typing: + # PTK requires history search to be none when completing while typing + enable_history_search = False + if HAS_PYGMENTS: + self.styler.style_name = env.get('XONSH_COLOR_STYLE') + completer = None if completions_display == 'none' else self.pt_completer + + if not env.get('UPDATE_PROMPT_ON_KEYPRESS'): + get_prompt_tokens = self.prompt_tokens(None) + get_rprompt_tokens = self.rprompt_tokens(None) + get_bottom_toolbar_tokens = self.bottom_toolbar_tokens(None) + else: + get_prompt_tokens = self.prompt_tokens + get_rprompt_tokens = self.rprompt_tokens + get_bottom_toolbar_tokens = self.bottom_toolbar_tokens + + with self.prompter: + prompt_args = { + 'mouse_support': mouse_support, + 'auto_suggest': auto_suggest, + 'message': get_prompt_tokens, + 'rprompt': get_rprompt_tokens, + 'bottom_toolbar': get_bottom_toolbar_tokens, + 'completer': completer, + 'multiline': multiline, + 'prompt_continuation': self.continuation_tokens, + 'history': history, + 'enable_history_search': enable_history_search, + 'reserve_space_for_menu': 0, + 'extra_key_bindings': self.key_bindings, + 'complete_style': complete_style, + 'complete_while_typing': complete_while_typing, + } + if builtins.__xonsh_env__.get('COLOR_INPUT'): + if HAS_PYGMENTS: + prompt_args['lexer'] = PygmentsLexer(pyghooks.XonshLexer) + style = style_from_pygments( + pyghooks.xonsh_style_proxy(self.styler)) + else: + style_dict = { + pygments_token_to_classname(key.__name__): value + for key, value in DEFAULT_STYLE_DICT + } + style = Style.from_dict(style_dict) + + prompt_args['style'] = style + + line = self.prompter.prompt(**prompt_args) + events.on_post_prompt.fire() + return line + + def prompt_tokens(self, cli): + return PygmentsTokens(super().prompt_tokens(cli)) + + def rprompt_tokens(self, cli): + return PygmentsTokens(super().rprompt_tokens(cli)) + + def bottom_toolbar_tokens(self, cli): + return PygmentsTokens(super().bottom_toolbar_tokens(cli)) + + def continuation_tokens(self, width, cli=None): + return PygmentsTokens( + super().continuation_tokens(width=width, cli=cli)) + + +def get_ptk_shell(**kwargs): + """Return a new ptk shell wrapper based on the installed version. + """ + if ptk_version_info()[:2] < (2, 0): + return PromptToolkitShell(**kwargs) + else: + return PromptToolkitShell2(**kwargs) diff --git a/xonsh/ptk/shortcuts.py b/xonsh/ptk/shortcuts.py index 02140868d..8a717fa87 100644 --- a/xonsh/ptk/shortcuts.py +++ b/xonsh/ptk/shortcuts.py @@ -2,15 +2,20 @@ import builtins import textwrap -from prompt_toolkit.interface import CommandLineInterface from prompt_toolkit.enums import EditingMode from prompt_toolkit.utils import DummyContext -from prompt_toolkit.shortcuts import (create_prompt_application, - create_eventloop, create_asyncio_eventloop, create_output) from xonsh.platform import ptk_version_info import xonsh.tools as xt +if ptk_version_info()[:2] < (2, 0): + from prompt_toolkit.interface import CommandLineInterface + from prompt_toolkit.shortcuts import ( + create_prompt_application, create_eventloop, create_asyncio_eventloop, + create_output) +else: + from prompt_toolkit.shortcuts import Prompt + class Prompter(object): @@ -120,3 +125,53 @@ class Prompter(object): def reset(self): """Resets the prompt and cli to a pristine state on this object.""" self.cli = None + + +class Prompter2(object): + """Prompter for ptk 2.0 + """ + def __init__(self, cli=None, *args, **kwargs): + """Implements a prompt that statefully holds a command-line + interface. When used as a context manager, it will return itself + on entry and reset itself on exit. + + Parameters + ---------- + cli : CommandLineInterface or None, optional + If this is not a CommandLineInterface object, such an object + will be created when the prompt() method is called. + """ + # TODO: maybe call this ``.prompt`` now since + # ``CommandLineInterface`` is gone? + self.cli = cli or Prompt(**kwargs) + self.major_minor = ptk_version_info()[:2] + + def __enter__(self): + self.reset() + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + def prompt(self, message='', **kwargs): + """Get input from the user and return it. + """ + if builtins.__xonsh_env__.get('VI_MODE'): + editing_mode = EditingMode.VI + else: + editing_mode = EditingMode.EMACS + + kwargs['editing_mode'] = editing_mode + self.cli.prompt(message=message, **kwargs) + + def reset(self): + """Resets the prompt and cli to a pristine state on this object.""" + # XXX Is this necessary any more? + # self.prompt = None + + +def get_prompter(): + if ptk_version_info()[:2] < (2, 0): + return Prompter() + else: + return Prompter2() diff --git a/xonsh/shell.py b/xonsh/shell.py index 1fc259785..a712fc092 100644 --- a/xonsh/shell.py +++ b/xonsh/shell.py @@ -142,7 +142,7 @@ class Shell(object): if shell_type == 'none': from xonsh.base_shell import BaseShell as shell_class elif shell_type == 'prompt_toolkit': - from xonsh.ptk.shell import PromptToolkitShell as shell_class + from xonsh.ptk.shell import get_ptk_shell as shell_class elif shell_type == 'readline': from xonsh.readline_shell import ReadlineShell as shell_class else: