From 901fd58ba81734a77e4899d7a9b258f42572b4b3 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Wed, 28 Oct 2026 01:24:03 -0400 Subject: [PATCH] Fixed deletion checking bug that could cause misreading of the control+r prompt. --- internal/reader/changes.go | 5 ++++- internal/terminal/emulator_linux_test.go | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal/reader/changes.go b/internal/reader/changes.go index d77955c0..5012fb53 100644 --- a/internal/reader/changes.go +++ b/internal/reader/changes.go @@ -78,7 +78,10 @@ func changes(previous, next screen.Snapshot, cursorEcho bool) []presentation.Mes if len(changedRows) == 1 && scrollStart == scrollEnd { row := changedRows[0] before, after := previous.Line(row), next.Line(row) - if next.Y == previous.Y && next.Y == row && next.X < previous.X && previous.X-next.X <= 4 && len([]rune(next.Text(row))) < len([]rune(previous.Text(row))) { + // A shorter replacement prompt can also move the cursor left. Only + // echo removed text when the surrounding text matches a deletion. + if next.Y == previous.Y && next.Y == row && next.X < previous.X && previous.X-next.X <= 4 && len([]rune(next.Text(row))) < len([]rune(previous.Text(row))) && + slices.Equal(before[:next.X], after[:next.X]) && strings.TrimRight(string(before[previous.X:]), " ") == strings.TrimRight(string(after[next.X:]), " ") { removed := before[next.X:previous.X] if len(removed) == 1 { return []presentation.Message{presentation.Character_message(removed[0])} diff --git a/internal/terminal/emulator_linux_test.go b/internal/terminal/emulator_linux_test.go index 72f5c0b8..4ed78e77 100644 --- a/internal/terminal/emulator_linux_test.go +++ b/internal/terminal/emulator_linux_test.go @@ -140,6 +140,27 @@ func TestPTYAlternateScreenResizeAndScroll(t *testing.T) { } } +func TestPTYReverseSearchPromptReplacement(t *testing.T) { + for _, promptWidth := range []int{2, 23, 24, 25, 26, 40} { + t.Run(fmt.Sprint(promptWidth), func(t *testing.T) { + emulator, err := new_emulator(3, 80) + if err != nil { + t.Fatal(err) + } + defer emulator.close() + engine := reader.New() + emulator.feed([]byte(strings.Repeat("p", promptWidth-2) + "$ ")) + engine.Update(emulator.snapshot(true, false)) + // Bash Readline repaints the prompt and clears any remaining tail. + searchPrompt := "(reverse-i-search)`': " + emulator.feed([]byte("\r" + searchPrompt + "\x1b[K")) + if got := engine.Update(emulator.snapshot(true, false)); len(got) != 1 || got[0].Character != nil || strings.TrimRight(got[0].Text, " ") != strings.TrimRight(searchPrompt, " ") { + t.Fatalf("search prompt: got %+v, want %q", got, searchPrompt) + } + }) + } +} + func TestPTYCarriageReturnEraseAndCursorEcho(t *testing.T) { emulator, err := new_emulator(3, 20) if err != nil {