From bcad62b442d6dfb213fa111b8f54c3dd7bae1bdd Mon Sep 17 00:00:00 2001 From: sharpchen Date: Mon, 14 Sep 2026 00:44:47 +0800 Subject: [PATCH] Fix AcceptAndGetNext for Forward/ReverseSearchHistory --- PSReadLine/History.cs | 4 ++++ PSReadLine/ReadLine.cs | 27 ++++++++++++++++++++++++--- test/BasicEditingTest.cs | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index c1490a230..19c09fa0a 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -98,6 +98,8 @@ public class HistoryItem private int _getNextHistoryIndex; private int _searchHistoryCommandCount; private int _recallHistoryCommandCount; + // Indicating the count of history operation already executed, + // its value is incremented on SaveCurrentLine which is called on most history recall functions e.g. NextHistory private int _anyHistoryCommandCount; private string _searchHistoryPrefix; // When cycling through history, the current line (not yet added to history) @@ -874,6 +876,8 @@ private void SaveCurrentLine() MaybeReadHistoryFile(); _anyHistoryCommandCount += 1; + // can only have one drafted line instance + // if the drafted line isn't consumed, do not reassign if (_savedCurrentLine.CommandLine == null) { _savedCurrentLine.CommandLine = _buffer.ToString(); diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index da890bbba..c10116dec 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -69,6 +69,7 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods private bool _statusIsErrorMessage; private string _statusLinePrompt; private string _acceptedCommandLine; + // edits of current loaded line, this reference gets updated in UpdateFromHistory private List _edits; private int _editGroupStart; private int _undoEditIndex; @@ -549,6 +550,8 @@ private string InputLoop() var visualSelectionCommandCount = _visualSelectionCommandCount; var moveToLineCommandCount = _moveToLineCommandCount; var moveToEndOfLineCommandCount = _moveToEndOfLineCommandCount; + var currentHistoryIndex = _currentHistoryIndex; + var prevLine = _buffer.ToString(); // We attempt to handle window resizing only once per a keybinding processing, because we assume the // window resizing cannot and shouldn't happen within the processing of a given keybinding. @@ -610,16 +613,34 @@ private string InputLoop() { _recallHistoryCommandCount = 0; } + // if the latest input didn't trigger a SaveCurrentLine call if (anyHistoryCommandCount == _anyHistoryCommandCount) { if (_anyHistoryCommandCount > 0) { - ClearSavedCurrentLine(); - _hashedHistory = null; - _currentHistoryIndex = _history.Count; + // _savedCurrentLine should be consumed only if current line has the same content + if (_savedCurrentLine.CommandLine == _buffer.ToString()) + { + ClearSavedCurrentLine(); + _hashedHistory = null; + _currentHistoryIndex = _history.Count; + } } + // this might not be desired sometimes as non-editing inputs e.g. moving cursor also trigger this _anyHistoryCommandCount = 0; } + // if any edit happened on same history + if (currentHistoryIndex == _currentHistoryIndex && prevLine != _buffer.ToString()) + { + // once edited, we should reset the _savedCurrentLine + ClearSavedCurrentLine(); + // if the current line is recalled and edited, + // it become effectively a new command, we should reset _currentHistoryIndex + if (_currentHistoryIndex != _history.Count) + { + _currentHistoryIndex = _history.Count; + } + } if (visualSelectionCommandCount == _visualSelectionCommandCount && _visualSelectionCommandCount > 0) { _visualSelectionCommandCount = 0; diff --git a/test/BasicEditingTest.cs b/test/BasicEditingTest.cs index ac5c7836f..7699c38ba 100644 --- a/test/BasicEditingTest.cs +++ b/test/BasicEditingTest.cs @@ -327,6 +327,46 @@ public void AcceptAndGetNextWithHistorySearch() Test("zzz", Keys(_.DownArrow, _.Enter)); } + [SkippableFact] + public void AcceptAndGetNextWithInteractiveHistorySearch() + { + TestSetup(KeyMode.Emacs); + // NOTE: this history didn't reach the default capacity + SetHistory("echo 1", "echo 2", "echo 3", "echo 4", "echo 5"); + + // AcceptAndGetNext during interactive search + // NOTE: this should cycle back to "echo 1" after "echo 5" executed + Test("echo 1", Keys(_.Ctrl_r, + "echo 1", + _.Ctrl_o, + CheckThat(() => AssertLineIs("echo 2")), + _.Ctrl_o, + CheckThat(() => AssertLineIs("echo 3")), + _.Ctrl_o, + CheckThat(() => AssertLineIs("echo 4")), + _.Ctrl_o, + CheckThat(() => AssertLineIs("echo 5")), + _.Ctrl_o, InputAcceptedNow + )); + + // accept the result and AcceptAndGetNext + // NOTE: this should cycle back to "echo 1" after "echo 5" executed + Test("echo 1", Keys(_.Ctrl_r, + "echo 1", + _.RightArrow, // any key to accept the match and quit interactive search, see implementation of InteractiveHistorySearchLoop + CheckThat(() => AssertLineIs("echo 1")), + _.Ctrl_o, + CheckThat(() => AssertLineIs("echo 2")), + _.Ctrl_o, + CheckThat(() => AssertLineIs("echo 3")), + _.Ctrl_o, + CheckThat(() => AssertLineIs("echo 4")), + _.Ctrl_o, + CheckThat(() => AssertLineIs("echo 5")), + _.Ctrl_o, InputAcceptedNow + )); + } + [SkippableFact] public void AddLine() {