mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-06-09 03:51:20 -03:00
Multi-line autosuggestions
Unlike other shells, fish tries to make it easy to work with multiline commands. Arguably, it's often better to use a full text editor but the shell can feel more convenient. Spreading long commands into multiple lines can improve readability, especially when there is some semantic grouping (loops, pipelines, command substitutions, quoted parts). Note that in Unix shell, every quoted string can span multiple lines, like Python's triple quotes, so the barrier to writing a multiline command is quite low. However these commands are not autosuggested. From1c4e5cadf2 (commitcomment-150853293)> the reason we don't offer multi-line autosuggestion is that they > can cause the command line to "jump" to make room for the second > and third lines, if you're at the bottom of your terminal. This jumping (as done by nushell for example) might be surprising, especially since there is no limit on the height of a command. Let's maybe avoid this jumping by rendering only however many lines from the autosuggestion can fit on the screen without scrolling. The truncation is hinted at by a single ellipsis ("…") after the last suggested character, just like when a single-line autosuggestion is truncated. (We might want to use something else in future.) To implement this, query for the cursor position after every command, so we know the y-position of the shell prompt within the terminal window (whose height we already know). Also, after we register a terminal window resize, query for the cursor position before doing anything else (until we od #12004, only height changes are relevant), to prevent this scenario: 1. move prompt to bottom of terminal 2. reduce terminal height 3. increase terminal height 4. type a command that triggers a multi-line autosuggestion 5. observe that it would fail to truncate properly As a refresher: when we fail to receive a query response, we always wait for 2 seconds, except if the initial query had also failed, seeb907bc775a(Use a low TTY query timeout only if first query failed, 2025-09-25). If the terminal does not support cursor position report (which is unlikely), show at most 1 line worth of autosuggestion. Note that either way, we don't skip multiline commands anymore. This might make the behavior worse on such terminals, which are probably not important enough. Alternatively, we could use no limit for such terminals, that's probably the better fallback behavior. The only reason I didn't do that yet is to stay a little bit closer to historical behavior. Storing the prompt's position simplifies scrollback-push and the mouse click handler, which no longer need to query. Move some associated code to the screen module. Technically we don't need to query for cursor position if the previous command was empty. But for now we do, trading a potential optimization for andother simplification. Disable this feature in pexpect tests for now, since those are still missing some terminal emulation features.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
#RUN: %fish %s
|
||||
#REQUIRES: command -v tmux
|
||||
|
||||
isolated-tmux-start -C '
|
||||
set -g i 0
|
||||
function fish_prompt
|
||||
set -g i (math $i + 1)
|
||||
printf "$i.%s \n" (seq $i)
|
||||
end
|
||||
history append "\
|
||||
if true
|
||||
echo hello1
|
||||
echo hello2
|
||||
echo hello3
|
||||
echo hello4
|
||||
echo hello5
|
||||
echo hello6
|
||||
echo hello7
|
||||
echo hello8
|
||||
end"
|
||||
'
|
||||
|
||||
isolated-tmux send-keys Enter
|
||||
tmux-sleep
|
||||
isolated-tmux send-keys i
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | string replace -r ^ ^
|
||||
# CHECK: ^1.1
|
||||
# CHECK: ^2.1
|
||||
# CHECK: ^2.2 if true
|
||||
# CHECK: ^ echo hello1
|
||||
# CHECK: ^ echo hello2
|
||||
# CHECK: ^ echo hello3
|
||||
# CHECK: ^ echo hello4
|
||||
# CHECK: ^ echo hello5
|
||||
# CHECK: ^ echo hello6
|
||||
# CHECK: ^ echo hello7…
|
||||
@@ -0,0 +1,36 @@
|
||||
#RUN: %fish %s
|
||||
#REQUIRES: command -v tmux
|
||||
|
||||
isolated-tmux-start -C '
|
||||
tmux resize-window -y 10
|
||||
history append "\
|
||||
if true
|
||||
echo hello1
|
||||
echo hello2
|
||||
echo hello3
|
||||
echo hello4
|
||||
echo hello5
|
||||
end"
|
||||
'
|
||||
|
||||
isolated-tmux \
|
||||
send-keys (for i in (seq 9); echo Enter; end) \; \
|
||||
resize-window -y 5
|
||||
tmux-sleep
|
||||
isolated-tmux \
|
||||
resize-window -y 10
|
||||
tmux-sleep
|
||||
isolated-tmux \
|
||||
send-keys i
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | string replace -r ^ ^
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0>
|
||||
# CHECK: ^prompt 0> if true…
|
||||
162
tests/checks/tmux-autosuggestion-multiline.fish
Normal file
162
tests/checks/tmux-autosuggestion-multiline.fish
Normal file
@@ -0,0 +1,162 @@
|
||||
#RUN: %fish %s
|
||||
#REQUIRES: command -v tmux
|
||||
|
||||
isolated-tmux-start
|
||||
|
||||
isolated-tmux send-keys \
|
||||
'function fish_prompt; echo "prompt> "; end' Enter \
|
||||
'if true' Enter \
|
||||
"echo $(printf %050d)" Enter \
|
||||
"echo $(printf %0100d)" Enter \
|
||||
'e' 'n' 'd' Enter C-l
|
||||
|
||||
isolated-tmux send-keys 'if'
|
||||
tmux-sleep
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed /if/,/end/s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
# CHECK: ^ end
|
||||
|
||||
# Enter does not invalidate autosuggestion.
|
||||
isolated-tmux send-keys ' true' Enter
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed /if/,/end/s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
# CHECK: ^ end
|
||||
|
||||
# Autosuggestion is also computed after Enter.
|
||||
isolated-tmux send-keys C-u C-u C-u 'if true' Enter
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p \; send-keys C-u C-u C-u C-l | sed /if/,/end/s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
# CHECK: ^ end
|
||||
|
||||
# Test smaller windows; only the lines that fit will be shown.
|
||||
isolated-tmux send-keys 'if' \; resize-window -y 4
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Currently, we take either all or nothing from soft-wrapped suggestion-lines.
|
||||
# The ellipsis means that we'll get more lines.
|
||||
isolated-tmux resize-window -y 3
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Test that truncation also works after the resize.
|
||||
isolated-tmux send-keys C-u if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Test that we truncate such that the prompt is never pushed up.
|
||||
isolated-tmux resize-window -y 5 \; send-keys C-u Enter if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt>
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Again, we take all or nothing from a soft-wrapped line.
|
||||
isolated-tmux send-keys C-u Enter if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt>
|
||||
# CHECK: ^prompt>
|
||||
# CHECK: ^prompt> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Now try with a multiline prompt.
|
||||
isolated-tmux send-keys C-u 'function fish_prompt; printf "prompt-line%d/2> \n" 1 2; end' Enter C-l Enter if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2>
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
|
||||
isolated-tmux send-keys C-u \; resize-window -y 6 \; send-keys if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2>
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
isolated-tmux send-keys C-u \; resize-window -y 7 \; send-keys if
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2>
|
||||
# CHECK: ^prompt-line1/2>
|
||||
# CHECK: ^prompt-line2/2> if true
|
||||
# CHECK: ^ echo 00000000000000000000000000000000000000000000000000
|
||||
# CHECK: ^ echo 000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Autosuggestion with a line that barely wraps.
|
||||
isolated-tmux resize-window -x 80 -y 4 \; send-keys C-u \
|
||||
'function fish_prompt; printf "prompt-line1\n> "; end' Enter \
|
||||
b e g i n Enter \
|
||||
# prompt=2 command=2 indent=4
|
||||
": $(printf %072d)" Enter \
|
||||
Enter \
|
||||
Enter \
|
||||
Enter \
|
||||
e n d Enter C-l b e g i n
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^prompt-line1
|
||||
# CHECK: ^> begin
|
||||
# CHECK: ^ : 00000000000000000000000000000000000000000000000000000000000000000000000…
|
||||
# CHECK: ^
|
||||
|
||||
# Autosuggestions on a soft-wrapped commandline don't push the prompt.
|
||||
isolated-tmux resize-window -x 6 -y 4 \; send-keys C-u \
|
||||
'function fish_prompt; printf "> "; end' Enter \
|
||||
'echo l1 \\' Enter 'indented line continuation' Enter \
|
||||
C-l Enter 'e'
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^>
|
||||
# CHECK: ^> ech…
|
||||
# CHECK: ^
|
||||
# CHECK: ^
|
||||
|
||||
isolated-tmux resize-window -x 6 -y 4 \; send-keys C-u \
|
||||
'function fish_prompt; printf "> "; end' Enter \
|
||||
'echo wrapped \\' Enter \
|
||||
'l1 \\' Enter \
|
||||
'l2 \\' Enter \
|
||||
'l3' Enter \
|
||||
Enter Enter \
|
||||
'echo'
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed s/^/^/
|
||||
# CHECK: ^>
|
||||
# CHECK: ^>
|
||||
# CHECK: ^> echo
|
||||
# CHECK: ^ wrap…
|
||||
@@ -10,10 +10,10 @@ isolated-tmux-start -C '
|
||||
printf "> full prompt > "
|
||||
end
|
||||
end
|
||||
bind ctrl-j "set transient true; commandline -f repaint execute"
|
||||
bind ctrl-x "set transient true; commandline -f repaint execute"
|
||||
'
|
||||
|
||||
isolated-tmux send-keys 'echo foo' C-j
|
||||
isolated-tmux send-keys 'echo foo' C-x
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p
|
||||
# CHECK: > echo foo
|
||||
|
||||
@@ -179,6 +179,8 @@ class SpawnedProc(object):
|
||||
self.colorize = sys.stdout.isatty() or env.get("FISH_FORCE_COLOR", "0") == "1"
|
||||
self.messages = []
|
||||
self.start_time = None
|
||||
if "FISH_PEXPECT_TESTS_RUNNING" not in env:
|
||||
env["FISH_PEXPECT_TESTS_RUNNING"] = "1"
|
||||
self.spawn = pexpect.spawn(
|
||||
exe_path, env=env, encoding="utf-8", timeout=timeout, **kwargs
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
env = os.environ.copy()
|
||||
env["TERM"] = "not-dumb"
|
||||
env["FISH_TEST_NO_CURSOR_POSITION_QUERY"] = ""
|
||||
|
||||
sp = SpawnedProc(env=env, scroll_content_up_supported=True)
|
||||
sendline, expect_prompt = sp.sendline, sp.expect_prompt
|
||||
@@ -16,7 +17,8 @@ sp.send_cursor_position_report(y=10, x=5)
|
||||
sp.send_primary_device_attribute()
|
||||
sp.expect_str("\x1b[9S\x1b[9A")
|
||||
|
||||
sp.send(control("l"))
|
||||
sp.send("\r")
|
||||
sp.send_cursor_position_report(y=15, x=5)
|
||||
sp.send_primary_device_attribute()
|
||||
sp.send(control("l"))
|
||||
sp.expect_str("\x1b[14S\x1b[14A")
|
||||
|
||||
Reference in New Issue
Block a user