feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1 @@
from __future__ import unicode_literals

View File

@@ -0,0 +1,407 @@
# pylint: disable=function-redefined
from __future__ import unicode_literals
from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.filters import HasSelection, Condition, EmacsInsertMode, ViInsertMode
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout.screen import Point
from prompt_toolkit.mouse_events import MouseEventType, MouseEvent
from prompt_toolkit.renderer import HeightIsUnknownError
from prompt_toolkit.utils import suspend_to_background_supported, is_windows
from .named_commands import get_by_name
from ..registry import Registry
__all__ = (
'load_basic_bindings',
'load_abort_and_exit_bindings',
'load_basic_system_bindings',
'load_auto_suggestion_bindings',
)
def if_no_repeat(event):
""" Callable that returns True when the previous event was delivered to
another handler. """
return not event.is_repeat
def load_basic_bindings():
registry = Registry()
insert_mode = ViInsertMode() | EmacsInsertMode()
handle = registry.add_binding
has_selection = HasSelection()
@handle(Keys.ControlA)
@handle(Keys.ControlB)
@handle(Keys.ControlC)
@handle(Keys.ControlD)
@handle(Keys.ControlE)
@handle(Keys.ControlF)
@handle(Keys.ControlG)
@handle(Keys.ControlH)
@handle(Keys.ControlI)
@handle(Keys.ControlJ)
@handle(Keys.ControlK)
@handle(Keys.ControlL)
@handle(Keys.ControlM)
@handle(Keys.ControlN)
@handle(Keys.ControlO)
@handle(Keys.ControlP)
@handle(Keys.ControlQ)
@handle(Keys.ControlR)
@handle(Keys.ControlS)
@handle(Keys.ControlT)
@handle(Keys.ControlU)
@handle(Keys.ControlV)
@handle(Keys.ControlW)
@handle(Keys.ControlX)
@handle(Keys.ControlY)
@handle(Keys.ControlZ)
@handle(Keys.F1)
@handle(Keys.F2)
@handle(Keys.F3)
@handle(Keys.F4)
@handle(Keys.F5)
@handle(Keys.F6)
@handle(Keys.F7)
@handle(Keys.F8)
@handle(Keys.F9)
@handle(Keys.F10)
@handle(Keys.F11)
@handle(Keys.F12)
@handle(Keys.F13)
@handle(Keys.F14)
@handle(Keys.F15)
@handle(Keys.F16)
@handle(Keys.F17)
@handle(Keys.F18)
@handle(Keys.F19)
@handle(Keys.F20)
@handle(Keys.ControlSpace)
@handle(Keys.ControlBackslash)
@handle(Keys.ControlSquareClose)
@handle(Keys.ControlCircumflex)
@handle(Keys.ControlUnderscore)
@handle(Keys.Backspace)
@handle(Keys.Up)
@handle(Keys.Down)
@handle(Keys.Right)
@handle(Keys.Left)
@handle(Keys.ShiftUp)
@handle(Keys.ShiftDown)
@handle(Keys.ShiftRight)
@handle(Keys.ShiftLeft)
@handle(Keys.Home)
@handle(Keys.End)
@handle(Keys.Delete)
@handle(Keys.ShiftDelete)
@handle(Keys.ControlDelete)
@handle(Keys.PageUp)
@handle(Keys.PageDown)
@handle(Keys.BackTab)
@handle(Keys.Tab)
@handle(Keys.ControlLeft)
@handle(Keys.ControlRight)
@handle(Keys.ControlUp)
@handle(Keys.ControlDown)
@handle(Keys.Insert)
@handle(Keys.Ignore)
def _(event):
"""
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(Keys.Home)(get_by_name('beginning-of-line'))
handle(Keys.End)(get_by_name('end-of-line'))
handle(Keys.Left)(get_by_name('backward-char'))
handle(Keys.Right)(get_by_name('forward-char'))
handle(Keys.ControlUp)(get_by_name('previous-history'))
handle(Keys.ControlDown)(get_by_name('next-history'))
handle(Keys.ControlL)(get_by_name('clear-screen'))
handle(Keys.ControlK, filter=insert_mode)(get_by_name('kill-line'))
handle(Keys.ControlU, filter=insert_mode)(get_by_name('unix-line-discard'))
handle(Keys.ControlH, filter=insert_mode, save_before=if_no_repeat)(
get_by_name('backward-delete-char'))
handle(Keys.Backspace, filter=insert_mode, save_before=if_no_repeat)(
get_by_name('backward-delete-char'))
handle(Keys.Delete, filter=insert_mode, save_before=if_no_repeat)(
get_by_name('delete-char'))
handle(Keys.ShiftDelete, 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(Keys.ControlT, filter=insert_mode)(get_by_name('transpose-chars'))
handle(Keys.ControlW, filter=insert_mode)(get_by_name('unix-word-rubout'))
handle(Keys.ControlI, filter=insert_mode)(get_by_name('menu-complete'))
handle(Keys.BackTab, filter=insert_mode)(get_by_name('menu-complete-backward'))
handle(Keys.PageUp, filter= ~has_selection)(get_by_name('previous-history'))
handle(Keys.PageDown, filter= ~has_selection)(get_by_name('next-history'))
# CTRL keys.
text_before_cursor = Condition(lambda cli: cli.current_buffer.text)
handle(Keys.ControlD, filter=text_before_cursor & insert_mode)(get_by_name('delete-char'))
is_multiline = Condition(lambda cli: cli.current_buffer.is_multiline())
is_returnable = Condition(lambda cli: cli.current_buffer.accept_action.is_returnable)
@handle(Keys.ControlJ, filter=is_multiline & insert_mode)
def _(event):
" Newline (in case of multiline input. "
event.current_buffer.newline(copy_margin=not event.cli.in_paste_mode)
@handle(Keys.ControlJ, filter=~is_multiline & is_returnable)
def _(event):
" Enter, accept input. "
buff = event.current_buffer
buff.accept_action.validate_and_handle(event.cli, buff)
# Delete the word before the cursor.
@handle(Keys.Up)
def _(event):
event.current_buffer.auto_up(count=event.arg)
@handle(Keys.Down)
def _(event):
event.current_buffer.auto_down(count=event.arg)
@handle(Keys.Delete, filter=has_selection)
def _(event):
data = event.current_buffer.cut_selection()
event.cli.clipboard.set_data(data)
# Global bindings.
@handle(Keys.ControlZ)
def _(event):
"""
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.CPRResponse)
def _(event):
"""
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.cli.renderer.report_absolute_cursor_row(row)
@handle(Keys.BracketedPaste)
def _(event):
" 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)
@handle(Keys.Any, filter=Condition(lambda cli: cli.quoted_insert), eager=True)
def _(event):
"""
Handle quoted insert.
"""
event.current_buffer.insert_text(event.data, overwrite=False)
event.cli.quoted_insert = False
return registry
def load_mouse_bindings():
"""
Key bindings, required for mouse support.
(Mouse events enter through the key binding system.)
"""
registry = Registry()
@registry.add_binding(Keys.Vt100MouseEvent)
def _(event):
"""
Handling of incoming mouse event.
"""
# Typical: "Esc[MaB*"
# Urxvt: "Esc[96;14;13M"
# Xterm SGR: "Esc[<64;85;12M"
# Parse incoming packet.
if event.data[2] == 'M':
# Typical.
mouse_event, x, y = map(ord, event.data[3:])
mouse_event = {
32: MouseEventType.MOUSE_DOWN,
35: MouseEventType.MOUSE_UP,
96: MouseEventType.SCROLL_UP,
97: MouseEventType.SCROLL_DOWN,
}.get(mouse_event)
# Handle situations where `PosixStdinReader` used surrogateescapes.
if x >= 0xdc00: x-= 0xdc00
if y >= 0xdc00: y-= 0xdc00
x -= 32
y -= 32
else:
# Urxvt and Xterm SGR.
# When the '<' is not present, we are not using the Xterm SGR mode,
# but Urxvt instead.
data = event.data[2:]
if data[:1] == '<':
sgr = True
data = data[1:]
else:
sgr = False
# Extract coordinates.
mouse_event, x, y = map(int, data[:-1].split(';'))
m = data[-1]
# Parse event type.
if sgr:
mouse_event = {
(0, 'M'): MouseEventType.MOUSE_DOWN,
(0, 'm'): MouseEventType.MOUSE_UP,
(64, 'M'): MouseEventType.SCROLL_UP,
(65, 'M'): MouseEventType.SCROLL_DOWN,
}.get((mouse_event, m))
else:
mouse_event = {
32: MouseEventType.MOUSE_DOWN,
35: MouseEventType.MOUSE_UP,
96: MouseEventType.SCROLL_UP,
97: MouseEventType.SCROLL_DOWN,
}.get(mouse_event)
x -= 1
y -= 1
# Only handle mouse events when we know the window height.
if event.cli.renderer.height_is_known and mouse_event is not None:
# Take region above the layout into account. The reported
# coordinates are absolute to the visible part of the terminal.
try:
y -= event.cli.renderer.rows_above_layout
except HeightIsUnknownError:
return
# Call the mouse handler from the renderer.
handler = event.cli.renderer.mouse_handlers.mouse_handlers[x,y]
handler(event.cli, MouseEvent(position=Point(x=x, y=y),
event_type=mouse_event))
@registry.add_binding(Keys.WindowsMouseEvent)
def _(event):
"""
Handling of mouse events for Windows.
"""
assert is_windows() # This key binding should only exist for Windows.
# Parse data.
event_type, x, y = event.data.split(';')
x = int(x)
y = int(y)
# Make coordinates absolute to the visible part of the terminal.
screen_buffer_info = event.cli.renderer.output.get_win32_screen_buffer_info()
rows_above_cursor = screen_buffer_info.dwCursorPosition.Y - event.cli.renderer._cursor_pos.y
y -= rows_above_cursor
# Call the mouse event handler.
handler = event.cli.renderer.mouse_handlers.mouse_handlers[x,y]
handler(event.cli, MouseEvent(position=Point(x=x, y=y),
event_type=event_type))
return registry
def load_abort_and_exit_bindings():
"""
Basic bindings for abort (Ctrl-C) and exit (Ctrl-D).
"""
registry = Registry()
handle = registry.add_binding
@handle(Keys.ControlC)
def _(event):
" Abort when Control-C has been pressed. "
event.cli.abort()
@Condition
def ctrl_d_condition(cli):
""" Ctrl-D binding is only active when the default buffer is selected
and empty. """
return (cli.current_buffer_name == DEFAULT_BUFFER and
not cli.current_buffer.text)
handle(Keys.ControlD, filter=ctrl_d_condition)(get_by_name('end-of-file'))
return registry
def load_basic_system_bindings():
"""
Basic system bindings (For both Emacs and Vi mode.)
"""
registry = Registry()
suspend_supported = Condition(
lambda cli: suspend_to_background_supported())
@registry.add_binding(Keys.ControlZ, filter=suspend_supported)
def _(event):
"""
Suspend process to background.
"""
event.cli.suspend_to_background()
return registry
def load_auto_suggestion_bindings():
"""
Key bindings for accepting auto suggestion text.
"""
registry = Registry()
handle = registry.add_binding
suggestion_available = Condition(
lambda cli:
cli.current_buffer.suggestion is not None and
cli.current_buffer.document.is_cursor_at_the_end)
@handle(Keys.ControlF, filter=suggestion_available)
@handle(Keys.ControlE, filter=suggestion_available)
@handle(Keys.Right, filter=suggestion_available)
def _(event):
" Accept suggestion. "
b = event.current_buffer
suggestion = b.suggestion
if suggestion:
b.insert_text(suggestion.text)
return registry

View File

@@ -0,0 +1,161 @@
"""
Key binding handlers for displaying completions.
"""
from __future__ import unicode_literals
from prompt_toolkit.completion import CompleteEvent, get_common_complete_suffix
from prompt_toolkit.utils import get_cwidth
from prompt_toolkit.keys import Keys
from prompt_toolkit.key_binding.registry import Registry
import math
__all__ = (
'generate_completions',
'display_completions_like_readline',
)
def generate_completions(event):
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:
event.cli.start_completion(insert_common_part=True, select_first=False)
def display_completions_like_readline(event):
"""
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.
registry.add_binding(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.cli, completions)
def _display_completions_like_readline(cli, completions):
"""
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.shortcuts import create_confirm_application
assert isinstance(completions, list)
# Get terminal dimensions.
term_size = cli.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.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):
# 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 = []
for r in range(page_row_count):
for c in range(column_count):
try:
result.append(page_columns[c][r].text.ljust(max_compl_width))
except IndexError:
pass
result.append('\n')
cli.output.write(''.join(result))
cli.output.flush()
# User interaction through an application generator function.
def run():
if len(completions) > completions_per_page:
# Ask confirmation if it doesn't fit on the screen.
message = 'Display all {} possibilities? (y on n) '.format(len(completions))
confirm = yield create_confirm_application(message)
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 = yield _create_more_application()
if not show_more:
return
else:
cli.output.write('\n'); cli.output.flush()
else:
# Display all completions.
display(0)
cli.run_application_generator(run, render_cli_done=True)
def _create_more_application():
"""
Create an `Application` instance that displays the "--MORE--".
"""
from prompt_toolkit.shortcuts import create_prompt_application
registry = Registry()
@registry.add_binding(' ')
@registry.add_binding('y')
@registry.add_binding('Y')
@registry.add_binding(Keys.ControlJ)
@registry.add_binding(Keys.ControlI) # Tab.
def _(event):
event.cli.set_return_value(True)
@registry.add_binding('n')
@registry.add_binding('N')
@registry.add_binding('q')
@registry.add_binding('Q')
@registry.add_binding(Keys.ControlC)
def _(event):
event.cli.set_return_value(False)
return create_prompt_application(
'--MORE--', key_bindings_registry=registry, erase_when_done=True)

View File

@@ -0,0 +1,451 @@
# pylint: disable=function-redefined
from __future__ import unicode_literals
from prompt_toolkit.buffer import SelectionType, indent, unindent
from prompt_toolkit.keys import Keys
from prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER, SYSTEM_BUFFER
from prompt_toolkit.filters import Condition, EmacsMode, HasSelection, EmacsInsertMode, HasFocus, HasArg
from prompt_toolkit.completion import CompleteEvent
from .scroll import scroll_page_up, scroll_page_down
from .named_commands import get_by_name
from ..registry import Registry, ConditionalRegistry
__all__ = (
'load_emacs_bindings',
'load_emacs_search_bindings',
'load_emacs_system_bindings',
'load_extra_emacs_page_navigation_bindings',
)
def load_emacs_bindings():
"""
Some e-macs extensions.
"""
# Overview of Readline emacs commands:
# http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf
registry = ConditionalRegistry(Registry(), EmacsMode())
handle = registry.add_binding
insert_mode = EmacsInsertMode()
has_selection = HasSelection()
@handle(Keys.Escape)
def _(event):
"""
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(Keys.ControlA)(get_by_name('beginning-of-line'))
handle(Keys.ControlB)(get_by_name('backward-char'))
handle(Keys.ControlDelete, filter=insert_mode)(get_by_name('kill-word'))
handle(Keys.ControlE)(get_by_name('end-of-line'))
handle(Keys.ControlF)(get_by_name('forward-char'))
handle(Keys.ControlLeft)(get_by_name('backward-word'))
handle(Keys.ControlRight)(get_by_name('forward-word'))
handle(Keys.ControlX, 'r', 'y', filter=insert_mode)(get_by_name('yank'))
handle(Keys.ControlY, filter=insert_mode)(get_by_name('yank'))
handle(Keys.Escape, 'b')(get_by_name('backward-word'))
handle(Keys.Escape, 'c', filter=insert_mode)(get_by_name('capitalize-word'))
handle(Keys.Escape, 'd', filter=insert_mode)(get_by_name('kill-word'))
handle(Keys.Escape, 'f')(get_by_name('forward-word'))
handle(Keys.Escape, 'l', filter=insert_mode)(get_by_name('downcase-word'))
handle(Keys.Escape, 'u', filter=insert_mode)(get_by_name('uppercase-word'))
handle(Keys.Escape, 'y', filter=insert_mode)(get_by_name('yank-pop'))
handle(Keys.Escape, Keys.ControlH, filter=insert_mode)(get_by_name('backward-kill-word'))
handle(Keys.Escape, Keys.Backspace, filter=insert_mode)(get_by_name('backward-kill-word'))
handle(Keys.Escape, '\\', filter=insert_mode)(get_by_name('delete-horizontal-space'))
handle(Keys.ControlUnderscore, save_before=(lambda e: False), filter=insert_mode)(
get_by_name('undo'))
handle(Keys.ControlX, Keys.ControlU, save_before=(lambda e: False), filter=insert_mode)(
get_by_name('undo'))
handle(Keys.Escape, '<', filter= ~has_selection)(get_by_name('beginning-of-history'))
handle(Keys.Escape, '>', filter= ~has_selection)(get_by_name('end-of-history'))
handle(Keys.Escape, '.', filter=insert_mode)(get_by_name('yank-last-arg'))
handle(Keys.Escape, '_', filter=insert_mode)(get_by_name('yank-last-arg'))
handle(Keys.Escape, Keys.ControlY, filter=insert_mode)(get_by_name('yank-nth-arg'))
handle(Keys.Escape, '#', filter=insert_mode)(get_by_name('insert-comment'))
handle(Keys.ControlO)(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(Keys.ControlQ, filter= ~has_selection)(get_by_name('quoted-insert'))
handle(Keys.ControlX, '(')(get_by_name('start-kbd-macro'))
handle(Keys.ControlX, ')')(get_by_name('end-kbd-macro'))
handle(Keys.ControlX, 'e')(get_by_name('call-last-kbd-macro'))
@handle(Keys.ControlN)
def _(event):
" Next line. "
event.current_buffer.auto_down()
@handle(Keys.ControlP)
def _(event):
" Previous line. "
event.current_buffer.auto_up(count=event.arg)
def handle_digit(c):
"""
Handle input of arguments.
The first number needs to be preceeded by escape.
"""
@handle(c, filter=HasArg())
@handle(Keys.Escape, c)
def _(event):
event.append_to_arg_count(c)
for c in '0123456789':
handle_digit(c)
@handle(Keys.Escape, '-', filter=~HasArg())
def _(event):
"""
"""
if event._arg is None:
event.append_to_arg_count('-')
@handle('-', filter=Condition(lambda cli: cli.input_processor.arg == '-'))
def _(event):
"""
When '-' is typed again, after exactly '-' has been given as an
argument, ignore this.
"""
event.cli.input_processor.arg = '-'
is_returnable = Condition(
lambda cli: cli.current_buffer.accept_action.is_returnable)
# Meta + Newline: always accept input.
handle(Keys.Escape, Keys.ControlJ, filter=insert_mode & is_returnable)(
get_by_name('accept-line'))
def character_search(buff, char, count):
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(Keys.ControlSquareClose, Keys.Any)
def _(event):
" When Ctl-] + a character is pressed. go to that character. "
# Also named 'character-search'
character_search(event.current_buffer, event.data, event.arg)
@handle(Keys.Escape, Keys.ControlSquareClose, Keys.Any)
def _(event):
" Like Ctl-], but backwards. "
# Also named 'character-search-backward'
character_search(event.current_buffer, event.data, -event.arg)
@handle(Keys.Escape, 'a')
def _(event):
" Previous sentence. "
# TODO:
@handle(Keys.Escape, 'e')
def _(event):
" Move to end of sentence. "
# TODO:
@handle(Keys.Escape, 't', filter=insert_mode)
def _(event):
"""
Swap the last two words before the cursor.
"""
# TODO
@handle(Keys.Escape, '*', filter=insert_mode)
def _(event):
"""
`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(Keys.ControlX, Keys.ControlX)
def _(event):
"""
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(Keys.ControlSpace)
def _(event):
"""
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(Keys.ControlG, filter= ~has_selection)
def _(event):
"""
Control + G: Cancel completion menu and validation state.
"""
event.current_buffer.complete_state = None
event.current_buffer.validation_error = None
@handle(Keys.ControlG, filter=has_selection)
def _(event):
"""
Cancel selection.
"""
event.current_buffer.exit_selection()
@handle(Keys.ControlW, filter=has_selection)
@handle(Keys.ControlX, 'r', 'k', filter=has_selection)
def _(event):
"""
Cut selected text.
"""
data = event.current_buffer.cut_selection()
event.cli.clipboard.set_data(data)
@handle(Keys.Escape, 'w', filter=has_selection)
def _(event):
"""
Copy selected text.
"""
data = event.current_buffer.copy_selection()
event.cli.clipboard.set_data(data)
@handle(Keys.Escape, Keys.Left)
def _(event):
"""
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(Keys.Escape, Keys.Right)
def _(event):
"""
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(Keys.Escape, '/', filter=insert_mode)
def _(event):
"""
M-/: Complete.
"""
b = event.current_buffer
if b.complete_state:
b.complete_next()
else:
event.cli.start_completion(select_first=True)
@handle(Keys.ControlC, '>', filter=has_selection)
def _(event):
"""
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(Keys.ControlC, '<', filter=has_selection)
def _(event):
"""
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 registry
def load_emacs_open_in_editor_bindings():
"""
Pressing C-X C-E will open the buffer in an external editor.
"""
registry = Registry()
registry.add_binding(Keys.ControlX, Keys.ControlE,
filter=EmacsMode() & ~HasSelection())(
get_by_name('edit-and-execute-command'))
return registry
def load_emacs_system_bindings():
registry = ConditionalRegistry(Registry(), EmacsMode())
handle = registry.add_binding
has_focus = HasFocus(SYSTEM_BUFFER)
@handle(Keys.Escape, '!', filter= ~has_focus)
def _(event):
"""
M-'!' opens the system prompt.
"""
event.cli.push_focus(SYSTEM_BUFFER)
@handle(Keys.Escape, filter=has_focus)
@handle(Keys.ControlG, filter=has_focus)
@handle(Keys.ControlC, filter=has_focus)
def _(event):
"""
Cancel system prompt.
"""
event.cli.buffers[SYSTEM_BUFFER].reset()
event.cli.pop_focus()
@handle(Keys.ControlJ, filter=has_focus)
def _(event):
"""
Run system command.
"""
system_line = event.cli.buffers[SYSTEM_BUFFER]
event.cli.run_system_command(system_line.text)
system_line.reset(append_to_history=True)
# Focus previous buffer again.
event.cli.pop_focus()
return registry
def load_emacs_search_bindings(get_search_state=None):
registry = ConditionalRegistry(Registry(), EmacsMode())
handle = registry.add_binding
has_focus = HasFocus(SEARCH_BUFFER)
assert get_search_state is None or callable(get_search_state)
if not get_search_state:
def get_search_state(cli): return cli.search_state
@handle(Keys.ControlG, filter=has_focus)
@handle(Keys.ControlC, filter=has_focus)
# NOTE: the reason for not also binding Escape to this one, is that we want
# Alt+Enter to accept input directly in incremental search mode.
def _(event):
"""
Abort an incremental search and restore the original line.
"""
search_buffer = event.cli.buffers[SEARCH_BUFFER]
search_buffer.reset()
event.cli.pop_focus()
@handle(Keys.ControlJ, filter=has_focus)
def _(event):
"""
When enter pressed in isearch, quit isearch mode. (Multiline
isearch would be too complicated.)
"""
input_buffer = event.cli.buffers.previous(event.cli)
search_buffer = event.cli.buffers[SEARCH_BUFFER]
# Update search state.
if search_buffer.text:
get_search_state(event.cli).text = search_buffer.text
# Apply search.
input_buffer.apply_search(get_search_state(event.cli), include_current_position=True)
# Add query to history of search line.
search_buffer.append_to_history()
search_buffer.reset()
# Focus previous document again.
event.cli.pop_focus()
@handle(Keys.ControlR, filter= ~has_focus)
def _(event):
get_search_state(event.cli).direction = IncrementalSearchDirection.BACKWARD
event.cli.push_focus(SEARCH_BUFFER)
@handle(Keys.ControlS, filter= ~has_focus)
def _(event):
get_search_state(event.cli).direction = IncrementalSearchDirection.FORWARD
event.cli.push_focus(SEARCH_BUFFER)
def incremental_search(cli, direction, count=1):
" Apply search, but keep search buffer focussed. "
# Update search_state.
search_state = get_search_state(cli)
direction_changed = search_state.direction != direction
search_state.text = cli.buffers[SEARCH_BUFFER].text
search_state.direction = direction
# Apply search to current buffer.
if not direction_changed:
input_buffer = cli.buffers.previous(cli)
input_buffer.apply_search(search_state,
include_current_position=False, count=count)
@handle(Keys.ControlR, filter=has_focus)
@handle(Keys.Up, filter=has_focus)
def _(event):
incremental_search(event.cli, IncrementalSearchDirection.BACKWARD, count=event.arg)
@handle(Keys.ControlS, filter=has_focus)
@handle(Keys.Down, filter=has_focus)
def _(event):
incremental_search(event.cli, IncrementalSearchDirection.FORWARD, count=event.arg)
return registry
def load_extra_emacs_page_navigation_bindings():
"""
Key bindings, for scrolling up and down through pages.
This are separate bindings, because GNU readline doesn't have them.
"""
registry = ConditionalRegistry(Registry(), EmacsMode())
handle = registry.add_binding
handle(Keys.ControlV)(scroll_page_down)
handle(Keys.PageDown)(scroll_page_down)
handle(Keys.Escape, 'v')(scroll_page_up)
handle(Keys.PageUp)(scroll_page_up)
return registry

View File

@@ -0,0 +1,578 @@
"""
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 __future__ import unicode_literals
from prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER
from prompt_toolkit.selection import PasteMode
from six.moves import range
import six
from .completion import generate_completions, display_completions_like_readline
from prompt_toolkit.document import Document
from prompt_toolkit.enums import EditingMode
from prompt_toolkit.key_binding.input_processor import KeyPress
from prompt_toolkit.keys import Keys
__all__ = (
'get_by_name',
)
# Registry that maps the Readline command names to their handlers.
_readline_commands = {}
def register(name):
"""
Store handler in the `_readline_commands` dictionary.
"""
assert isinstance(name, six.text_type)
def decorator(handler):
assert callable(handler)
_readline_commands[name] = handler
return handler
return decorator
def get_by_name(name):
"""
Return the handler for the (Readline) command with the given name.
"""
try:
return _readline_commands[name]
except KeyError:
raise KeyError('Unknown readline command: %r' % name)
#
# Commands for moving
# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html
#
@register('beginning-of-line')
def beginning_of_line(event):
" 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):
" 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):
" 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):
" 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):
"""
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):
"""
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):
"""
Clear the screen and redraw everything at the top of the screen.
"""
event.cli.renderer.clear()
@register('redraw-current-line')
def redraw_current_line(event):
"""
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):
" Accept the line regardless of where the cursor is. "
b = event.current_buffer
b.accept_action.validate_and_handle(event.cli, b)
@register('previous-history')
def previous_history(event):
" 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):
" 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):
" Move to the first line in the history. "
event.current_buffer.go_to_history(0)
@register('end-of-history')
def end_of_history(event):
"""
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):
"""
Search backward starting at the current line and moving `up' through
the history as necessary. This is an incremental search.
"""
event.cli.current_search_state.direction = IncrementalSearchDirection.BACKWARD
event.cli.push_focus(SEARCH_BUFFER)
#
# Commands for changing text
#
@register('end-of-file')
def end_of_file(event):
"""
Exit.
"""
event.cli.exit()
@register('delete-char')
def delete_char(event):
" Delete character before the cursor. "
deleted = event.current_buffer.delete(count=event.arg)
if not deleted:
event.cli.output.bell()
@register('backward-delete-char')
def backward_delete_char(event):
" 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.cli.output.bell()
@register('self-insert')
def self_insert(event):
" Insert yourself. "
event.current_buffer.insert_text(event.data * event.arg)
@register('transpose-chars')
def transpose_chars(event):
"""
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):
"""
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):
"""
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):
"""
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):
"""
Add the next character typed to the line verbatim. This is how to insert
key sequences like C-q, for example.
"""
event.cli.quoted_insert = True
#
# Killing and yanking.
#
@register('kill-line')
def kill_line(event):
"""
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.cli.clipboard.set_text(deleted)
@register('kill-word')
def kill_word(event):
"""
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)
event.cli.clipboard.set_text(deleted)
@register('unix-word-rubout')
def unix_word_rubout(event, WORD=True):
"""
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.cli.clipboard.get_data().text
event.cli.clipboard.set_text(deleted)
else:
# Nothing to delete. Bell.
event.cli.output.bell()
@register('backward-kill-word')
def backward_kill_word(event):
"""
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):
" 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):
"""
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.cli.clipboard.set_text(deleted)
@register('yank')
def yank(event):
"""
Paste before cursor.
"""
event.current_buffer.paste_clipboard_data(
event.cli.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS)
@register('yank-nth-arg')
def yank_nth_arg(event):
"""
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):
"""
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):
"""
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.cli.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):
" Attempt to perform completion. "
display_completions_like_readline(event)
@register('menu-complete')
def menu_complete(event):
"""
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):
" Move backward through the list of possible completions. "
event.current_buffer.complete_previous()
#
# Keyboard macros.
#
@register('start-kbd-macro')
def start_kbd_macro(event):
"""
Begin saving the characters typed into the current keyboard macro.
"""
event.cli.input_processor.start_macro()
@register('end-kbd-macro')
def start_kbd_macro(event):
"""
Stop saving the characters typed into the current keyboard macro and save
the definition.
"""
event.cli.input_processor.end_macro()
@register('call-last-kbd-macro')
def start_kbd_macro(event):
"""
Re-execute the last keyboard macro defined, by making the characters in the
macro appear as if typed at the keyboard.
"""
event.cli.input_processor.call_macro()
@register('print-last-kbd-macro')
def print_last_kbd_macro(event):
" Print the last keboard macro. "
# TODO: Make the format suitable for the inputrc file.
def print_macro():
for k in event.cli.input_processor.macro:
print(k)
event.cli.run_in_terminal(print_macro)
#
# Miscellaneous Commands.
#
@register('undo')
def undo(event):
" Incremental undo. "
event.current_buffer.undo()
@register('insert-comment')
def insert_comment(event):
"""
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):
return line[1:] if line.startswith('#') else line
else:
def change(line):
return '#' + line
buff.document = Document(
text='\n'.join(map(change, buff.text.splitlines())),
cursor_position=0)
# Accept input.
buff.accept_action.validate_and_handle(event.cli, buff)
@register('vi-editing-mode')
def vi_editing_mode(event):
" Switch to Vi editing mode. "
event.cli.editing_mode = EditingMode.VI
@register('emacs-editing-mode')
def emacs_editing_mode(event):
" Switch to Emacs editing mode. "
event.cli.editing_mode = EditingMode.EMACS
@register('prefix-meta')
def prefix_meta(event):
"""
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'::
registry.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta)
"""
event.cli.input_processor.feed(KeyPress(Keys.Escape))
@register('operate-and-get-next')
def operate_and_get_next(event):
"""
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.accept_action.validate_and_handle(event.cli, buff)
# Set the new index at the start of the next run.
def set_working_index():
if new_index < len(buff._working_lines):
buff.working_index = new_index
event.cli.pre_run_callables.append(set_working_index)
@register('edit-and-execute-command')
def edit_and_execute(event):
"""
Invoke an editor on the current command line, and accept the result.
"""
buff = event.current_buffer
buff.open_in_editor(event.cli)
buff.accept_action.validate_and_handle(event.cli, buff)

View File

@@ -0,0 +1,185 @@
"""
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 __future__ import unicode_literals
from prompt_toolkit.layout.utils import find_window_for_buffer_name
from six.moves import range
__all__ = (
'scroll_forward',
'scroll_backward',
'scroll_half_page_up',
'scroll_half_page_down',
'scroll_one_line_up',
'scroll_one_line_down',
)
def _current_window_for_event(event):
"""
Return the `Window` for the currently focussed Buffer.
"""
return find_window_for_buffer_name(event.cli, event.cli.current_buffer_name)
def scroll_forward(event, half=False):
"""
Scroll window down.
"""
w = _current_window_for_event(event)
b = event.cli.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, half=False):
"""
Scroll window up.
"""
w = _current_window_for_event(event)
b = event.cli.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):
"""
Same as ControlF, but only scroll half a page.
"""
scroll_forward(event, half=True)
def scroll_half_page_up(event):
"""
Same as ControlB, but only scroll half a page.
"""
scroll_backward(event, half=True)
def scroll_one_line_down(event):
"""
scroll_offset += 1
"""
w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name)
b = event.cli.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):
"""
scroll_offset -= 1
"""
w = find_window_for_buffer_name(event.cli, event.cli.current_buffer_name)
b = event.cli.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):
"""
Scroll page down. (Prefer the cursor at the top of the page, after scrolling.)
"""
w = _current_window_for_event(event)
b = event.cli.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):
"""
Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.)
"""
w = _current_window_for_event(event)
b = event.cli.current_buffer
if w and w.render_info:
# Put cursor at the first visible line. (But make sure that the cursor
# moves at least one line up.)
line_index = max(0, min(w.render_info.first_visible_line(),
b.document.cursor_position_row - 1))
b.cursor_position = b.document.translate_row_col_to_index(line_index, 0)
b.cursor_position += b.document.get_start_of_line_position(after_whitespace=True)
# Set the scroll offset. We can safely set it to zero; the Window will
# make sure that it scrolls at least until the cursor becomes visible.
w.vertical_scroll = 0

