Uploaded Test files

This commit is contained in:
Batuhan Berk Başoğlu 2020-11-12 11:05:57 -05:00
parent f584ad9d97
commit 2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions

View file

@ -0,0 +1,20 @@
from .key_bindings import (
ConditionalKeyBindings,
DynamicKeyBindings,
KeyBindings,
KeyBindingsBase,
merge_key_bindings,
)
from .key_processor import KeyPress, KeyPressEvent
__all__ = [
# key_bindings.
"ConditionalKeyBindings",
"DynamicKeyBindings",
"KeyBindings",
"KeyBindingsBase",
"merge_key_bindings",
# key_processor
"KeyPress",
"KeyPressEvent",
]

View file

@ -0,0 +1,63 @@
"""
Key bindings for auto suggestion (for fish-style auto suggestion).
"""
import re
from prompt_toolkit.application.current import get_app
from prompt_toolkit.filters import Condition, emacs_mode
from prompt_toolkit.key_binding.key_bindings import KeyBindings
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
__all__ = [
"load_auto_suggest_bindings",
]
E = KeyPressEvent
def load_auto_suggest_bindings() -> KeyBindings:
"""
Key bindings for accepting auto suggestion text.
(This has to come after the Vi bindings, because they also have an
implementation for the "right arrow", but we really want the suggestion
binding when a suggestion is available.)
"""
key_bindings = KeyBindings()
handle = key_bindings.add
@Condition
def suggestion_available() -> bool:
app = get_app()
return (
app.current_buffer.suggestion is not None
and len(app.current_buffer.suggestion.text) > 0
and app.current_buffer.document.is_cursor_at_the_end
)
@handle("c-f", filter=suggestion_available)
@handle("c-e", filter=suggestion_available)
@handle("right", filter=suggestion_available)
def _accept(event: E) -> None:
"""
Accept suggestion.
"""
b = event.current_buffer
suggestion = b.suggestion
if suggestion:
b.insert_text(suggestion.text)
@handle("escape", "f", filter=suggestion_available & emacs_mode)
def _fill(event: E) -> None:
"""
Fill partial suggestion.
"""
b = event.current_buffer
suggestion = b.suggestion
if suggestion:
t = re.split(r"(\S+\s+)", suggestion.text)
b.insert_text(next(x for x in t if x))
return key_bindings

View file

@ -0,0 +1,252 @@
# pylint: disable=function-redefined
from prompt_toolkit.application.current import get_app
from prompt_toolkit.filters import (
Condition,
emacs_insert_mode,
has_selection,
in_paste_mode,
is_multiline,
vi_insert_mode,
)
from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
from prompt_toolkit.keys import Keys
from ..key_bindings import KeyBindings
from .named_commands import get_by_name
__all__ = [
"load_basic_bindings",
]
E = KeyPressEvent
def if_no_repeat(event: E) -> bool:
"""Callable that returns True when the previous event was delivered to
another handler."""
return not event.is_repeat
def load_basic_bindings() -> KeyBindings:
key_bindings = KeyBindings()
insert_mode = vi_insert_mode | emacs_insert_mode
handle = key_bindings.add
@handle("c-a")
@handle("c-b")
@handle("c-c")
@handle("c-d")
@handle("c-e")
@handle("c-f")
@handle("c-g")
@handle("c-h")
@handle("c-i")
@handle("c-j")
@handle("c-k")
@handle("c-l")
@handle("c-m")
@handle("c-n")
@handle("c-o")
@handle("c-p")
@handle("c-q")
@handle("c-r")
@handle("c-s")
@handle("c-t")
@handle("c-u")
@handle("c-v")
@handle("c-w")
@handle("c-x")
@handle("c-y")
@handle("c-z")
@handle("f1")
@handle("f2")
@handle("f3")
@handle("f4")
@handle("f5")
@handle("f6")
@handle("f7")
@handle("f8")
@handle("f9")
@handle("f10")
@handle("f11")
@handle("f12")
@handle("f13")
@handle("f14")
@handle("f15")
@handle("f16")
@handle("f17")
@handle("f18")
@handle("f19")
@handle("f20")
@handle("f21")
@handle("f22")
@handle("f23")
@handle("f24")
@handle("c-@") # Also c-space.
@handle("c-\\")
@handle("c-]")
@handle("c-^")
@handle("c-_")
@handle("backspace")
@handle("up")
@handle("down")
@handle("right")
@handle("left")
@handle("s-up")
@handle("s-down")
@handle("s-right")
@handle("s-left")
@handle("home")
@handle("end")
@handle("s-home")
@handle("s-end")
@handle("delete")
@handle("s-delete")
@handle("c-delete")
@handle("pageup")
@handle("pagedown")
@handle("s-tab")
@handle("tab")
@handle("c-s-left")
@handle("c-s-right")
@handle("c-s-home")
@handle("c-s-end")
@handle("c-left")
@handle("c-right")
@handle("c-up")
@handle("c-down")
@handle("c-home")
@handle("c-end")
@handle("insert")
@handle("s-insert")
@handle("c-insert")
@handle(Keys.Ignore)
def _ignore(event: E) -> None:
"""
First, for any of these keys, Don't do anything by default. Also don't
catch them in the 'Any' handler which will insert them as data.
If people want to insert these characters as a literal, they can always
do by doing a quoted insert. (ControlQ in emacs mode, ControlV in Vi
mode.)
"""
pass
# Readline-style bindings.
handle("home")(get_by_name("beginning-of-line"))
handle("end")(get_by_name("end-of-line"))
handle("left")(get_by_name("backward-char"))
handle("right")(get_by_name("forward-char"))
handle("c-up")(get_by_name("previous-history"))
handle("c-down")(get_by_name("next-history"))
handle("c-l")(get_by_name("clear-screen"))
handle("c-k", filter=insert_mode)(get_by_name("kill-line"))
handle("c-u", filter=insert_mode)(get_by_name("unix-line-discard"))
handle("backspace", filter=insert_mode, save_before=if_no_repeat)(
get_by_name("backward-delete-char")
)
handle("delete", filter=insert_mode, save_before=if_no_repeat)(
get_by_name("delete-char")
)
handle("c-delete", filter=insert_mode, save_before=if_no_repeat)(
get_by_name("delete-char")
)
handle(Keys.Any, filter=insert_mode, save_before=if_no_repeat)(
get_by_name("self-insert")
)
handle("c-t", filter=insert_mode)(get_by_name("transpose-chars"))
handle("c-i", filter=insert_mode)(get_by_name("menu-complete"))
handle("s-tab", filter=insert_mode)(get_by_name("menu-complete-backward"))
# Control-W should delete, using whitespace as separator, while M-Del
# should delete using [^a-zA-Z0-9] as a boundary.
handle("c-w", filter=insert_mode)(get_by_name("unix-word-rubout"))
handle("pageup", filter=~has_selection)(get_by_name("previous-history"))
handle("pagedown", filter=~has_selection)(get_by_name("next-history"))
# CTRL keys.
@Condition
def has_text_before_cursor() -> bool:
return bool(get_app().current_buffer.text)
handle("c-d", filter=has_text_before_cursor & insert_mode)(
get_by_name("delete-char")
)
@handle("enter", filter=insert_mode & is_multiline)
def _newline(event: E) -> None:
"""
Newline (in case of multiline input.
"""
event.current_buffer.newline(copy_margin=not in_paste_mode())
@handle("c-j")
def _newline2(event: E) -> None:
r"""
By default, handle \n as if it were a \r (enter).
(It appears that some terminals send \n instead of \r when pressing
enter. - at least the Linux subsystem for Windows.)
"""
event.key_processor.feed(KeyPress(Keys.ControlM, "\r"), first=True)
# Delete the word before the cursor.
@handle("up")
def _go_up(event: E) -> None:
event.current_buffer.auto_up(count=event.arg)
@handle("down")
def _go_down(event: E) -> None:
event.current_buffer.auto_down(count=event.arg)
@handle("delete", filter=has_selection)
def _cut(event: E) -> None:
data = event.current_buffer.cut_selection()
event.app.clipboard.set_data(data)
# Global bindings.
@handle("c-z")
def _insert_ctrl_z(event: E) -> None:
"""
By default, control-Z should literally insert Ctrl-Z.
(Ansi Ctrl-Z, code 26 in MSDOS means End-Of-File.
In a Python REPL for instance, it's possible to type
Control-Z followed by enter to quit.)
When the system bindings are loaded and suspend-to-background is
supported, that will override this binding.
"""
event.current_buffer.insert_text(event.data)
@handle(Keys.BracketedPaste)
def _paste(event: E) -> None:
"""
Pasting from clipboard.
"""
data = event.data
# Be sure to use \n as line ending.
# Some terminals (Like iTerm2) seem to paste \r\n line endings in a
# bracketed paste. See: https://github.com/ipython/ipython/issues/9737
data = data.replace("\r\n", "\n")
data = data.replace("\r", "\n")
event.current_buffer.insert_text(data)
@Condition
def in_quoted_insert() -> bool:
return get_app().quoted_insert
@handle(Keys.Any, filter=in_quoted_insert, eager=True)
def _insert_text(event: E) -> None:
"""
Handle quoted insert.
"""
event.current_buffer.insert_text(event.data, overwrite=False)
event.app.quoted_insert = False
return key_bindings

View file

