mirror of
https://github.com/aykhans/AzSuicideDataVisualization.git
synced 2025-07-01 22:13:01 +00:00
first commit
This commit is contained in:
@ -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",
|
||||
]
|
@ -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,253 @@
|
||||
# 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("<sigint>")
|
||||
@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[object]", 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(
|
||||
f"Display all {len(completions)} possibilities?",
|
||||
).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[bool]":
|
||||
"""
|
||||
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,557 @@
|
||||
# pylint: disable=function-redefined
|
||||
from typing import Dict, Union
|
||||
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.buffer import Buffer, 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 prompt_toolkit.selection import SelectionType
|
||||
|
||||
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,347 @@
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, FrozenSet
|
||||
|
||||
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 (
|
||||
MouseButton,
|
||||
MouseEvent,
|
||||
MouseEventType,
|
||||
MouseModifier,
|
||||
)
|
||||
|
||||
from ..key_bindings import KeyBindings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
|
||||
|
||||
__all__ = [
|
||||
"load_mouse_bindings",
|
||||
]
|
||||
|
||||
E = KeyPressEvent
|
||||
|
||||
# fmt: off
|
||||
# flake8: noqa E201
|
||||
SCROLL_UP = MouseEventType.SCROLL_UP
|
||||
SCROLL_DOWN = MouseEventType.SCROLL_DOWN
|
||||
MOUSE_DOWN = MouseEventType.MOUSE_DOWN
|
||||
MOUSE_MOVE = MouseEventType.MOUSE_MOVE
|
||||
MOUSE_UP = MouseEventType.MOUSE_UP
|
||||
|
||||
NO_MODIFIER : FrozenSet[MouseModifier] = frozenset()
|
||||
SHIFT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT})
|
||||
ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT})
|
||||
SHIFT_ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT})
|
||||
CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.CONTROL})
|
||||
SHIFT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.CONTROL})
|
||||
ALT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT, MouseModifier.CONTROL})
|
||||
SHIFT_ALT_CONTROL: FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT, MouseModifier.CONTROL})
|
||||
UNKNOWN_MODIFIER : FrozenSet[MouseModifier] = frozenset()
|
||||
|
||||
LEFT = MouseButton.LEFT
|
||||
MIDDLE = MouseButton.MIDDLE
|
||||
RIGHT = MouseButton.RIGHT
|
||||
NO_BUTTON = MouseButton.NONE
|
||||
UNKNOWN_BUTTON = MouseButton.UNKNOWN
|
||||
|
||||
xterm_sgr_mouse_events = {
|
||||
( 0, 'm') : (LEFT, MOUSE_UP, NO_MODIFIER), # left_up 0+ + + =0
|
||||
( 4, 'm') : (LEFT, MOUSE_UP, SHIFT), # left_up Shift 0+4+ + =4
|
||||
( 8, 'm') : (LEFT, MOUSE_UP, ALT), # left_up Alt 0+ +8+ =8
|
||||
(12, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT), # left_up Shift Alt 0+4+8+ =12
|
||||
(16, 'm') : (LEFT, MOUSE_UP, CONTROL), # left_up Control 0+ + +16=16
|
||||
(20, 'm') : (LEFT, MOUSE_UP, SHIFT_CONTROL), # left_up Shift Control 0+4+ +16=20
|
||||
(24, 'm') : (LEFT, MOUSE_UP, ALT_CONTROL), # left_up Alt Control 0+ +8+16=24
|
||||
(28, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT_CONTROL), # left_up Shift Alt Control 0+4+8+16=28
|
||||
|
||||
( 1, 'm') : (MIDDLE, MOUSE_UP, NO_MODIFIER), # middle_up 1+ + + =1
|
||||
( 5, 'm') : (MIDDLE, MOUSE_UP, SHIFT), # middle_up Shift 1+4+ + =5
|
||||
( 9, 'm') : (MIDDLE, MOUSE_UP, ALT), # middle_up Alt 1+ +8+ =9
|
||||
(13, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT), # middle_up Shift Alt 1+4+8+ =13
|
||||
(17, 'm') : (MIDDLE, MOUSE_UP, CONTROL), # middle_up Control 1+ + +16=17
|
||||
(21, 'm') : (MIDDLE, MOUSE_UP, SHIFT_CONTROL), # middle_up Shift Control 1+4+ +16=21
|
||||
(25, 'm') : (MIDDLE, MOUSE_UP, ALT_CONTROL), # middle_up Alt Control 1+ +8+16=25
|
||||
(29, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT_CONTROL), # middle_up Shift Alt Control 1+4+8+16=29
|
||||
|
||||
( 2, 'm') : (RIGHT, MOUSE_UP, NO_MODIFIER), # right_up 2+ + + =2
|
||||
( 6, 'm') : (RIGHT, MOUSE_UP, SHIFT), # right_up Shift 2+4+ + =6
|
||||
(10, 'm') : (RIGHT, MOUSE_UP, ALT), # right_up Alt 2+ +8+ =10
|
||||
(14, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT), # right_up Shift Alt 2+4+8+ =14
|
||||
(18, 'm') : (RIGHT, MOUSE_UP, CONTROL), # right_up Control 2+ + +16=18
|
||||
(22, 'm') : (RIGHT, MOUSE_UP, SHIFT_CONTROL), # right_up Shift Control 2+4+ +16=22
|
||||
(26, 'm') : (RIGHT, MOUSE_UP, ALT_CONTROL), # right_up Alt Control 2+ +8+16=26
|
||||
(30, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT_CONTROL), # right_up Shift Alt Control 2+4+8+16=30
|
||||
|
||||
( 0, 'M') : (LEFT, MOUSE_DOWN, NO_MODIFIER), # left_down 0+ + + =0
|
||||
( 4, 'M') : (LEFT, MOUSE_DOWN, SHIFT), # left_down Shift 0+4+ + =4
|
||||
( 8, 'M') : (LEFT, MOUSE_DOWN, ALT), # left_down Alt 0+ +8+ =8
|
||||
(12, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT), # left_down Shift Alt 0+4+8+ =12
|
||||
(16, 'M') : (LEFT, MOUSE_DOWN, CONTROL), # left_down Control 0+ + +16=16
|
||||
(20, 'M') : (LEFT, MOUSE_DOWN, SHIFT_CONTROL), # left_down Shift Control 0+4+ +16=20
|
||||
(24, 'M') : (LEFT, MOUSE_DOWN, ALT_CONTROL), # left_down Alt Control 0+ +8+16=24
|
||||
(28, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # left_down Shift Alt Control 0+4+8+16=28
|
||||
|
||||
( 1, 'M') : (MIDDLE, MOUSE_DOWN, NO_MODIFIER), # middle_down 1+ + + =1
|
||||
( 5, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT), # middle_down Shift 1+4+ + =5
|
||||
( 9, 'M') : (MIDDLE, MOUSE_DOWN, ALT), # middle_down Alt 1+ +8+ =9
|
||||
(13, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT), # middle_down Shift Alt 1+4+8+ =13
|
||||
(17, 'M') : (MIDDLE, MOUSE_DOWN, CONTROL), # middle_down Control 1+ + +16=17
|
||||
(21, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_CONTROL), # middle_down Shift Control 1+4+ +16=21
|
||||
(25, 'M') : (MIDDLE, MOUSE_DOWN, ALT_CONTROL), # middle_down Alt Control 1+ +8+16=25
|
||||
(29, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT_CONTROL), # middle_down Shift Alt Control 1+4+8+16=29
|
||||
|
||||
( 2, 'M') : (RIGHT, MOUSE_DOWN, NO_MODIFIER), # right_down 2+ + + =2
|
||||
( 6, 'M') : (RIGHT, MOUSE_DOWN, SHIFT), # right_down Shift 2+4+ + =6
|
||||
(10, 'M') : (RIGHT, MOUSE_DOWN, ALT), # right_down Alt 2+ +8+ =10
|
||||
(14, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT), # right_down Shift Alt 2+4+8+ =14
|
||||
(18, 'M') : (RIGHT, MOUSE_DOWN, CONTROL), # right_down Control 2+ + +16=18
|
||||
(22, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_CONTROL), # right_down Shift Control 2+4+ +16=22
|
||||
(26, 'M') : (RIGHT, MOUSE_DOWN, ALT_CONTROL), # right_down Alt Control 2+ +8+16=26
|
||||
(30, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # right_down Shift Alt Control 2+4+8+16=30
|
||||
|
||||
(32, 'M') : (LEFT, MOUSE_MOVE, NO_MODIFIER), # left_drag 32+ + + =32
|
||||
(36, 'M') : (LEFT, MOUSE_MOVE, SHIFT), # left_drag Shift 32+4+ + =36
|
||||
(40, 'M') : (LEFT, MOUSE_MOVE, ALT), # left_drag Alt 32+ +8+ =40
|
||||
(44, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT), # left_drag Shift Alt 32+4+8+ =44
|
||||
(48, 'M') : (LEFT, MOUSE_MOVE, CONTROL), # left_drag Control 32+ + +16=48
|
||||
(52, 'M') : (LEFT, MOUSE_MOVE, SHIFT_CONTROL), # left_drag Shift Control 32+4+ +16=52
|
||||
(56, 'M') : (LEFT, MOUSE_MOVE, ALT_CONTROL), # left_drag Alt Control 32+ +8+16=56
|
||||
(60, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # left_drag Shift Alt Control 32+4+8+16=60
|
||||
|
||||
(33, 'M') : (MIDDLE, MOUSE_MOVE, NO_MODIFIER), # middle_drag 33+ + + =33
|
||||
(37, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT), # middle_drag Shift 33+4+ + =37
|
||||
(41, 'M') : (MIDDLE, MOUSE_MOVE, ALT), # middle_drag Alt 33+ +8+ =41
|
||||
(45, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT), # middle_drag Shift Alt 33+4+8+ =45
|
||||
(49, 'M') : (MIDDLE, MOUSE_MOVE, CONTROL), # middle_drag Control 33+ + +16=49
|
||||
(53, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_CONTROL), # middle_drag Shift Control 33+4+ +16=53
|
||||
(57, 'M') : (MIDDLE, MOUSE_MOVE, ALT_CONTROL), # middle_drag Alt Control 33+ +8+16=57
|
||||
(61, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT_CONTROL), # middle_drag Shift Alt Control 33+4+8+16=61
|
||||
|
||||
(34, 'M') : (RIGHT, MOUSE_MOVE, NO_MODIFIER), # right_drag 34+ + + =34
|
||||
(38, 'M') : (RIGHT, MOUSE_MOVE, SHIFT), # right_drag Shift 34+4+ + =38
|
||||
(42, 'M') : (RIGHT, MOUSE_MOVE, ALT), # right_drag Alt 34+ +8+ =42
|
||||
(46, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT), # right_drag Shift Alt 34+4+8+ =46
|
||||
(50, 'M') : (RIGHT, MOUSE_MOVE, CONTROL), # right_drag Control 34+ + +16=50
|
||||
(54, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_CONTROL), # right_drag Shift Control 34+4+ +16=54
|
||||
(58, 'M') : (RIGHT, MOUSE_MOVE, ALT_CONTROL), # right_drag Alt Control 34+ +8+16=58
|
||||
(62, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # right_drag Shift Alt Control 34+4+8+16=62
|
||||
|
||||
(35, 'M') : (NO_BUTTON, MOUSE_MOVE, NO_MODIFIER), # none_drag 35+ + + =35
|
||||
(39, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT), # none_drag Shift 35+4+ + =39
|
||||
(43, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT), # none_drag Alt 35+ +8+ =43
|
||||
(47, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT), # none_drag Shift Alt 35+4+8+ =47
|
||||
(51, 'M') : (NO_BUTTON, MOUSE_MOVE, CONTROL), # none_drag Control 35+ + +16=51
|
||||
(55, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_CONTROL), # none_drag Shift Control 35+4+ +16=55
|
||||
(59, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT_CONTROL), # none_drag Alt Control 35+ +8+16=59
|
||||
(63, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT_CONTROL), # none_drag Shift Alt Control 35+4+8+16=63
|
||||
|
||||
(64, 'M') : (NO_BUTTON, SCROLL_UP, NO_MODIFIER), # scroll_up 64+ + + =64
|
||||
(68, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT), # scroll_up Shift 64+4+ + =68
|
||||
(72, 'M') : (NO_BUTTON, SCROLL_UP, ALT), # scroll_up Alt 64+ +8+ =72
|
||||
(76, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT), # scroll_up Shift Alt 64+4+8+ =76
|
||||
(80, 'M') : (NO_BUTTON, SCROLL_UP, CONTROL), # scroll_up Control 64+ + +16=80
|
||||
(84, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_CONTROL), # scroll_up Shift Control 64+4+ +16=84
|
||||
(88, 'M') : (NO_BUTTON, SCROLL_UP, ALT_CONTROL), # scroll_up Alt Control 64+ +8+16=88
|
||||
(92, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT_CONTROL), # scroll_up Shift Alt Control 64+4+8+16=92
|
||||
|
||||
(65, 'M') : (NO_BUTTON, SCROLL_DOWN, NO_MODIFIER), # scroll_down 64+ + + =65
|
||||
(69, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT), # scroll_down Shift 64+4+ + =69
|
||||
(73, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT), # scroll_down Alt 64+ +8+ =73
|
||||
(77, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT), # scroll_down Shift Alt 64+4+8+ =77
|
||||
(81, 'M') : (NO_BUTTON, SCROLL_DOWN, CONTROL), # scroll_down Control 64+ + +16=81
|
||||
(85, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_CONTROL), # scroll_down Shift Control 64+4+ +16=85
|
||||
(89, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT_CONTROL), # scroll_down Alt Control 64+ +8+16=89
|
||||
(93, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT_CONTROL), # scroll_down Shift Alt Control 64+4+8+16=93
|
||||
}
|
||||
|
||||
typical_mouse_events = {
|
||||
32: (LEFT , MOUSE_DOWN , UNKNOWN_MODIFIER),
|
||||
33: (MIDDLE , MOUSE_DOWN , UNKNOWN_MODIFIER),
|
||||
34: (RIGHT , MOUSE_DOWN , UNKNOWN_MODIFIER),
|
||||
35: (UNKNOWN_BUTTON , MOUSE_UP , UNKNOWN_MODIFIER),
|
||||
|
||||
64: (LEFT , MOUSE_MOVE , UNKNOWN_MODIFIER),
|
||||
65: (MIDDLE , MOUSE_MOVE , UNKNOWN_MODIFIER),
|
||||
66: (RIGHT , MOUSE_MOVE , UNKNOWN_MODIFIER),
|
||||
67: (NO_BUTTON , MOUSE_MOVE , UNKNOWN_MODIFIER),
|
||||
|
||||
96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER),
|
||||
97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER),
|
||||
}
|
||||
|
||||
urxvt_mouse_events={
|
||||
32: (UNKNOWN_BUTTON, MOUSE_DOWN , UNKNOWN_MODIFIER),
|
||||
35: (UNKNOWN_BUTTON, MOUSE_UP , UNKNOWN_MODIFIER),
|
||||
96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER),
|
||||
97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER),
|
||||
}
|
||||
# fmt:on
|
||||
|
||||
|
||||
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) -> "NotImplementedOrNone":
|
||||
"""
|
||||
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:])
|
||||
|
||||
# TODO: Is it possible to add modifiers here?
|
||||
mouse_button, mouse_event_type, mouse_modifiers = typical_mouse_events[
|
||||
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:
|
||||
try:
|
||||
(
|
||||
mouse_button,
|
||||
mouse_event_type,
|
||||
mouse_modifiers,
|
||||
) = xterm_sgr_mouse_events[mouse_event, m]
|
||||
except KeyError:
|
||||
return NotImplemented
|
||||
|
||||
else:
|
||||
# Some other terminals, like urxvt, Hyper terminal, ...
|
||||
(
|
||||
mouse_button,
|
||||
mouse_event_type,
|
||||
mouse_modifiers,
|
||||
) = urxvt_mouse_events.get(
|
||||
mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER)
|
||||
)
|
||||
|
||||
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 NotImplemented
|
||||
|
||||
# Call the mouse handler from the renderer.
|
||||
|
||||
# Note: This can return `NotImplemented` if no mouse handler was
|
||||
# found for this position, or if no repainting needs to
|
||||
# happen. this way, we avoid excessive repaints during mouse
|
||||
# movements.
|
||||
handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
|
||||
return handler(
|
||||
MouseEvent(
|
||||
position=Point(x=x, y=y),
|
||||
event_type=mouse_event_type,
|
||||
button=mouse_button,
|
||||
modifiers=mouse_modifiers,
|
||||
)
|
||||
)
|
||||
|
||||
return NotImplemented
|
||||
|
||||
@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) -> "NotImplementedOrNone":
|
||||
"""
|
||||
Handling of mouse events for Windows.
|
||||
"""
|
||||
# This key binding should only exist for Windows.
|
||||
if sys.platform == "win32":
|
||||
# Parse data.
|
||||
pieces = event.data.split(";")
|
||||
|
||||
button = MouseButton(pieces[0])
|
||||
event_type = MouseEventType(pieces[1])
|
||||
x = int(pieces[2])
|
||||
y = int(pieces[3])
|
||||
|
||||
# Make coordinates absolute to the visible part of the terminal.
|
||||
output = event.app.renderer.output
|
||||
|
||||
from prompt_toolkit.output.win32 import Win32Output
|
||||
from prompt_toolkit.output.windows10 import Windows10_Output
|
||||
|
||||
if isinstance(output, (Win32Output, Windows10_Output)):
|
||||
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.
|
||||
# (Can return `NotImplemented`.)
|
||||
handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
|
||||
|
||||
return handler(
|
||||
MouseEvent(
|
||||
position=Point(x=x, y=y),
|
||||
event_type=event_type,
|
||||
button=button,
|
||||
modifiers=UNKNOWN_MODIFIER,
|
||||
)
|
||||
)
|
||||
|
||||
# No mouse handler found. Return `NotImplemented` so that we don't
|
||||
# invalidate the UI.
|
||||
return NotImplemented
|
||||
|
||||
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(),
|
||||
]
|
||||
)
|
1377
.venv/Lib/site-packages/prompt_toolkit/key_binding/digraphs.py
Normal file
1377
.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,672 @@
|
||||
"""
|
||||
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 inspect import isawaitable
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Awaitable,
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Avoid circular imports.
|
||||
from .key_processor import KeyPressEvent
|
||||
|
||||
# The only two return values for a mouse hander (and key bindings) are
|
||||
# `None` and `NotImplemented`. For the type checker it's best to annotate
|
||||
# this as `object`. (The consumer never expects a more specific instance:
|
||||
# checking for NotImplemented can be done using `is NotImplemented`.)
|
||||
NotImplementedOrNone = object
|
||||
# Other non-working options are:
|
||||
# * Optional[Literal[NotImplemented]]
|
||||
# --> Doesn't work, Literal can't take an Any.
|
||||
# * None
|
||||
# --> Doesn't work. We can't assign the result of a function that
|
||||
# returns `None` to a variable.
|
||||
# * Any
|
||||
# --> Works, but too broad.
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NotImplementedOrNone",
|
||||
"Binding",
|
||||
"KeyBindingsBase",
|
||||
"KeyBindings",
|
||||
"ConditionalKeyBindings",
|
||||
"merge_key_bindings",
|
||||
"DynamicKeyBindings",
|
||||
"GlobalOnlyKeyBindings",
|
||||
]
|
||||
|
||||
# Key bindings can be regular functions or coroutines.
|
||||
# In both cases, if they return `NotImplemented`, the UI won't be invalidated.
|
||||
# This is mainly used in case of mouse move events, to prevent excessive
|
||||
# repainting during mouse move events.
|
||||
KeyHandlerCallable = Callable[
|
||||
["KeyPressEvent"], Union["NotImplementedOrNone", Awaitable["NotImplementedOrNone"]]
|
||||
]
|
||||
|
||||
|
||||
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:
|
||||
result = self.handler(event)
|
||||
|
||||
# If the handler is a coroutine, create an asyncio task.
|
||||
if isawaitable(result):
|
||||
awaitable = cast(Awaitable["NotImplementedOrNone"], result)
|
||||
|
||||
async def bg_task() -> None:
|
||||
result = await awaitable
|
||||
if result != NotImplemented:
|
||||
event.app.invalidate()
|
||||
|
||||
event.app.create_background_task(bg_task())
|
||||
|
||||
elif result != NotImplemented:
|
||||
event.app.invalidate()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "{}(keys={!r}, handler={!r})".format(
|
||||
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(f"Binding not found: {function!r}")
|
||||
|
||||
# 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(f"Invalid key: {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, keys: KeysTuple) -> List[Binding]:
|
||||
self._update_cache()
|
||||
return self._bindings2.get_bindings_for_keys(keys)
|
||||
|
||||
def get_bindings_starting_with_keys(self, keys: KeysTuple) -> List[Binding]:
|
||||
self._update_cache()
|
||||
return self._bindings2.get_bindings_starting_with_keys(keys)
|
||||
|
||||
|
||||
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,528 @@
|
||||
"""
|
||||
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 f"{self.__class__.__name__}(key={self.key!r}, data={self.data!r})"
|
||||
|
||||
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]] = 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 = {
|
||||
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()
|
||||
|
||||
is_flush = False
|
||||
|
||||
while not_empty():
|
||||
# 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()
|
||||
raise
|
||||
|
||||
if not is_flush and not is_cpr:
|
||||
self.after_key_press.fire()
|
||||
|
||||
# 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())
|
||||
|
||||
def send_sigint(self) -> None:
|
||||
"""
|
||||
Send SIGINT. Immediately call the SIGINT key handler.
|
||||
"""
|
||||
self.feed(KeyPress(key=Keys.SIGINT), first=True)
|
||||
self.process_keys()
|
||||
|
||||
|
||||
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})".format(
|
||||
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 = f"{current}{data}"
|
||||
|
||||
self.key_processor.arg = result
|
||||
|
||||
@property
|
||||
def cli(self) -> "Application[Any]":
|
||||
"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 = ""
|
Reference in New Issue
Block a user