diff options
Diffstat (limited to '')
-rw-r--r-- | tests/test_cli.py | 941 |
1 files changed, 941 insertions, 0 deletions
diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..3a16e9f --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,941 @@ +""" +These are almost end-to-end tests. They create a Prompt, feed it with some +input and check the result. +""" +from __future__ import annotations + +from functools import partial + +import pytest + +from prompt_toolkit.clipboard import ClipboardData, InMemoryClipboard +from prompt_toolkit.enums import EditingMode +from prompt_toolkit.filters import ViInsertMode +from prompt_toolkit.history import InMemoryHistory +from prompt_toolkit.input.defaults import create_pipe_input +from prompt_toolkit.input.vt100_parser import ANSI_SEQUENCES +from prompt_toolkit.key_binding.bindings.named_commands import prefix_meta +from prompt_toolkit.key_binding.key_bindings import KeyBindings +from prompt_toolkit.output import DummyOutput +from prompt_toolkit.shortcuts import PromptSession + + +def _history(): + h = InMemoryHistory() + h.append_string("line1 first input") + h.append_string("line2 second input") + h.append_string("line3 third input") + return h + + +def _feed_cli_with_input( + text, + editing_mode=EditingMode.EMACS, + clipboard=None, + history=None, + multiline=False, + check_line_ending=True, + key_bindings=None, +): + """ + Create a Prompt, feed it with the given user input and return the CLI + object. + + This returns a (result, Application) tuple. + """ + # If the given text doesn't end with a newline, the interface won't finish. + if check_line_ending: + assert text.endswith("\r") + + with create_pipe_input() as inp: + inp.send_text(text) + session = PromptSession( + input=inp, + output=DummyOutput(), + editing_mode=editing_mode, + history=history, + multiline=multiline, + clipboard=clipboard, + key_bindings=key_bindings, + ) + + _ = session.prompt() + return session.default_buffer.document, session.app + + +def test_simple_text_input(): + # Simple text input, followed by enter. + result, cli = _feed_cli_with_input("hello\r") + assert result.text == "hello" + assert cli.current_buffer.text == "hello" + + +def test_emacs_cursor_movements(): + """ + Test cursor movements with Emacs key bindings. + """ + # ControlA (beginning-of-line) + result, cli = _feed_cli_with_input("hello\x01X\r") + assert result.text == "Xhello" + + # ControlE (end-of-line) + result, cli = _feed_cli_with_input("hello\x01X\x05Y\r") + assert result.text == "XhelloY" + + # ControlH or \b + result, cli = _feed_cli_with_input("hello\x08X\r") + assert result.text == "hellX" + + # Delete. (Left, left, delete) + result, cli = _feed_cli_with_input("hello\x1b[D\x1b[D\x1b[3~\r") + assert result.text == "helo" + + # Left. + result, cli = _feed_cli_with_input("hello\x1b[DX\r") + assert result.text == "hellXo" + + # ControlA, right + result, cli = _feed_cli_with_input("hello\x01\x1b[CX\r") + assert result.text == "hXello" + + # ControlB (backward-char) + result, cli = _feed_cli_with_input("hello\x02X\r") + assert result.text == "hellXo" + + # ControlF (forward-char) + result, cli = _feed_cli_with_input("hello\x01\x06X\r") + assert result.text == "hXello" + + # ControlD: delete after cursor. + result, cli = _feed_cli_with_input("hello\x01\x04\r") + assert result.text == "ello" + + # ControlD at the end of the input ssshould not do anything. + result, cli = _feed_cli_with_input("hello\x04\r") + assert result.text == "hello" + + # Left, Left, ControlK (kill-line) + result, cli = _feed_cli_with_input("hello\x1b[D\x1b[D\x0b\r") + assert result.text == "hel" + + # Left, Left Esc- ControlK (kill-line, but negative) + result, cli = _feed_cli_with_input("hello\x1b[D\x1b[D\x1b-\x0b\r") + assert result.text == "lo" + + # ControlL: should not influence the result. + result, cli = _feed_cli_with_input("hello\x0c\r") + assert result.text == "hello" + + # ControlRight (forward-word) + result, cli = _feed_cli_with_input("hello world\x01X\x1b[1;5CY\r") + assert result.text == "XhelloY world" + + # ContrlolLeft (backward-word) + result, cli = _feed_cli_with_input("hello world\x1b[1;5DY\r") + assert result.text == "hello Yworld" + + # <esc>-f with argument. (forward-word) + result, cli = _feed_cli_with_input("hello world abc def\x01\x1b3\x1bfX\r") + assert result.text == "hello world abcX def" + + # <esc>-f with negative argument. (forward-word) + result, cli = _feed_cli_with_input("hello world abc def\x1b-\x1b3\x1bfX\r") + assert result.text == "hello Xworld abc def" + + # <esc>-b with argument. (backward-word) + result, cli = _feed_cli_with_input("hello world abc def\x1b3\x1bbX\r") + assert result.text == "hello Xworld abc def" + + # <esc>-b with negative argument. (backward-word) + result, cli = _feed_cli_with_input("hello world abc def\x01\x1b-\x1b3\x1bbX\r") + assert result.text == "hello world abc Xdef" + + # ControlW (kill-word / unix-word-rubout) + result, cli = _feed_cli_with_input("hello world\x17\r") + assert result.text == "hello " + assert cli.clipboard.get_data().text == "world" + + result, cli = _feed_cli_with_input("test hello world\x1b2\x17\r") + assert result.text == "test " + + # Escape Backspace (unix-word-rubout) + result, cli = _feed_cli_with_input("hello world\x1b\x7f\r") + assert result.text == "hello " + assert cli.clipboard.get_data().text == "world" + + result, cli = _feed_cli_with_input("hello world\x1b\x08\r") + assert result.text == "hello " + assert cli.clipboard.get_data().text == "world" + + # Backspace (backward-delete-char) + result, cli = _feed_cli_with_input("hello world\x7f\r") + assert result.text == "hello worl" + assert result.cursor_position == len("hello worl") + + result, cli = _feed_cli_with_input("hello world\x08\r") + assert result.text == "hello worl" + assert result.cursor_position == len("hello worl") + + # Delete (delete-char) + result, cli = _feed_cli_with_input("hello world\x01\x1b[3~\r") + assert result.text == "ello world" + assert result.cursor_position == 0 + + # Escape-\\ (delete-horizontal-space) + result, cli = _feed_cli_with_input("hello world\x1b8\x02\x1b\\\r") + assert result.text == "helloworld" + assert result.cursor_position == len("hello") + + +def test_emacs_kill_multiple_words_and_paste(): + # Using control-w twice should place both words on the clipboard. + result, cli = _feed_cli_with_input( + "hello world test\x17\x17--\x19\x19\r" # Twice c-w. Twice c-y. + ) + assert result.text == "hello --world testworld test" + assert cli.clipboard.get_data().text == "world test" + + # Using alt-d twice should place both words on the clipboard. + result, cli = _feed_cli_with_input( + "hello world test" + "\x1bb\x1bb" # Twice left. + "\x1bd\x1bd" # Twice kill-word. + "abc" + "\x19" # Paste. + "\r" + ) + assert result.text == "hello abcworld test" + assert cli.clipboard.get_data().text == "world test" + + +def test_interrupts(): + # ControlC: raise KeyboardInterrupt. + with pytest.raises(KeyboardInterrupt): + result, cli = _feed_cli_with_input("hello\x03\r") + + with pytest.raises(KeyboardInterrupt): + result, cli = _feed_cli_with_input("hello\x03\r") + + # ControlD without any input: raises EOFError. + with pytest.raises(EOFError): + result, cli = _feed_cli_with_input("\x04\r") + + +def test_emacs_yank(): + # ControlY (yank) + c = InMemoryClipboard(ClipboardData("XYZ")) + result, cli = _feed_cli_with_input("hello\x02\x19\r", clipboard=c) + assert result.text == "hellXYZo" + assert result.cursor_position == len("hellXYZ") + + +def test_quoted_insert(): + # ControlQ - ControlB (quoted-insert) + result, cli = _feed_cli_with_input("hello\x11\x02\r") + assert result.text == "hello\x02" + + +def test_transformations(): + # Meta-c (capitalize-word) + result, cli = _feed_cli_with_input("hello world\01\x1bc\r") + assert result.text == "Hello world" + assert result.cursor_position == len("Hello") + + # Meta-u (uppercase-word) + result, cli = _feed_cli_with_input("hello world\01\x1bu\r") + assert result.text == "HELLO world" + assert result.cursor_position == len("Hello") + + # Meta-u (downcase-word) + result, cli = _feed_cli_with_input("HELLO WORLD\01\x1bl\r") + assert result.text == "hello WORLD" + assert result.cursor_position == len("Hello") + + # ControlT (transpose-chars) + result, cli = _feed_cli_with_input("hello\x14\r") + assert result.text == "helol" + assert result.cursor_position == len("hello") + + # Left, Left, Control-T (transpose-chars) + result, cli = _feed_cli_with_input("abcde\x1b[D\x1b[D\x14\r") + assert result.text == "abdce" + assert result.cursor_position == len("abcd") + + +def test_emacs_other_bindings(): + # Transpose characters. + result, cli = _feed_cli_with_input("abcde\x14X\r") # Ctrl-T + assert result.text == "abcedX" + + # Left, Left, Transpose. (This is slightly different.) + result, cli = _feed_cli_with_input("abcde\x1b[D\x1b[D\x14X\r") + assert result.text == "abdcXe" + + # Clear before cursor. + result, cli = _feed_cli_with_input("hello\x1b[D\x1b[D\x15X\r") + assert result.text == "Xlo" + + # unix-word-rubout: delete word before the cursor. + # (ControlW). + result, cli = _feed_cli_with_input("hello world test\x17X\r") + assert result.text == "hello world X" + + result, cli = _feed_cli_with_input("hello world /some/very/long/path\x17X\r") + assert result.text == "hello world X" + + # (with argument.) + result, cli = _feed_cli_with_input("hello world test\x1b2\x17X\r") + assert result.text == "hello X" + + result, cli = _feed_cli_with_input("hello world /some/very/long/path\x1b2\x17X\r") + assert result.text == "hello X" + + # backward-kill-word: delete word before the cursor. + # (Esc-ControlH). + result, cli = _feed_cli_with_input("hello world /some/very/long/path\x1b\x08X\r") + assert result.text == "hello world /some/very/long/X" + + # (with arguments.) + result, cli = _feed_cli_with_input( + "hello world /some/very/long/path\x1b3\x1b\x08X\r" + ) + assert result.text == "hello world /some/very/X" + + +def test_controlx_controlx(): + # At the end: go to the start of the line. + result, cli = _feed_cli_with_input("hello world\x18\x18X\r") + assert result.text == "Xhello world" + assert result.cursor_position == 1 + + # At the start: go to the end of the line. + result, cli = _feed_cli_with_input("hello world\x01\x18\x18X\r") + assert result.text == "hello worldX" + + # Left, Left Control-X Control-X: go to the end of the line. + result, cli = _feed_cli_with_input("hello world\x1b[D\x1b[D\x18\x18X\r") + assert result.text == "hello worldX" + + +def test_emacs_history_bindings(): + # Adding a new item to the history. + history = _history() + result, cli = _feed_cli_with_input("new input\r", history=history) + assert result.text == "new input" + history.get_strings()[-1] == "new input" + + # Go up in history, and accept the last item. + result, cli = _feed_cli_with_input("hello\x1b[A\r", history=history) + assert result.text == "new input" + + # Esc< (beginning-of-history) + result, cli = _feed_cli_with_input("hello\x1b<\r", history=history) + assert result.text == "line1 first input" + + # Esc> (end-of-history) + result, cli = _feed_cli_with_input( + "another item\x1b[A\x1b[a\x1b>\r", history=history + ) + assert result.text == "another item" + + # ControlUp (previous-history) + result, cli = _feed_cli_with_input("\x1b[1;5A\r", history=history) + assert result.text == "another item" + + # Esc< ControlDown (beginning-of-history, next-history) + result, cli = _feed_cli_with_input("\x1b<\x1b[1;5B\r", history=history) + assert result.text == "line2 second input" + + +def test_emacs_reverse_search(): + history = _history() + + # ControlR (reverse-search-history) + result, cli = _feed_cli_with_input("\x12input\r\r", history=history) + assert result.text == "line3 third input" + + # Hitting ControlR twice. + result, cli = _feed_cli_with_input("\x12input\x12\r\r", history=history) + assert result.text == "line2 second input" + + +def test_emacs_arguments(): + """ + Test various combinations of arguments in Emacs mode. + """ + # esc 4 + result, cli = _feed_cli_with_input("\x1b4x\r") + assert result.text == "xxxx" + + # esc 4 4 + result, cli = _feed_cli_with_input("\x1b44x\r") + assert result.text == "x" * 44 + + # esc 4 esc 4 + result, cli = _feed_cli_with_input("\x1b4\x1b4x\r") + assert result.text == "x" * 44 + + # esc - right (-1 position to the right, equals 1 to the left.) + result, cli = _feed_cli_with_input("aaaa\x1b-\x1b[Cbbbb\r") + assert result.text == "aaabbbba" + + # esc - 3 right + result, cli = _feed_cli_with_input("aaaa\x1b-3\x1b[Cbbbb\r") + assert result.text == "abbbbaaa" + + # esc - - - 3 right + result, cli = _feed_cli_with_input("aaaa\x1b---3\x1b[Cbbbb\r") + assert result.text == "abbbbaaa" + + +def test_emacs_arguments_for_all_commands(): + """ + Test all Emacs commands with Meta-[0-9] arguments (both positive and + negative). No one should crash. + """ + for key in ANSI_SEQUENCES: + # Ignore BracketedPaste. This would hang forever, because it waits for + # the end sequence. + if key != "\x1b[200~": + try: + # Note: we add an 'X' after the key, because Ctrl-Q (quoted-insert) + # expects something to follow. We add an additional \r, because + # Ctrl-R and Ctrl-S (reverse-search) expect that. + result, cli = _feed_cli_with_input("hello\x1b4" + key + "X\r\r") + + result, cli = _feed_cli_with_input("hello\x1b-" + key + "X\r\r") + except KeyboardInterrupt: + # This exception should only be raised for Ctrl-C + assert key == "\x03" + + +def test_emacs_kill_ring(): + operations = ( + # abc ControlA ControlK + "abc\x01\x0b" + # def ControlA ControlK + "def\x01\x0b" + # ghi ControlA ControlK + "ghi\x01\x0b" + # ControlY (yank) + "\x19" + ) + + result, cli = _feed_cli_with_input(operations + "\r") + assert result.text == "ghi" + + result, cli = _feed_cli_with_input(operations + "\x1by\r") + assert result.text == "def" + + result, cli = _feed_cli_with_input(operations + "\x1by\x1by\r") + assert result.text == "abc" + + result, cli = _feed_cli_with_input(operations + "\x1by\x1by\x1by\r") + assert result.text == "ghi" + + +def test_emacs_selection(): + # Copy/paste empty selection should not do anything. + operations = ( + "hello" + # Twice left. + "\x1b[D\x1b[D" + # Control-Space + "\x00" + # ControlW (cut) + "\x17" + # ControlY twice. (paste twice) + "\x19\x19\r" + ) + + result, cli = _feed_cli_with_input(operations) + assert result.text == "hello" + + # Copy/paste one character. + operations = ( + "hello" + # Twice left. + "\x1b[D\x1b[D" + # Control-Space + "\x00" + # Right. + "\x1b[C" + # ControlW (cut) + "\x17" + # ControlA (Home). + "\x01" + # ControlY (paste) + "\x19\r" + ) + + result, cli = _feed_cli_with_input(operations) + assert result.text == "lhelo" + + +def test_emacs_insert_comment(): + # Test insert-comment (M-#) binding. + result, cli = _feed_cli_with_input("hello\x1b#", check_line_ending=False) + assert result.text == "#hello" + + result, cli = _feed_cli_with_input( + "hello\rworld\x1b#", check_line_ending=False, multiline=True + ) + assert result.text == "#hello\n#world" + + +def test_emacs_record_macro(): + operations = ( + " " + "\x18(" # Start recording macro. C-X( + "hello" + "\x18)" # Stop recording macro. + " " + "\x18e" # Execute macro. + "\x18e" # Execute macro. + "\r" + ) + + result, cli = _feed_cli_with_input(operations) + assert result.text == " hello hellohello" + + +def test_emacs_nested_macro(): + "Test calling the macro within a macro." + # Calling a macro within a macro should take the previous recording (if one + # exists), not the one that is in progress. + operations = ( + "\x18(" # Start recording macro. C-X( + "hello" + "\x18e" # Execute macro. + "\x18)" # Stop recording macro. + "\x18e" # Execute macro. + "\r" + ) + + result, cli = _feed_cli_with_input(operations) + assert result.text == "hellohello" + + operations = ( + "\x18(" # Start recording macro. C-X( + "hello" + "\x18)" # Stop recording macro. + "\x18(" # Start recording macro. C-X( + "\x18e" # Execute macro. + "world" + "\x18)" # Stop recording macro. + "\x01\x0b" # Delete all (c-a c-k). + "\x18e" # Execute macro. + "\r" + ) + + result, cli = _feed_cli_with_input(operations) + assert result.text == "helloworld" + + +def test_prefix_meta(): + # Test the prefix-meta command. + b = KeyBindings() + b.add("j", "j", filter=ViInsertMode())(prefix_meta) + + result, cli = _feed_cli_with_input( + "hellojjIX\r", key_bindings=b, editing_mode=EditingMode.VI + ) + assert result.text == "Xhello" + + +def test_bracketed_paste(): + result, cli = _feed_cli_with_input("\x1b[200~hello world\x1b[201~\r") + assert result.text == "hello world" + + result, cli = _feed_cli_with_input("\x1b[200~hello\rworld\x1b[201~\x1b\r") + assert result.text == "hello\nworld" + + # With \r\n endings. + result, cli = _feed_cli_with_input("\x1b[200~hello\r\nworld\x1b[201~\x1b\r") + assert result.text == "hello\nworld" + + # With \n endings. + result, cli = _feed_cli_with_input("\x1b[200~hello\nworld\x1b[201~\x1b\r") + assert result.text == "hello\nworld" + + +def test_vi_cursor_movements(): + """ + Test cursor movements with Vi key bindings. + """ + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) + + result, cli = feed("\x1b\r") + assert result.text == "" + assert cli.editing_mode == EditingMode.VI + + # Esc h a X + result, cli = feed("hello\x1bhaX\r") + assert result.text == "hellXo" + + # Esc I X + result, cli = feed("hello\x1bIX\r") + assert result.text == "Xhello" + + # Esc I X + result, cli = feed("hello\x1bIX\r") + assert result.text == "Xhello" + + # Esc 2hiX + result, cli = feed("hello\x1b2hiX\r") + assert result.text == "heXllo" + + # Esc 2h2liX + result, cli = feed("hello\x1b2h2liX\r") + assert result.text == "hellXo" + + # Esc \b\b + result, cli = feed("hello\b\b\r") + assert result.text == "hel" + + # Esc \b\b + result, cli = feed("hello\b\b\r") + assert result.text == "hel" + + # Esc 2h D + result, cli = feed("hello\x1b2hD\r") + assert result.text == "he" + + # Esc 2h rX \r + result, cli = feed("hello\x1b2hrX\r") + assert result.text == "heXlo" + + +def test_vi_operators(): + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) + + # Esc g~0 + result, cli = feed("hello\x1bg~0\r") + assert result.text == "HELLo" + + # Esc gU0 + result, cli = feed("hello\x1bgU0\r") + assert result.text == "HELLo" + + # Esc d0 + result, cli = feed("hello\x1bd0\r") + assert result.text == "o" + + +def test_vi_text_objects(): + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) + + # Esc gUgg + result, cli = feed("hello\x1bgUgg\r") + assert result.text == "HELLO" + + # Esc gUU + result, cli = feed("hello\x1bgUU\r") + assert result.text == "HELLO" + + # Esc di( + result, cli = feed("before(inside)after\x1b8hdi(\r") + assert result.text == "before()after" + + # Esc di[ + result, cli = feed("before[inside]after\x1b8hdi[\r") + assert result.text == "before[]after" + + # Esc da( + result, cli = feed("before(inside)after\x1b8hda(\r") + assert result.text == "beforeafter" + + +def test_vi_digraphs(): + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) + + # C-K o/ + result, cli = feed("hello\x0bo/\r") + assert result.text == "helloø" + + # C-K /o (reversed input.) + result, cli = feed("hello\x0b/o\r") + assert result.text == "helloø" + + # C-K e: + result, cli = feed("hello\x0be:\r") + assert result.text == "helloë" + + # C-K xxy (Unknown digraph.) + result, cli = feed("hello\x0bxxy\r") + assert result.text == "helloy" + + +def test_vi_block_editing(): + "Test Vi Control-V style block insertion." + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) + + operations = ( + # Six lines of text. + "-line1\r-line2\r-line3\r-line4\r-line5\r-line6" + # Go to the second character of the second line. + "\x1bkkkkkkkj0l" + # Enter Visual block mode. + "\x16" + # Go down two more lines. + "jj" + # Go 3 characters to the right. + "lll" + # Go to insert mode. + "insert" # (Will be replaced.) + # Insert stars. + "***" + # Escape again. + "\x1b\r" + ) + + # Control-I + result, cli = feed(operations.replace("insert", "I")) + + assert result.text == "-line1\n-***line2\n-***line3\n-***line4\n-line5\n-line6" + + # Control-A + result, cli = feed(operations.replace("insert", "A")) + + assert result.text == "-line1\n-line***2\n-line***3\n-line***4\n-line5\n-line6" + + +def test_vi_block_editing_empty_lines(): + "Test block editing on empty lines." + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) + + operations = ( + # Six empty lines. + "\r\r\r\r\r" + # Go to beginning of the document. + "\x1bgg" + # Enter Visual block mode. + "\x16" + # Go down two more lines. + "jj" + # Go 3 characters to the right. + "lll" + # Go to insert mode. + "insert" # (Will be replaced.) + # Insert stars. + "***" + # Escape again. + "\x1b\r" + ) + + # Control-I + result, cli = feed(operations.replace("insert", "I")) + + assert result.text == "***\n***\n***\n\n\n" + + # Control-A + result, cli = feed(operations.replace("insert", "A")) + + assert result.text == "***\n***\n***\n\n\n" + + +def test_vi_visual_line_copy(): + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) + + operations = ( + # Three lines of text. + "-line1\r-line2\r-line3\r-line4\r-line5\r-line6" + # Go to the second character of the second line. + "\x1bkkkkkkkj0l" + # Enter Visual linemode. + "V" + # Go down one line. + "j" + # Go 3 characters to the right (should not do much). + "lll" + # Copy this block. + "y" + # Go down one line. + "j" + # Insert block twice. + "2p" + # Escape again. + "\x1b\r" + ) + + result, cli = feed(operations) + + assert ( + result.text + == "-line1\n-line2\n-line3\n-line4\n-line2\n-line3\n-line2\n-line3\n-line5\n-line6" + ) + + +def test_vi_visual_empty_line(): + """ + Test edge case with an empty line in Visual-line mode. + """ + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) + + # 1. Delete first two lines. + operations = ( + # Three lines of text. The middle one is empty. + "hello\r\rworld" + # Go to the start. + "\x1bgg" + # Visual line and move down. + "Vj" + # Delete. + "d\r" + ) + result, cli = feed(operations) + assert result.text == "world" + + # 1. Delete middle line. + operations = ( + # Three lines of text. The middle one is empty. + "hello\r\rworld" + # Go to middle line. + "\x1bggj" + # Delete line + "Vd\r" + ) + + result, cli = feed(operations) + assert result.text == "hello\nworld" + + +def test_vi_character_delete_after_cursor(): + "Test 'x' keypress." + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) + + # Delete one character. + result, cli = feed("abcd\x1bHx\r") + assert result.text == "bcd" + + # Delete multiple character.s + result, cli = feed("abcd\x1bH3x\r") + assert result.text == "d" + + # Delete on empty line. + result, cli = feed("\x1bo\x1bo\x1bggx\r") + assert result.text == "\n\n" + + # Delete multiple on empty line. + result, cli = feed("\x1bo\x1bo\x1bgg10x\r") + assert result.text == "\n\n" + + # Delete multiple on empty line. + result, cli = feed("hello\x1bo\x1bo\x1bgg3x\r") + assert result.text == "lo\n\n" + + +def test_vi_character_delete_before_cursor(): + "Test 'X' keypress." + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI, multiline=True) + + # Delete one character. + result, cli = feed("abcd\x1bX\r") + assert result.text == "abd" + + # Delete multiple character. + result, cli = feed("hello world\x1b3X\r") + assert result.text == "hello wd" + + # Delete multiple character on multiple lines. + result, cli = feed("hello\x1boworld\x1bgg$3X\r") + assert result.text == "ho\nworld" + + result, cli = feed("hello\x1boworld\x1b100X\r") + assert result.text == "hello\nd" + + # Delete on empty line. + result, cli = feed("\x1bo\x1bo\x1b10X\r") + assert result.text == "\n\n" + + +def test_vi_character_paste(): + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) + + # Test 'p' character paste. + result, cli = feed("abcde\x1bhhxp\r") + assert result.text == "abdce" + assert result.cursor_position == 3 + + # Test 'P' character paste. + result, cli = feed("abcde\x1bhhxP\r") + assert result.text == "abcde" + assert result.cursor_position == 2 + + +def test_vi_temp_navigation_mode(): + """ + Test c-o binding: go for one action into navigation mode. + """ + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) + + result, cli = feed("abcde" "\x0f" "3h" "x\r") # c-o # 3 times to the left. + assert result.text == "axbcde" + assert result.cursor_position == 2 + + result, cli = feed("abcde" "\x0f" "b" "x\r") # c-o # One word backwards. + assert result.text == "xabcde" + assert result.cursor_position == 1 + + # In replace mode + result, cli = feed( + "abcdef" + "\x1b" # Navigation mode. + "0l" # Start of line, one character to the right. + "R" # Replace mode + "78" + "\x0f" # c-o + "l" # One character forwards. + "9\r" + ) + assert result.text == "a78d9f" + assert result.cursor_position == 5 + + +def test_vi_macros(): + feed = partial(_feed_cli_with_input, editing_mode=EditingMode.VI) + + # Record and execute macro. + result, cli = feed("\x1bqcahello\x1bq@c\r") + assert result.text == "hellohello" + assert result.cursor_position == 9 + + # Running unknown macro. + result, cli = feed("\x1b@d\r") + assert result.text == "" + assert result.cursor_position == 0 + + # When a macro is called within a macro. + # It shouldn't result in eternal recursion. + result, cli = feed("\x1bqxahello\x1b@xq@x\r") + assert result.text == "hellohello" + assert result.cursor_position == 9 + + # Nested macros. + result, cli = feed( + # Define macro 'x'. + "\x1bqxahello\x1bq" + # Define macro 'y' which calls 'x'. + "qya\x1b@xaworld\x1bq" + # Delete line. + "2dd" + # Execute 'y' + "@y\r" + ) + + assert result.text == "helloworld" + + +def test_accept_default(): + """ + Test `prompt(accept_default=True)`. + """ + with create_pipe_input() as inp: + session = PromptSession(input=inp, output=DummyOutput()) + result = session.prompt(default="hello", accept_default=True) + assert result == "hello" + + # Test calling prompt() for a second time. (We had an issue where the + # prompt reset between calls happened at the wrong time, breaking this.) + result = session.prompt(default="world", accept_default=True) + assert result == "world" |