mirror of
https://github.com/aykhans/AzSuicideDataVisualization.git
synced 2025-07-02 06:22:25 +00:00
first commit
This commit is contained in:
@ -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
Reference in New Issue
Block a user