diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs
index df6c9e9a..a0f4757a 100644
--- a/PSReadLine/Render.cs
+++ b/PSReadLine/Render.cs
@@ -28,6 +28,15 @@ public override string ToString()
public partial class PSConsoleReadLine
{
+ struct LineInfoForRendering
+ {
+ public int CurrentLogicalLineIndex;
+ public int CurrentPhysicalLineCount;
+ public int PreviousLogicalLineIndex;
+ public int PreviousPhysicalLineCount;
+ public int PseudoPhysicalLineOffset;
+ }
+
struct RenderedLineData
{
public string line;
@@ -340,135 +349,322 @@ void MaybeEmphasize(int i, string currColor)
return currentLogicalLine + 1;
}
- private void ReallyRender(RenderData renderData, string defaultColor)
+ ///
+ /// Flip the color on the prompt if the error state changed.
+ ///
+ ///
+ /// A bool value indicating whether we need to flip the color,
+ /// namely whether we moved cursor to the initial position.
+ ///
+ private bool RenderErrorPrompt(RenderData renderData, string defaultColor)
{
- string activeColor = "";
+ // We may need to flip the color on the prompt if the error state changed.
- void UpdateColorsIfNecessary(string newColor)
+ int bufferWidth = _console.BufferWidth;
+ string promptText = _options.PromptText;
+
+ if (string.IsNullOrEmpty(promptText) || _initialY < 0)
{
- if (!object.ReferenceEquals(newColor, activeColor))
+ // No need to flip the prompt color if either the error prompt is not defined
+ // or the initial cursor point has already been scrolled off the buffer.
+ return false;
+ }
+
+ renderData.errorPrompt = (_parseErrors != null && _parseErrors.Length > 0);
+ if (renderData.errorPrompt == _previousRender.errorPrompt)
+ {
+ // No need to flip the prompt color if the error state didn't change.
+ return false;
+ }
+
+ // We need to update the prompt
+ _console.SetCursorPosition(_initialX, _initialY);
+
+ // promptBufferCells is the number of visible characters in the prompt
+ int promptBufferCells = LengthInBufferCells(promptText);
+ bool renderErrorPrompt = false;
+
+ if (_console.CursorLeft >= promptBufferCells)
+ {
+ renderErrorPrompt = true;
+ _console.CursorLeft -= promptBufferCells;
+ }
+ else
+ {
+ // The 'CursorLeft' could be less than error-prompt-cell-length in one of the following 3 cases:
+ // 1. console buffer was resized, which causes the initial cursor to appear on the next line;
+ // 2. prompt string gets longer (e.g. by 'cd' into nested folders), which causes the line to be wrapped to the next line;
+ // 3. the prompt function was changed, which causes the new prompt string is shorter than the error prompt.
+ // Here, we always assume it's the case 1 or 2, and wrap back to the previous line to change the error prompt color.
+ // In case of case 3, the rendering would be off, but it's more of a user error because the prompt is changed without
+ // updating 'PromptText' with 'Set-PSReadLineOption'.
+
+ int diffs = promptBufferCells - _console.CursorLeft;
+ int newX = bufferWidth - diffs % bufferWidth;
+ int newY = _initialY - diffs / bufferWidth - 1;
+
+ // newY could be less than 0 if 'PromptText' is manually set to be a long string.
+ if (newY >= 0)
{
- _console.Write(newColor);
- activeColor = newColor;
+ renderErrorPrompt = true;
+ _console.SetCursorPosition(newX, newY);
}
}
- // TODO: avoid writing everything.
+ if (renderErrorPrompt)
+ {
+ string color = renderData.errorPrompt ? _options._errorColor : defaultColor;
+ if (renderData.errorPrompt && promptBufferCells != promptText.Length)
+ {
+ promptText = promptText.Substring(promptText.Length - promptBufferCells);
+ }
+ _console.Write(color);
+ _console.Write(promptText);
+ _console.Write("\x1b[0m");
+ }
- var bufferWidth = _console.BufferWidth;
- var bufferHeight = _console.BufferHeight;
+ return true;
+ }
- // In case the buffer was resized
- RecomputeInitialCoords();
- renderData.bufferWidth = bufferWidth;
- renderData.bufferHeight = bufferHeight;
+ ///
+ /// Given the length of a logical line, calculate the number of physical lines it takes to render
+ /// the logical line on the console.
+ ///
+ private int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysicalLine)
+ {
+ int cnt = 1;
+ int bufferWidth = _console.BufferWidth;
- // Move the cursor to where we started, but make cursor invisible while we're rendering.
- _console.CursorVisible = false;
- _console.SetCursorPosition(_initialX, _initialY);
+ if (isFirstLogicalLine)
+ {
+ // The first logical line has the user prompt that we don't touch
+ // (except where we turn part to red, but we've finished that
+ // before getting here.)
+ var maxFirstLine = bufferWidth - _initialX;
+ if (columns > maxFirstLine)
+ {
+ cnt += 1;
+ columns -= maxFirstLine;
+ }
+ else
+ {
+ lenLastPhysicalLine = columns;
+ return 1;
+ }
+ }
- // Possibly need to flip the color on the prompt if the error state changed.
- var promptText = _options.PromptText;
- if (!string.IsNullOrEmpty(promptText))
+ lenLastPhysicalLine = columns % bufferWidth;
+ if (lenLastPhysicalLine == 0)
{
- renderData.errorPrompt = (_parseErrors != null && _parseErrors.Length > 0);
- if (renderData.errorPrompt != _previousRender.errorPrompt)
+ // Handle the last column when the columns is equal to n * bufferWidth
+ // where n >= 1 integers
+ lenLastPhysicalLine = bufferWidth;
+ return cnt - 1 + columns / bufferWidth;
+ }
+
+ return cnt + columns / bufferWidth;
+ }
+
+ ///
+ /// We avoid re-rendering everything while editing if it's possible.
+ /// This method attempts to find the first changed logical line and move the cursor to the right position for the subsequent rendering.
+ ///
+ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, RenderData renderData, out LineInfoForRendering lineInfoForRendering)
+ {
+ int bufferWidth = _console.BufferWidth;
+ int bufferHeight = _console.BufferHeight;
+
+ RenderedLineData[] previousRenderLines = _previousRender.lines;
+ int previousLogicalLine = 0;
+ int previousPhysicalLine = 0;
+
+ RenderedLineData[] renderLines = renderData.lines;
+ int logicalLine = 0;
+ int physicalLine = 0;
+ int pseudoPhysicalLineOffset = 0;
+
+ bool hasToWriteAll = true;
+
+ if (renderLines.Length > 1)
+ {
+ // There are multiple logical lines, so it's possible the first N logical lines are not affected by the user's editing,
+ // in which case, we can skip rendering until reaching the first changed logical line.
+
+ int minLinesLength = previousRenderLines.Length;
+ int linesToCheck = -1;
+
+ if (renderLines.Length < previousRenderLines.Length)
{
- // We need to update the prompt
+ minLinesLength = renderLines.Length;
- // promptBufferCells is the number of visible characters in the prompt
- int promptBufferCells = LengthInBufferCells(promptText);
- bool renderErrorPrompt = false;
+ // When the initial cursor position has been scrolled off the buffer, it's possible the editing deletes some texts and
+ // potentially causes the final cursor position to be off the buffer as well. In this case, we should start rendering
+ // from the logical line where the cursor is supposed to be moved to eventually.
+ // Here we check for this situation, and calculate the physical line count to check later if we are in this situation.
- if (_console.CursorLeft >= promptBufferCells)
- {
- renderErrorPrompt = true;
- _console.CursorLeft -= promptBufferCells;
- }
- else
+ if (_initialY < 0)
{
- // The 'CursorLeft' could be less than error-prompt-cell-length in one of the following 3 cases:
- // 1. console buffer was resized, which causes the initial cursor to appear on the next line;
- // 2. prompt string gets longer (e.g. by 'cd' into nested folders), which causes the line to be wrapped to the next line;
- // 3. the prompt function was changed, which causes the new prompt string is shorter than the error prompt.
- // Here, we always assume it's the case 1 or 2, and wrap back to the previous line to change the error prompt color.
- // In case of case 3, the rendering would be off, but it's more of a user error because the prompt is changed without
- // updating 'PromptText' with 'Set-PSReadLineOption'.
-
- int diffs = promptBufferCells - _console.CursorLeft;
- int newX = bufferWidth - diffs % bufferWidth;
- int newY = _initialY - diffs / bufferWidth - 1;
-
- // newY could be less than 0 if 'PromptText' is manually set to be a long string.
- if (newY >= 0)
+ int y = ConvertOffsetToPoint(_current).Y;
+ if (y < 0)
{
- renderErrorPrompt = true;
- _console.SetCursorPosition(newX, newY);
+ // Number of physical lines from the initial row to the row where the cursor is supposed to be set at.
+ linesToCheck = y - _initialY + 1;
}
}
+ }
- if (renderErrorPrompt)
+ // Find the first logical line that was changed.
+ for (; logicalLine < minLinesLength; logicalLine++)
+ {
+ // Found the first different logical line? Break out the loop.
+ if (renderLines[logicalLine].line != previousRenderLines[logicalLine].line) { break; }
+
+ int count = PhysicalLineCount(renderLines[logicalLine].columns, logicalLine == 0, out _);
+ physicalLine += count;
+
+ if (linesToCheck < 0)
{
- var color = renderData.errorPrompt ? _options._errorColor : defaultColor;
- if (renderData.errorPrompt && promptBufferCells != promptText.Length)
- promptText = promptText.Substring(promptText.Length - promptBufferCells);
- UpdateColorsIfNecessary(color);
- _console.Write(promptText);
- _console.Write("\x1b[0m");
+ continue;
+ }
+ else if (physicalLine >= linesToCheck)
+ {
+ physicalLine -= count;
+ break;
}
}
- }
- int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysicalLine)
- {
- int cnt = 1;
- if (isFirstLogicalLine)
+ if (logicalLine > 0)
{
- // The first logical line has the user prompt that we don't touch
- // (except where we turn part to red, but we've finished that
- // before getting here.)
- var maxFirstLine = bufferWidth - _initialX;
- if (columns > maxFirstLine)
+ // Some logical lines at the top were not affected by the editing.
+ // We only need to write starting from the first changed logical line.
+ hasToWriteAll = false;
+ previousLogicalLine = logicalLine;
+ previousPhysicalLine = physicalLine;
+
+ var newTop = _initialY + physicalLine;
+ if (newTop == bufferHeight)
{
- cnt += 1;
- columns -= maxFirstLine;
+ if (logicalLine < renderLines.Length)
+ {
+ // This could happen when adding a new line in the end of the very last line.
+ // In this case, we scroll up by writing out a new line.
+ _console.SetCursorPosition(left: bufferWidth - 1, top: bufferHeight - 1);
+ _console.Write("\n");
+ }
+
+ // It might happen that 'logicalLine == renderLines.Length'. This means the current
+ // logical lines to be rendered are exactly the same the the previous logical lines.
+ // No need to do anything in this case, as we don't need to render anything.
}
else
{
- lenLastPhysicalLine = columns;
- return 1;
+ // For the logical line that we will start to re-render from, it's possible that
+ // 1. the whole logical line had already been scrolled up-off the buffer. This could happen when you backward delete characters
+ // on the first line in buffer and cause the current line to be folded to the previous line.
+ // 2. the logical line spans on multiple physical lines and the top a few physical lines had already been scrolled off the buffer.
+ // This could happen when you edit on the top a few physical lines in the buffer, which belong to a longer logical line.
+ // Either of them will cause 'newTop' to be less than 0.
+ if (newTop < 0)
+ {
+ // In this case, we will render the whole logical line starting from the upper-left-most point of the window.
+ // By doing this, we are essentially adding a few pseudo physical lines (the physical lines that belong to the logical line but
+ // had been scrolled off the buffer would be re-rendered). So, update 'physicalLine'.
+ pseudoPhysicalLineOffset = 0 - newTop;
+ physicalLine += pseudoPhysicalLineOffset;
+ newTop = 0;
+ }
+
+ _console.SetCursorPosition(left: 0, top: newTop);
}
}
+ }
- lenLastPhysicalLine = columns % bufferWidth;
- if (lenLastPhysicalLine == 0)
+ if (hasToWriteAll && !cursorMovedToInitialPos)
+ {
+ // The editing was in the first logical line. We have to write everything in this case.
+ // Move the cursor to the initial position if we haven't done so.
+ if (_initialY < 0)
{
- // Handle the last column when the columns is equal to n * bufferWidth
- // where n >= 1 integers
- lenLastPhysicalLine = bufferWidth;
- return cnt - 1 + columns / bufferWidth;
+ // The prompt had been scrolled up-off the buffer. Now we are about to render from the very
+ // beginning, so we clear the screen and invoke/print the prompt line.
+ _console.Write("\x1b[2J");
+ _console.SetCursorPosition(0, _console.WindowTop);
+
+ string newPrompt = GetPrompt();
+ if (!string.IsNullOrEmpty(newPrompt))
+ {
+ _console.Write(newPrompt);
+ }
+
+ _initialX = _console.CursorLeft;
+ _initialY = _console.CursorTop;
+ _previousRender = _initialPrevRender;
+ }
+ else
+ {
+ _console.SetCursorPosition(_initialX, _initialY);
}
+ }
+
+ lineInfoForRendering = default;
+ lineInfoForRendering.CurrentLogicalLineIndex = logicalLine;
+ lineInfoForRendering.CurrentPhysicalLineCount = physicalLine;
+ lineInfoForRendering.PreviousLogicalLineIndex = previousLogicalLine;
+ lineInfoForRendering.PreviousPhysicalLineCount = previousPhysicalLine;
+ lineInfoForRendering.PseudoPhysicalLineOffset = pseudoPhysicalLineOffset;
+ }
+
+ private void ReallyRender(RenderData renderData, string defaultColor)
+ {
+ string activeColor = "";
+ int bufferWidth = _console.BufferWidth;
+ int bufferHeight = _console.BufferHeight;
- return cnt + columns / bufferWidth;
+ void UpdateColorsIfNecessary(string newColor)
+ {
+ if (!object.ReferenceEquals(newColor, activeColor))
+ {
+ _console.Write(newColor);
+ activeColor = newColor;
+ }
}
- var previousRenderLines = _previousRender.lines;
- var previousLogicalLine = 0;
- var previousPhysicalLine = 0;
+ // In case the buffer was resized
+ RecomputeInitialCoords();
+ renderData.bufferWidth = bufferWidth;
+ renderData.bufferHeight = bufferHeight;
+
+ // Make cursor invisible while we're rendering.
+ _console.CursorVisible = false;
- var renderLines = renderData.lines;
- var logicalLine = 0;
- var physicalLine = 0;
- var lenPrevLastLine = 0;
+ // Change the prompt color if the parsing error state changed.
+ bool cursorMovedToInitialPos = RenderErrorPrompt(renderData, defaultColor);
+
+ // Calculate what to render and where to start the rendering.
+ LineInfoForRendering lineInfoForRendering;
+ CalculateWhereAndWhatToRender(cursorMovedToInitialPos, renderData, out lineInfoForRendering);
+
+ RenderedLineData[] previousRenderLines = _previousRender.lines;
+ int previousLogicalLine = lineInfoForRendering.PreviousLogicalLineIndex;
+ int previousPhysicalLine = lineInfoForRendering.PreviousPhysicalLineCount;
+
+ RenderedLineData[] renderLines = renderData.lines;
+ int logicalLine = lineInfoForRendering.CurrentLogicalLineIndex;
+ int physicalLine = lineInfoForRendering.CurrentPhysicalLineCount;
+ int pseudoPhysicalLineOffset = lineInfoForRendering.PseudoPhysicalLineOffset;
+
+ int lenPrevLastLine = 0;
+ int logicalLineStartIndex = logicalLine;
+ int physicalLineStartCount = physicalLine;
for (; logicalLine < renderLines.Length; logicalLine++)
{
- if (logicalLine != 0) _console.Write("\n");
+ if (logicalLine != logicalLineStartIndex) _console.Write("\n");
var lineData = renderLines[logicalLine];
_console.Write(lineData.line);
- physicalLine += PhysicalLineCount(lineData.columns, logicalLine == 0, out var lenLastLine);
+ physicalLine += PhysicalLineCount(lineData.columns, logicalLine == 0, out int lenLastLine);
// Find the previous logical line (if any) that would have rendered
// the current physical line because we may need to clear it.
@@ -503,7 +699,7 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi
// need to clear to the end of the line.
if (lenLastLine < bufferWidth)
{
- lenToClear = bufferWidth - (lenLastLine % bufferWidth);
+ lenToClear = bufferWidth - lenLastLine;
if (physicalLine == 1)
lenToClear -= _initialX;
}
@@ -518,27 +714,34 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi
UpdateColorsIfNecessary(defaultColor);
- while (previousPhysicalLine > physicalLine)
+ // The last logical line is shorter than our previous render? Clear them.
+ for (int currentLines = physicalLine; currentLines < previousPhysicalLine;)
{
- _console.SetCursorPosition(0, _initialY + physicalLine);
+ _console.SetCursorPosition(0, _initialY + currentLines);
- physicalLine += 1;
- var lenToClear = physicalLine == previousPhysicalLine ? lenPrevLastLine : bufferWidth;
+ currentLines++;
+ var lenToClear = currentLines == previousPhysicalLine ? lenPrevLastLine : bufferWidth;
if (lenToClear > 0)
{
_console.Write(Spaces(lenToClear));
}
}
- // Fewer lines than our last render? Clear them.
+ // Fewer logical lines than our previous render? Clear them.
for (; previousLogicalLine < previousRenderLines.Length; previousLogicalLine++)
{
- _console.Write("\n");
+ // No need to write new line if all we need is to clear the extra previous render.
+ if (logicalLineStartIndex < renderLines.Length) { _console.Write("\n"); }
_console.Write(Spaces(previousRenderLines[previousLogicalLine].columns));
}
+ // Preserve the current render data.
_previousRender = renderData;
+ // If we counted pseudo physical lines, deduct them to get the real physical line counts
+ // before updating '_initialY'.
+ physicalLine -= pseudoPhysicalLineOffset;
+
// Reset the colors after we've finished all our rendering.
_console.Write("\x1b[0m");
@@ -547,6 +750,26 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi
// We had to scroll to render everything, update _initialY
_initialY = bufferHeight - physicalLine;
}
+ else if (pseudoPhysicalLineOffset > 0)
+ {
+ // When we rewrote a logical line (or part of a logical line) that had previously been scrolled up-off
+ // the buffer (fully or partially), we need to adjust '_initialY' if the changes to that logical line
+ // don't result in the same number of physical lines to be scrolled up-off the buffer.
+
+ // Calculate the total number of physical lines starting from the logical line we re-wrote.
+ int physicalLinesStartingFromTheRewrittenLogicalLine =
+ physicalLine - (physicalLineStartCount - pseudoPhysicalLineOffset);
+
+ Debug.Assert(
+ bufferHeight + pseudoPhysicalLineOffset >= physicalLinesStartingFromTheRewrittenLogicalLine,
+ "number of physical lines starting from the first changed logical line should be no more than the buffer height plus the pseudo lines we added.");
+
+ int offset = physicalLinesStartingFromTheRewrittenLogicalLine > bufferHeight
+ ? pseudoPhysicalLineOffset - (physicalLinesStartingFromTheRewrittenLogicalLine - bufferHeight)
+ : pseudoPhysicalLineOffset;
+
+ _initialY += offset;
+ }
// Calculate the coord to place the cursor for the next input.
var point = ConvertOffsetToPoint(_current);
@@ -562,6 +785,24 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi
_initialY -= 1;
point.Y -= 1;
}
+ else if (point.Y == -1)
+ {
+ // This could only happen in two cases:
+ //
+ // 1. when you are adding characters to the first line in the buffer (top = 0) to make the logical line
+ // wrap to one extra physical line. This would cause the buffer to scroll up and push the line being
+ // edited up-off the buffer.
+ // 2. when you are deleting characters backwards from the first line in the buffer without changing the
+ // number of physical lines (either editing the same logical line or causing the current logical line
+ // to merge in the previous but still span to the current physical line). The cursor is supposed to
+ // appear in the previous line (which is off the buffer).
+ //
+ // In these case, we move the cursor to the upper-left-most position of the window, where it's closest to
+ // the previous editing position, and update '_current' appropriately.
+
+ _current += (bufferWidth - point.X);
+ point.X = point.Y = 0;
+ }
_console.SetCursorPosition(point.X, point.Y);
_console.CursorVisible = true;
@@ -720,6 +961,12 @@ private void MoveCursor(int newCursor)
_previousRender.bufferHeight = _console.BufferHeight;
var point = ConvertOffsetToPoint(newCursor);
+ if (point.Y < 0)
+ {
+ Ding();
+ return;
+ }
+
_console.SetCursorPosition(point.X, point.Y);
_current = newCursor;
}
@@ -827,7 +1074,13 @@ private int ConvertLineAndColumnToOffset(Point point)
{
x = size;
}
- y += 1;
+
+ // If the next character is newline, let the next loop
+ // iteration increment y and adjust x.
+ if (!(offset + 1 < _buffer.Length && _buffer[offset + 1] == '\n'))
+ {
+ y += 1;
+ }
}
}
}