Uploaded Test files
This commit is contained in:
parent
f584ad9d97
commit
2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions
|
@ -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",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
2221
venv/Lib/site-packages/prompt_toolkit/key_binding/bindings/vi.py
Normal file
2221
venv/Lib/site-packages/prompt_toolkit/key_binding/bindings/vi.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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(),
|
||||
]
|
||||
)
|
1378
venv/Lib/site-packages/prompt_toolkit/key_binding/digraphs.py
Normal file
1378
venv/Lib/site-packages/prompt_toolkit/key_binding/digraphs.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
|
@ -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
|
|
@ -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
|
107
venv/Lib/site-packages/prompt_toolkit/key_binding/vi_state.py
Normal file
107
venv/Lib/site-packages/prompt_toolkit/key_binding/vi_state.py
Normal 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 = ""
|
Loading…
Add table
Add a link
Reference in a new issue