Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions PSReadLine/ReadLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
28 changes: 24 additions & 4 deletions PSReadLine/Render.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ struct LineInfoForRendering
};
private int _initialX;
private int _initialY;

/// <summary>
/// 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.
/// </summary>
private int _initialPromptCells;

private bool _waitingToRender;
private bool _handlePotentialResizing;

Expand Down Expand Up @@ -885,6 +897,7 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, RenderD
}

_initialX = _console.CursorLeft;
_initialPromptCells = _initialX;
_initialY = _console.CursorTop;
_previousRender = _initialPrevRender;
}
Expand Down Expand Up @@ -1244,6 +1257,7 @@ private void RecomputeInitialCoords(bool isTextBufferUnchanged)
}

_initialX = _console.CursorLeft;
_initialPromptCells = _initialX;
_initialY = _console.CursorTop;
_previousRender = _initialPrevRender;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
82 changes: 82 additions & 0 deletions test/ResizingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
}