1
0
Fork 0
ptpython/ptpython/key_bindings.py
Daniel Baumann 568aae77cf
Merging upstream version 3.0.23.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-09 18:30:14 +01:00

337 lines
10 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING
from prompt_toolkit.application import get_app
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.document import Document
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.filters import (
Condition,
emacs_insert_mode,
emacs_mode,
has_focus,
has_selection,
vi_insert_mode,
)
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.bindings.named_commands import get_by_name
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.keys import Keys
from .utils import document_is_multiline_python
if TYPE_CHECKING:
from .python_input import PythonInput
__all__ = [
"load_python_bindings",
"load_sidebar_bindings",
"load_confirm_exit_bindings",
]
E = KeyPressEvent
@Condition
def tab_should_insert_whitespace() -> bool:
"""
When the 'tab' key is pressed with only whitespace character before the
cursor, do autocompletion. Otherwise, insert indentation.
Except for the first character at the first line. Then always do a
completion. It doesn't make sense to start the first line with
indentation.
"""
b = get_app().current_buffer
before_cursor = b.document.current_line_before_cursor
return bool(b.text and (not before_cursor or before_cursor.isspace()))
def load_python_bindings(python_input: PythonInput) -> KeyBindings:
"""
Custom key bindings.
"""
bindings = KeyBindings()
sidebar_visible = Condition(lambda: python_input.show_sidebar)
handle = bindings.add
@handle("c-l")
def _(event: E) -> None:
"""
Clear whole screen and render again -- also when the sidebar is visible.
"""
event.app.renderer.clear()
@handle("c-z")
def _(event: E) -> None:
"""
Suspend.
"""
if python_input.enable_system_bindings:
event.app.suspend_to_background()
# Delete word before cursor, but use all Python symbols as separators
# (WORD=False).
handle("c-w")(get_by_name("backward-kill-word"))
@handle("f2")
def _(event: E) -> None:
"""
Show/hide sidebar.
"""
python_input.show_sidebar = not python_input.show_sidebar
if python_input.show_sidebar:
event.app.layout.focus(python_input.ptpython_layout.sidebar)
else:
event.app.layout.focus_last()
@handle("f3")
def _(event: E) -> None:
"""
Select from the history.
"""
python_input.enter_history()
@handle("f4")
def _(event: E) -> None:
"""
Toggle between Vi and Emacs mode.
"""
python_input.vi_mode = not python_input.vi_mode
@handle("f6")
def _(event: E) -> None:
"""
Enable/Disable paste mode.
"""
python_input.paste_mode = not python_input.paste_mode
@handle(
"tab", filter=~sidebar_visible & ~has_selection & tab_should_insert_whitespace
)
def _(event: E) -> None:
"""
When tab should insert whitespace, do that instead of completion.
"""
event.app.current_buffer.insert_text(" ")
@Condition
def is_multiline() -> bool:
return document_is_multiline_python(python_input.default_buffer.document)
@handle(
"enter",
filter=~sidebar_visible
& ~has_selection
& (vi_insert_mode | emacs_insert_mode)
& has_focus(DEFAULT_BUFFER)
& ~is_multiline,
)
@handle(Keys.Escape, Keys.Enter, filter=~sidebar_visible & emacs_mode)
def _(event: E) -> None:
"""
Accept input (for single line input).
"""
b = event.current_buffer
if b.validate():
# When the cursor is at the end, and we have an empty line:
# drop the empty lines, but return the value.
b.document = Document(
text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
)
b.validate_and_handle()
@handle(
"enter",
filter=~sidebar_visible
& ~has_selection
& (vi_insert_mode | emacs_insert_mode)
& has_focus(DEFAULT_BUFFER)
& is_multiline,
)
def _(event: E) -> None:
"""
Behaviour of the Enter key.
Auto indent after newline/Enter.
(When not in Vi navigation mode, and when multiline is enabled.)
"""
b = event.current_buffer
empty_lines_required = python_input.accept_input_on_enter or 10000
def at_the_end(b: Buffer) -> bool:
"""we consider the cursor at the end when there is no text after
the cursor, or only whitespace."""
text = b.document.text_after_cursor
return text == "" or (text.isspace() and "\n" not in text)
if python_input.paste_mode:
# In paste mode, always insert text.
b.insert_text("\n")
elif at_the_end(b) and b.document.text.replace(" ", "").endswith(
"\n" * (empty_lines_required - 1)
):
# When the cursor is at the end, and we have an empty line:
# drop the empty lines, but return the value.
if b.validate():
b.document = Document(
text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
)
b.validate_and_handle()
else:
auto_newline(b)
@handle(
"c-d",
filter=~sidebar_visible
& has_focus(python_input.default_buffer)
& Condition(
lambda:
# The current buffer is empty.
not get_app().current_buffer.text
),
)
def _(event: E) -> None:
"""
Override Control-D exit, to ask for confirmation.
"""
if python_input.confirm_exit:
# Show exit confirmation and focus it (focusing is important for
# making sure the default buffer key bindings are not active).
python_input.show_exit_confirmation = True
python_input.app.layout.focus(
python_input.ptpython_layout.exit_confirmation
)
else:
event.app.exit(exception=EOFError)
@handle("c-c", filter=has_focus(python_input.default_buffer))
def _(event: E) -> None:
"Abort when Control-C has been pressed."
event.app.exit(exception=KeyboardInterrupt, style="class:aborting")
return bindings
def load_sidebar_bindings(python_input: PythonInput) -> KeyBindings:
"""
Load bindings for the navigation in the sidebar.
"""
bindings = KeyBindings()
handle = bindings.add
sidebar_visible = Condition(lambda: python_input.show_sidebar)
@handle("up", filter=sidebar_visible)
@handle("c-p", filter=sidebar_visible)
@handle("k", filter=sidebar_visible)
def _(event: E) -> None:
"Go to previous option."
python_input.selected_option_index = (
python_input.selected_option_index - 1
) % python_input.option_count
@handle("down", filter=sidebar_visible)
@handle("c-n", filter=sidebar_visible)
@handle("j", filter=sidebar_visible)
def _(event: E) -> None:
"Go to next option."
python_input.selected_option_index = (
python_input.selected_option_index + 1
) % python_input.option_count
@handle("right", filter=sidebar_visible)
@handle("l", filter=sidebar_visible)
@handle(" ", filter=sidebar_visible)
def _(event: E) -> None:
"Select next value for current option."
option = python_input.selected_option
option.activate_next()
@handle("left", filter=sidebar_visible)
@handle("h", filter=sidebar_visible)
def _(event: E) -> None:
"Select previous value for current option."
option = python_input.selected_option
option.activate_previous()
@handle("c-c", filter=sidebar_visible)
@handle("c-d", filter=sidebar_visible)
@handle("c-d", filter=sidebar_visible)
@handle("enter", filter=sidebar_visible)
@handle("escape", filter=sidebar_visible)
def _(event: E) -> None:
"Hide sidebar."
python_input.show_sidebar = False
event.app.layout.focus_last()
return bindings
def load_confirm_exit_bindings(python_input: PythonInput) -> KeyBindings:
"""
Handle yes/no key presses when the exit confirmation is shown.
"""
bindings = KeyBindings()
handle = bindings.add
confirmation_visible = Condition(lambda: python_input.show_exit_confirmation)
@handle("y", filter=confirmation_visible)
@handle("Y", filter=confirmation_visible)
@handle("enter", filter=confirmation_visible)
@handle("c-d", filter=confirmation_visible)
def _(event: E) -> None:
"""
Really quit.
"""
event.app.exit(exception=EOFError, style="class:exiting")
@handle(Keys.Any, filter=confirmation_visible)
def _(event: E) -> None:
"""
Cancel exit.
"""
python_input.show_exit_confirmation = False
python_input.app.layout.focus_previous()
return bindings
def auto_newline(buffer: Buffer) -> None:
r"""
Insert \n at the cursor position. Also add necessary padding.
"""
insert_text = buffer.insert_text
if buffer.document.current_line_after_cursor:
# When we are in the middle of a line. Always insert a newline.
insert_text("\n")
else:
# Go to new line, but also add indentation.
current_line = buffer.document.current_line_before_cursor.rstrip()
insert_text("\n")
# Unident if the last line ends with 'pass', remove four spaces.
unindent = current_line.rstrip().endswith(" pass")
# Copy whitespace from current line
current_line2 = current_line[4:] if unindent else current_line
for c in current_line2:
if c.isspace():
insert_text(c)
else:
break
# If the last line ends with a colon, add four extra spaces.
if current_line[-1:] == ":":
for x in range(4):
insert_text(" ")