From b7aa32326179907ec4b0aaf952fbef2c72b9a915 Mon Sep 17 00:00:00 2001 From: sharpchen Date: Sun, 6 Sep 2026 04:27:20 +0800 Subject: [PATCH] Fix AcceptAndGetNext skipping history problem --- PSReadLine/History.cs | 8 ++- PSReadLine/Options.cs | 2 +- PSReadLine/ReadLine.cs | 12 ++-- PSReadLine/Render.cs | 4 ++ PSReadLine/{HistoryQueue.cs => RingBuffer.cs} | 55 +++++-------------- test/BasicEditingTest.cs | 6 ++ 6 files changed, 39 insertions(+), 48 deletions(-) rename PSReadLine/{HistoryQueue.cs => RingBuffer.cs} (65%) diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index c1490a230..c0f8cb8cd 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -90,8 +90,8 @@ public class HistoryItem } // History state - private HistoryQueue _history; - private HistoryQueue _recentHistory; + private RingBuffer _history; + private RingBuffer _recentHistory; private HistoryItem _previousHistoryItem; private Dictionary _hashedHistory; private int _currentHistoryIndex; @@ -827,6 +827,10 @@ public static HistoryItem[] GetHistoryItems() enum HistoryMoveCursor { ToEnd, ToBeginning, DontMove } + /// + /// Set current line from the history item `_currentHistoryIndex` pointing to. + /// + /// How to move cursor after line being updated private void UpdateFromHistory(HistoryMoveCursor moveCursor) { string line; diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index ffdf0241c..81e88f544 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -47,7 +47,7 @@ private void SetOptionsInternal(SetPSReadLineOption options) Options.MaximumHistoryCount = options.MaximumHistoryCount; if (_history != null) { - var newHistory = new HistoryQueue(Options.MaximumHistoryCount); + var newHistory = new RingBuffer(Options.MaximumHistoryCount); while (_history.Count > Options.MaximumHistoryCount) { _history.Dequeue(); diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index da890bbba..64ba96cf8 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -79,7 +79,7 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods private static readonly Stopwatch _readkeyStopwatch = new Stopwatch(); // Save a fixed # of keys so we can reconstruct a repro after a crash - private static readonly HistoryQueue _lastNKeys = new HistoryQueue(200); + private static readonly RingBuffer _lastNKeys = new RingBuffer(200); // Tokens etc. private Token[] _tokens; @@ -818,7 +818,11 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) if (_getNextHistoryIndex > 0) { - _currentHistoryIndex = _getNextHistoryIndex; + // This branch is specifically reached after AcceptAndGetNext and the command execution finished, + // a new history item will be enqueued into `_history`. + // If `_history` ring buffer is full, the original history item `_currentHistoryIndex` pointed to + // will be pushed forward after new item is enqueued, so we need to decrement the index by 1 here + _currentHistoryIndex = _getNextHistoryIndex - (_history.Count == _history.Capacity ? 1 : 0); UpdateFromHistory(HistoryMoveCursor.ToEnd); _getNextHistoryIndex = 0; if (_searchHistoryCommandCount > 0) @@ -891,8 +895,8 @@ private void DelayedOneTimeInitialize() _historyFileMutex = new Mutex(false, GetHistorySaveFileMutexName()); - _history = new HistoryQueue(Options.MaximumHistoryCount); - _recentHistory = new HistoryQueue(capacity: 5); + _history = new RingBuffer(Options.MaximumHistoryCount); + _recentHistory = new RingBuffer(capacity: 5); _currentHistoryIndex = 0; bool readHistoryFile = true; diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index 253f7fbef..48d383193 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -171,6 +171,10 @@ struct LineInfoForRendering private ConsoleColor _initialForeground; private ConsoleColor _initialBackground; + + /// + /// Current cursor position + /// private int _current; private int _emphasisStart; private int _emphasisLength; diff --git a/PSReadLine/HistoryQueue.cs b/PSReadLine/RingBuffer.cs similarity index 65% rename from PSReadLine/HistoryQueue.cs rename to PSReadLine/RingBuffer.cs index c7308ce58..da57831d4 100644 --- a/PSReadLine/HistoryQueue.cs +++ b/PSReadLine/RingBuffer.cs @@ -3,7 +3,6 @@ --********************************************************************/ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -12,12 +11,12 @@ namespace Microsoft.PowerShell [ExcludeFromCodeCoverage] internal sealed class QueueDebugView { - private readonly HistoryQueue _queue; + private readonly RingBuffer _queue; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public T[] Items => this._queue.ToArray(); - public QueueDebugView(HistoryQueue queue) + public QueueDebugView(RingBuffer queue) { this._queue = queue ?? throw new ArgumentNullException(nameof(queue)); } @@ -25,50 +24,28 @@ public QueueDebugView(HistoryQueue queue) [DebuggerDisplay("Count = {" + nameof(Count) + "}")] [DebuggerTypeProxy(typeof(QueueDebugView<>))] - internal class HistoryQueue + internal class RingBuffer { private readonly T[] _array; private int _head; private int _tail; - public HistoryQueue(int capacity) + public int Capacity => _array.Length; + public int Count { get; private set; } + + public RingBuffer(int capacity) { - Debug.Assert(capacity > 0); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(capacity, 0); _array = new T[capacity]; _head = _tail = Count = 0; } public void Clear() { - for (int i = 0; i < Count; i++) - { - this[i] = default(T); - } + Array.Clear(_array); _head = _tail = Count = 0; } - public bool Contains(T item) - { - return IndexOf(item) != -1; - } - - public int Count { get; private set; } - - public int IndexOf(T item) - { - // REVIEW: should we use case insensitive here? - var eqComparer = EqualityComparer.Default; - for (int i = 0; i < Count; i++) - { - if (eqComparer.Equals(this[i], item)) - { - return i; - } - } - - return -1; - } - public void Enqueue(T item) { if (Count == _array.Length) @@ -82,10 +59,10 @@ public void Enqueue(T item) public T Dequeue() { - Debug.Assert(Count > 0); + if (Count == 0) throw new InvalidOperationException("RingBuffer is empty"); T obj = _array[_head]; - _array[_head] = default(T); + _array[_head] = default; _head = (_head + 1) % _array.Length; Count -= 1; return obj; @@ -113,14 +90,10 @@ public T[] ToArray() public T this[int index] { get - { - Debug.Assert(index >= 0 && index < Count); - return _array[(_head + index) % _array.Length]; - } - set { - Debug.Assert(index >= 0 && index < Count); - _array[(_head + index) % _array.Length] = value; + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count); + return _array[(_head + index) % _array.Length]; } } } diff --git a/test/BasicEditingTest.cs b/test/BasicEditingTest.cs index ac5c7836f..d5ab31646 100644 --- a/test/BasicEditingTest.cs +++ b/test/BasicEditingTest.cs @@ -312,6 +312,12 @@ public void AcceptAndGetNext() SetHistory("echo 1", "echo 2"); Test("echo 1", Keys("e", _.UpArrow, _.UpArrow, _.Ctrl_o, InputAcceptedNow)); Test("eee", Keys(_.DownArrow, _.DownArrow, "ee", _.Enter)); + + // if _history ring buffer is full, should work properly as well + PSConsoleReadLine.SetOptions(new() { MaximumHistoryCount = 3 }); + SetHistory("echo 1", "echo 2", "echo 3"); + Test("echo 1", Keys("e", _.UpArrow, _.UpArrow, _.UpArrow, _.Ctrl_o, InputAcceptedNow)); + Test("echo 2", Keys(_.Enter)); } [SkippableFact]