View File

@@ -0,0 +1,119 @@
"""
Default key bindings.::
registry = load_key_bindings()
app = Application(key_bindings_registry=registry)
"""
from __future__ import unicode_literals
from prompt_toolkit.key_binding.registry import ConditionalRegistry, MergedRegistry
from prompt_toolkit.key_binding.bindings.basic import load_basic_bindings, load_abort_and_exit_bindings, load_basic_system_bindings, load_auto_suggestion_bindings, load_mouse_bindings
from prompt_toolkit.key_binding.bindings.emacs import load_emacs_bindings, load_emacs_system_bindings, load_emacs_search_bindings, load_emacs_open_in_editor_bindings, load_extra_emacs_page_navigation_bindings
from prompt_toolkit.key_binding.bindings.vi import load_vi_bindings, load_vi_system_bindings, load_vi_search_bindings, load_vi_open_in_editor_bindings, load_extra_vi_page_navigation_bindings
from prompt_toolkit.filters import to_cli_filter
__all__ = (
'load_key_bindings',
'load_key_bindings_for_prompt',
)
def load_key_bindings(
get_search_state=None,
enable_abort_and_exit_bindings=False,
enable_system_bindings=False,
enable_search=False,
enable_open_in_editor=False,
enable_extra_page_navigation=False,
enable_auto_suggest_bindings=False):
"""
Create a Registry object that contains the default key bindings.
:param enable_abort_and_exit_bindings: Filter to enable Ctrl-C and Ctrl-D.
:param enable_system_bindings: Filter to enable the system bindings (meta-!
prompt and Control-Z suspension.)
:param enable_search: Filter to enable the search bindings.
:param enable_open_in_editor: Filter to enable open-in-editor.
:param enable_open_in_editor: Filter to enable open-in-editor.
:param enable_extra_page_navigation: Filter for enabling extra page
navigation. (Bindings for up/down scrolling through long pages, like in
Emacs or Vi.)
:param enable_auto_suggest_bindings: Filter to enable fish-style suggestions.
"""
assert get_search_state is None or callable(get_search_state)
# Accept both Filters and booleans as input.
enable_abort_and_exit_bindings = to_cli_filter(enable_abort_and_exit_bindings)
enable_system_bindings = to_cli_filter(enable_system_bindings)
enable_search = to_cli_filter(enable_search)
enable_open_in_editor = to_cli_filter(enable_open_in_editor)
enable_extra_page_navigation = to_cli_filter(enable_extra_page_navigation)
enable_auto_suggest_bindings = to_cli_filter(enable_auto_suggest_bindings)
registry = MergedRegistry([
# Load basic bindings.
load_basic_bindings(),
load_mouse_bindings(),
ConditionalRegistry(load_abort_and_exit_bindings(),
enable_abort_and_exit_bindings),
ConditionalRegistry(load_basic_system_bindings(),
enable_system_bindings),
# Load emacs bindings.
load_emacs_bindings(),
ConditionalRegistry(load_emacs_open_in_editor_bindings(),
enable_open_in_editor),
ConditionalRegistry(load_emacs_search_bindings(get_search_state=get_search_state),
enable_search),
ConditionalRegistry(load_emacs_system_bindings(),
enable_system_bindings),
ConditionalRegistry(load_extra_emacs_page_navigation_bindings(),
enable_extra_page_navigation),
# Load Vi bindings.
load_vi_bindings(get_search_state=get_search_state),
ConditionalRegistry(load_vi_open_in_editor_bindings(),
enable_open_in_editor),
ConditionalRegistry(load_vi_search_bindings(get_search_state=get_search_state),
enable_search),
ConditionalRegistry(load_vi_system_bindings(),
enable_system_bindings),
ConditionalRegistry(load_extra_vi_page_navigation_bindings(),
enable_extra_page_navigation),
# Suggestion bindings.
# (This has to come at the end, because the Vi bindings also have an
# implementation for the "right arrow", but we really want the
# suggestion binding when a suggestion is available.)
ConditionalRegistry(load_auto_suggestion_bindings(),
enable_auto_suggest_bindings),
])
return registry
def load_key_bindings_for_prompt(**kw):
"""
Create a ``Registry`` object with the defaults key bindings for an input
prompt.
This activates the key bindings for abort/exit (Ctrl-C/Ctrl-D),
incremental search and auto suggestions.
(Not for full screen applications.)
"""
kw.setdefault('enable_abort_and_exit_bindings', True)
kw.setdefault('enable_search', True)
kw.setdefault('enable_auto_suggest_bindings', True)
return load_key_bindings(**kw)

