diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index da890bbb..d075f23a 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -784,6 +784,7 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) _parseErrors = null; _inputAccepted = false; _initialX = _console.CursorLeft; + _initialPromptCells = _initialX; _initialY = _console.CursorTop; _initialForeground = _console.ForegroundColor; _initialBackground = _console.BackgroundColor; @@ -1105,6 +1106,7 @@ public static void InvokePrompt(ConsoleKeyInfo? key = null, object arg = null) console.Write(newPrompt); _singleton._initialX = console.CursorLeft; + _singleton._initialPromptCells = _singleton._initialX; _singleton._initialY = console.CursorTop; _singleton._previousRender = _initialPrevRender; _singleton._previousRender.UpdateConsoleInfo(console); diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index 253f7fbe..81c7d5d5 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -166,6 +166,18 @@ struct LineInfoForRendering }; private int _initialX; private int _initialY; + + /// + /// The width, in buffer cells, of the last logical line of the prompt, measured from column 0 + /// of the physical line where that logical line starts. + /// This does not depend on the buffer width, whereas '_initialX' is the column of the same + /// point at the current buffer width, and hence is only ever this value modulo that width. + /// We keep it so that '_initialX' can be recomputed after the buffer width changes: reducing + /// '_initialX' in place would discard how many physical lines the prompt spans, and the + /// column could then never be recovered when the buffer is made wider again. + /// + private int _initialPromptCells; + private bool _waitingToRender; private bool _handlePotentialResizing; @@ -885,6 +897,7 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, RenderD } _initialX = _console.CursorLeft; + _initialPromptCells = _initialX; _initialY = _console.CursorTop; _previousRender = _initialPrevRender; } @@ -1244,6 +1257,7 @@ private void RecomputeInitialCoords(bool isTextBufferUnchanged) } _initialX = _console.CursorLeft; + _initialPromptCells = _initialX; _initialY = _console.CursorTop; _previousRender = _initialPrevRender; } @@ -1257,8 +1271,11 @@ private void RecomputeInitialCoords(bool isTextBufferUnchanged) // The '_buffer' and '_current' still reflects what has been rendered on the screen, // so we can use them to re-calculate the initial coordinates in this case. - // Recompute X from the buffer width: - _initialX %= _console.BufferWidth; + // Recompute X from the prompt's cell width, which doesn't change with the buffer width. + // Reducing '_initialX' in place instead gives the same result for the first narrowing, + // but loses how many physical lines the prompt spans, so the column could not be + // recovered when the buffer is made wider again. + _initialX = _initialPromptCells % _console.BufferWidth; // Recompute Y from the cursor _initialY = 0; @@ -1293,8 +1310,11 @@ private void RecomputeInitialCoords(bool isTextBufferUnchanged) throw new InvalidOperationException(message); } - // Recompute X from the buffer width: - _initialX %= _console.BufferWidth; + // Recompute X from the prompt's cell width, which doesn't change with the buffer width. + // Reducing '_initialX' in place instead gives the same result for the first narrowing, + // but loses how many physical lines the prompt spans, so the column could not be + // recovered when the buffer is made wider again. + _initialX = _initialPromptCells % _console.BufferWidth; // Recompute Y from the cursor _initialY = 0; diff --git a/test/ResizingTest.cs b/test/ResizingTest.cs index a02ed783..d4459b76 100644 --- a/test/ResizingTest.cs +++ b/test/ResizingTest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Text; using Microsoft.PowerShell; using Newtonsoft.Json; using Xunit; @@ -182,5 +183,86 @@ public void PhysicalLineCountMethod_ShouldWork() } } } + + private static FieldInfo GetInstanceField(string name) + { + return typeof(PSConsoleReadLine).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); + } + + [Fact] + public void RecomputeInitialCoords_ShouldRecoverInitialXWhenBufferGetsWider() + { + // The column of the initial coordinates is the width of the prompt reduced modulo the + // buffer width, so a prompt of 36 cells walked through the buffer widths below has to + // give 36, 1, 36, 11 and 36 in turn. Reducing the column in place on each change gives + // the right answer only for the first one, because it discards how many physical lines + // the prompt spans and the column can then no longer be recovered. + // + // Only the column is checked here. Recovering the row relies on the terminal having + // reflowed the screen buffer, which the test console does not do. + const int promptCells = 36; + const int bufferHeight = 100; + int[] bufferWidths = { 100, 35, 60, 25, 100 }; + + PSConsoleReadLine instance = GetPSConsoleReadLineSingleton(); + FieldInfo consoleField = GetInstanceField("_console"); + FieldInfo bufferField = GetInstanceField("_buffer"); + FieldInfo currentField = GetInstanceField("_current"); + FieldInfo initialXField = GetInstanceField("_initialX"); + FieldInfo initialYField = GetInstanceField("_initialY"); + FieldInfo initialPromptCellsField = GetInstanceField("_initialPromptCells"); + FieldInfo previousRenderField = GetInstanceField("_previousRender"); + FieldInfo handlePotentialResizingField = GetInstanceField("_handlePotentialResizing"); + MethodInfo recomputeInitialCoords = typeof(PSConsoleReadLine) + .GetMethod("RecomputeInitialCoords", BindingFlags.Instance | BindingFlags.NonPublic); + + object savedConsole = consoleField.GetValue(instance); + object savedBuffer = bufferField.GetValue(instance); + object savedCurrent = currentField.GetValue(instance); + object savedPreviousRender = previousRenderField.GetValue(instance); + + try + { + // An empty input keeps the initial row at 0 throughout, so a plain test console is + // all that is needed to report each new buffer width. + bufferField.SetValue(instance, new StringBuilder()); + currentField.SetValue(instance, 0); + initialPromptCellsField.SetValue(instance, promptCells); + initialXField.SetValue(instance, promptCells % bufferWidths[0]); + initialYField.SetValue(instance, 0); + + RenderData previousRender = new() + { + lines = new[] { new RenderedLineData(line: "", isFirstLogicalLine: true) } + }; + + foreach (int bufferWidth in bufferWidths) + { + TestConsole console = new(_, bufferWidth, bufferHeight); + consoleField.SetValue(instance, console); + + previousRender.initialY = (int)initialYField.GetValue(instance); + previousRenderField.SetValue(instance, previousRender); + handlePotentialResizingField.SetValue(instance, true); + + recomputeInitialCoords.Invoke(instance, new object[] { true }); + + int initialX = (int)initialXField.GetValue(instance); + Assert.True( + promptCells % bufferWidth == initialX, + $"buffer width {bufferWidth}: initial column is {initialX} but should be {promptCells % bufferWidth}"); + + // The render data now describes the buffer as it was before the next change. + previousRender.UpdateConsoleInfo(console); + } + } + finally + { + consoleField.SetValue(instance, savedConsole); + bufferField.SetValue(instance, savedBuffer); + currentField.SetValue(instance, savedCurrent); + previousRenderField.SetValue(instance, savedPreviousRender); + } + } } }