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,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