diff options
author | Jonathan Slenders <jonathan@slenders.be> | 2016-07-15 22:44:37 +0200 |
---|---|---|
committer | Jonathan Slenders <jonathan@slenders.be> | 2016-07-15 22:45:17 +0200 |
commit | 60d03a2d897a39c7a90dab8388cad5977d69e58b (patch) | |
tree | cdec925e610f95887a834b90b613739f697a9614 | |
parent | edf8680191534468a800b51b9d3f81fcda1771ec (diff) |
Grouped named commands (with a GNU Readline name) in separate file for reusability.
-rw-r--r-- | prompt_toolkit/key_binding/bindings/basic.py | 158 | ||||
-rw-r--r-- | prompt_toolkit/key_binding/bindings/emacs.py | 266 | ||||
-rw-r--r-- | prompt_toolkit/key_binding/bindings/named_commands.py | 364 | ||||
-rw-r--r-- | tests/test_cli.py | 99 |
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 |