View File

@@ -0,0 +1,372 @@
# *** encoding: utf-8 ***
"""
An :class:`~.InputProcessor` receives callbacks for the keystrokes parsed from
the input in the :class:`~prompt_toolkit.inputstream.InputStream` instance.
The `InputProcessor` will according to the implemented keybindings call the
correct callbacks when new key presses are feed through `feed`.
"""
from __future__ import unicode_literals
from prompt_toolkit.buffer import EditReadOnlyBuffer
from prompt_toolkit.filters.cli import ViNavigationMode
from prompt_toolkit.keys import Keys, Key
from prompt_toolkit.utils import Event
from .registry import BaseRegistry
from collections import deque
from six.moves import range
import weakref
import six
__all__ = (
'InputProcessor',
'KeyPress',
)
class KeyPress(object):
"""
:param key: A `Keys` instance or text (one character).
:param data: The received string on stdin. (Often vt100 escape codes.)
"""
def __init__(self, key, data=None):
assert isinstance(key, (six.text_type, Key))
assert data is None or isinstance(data, six.text_type)
if data is None:
data = key.name if isinstance(key, Key) else key
self.key = key
self.data = data
def __repr__(self):
return '%s(key=%r, data=%r)' % (
self.__class__.__name__, self.key, self.data)
def __eq__(self, other):
return self.key == other.key and self.data == other.data
class InputProcessor(object):
"""
Statemachine that receives :class:`KeyPress` instances and according to the
key bindings in the given :class:`Registry`, calls the matching handlers.
::
p = InputProcessor(registry)
# 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 registry.
:param registry: `BaseRegistry` instance.
:param cli_ref: weakref to `CommandLineInterface`.
"""
def __init__(self, registry, cli_ref):
assert isinstance(registry, BaseRegistry)
self._registry = registry
self._cli_ref = cli_ref
self.beforeKeyPress = Event(self)
self.afterKeyPress = Event(self)
# The queue of keys not yet send to our _process generator/state machine.
self.input_queue = 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 = []
# Simple macro recording. (Like readline does.)
self.record_macro = False
self.macro = []
self.reset()
def reset(self):
self._previous_key_sequence = None
self._previous_handler = None
self._process_coroutine = self._process()
self._process_coroutine.send(None)
#: Readline argument (for repetition of commands.)
#: https://www.gnu.org/software/bash/manual/html_node/Readline-Arguments.html
self.arg = None
def start_macro(self):
" Start recording macro. "
self.record_macro = True
self.macro = []
def end_macro(self):
" End recording macro. "
self.record_macro = False
def call_macro(self):
for k in self.macro:
self.feed(k)
def _get_matches(self, key_presses):
"""
For a list of :class:`KeyPress` instances. Give the matching handlers
that would handle this.
"""
keys = tuple(k.key for k in key_presses)
cli = self._cli_ref()
# Try match, with mode flag
return [b for b in self._registry.get_bindings_for_keys(keys) if b.filter(cli)]
def _is_prefix_of_longer_match(self, key_presses):
"""
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)
cli = self._cli_ref()
# Get the filters for all the key bindings that have a longer match.
# Note that we transform it into a `set`, because we don't care about
# the actual bindings and executing it more than once doesn't make
# sense. (Many key bindings share the same filter.)
filters = set(b.filter for b in self._registry.get_bindings_starting_with_keys(keys))
# When any key binding is active, return True.
return any(f(cli) for f in filters)
def _process(self):
"""
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:
if retry:
retry = False
else:
buffer.append((yield))
# If we have some key presses, check for matches.
if buffer:
is_prefix_of_longer_match = self._is_prefix_of_longer_match(buffer)
matches = self._get_matches(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(self._cli_ref())]
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):
"""
Add a new :class:`KeyPress` to the input queue.
(Don't forget to call `process_keys` in order to process the queue.)
"""
assert isinstance(key_press, KeyPress)
self.input_queue.append(key_press)
def process_keys(self):
"""
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.
"""
while self.input_queue:
key_press = self.input_queue.popleft()
if key_press.key != Keys.CPRResponse:
self.beforeKeyPress.fire()
self._process_coroutine.send(key_press)
if key_press.key != Keys.CPRResponse:
self.afterKeyPress.fire()
# Invalidate user interface.
cli = self._cli_ref()
if cli:
cli.invalidate()
def _call_handler(self, handler, key_sequence=None):
was_recording = self.record_macro
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.
cli = event.cli # Can be `None` (In unit-tests only.)
if handler.save_before(event) and cli:
cli.current_buffer.save_to_undo_stack()
# Call handler.
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 just silently ignore that.
pass
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 self.record_macro and was_recording:
self.macro.extend(key_sequence)
def _fix_vi_cursor_position(self, event):
"""
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.)
"""
cli = self._cli_ref()
if cli:
buff = cli.current_buffer
preferred_column = buff.preferred_column
if (ViNavigationMode()(event.cli) 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
class KeyPressEvent(object):
"""
Key press event, delivered to key bindings.
:param input_processor_ref: Weak reference to the `InputProcessor`.
: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, input_processor_ref, arg=None, key_sequence=None,
previous_key_sequence=None, is_repeat=False):
self._input_processor_ref = input_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
def __repr__(self):
return 'KeyPressEvent(arg=%r, key_sequence=%r, is_repeat=%r)' % (
self.arg, self.key_sequence, self.is_repeat)
@property
def data(self):
return self.key_sequence[-1].data
@property
def input_processor(self):
return self._input_processor_ref()
@property
def cli(self):
"""
Command line interface.
"""
return self.input_processor._cli_ref()
@property
def current_buffer(self):
"""
The current buffer.
"""
return self.cli.current_buffer
@property
def arg(self):
"""
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):
"""
True if repetition argument was explicitly provided.
"""
return self._arg is not None
def append_to_arg_count(self, data):
"""
Add digit to the input argument.
:param data: the typed digit as string
"""
assert data in '-0123456789'
current = self._arg
if data == '-':
assert current is None or current == '-'
result = data
elif current is None:
result = data
else:
result = "%s%s" % (current, data)
self.input_processor.arg = result

