##// END OF EJS Templates
try to fix build on 3.13
try to fix build on 3.13

File last commit:

r28292:aa0576f7
r28775:7759004a
Show More
__init__.py
630 lines | 18.0 KiB | text/x-python | PythonLexer
"""
Module to define and register Terminal IPython shortcuts with
:mod:`prompt_toolkit`
"""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import signal
import sys
import warnings
from dataclasses import dataclass
from typing import Callable, Any, Optional, List
from prompt_toolkit.application.current import get_app
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.key_binding.bindings import named_commands as nc
from prompt_toolkit.key_binding.bindings.completion import (
display_completions_like_readline,
)
from prompt_toolkit.key_binding.vi_state import InputMode, ViState
from prompt_toolkit.filters import Condition
from IPython.core.getipython import get_ipython
from IPython.terminal.shortcuts import auto_match as match
from IPython.terminal.shortcuts import auto_suggest
from IPython.terminal.shortcuts.filters import filter_from_string
from IPython.utils.decorators import undoc
from prompt_toolkit.enums import DEFAULT_BUFFER
__all__ = ["create_ipython_shortcuts"]
@dataclass
class BaseBinding:
command: Callable[[KeyPressEvent], Any]
keys: List[str]
@dataclass
class RuntimeBinding(BaseBinding):
filter: Condition
@dataclass
class Binding(BaseBinding):
# while filter could be created by referencing variables directly (rather
# than created from strings), by using strings we ensure that users will
# be able to create filters in configuration (e.g. JSON) files too, which
# also benefits the documentation by enforcing human-readable filter names.
condition: Optional[str] = None
def __post_init__(self):
if self.condition:
self.filter = filter_from_string(self.condition)
else:
self.filter = None
def create_identifier(handler: Callable):
parts = handler.__module__.split(".")
name = handler.__name__
package = parts[0]
if len(parts) > 1:
final_module = parts[-1]
return f"{package}:{final_module}.{name}"
else:
return f"{package}:{name}"
AUTO_MATCH_BINDINGS = [
*[
Binding(
cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
)
for key, cmd in match.auto_match_parens.items()
],
*[
# raw string
Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
for key, cmd in match.auto_match_parens_raw_string.items()
],
Binding(
match.double_quote,
['"'],
"focused_insert"
" & auto_match"
" & not_inside_unclosed_string"
" & preceded_by_paired_double_quotes"
" & followed_by_closing_paren_or_end",
),
Binding(
match.single_quote,
["'"],
"focused_insert"
" & auto_match"
" & not_inside_unclosed_string"
" & preceded_by_paired_single_quotes"
" & followed_by_closing_paren_or_end",
),
Binding(
match.docstring_double_quotes,
['"'],
"focused_insert"
" & auto_match"
" & not_inside_unclosed_string"
" & preceded_by_two_double_quotes",
),
Binding(
match.docstring_single_quotes,
["'"],
"focused_insert"
" & auto_match"
" & not_inside_unclosed_string"
" & preceded_by_two_single_quotes",
),
Binding(
match.skip_over,
[")"],
"focused_insert & auto_match & followed_by_closing_round_paren",
),
Binding(
match.skip_over,
["]"],
"focused_insert & auto_match & followed_by_closing_bracket",
),
Binding(
match.skip_over,
["}"],
"focused_insert & auto_match & followed_by_closing_brace",
),
Binding(
match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
),
Binding(
match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
),
Binding(
match.delete_pair,
["backspace"],
"focused_insert"
" & preceded_by_opening_round_paren"
" & auto_match"
" & followed_by_closing_round_paren",
),
Binding(
match.delete_pair,
["backspace"],
"focused_insert"
" & preceded_by_opening_bracket"
" & auto_match"
" & followed_by_closing_bracket",
),
Binding(
match.delete_pair,
["backspace"],
"focused_insert"
" & preceded_by_opening_brace"
" & auto_match"
" & followed_by_closing_brace",
),
Binding(
match.delete_pair,
["backspace"],
"focused_insert"
" & preceded_by_double_quote"
" & auto_match"
" & followed_by_double_quote",
),
Binding(
match.delete_pair,
["backspace"],
"focused_insert"
" & preceded_by_single_quote"
" & auto_match"
" & followed_by_single_quote",
),
]
AUTO_SUGGEST_BINDINGS = [
# there are two reasons for re-defining bindings defined upstream:
# 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
# 2) prompt-toolkit checks if we are at the end of text, not end of line
# hence it does not work in multi-line mode of navigable provider
Binding(
auto_suggest.accept_or_jump_to_end,
["end"],
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
),
Binding(
auto_suggest.accept_or_jump_to_end,
["c-e"],
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
),
Binding(
auto_suggest.accept,
["c-f"],
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
),
Binding(
auto_suggest.accept,
["right"],
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
),
Binding(
auto_suggest.accept_word,
["escape", "f"],
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
),
Binding(
auto_suggest.accept_token,
["c-right"],
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
),
Binding(
auto_suggest.discard,
["escape"],
# note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
# as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
"has_suggestion & default_buffer_focused & emacs_insert_mode",
),
Binding(
auto_suggest.discard,
["delete"],
"has_suggestion & default_buffer_focused & emacs_insert_mode",
),
Binding(
auto_suggest.swap_autosuggestion_up,
["c-up"],
"navigable_suggestions"
" & ~has_line_above"
" & has_suggestion"
" & default_buffer_focused",
),
Binding(
auto_suggest.swap_autosuggestion_down,
["c-down"],
"navigable_suggestions"
" & ~has_line_below"
" & has_suggestion"
" & default_buffer_focused",
),
Binding(
auto_suggest.up_and_update_hint,
["c-up"],
"has_line_above & navigable_suggestions & default_buffer_focused",
),
Binding(
auto_suggest.down_and_update_hint,
["c-down"],
"has_line_below & navigable_suggestions & default_buffer_focused",
),
Binding(
auto_suggest.accept_character,
["escape", "right"],
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
),
Binding(
auto_suggest.accept_and_move_cursor_left,
["c-left"],
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
),
Binding(
auto_suggest.accept_and_keep_cursor,
["escape", "down"],
"has_suggestion & default_buffer_focused & emacs_insert_mode",
),
Binding(
auto_suggest.backspace_and_resume_hint,
["backspace"],
# no `has_suggestion` here to allow resuming if no suggestion
"default_buffer_focused & emacs_like_insert_mode",
),
Binding(
auto_suggest.resume_hinting,
["right"],
"is_cursor_at_the_end_of_line"
" & default_buffer_focused"
" & emacs_like_insert_mode"
" & pass_through",
),
]
SIMPLE_CONTROL_BINDINGS = [
Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
for key, cmd in {
"c-a": nc.beginning_of_line,
"c-b": nc.backward_char,
"c-k": nc.kill_line,
"c-w": nc.backward_kill_word,
"c-y": nc.yank,
"c-_": nc.undo,
}.items()
]
ALT_AND_COMOBO_CONTROL_BINDINGS = [
Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
for keys, cmd in {
# Control Combos
("c-x", "c-e"): nc.edit_and_execute,
("c-x", "e"): nc.edit_and_execute,
# Alt
("escape", "b"): nc.backward_word,
("escape", "c"): nc.capitalize_word,
("escape", "d"): nc.kill_word,
("escape", "h"): nc.backward_kill_word,
("escape", "l"): nc.downcase_word,
("escape", "u"): nc.uppercase_word,
("escape", "y"): nc.yank_pop,
("escape", "."): nc.yank_last_arg,
}.items()
]
def add_binding(bindings: KeyBindings, binding: Binding):
bindings.add(
*binding.keys,
**({"filter": binding.filter} if binding.filter is not None else {}),
)(binding.command)
def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
"""Set up the prompt_toolkit keyboard shortcuts for IPython.
Parameters
----------
shell: InteractiveShell
The current IPython shell Instance
skip: List[Binding]
Bindings to skip.
Returns
-------
KeyBindings
the keybinding instance for prompt toolkit.
"""
kb = KeyBindings()
skip = skip or []
for binding in KEY_BINDINGS:
skip_this_one = False
for to_skip in skip:
if (
to_skip.command == binding.command
and to_skip.filter == binding.filter
and to_skip.keys == binding.keys
):
skip_this_one = True
break
if skip_this_one:
continue
add_binding(kb, binding)
def get_input_mode(self):
app = get_app()
app.ttimeoutlen = shell.ttimeoutlen
app.timeoutlen = shell.timeoutlen
return self._input_mode
def set_input_mode(self, mode):
shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
cursor = "\x1b[{} q".format(shape)
sys.stdout.write(cursor)
sys.stdout.flush()
self._input_mode = mode
if shell.editing_mode == "vi" and shell.modal_cursor:
ViState._input_mode = InputMode.INSERT # type: ignore
ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
return kb
def reformat_and_execute(event):
"""Reformat code and execute it"""
shell = get_ipython()
reformat_text_before_cursor(
event.current_buffer, event.current_buffer.document, shell
)
event.current_buffer.validate_and_handle()
def reformat_text_before_cursor(buffer, document, shell):
text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
try:
formatted_text = shell.reformat_handler(text)
buffer.insert_text(formatted_text)
except Exception as e:
buffer.insert_text(text)
def handle_return_or_newline_or_execute(event):
shell = get_ipython()
if getattr(shell, "handle_return", None):
return shell.handle_return(shell)(event)
else:
return newline_or_execute_outer(shell)(event)
def newline_or_execute_outer(shell):
def newline_or_execute(event):
"""When the user presses return, insert a newline or execute the code."""
b = event.current_buffer
d = b.document
if b.complete_state:
cc = b.complete_state.current_completion
if cc:
b.apply_completion(cc)
else:
b.cancel_completion()
return
# If there's only one line, treat it as if the cursor is at the end.
# See https://github.com/ipython/ipython/issues/10425
if d.line_count == 1:
check_text = d.text
else:
check_text = d.text[: d.cursor_position]
status, indent = shell.check_complete(check_text)
# if all we have after the cursor is whitespace: reformat current text
# before cursor
after_cursor = d.text[d.cursor_position :]
reformatted = False
if not after_cursor.strip():
reformat_text_before_cursor(b, d, shell)
reformatted = True
if not (
d.on_last_line
or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
):
if shell.autoindent:
b.insert_text("\n" + indent)
else:
b.insert_text("\n")
return
if (status != "incomplete") and b.accept_handler:
if not reformatted:
reformat_text_before_cursor(b, d, shell)
b.validate_and_handle()
else:
if shell.autoindent:
b.insert_text("\n" + indent)
else:
b.insert_text("\n")
return newline_or_execute
def previous_history_or_previous_completion(event):
"""
Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
If completer is open this still select previous completion.
"""
event.current_buffer.auto_up()
def next_history_or_next_completion(event):
"""
Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
If completer is open this still select next completion.
"""
event.current_buffer.auto_down()
def dismiss_completion(event):
"""Dismiss completion"""
b = event.current_buffer
if b.complete_state:
b.cancel_completion()
def reset_buffer(event):
"""Reset buffer"""
b = event.current_buffer
if b.complete_state:
b.cancel_completion()
else:
b.reset()
def reset_search_buffer(event):
"""Reset search buffer"""
if event.current_buffer.document.text:
event.current_buffer.reset()
else:
event.app.layout.focus(DEFAULT_BUFFER)
def suspend_to_bg(event):
"""Suspend to background"""
event.app.suspend_to_background()
def quit(event):
"""
Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
On platforms that support SIGQUIT, send SIGQUIT to the current process.
On other platforms, just exit the process with a message.
"""
sigquit = getattr(signal, "SIGQUIT", None)
if sigquit is not None:
os.kill(0, signal.SIGQUIT)
else:
sys.exit("Quit")
def indent_buffer(event):
"""Indent buffer"""
event.current_buffer.insert_text(" " * 4)
def newline_autoindent(event):
"""Insert a newline after the cursor indented appropriately.
Fancier version of former ``newline_with_copy_margin`` which should
compute the correct indentation of the inserted line. That is to say, indent
by 4 extra space after a function definition, class definition, context
manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
"""
shell = get_ipython()
inputsplitter = shell.input_transformer_manager
b = event.current_buffer
d = b.document
if b.complete_state:
b.cancel_completion()
text = d.text[: d.cursor_position] + "\n"
_, indent = inputsplitter.check_complete(text)
b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
def open_input_in_editor(event):
"""Open code from input in external editor"""
event.app.current_buffer.open_in_editor()
if sys.platform == "win32":
from IPython.core.error import TryNext
from IPython.lib.clipboard import (
ClipboardEmpty,
tkinter_clipboard_get,
win32_clipboard_get,
)
@undoc
def win_paste(event):
try:
text = win32_clipboard_get()
except TryNext:
try:
text = tkinter_clipboard_get()
except (TryNext, ClipboardEmpty):
return
except ClipboardEmpty:
return
event.current_buffer.insert_text(text.replace("\t", " " * 4))
else:
@undoc
def win_paste(event):
"""Stub used on other platforms"""
pass
KEY_BINDINGS = [
Binding(
handle_return_or_newline_or_execute,
["enter"],
"default_buffer_focused & ~has_selection & insert_mode",
),
Binding(
reformat_and_execute,
["escape", "enter"],
"default_buffer_focused & ~has_selection & insert_mode & ebivim",
),
Binding(quit, ["c-\\"]),
Binding(
previous_history_or_previous_completion,
["c-p"],
"vi_insert_mode & default_buffer_focused",
),
Binding(
next_history_or_next_completion,
["c-n"],
"vi_insert_mode & default_buffer_focused",
),
Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
Binding(
indent_buffer,
["tab"], # Ctrl+I == Tab
"default_buffer_focused"
" & ~has_selection"
" & insert_mode"
" & cursor_in_leading_ws",
),
Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
*AUTO_MATCH_BINDINGS,
*AUTO_SUGGEST_BINDINGS,
Binding(
display_completions_like_readline,
["c-i"],
"readline_like_completions"
" & default_buffer_focused"
" & ~has_selection"
" & insert_mode"
" & ~cursor_in_leading_ws",
),
Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
*SIMPLE_CONTROL_BINDINGS,
*ALT_AND_COMOBO_CONTROL_BINDINGS,
]