On undo after execute, restore the cursor position

Ever since 149594f974 (Initial revision, 2005-09-20), we move the
cursor to the end of the commandline just before executing it.

This is so we can move the cursor to the line below the command line,
so moving the cursor is relevant if one presses enter on say, the
first line of a multi-line commandline.

As mentioned in #10838 and others, it can be useful to restore the
cursor position when recalling commandline from history. Make undo
restore the position where enter was pressed, instead of implicitly
moving the cursor to the end. This allows to quickly correct small
mistakes in large commandlines that failed recently.

This requires a new way of moving the cursor below the command line.
Test changes include unrelated cleanup of history.py.
This commit is contained in:
Johannes Altmanninger 2024-12-21 10:27:52 +01:00
parent f9fb026085
commit 610338cc70
10 changed files with 87 additions and 49 deletions

View file

@ -1,3 +1,36 @@
fish 4.1.0 (released ???)
=========================
Notable improvements and fixes
------------------------------
Deprecations and removed features
---------------------------------
Scripting improvements
----------------------
Interactive improvements
------------------------
New or improved bindings
^^^^^^^^^^^^^^^^^^^^^^^^
- :kbd:`ctrl-z` (undo) after executing a command will restore the previous cursor position instead of placing the cursor at the end of the command line.
Completions
^^^^^^^^^^^
Improved terminal support
^^^^^^^^^^^^^^^^^^^^^^^^^
Other improvements
------------------
For distributors
----------------
--------------
fish 4.0b1 (released December 17, 2024)
=======================================

View file