View File

@@ -0,0 +1,96 @@
"""
DEPRECATED:
Use `prompt_toolkit.key_binding.defaults.load_key_bindings` instead.
:class:`KeyBindingManager` is a utility (or shortcut) for loading all the key
bindings in a key binding registry, with a logic set of filters to quickly to
quickly change from Vi to Emacs key bindings at runtime.
You don't have to use this, but it's practical.
Usage::
manager = KeyBindingManager()
app = Application(key_bindings_registry=manager.registry)
"""
from __future__ import unicode_literals
from .defaults import load_key_bindings
from prompt_toolkit.filters import to_cli_filter
from prompt_toolkit.key_binding.registry import Registry, ConditionalRegistry, MergedRegistry
__all__ = (
'KeyBindingManager',
)
class KeyBindingManager(object):
"""
Utility for loading all key bindings into memory.
:param registry: Optional `Registry` instance.
:param enable_abort_and_exit_bindings: Filter to enable Ctrl-C and Ctrl-D.
:param enable_system_bindings: Filter to enable the system bindings
(meta-! prompt and Control-Z suspension.)
:param enable_search: Filter to enable the search bindings.
:param enable_open_in_editor: Filter to enable open-in-editor.
:param enable_open_in_editor: Filter to enable open-in-editor.
:param enable_extra_page_navigation: Filter for enabling extra page navigation.
(Bindings for up/down scrolling through long pages, like in Emacs or Vi.)
:param enable_auto_suggest_bindings: Filter to enable fish-style suggestions.
:param enable_vi_mode: Deprecated!
"""
def __init__(self,
registry=None, # XXX: not used anymore.
enable_vi_mode=None, # (`enable_vi_mode` is deprecated.)
enable_all=True, #
get_search_state=None,
enable_abort_and_exit_bindings=False,
enable_system_bindings=False,
enable_search=False,
enable_open_in_editor=False,
enable_extra_page_navigation=False,
enable_auto_suggest_bindings=False):
assert registry is None or isinstance(registry, Registry)
assert get_search_state is None or callable(get_search_state)
enable_all = to_cli_filter(enable_all)
defaults = load_key_bindings(
get_search_state=get_search_state,
enable_abort_and_exit_bindings=enable_abort_and_exit_bindings,
enable_system_bindings=enable_system_bindings,
enable_search=enable_search,
enable_open_in_editor=enable_open_in_editor,
enable_extra_page_navigation=enable_extra_page_navigation,
enable_auto_suggest_bindings=enable_auto_suggest_bindings)
# Note, we wrap this whole thing again in a MergedRegistry, because we
# don't want the `enable_all` settings to apply on items that were
# added to the registry as a whole.
self.registry = MergedRegistry([
ConditionalRegistry(defaults, enable_all)
])
@classmethod
def for_prompt(cls, **kw):
"""
Create a ``KeyBindingManager`` with the defaults for an input prompt.
This activates the key bindings for abort/exit (Ctrl-C/Ctrl-D),
incremental search and auto suggestions.
(Not for full screen applications.)
"""
kw.setdefault('enable_abort_and_exit_bindings', True)
kw.setdefault('enable_search', True)
kw.setdefault('enable_auto_suggest_bindings', True)
return cls(**kw)
def reset(self, cli):
# For backwards compatibility.
pass
def get_vi_state(self, cli):
# Deprecated!
return cli.vi_state