@ -0,0 +1,203 @@
"""
Key binding handlers for displaying completions.
"""
import asyncio
import math
from typing import TYPE_CHECKING, List
from prompt_toolkit.application.run_in_terminal import in_terminal
from prompt_toolkit.completion import (
CompleteEvent,
Completion,
get_common_complete_suffix,
)
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.key_binding.key_bindings import KeyBindings
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.keys import Keys
from prompt_toolkit.utils import get_cwidth
if TYPE_CHECKING:
from prompt_toolkit.application import Application
from prompt_toolkit.shortcuts import PromptSession
__all__ = [
"generate_completions",
"display_completions_like_readline",
]
E = KeyPressEvent
def generate_completions(event: E) -> None:
r"""
Tab-completion: where the first tab completes the common suffix and the
second tab lists all the completions.
"""
b = event.current_buffer
# When already navigating through completions, select the next one.
if b.complete_state:
b.complete_next()
else:
b.start_completion(insert_common_part=True)
def display_completions_like_readline(event: E) -> None:
"""
Key binding handler for readline-style tab completion.
This is meant to be as similar as possible to the way how readline displays
completions.
Generate the completions immediately (blocking) and display them above the
prompt in columns.
Usage::
# Call this handler when 'Tab' has been pressed.
key_bindings.add(Keys.ControlI)(display_completions_like_readline)
"""
# Request completions.
b = event.current_buffer
if b.completer is None:
return
complete_event = CompleteEvent(completion_requested=True)
completions = list(b.completer.get_completions(b.document, complete_event))
# Calculate the common suffix.
common_suffix = get_common_complete_suffix(b.document, completions)
# One completion: insert it.
if len(completions) == 1:
b.delete_before_cursor(-completions[0].start_position)
b.insert_text(completions[0].text)
# Multiple completions with common part.
elif common_suffix:
b.insert_text(common_suffix)
# Otherwise: display all completions.
elif completions:
_display_completions_like_readline(event.app, completions)
def _display_completions_like_readline(
app: "Application", completions: List[Completion]
) -> "asyncio.Task[None]":
"""
Display the list of completions in columns above the prompt.
This will ask for a confirmation if there are too many completions to fit
on a single page and provide a paginator to walk through them.
"""
from prompt_toolkit.formatted_text import to_formatted_text
from prompt_toolkit.shortcuts.prompt import create_confirm_session
# Get terminal dimensions.
term_size = app.output.get_size()
term_width = term_size.columns
term_height = term_size.rows
# Calculate amount of required columns/rows for displaying the
# completions. (Keep in mind that completions are displayed
# alphabetically column-wise.)
max_compl_width = min(
term_width, max(get_cwidth(c.display_text) for c in completions) + 1
)
column_count = max(1, term_width // max_compl_width)
completions_per_page = column_count * (term_height - 1)
page_count = int(math.ceil(len(completions) / float(completions_per_page)))
# Note: math.ceil can return float on Python2.
def display(page: int) -> None:
# Display completions.
page_completions = completions[
page * completions_per_page : (page + 1) * completions_per_page
]
page_row_count = int(math.ceil(len(page_completions) / float(column_count)))
page_columns = [
page_completions[i * page_row_count : (i + 1) * page_row_count]
for i in range(column_count)
]
result: StyleAndTextTuples = []
for r in range(page_row_count):
for c in range(column_count):
try:
completion = page_columns[c][r]
style = "class:readline-like-completions.completion " + (
completion.style or ""
)
result.extend(to_formatted_text(completion.display, style=style))
# Add padding.
padding = max_compl_width - get_cwidth(completion.display_text)
result.append((completion.style, " " * padding))
except IndexError:
pass
result.append(("", "\n"))
app.print_text(to_formatted_text(result, "class:readline-like-completions"))
# User interaction through an application generator function.
async def run_compl() -> None:
" Coroutine. "
async with in_terminal(render_cli_done=True):
if len(completions) > completions_per_page:
# Ask confirmation if it doesn't fit on the screen.
confirm = await create_confirm_session(
"Display all {} possibilities?".format(len(completions)),
).prompt_async()
if confirm:
# Display pages.
for page in range(page_count):
display(page)
if page != page_count - 1:
# Display --MORE-- and go to the next page.
show_more = await _create_more_session(
"--MORE--"
).prompt_async()
if not show_more:
return
else:
app.output.flush()
else:
# Display all completions.
display(0)
return app.create_background_task(run_compl())
def _create_more_session(message: str = "--MORE--") -> "PromptSession":
"""
Create a `PromptSession` object for displaying the "--MORE--".
"""
from prompt_toolkit.shortcuts import PromptSession
bindings = KeyBindings()
@bindings.add(" ")
@bindings.add("y")
@bindings.add("Y")
@bindings.add(Keys.ControlJ)
@bindings.add(Keys.ControlM)
@bindings.add(Keys.ControlI) # Tab.
def _yes(event: E) -> None:
event.app.exit(result=True)
@bindings.add("n")
@bindings.add("N")
@bindings.add("q")
@bindings.add("Q")
@bindings.add(Keys.ControlC)
def _no(event: E) -> None:
event.app.exit(result=False)
@bindings.add(Keys.Any)
def _ignore(event: E) -> None:
" Disable inserting of text. "
return PromptSession(message, key_bindings=bindings, erase_when_done=True)

View file

@ -0,0 +1,28 @@
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.keys import Keys
from ..key_bindings import KeyBindings
__all__ = [
"load_cpr_bindings",
]
E = KeyPressEvent
def load_cpr_bindings() -> KeyBindings:
key_bindings = KeyBindings()
@key_bindings.add(Keys.CPRResponse, save_before=lambda e: False)
def _(event: E) -> None:
"""
Handle incoming Cursor-Position-Request response.
"""
# The incoming data looks like u'\x1b[35;1R'
# Parse row/col information.
row, col = map(int, event.data[2:-1].split(";"))
# Report absolute cursor position to the renderer.
event.app.renderer.report_absolute_cursor_row(row)
return key_bindings

View file

@ -0,0 +1,556 @@
# pylint: disable=function-redefined
from typing import Dict, Union
from prompt_toolkit.application.current import get_app
from prompt_toolkit.buffer import Buffer, SelectionType, indent, unindent
from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.filters import (
Condition,
emacs_insert_mode,
emacs_mode,
has_arg,
has_selection,
in_paste_mode,
is_multiline,
is_read_only,
shift_selection_mode,
vi_search_direction_reversed,
)
from prompt_toolkit.key_binding.key_bindings import Binding
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.keys import Keys
from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase
from .named_commands import get_by_name
__all__ = [
"load_emacs_bindings",
"load_emacs_search_bindings",
"load_emacs_shift_selection_bindings",
]
E = KeyPressEvent
def load_emacs_bindings() -> KeyBindingsBase:
"""
Some e-macs extensions.
"""
# Overview of Readline emacs commands:
# http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf
key_bindings = KeyBindings()
handle = key_bindings.add
insert_mode = emacs_insert_mode
@handle("escape")
def _esc(event: E) -> None:
"""
By default, ignore escape key.
(If we don't put this here, and Esc is followed by a key which sequence
is not handled, we'll insert an Escape character in the input stream.
Something we don't want and happens to easily in emacs mode.
Further, people can always use ControlQ to do a quoted insert.)
"""
pass
handle("c-a")(get_by_name("beginning-of-line"))
handle("c-b")(get_by_name("backward-char"))
handle("c-delete", filter=insert_mode)(get_by_name("kill-word"))
handle("c-e")(get_by_name("end-of-line"))
handle("c-f")(get_by_name("forward-char"))
handle("c-left")(get_by_name("backward-word"))
handle("c-right")(get_by_name("forward-word"))
handle("c-x", "r", "y", filter=insert_mode)(get_by_name("yank"))
handle("c-y", filter=insert_mode)(get_by_name("yank"))
handle("escape", "b")(get_by_name("backward-word"))
handle("escape", "c", filter=insert_mode)(get_by_name("capitalize-word"))
handle("escape", "d", filter=insert_mode)(get_by_name("kill-word"))
handle("escape", "f")(get_by_name("forward-word"))
handle("escape", "l", filter=insert_mode)(get_by_name("downcase-word"))
handle("escape", "u", filter=insert_mode)(get_by_name("uppercase-word"))
handle("escape", "y", filter=insert_mode)(get_by_name("yank-pop"))
handle("escape", "backspace", filter=insert_mode)(get_by_name("backward-kill-word"))
handle("escape", "\\", filter=insert_mode)(get_by_name("delete-horizontal-space"))
handle("c-home")(get_by_name("beginning-of-buffer"))
handle("c-end")(get_by_name("end-of-buffer"))
handle("c-_", save_before=(lambda e: False), filter=insert_mode)(
get_by_name("undo")
)
handle("c-x", "c-u", save_before=(lambda e: False), filter=insert_mode)(
get_by_name("undo")
)
handle("escape", "<", filter=~has_selection)(get_by_name("beginning-of-history"))
handle("escape", ">", filter=~has_selection)(get_by_name("end-of-history"))
handle("escape", ".", filter=insert_mode)(get_by_name("yank-last-arg"))
handle("escape", "_", filter=insert_mode)(get_by_name("yank-last-arg"))
handle("escape", "c-y", filter=insert_mode)(get_by_name("yank-nth-arg"))
handle("escape", "#", filter=insert_mode)(get_by_name("insert-comment"))
handle("c-o")(get_by_name("operate-and-get-next"))
# ControlQ does a quoted insert. Not that for vt100 terminals, you have to
# disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and
# Ctrl-S are captured by the terminal.
handle("c-q", filter=~has_selection)(get_by_name("quoted-insert"))
handle("c-x", "(")(get_by_name("start-kbd-macro"))
handle("c-x", ")")(get_by_name("end-kbd-macro"))
handle("c-x", "e")(get_by_name("call-last-kbd-macro"))
@handle("c-n")
def _next(event: E) -> None:
" Next line. "
event.current_buffer.auto_down()
@handle("c-p")
def _prev(event: E) -> None:
" Previous line. "
event.current_buffer.auto_up(count=event.arg)
def handle_digit(c: str) -> None:
"""
Handle input of arguments.
The first number needs to be preceded by escape.
"""
@handle(c, filter=has_arg)
@handle("escape", c)
def _(event: E) -> None:
event.append_to_arg_count(c)
for c in "0123456789":
handle_digit(c)
@handle("escape", "-", filter=~has_arg)
def _meta_dash(event: E) -> None:
""""""
if event._arg is None:
event.append_to_arg_count("-")
@handle("-", filter=Condition(lambda: get_app().key_processor.arg == "-"))
def _dash(event: E) -> None:
"""
When '-' is typed again, after exactly '-' has been given as an
argument, ignore this.
"""
event.app.key_processor.arg = "-"
@Condition
def is_returnable() -> bool:
return get_app().current_buffer.is_returnable
# Meta + Enter: always accept input.
handle("escape", "enter", filter=insert_mode & is_returnable)(
get_by_name("accept-line")
)
# Enter: accept input in single line mode.
handle("enter", filter=insert_mode & is_returnable & ~is_multiline)(
get_by_name("accept-line")
)
def character_search(buff: Buffer, char: str, count: int) -> None:
if count < 0:
match = buff.document.find_backwards(
char, in_current_line=True, count=-count
)
else:
match = buff.document.find(char, in_current_line=True, count=count)
if match is not None:
buff.cursor_position += match
@handle("c-]", Keys.Any)
def _goto_char(event: E) -> None:
" When Ctl-] + a character is pressed. go to that character. "
# Also named 'character-search'
character_search(event.current_buffer, event.data, event.arg)
@handle("escape", "c-]", Keys.Any)
def _goto_char_backwards(event: E) -> None:
" Like Ctl-], but backwards. "
# Also named 'character-search-backward'
character_search(event.current_buffer, event.data, -event.arg)
@handle("escape", "a")
def _prev_sentence(event: E) -> None:
" Previous sentence. "
# TODO:
@handle("escape", "e")
def _end_of_sentence(event: E) -> None:
" Move to end of sentence. "
# TODO:
@handle("escape", "t", filter=insert_mode)
def _swap_characters(event: E) -> None:
"""
Swap the last two words before the cursor.
"""
# TODO
@handle("escape", "*", filter=insert_mode)
def _insert_all_completions(event: E) -> None:
"""
`meta-*`: Insert all possible completions of the preceding text.
"""
buff = event.current_buffer
# List all completions.
complete_event = CompleteEvent(text_inserted=False, completion_requested=True)
completions = list(
buff.completer.get_completions(buff.document, complete_event)
)
# Insert them.
text_to_insert = " ".join(c.text for c in completions)
buff.insert_text(text_to_insert)
@handle("c-x", "c-x")
def _toggle_start_end(event: E) -> None:
"""
Move cursor back and forth between the start and end of the current
line.
"""
buffer = event.current_buffer
if buffer.document.is_cursor_at_the_end_of_line:
buffer.cursor_position += buffer.document.get_start_of_line_position(
after_whitespace=False
)
else:
buffer.cursor_position += buffer.document.get_end_of_line_position()
@handle("c-@") # Control-space or Control-@
def _start_selection(event: E) -> None:
"""
Start of the selection (if the current buffer is not empty).
"""
# Take the current cursor position as the start of this selection.
buff = event.current_buffer
if buff.text:
buff.start_selection(selection_type=SelectionType.CHARACTERS)
@handle("c-g", filter=~has_selection)
def _cancel(event: E) -> None:
"""
Control + G: Cancel completion menu and validation state.
"""
event.current_buffer.complete_state = None
event.current_buffer.validation_error = None
@handle("c-g", filter=has_selection)
def _cancel_selection(event: E) -> None:
"""
Cancel selection.
"""
event.current_buffer.exit_selection()
@handle("c-w", filter=has_selection)
@handle("c-x", "r", "k", filter=has_selection)
def _cut(event: E) -> None:
"""
Cut selected text.
"""
data = event.current_buffer.cut_selection()
event.app.clipboard.set_data(data)
@handle("escape", "w", filter=has_selection)
def _copy(event: E) -> None:
"""
Copy selected text.
"""
data = event.current_buffer.copy_selection()
event.app.clipboard.set_data(data)
@handle("escape", "left")
def _start_of_word(event: E) -> None:
"""
Cursor to start of previous word.
"""
buffer = event.current_buffer
buffer.cursor_position += (
buffer.document.find_previous_word_beginning(count=event.arg) or 0
)
@handle("escape", "right")
def _start_next_word(event: E) -> None:
"""
Cursor to start of next word.
"""
buffer = event.current_buffer
buffer.cursor_position += (
buffer.document.find_next_word_beginning(count=event.arg)
or buffer.document.get_end_of_document_position()
)
@handle("escape", "/", filter=insert_mode)
def _complete(event: E) -> None:
"""
M-/: Complete.
"""
b = event.current_buffer
if b.complete_state:
b.complete_next()
else:
b.start_completion(select_first=True)
@handle("c-c", ">", filter=has_selection)
def _indent(event: E) -> None:
"""
Indent selected text.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.get_start_of_line_position(
after_whitespace=True
)
from_, to = buffer.document.selection_range()
from_, _ = buffer.document.translate_index_to_position(from_)
to, _ = buffer.document.translate_index_to_position(to)
indent(buffer, from_, to + 1, count=event.arg)
@handle("c-c", "<", filter=has_selection)
def _unindent(event: E) -> None:
"""
Unindent selected text.
"""
buffer = event.current_buffer
from_, to = buffer.document.selection_range()
from_, _ = buffer.document.translate_index_to_position(from_)
to, _ = buffer.document.translate_index_to_position(to)
unindent(buffer, from_, to + 1, count=event.arg)
return ConditionalKeyBindings(key_bindings, emacs_mode)
def load_emacs_search_bindings() -> KeyBindingsBase:
key_bindings = KeyBindings()
handle = key_bindings.add
from . import search
# NOTE: We don't bind 'Escape' to 'abort_search'. The reason is that we
# want Alt+Enter to accept input directly in incremental search mode.
# Instead, we have double escape.
handle("c-r")(search.start_reverse_incremental_search)
handle("c-s")(search.start_forward_incremental_search)
handle("c-c")(search.abort_search)
handle("c-g")(search.abort_search)
handle("c-r")(search.reverse_incremental_search)
handle("c-s")(search.forward_incremental_search)
handle("up")(search.reverse_incremental_search)
handle("down")(search.forward_incremental_search)
handle("enter")(search.accept_search)
# Handling of escape.
handle("escape", eager=True)(search.accept_search)
# Like Readline, it's more natural to accept the search when escape has
# been pressed, however instead the following two bindings could be used
# instead.
# #handle('escape', 'escape', eager=True)(search.abort_search)
# #handle('escape', 'enter', eager=True)(search.accept_search_and_accept_input)
# If Read-only: also include the following key bindings:
# '/' and '?' key bindings for searching, just like Vi mode.
handle("?", filter=is_read_only & ~vi_search_direction_reversed)(
search.start_reverse_incremental_search
)
handle("/", filter=is_read_only & ~vi_search_direction_reversed)(
search.start_forward_incremental_search
)
handle("?", filter=is_read_only & vi_search_direction_reversed)(
search.start_forward_incremental_search
)
handle("/", filter=is_read_only & vi_search_direction_reversed)(
search.start_reverse_incremental_search
)
@handle("n", filter=is_read_only)
def _jump_next(event: E) -> None:
" Jump to next match. "
event.current_buffer.apply_search(
event.app.current_search_state,
include_current_position=False,
count=event.arg,
)
@handle("N", filter=is_read_only)
def _jump_prev(event: E) -> None:
" Jump to previous match. "
event.current_buffer.apply_search(
~event.app.current_search_state,
include_current_position=False,
count=event.arg,
)
return ConditionalKeyBindings(key_bindings, emacs_mode)
def load_emacs_shift_selection_bindings() -> KeyBindingsBase:
"""
Bindings to select text with shift + cursor movements
"""
key_bindings = KeyBindings()
handle = key_bindings.add
def unshift_move(event: E) -> None:
"""
Used for the shift selection mode. When called with
a shift + movement key press event, moves the cursor
as if shift is not pressed.
"""
key = event.key_sequence[0].key
if key == Keys.ShiftUp:
event.current_buffer.auto_up(count=event.arg)
return
if key == Keys.ShiftDown:
event.current_buffer.auto_down(count=event.arg)
return
# the other keys are handled through their readline command
key_to_command: Dict[Union[Keys, str], str] = {
Keys.ShiftLeft: "backward-char",
Keys.ShiftRight: "forward-char",
Keys.ShiftHome: "beginning-of-line",
Keys.ShiftEnd: "end-of-line",
Keys.ControlShiftLeft: "backward-word",
Keys.ControlShiftRight: "forward-word",
Keys.ControlShiftHome: "beginning-of-buffer",
Keys.ControlShiftEnd: "end-of-buffer",
}
try:
# Both the dict lookup and `get_by_name` can raise KeyError.
binding = get_by_name(key_to_command[key])
except KeyError:
pass
else: # (`else` is not really needed here.)
if isinstance(binding, Binding):
# (It should always be a binding here)
binding.call(event)
@handle("s-left", filter=~has_selection)
@handle("s-right", filter=~has_selection)
@handle("s-up", filter=~has_selection)
@handle("s-down", filter=~has_selection)
@handle("s-home", filter=~has_selection)
@handle("s-end", filter=~has_selection)
@handle("c-s-left", filter=~has_selection)
@handle("c-s-right", filter=~has_selection)
@handle("c-s-home", filter=~has_selection)
@handle("c-s-end", filter=~has_selection)
def _start_selection(event: E) -> None:
"""
Start selection with shift + movement.
"""
# Take the current cursor position as the start of this selection.
buff = event.current_buffer
if buff.text:
buff.start_selection(selection_type=SelectionType.CHARACTERS)
if buff.selection_state is not None:
# (`selection_state` should never be `None`, it is created by
# `start_selection`.)
buff.selection_state.enter_shift_mode()
# Then move the cursor
original_position = buff.cursor_position
unshift_move(event)
if buff.cursor_position == original_position:
# Cursor didn't actually move - so cancel selection
# to avoid having an empty selection
buff.exit_selection()
@handle("s-left", filter=shift_selection_mode)
@handle("s-right", filter=shift_selection_mode)
@handle("s-up", filter=shift_selection_mode)
@handle("s-down", filter=shift_selection_mode)
@handle("s-home", filter=shift_selection_mode)
@handle("s-end", filter=shift_selection_mode)
@handle("c-s-left", filter=shift_selection_mode)
@handle("c-s-right", filter=shift_selection_mode)
@handle("c-s-home", filter=shift_selection_mode)
@handle("c-s-end", filter=shift_selection_mode)
def _extend_selection(event: E) -> None:
"""
Extend the selection
"""
# Just move the cursor, like shift was not pressed
unshift_move(event)
buff = event.current_buffer
if buff.selection_state is not None:
if buff.cursor_position == buff.selection_state.original_cursor_position:
# selection is now empty, so cancel selection
buff.exit_selection()
@handle(Keys.Any, filter=shift_selection_mode)
def _replace_selection(event: E) -> None:
"""
Replace selection by what is typed
"""
event.current_buffer.cut_selection()
get_by_name("self-insert").call(event)
@handle("enter", filter=shift_selection_mode & is_multiline)
def _newline(event: E) -> None:
"""
A newline replaces the selection
"""
event.current_buffer.cut_selection()
event.current_buffer.newline(copy_margin=not in_paste_mode())
@handle("backspace", filter=shift_selection_mode)
def _delete(event: E) -> None:
"""
Delete selection.
"""
event.current_buffer.cut_selection()
@handle("c-y", filter=shift_selection_mode)
def _yank(event: E) -> None:
"""
In shift selection mode, yanking (pasting) replace the selection.
"""
buff = event.current_buffer
if buff.selection_state:
buff.cut_selection()
get_by_name("yank").call(event)
# moving the cursor in shift selection mode cancels the selection
@handle("left", filter=shift_selection_mode)
@handle("right", filter=shift_selection_mode)
@handle("up", filter=shift_selection_mode)
@handle("down", filter=shift_selection_mode)
@handle("home", filter=shift_selection_mode)
@handle("end", filter=shift_selection_mode)
@handle("c-left", filter=shift_selection_mode)
@handle("c-right", filter=shift_selection_mode)
@handle("c-home", filter=shift_selection_mode)
@handle("c-end", filter=shift_selection_mode)
def _cancel(event: E) -> None:
"""
Cancel selection.
"""
event.current_buffer.exit_selection()
# we then process the cursor movement
key_press = event.key_sequence[0]
event.key_processor.feed(key_press, first=True)
return ConditionalKeyBindings(key_bindings, emacs_mode)

View file

@ -0,0 +1,24 @@
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
__all__ = [
"focus_next",
"focus_previous",
]
E = KeyPressEvent
def focus_next(event: E) -> None:
"""
Focus the next visible Window.
(Often bound to the `Tab` key.)
"""
event.app.layout.focus_next()
def focus_previous(event: E) -> None:
"""
Focus the previous visible Window.
(Often bound to the `BackTab` key.)
"""
event.app.layout.focus_previous()

View file

@ -0,0 +1,146 @@
from prompt_toolkit.data_structures import Point
from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
from prompt_toolkit.keys import Keys
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
from prompt_toolkit.utils import is_windows
from ..key_bindings import KeyBindings
__all__ = [
"load_mouse_bindings",
]
E = KeyPressEvent
def load_mouse_bindings() -> KeyBindings:
"""
Key bindings, required for mouse support.
(Mouse events enter through the key binding system.)
"""
key_bindings = KeyBindings()
@key_bindings.add(Keys.Vt100MouseEvent)
def _(event: E) -> None:
"""
Handling of incoming mouse event.
"""
# TypicaL: "eSC[MaB*"
# Urxvt: "Esc[96;14;13M"
# Xterm SGR: "Esc[<64;85;12M"
# Parse incoming packet.
if event.data[2] == "M":
# Typical.
mouse_event, x, y = map(ord, event.data[3:])
mouse_event_type = {
32: MouseEventType.MOUSE_DOWN,
35: MouseEventType.MOUSE_UP,
96: MouseEventType.SCROLL_UP,
97: MouseEventType.SCROLL_DOWN,
}.get(mouse_event)
# Handle situations where `PosixStdinReader` used surrogateescapes.
if x >= 0xDC00:
x -= 0xDC00
if y >= 0xDC00:
y -= 0xDC00
x -= 32
y -= 32
else:
# Urxvt and Xterm SGR.
# When the '<' is not present, we are not using the Xterm SGR mode,
# but Urxvt instead.
data = event.data[2:]
if data[:1] == "<":
sgr = True
data = data[1:]
else:
sgr = False
# Extract coordinates.
mouse_event, x, y = map(int, data[:-1].split(";"))
m = data[-1]
# Parse event type.
if sgr:
mouse_event_type = {
(0, "M"): MouseEventType.MOUSE_DOWN,
(0, "m"): MouseEventType.MOUSE_UP,
(64, "M"): MouseEventType.SCROLL_UP,
(65, "M"): MouseEventType.SCROLL_DOWN,
}.get((mouse_event, m))
else:
mouse_event_type = {
32: MouseEventType.MOUSE_DOWN,
35: MouseEventType.MOUSE_UP,
96: MouseEventType.SCROLL_UP,
97: MouseEventType.SCROLL_DOWN,
}.get(mouse_event)
x -= 1
y -= 1
# Only handle mouse events when we know the window height.
if event.app.renderer.height_is_known and mouse_event_type is not None:
# Take region above the layout into account. The reported
# coordinates are absolute to the visible part of the terminal.
from prompt_toolkit.renderer import HeightIsUnknownError
try:
y -= event.app.renderer.rows_above_layout
except HeightIsUnknownError:
return
# Call the mouse handler from the renderer.
handler = event.app.renderer.mouse_handlers.mouse_handlers[x, y]
handler(MouseEvent(position=Point(x=x, y=y), event_type=mouse_event_type))
@key_bindings.add(Keys.ScrollUp)
def _scroll_up(event: E) -> None:
"""
Scroll up event without cursor position.
"""
# We don't receive a cursor position, so we don't know which window to
# scroll. Just send an 'up' key press instead.
event.key_processor.feed(KeyPress(Keys.Up), first=True)
@key_bindings.add(Keys.ScrollDown)
def _scroll_down(event: E) -> None:
"""
Scroll down event without cursor position.
"""
event.key_processor.feed(KeyPress(Keys.Down), first=True)
@key_bindings.add(Keys.WindowsMouseEvent)
def _mouse(event: E) -> None:
"""
Handling of mouse events for Windows.
"""
assert is_windows() # This key binding should only exist for Windows.
# Parse data.
pieces = event.data.split(";")
event_type = MouseEventType(pieces[0])
x = int(pieces[1])
y = int(pieces[2])
# Make coordinates absolute to the visible part of the terminal.
output = event.app.renderer.output
from prompt_toolkit.output.win32 import Win32Output
if isinstance(output, Win32Output):
screen_buffer_info = output.get_win32_screen_buffer_info()
rows_above_cursor = (
screen_buffer_info.dwCursorPosition.Y - event.app.renderer._cursor_pos.y
)
y -= rows_above_cursor
# Call the mouse event handler.
handler = event.app.renderer.mouse_handlers.mouse_handlers[x, y]
handler(MouseEvent(position=Point(x=x, y=y), event_type=event_type))
return key_bindings

View file

@ -0,0 +1,687 @@
"""
Key bindings which are also known by GNU Readline by the given names.
See: http://www.delorie.com/gnu/docs/readline/rlman_13.html
"""
from typing import Callable, Dict, TypeVar, Union, cast
from prompt_toolkit.document import Document
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.key_binding.key_bindings import Binding, key_binding
from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout.controls import BufferControl
from prompt_toolkit.search import SearchDirection
from prompt_toolkit.selection import PasteMode
from .completion import display_completions_like_readline, generate_completions
__all__ = [
"get_by_name",
]
# Typing.
_Handler = Callable[[KeyPressEvent], None]
_HandlerOrBinding = Union[_Handler, Binding]
_T = TypeVar("_T", bound=_HandlerOrBinding)
E = KeyPressEvent
# Registry that maps the Readline command names to their handlers.
_readline_commands: Dict[str, Binding] = {}
def register(name: str) -> Callable[[_T], _T]:
"""
Store handler in the `_readline_commands` dictionary.
"""
def decorator(handler: _T) -> _T:
" `handler` is a callable or Binding. "
if isinstance(handler, Binding):
_readline_commands[name] = handler
else:
_readline_commands[name] = key_binding()(cast(_Handler, handler))
return handler
return decorator
def get_by_name(name: str) -> Binding:
"""
Return the handler for the (Readline) command with the given name.
"""
try:
return _readline_commands[name]
except KeyError as e:
raise KeyError("Unknown Readline command: %r" % name) from e
#
# Commands for moving
# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html
#
@register("beginning-of-buffer")
def beginning_of_buffer(event: E) -> None:
"""
Move to the start of the buffer.
"""
buff = event.current_buffer
buff.cursor_position = 0
@register("end-of-buffer")
def end_of_buffer(event: E) -> None:
"""
Move to the end of the buffer.
"""
buff = event.current_buffer
buff.cursor_position = len(buff.text)
@register("beginning-of-line")
def beginning_of_line(event: E) -> None:
"""
Move to the start of the current line.
"""
buff = event.current_buffer
buff.cursor_position += buff.document.get_start_of_line_position(
after_whitespace=False
)
@register("end-of-line")
def end_of_line(event: E) -> None:
"""
Move to the end of the line.
"""
buff = event.current_buffer
buff.cursor_position += buff.document.get_end_of_line_position()
@register("forward-char")
def forward_char(event: E) -> None:
"""
Move forward a character.
"""
buff = event.current_buffer
buff.cursor_position += buff.document.get_cursor_right_position(count=event.arg)
@register("backward-char")
def backward_char(event: E) -> None:
" Move back a character. "
buff = event.current_buffer
buff.cursor_position += buff.document.get_cursor_left_position(count=event.arg)
@register("forward-word")
def forward_word(event: E) -> None:
"""
Move forward to the end of the next word. Words are composed of letters and
digits.
"""
buff = event.current_buffer
pos = buff.document.find_next_word_ending(count=event.arg)
if pos:
buff.cursor_position += pos
@register("backward-word")
def backward_word(event: E) -> None:
"""
Move back to the start of the current or previous word. Words are composed
of letters and digits.
"""
buff = event.current_buffer
pos = buff.document.find_previous_word_beginning(count=event.arg)
if pos:
buff.cursor_position += pos
@register("clear-screen")
def clear_screen(event: E) -> None:
"""
Clear the screen and redraw everything at the top of the screen.
"""
event.app.renderer.clear()
@register("redraw-current-line")
def redraw_current_line(event: E) -> None:
"""
Refresh the current line.
(Readline defines this command, but prompt-toolkit doesn't have it.)
"""
pass
#
# Commands for manipulating the history.
# See: http://www.delorie.com/gnu/docs/readline/rlman_15.html
#
@register("accept-line")
def accept_line(event: E) -> None:
"""
Accept the line regardless of where the cursor is.
"""
event.current_buffer.validate_and_handle()
@register("previous-history")
def previous_history(event: E) -> None:
"""
Move `back` through the history list, fetching the previous command.
"""
event.current_buffer.history_backward(count=event.arg)
@register("next-history")
def next_history(event: E) -> None:
"""
Move `forward` through the history list, fetching the next command.
"""
event.current_buffer.history_forward(count=event.arg)
@register("beginning-of-history")
def beginning_of_history(event: E) -> None:
"""
Move to the first line in the history.
"""
event.current_buffer.go_to_history(0)
@register("end-of-history")
def end_of_history(event: E) -> None:
"""
Move to the end of the input history, i.e., the line currently being entered.
"""
event.current_buffer.history_forward(count=10 ** 100)
buff = event.current_buffer
buff.go_to_history(len(buff._working_lines) - 1)
@register("reverse-search-history")
def reverse_search_history(event: E) -> None:
"""
Search backward starting at the current line and moving `up` through
the history as necessary. This is an incremental search.
"""
control = event.app.layout.current_control
if isinstance(control, BufferControl) and control.search_buffer_control:
event.app.current_search_state.direction = SearchDirection.BACKWARD
event.app.layout.current_control = control.search_buffer_control
#
# Commands for changing text
#
@register("end-of-file")
def end_of_file(event: E) -> None:
"""
Exit.
"""
event.app.exit()
@register("delete-char")
def delete_char(event: E) -> None:
"""
Delete character before the cursor.
"""
deleted = event.current_buffer.delete(count=event.arg)
if not deleted:
event.app.output.bell()
@register("backward-delete-char")
def backward_delete_char(event: E) -> None:
"""
Delete the character behind the cursor.
"""
if event.arg < 0:
# When a negative argument has been given, this should delete in front
# of the cursor.
deleted = event.current_buffer.delete(count=-event.arg)
else:
deleted = event.current_buffer.delete_before_cursor(count=event.arg)
if not deleted:
event.app.output.bell()
@register("self-insert")
def self_insert(event: E) -> None:
"""
Insert yourself.
"""
event.current_buffer.insert_text(event.data * event.arg)
@register("transpose-chars")
def transpose_chars(event: E) -> None:
"""
Emulate Emacs transpose-char behavior: at the beginning of the buffer,
do nothing. At the end of a line or buffer, swap the characters before
the cursor. Otherwise, move the cursor right, and then swap the
characters before the cursor.
"""
b = event.current_buffer
p = b.cursor_position
if p == 0:
return
elif p == len(b.text) or b.text[p] == "\n":
b.swap_characters_before_cursor()
else:
b.cursor_position += b.document.get_cursor_right_position()
b.swap_characters_before_cursor()
@register("uppercase-word")
def uppercase_word(event: E) -> None:
"""
Uppercase the current (or following) word.
"""
buff = event.current_buffer
for i in range(event.arg):
pos = buff.document.find_next_word_ending()
words = buff.document.text_after_cursor[:pos]
buff.insert_text(words.upper(), overwrite=True)
@register("downcase-word")
def downcase_word(event: E) -> None:
"""
Lowercase the current (or following) word.
"""
buff = event.current_buffer
for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!!
pos = buff.document.find_next_word_ending()
words = buff.document.text_after_cursor[:pos]
buff.insert_text(words.lower(), overwrite=True)
@register("capitalize-word")
def capitalize_word(event: E) -> None:
"""
Capitalize the current (or following) word.
"""
buff = event.current_buffer
for i in range(event.arg):
pos = buff.document.find_next_word_ending()
words = buff.document.text_after_cursor[:pos]
buff.insert_text(words.title(), overwrite=True)
@register("quoted-insert")
def quoted_insert(event: E) -> None:
"""
Add the next character typed to the line verbatim. This is how to insert
key sequences like C-q, for example.
"""
event.app.quoted_insert = True
#
# Killing and yanking.
#
@register("kill-line")
def kill_line(event: E) -> None:
"""
Kill the text from the cursor to the end of the line.
If we are at the end of the line, this should remove the newline.
(That way, it is possible to delete multiple lines by executing this
command multiple times.)
"""
buff = event.current_buffer
if event.arg < 0:
deleted = buff.delete_before_cursor(
count=-buff.document.get_start_of_line_position()
)
else:
if buff.document.current_char == "\n":
deleted = buff.delete(1)
else:
deleted = buff.delete(count=buff.document.get_end_of_line_position())
event.app.clipboard.set_text(deleted)
@register("kill-word")
def kill_word(event: E) -> None:
"""
Kill from point to the end of the current word, or if between words, to the
end of the next word. Word boundaries are the same as forward-word.
"""
buff = event.current_buffer
pos = buff.document.find_next_word_ending(count=event.arg)
if pos:
deleted = buff.delete(count=pos)
if event.is_repeat:
deleted = event.app.clipboard.get_data().text + deleted
event.app.clipboard.set_text(deleted)
@register("unix-word-rubout")
def unix_word_rubout(event: E, WORD: bool = True) -> None:
"""
Kill the word behind point, using whitespace as a word boundary.
Usually bound to ControlW.
"""
buff = event.current_buffer
pos = buff.document.find_start_of_previous_word(count=event.arg, WORD=WORD)
if pos is None:
# Nothing found? delete until the start of the document. (The
# input starts with whitespace and no words were found before the
# cursor.)
pos = -buff.cursor_position
if pos:
deleted = buff.delete_before_cursor(count=-pos)
# If the previous key press was also Control-W, concatenate deleted
# text.
if event.is_repeat:
deleted += event.app.clipboard.get_data().text
event.app.clipboard.set_text(deleted)
else:
# Nothing to delete. Bell.
event.app.output.bell()
@register("backward-kill-word")
def backward_kill_word(event: E) -> None:
"""
Kills the word before point, using "not a letter nor a digit" as a word boundary.
Usually bound to M-Del or M-Backspace.
"""
unix_word_rubout(event, WORD=False)
@register("delete-horizontal-space")
def delete_horizontal_space(event: E) -> None:
"""
Delete all spaces and tabs around point.
"""
buff = event.current_buffer
text_before_cursor = buff.document.text_before_cursor
text_after_cursor = buff.document.text_after_cursor
delete_before = len(text_before_cursor) - len(text_before_cursor.rstrip("\t "))
delete_after = len(text_after_cursor) - len(text_after_cursor.lstrip("\t "))
buff.delete_before_cursor(count=delete_before)
buff.delete(count=delete_after)
@register("unix-line-discard")
def unix_line_discard(event: E) -> None:
"""
Kill backward from the cursor to the beginning of the current line.
"""
buff = event.current_buffer
if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0:
buff.delete_before_cursor(count=1)
else:
deleted = buff.delete_before_cursor(
count=-buff.document.get_start_of_line_position()
)
event.app.clipboard.set_text(deleted)
@register("yank")
def yank(event: E) -> None:
"""
Paste before cursor.
"""
event.current_buffer.paste_clipboard_data(
event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS
)
@register("yank-nth-arg")
def yank_nth_arg(event: E) -> None:
"""
Insert the first argument of the previous command. With an argument, insert
the nth word from the previous command (start counting at 0).
"""
n = event.arg if event.arg_present else None
event.current_buffer.yank_nth_arg(n)
@register("yank-last-arg")
def yank_last_arg(event: E) -> None:
"""
Like `yank_nth_arg`, but if no argument has been given, yank the last word
of each line.
"""
n = event.arg if event.arg_present else None
event.current_buffer.yank_last_arg(n)
@register("yank-pop")
def yank_pop(event: E) -> None:
"""
Rotate the kill ring, and yank the new top. Only works following yank or
yank-pop.
"""
buff = event.current_buffer
doc_before_paste = buff.document_before_paste
clipboard = event.app.clipboard
if doc_before_paste is not None:
buff.document = doc_before_paste
clipboard.rotate()
buff.paste_clipboard_data(clipboard.get_data(), paste_mode=PasteMode.EMACS)
#
# Completion.
#
@register("complete")
def complete(event: E) -> None:
"""
Attempt to perform completion.
"""
display_completions_like_readline(event)
@register("menu-complete")
def menu_complete(event: E) -> None:
"""
Generate completions, or go to the next completion. (This is the default
way of completing input in prompt_toolkit.)
"""
generate_completions(event)
@register("menu-complete-backward")
def menu_complete_backward(event: E) -> None:
"""
Move backward through the list of possible completions.
"""
event.current_buffer.complete_previous()
#
# Keyboard macros.
#
@register("start-kbd-macro")
def start_kbd_macro(event: E) -> None:
"""
Begin saving the characters typed into the current keyboard macro.
"""
event.app.emacs_state.start_macro()
@register("end-kbd-macro")
def end_kbd_macro(event: E) -> None:
"""
Stop saving the characters typed into the current keyboard macro and save
the definition.
"""
event.app.emacs_state.end_macro()
@register("call-last-kbd-macro")
@key_binding(record_in_macro=False)
def call_last_kbd_macro(event: E) -> None:
"""
Re-execute the last keyboard macro defined, by making the characters in the
macro appear as if typed at the keyboard.
Notice that we pass `record_in_macro=False`. This ensures that the 'c-x e'
key sequence doesn't appear in the recording itself. This function inserts
the body of the called macro back into the KeyProcessor, so these keys will
be added later on to the macro of their handlers have `record_in_macro=True`.
"""
# Insert the macro.
macro = event.app.emacs_state.macro
if macro:
event.app.key_processor.feed_multiple(macro, first=True)
@register("print-last-kbd-macro")
def print_last_kbd_macro(event: E) -> None:
"""
Print the last keyboard macro.
"""
# TODO: Make the format suitable for the inputrc file.
def print_macro() -> None:
macro = event.app.emacs_state.macro
if macro:
for k in macro:
print(k)
from prompt_toolkit.application.run_in_terminal import run_in_terminal
run_in_terminal(print_macro)
#
# Miscellaneous Commands.
#
@register("undo")
def undo(event: E) -> None:
"""
Incremental undo.
"""
event.current_buffer.undo()
@register("insert-comment")
def insert_comment(event: E) -> None:
"""
Without numeric argument, comment all lines.
With numeric argument, uncomment all lines.
In any case accept the input.
"""
buff = event.current_buffer
# Transform all lines.
if event.arg != 1:
def change(line: str) -> str:
return line[1:] if line.startswith("#") else line
else:
def change(line: str) -> str:
return "#" + line
buff.document = Document(
text="\n".join(map(change, buff.text.splitlines())), cursor_position=0
)
# Accept input.
buff.validate_and_handle()
@register("vi-editing-mode")
def vi_editing_mode(event: E) -> None:
"""
Switch to Vi editing mode.
"""
event.app.editing_mode = EditingMode.VI
@register("emacs-editing-mode")
def emacs_editing_mode(event: E) -> None:
"""
Switch to Emacs editing mode.
"""
event.app.editing_mode = EditingMode.EMACS
@register("prefix-meta")
def prefix_meta(event: E) -> None:
"""
Metafy the next character typed. This is for keyboards without a meta key.
Sometimes people also want to bind other keys to Meta, e.g. 'jj'::
key_bindings.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta)
"""
# ('first' should be true, because we want to insert it at the current
# position in the queue.)
event.app.key_processor.feed(KeyPress(Keys.Escape), first=True)
@register("operate-and-get-next")
def operate_and_get_next(event: E) -> None:
"""
Accept the current line for execution and fetch the next line relative to
the current line from the history for editing.
"""
buff = event.current_buffer
new_index = buff.working_index + 1
# Accept the current input. (This will also redraw the interface in the
# 'done' state.)
buff.validate_and_handle()
# Set the new index at the start of the next run.
def set_working_index() -> None:
if new_index < len(buff._working_lines):
buff.working_index = new_index
event.app.pre_run_callables.append(set_working_index)
@register("edit-and-execute-command")
def edit_and_execute(event: E) -> None:
"""
Invoke an editor on the current command line, and accept the result.
"""
buff = event.current_buffer
buff.open_in_editor(validate_and_handle=True)

View file

@ -0,0 +1,49 @@
"""
Open in editor key bindings.
"""
from prompt_toolkit.filters import emacs_mode, has_selection, vi_navigation_mode
from ..key_bindings import KeyBindings, KeyBindingsBase, merge_key_bindings
from .named_commands import get_by_name
__all__ = [
"load_open_in_editor_bindings",
"load_emacs_open_in_editor_bindings",
"load_vi_open_in_editor_bindings",
]
def load_open_in_editor_bindings() -> KeyBindingsBase:
"""
Load both the Vi and emacs key bindings for handling edit-and-execute-command.
"""
return merge_key_bindings(
[
load_emacs_open_in_editor_bindings(),
load_vi_open_in_editor_bindings(),
]
)
def load_emacs_open_in_editor_bindings() -> KeyBindings:
"""
Pressing C-X C-E will open the buffer in an external editor.
"""
key_bindings = KeyBindings()
key_bindings.add("c-x", "c-e", filter=emacs_mode & ~has_selection)(
get_by_name("edit-and-execute-command")
)
return key_bindings
def load_vi_open_in_editor_bindings() -> KeyBindings:
"""
Pressing 'v' in navigation mode will open the buffer in an external editor.
"""
key_bindings = KeyBindings()
key_bindings.add("v", filter=vi_navigation_mode)(
get_by_name("edit-and-execute-command")
)
return key_bindings

View file

@ -0,0 +1,82 @@
"""
Key bindings for extra page navigation: bindings for up/down scrolling through
long pages, like in Emacs or Vi.
"""
from prompt_toolkit.filters import buffer_has_focus, emacs_mode, vi_mode
from prompt_toolkit.key_binding.key_bindings import (
ConditionalKeyBindings,
KeyBindings,
KeyBindingsBase,
merge_key_bindings,
)
from .scroll import (
scroll_backward,
scroll_forward,
scroll_half_page_down,
scroll_half_page_up,
scroll_one_line_down,
scroll_one_line_up,
scroll_page_down,
scroll_page_up,
)
__all__ = [
"load_page_navigation_bindings",
"load_emacs_page_navigation_bindings",
"load_vi_page_navigation_bindings",
]
def load_page_navigation_bindings() -> KeyBindingsBase:
"""
Load both the Vi and Emacs bindings for page navigation.
"""
# Only enable when a `Buffer` is focused, otherwise, we would catch keys
# when another widget is focused (like for instance `c-d` in a
# ptterm.Terminal).
return ConditionalKeyBindings(
merge_key_bindings(
[
load_emacs_page_navigation_bindings(),
load_vi_page_navigation_bindings(),
]
),
buffer_has_focus,
)
def load_emacs_page_navigation_bindings() -> KeyBindingsBase:
"""
Key bindings, for scrolling up and down through pages.
This are separate bindings, because GNU readline doesn't have them.
"""
key_bindings = KeyBindings()
handle = key_bindings.add
handle("c-v")(scroll_page_down)
handle("pagedown")(scroll_page_down)
handle("escape", "v")(scroll_page_up)
handle("pageup")(scroll_page_up)
return ConditionalKeyBindings(key_bindings, emacs_mode)
def load_vi_page_navigation_bindings() -> KeyBindingsBase:
"""
Key bindings, for scrolling up and down through pages.
This are separate bindings, because GNU readline doesn't have them.
"""
key_bindings = KeyBindings()
handle = key_bindings.add
handle("c-f")(scroll_forward)
handle("c-b")(scroll_backward)
handle("c-d")(scroll_half_page_down)
handle("c-u")(scroll_half_page_up)
handle("c-e")(scroll_one_line_down)
handle("c-y")(scroll_one_line_up)
handle("pagedown")(scroll_page_down)
handle("pageup")(scroll_page_up)
return ConditionalKeyBindings(key_bindings, vi_mode)

View file

@ -0,0 +1,187 @@
"""
Key bindings, for scrolling up and down through pages.
This are separate bindings, because GNU readline doesn't have them, but
they are very useful for navigating through long multiline buffers, like in
Vi, Emacs, etc...
"""
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
__all__ = [
"scroll_forward",
"scroll_backward",
"scroll_half_page_up",
"scroll_half_page_down",
"scroll_one_line_up",
"scroll_one_line_down",
]
E = KeyPressEvent
def scroll_forward(event: E, half: bool = False) -> None:
"""
Scroll window down.
"""
w = event.app.layout.current_window
b = event.app.current_buffer
if w and w.render_info:
info = w.render_info
ui_content = info.ui_content
# Height to scroll.
scroll_height = info.window_height
if half:
scroll_height //= 2
# Calculate how many lines is equivalent to that vertical space.
y = b.document.cursor_position_row + 1
height = 0
while y < ui_content.line_count:
line_height = info.get_height_for_line(y)
if height + line_height < scroll_height:
height += line_height
y += 1
else:
break
b.cursor_position = b.document.translate_row_col_to_index(y, 0)
def scroll_backward(event: E, half: bool = False) -> None:
"""
Scroll window up.
"""
w = event.app.layout.current_window
b = event.app.current_buffer
if w and w.render_info:
info = w.render_info
# Height to scroll.
scroll_height = info.window_height
if half:
scroll_height //= 2
# Calculate how many lines is equivalent to that vertical space.
y = max(0, b.document.cursor_position_row - 1)
height = 0
while y > 0:
line_height = info.get_height_for_line(y)
if height + line_height < scroll_height:
height += line_height
y -= 1
else:
break
b.cursor_position = b.document.translate_row_col_to_index(y, 0)
def scroll_half_page_down(event: E) -> None:
"""
Same as ControlF, but only scroll half a page.
"""
scroll_forward(event, half=True)
def scroll_half_page_up(event: E) -> None:
"""
Same as ControlB, but only scroll half a page.
"""
scroll_backward(event, half=True)
def scroll_one_line_down(event: E) -> None:
"""
scroll_offset += 1
"""
w = event.app.layout.current_window
b = event.app.current_buffer
if w:
# When the cursor is at the top, move to the next line. (Otherwise, only scroll.)
if w.render_info:
info = w.render_info
if w.vertical_scroll < info.content_height - info.window_height:
if info.cursor_position.y <= info.configured_scroll_offsets.top:
b.cursor_position += b.document.get_cursor_down_position()
w.vertical_scroll += 1
def scroll_one_line_up(event: E) -> None:
"""
scroll_offset -= 1
"""
w = event.app.layout.current_window
b = event.app.current_buffer
if w:
# When the cursor is at the bottom, move to the previous line. (Otherwise, only scroll.)
if w.render_info:
info = w.render_info
if w.vertical_scroll > 0:
first_line_height = info.get_height_for_line(info.first_visible_line())
cursor_up = info.cursor_position.y - (
info.window_height
- 1
- first_line_height
- info.configured_scroll_offsets.bottom
)
# Move cursor up, as many steps as the height of the first line.
# TODO: not entirely correct yet, in case of line wrapping and many long lines.
for _ in range(max(0, cursor_up)):
b.cursor_position += b.document.get_cursor_up_position()
# Scroll window
w.vertical_scroll -= 1
def scroll_page_down(event: E) -> None:
"""
Scroll page down. (Prefer the cursor at the top of the page, after scrolling.)
"""
w = event.app.layout.current_window
b = event.app.current_buffer
if w and w.render_info:
# Scroll down one page.
line_index = max(w.render_info.last_visible_line(), w.vertical_scroll + 1)
w.vertical_scroll = line_index
b.cursor_position = b.document.translate_row_col_to_index(line_index, 0)
b.cursor_position += b.document.get_start_of_line_position(
after_whitespace=True
)
def scroll_page_up(event: E) -> None:
"""
Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.)
"""
w = event.app.layout.current_window
b = event.app.current_buffer
if w and w.render_info:
# Put cursor at the first visible line. (But make sure that the cursor
# moves at least one line up.)
line_index = max(
0,
min(w.render_info.first_visible_line(), b.document.cursor_position_row - 1),
)
b.cursor_position = b.document.translate_row_col_to_index(line_index, 0)
b.cursor_position += b.document.get_start_of_line_position(
after_whitespace=True
)
# Set the scroll offset. We can safely set it to zero; the Window will
# make sure that it scrolls at least until the cursor becomes visible.
w.vertical_scroll = 0

View file

@ -0,0 +1,93 @@
"""
Search related key bindings.
"""
from prompt_toolkit import search
from prompt_toolkit.application.current import get_app
from prompt_toolkit.filters import Condition, control_is_searchable, is_searching
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from ..key_bindings import key_binding
__all__ = [
"abort_search",
"accept_search",
"start_reverse_incremental_search",
"start_forward_incremental_search",
"reverse_incremental_search",
"forward_incremental_search",
"accept_search_and_accept_input",
]
E = KeyPressEvent
@key_binding(filter=is_searching)
def abort_search(event: E) -> None:
"""
Abort an incremental search and restore the original
line.
(Usually bound to ControlG/ControlC.)
"""
search.stop_search()
@key_binding(filter=is_searching)
def accept_search(event: E) -> None:
"""
When enter pressed in isearch, quit isearch mode. (Multiline
isearch would be too complicated.)
(Usually bound to Enter.)
"""
search.accept_search()
@key_binding(filter=control_is_searchable)
def start_reverse_incremental_search(event: E) -> None:
"""
Enter reverse incremental search.
(Usually ControlR.)
"""
search.start_search(direction=search.SearchDirection.BACKWARD)
@key_binding(filter=control_is_searchable)
def start_forward_incremental_search(event: E) -> None:
"""
Enter forward incremental search.
(Usually ControlS.)
"""
search.start_search(direction=search.SearchDirection.FORWARD)
@key_binding(filter=is_searching)
def reverse_incremental_search(event: E) -> None:
"""
Apply reverse incremental search, but keep search buffer focused.
"""
search.do_incremental_search(search.SearchDirection.BACKWARD, count=event.arg)
@key_binding(filter=is_searching)
def forward_incremental_search(event: E) -> None:
"""
Apply forward incremental search, but keep search buffer focused.
"""
search.do_incremental_search(search.SearchDirection.FORWARD, count=event.arg)
@Condition
def _previous_buffer_is_returnable() -> bool:
"""
True if the previously focused buffer has a return handler.
"""
prev_control = get_app().layout.search_target_buffer_control
return bool(prev_control and prev_control.buffer.is_returnable)
@key_binding(filter=is_searching & _previous_buffer_is_returnable)
def accept_search_and_accept_input(event: E) -> None:
"""
Accept the search operation first, then accept the input.
"""
search.accept_search()
event.current_buffer.validate_and_handle()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,60 @@
"""
Default key bindings.::
key_bindings = load_key_bindings()
app = Application(key_bindings=key_bindings)
"""
from prompt_toolkit.filters import buffer_has_focus
from prompt_toolkit.key_binding.bindings.basic import load_basic_bindings
from prompt_toolkit.key_binding.bindings.cpr import load_cpr_bindings
from prompt_toolkit.key_binding.bindings.emacs import (
load_emacs_bindings,
load_emacs_search_bindings,
load_emacs_shift_selection_bindings,
)
from prompt_toolkit.key_binding.bindings.mouse import load_mouse_bindings
from prompt_toolkit.key_binding.bindings.vi import (
load_vi_bindings,
load_vi_search_bindings,
)
from prompt_toolkit.key_binding.key_bindings import (
ConditionalKeyBindings,
KeyBindingsBase,
merge_key_bindings,
)
__all__ = [
"load_key_bindings",
]
def load_key_bindings() -> KeyBindingsBase:
"""
Create a KeyBindings object that contains the default key bindings.
"""
all_bindings = merge_key_bindings(
[
# Load basic bindings.
load_basic_bindings(),
# Load emacs bindings.
load_emacs_bindings(),
load_emacs_search_bindings(),
load_emacs_shift_selection_bindings(),
# Load Vi bindings.
load_vi_bindings(),
load_vi_search_bindings(),
]
)
return merge_key_bindings(
[
# Make sure that the above key bindings are only active if the
# currently focused control is a `BufferControl`. For other controls, we
# don't want these key bindings to intervene. (This would break "ptterm"
# for instance, which handles 'Keys.Any' in the user control itself.)
ConditionalKeyBindings(all_bindings, buffer_has_focus),
# Active, even when no buffer has been focused.
load_mouse_bindings(),
load_cpr_bindings(),
]
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,36 @@
from typing import List, Optional
from .key_processor import KeyPress
__all__ = [
"EmacsState",
]
class EmacsState:
"""
Mutable class to hold Emacs specific state.
"""
def __init__(self) -> None:
# Simple macro recording. (Like Readline does.)
# (For Emacs mode.)
self.macro: Optional[List[KeyPress]] = []
self.current_recording: Optional[List[KeyPress]] = None
def reset(self) -> None:
self.current_recording = None
@property
def is_recording(self) -> bool:
" Tell whether we are recording a macro. "
return self.current_recording is not None
def start_macro(self) -> None:
" Start recording macro. "
self.current_recording = []
def end_macro(self) -> None:
" End recording macro. "
self.macro = self.current_recording
self.current_recording = None

View file

@ -0,0 +1,635 @@
"""
Key bindings registry.
A `KeyBindings` object is a container that holds a list of key bindings. It has a
very efficient internal data structure for checking which key bindings apply
for a pressed key.
Typical usage::
kb = KeyBindings()
@kb.add(Keys.ControlX, Keys.ControlC, filter=INSERT)
def handler(event):
# Handle ControlX-ControlC key sequence.
pass
It is also possible to combine multiple KeyBindings objects. We do this in the
default key bindings. There are some KeyBindings objects that contain the Emacs
bindings, while others contain the Vi bindings. They are merged together using
`merge_key_bindings`.
We also have a `ConditionalKeyBindings` object that can enable/disable a group of
key bindings at once.
It is also possible to add a filter to a function, before a key binding has
been assigned, through the `key_binding` decorator.::
# First define a key handler with the `filter`.
@key_binding(filter=condition)
def my_key_binding(event):
...
# Later, add it to the key bindings.
kb.add(Keys.A, my_key_binding)
"""
from abc import ABCMeta, abstractmethod, abstractproperty
from typing import (
TYPE_CHECKING,
Callable,
Hashable,
List,
Optional,
Sequence,
Tuple,
TypeVar,
Union,
cast,
)
from prompt_toolkit.cache import SimpleCache
from prompt_toolkit.filters import FilterOrBool, Never, to_filter
from prompt_toolkit.keys import KEY_ALIASES, Keys
# Avoid circular imports.
if TYPE_CHECKING:
from .key_processor import KeyPressEvent
__all__ = [
"Binding",
"KeyBindingsBase",
"KeyBindings",
"ConditionalKeyBindings",
"merge_key_bindings",
"DynamicKeyBindings",
"GlobalOnlyKeyBindings",
]
KeyHandlerCallable = Callable[["KeyPressEvent"], None]
class Binding:
"""
Key binding: (key sequence + handler + filter).
(Immutable binding class.)
:param record_in_macro: When True, don't record this key binding when a
macro is recorded.
"""
def __init__(
self,
keys: Tuple[Union[Keys, str], ...],
handler: KeyHandlerCallable,
filter: FilterOrBool = True,
eager: FilterOrBool = False,
is_global: FilterOrBool = False,
save_before: Callable[["KeyPressEvent"], bool] = (lambda e: True),
record_in_macro: FilterOrBool = True,
) -> None:
self.keys = keys
self.handler = handler
self.filter = to_filter(filter)
self.eager = to_filter(eager)
self.is_global = to_filter(is_global)
self.save_before = save_before
self.record_in_macro = to_filter(record_in_macro)
def call(self, event: "KeyPressEvent") -> None:
self.handler(event)
def __repr__(self) -> str:
return "%s(keys=%r, handler=%r)" % (
self.__class__.__name__,
self.keys,
self.handler,
)
# Sequence of keys presses.
KeysTuple = Tuple[Union[Keys, str], ...]
class KeyBindingsBase(metaclass=ABCMeta):
"""
Interface for a KeyBindings.
"""
@abstractproperty
def _version(self) -> Hashable:
"""
For cache invalidation. - This should increase every time that
something changes.
"""
return 0
@abstractmethod
def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]:
"""
Return a list of key bindings that can handle these keys.
(This return also inactive bindings, so the `filter` still has to be
called, for checking it.)
:param keys: tuple of keys.
"""
return []
@abstractmethod
def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]:
"""
Return a list of key bindings that handle a key sequence starting with
`keys`. (It does only return bindings for which the sequences are
longer than `keys`. And like `get_bindings_for_keys`, it also includes
inactive bindings.)
:param keys: tuple of keys.
"""
return []
@abstractproperty
def bindings(self) -> List[Binding]:
"""
List of `Binding` objects.
(These need to be exposed, so that `KeyBindings` objects can be merged
together.)
"""
return []
# `add` and `remove` don't have to be part of this interface.
T = TypeVar("T", bound=Union[KeyHandlerCallable, Binding])
class KeyBindings(KeyBindingsBase):
"""
A container for a set of key bindings.
Example usage::
kb = KeyBindings()
@kb.add('c-t')
def _(event):
print('Control-T pressed')
@kb.add('c-a', 'c-b')
def _(event):
print('Control-A pressed, followed by Control-B')
@kb.add('c-x', filter=is_searching)
def _(event):
print('Control-X pressed') # Works only if we are searching.
"""
def __init__(self) -> None:
self._bindings: List[Binding] = []
self._get_bindings_for_keys_cache: SimpleCache[
KeysTuple, List[Binding]
] = SimpleCache(maxsize=10000)
self._get_bindings_starting_with_keys_cache: SimpleCache[
KeysTuple, List[Binding]
] = SimpleCache(maxsize=1000)
self.__version = 0 # For cache invalidation.
def _clear_cache(self) -> None:
self.__version += 1
self._get_bindings_for_keys_cache.clear()
self._get_bindings_starting_with_keys_cache.clear()
@property
def bindings(self) -> List[Binding]:
return self._bindings
@property
def _version(self) -> Hashable:
return self.__version
def add(
self,
*keys: Union[Keys, str],
filter: FilterOrBool = True,
eager: FilterOrBool = False,
is_global: FilterOrBool = False,
save_before: Callable[["KeyPressEvent"], bool] = (lambda e: True),
record_in_macro: FilterOrBool = True,
) -> Callable[[T], T]:
"""
Decorator for adding a key bindings.
:param filter: :class:`~prompt_toolkit.filters.Filter` to determine
when this key binding is active.
:param eager: :class:`~prompt_toolkit.filters.Filter` or `bool`.
When True, ignore potential longer matches when this key binding is
hit. E.g. when there is an active eager key binding for Ctrl-X,
execute the handler immediately and ignore the key binding for
Ctrl-X Ctrl-E of which it is a prefix.
:param is_global: When this key bindings is added to a `Container` or
`Control`, make it a global (always active) binding.
:param save_before: Callable that takes an `Event` and returns True if
we should save the current buffer, before handling the event.
(That's the default.)
:param record_in_macro: Record these key bindings when a macro is
being recorded. (True by default.)
"""
assert keys
keys = tuple(_parse_key(k) for k in keys)
if isinstance(filter, Never):
# When a filter is Never, it will always stay disabled, so in that
# case don't bother putting it in the key bindings. It will slow
# down every key press otherwise.
def decorator(func: T) -> T:
return func
else:
def decorator(func: T) -> T:
if isinstance(func, Binding):
# We're adding an existing Binding object.
self.bindings.append(
Binding(
keys,
func.handler,
filter=func.filter & to_filter(filter),
eager=to_filter(eager) | func.eager,
is_global=to_filter(is_global) | func.is_global,
save_before=func.save_before,
record_in_macro=func.record_in_macro,
)
)
else:
self.bindings.append(
Binding(
keys,
cast(KeyHandlerCallable, func),
filter=filter,
eager=eager,
is_global=is_global,
save_before=save_before,
record_in_macro=record_in_macro,
)
)
self._clear_cache()
return func
return decorator
def remove(self, *args: Union[Keys, str, KeyHandlerCallable]) -> None:
"""
Remove a key binding.
This expects either a function that was given to `add` method as
parameter or a sequence of key bindings.
Raises `ValueError` when no bindings was found.
Usage::
remove(handler) # Pass handler.
remove('c-x', 'c-a') # Or pass the key bindings.
"""
found = False
if callable(args[0]):
assert len(args) == 1
function = args[0]
# Remove the given function.
for b in self.bindings:
if b.handler == function:
self.bindings.remove(b)
found = True
else:
assert len(args) > 0
args = cast(Tuple[Union[Keys, str]], args)
# Remove this sequence of key bindings.
keys = tuple(_parse_key(k) for k in args)
for b in self.bindings:
if b.keys == keys:
self.bindings.remove(b)
found = True
if found:
self._clear_cache()
else:
# No key binding found for this function. Raise ValueError.
raise ValueError("Binding not found: %r" % (function,))
# For backwards-compatibility.
add_binding = add
remove_binding = remove
def get_bindings_for_keys(self, keys: KeysTuple) -> List[Binding]:
"""
Return a list of key bindings that can handle this key.
(This return also inactive bindings, so the `filter` still has to be
called, for checking it.)
:param keys: tuple of keys.
"""
def get() -> List[Binding]:
result: List[Tuple[int, Binding]] = []
for b in self.bindings:
if len(keys) == len(b.keys):
match = True
any_count = 0
for i, j in zip(b.keys, keys):
if i != j and i != Keys.Any:
match = False
break
if i == Keys.Any:
any_count += 1
if match:
result.append((any_count, b))
# Place bindings that have more 'Any' occurrences in them at the end.
result = sorted(result, key=lambda item: -item[0])
return [item[1] for item in result]
return self._get_bindings_for_keys_cache.get(keys, get)
def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]:
"""
Return a list of key bindings that handle a key sequence starting with
`keys`. (It does only return bindings for which the sequences are
longer than `keys`. And like `get_bindings_for_keys`, it also includes
inactive bindings.)
:param keys: tuple of keys.
"""
def get() -> List[Binding]:
result = []
for b in self.bindings:
if len(keys) < len(b.keys):
match = True
for i, j in zip(b.keys, keys):
if i != j and i != Keys.Any:
match = False
break
if match:
result.append(b)
return result
return self._get_bindings_starting_with_keys_cache.get(keys, get)
def _parse_key(key: Union[Keys, str]) -> Union[str, Keys]:
"""
Replace key by alias and verify whether it's a valid one.
"""
# Already a parse key? -> Return it.
if isinstance(key, Keys):
return key
# Lookup aliases.
key = KEY_ALIASES.get(key, key)
# Replace 'space' by ' '
if key == "space":
key = " "
# Return as `Key` object when it's a special key.
try:
return Keys(key)
except ValueError:
pass
# Final validation.
if len(key) != 1:
raise ValueError("Invalid key: %s" % (key,))
return key
def key_binding(
filter: FilterOrBool = True,
eager: FilterOrBool = False,
is_global: FilterOrBool = False,
save_before: Callable[["KeyPressEvent"], bool] = (lambda event: True),
record_in_macro: FilterOrBool = True,
) -> Callable[[KeyHandlerCallable], Binding]:
"""
Decorator that turn a function into a `Binding` object. This can be added
to a `KeyBindings` object when a key binding is assigned.
"""
assert save_before is None or callable(save_before)
filter = to_filter(filter)
eager = to_filter(eager)
is_global = to_filter(is_global)
save_before = save_before
record_in_macro = to_filter(record_in_macro)
keys = ()
def decorator(function: KeyHandlerCallable) -> Binding:
return Binding(
keys,
function,
filter=filter,
eager=eager,
is_global=is_global,
save_before=save_before,
record_in_macro=record_in_macro,
)
return decorator
class _Proxy(KeyBindingsBase):
"""
Common part for ConditionalKeyBindings and _MergedKeyBindings.
"""
def __init__(self) -> None:
# `KeyBindings` to be synchronized with all the others.
self._bindings2: KeyBindingsBase = KeyBindings()
self._last_version: Hashable = ()
def _update_cache(self) -> None:
"""
If `self._last_version` is outdated, then this should update
the version and `self._bindings2`.
"""
raise NotImplementedError
# Proxy methods to self._bindings2.
@property
def bindings(self) -> List[Binding]:
self._update_cache()
return self._bindings2.bindings
@property
def _version(self) -> Hashable:
self._update_cache()
return self._last_version
def get_bindings_for_keys(self, *a, **kw):
self._update_cache()
return self._bindings2.get_bindings_for_keys(*a, **kw)
def get_bindings_starting_with_keys(self, *a, **kw):
self._update_cache()
return self._bindings2.get_bindings_starting_with_keys(*a, **kw)
class ConditionalKeyBindings(_Proxy):
"""
Wraps around a `KeyBindings`. Disable/enable all the key bindings according to
the given (additional) filter.::
@Condition
def setting_is_true():
return True # or False
registry = ConditionalKeyBindings(key_bindings, setting_is_true)
When new key bindings are added to this object. They are also
enable/disabled according to the given `filter`.
:param registries: List of :class:`.KeyBindings` objects.
:param filter: :class:`~prompt_toolkit.filters.Filter` object.
"""
def __init__(
self, key_bindings: KeyBindingsBase, filter: FilterOrBool = True
) -> None:
_Proxy.__init__(self)
self.key_bindings = key_bindings
self.filter = to_filter(filter)
def _update_cache(self) -> None:
" If the original key bindings was changed. Update our copy version. "
expected_version = self.key_bindings._version
if self._last_version != expected_version:
bindings2 = KeyBindings()
# Copy all bindings from `self.key_bindings`, adding our condition.
for b in self.key_bindings.bindings:
bindings2.bindings.append(
Binding(
keys=b.keys,
handler=b.handler,
filter=self.filter & b.filter,
eager=b.eager,
is_global=b.is_global,
save_before=b.save_before,
record_in_macro=b.record_in_macro,
)
)
self._bindings2 = bindings2
self._last_version = expected_version
class _MergedKeyBindings(_Proxy):
"""
Merge multiple registries of key bindings into one.
This class acts as a proxy to multiple :class:`.KeyBindings` objects, but
behaves as if this is just one bigger :class:`.KeyBindings`.
:param registries: List of :class:`.KeyBindings` objects.
"""
def __init__(self, registries: Sequence[KeyBindingsBase]) -> None:
_Proxy.__init__(self)
self.registries = registries
def _update_cache(self) -> None:
"""
If one of the original registries was changed. Update our merged
version.
"""
expected_version = tuple(r._version for r in self.registries)
if self._last_version != expected_version:
bindings2 = KeyBindings()
for reg in self.registries:
bindings2.bindings.extend(reg.bindings)
self._bindings2 = bindings2
self._last_version = expected_version
def merge_key_bindings(bindings: Sequence[KeyBindingsBase]) -> _MergedKeyBindings:
"""
Merge multiple :class:`.Keybinding` objects together.
Usage::
bindings = merge_key_bindings([bindings1, bindings2, ...])
"""
return _MergedKeyBindings(bindings)
class DynamicKeyBindings(_Proxy):
"""
KeyBindings class that can dynamically returns any KeyBindings.
:param get_key_bindings: Callable that returns a :class:`.KeyBindings` instance.
"""
def __init__(
self, get_key_bindings: Callable[[], Optional[KeyBindingsBase]]
) -> None:
self.get_key_bindings = get_key_bindings
self.__version = 0
self._last_child_version = None
self._dummy = KeyBindings() # Empty key bindings.
def _update_cache(self) -> None:
key_bindings = self.get_key_bindings() or self._dummy
assert isinstance(key_bindings, KeyBindingsBase)
version = id(key_bindings), key_bindings._version
self._bindings2 = key_bindings
self._last_version = version
class GlobalOnlyKeyBindings(_Proxy):
"""
Wrapper around a :class:`.KeyBindings` object that only exposes the global
key bindings.
"""
def __init__(self, key_bindings: KeyBindingsBase) -> None:
_Proxy.__init__(self)
self.key_bindings = key_bindings
def _update_cache(self) -> None:
"""
If one of the original registries was changed. Update our merged
version.
"""
expected_version = self.key_bindings._version
if self._last_version != expected_version:
bindings2 = KeyBindings()
for b in self.key_bindings.bindings:
if b.is_global():
bindings2.bindings.append(b)
self._bindings2 = bindings2
self._last_version = expected_version

View file

@ -0,0 +1,530 @@
# *** encoding: utf-8 ***
"""
An :class:`~.KeyProcessor` receives callbacks for the keystrokes parsed from
the input in the :class:`~prompt_toolkit.inputstream.InputStream` instance.
The `KeyProcessor` will according to the implemented keybindings call the
correct callbacks when new key presses are feed through `feed`.
"""
import weakref
from asyncio import Task, sleep
from collections import deque
from typing import TYPE_CHECKING, Any, Deque, Generator, List, Optional, Union
from prompt_toolkit.application.current import get_app
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.filters.app import vi_navigation_mode
from prompt_toolkit.keys import Keys
from prompt_toolkit.utils import Event
from .key_bindings import Binding, KeyBindingsBase
if TYPE_CHECKING:
from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
__all__ = [
"KeyProcessor",
"KeyPress",
"KeyPressEvent",
]
class KeyPress:
"""
:param key: A `Keys` instance or text (one character).
:param data: The received string on stdin. (Often vt100 escape codes.)
"""
def __init__(self, key: Union[Keys, str], data: Optional[str] = None) -> None:
assert isinstance(key, Keys) or len(key) == 1
if data is None:
if isinstance(key, Keys):
data = key.value
else:
data = key # 'key' is a one character string.
self.key = key
self.data = data
def __repr__(self) -> str:
return "%s(key=%r, data=%r)" % (self.__class__.__name__, self.key, self.data)
def __eq__(self, other: object) -> bool:
if not isinstance(other, KeyPress):
return False
return self.key == other.key and self.data == other.data
"""
Helper object to indicate flush operation in the KeyProcessor.
NOTE: the implementation is very similar to the VT100 parser.
"""
_Flush = KeyPress("?", data="_Flush")
class KeyProcessor:
"""
Statemachine that receives :class:`KeyPress` instances and according to the
key bindings in the given :class:`KeyBindings`, calls the matching handlers.
::
p = KeyProcessor(key_bindings)
# Send keys into the processor.
p.feed(KeyPress(Keys.ControlX, '\x18'))
p.feed(KeyPress(Keys.ControlC, '\x03')
# Process all the keys in the queue.
p.process_keys()
# Now the ControlX-ControlC callback will be called if this sequence is
# registered in the key bindings.
:param key_bindings: `KeyBindingsBase` instance.
"""
def __init__(self, key_bindings: KeyBindingsBase) -> None:
self._bindings = key_bindings
self.before_key_press = Event(self)
self.after_key_press = Event(self)
self._flush_wait_task: Optional[Task] = None
self.reset()
def reset(self) -> None:
self._previous_key_sequence: List[KeyPress] = []
self._previous_handler: Optional[Binding] = None
# The queue of keys not yet send to our _process generator/state machine.
self.input_queue: Deque[KeyPress] = deque()
# The key buffer that is matched in the generator state machine.
# (This is at at most the amount of keys that make up for one key binding.)
self.key_buffer: List[KeyPress] = []
#: Readline argument (for repetition of commands.)
#: https://www.gnu.org/software/bash/manual/html_node/Readline-Arguments.html
self.arg: Optional[str] = None
# Start the processor coroutine.
self._process_coroutine = self._process()
self._process_coroutine.send(None) # type: ignore
def _get_matches(self, key_presses: List[KeyPress]) -> List[Binding]:
"""
For a list of :class:`KeyPress` instances. Give the matching handlers
that would handle this.
"""
keys = tuple(k.key for k in key_presses)
# Try match, with mode flag
return [b for b in self._bindings.get_bindings_for_keys(keys) if b.filter()]
def _is_prefix_of_longer_match(self, key_presses: List[KeyPress]) -> bool:
"""
For a list of :class:`KeyPress` instances. Return True if there is any
handler that is bound to a suffix of this keys.
"""
keys = tuple(k.key for k in key_presses)
# Get the filters for all the key bindings that have a longer match.
# Note that we transform it into a `set`, because we don't care about
# the actual bindings and executing it more than once doesn't make
# sense. (Many key bindings share the same filter.)
filters = set(
b.filter for b in self._bindings.get_bindings_starting_with_keys(keys)
)
# When any key binding is active, return True.
return any(f() for f in filters)
def _process(self) -> Generator[None, KeyPress, None]:
"""
Coroutine implementing the key match algorithm. Key strokes are sent
into this generator, and it calls the appropriate handlers.
"""
buffer = self.key_buffer
retry = False
while True:
flush = False
if retry:
retry = False
else:
key = yield
if key is _Flush:
flush = True
else:
buffer.append(key)
# If we have some key presses, check for matches.
if buffer:
matches = self._get_matches(buffer)
if flush:
is_prefix_of_longer_match = False
else:
is_prefix_of_longer_match = self._is_prefix_of_longer_match(buffer)
# When eager matches were found, give priority to them and also
# ignore all the longer matches.
eager_matches = [m for m in matches if m.eager()]
if eager_matches:
matches = eager_matches
is_prefix_of_longer_match = False
# Exact matches found, call handler.
if not is_prefix_of_longer_match and matches:
self._call_handler(matches[-1], key_sequence=buffer[:])
del buffer[:] # Keep reference.
# No match found.
elif not is_prefix_of_longer_match and not matches:
retry = True
found = False
# Loop over the input, try longest match first and shift.
for i in range(len(buffer), 0, -1):
matches = self._get_matches(buffer[:i])
if matches:
self._call_handler(matches[-1], key_sequence=buffer[:i])
del buffer[:i]
found = True
break
if not found:
del buffer[:1]
def feed(self, key_press: KeyPress, first: bool = False) -> None:
"""
Add a new :class:`KeyPress` to the input queue.
(Don't forget to call `process_keys` in order to process the queue.)
:param first: If true, insert before everything else.
"""
if first:
self.input_queue.appendleft(key_press)
else:
self.input_queue.append(key_press)
def feed_multiple(self, key_presses: List[KeyPress], first: bool = False) -> None:
"""
:param first: If true, insert before everything else.
"""
if first:
self.input_queue.extendleft(reversed(key_presses))
else:
self.input_queue.extend(key_presses)
def process_keys(self) -> None:
"""
Process all the keys in the `input_queue`.
(To be called after `feed`.)
Note: because of the `feed`/`process_keys` separation, it is
possible to call `feed` from inside a key binding.
This function keeps looping until the queue is empty.
"""
app = get_app()
def not_empty() -> bool:
# When the application result is set, stop processing keys. (E.g.
# if ENTER was received, followed by a few additional key strokes,
# leave the other keys in the queue.)
if app.is_done:
# But if there are still CPRResponse keys in the queue, these
# need to be processed.
return any(k for k in self.input_queue if k.key == Keys.CPRResponse)
else:
return bool(self.input_queue)
def get_next() -> KeyPress:
if app.is_done:
# Only process CPR responses. Everything else is typeahead.
cpr = [k for k in self.input_queue if k.key == Keys.CPRResponse][0]
self.input_queue.remove(cpr)
return cpr
else:
return self.input_queue.popleft()
keys_processed = False
is_flush = False
while not_empty():
keys_processed = True
# Process next key.
key_press = get_next()
is_flush = key_press is _Flush
is_cpr = key_press.key == Keys.CPRResponse
if not is_flush and not is_cpr:
self.before_key_press.fire()
try:
self._process_coroutine.send(key_press)
except Exception:
# If for some reason something goes wrong in the parser, (maybe
# an exception was raised) restart the processor for next time.
self.reset()
self.empty_queue()
app.invalidate()
raise
if not is_flush and not is_cpr:
self.after_key_press.fire()
if keys_processed:
# Invalidate user interface.
app.invalidate()
# Skip timeout if the last key was flush.
if not is_flush:
self._start_timeout()
def empty_queue(self) -> List[KeyPress]:
"""
Empty the input queue. Return the unprocessed input.
"""
key_presses = list(self.input_queue)
self.input_queue.clear()
# Filter out CPRs. We don't want to return these.
key_presses = [k for k in key_presses if k.key != Keys.CPRResponse]
return key_presses
def _call_handler(self, handler: Binding, key_sequence: List[KeyPress]) -> None:
app = get_app()
was_recording_emacs = app.emacs_state.is_recording
was_recording_vi = bool(app.vi_state.recording_register)
was_temporary_navigation_mode = app.vi_state.temporary_navigation_mode
arg = self.arg
self.arg = None
event = KeyPressEvent(
weakref.ref(self),
arg=arg,
key_sequence=key_sequence,
previous_key_sequence=self._previous_key_sequence,
is_repeat=(handler == self._previous_handler),
)
# Save the state of the current buffer.
if handler.save_before(event):
event.app.current_buffer.save_to_undo_stack()
# Call handler.
from prompt_toolkit.buffer import EditReadOnlyBuffer
try:
handler.call(event)
self._fix_vi_cursor_position(event)
except EditReadOnlyBuffer:
# When a key binding does an attempt to change a buffer which is
# read-only, we can ignore that. We sound a bell and go on.
app.output.bell()
if was_temporary_navigation_mode:
self._leave_vi_temp_navigation_mode(event)
self._previous_key_sequence = key_sequence
self._previous_handler = handler
# Record the key sequence in our macro. (Only if we're in macro mode
# before and after executing the key.)
if handler.record_in_macro():
if app.emacs_state.is_recording and was_recording_emacs:
recording = app.emacs_state.current_recording
if recording is not None: # Should always be true, given that
# `was_recording_emacs` is set.
recording.extend(key_sequence)
if app.vi_state.recording_register and was_recording_vi:
for k in key_sequence:
app.vi_state.current_recording += k.data
def _fix_vi_cursor_position(self, event: "KeyPressEvent") -> None:
"""
After every command, make sure that if we are in Vi navigation mode, we
never put the cursor after the last character of a line. (Unless it's
an empty line.)
"""
app = event.app
buff = app.current_buffer
preferred_column = buff.preferred_column
if (
vi_navigation_mode()
and buff.document.is_cursor_at_the_end_of_line
and len(buff.document.current_line) > 0
):
buff.cursor_position -= 1
# Set the preferred_column for arrow up/down again.
# (This was cleared after changing the cursor position.)
buff.preferred_column = preferred_column
def _leave_vi_temp_navigation_mode(self, event: "KeyPressEvent") -> None:
"""
If we're in Vi temporary navigation (normal) mode, return to
insert/replace mode after executing one action.
"""
app = event.app
if app.editing_mode == EditingMode.VI:
# Not waiting for a text object and no argument has been given.
if app.vi_state.operator_func is None and self.arg is None:
app.vi_state.temporary_navigation_mode = False
def _start_timeout(self) -> None:
"""
Start auto flush timeout. Similar to Vim's `timeoutlen` option.
Start a background coroutine with a timer. When this timeout expires
and no key was pressed in the meantime, we flush all data in the queue
and call the appropriate key binding handlers.
"""
app = get_app()
timeout = app.timeoutlen
if timeout is None:
return
async def wait() -> None:
" Wait for timeout. "
# This sleep can be cancelled. In that case we don't flush.
await sleep(timeout)
if len(self.key_buffer) > 0:
# (No keys pressed in the meantime.)
flush_keys()
def flush_keys() -> None:
" Flush keys. "
self.feed(_Flush)
self.process_keys()
# Automatically flush keys.
if self._flush_wait_task:
self._flush_wait_task.cancel()
self._flush_wait_task = app.create_background_task(wait())
class KeyPressEvent:
"""
Key press event, delivered to key bindings.
:param key_processor_ref: Weak reference to the `KeyProcessor`.
:param arg: Repetition argument.
:param key_sequence: List of `KeyPress` instances.
:param previouskey_sequence: Previous list of `KeyPress` instances.
:param is_repeat: True when the previous event was delivered to the same handler.
"""
def __init__(
self,
key_processor_ref: "weakref.ReferenceType[KeyProcessor]",
arg: Optional[str],
key_sequence: List[KeyPress],
previous_key_sequence: List[KeyPress],
is_repeat: bool,
) -> None:
self._key_processor_ref = key_processor_ref
self.key_sequence = key_sequence
self.previous_key_sequence = previous_key_sequence
#: True when the previous key sequence was handled by the same handler.
self.is_repeat = is_repeat
self._arg = arg
self._app = get_app()
def __repr__(self) -> str:
return "KeyPressEvent(arg=%r, key_sequence=%r, is_repeat=%r)" % (
self.arg,
self.key_sequence,
self.is_repeat,
)
@property
def data(self) -> str:
return self.key_sequence[-1].data
@property
def key_processor(self) -> KeyProcessor:
processor = self._key_processor_ref()
if processor is None:
raise Exception("KeyProcessor was lost. This should not happen.")
return processor
@property
def app(self) -> "Application[Any]":
"""
The current `Application` object.
"""
return self._app
@property
def current_buffer(self) -> "Buffer":
"""
The current buffer.
"""
return self.app.current_buffer
@property
def arg(self) -> int:
"""
Repetition argument.
"""
if self._arg == "-":
return -1
result = int(self._arg or 1)
# Don't exceed a million.
if int(result) >= 1000000:
result = 1
return result
@property
def arg_present(self) -> bool:
"""
True if repetition argument was explicitly provided.
"""
return self._arg is not None
def append_to_arg_count(self, data: str) -> None:
"""
Add digit to the input argument.
:param data: the typed digit as string
"""
assert data in "-0123456789"
current = self._arg
if data == "-":
assert current is None or current == "-"
result = data
elif current is None:
result = data
else:
result = "%s%s" % (current, data)
self.key_processor.arg = result
@property
def cli(self) -> "Application":
" For backward-compatibility. "
return self.app

View file

@ -0,0 +1,107 @@
from enum import Enum
from typing import TYPE_CHECKING, Callable, Dict, Optional
from prompt_toolkit.clipboard import ClipboardData
if TYPE_CHECKING:
from .key_bindings.vi import TextObject
from .key_processor import KeyPressEvent
__all__ = [
"InputMode",
"CharacterFind",
"ViState",
]
class InputMode(str, Enum):
value: str
INSERT = "vi-insert"
INSERT_MULTIPLE = "vi-insert-multiple"
NAVIGATION = "vi-navigation" # Normal mode.
REPLACE = "vi-replace"
REPLACE_SINGLE = "vi-replace-single"
class CharacterFind:
def __init__(self, character: str, backwards: bool = False) -> None:
self.character = character
self.backwards = backwards
class ViState:
"""
Mutable class to hold the state of the Vi navigation.
"""
def __init__(self) -> None:
#: None or CharacterFind instance. (This is used to repeat the last
#: search in Vi mode, by pressing the 'n' or 'N' in navigation mode.)
self.last_character_find: Optional[CharacterFind] = None
# When an operator is given and we are waiting for text object,
# -- e.g. in the case of 'dw', after the 'd' --, an operator callback
# is set here.
self.operator_func: Optional[
Callable[["KeyPressEvent", "TextObject"], None]
] = None
self.operator_arg: Optional[int] = None
#: Named registers. Maps register name (e.g. 'a') to
#: :class:`ClipboardData` instances.
self.named_registers: Dict[str, ClipboardData] = {}
#: The Vi mode we're currently in to.
self.__input_mode = InputMode.INSERT
#: Waiting for digraph.
self.waiting_for_digraph = False
self.digraph_symbol1: Optional[str] = None # (None or a symbol.)
#: When true, make ~ act as an operator.
self.tilde_operator = False
#: Register in which we are recording a macro.
#: `None` when not recording anything.
# Note that the recording is only stored in the register after the
# recording is stopped. So we record in a separate `current_recording`
# variable.
self.recording_register: Optional[str] = None
self.current_recording: str = ""
# Temporary navigation (normal) mode.
# This happens when control-o has been pressed in insert or replace
# mode. The user can now do one navigation action and we'll return back
# to insert/replace.
self.temporary_navigation_mode = False
@property
def input_mode(self) -> InputMode:
" Get `InputMode`. "
return self.__input_mode
@input_mode.setter
def input_mode(self, value: InputMode) -> None:
" Set `InputMode`. "
if value == InputMode.NAVIGATION:
self.waiting_for_digraph = False
self.operator_func = None
self.operator_arg = None
self.__input_mode = value
def reset(self) -> None:
"""
Reset state, go back to the given mode. INSERT by default.
"""
# Go back to insert mode.
self.input_mode = InputMode.INSERT
self.waiting_for_digraph = False
self.operator_func = None
self.operator_arg = None
# Reset recording state.
self.recording_register = None
self.current_recording = ""