summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJonathan Slenders <jonathan@slenders.be>2016-07-15 22:44:37 +0200
committerJonathan Slenders <jonathan@slenders.be>2016-07-15 22:45:17 +0200
commit60d03a2d897a39c7a90dab8388cad5977d69e58b (patch)
treecdec925e610f95887a834b90b613739f697a9614
parentedf8680191534468a800b51b9d3f81fcda1771ec (diff)
Grouped named commands (with a GNU Readline name) in separate file for reusability.
-rw-r--r--prompt_toolkit/key_binding/bindings/basic.py158
-rw-r--r--prompt_toolkit/key_binding/bindings/emacs.py266
-rw-r--r--prompt_toolkit/key_binding/bindings/named_commands.py364
-rw-r--r--tests/test_cli.py99
4 files changed, 538 insertions, 349 deletions
diff --git a/prompt_toolkit/key_binding/bindings/basic.py b/prompt_toolkit/key_binding/bindings/basic.py
index be89cbde..89f3e109 100644
--- a/prompt_toolkit/key_binding/bindings/basic.py
+++ b/prompt_toolkit/key_binding/bindings/basic.py
@@ -9,8 +9,8 @@ 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 .completion import generate_completions
from .utils import create_handle_decorator
+from .named_commands import get_by_name
__all__ = (
@@ -115,27 +115,36 @@ def load_basic_bindings(registry, filter=Always()):
"""
pass
- @handle(Keys.Home)
- def _(event):
- b = event.current_buffer
- b.cursor_position += b.document.get_start_of_line_position()
-
- @handle(Keys.End)
- def _(event):
- b = event.current_buffer
- b.cursor_position += b.document.get_end_of_line_position()
+ # 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.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('complete'))
+
+ 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)
- def _(event):
- " Delete text before cursor. "
- event.current_buffer.delete(event.arg)
-
- # Tab completion. (ControlI == Tab)
- handle(Keys.ControlI, filter=insert_mode)(generate_completions)
+ handle(Keys.ControlD, filter=text_before_cursor & insert_mode)(get_by_name('delete-char'))
@handle(Keys.BackTab, filter=insert_mode)
def _(event):
@@ -158,86 +167,7 @@ def load_basic_bindings(registry, filter=Always()):
buff = event.current_buffer
buff.accept_action.validate_and_handle(event.cli, buff)
- @handle(Keys.ControlK, filter=insert_mode)
- def _(event):
- buffer = event.current_buffer
- deleted = buffer.delete(count=buffer.document.get_end_of_line_position())
- event.cli.clipboard.set_text(deleted)
-
- @handle(Keys.ControlT, filter=insert_mode)
- def _(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()
-
- @handle(Keys.ControlU, filter=insert_mode)
- def _(event):
- """
- Clears the line before the cursor position. If you are at the end of
- the line, clears the entire line.
- """
- buffer = event.current_buffer
- deleted = buffer.delete_before_cursor(count=-buffer.document.get_start_of_line_position())
- event.cli.clipboard.set_text(deleted)
-
- @handle(Keys.ControlW, filter=insert_mode)
- def _(event):
- """
- Delete the word before the cursor.
- """
- buffer = event.current_buffer
- pos = buffer.document.find_start_of_previous_word(count=event.arg)
-
- 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 = - buffer.cursor_position
-
- if pos:
- deleted = buffer.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()
-
- @handle(Keys.PageUp, filter= ~has_selection)
- @handle(Keys.ControlUp)
- def _(event):
- event.current_buffer.history_backward()
-
- @handle(Keys.PageDown, filter= ~has_selection)
- @handle(Keys.ControlDown)
- def _(event):
- event.current_buffer.history_forward()
-
- @handle(Keys.Left)
- def _(event):
- buffer = event.current_buffer
- buffer.cursor_position += buffer.document.get_cursor_left_position(count=event.arg)
-
- @handle(Keys.Right)
- def _(event):
- buffer = event.current_buffer
- buffer.cursor_position += buffer.document.get_cursor_right_position(count=event.arg)
+ # Delete the word before the cursor.
@handle(Keys.Up, filter= ~has_selection)
def _(event):
@@ -255,40 +185,13 @@ def load_basic_bindings(registry, filter=Always()):
def _(event):
event.current_buffer.cursor_down(count=event.arg)
- @handle(Keys.ControlH, filter=insert_mode, save_before=if_no_repeat)
- def _(event):
- " Backspace: delete before cursor. "
- deleted = event.current_buffer.delete_before_cursor(count=event.arg)
- if not deleted:
- event.cli.output.bell()
-
- @handle(Keys.Delete, filter=insert_mode, save_before=if_no_repeat)
- @handle(Keys.ShiftDelete, filter=insert_mode, save_before=if_no_repeat)
- def _(event):
- deleted = event.current_buffer.delete(count=event.arg)
-
- if not deleted:
- event.cli.output.bell()
-
@handle(Keys.Delete, filter=has_selection)
def _(event):
data = event.current_buffer.cut_selection()
event.cli.clipboard.set_data(data)
- @handle(Keys.Any, filter=insert_mode, save_before=if_no_repeat)
- def _(event):
- """
- Insert data at cursor position.
- """
- event.current_buffer.insert_text(event.data * event.arg)
-
# Global bindings. These are never disabled and don't include the default filter.
- @handle(Keys.ControlL)
- def _(event):
- " Clear whole screen and redraw. "
- event.cli.renderer.clear()
-
@handle(Keys.ControlZ)
def _(event):
"""
@@ -441,10 +344,7 @@ def load_abort_and_exit_bindings(registry, filter=Always()):
return (cli.current_buffer_name == DEFAULT_BUFFER and
not cli.current_buffer.text)
- @handle(Keys.ControlD, filter=ctrl_d_condition)
- def _(event):
- " Exit on Control-D when the input is empty. "
- event.cli.exit()
+ handle(Keys.ControlD, filter=ctrl_d_condition)(get_by_name('end-of-file'))
def load_basic_system_bindings(registry, filter=Always()):
diff --git a/prompt_toolkit/key_binding/bindings/emacs.py b/prompt_toolkit/key_binding/bindings/emacs.py
index ba2358cf..5dec506b 100644
--- a/prompt_toolkit/key_binding/bindings/emacs.py
+++ b/prompt_toolkit/key_binding/bindings/emacs.py
@@ -8,8 +8,7 @@ from prompt_toolkit.completion import CompleteEvent
from .utils import create_handle_decorator
from .scroll import scroll_page_up, scroll_page_down
-
-from six.moves import range
+from .named_commands import get_by_name
__all__ = (
'load_emacs_bindings',
@@ -43,71 +42,57 @@ def load_emacs_bindings(registry, filter=Always()):
"""
pass
- @handle(Keys.ControlA)
- def _(event):
- """
- Start of line.
- """
- buffer = event.current_buffer
- buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=False)
-
- @handle(Keys.ControlB)
- def _(event):
- """
- Character back.
- """
- buffer = event.current_buffer
- buffer.cursor_position += buffer.document.get_cursor_left_position(count=event.arg)
-
- @handle(Keys.ControlE)
- def _(event):
- """
- End of line.
- """
- buffer = event.current_buffer
- buffer.cursor_position += buffer.document.get_end_of_line_position()
-
- @handle(Keys.ControlF)
- def _(event):
- """
- Character forward.
- """
- buffer = event.current_buffer
- buffer.cursor_position += buffer.document.get_cursor_right_position(count=event.arg)
+ 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, Keys.Backspace, filter=insert_mode)(get_by_name('unix-word-rubout'))
+ 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.ControlN, filter= ~has_selection)
def _(event):
- """
- Next line.
- """
+ " Next line. "
event.current_buffer.auto_down()
@handle(Keys.ControlN, filter=has_selection)
def _(event):
- """
- Next line.
- """
+ " Next line (but don't cycle through history.) "
event.current_buffer.cursor_down()
@handle(Keys.ControlO, filter=insert_mode)
def _(event):
- """
- Insert newline, but don't move the cursor.
- """
+ " Insert newline, but don't move the cursor. "
event.current_buffer.insert_text('\n', move_cursor=False)
@handle(Keys.ControlP, filter= ~has_selection)
def _(event):
- """
- Previous line.
- """
+ " Previous line. "
event.current_buffer.auto_up(count=event.arg)
@handle(Keys.ControlP, filter=has_selection)
def _(event):
- """
- Previous line.
- """
+ " Previous line. "
event.current_buffer.cursor_up(count=event.arg)
@handle(Keys.ControlQ, Keys.Any, filter= ~has_selection)
@@ -121,22 +106,6 @@ def load_emacs_bindings(registry, filter=Always()):
"""
event.current_buffer.insert_text(event.data, overwrite=False)
- @handle(Keys.ControlY, filter=insert_mode)
- @handle(Keys.ControlX, 'r', 'y', filter=insert_mode)
- def _(event):
- """
- Paste before cursor.
- """
- event.current_buffer.paste_clipboard_data(
- event.cli.clipboard.get_data(), count=event.arg, before=True)
-
- @handle(Keys.ControlUnderscore, save_before=(lambda e: False), filter=insert_mode)
- def _(event):
- """
- Undo.
- """
- event.current_buffer.undo()
-
def handle_digit(c):
"""
Handle Alt + digit in the `meta_digit` method.
@@ -158,123 +127,38 @@ def load_emacs_bindings(registry, filter=Always()):
is_returnable = Condition(
lambda cli: cli.current_buffer.accept_action.is_returnable)
- @handle(Keys.Escape, Keys.ControlJ, filter=insert_mode & is_returnable)
- def _(event):
- """
- Meta + Newline: always accept input.
- """
- b = event.current_buffer
- b.accept_action.validate_and_handle(event.cli, b)
+ # 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)
- @handle(Keys.ControlSquareClose, Keys.Any)
- def _(event):
- """
- When Ctl-] + a character is pressed. go to that character.
- """
- match = event.current_buffer.document.find(event.data, in_current_line=True, count=(event.arg))
if match is not None:
- event.current_buffer.cursor_position += match
+ buff.cursor_position += match
- @handle(Keys.Escape, Keys.Backspace, filter=insert_mode)
+ @handle(Keys.ControlSquareClose, Keys.Any)
def _(event):
- """
- Delete word backwards.
- """
- buffer = event.current_buffer
- pos = buffer.document.find_start_of_previous_word(count=event.arg)
-
- if pos is None:
- # Nothing found. Only whitespace before the cursor?
- pos = - buffer.cursor_position
+ " When Ctl-] + a character is pressed. go to that character. "
+ character_search(event.current_buffer, event.data, event.arg)
- if pos:
- deleted = buffer.delete_before_cursor(count=-pos)
- event.cli.clipboard.set_text(deleted)
-
- @handle(Keys.ControlDelete, filter=insert_mode)
+ @handle(Keys.Escape, Keys.ControlSquareClose, Keys.Any)
def _(event):
- """
- Delete word after cursor.
- """
- 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)
+ " Like Ctl-], but backwards. "
+ character_search(event.current_buffer, event.data, -event.arg)
@handle(Keys.Escape, 'a')
def _(event):
- """
- Previous sentence.
- """
+ " Previous sentence. "
# TODO:
- pass
-
- @handle(Keys.Escape, 'c', filter=insert_mode)
- def _(event):
- """
- Capitalize the current (or following) word.
- """
- buffer = event.current_buffer
-
- for i in range(event.arg):
- pos = buffer.document.find_next_word_ending()
- words = buffer.document.text_after_cursor[:pos]
- buffer.insert_text(words.title(), overwrite=True)
-
- @handle(Keys.Escape, 'd', filter=insert_mode)
- def _(event):
- """
- Delete word forwards.
- """
- buffer = event.current_buffer
- pos = buffer.document.find_next_word_ending(count=event.arg)
-
- if pos:
- deleted = buffer.delete(count=pos)
- event.cli.clipboard.set_text(deleted)
@handle(Keys.Escape, 'e')
def _(event):
- """ Move to end of sentence. """
+ " Move to end of sentence. "
# TODO:
- pass
-
- @handle(Keys.Escape, 'f')
- @handle(Keys.ControlRight)
- def _(event):
- """
- Cursor to end of next word.
- """
- buffer= event.current_buffer
- pos = buffer.document.find_next_word_ending(count=event.arg)
-
- if pos:
- buffer.cursor_position += pos
-
- @handle(Keys.Escape, 'b')
- @handle(Keys.ControlLeft)
- def _(event):
- """
- Cursor to start of previous word.
- """
- buffer = event.current_buffer
- pos = buffer.document.find_previous_word_beginning(count=event.arg)
- if pos:
- buffer.cursor_position += pos
-
- @handle(Keys.Escape, 'l', filter=insert_mode)
- def _(event):
- """
- Lowercase the current (or following) word.
- """
- buffer = event.current_buffer
-
- for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!!
- pos = buffer.document.find_next_word_ending()
- words = buffer.document.text_after_cursor[:pos]
- buffer.insert_text(words.lower(), overwrite=True)
@handle(Keys.Escape, 't', filter=insert_mode)
def _(event):
@@ -283,18 +167,6 @@ def load_emacs_bindings(registry, filter=Always()):
"""
# TODO
- @handle(Keys.Escape, 'u', filter=insert_mode)
- def _(event):
- """
- Uppercase the current (or following) word.
- """
- buffer = event.current_buffer
-
- for i in range(event.arg):
- pos = buffer.document.find_next_word_ending()
- words = buffer.document.text_after_cursor[:pos]
- buffer.insert_text(words.upper(), overwrite=True)
-
@handle(Keys.Escape, '.', filter=insert_mode)
def _(event):
"""
@@ -302,22 +174,6 @@ def load_emacs_bindings(registry, filter=Always()):
"""
# TODO
- @handle(Keys.Escape, '\\', filter=insert_mode)
- def _(event):
- """
- Delete all spaces and tabs around point.
- (delete-horizontal-space)
- """
- 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)
-
@handle(Keys.Escape, '*', filter=insert_mode)
def _(event):
"""
@@ -333,10 +189,6 @@ def load_emacs_bindings(registry, filter=Always()):
text_to_insert = ' '.join(c.text for c in completions)
buff.insert_text(text_to_insert)
- @handle(Keys.ControlX, Keys.ControlU, save_before=(lambda e: False), filter=insert_mode)
- def _(event):
- event.current_buffer.undo()
-
@handle(Keys.ControlX, Keys.ControlX)
def _(event):
"""
@@ -392,22 +244,6 @@ def load_emacs_bindings(registry, filter=Always()):
data = event.current_buffer.copy_selection()
event.cli.clipboard.set_data(data)
- @handle(Keys.Escape, '<', filter= ~has_selection)
- def _(event):
- """
- Move to the first line in the history.
- """
- event.current_buffer.go_to_history(0)
-
- @handle(Keys.Escape, '>', filter= ~has_selection)
- def _(event):
- """
- Move to the end of the input history.
- This is the line we are editing.
- """
- buffer = event.current_buffer
- buffer.go_to_history(len(buffer._working_lines) - 1)
-
@handle(Keys.Escape, Keys.Left)
def _(event):
"""
diff --git a/prompt_toolkit/key_binding/bindings/named_commands.py b/prompt_toolkit/key_binding/bindings/named_commands.py
new file mode 100644
index 00000000..6aaa0c40
--- /dev/null
+++ b/prompt_toolkit/key_binding/bindings/named_commands.py
@@ -0,0 +1,364 @@
+"""
+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 six.moves import range
+import six
+
+from .completion import generate_completions
+
+__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.count)
+
+
+
+@register('next-history')
+def next_history(event):
+ " Move `forward' through the history list, fetching the next command. "
+ event.current_buffer.history_forward(count=event.count)
+
+
+@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. "
+ 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)
+
+#
+# Killing and yanking.
+#
+
+@register('kill-line')
+def kill_line(event):
+ """
+ Kill the text from the cursor to the end of the line.
+ """
+ buff = event.current_buffer
+ 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')
+@register('backward-kill-word') # XXX: backward-kill-word is actually slightly different.
+def unix_word_rubout(event):
+ """
+ Kill the word behind point. Word boundaries are the same as backward-word.
+ """
+ buff = event.current_buffer
+ pos = buff.document.find_start_of_previous_word(count=event.arg)
+
+ 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('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
+ deleted = buff.delete_before_cursor(count=-buff.document.get_start_of_line_position())
+ event.cli.clipboard.set_text(deleted)
+
+@register('yank')
+@register('yank-pop')
+def yank(event):
+ """
+ Paste before cursor.
+ """
+ event.current_buffer.paste_clipboard_data(
+ event.cli.clipboard.get_data(), count=event.arg, before=True)
+
+#
+# Completion.
+#
+
+@register('complete')
+def complete(event):
+ generate_completions(event)
+
+
+#
+# Miscellaneous Commands.
+#
+
+@register('undo')
+def undo(event):
+ " Incremental undo. "
+ event.current_buffer.undo()
diff --git a/tests/test_cli.py b/tests/test_cli.py
index b31a799f..d4792282 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -10,10 +10,11 @@ from prompt_toolkit.eventloop.posix import PosixEventLoop
from prompt_toolkit.input import PipeInput
from prompt_toolkit.interface import CommandLineInterface
from prompt_toolkit.output import DummyOutput
+from prompt_toolkit.clipboard import InMemoryClipboard, ClipboardData
from functools import partial
-def _feed_cli_with_input(text, editing_mode=EditingMode.EMACS):
+def _feed_cli_with_input(text, editing_mode=EditingMode.EMACS, clipboard=None):
"""
Create a CommandLineInterface, feed it with the given user input and return
the CLI object.
@@ -28,7 +29,10 @@ def _feed_cli_with_input(text, editing_mode=EditingMode.EMACS):
inp = PipeInput()
inp.send_text(text)
cli = CommandLineInterface(
- application=Application(editing_mode=editing_mode),
+ application=Application(
+ editing_mode=editing_mode,
+ clipboard=clipboard or InMemoryClipboard(),
+ ),
eventloop=loop,
input=inp,
output=DummyOutput())
@@ -50,10 +54,14 @@ def test_emacs_cursor_movements():
"""
Test cursor movements with Emacs key bindings.
"""
- # ControlA
+ # ControlA (beginning-of-line)
result, cli = _feed_cli_with_input('hello\x01X\n')
assert result.text == 'Xhello'
+ # ControlE (end-of-line)
+ result, cli = _feed_cli_with_input('hello\x01X\x05Y\n')
+ assert result.text == 'XhelloY'
+
# ControlH or \b
result, cli = _feed_cli_with_input('hello\x08X\n')
assert result.text == 'hellX'
@@ -74,10 +82,14 @@ def test_emacs_cursor_movements():
result, cli = _feed_cli_with_input('hello\x01\x1b[CX\n')
assert result.text == 'hXello'
- # ControlB (Emacs cursor left.)
+ # ControlB (backward-char)
result, cli = _feed_cli_with_input('hello\x02X\n')
assert result.text == 'hellXo'
+ # ControlF (forward-char)
+ result, cli = _feed_cli_with_input('hello\x01\x06X\n')
+ assert result.text == 'hXello'
+
# ControlC: ignored by default, unless the prompt-bindings are loaded.
result, cli = _feed_cli_with_input('hello\x03\n')
assert result.text == 'hello'
@@ -86,7 +98,7 @@ def test_emacs_cursor_movements():
result, cli = _feed_cli_with_input('hello\x04\n')
assert result.text == 'hello'
- # Left, Left, ControlK
+ # Left, Left, ControlK (kill-line)
result, cli = _feed_cli_with_input('hello\x1b[D\x1b[D\x0b\n')
assert result.text == 'hel'
@@ -94,6 +106,83 @@ def test_emacs_cursor_movements():
result, cli = _feed_cli_with_input('hello\x0c\n')
assert result.text == 'hello'
+ # ControlRight (forward-word)
+ result, cli = _feed_cli_with_input('hello world\x01X\x1b[1;5CY\n')
+ assert result.text == 'XhelloY world'
+
+ # ContrlolLeft (backward-word)
+ result, cli = _feed_cli_with_input('hello world\x1b[1;5DY\n')
+ assert result.text == 'hello Yworld'
+
+ # ControlW (kill-word / unix-word-rubout)
+ result, cli = _feed_cli_with_input('hello world\x17\n')
+ assert result.text == 'hello '
+ assert cli.clipboard.get_data().text == 'world'
+
+ result, cli = _feed_cli_with_input('test hello world\x1b2\x17\n')
+ assert result.text == 'test '
+
+ # Escape Backspace (unix-word-rubout)
+ result, cli = _feed_cli_with_input('hello world\x1b\x08\n')
+ assert result.text == 'hello '
+ assert cli.clipboard.get_data().text == 'world'
+
+ # Backspace (backward-delete-char)
+ result, cli = _feed_cli_with_input('hello w