View File

@@ -0,0 +1,350 @@
"""
Key bindings registry.
A `Registry` 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::
r = Registry()
@r.add_binding(Keys.ControlX, Keys.ControlC, filter=INSERT)
def handler(event):
# Handle ControlX-ControlC key sequence.
pass
It is also possible to combine multiple registries. We do this in the default
key bindings. There are some registries that contain Emacs bindings, while
others contain the Vi bindings. They are merged together using a
`MergedRegistry`.
We also have a `ConditionalRegistry` object that can enable/disable a group of
key bindings at once.
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
from prompt_toolkit.cache import SimpleCache
from prompt_toolkit.filters import CLIFilter, to_cli_filter, Never
from prompt_toolkit.keys import Key, Keys
from six import text_type, with_metaclass
__all__ = (
'BaseRegistry',
'Registry',
'ConditionalRegistry',
'MergedRegistry',
)
class _Binding(object):
"""
(Immutable binding class.)
"""
def __init__(self, keys, handler, filter=None, eager=None, save_before=None):
assert isinstance(keys, tuple)
assert callable(handler)
assert isinstance(filter, CLIFilter)
assert isinstance(eager, CLIFilter)
assert callable(save_before)
self.keys = keys
self.handler = handler
self.filter = filter
self.eager = eager
self.save_before = save_before
def call(self, event):
return self.handler(event)
def __repr__(self):
return '%s(keys=%r, handler=%r)' % (
self.__class__.__name__, self.keys, self.handler)
class BaseRegistry(with_metaclass(ABCMeta, object)):
"""
Interface for a Registry.
"""
_version = 0 # For cache invalidation.
@abstractmethod
def get_bindings_for_keys(self, keys):
pass
@abstractmethod
def get_bindings_starting_with_keys(self, keys):
pass
# `add_binding` and `remove_binding` don't have to be part of this
# interface.
class Registry(BaseRegistry):
"""
Key binding registry.
"""
def __init__(self):
self.key_bindings = []
self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
self._version = 0 # For cache invalidation.
def _clear_cache(self):
self._version += 1
self._get_bindings_for_keys_cache.clear()
self._get_bindings_starting_with_keys_cache.clear()
def add_binding(self, *keys, **kwargs):
"""
Decorator for annotating key bindings.
:param filter: :class:`~prompt_toolkit.filters.CLIFilter` to determine
when this key binding is active.
:param eager: :class:`~prompt_toolkit.filters.CLIFilter` 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 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.)
"""
filter = to_cli_filter(kwargs.pop('filter', True))
eager = to_cli_filter(kwargs.pop('eager', False))
save_before = kwargs.pop('save_before', lambda e: True)
to_cli_filter(kwargs.pop('invalidate_ui', True)) # Deprecated! (ignored.)
assert not kwargs
assert keys
assert all(isinstance(k, (Key, text_type)) for k in keys), \
'Key bindings should consist of Key and string (unicode) instances.'
assert callable(save_before)
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 registry. It will slow down every key
# press otherwise.
def decorator(func):
return func
else:
def decorator(func):
self.key_bindings.append(
_Binding(keys, func, filter=filter, eager=eager,
save_before=save_before))
self._clear_cache()
return func
return decorator
def remove_binding(self, function):
"""
Remove a key binding.
This expects a function that was given to `add_binding` method as
parameter. Raises `ValueError` when the given function was not
registered before.
"""
assert callable(function)
for b in self.key_bindings:
if b.handler == function:
self.key_bindings.remove(b)
self._clear_cache()
return
# No key binding found for this function. Raise ValueError.
raise ValueError('Binding not found: %r' % (function, ))
def get_bindings_for_keys(self, keys):
"""
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():
result = []
for b in self.key_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' occurences 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):
"""
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():
result = []
for b in self.key_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)
class _AddRemoveMixin(BaseRegistry):
"""
Common part for ConditionalRegistry and MergedRegistry.
"""
def __init__(self):
# `Registry` to be synchronized with all the others.
self._registry2 = Registry()
self._last_version = None
# The 'extra' registry. Mostly for backwards compatibility.
self._extra_registry = Registry()
def _update_cache(self):
raise NotImplementedError
# For backwards, compatibility, we allow adding bindings to both
# ConditionalRegistry and MergedRegistry. This is however not the
# recommended way. Better is to create a new registry and merge them
# together using MergedRegistry.
def add_binding(self, *k, **kw):
return self._extra_registry.add_binding(*k, **kw)
def remove_binding(self, *k, **kw):
return self._extra_registry.remove_binding(*k, **kw)
# Proxy methods to self._registry2.
@property
def key_bindings(self):
self._update_cache()
return self._registry2.key_bindings
@property
def _version(self):
self._update_cache()
return self._last_version
def get_bindings_for_keys(self, *a, **kw):
self._update_cache()
return self._registry2.get_bindings_for_keys(*a, **kw)
def get_bindings_starting_with_keys(self, *a, **kw):
self._update_cache()
return self._registry2.get_bindings_starting_with_keys(*a, **kw)
class ConditionalRegistry(_AddRemoveMixin):
"""
Wraps around a `Registry`. Disable/enable all the key bindings according to
the given (additional) filter.::
@Condition
def setting_is_true(cli):
return True # or False
registy = ConditionalRegistry(registry, 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 `Registry` objects.
:param filter: `CLIFilter` object.
"""
def __init__(self, registry=None, filter=True):
registry = registry or Registry()
assert isinstance(registry, BaseRegistry)
_AddRemoveMixin.__init__(self)
self.registry = registry
self.filter = to_cli_filter(filter)
def _update_cache(self):
" If the original registry was changed. Update our copy version. "
expected_version = (self.registry._version, self._extra_registry._version)
if self._last_version != expected_version:
registry2 = Registry()
# Copy all bindings from `self.registry`, adding our condition.
for reg in (self.registry, self._extra_registry):
for b in reg.key_bindings:
registry2.key_bindings.append(
_Binding(
keys=b.keys,
handler=b.handler,
filter=self.filter & b.filter,
eager=b.eager,
save_before=b.save_before))
self._registry2 = registry2
self._last_version = expected_version
class MergedRegistry(_AddRemoveMixin):
"""
Merge multiple registries of key bindings into one.
This class acts as a proxy to multiple `Registry` objects, but behaves as
if this is just one bigger `Registry`.
:param registries: List of `Registry` objects.
"""
def __init__(self, registries):
assert all(isinstance(r, BaseRegistry) for r in registries)
_AddRemoveMixin.__init__(self)
self.registries = registries
def _update_cache(self):
"""
If one of the original registries was changed. Update our merged
version.
"""
expected_version = (
tuple(r._version for r in self.registries) +
(self._extra_registry._version, ))
if self._last_version != expected_version:
registry2 = Registry()
for reg in self.registries:
registry2.key_bindings.extend(reg.key_bindings)
# Copy all bindings from `self._extra_registry`.
registry2.key_bindings.extend(self._extra_registry.key_bindings)
self._registry2 = registry2
self._last_version = expected_version

View File

@@ -0,0 +1,61 @@
from __future__ import unicode_literals
__all__ = (
'InputMode',
'CharacterFind',
'ViState',
)
class InputMode(object):
INSERT = 'vi-insert'
INSERT_MULTIPLE = 'vi-insert-multiple'
NAVIGATION = 'vi-navigation'
REPLACE = 'vi-replace'
class CharacterFind(object):
def __init__(self, character, backwards=False):
self.character = character
self.backwards = backwards
class ViState(object):
"""
Mutable class to hold the state of the Vi navigation.
"""
def __init__(self):
#: 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 = 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 = None
self.operator_arg = None
#: Named registers. Maps register name (e.g. 'a') to
#: :class:`ClipboardData` instances.
self.named_registers = {}
#: The Vi mode we're currently in to.
self.input_mode = InputMode.INSERT
#: Waiting for digraph.
self.waiting_for_digraph = False
self.digraph_symbol1 = None # (None or a symbol.)
#: When true, make ~ act as an operator.
self.tilde_operator = False
def reset(self, mode=InputMode.INSERT):
"""
Reset state, go back to the given mode. INSERT by default.
"""
# Go back to insert mode.
self.input_mode = mode
self.waiting_for_digraph = False
self.operator_func = None
self.operator_arg = None