@ -129,6 +129,10 @@ class Message(object):
"""Return a output message with the given text."""
return Message(Message.DIR_OUTPUT, text, when)
# Sequences for moving the cursor below the commandline. This happens before executing.
MOVE_TO_END: str = r"(?:\r\n|\x1b\[2 q)"
TO_END: str = MOVE_TO_END + r"[^\n]*"
TO_END_SUFFIX: str = r"[^\n]*" + MOVE_TO_END
class SpawnedProc(object):
"""A process, talking to our ptty. This wraps pexpect.spawn.

View file

@ -1972,11 +1972,8 @@ impl<'a> Reader<'a> {
zelf.finish_highlighting_before_exec();
}
// Emit a newline so that the output is on the line after the command.
// But do not emit a newline if the cursor has wrapped onto a new line all its own - see #6826.
if !zelf.screen.cursor_is_wrapped_to_own_line() {
let _ = write_to_fd(b"\n", STDOUT_FILENO);
}
// Move the cursor so that output is on the line after the command.
zelf.screen.move_to_end();
// HACK: If stdin isn't the same terminal as stdout, we just moved the cursor.
// For now, just reset it to the beginning of the line.
@ -3582,7 +3579,6 @@ impl<'a> Reader<'a> {
self.add_to_history();
self.rls_mut().finished = true;
self.update_buff_pos(elt, Some(self.command_line_len()));
true
}

View file

@ -473,6 +473,10 @@ impl Screen {
self.save_status();
}
pub fn move_to_end(&mut self) {
self.r#move(0, self.actual.line_count());
}
/// Resets the screen buffer's internal knowledge about the contents of the screen,
/// abandoning the current line and going to the next line.
/// If clear_to_eos is set,

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
from pexpect_helper import SpawnedProc, TO_END
import os
import platform
import sys
@ -42,7 +42,7 @@ expect_prompt("")
# 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
expect_prompt(TO_END + "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.
@ -51,7 +51,7 @@ send("\033")
sleep(0.010)
send("t\r")
# emacs transpose words, default timeout: short delay
expect_prompt("\r\n.*jkl ghi\r\n")
expect_prompt(TO_END + "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.
@ -60,11 +60,11 @@ send("\033")
sleep(0.250)
send("t\r")
# emacs transpose words, default timeout: long delay
expect_prompt("\r\n.*mno pqrt\r\n")
expect_prompt(TO_END + "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")
expect_prompt(TO_END + "default", unmatched="Unexpected bind modes")
# Test vi key bindings.
# This should leave vi mode in the insert state.
@ -74,7 +74,7 @@ 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"
TO_END + "success: default escape timeout", unmatched="prime vi mode, default timeout"
)
send("echo fail: default escape timeout")
@ -88,7 +88,7 @@ sleep(0.250)
send("ddi")
sendline("echo success: default escape timeout")
expect_prompt(
"\r\n.*success: default escape timeout\r\n",
TO_END + "success: default escape timeout\r\n",
unmatched="vi replace line, default timeout: long delay",
)
@ -103,7 +103,7 @@ send("\033")
sleep(0.400)
send("hhrAi\r")
expect_prompt(
"\r\n.*TAXT\r\n", unmatched="vi mode replace char, default timeout: long delay"
TO_END + "TAXT\r\n", unmatched="vi mode replace char, default timeout: long delay"
)
# Test deleting characters with 'x'.
@ -115,7 +115,7 @@ 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"
TO_END + "MORE\r\n", unmatched="vi mode delete char, default timeout: long delay"
)
# Test jumping forward til before a character with t
@ -127,7 +127,7 @@ send("0tTD\r")
# vi mode forward-jump-till character, default timeout: long delay
expect_prompt(
"\r\n.*MORE\r\n",
TO_END + "MORE\r\n",
unmatched="vi mode forward-jump-till character, default timeout: long delay",
)
@ -140,7 +140,7 @@ expect_prompt(
# send("TSD\r")
# # vi mode backward-jump-till character, default timeout: long delay
# expect_prompt(
# "\r\n.*MORE-TEXT-IS\r\n",
# TO_END + "MORE-TEXT-IS\r\n",
# unmatched="vi mode backward-jump-till character, default timeout: long delay",
# )
@ -152,7 +152,7 @@ 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",
TO_END + "MORE-TEXT\r\n",
unmatched="vi mode backward-jump-to character and repeat, default timeout: long delay",
)
@ -164,7 +164,7 @@ 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",
TO_END + "MORE-TEXT-IS\r\n",
unmatched="vi mode backward-jump-to character, and reverse, default timeout: long delay",
)
@ -179,7 +179,7 @@ send("ddi")
sleep(0.25)
send("echo success: lengthened escape timeout\r")
expect_prompt(
"\r\n.*success: lengthened escape timeout\r\n",
TO_END + "success: lengthened escape timeout\r\n",
unmatched="vi replace line, 100ms timeout: long delay",
)
@ -191,7 +191,7 @@ sleep(0.010)
send("ddi")
send("inserted\r")
expect_prompt(
"\r\n.*fail: no normal modediinserted\r\n",
TO_END + "fail: no normal modediinserted\r\n",
unmatched="vi replace line, 100ms timeout: short delay",
)
@ -208,7 +208,7 @@ 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'")
expect_prompt(TO_END + "TENT\r\n", unmatched="Couldn't find expected output 'TENT'")
# Test sequence key delay
send("set -g fish_sequence_key_delay_ms 200\r")
@ -239,7 +239,7 @@ expect_prompt("foo")
# 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")
# expect_prompt(TO_END + "SOME TeXT\r\n", unmatched="Couldn't find expected output 'SOME TeXT")
# Now test that exactly the expected bind modes are defined
sendline("bind --list-modes")
@ -255,7 +255,7 @@ 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",
TO_END + "fish_escape_delay_ms=50\r\n",
unmatched="default-mode custom timeout not set correctly",
)
@ -270,7 +270,7 @@ 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"
TO_END + "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
@ -301,7 +301,7 @@ expect_prompt()
send("foo ")
expect_str("echo foonanana")
send(" banana\r")
expect_str(" banana\r")
expect_str(" banana")
expect_prompt("foonanana banana")
# Ensure that nul can be bound properly (#3189).
@ -329,7 +329,7 @@ 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")
expect_prompt(TO_END + "b c d")
# Test that overriding the escape binding works
# and does not inhibit other escape sequences (up-arrow in this case).
@ -345,7 +345,7 @@ 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")
expect_prompt(TO_END + "b c d")
# Check that ctrl-z can be bound
sendline('bind ctrl-z "echo bound ctrl-z"')

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
from pexpect_helper import SpawnedProc, TO_END
import os
import sys
import signal
@ -16,7 +16,7 @@ send("set -g fish_key_bindings fish_vi_key_bindings\r")
expect_prompt()
send("echo ready to go\r")
expect_prompt(f"\r\n.*ready to go\r\n")
expect_prompt(TO_END + f"ready to go\r\n")
send(
"function add_change --on-variable fish_bind_mode ; set -g MODE_CHANGES $MODE_CHANGES $fish_bind_mode ; end\r"
)
@ -42,7 +42,7 @@ send("i")
sleep(10 if "CI" in os.environ else 1)
send("echo mode changes: $MODE_CHANGES\r")
expect_prompt("\r\n.*mode changes: default insert default insert\r\n")
expect_prompt(TO_END + "mode changes: default insert default insert\r\n")
# Regression test for #8125.
# Control-C should return us to insert mode.
@ -70,4 +70,4 @@ sleep(timeout)
# We should be back in insert mode now.
send("echo mode changes: $MODE_CHANGES\r")
expect_prompt("\r\n.*mode changes: default insert\r\n")
expect_prompt(TO_END + "mode changes: default insert\r\n")

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
from pexpect_helper import SpawnedProc, TO_END
sp = SpawnedProc()
send, sendline, sleep, expect_prompt, expect_re, expect_str = (
@ -75,6 +75,6 @@ send("echo fo\t")
expect_re("foooo")
send("\x07")
sendline("echo bar")
expect_re("\n.*bar")
expect_re(TO_END + "bar")
sendline("echo fo\t")
expect_re("foooo")

View file

@ -11,7 +11,7 @@
# The history function might pipe output through the user's pager. We don't
# want something like `less` to complicate matters so force the use of `cat`.
from pexpect_helper import SpawnedProc
from pexpect_helper import SpawnedProc, TO_END, TO_END_SUFFIX
import os
os.environ["PAGER"] = "cat"
@ -98,7 +98,7 @@ expect_prompt("echo start1; builtin history; echo end1\r\n")
# ==========
# Delete a single command we recently ran.
sendline("history delete -e -C 'echo hello'")
expect_prompt("history delete -e -C 'echo hello'\r\n")
expect_prompt("history delete -e -C 'echo hello'" + TO_END_SUFFIX)
sendline("echo count hello (history search -e -C 'echo hello' | wc -l | string trim)")
expect_prompt("count hello 0\r\n")
@ -107,12 +107,13 @@ expect_prompt("count hello 0\r\n")
# delete the first entry matched by the prefix search (the most recent command
# sent above that matches).
sendline("history delete -p 'echo hello'")
expect_re("history delete -p 'echo hello'\r\n")
expect_re("\[1\] echo hello AGAIN\r\n")
expect_re("\[2\] echo hello again\r\n\r\n")
expect_re(
"Enter nothing to cancel the delete, or\r\nEnter one or more of the entry IDs or ranges like '5..12', separated by a space.\r\nFor example '7 10..15 35 788..812'.\r\nEnter 'all' to delete all the matching entries.\r\n"
)
expect_re("history delete -p 'echo hello'" + TO_END_SUFFIX)
expect_re("\[1\] echo hello AGAIN" + TO_END_SUFFIX)
expect_re("\[2\] echo hello again" + TO_END_SUFFIX)
expect_re("Enter nothing to cancel the delete, or\r\n")
expect_re("Enter one or more of the entry IDs or ranges like '5..12', separated by a space.\r\n")
expect_re("For example '7 10..15 35 788..812'.\r\n")
expect_re("Enter 'all' to delete all the matching entries.\r\n")
expect_re("Delete which entries\? ")
sendline("1")
expect_prompt('Deleting history entry 1: "echo hello AGAIN"\r\n')
@ -177,7 +178,7 @@ expect_prompt()
sendline("history clear-session")
expect_prompt()
sendline("history search --exact 'echo after' | cat")
expect_prompt("\r\n")
expect_prompt()
# Check history filtering
# We store anything that starts with "echo ephemeral".

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
from pexpect_helper import SpawnedProc, TO_END
sp = SpawnedProc()
send, sendline, sleep, expect_prompt, expect_re, expect_str = (
@ -17,7 +17,7 @@ def expect_read_prompt():
def expect_marker(text):
expect_prompt("\r\n.*@MARKER:" + str(text) + "@\\r\\n")
expect_prompt(TO_END + "@MARKER:" + str(text) + "@\\r\\n")
def print_var_contents(varname, expected):

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
from pexpect_helper import SpawnedProc
from pexpect_helper import SpawnedProc, TO_END
sp = SpawnedProc()
send, sendline, sleep, expect_prompt, expect_re, expect_str = (
@ -22,11 +22,11 @@ expect_prompt("")
# Validate standalone behavior
sendline("status current-commandline")
expect_prompt("\r\n.*status current-commandline\r\n")
expect_prompt(TO_END + "status current-commandline\r\n")
# Validate behavior as part of a command chain
sendline("true 7 && status current-commandline")
expect_prompt("\r\n.*true 7 && status current-commandline\r\n")
expect_prompt(TO_END + "true 7 && status current-commandline\r\n")
# Validate behavior when used in a function
sendline("function report; set -g last_cmdline (status current-commandline); end")
@ -34,7 +34,7 @@ expect_prompt("")
sendline("report 27")
expect_prompt("")
sendline("echo $last_cmdline")
expect_prompt("\r\n.*report 27\r\n")
expect_prompt(TO_END + "report 27\r\n")
# Exit
send("\x04") # <c-d>