#!/usr/bin/env python3 from pexpect_helper import SpawnedProc, control import os import platform import sys # Skip on macOS on Github Actions because it's too resource-starved # and fails this a lot. # # Presumably we still have users on macOS that would notice binding errors if "CI" in os.environ and platform.system() == "Darwin": sys.exit(127) sp = SpawnedProc() send, sendline, sleep, expect_prompt, expect_re, expect_str = ( sp.send, sp.sendline, sp.sleep, sp.expect_prompt, sp.expect_re, sp.expect_str, ) expect_prompt() sendline("bind ctrl-l repaint") expect_prompt() # Clear twice (regression test for #7280). send("\f") expect_prompt(increment=False) send("\f") expect_prompt(increment=False) # Test that kill-selection after selection is cleared doesn't crash sendline("bind ctrl-space begin-selection") expect_prompt() sendline("bind ctrl-w kill-selection end-selection") expect_prompt() send("echo 123") # Send Ctrl-Space using CSI u encoding send("\x1b[32;5u") # Send Ctrl-C to clear the command line send("\x1b[99;5u") # Send Ctrl-W which used to crash send("\x1b[119;5u") sendline("bind --erase ctrl-space ctrl-w") expect_prompt() # Fish should start in default-mode (i.e., emacs) bindings. The default escape # timeout is 30ms. # # Because common CI systems are awful, we have to increase this: sendline("set -g fish_escape_delay_ms 120") expect_prompt("") # Verify the emacs transpose word (\et) behavior using various delays, # including none, after the escape character. # Start by testing with no delay. This should transpose the words. send("echo abc def") send("\033t\r") expect_prompt("\r\n.*def abc\r\n") # emacs transpose words, default timeout: no delay # Now test with a delay > 0 and < the escape timeout. This should transpose # the words. send("echo ghi jkl") send("\033") sleep(0.010) send("t\r") # emacs transpose words, default timeout: short delay expect_prompt("\r\n.*jkl ghi\r\n") # Now test with a delay > the escape timeout. The transposition should not # occur and the "t" should become part of the text that is echoed. send("echo mno pqr") send("\033") sleep(0.250) send("t\r") # emacs transpose words, default timeout: long delay expect_prompt("\r\n.*mno pqrt\r\n") # Now test that exactly the expected bind modes are defined sendline("bind --list-modes") expect_prompt("\r\n.*default", unmatched="Unexpected bind modes") # Test vi key bindings. # This should leave vi mode in the insert state. sendline("set -g fish_key_bindings fish_vi_key_bindings") expect_prompt() # Go through a prompt cycle to let fish catch up, it may be slow due to ASAN sendline("echo success: default escape timeout") expect_prompt( "\r\n.*success: default escape timeout", unmatched="prime vi mode, default timeout" ) send("echo fail: default escape timeout") expect_str("echo fail: default escape timeout") send("\033") # Delay needed to allow fish to transition to vi "normal" mode. The delay is # longer than strictly necessary to let fish catch up as it may be slow due to # ASAN. sleep(0.250) send("ddi") sendline("echo success: default escape timeout") expect_prompt( "\r\n.*success: default escape timeout\r\n", unmatched="vi replace line, default timeout: long delay", ) # Test replacing a single character. send("echo TEXT") send("\033") # Delay needed to allow fish to transition to vi "normal" mode. # Specifically alt+h *is* bound to __fish_man_page, # and I have seen this think that trigger with 300ms. # # The next step is to rip out this test because it's much more pain than it is worth sleep(0.400) send("hhrAi\r") expect_prompt( "\r\n.*TAXT\r\n", unmatched="vi mode replace char, default timeout: long delay" ) # Test deleting characters with 'x'. send("echo MORE-TEXT") send("\033") # Delay needed to allow fish to transition to vi "normal" mode. sleep(0.400) send("xxxxx\r") # vi mode delete char, default timeout: long delay expect_prompt( "\r\n.*MORE\r\n", unmatched="vi mode delete char, default timeout: long delay" ) # Test jumping forward til before a character with t send("echo MORE-TEXT-IS-NICE") send("\033") # Delay needed to allow fish to transition to vi "normal" mode. sleep(0.250) send("0tTD\r") # vi mode forward-jump-till character, default timeout: long delay expect_prompt( "\r\n.*MORE\r\n", unmatched="vi mode forward-jump-till character, default timeout: long delay", ) # DISABLED BECAUSE IT FAILS ON GITHUB ACTIONS # Test jumping backward til before a character with T # send("echo MORE-TEXT-IS-NICE") # send("\033") # # Delay needed to allow fish to transition to vi "normal" mode. # sleep(0.250) # send("TSD\r") # # vi mode backward-jump-till character, default timeout: long delay # expect_prompt( # "\r\n.*MORE-TEXT-IS\r\n", # unmatched="vi mode backward-jump-till character, default timeout: long delay", # ) # Test jumping backward with F and repeating send("echo MORE-TEXT-IS-NICE") send("\033") # Delay needed to allow fish to transition to vi "normal" mode. sleep(0.250) send("F-;D\r") # vi mode backward-jump-to character and repeat, default timeout: long delay expect_prompt( "\r\n.*MORE-TEXT\r\n", unmatched="vi mode backward-jump-to character and repeat, default timeout: long delay", ) # Test jumping backward with F w/reverse jump send("echo MORE-TEXT-IS-NICE") send("\033") # Delay needed to allow fish to transition to vi "normal" mode. sleep(0.250) send("F-F-,D\r") # vi mode backward-jump-to character, and reverse, default timeout: long delay expect_prompt( "\r\n.*MORE-TEXT-IS\r\n", unmatched="vi mode backward-jump-to character, and reverse, default timeout: long delay", ) # Verify that changing the escape timeout has an effect. send("set -g fish_escape_delay_ms 100\r") expect_prompt() send("echo fail: lengthened escape timeout") send("\033") sleep(0.400) send("ddi") sleep(0.25) send("echo success: lengthened escape timeout\r") expect_prompt( "\r\n.*success: lengthened escape timeout\r\n", unmatched="vi replace line, 100ms timeout: long delay", ) # Verify that we don't switch to vi normal mode if we don't wait long enough # after sending escape. send("echo fail: no normal mode") send("\033") sleep(0.010) send("ddi") send("inserted\r") expect_prompt( "\r\n.*fail: no normal modediinserted\r\n", unmatched="vi replace line, 100ms timeout: short delay", ) # Now set it back to speed up the tests - these don't use any escape+thing bindings! send("set -g fish_escape_delay_ms 50\r") expect_prompt() # Test 't' binding that contains non-zero arity function (forward-jump) followed # by another function (and) https://github.com/fish-shell/fish-shell/issues/2357 send("\033") sleep(0.200) send("ddiecho TEXT") expect_str("echo TEXT") send("\033") sleep(0.200) send("hhtTrN\r") expect_prompt("\r\n.*TENT\r\n", unmatched="Couldn't find expected output 'TENT'") # Test sequence key delay send("set -g fish_sequence_key_delay_ms 200\r") expect_prompt() send("bind -M insert jk 'commandline -i foo'\r") expect_prompt() send("echo jk") send("\r") expect_prompt("foo") send("echo j") sleep(0.300) send("k\r") expect_prompt("jk") send("set -e fish_sequence_key_delay_ms\r") expect_prompt() send("echo j") sleep(0.300) send("k\r") expect_prompt("foo") # Test '~' (togglecase-char) # HACK: Deactivated because it keeps failing on CI # send("\033") # sleep(0.100) # send("cc") # sleep(0.50) # send("echo some TExT\033") # sleep(0.300) # send("hh~~bbve~\r") # expect_prompt("\r\n.*SOME TeXT\r\n", unmatched="Couldn't find expected output 'SOME TeXT") send("echo echo") send("\033") sleep(0.200) send("bgU\r") expect_prompt("echo ECHO") send("echo 125") send("\033") sleep(0.200) send("0$i34\r") expect_prompt("echo 12345") # Test operator mode with count (d3w) send("echo one two three four five") send("\033") sleep(0.200) send("0w") send("d3w") sendline("") expect_prompt("echo four five") # Test count before operator (3dw) send("echo one two three four five") send("\033") sleep(0.200) send("0w") send("3dw") sendline("") expect_prompt("echo four five") # Test count on both (2d2w -> 4 words) send("echo one two three four five six") send("\033") sleep(0.200) send("0w") send("2d2w") sendline("") expect_prompt("echo five six") # Test change operator with count (c2w) send("echo one two three") send("\033") sleep(0.200) send("0w") send("c2wREPLACED") sendline("") expect_prompt("echo REPLACED three") # Test escape cancelling count send("echo one two three") send("\033") sleep(0.200) send("0w") send("3") send("\033") sleep(0.100) send("dw") sendline("") expect_prompt("echo two three") # Now test that exactly the expected bind modes are defined sendline("bind --list-modes") expect_prompt( "F\r\nT\r\ndefault\r\nf\r\ninsert\r\noperator\r\nreplace\r\nreplace_one\r\nt\r\nvisual\r\n", unmatched="Unexpected vi bind modes", ) # Test word movements # Test 'w' with underscore - should not jump over single punctuation send("echo abc_def") send("\033") sleep(0.200) send("0wwD\r") # From start, 'w' twice should stop at underscore, delete from there expect_prompt( "\r\n.*abc\r\n", unmatched="vi mode 'w' should stop at single punctuation" ) # Test 'w' with multiple spaces - should skip spaces and land at start of next word send("echo abc def") send("\033") sleep(0.200) send("0wwwD\r") # Skip 'echo', 'abc', 'def', then delete last char expect_prompt("\r\n.*abc de\r\n", unmatched="vi mode 'w' with multiple spaces") # Test 'w' with multiple punctuations - should stop at punctuation group send("echo abc...def") send("\033") sleep(0.200) send("0wwD\r") # Skip 'echo', then 'w' should stop at first '.', delete to end expect_prompt("\r\n.*abc\r\n", unmatched="vi mode 'w' with multiple punctuations") # Test 'diw' when cursor is on space - should delete only spaces send("echo abc def") send("\033") sleep(0.200) send("0wwhdiw\r") # Move to 'def', back to space, delete inner word (spaces only) expect_prompt( "\r\n.*abcdef\r\n", unmatched="vi mode 'diw' on space should delete spaces" ) # Test 'daw' - should delete word and trim trailing space send("echo abc def ghi") send("\033") sleep(0.200) send("0wwdaw\r") # Skip 'echo', move to 'def', delete word with space expect_prompt("\r\n.*abc ghi\r\n", unmatched="vi mode 'daw' should trim trailing space") # Test 'b' backward movement with punctuation - should stop at punctuation send("echo abc_def") send("\033") sleep(0.200) send("bD\r") # From end, 'b' should stop at 'd', delete to end expect_prompt("\r\n.*abc_\r\n", unmatched="vi mode 'b' should stop at punctuation") # Test 'e' end-of-word movement send("echo abc_def") send("\033") sleep(0.200) send("0weD\r") # From start, 'w' to 'abc', 'e' to end of 'abc', delete to end expect_prompt("\r\n.*ab\r\n", unmatched="vi mode 'e' should move to word end") # Test 'W' WORD movement - should skip punctuation within WORD send("echo abc-def ghi") send("\033") sleep(0.200) send("0wWD\r") # From start, 'w' to 'abc', 'W' should skip 'abc-def', delete 'ghi' expect_prompt( "\r\n.*abc-def\r\n", unmatched="vi mode 'W' should treat punctuation as part of WORD", ) # Test 'E' end-of-WORD movement send("echo abc-def ghi") send("\033") sleep(0.200) send("0wED\r") # From start, 'w' to 'abc', 'E' to end of 'abc-def', delete to end expect_prompt("\r\n.*abc-de\r\n", unmatched="vi mode 'E' should move to WORD end") # Test 'B' backward WORD movement send("echo abc-def ghi") send("\033") sleep(0.200) send("BD\r") # From end, 'B' backward to 'ghi', delete to end expect_prompt("\r\n.*abc-def\r\n", unmatched="vi mode 'B' backward WORD movement") # Test 'ge' backward to end of previous word send("echo abc def") send("\033") sleep(0.200) send("0wwgex\r") # Move to 'def', 'ge' to 'c' of 'abc', delete char with 'x' expect_prompt( "\r\n.*ab def\r\n", unmatched="vi mode 'ge' should move to previous word end" ) # Test 'gE' backward to end of previous WORD send("echo abc-def ghi") send("\033") sleep(0.200) send( "0WWgEx\r" ) # Use 'W' to move by WORDs: to 'abc-def', then 'ghi', then 'gE' back to 'f' of 'abc-def', delete char expect_prompt( "\r\n.*abc-de ghi\r\n", unmatched="vi mode 'gE' should move to previous WORD end" ) # Test 'diW' (delete inner WORD) with punctuation send("echo abc-def ghi") send("\033") sleep(0.200) send("0wldiW\r") # Move to 'bc-def', delete inner WORD expect_prompt("\r\n.*ghi\r\n", unmatched="vi mode 'diW' should delete entire WORD") # Test 'daW' (delete a WORD) with punctuation send("echo abc-def ghi") send("\033") sleep(0.200) send("0wldaW\r") # Move to 'bc-def', delete a WORD with space expect_prompt("\r\n.*ghi\r\n", unmatched="vi mode 'daW' should delete WORD and space") # Test Unicode character category separation # In vim, different unicode categories are separated into words send("echo abcあいう") send("\033") sleep(0.200) send("0wwD\r") # Skip 'echo', then from 'a' of 'abc', 'w' should stop at 'あ' expect_prompt( "\r\n.*abc\r\n", unmatched="vi mode 'w' should stop at Unicode category boundary" ) # Test Unicode with multiple categories send("echo abcあいう甲乙") send("\033") sleep(0.200) send("0wwwD\r") # Skip 'echo', 'abc', hiragana, then at kanji, delete to end expect_prompt( "\r\n.*abcあいう\r\n", unmatched="vi mode 'w' should separate hiragana and kanji" ) # Test 'cw' - change word, deletes to start of next word (like vim's 'dw') send("echo abc def") send("\033") sleep(0.200) send("0wcwXXX\r") # Move to 'abc', 'cw' deletes 'abc', type 'XXX' expect_prompt( "\r\n.*XXX def\r\n", unmatched="vi mode 'cw' should delete to start of next word" ) # Test 'ce' - change to end of word (like vim's 'de') send("echo abc def") send("\033") sleep(0.200) send("0wceXXX\r") # Move to 'abc', 'ce' deletes 'abc' (not the space), type 'XXX' expect_prompt( "\r\n.*XXX def\r\n", unmatched="vi mode 'ce' should change to end of word" ) # Test 'cW' - change WORD, deletes to start of next WORD (like vim's 'dW') send("echo abc-def ghi") send("\033") sleep(0.200) send( "0wcWXXX\r" ) # Move to 'abc-def', 'cW' deletes 'abc-def ' (including space), type 'XXX' expect_prompt( "\r\n.*XXXghi\r\n", unmatched="vi mode 'cW' should delete to start of next WORD" ) # Test 'cE' - change to end of WORD (like vim's 'dE') send("echo abc-def ghi") send("\033") sleep(0.200) send( "0wcEXXX\r" ) # Move to 'abc-def', 'cE' deletes 'abc-def' (not the space), type 'XXX' expect_prompt( "\r\n.*XXX ghi\r\n", unmatched="vi mode 'cE' should change to end of WORD" ) # Test running commands on empty line (should not crash) send("\033") sleep(0.200) send("dawdiwdwdedgedgE\r") # run many commands expect_prompt() # Test accepting autosuggestions with w/W sendline("echo test-suggestion test-suggestion") expect_prompt() send("echo te") sleep(0.100) send("\033") # Enter normal mode sleep(0.200) send("w") # forward-word-vi should accept 'st' from autosuggestion expect_str("echo test") send("w") # forward-word-vi should accept '-' expect_str("echo test-") send("w") # forward-word-vi should accept 'suggestion ' from autosuggestion expect_str("echo test-suggestion ") send("W\r") # forward-word-vi should accept 'test-suggestion' from autosuggestion expect_prompt("test-suggestion test-suggestion") # Switch back to regular (emacs mode) key bindings. sendline("set -g fish_key_bindings fish_default_key_bindings") expect_prompt() # Verify the custom escape timeout set earlier is still in effect. sendline("echo fish_escape_delay_ms=$fish_escape_delay_ms") expect_prompt( "\r\n.*fish_escape_delay_ms=50\r\n", unmatched="default-mode custom timeout not set correctly", ) sendline("set -g fish_escape_delay_ms 200") expect_prompt() # Verify the emacs transpose word (\et) behavior using various delays, # including none, after the escape character. # Start by testing with no delay. This should transpose the words. send("echo abc def") send("\033") send("t\r") expect_prompt( "\r\n.*def abc\r\n", unmatched="emacs transpose words fail, 200ms timeout: no delay" ) # Verify special characters, such as \cV, are not intercepted by the kernel # tty driver. Rather, they can be bound and handled by fish. sendline("bind ctrl-v 'echo ctrl-v seen'") expect_prompt() send("\026\r") expect_prompt("ctrl-v seen", unmatched="ctrl-v not seen") send("bind ctrl-o 'echo ctrl-o seen'\r") expect_prompt() send("\017\r") expect_prompt("ctrl-o seen", unmatched="ctrl-o not seen") # \x17 is ctrl-w. send("echo git@github.com:fish-shell/fish-shell") send("\x17\x17\r") expect_prompt("git@github.com:", unmatched="ctrl-w does not stop at :") send("echo git@github.com:fish-shell/fish-shell") send("\x17\x17\x17\r") expect_prompt("git@", unmatched="ctrl-w does not stop at @") sendline("abbr --add foo 'echo foonanana'") expect_prompt() sendline("bind ' ' expand-abbr or self-insert") expect_prompt() send("foo ") expect_str("echo foonanana") send(" banana\r") expect_str(" banana\r") expect_prompt("foonanana banana") # Ensure that nul can be bound properly (#3189). send("bind ctrl-space 'echo nul seen'\r") expect_prompt() send("\0" * 3) # We need to sleep briefly before emitting a newline, because when we execute the # key bindings above we place the tty in external-proc mode (see #7483) and restoring # the mode to shell-mode races with the newline emitted below (i.e. sometimes it may # be echoed). sleep(0.1) send("\r") expect_prompt("nul seen\r\n.*nul seen\r\n.*nul seen", unmatched="nul not seen") # Test self-insert-notfirst. (#6603) # Here the leading 'q's should be stripped, but the trailing ones not. sendline("bind q self-insert-notfirst") expect_prompt() sendline("qqqecho qqq") expect_prompt("qqq", unmatched="Leading qs not stripped") # Test bigword with single-character words. sendline("bind ctrl-g kill-bigword") expect_prompt() send("a b c d\x01") # ctrl-a, move back to the beginning of the line send("\x07") # ctrl-g, kill bigword sendline("echo") expect_prompt("\n.*b c d") # Test that overriding the escape binding works # and does not inhibit other escape sequences (up-arrow in this case). sendline("bind escape 'echo foo'") expect_prompt() send("\x1b") expect_str("foo") send("\x1b[A") expect_str("bind escape 'echo foo'") sendline("bind --erase escape") expect_prompt() send(" a b c d\x01") # ctrl-a, move back to the beginning of the line send("\x07") # ctrl-g, kill bigword sendline("echo") expect_prompt("\n.*b c d") # Check that ctrl-z can be bound sendline('bind ctrl-z "echo bound ctrl-z"') expect_prompt() send("\x1a") expect_str("bound ctrl-z") send("echo foobar") send("\x02\x02\x02") # ctrl-b, backward-char sendline("\x1bu") # alt+u, upcase word expect_prompt("fooBAR") sendline("bind ctrl-z history-prefix-search-backward") expect_prompt() sendline("echo this continues") expect_prompt() send("\x1a") sendline(" with this text") expect_prompt("this continues with this text") sendline( """ bind ctrl-g " commandline --insert 'echo foo ar' commandline -f backward-word commandline --insert b commandline -f backward-char commandline -f backward-char commandline -f delete-char " """.strip() ) expect_prompt() send("\x07") # ctrl-g send("\r") expect_prompt("foobar") # This should do nothing instead of crash sendline("commandline -f backward-jump") expect_prompt() sendline("commandline -f self-insert") expect_prompt() sendline("commandline -f and") expect_prompt() sendline("bind ctrl-g 'sleep 1' history-pager") expect_prompt() send("\x07") # ctrl-g send("\x1b[27u") # escape, to close pager sendline("bind ctrl-g kill-inner-word") expect_prompt() send("echo foo-bar") send("\x07") # ctrl-g sendline("baz") expect_str("foo-barbaz") expect_prompt() sendline("bind ctrl-g kill-a-word") expect_prompt() send("echo foo-bar") send("\x07") # ctrl-g sendline("qux") expect_str("foo-barqux") expect_prompt() sendline("bind ctrl-g backward-word-end") sendline("eco 12" + control("g") + "h") expect_str("12") expect_prompt() # Check that the builtin version of `exit` works # (for obvious reasons this MUST BE LAST) sendline("function myexit; echo exit; exit; end; bind ctrl-z myexit") expect_prompt() send("\x1a") expect_str("exit") for t in range(0, 50): if not sp.spawn.isalive(): break # This is cheesy, but on CI with thread-sanitizer this can be slow enough that the process is still running, so we sleep for a bit. sleep(0.1) else: print("Fish did not exit via binding!") sys.exit(1)