From 0a39f826003f7c130a35c9d4d2ab3d3fd31c5e3c Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 26 Jul 2019 16:37:30 -0700 Subject: [PATCH 1/6] Major change includes: - Update 'ReallyRender' to avoid writing everything every time when editing the text. Instead, we find the first different logical line, and starting writing from there. - Support editing text even if the top of the text has been scrolled up-off the buffer. - Disallow cursor to be set to a position that is off the buffer. - Fix a bug in 'ConvertLineAndColumnToOffset' so that a point can be translated to the offset correctly. --- PSReadLine/Render.cs | 323 +++++++++++++++++++++++++++++++++---------- 1 file changed, 253 insertions(+), 70 deletions(-) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index df6c9e9ad..da2f0a176 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -340,82 +340,97 @@ 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 = ""; + // Possibly need to flip the color on the prompt if the error state changed. - void UpdateColorsIfNecessary(string newColor) + var bufferWidth = _console.BufferWidth; + var promptText = _options.PromptText; + + if (string.IsNullOrEmpty(promptText) || _initialY < 0) { - if (!object.ReferenceEquals(newColor, activeColor)) - { - _console.Write(newColor); - activeColor = newColor; - } + // 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; } - // TODO: avoid writing everything. + 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 changed. + return false; + } - var bufferWidth = _console.BufferWidth; - var bufferHeight = _console.BufferHeight; + // We need to update the prompt + _console.SetCursorPosition(_initialX, _initialY); - // In case the buffer was resized - RecomputeInitialCoords(); - renderData.bufferWidth = bufferWidth; - renderData.bufferHeight = bufferHeight; + // promptBufferCells is the number of visible characters in the prompt + int promptBufferCells = LengthInBufferCells(promptText); + bool renderErrorPrompt = false; - // Move the cursor to where we started, but make cursor invisible while we're rendering. - _console.CursorVisible = false; - _console.SetCursorPosition(_initialX, _initialY); + 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) + { + renderErrorPrompt = true; + _console.SetCursorPosition(newX, newY); + } + } - // Possibly need to flip the color on the prompt if the error state changed. - var promptText = _options.PromptText; - if (!string.IsNullOrEmpty(promptText)) + if (renderErrorPrompt) { - renderData.errorPrompt = (_parseErrors != null && _parseErrors.Length > 0); - if (renderData.errorPrompt != _previousRender.errorPrompt) + var color = renderData.errorPrompt ? _options._errorColor : defaultColor; + if (renderData.errorPrompt && promptBufferCells != promptText.Length) { - // We need to update the prompt + promptText = promptText.Substring(promptText.Length - promptBufferCells); + } + _console.Write(color); + _console.Write(promptText); + _console.Write("\x1b[0m"); + } - // promptBufferCells is the number of visible characters in the prompt - int promptBufferCells = LengthInBufferCells(promptText); - bool renderErrorPrompt = false; + return true; + } - 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) - { - renderErrorPrompt = true; - _console.SetCursorPosition(newX, newY); - } - } + private void ReallyRender(RenderData renderData, string defaultColor) + { + string activeColor = ""; + var bufferWidth = _console.BufferWidth; + var bufferHeight = _console.BufferHeight; + var cursorX = _console.CursorLeft; + var cursorY = _console.CursorTop; - if (renderErrorPrompt) - { - 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"); - } + void UpdateColorsIfNecessary(string newColor) + { + if (!object.ReferenceEquals(newColor, activeColor)) + { + _console.Write(newColor); + activeColor = newColor; } } @@ -452,6 +467,16 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi return cnt + columns / bufferWidth; } + // In case the buffer was resized + RecomputeInitialCoords(); + renderData.bufferWidth = bufferWidth; + renderData.bufferHeight = bufferHeight; + + // Move the cursor to where we started, but make cursor invisible while we're rendering. + _console.CursorVisible = false; + + bool cursorMovedToInitialPos = RenderErrorPrompt(renderData, defaultColor); + var previousRenderLines = _previousRender.lines; var previousLogicalLine = 0; var previousPhysicalLine = 0; @@ -459,11 +484,117 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi var renderLines = renderData.lines; var logicalLine = 0; var physicalLine = 0; + var pseudoPhysicalLineOffset = 0; var lenPrevLastLine = 0; + // TODO: need to move to a separate method. + bool hasToWriteAll = true; + if (cursorY > _initialY && renderLines.Length > 1) + { + int minLineLength = previousRenderLines.Length; + int linesToCheck = -1; + + if (renderLines.Length < previousRenderLines.Length) + { + minLineLength = renderLines.Length; + if (cursorX == Options.ContinuationPrompt.Length && cursorY == 0) + { + linesToCheck = 0 - _initialY; + } + } + + // Find the first logical line that was changed, then write starting from that logical line. + for (; logicalLine < minLineLength; logicalLine++) + { + if (renderLines[logicalLine].line != previousRenderLines[logicalLine].line) { break; } + + int count = PhysicalLineCount(renderLines[logicalLine].columns, logicalLine == 0, out _); + physicalLine += count; + + if (physicalLine == linesToCheck && previousRenderLines[logicalLine + 1].columns == Options.ContinuationPrompt.Length) + { + if (ConvertOffsetToPoint(_current).Y == -1) + { + physicalLine -= count; + break; + } + } + } + + if (logicalLine > 0) + { + // The editing happens in the middle or end of the text. + // In this case, 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) + { + // 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"); + } + else + { + // It's possible that the changed logical line spans on multiple physical lines + // and the first a few physical lines have already been scrolled off the buffer, + // causing 'newTop' to be less than 0. + if (newTop < 0) + { + // In this case, given the logical line was changed, we would 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 have been scrolled off the buffer will be re-rendered), + // so update 'physicalLine'. + pseudoPhysicalLineOffset = 0 - newTop; + physicalLine += pseudoPhysicalLineOffset; + newTop = 0; + } + + _console.SetCursorPosition(left: 0, top: newTop); + } + } + } + + if (hasToWriteAll && !cursorMovedToInitialPos) + { + // The editing happens 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) + { + // The prompt line can be displayed, 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); + } + } + + renderLines = renderData.lines; + previousRenderLines = _previousRender.lines; + + 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); @@ -503,7 +634,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,12 +649,12 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi UpdateColorsIfNecessary(defaultColor); - while (previousPhysicalLine > physicalLine) + 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)); @@ -533,12 +664,17 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi // Fewer lines than our last 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 before updating '_initialY'. + physicalLine -= pseudoPhysicalLineOffset; + // Reset the colors after we've finished all our rendering. _console.Write("\x1b[0m"); @@ -547,6 +683,23 @@ 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. + + int physicalLinesStartingFromTheRewrittenLogicalLine = + physicalLine - (physicalLineStartCount - pseudoPhysicalLineOffset); + + Debug.Assert(bufferHeight + pseudoPhysicalLineOffset >= physicalLinesStartingFromTheRewrittenLogicalLine, ""); + + 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 +715,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 +891,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 +1004,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; + } } } } From fc5baea0189e858e39d4c81617a2edd91aec7e76 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 29 Jul 2019 16:14:00 -0700 Subject: [PATCH 2/6] Refactor code and add more comments --- PSReadLine/Render.cs | 225 ++++++++++++++++++++++++++++--------------- 1 file changed, 147 insertions(+), 78 deletions(-) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index da2f0a176..fdba95aec 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -28,6 +28,13 @@ public override string ToString() public partial class PSConsoleReadLine { + struct RenderedLineInfo + { + public int LogicalLineIndex; + public int PhysicalLineCount; + public int PseudoPhysicalLineOffset; + } + struct RenderedLineData { public string line; @@ -349,7 +356,7 @@ void MaybeEmphasize(int i, string currColor) /// private bool RenderErrorPrompt(RenderData renderData, string defaultColor) { - // Possibly need to flip the color on the prompt if the error state changed. + // We may need to flip the color on the prompt if the error state changed. var bufferWidth = _console.BufferWidth; var promptText = _options.PromptText; @@ -417,95 +424,104 @@ private bool RenderErrorPrompt(RenderData renderData, string defaultColor) return true; } - private void ReallyRender(RenderData renderData, string defaultColor) + /// + /// 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) { - string activeColor = ""; - var bufferWidth = _console.BufferWidth; - var bufferHeight = _console.BufferHeight; - var cursorX = _console.CursorLeft; - var cursorY = _console.CursorTop; + int cnt = 1; + int bufferWidth = _console.BufferWidth; - void UpdateColorsIfNecessary(string newColor) + if (isFirstLogicalLine) { - if (!object.ReferenceEquals(newColor, activeColor)) + // 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) { - _console.Write(newColor); - activeColor = newColor; + cnt += 1; + columns -= maxFirstLine; } - } - - int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysicalLine) - { - int cnt = 1; - 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; - } - } - - lenLastPhysicalLine = columns % bufferWidth; - if (lenLastPhysicalLine == 0) + else { - // Handle the last column when the columns is equal to n * bufferWidth - // where n >= 1 integers - lenLastPhysicalLine = bufferWidth; - return cnt - 1 + columns / bufferWidth; + lenLastPhysicalLine = columns; + return 1; } - - return cnt + columns / bufferWidth; } - // In case the buffer was resized - RecomputeInitialCoords(); - renderData.bufferWidth = bufferWidth; - renderData.bufferHeight = bufferHeight; + lenLastPhysicalLine = columns % bufferWidth; + if (lenLastPhysicalLine == 0) + { + // Handle the last column when the columns is equal to n * bufferWidth + // where n >= 1 integers + lenLastPhysicalLine = bufferWidth; + return cnt - 1 + columns / bufferWidth; + } - // Move the cursor to where we started, but make cursor invisible while we're rendering. - _console.CursorVisible = false; + return cnt + columns / bufferWidth; + } - bool cursorMovedToInitialPos = RenderErrorPrompt(renderData, defaultColor); + /// + /// 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, ref RenderData renderData, ref RenderedLineInfo current, ref RenderedLineInfo previous) + { + int bufferWidth = _console.BufferWidth; + int bufferHeight = _console.BufferHeight; + int cursorX = _console.CursorLeft; + int cursorY = _console.CursorTop; - var previousRenderLines = _previousRender.lines; - var previousLogicalLine = 0; - var previousPhysicalLine = 0; + RenderedLineData[] previousRenderLines = _previousRender.lines; + int previousLogicalLine = 0; + int previousPhysicalLine = 0; - var renderLines = renderData.lines; - var logicalLine = 0; - var physicalLine = 0; - var pseudoPhysicalLineOffset = 0; - var lenPrevLastLine = 0; + RenderedLineData[] renderLines = renderData.lines; + int logicalLine = 0; + int physicalLine = 0; + int pseudoPhysicalLineOffset = 0; - // TODO: need to move to a separate method. bool hasToWriteAll = true; + if (cursorY > _initialY && renderLines.Length > 1) { + // The current 'cursorTop' is below the initial 'cursorTop' and there are multiple logical lines. + // This indicates the user is editing in the middle or end of the existing text. + // In this case, it's possible that we can skip rendering until reaching the first changed logical line. + int minLineLength = previousRenderLines.Length; int linesToCheck = -1; if (renderLines.Length < previousRenderLines.Length) { + // Handle a special case: + // - top part of the text has been scrolled up-off the buffer; + // - the cursor is at the beginning of the first line in buffer; + // - the first line contains nothing but a new-line character; + // - the edit operation was backward delete a character. + // + // The editing was essentially removing the current empty line and move the cursor to the end of the previous line. + // In this case, what we want is to start rendering from the previous logical line, where the cursor is supposed + // to be moved to. However, if we only compare to find the first changed logical line, we will miss the right one + // here, because that logical line is the same as before since the next logical line wrapped to it is empty. + // + // If the current logical lines are less than the previous, and the cursor is at the beginning of the first line in buffer, + // then it's possible we are facing this special case and thus would need to do additional checks later. + minLineLength = renderLines.Length; if (cursorX == Options.ContinuationPrompt.Length && cursorY == 0) { + // Number of physical lines before counting the first line in buffer. linesToCheck = 0 - _initialY; } } - // Find the first logical line that was changed, then write starting from that logical line. + // Find the first logical line that was changed. for (; logicalLine < minLineLength; 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 _); @@ -513,6 +529,7 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi if (physicalLine == linesToCheck && previousRenderLines[logicalLine + 1].columns == Options.ContinuationPrompt.Length) { + // Additional check for the special case mentioned above: the cursor is supposed to be moved to the previous line. if (ConvertOffsetToPoint(_current).Y == -1) { physicalLine -= count; @@ -523,8 +540,8 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi if (logicalLine > 0) { - // The editing happens in the middle or end of the text. - // In this case, we only need to write starting from the first changed logical line. + // 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; @@ -539,17 +556,17 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi } else { - // It's possible that the changed logical line spans on multiple physical lines - // and the first a few physical lines have already been scrolled off the buffer, - // causing 'newTop' to be less than 0. + // 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, given the logical line was changed, we would 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 have been scrolled off the buffer will be re-rendered), - // so update 'physicalLine'. + // 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; @@ -562,11 +579,12 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi if (hasToWriteAll && !cursorMovedToInitialPos) { - // The editing happens in the first logical line. We have to write everything in this case. + // 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) { - // The prompt line can be displayed, so we clear the screen and invoke/print the prompt line. + // 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); @@ -586,9 +604,55 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi } } - renderLines = renderData.lines; - previousRenderLines = _previousRender.lines; + current.LogicalLineIndex = logicalLine; + current.PhysicalLineCount = physicalLine; + current.PseudoPhysicalLineOffset = pseudoPhysicalLineOffset; + + previous.LogicalLineIndex = previousLogicalLine; + previous.PhysicalLineCount = previousPhysicalLine; + previous.PseudoPhysicalLineOffset = 0; + } + + private void ReallyRender(RenderData renderData, string defaultColor) + { + string activeColor = ""; + int bufferWidth = _console.BufferWidth; + int bufferHeight = _console.BufferHeight; + + void UpdateColorsIfNecessary(string newColor) + { + if (!object.ReferenceEquals(newColor, activeColor)) + { + _console.Write(newColor); + activeColor = newColor; + } + } + + // In case the buffer was resized + RecomputeInitialCoords(); + renderData.bufferWidth = bufferWidth; + renderData.bufferHeight = bufferHeight; + + // Move the cursor to where we started, but make cursor invisible while we're rendering. + _console.CursorVisible = false; + + // 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. + RenderedLineInfo currentLineInfo = default, previousLineInfo = default; + CalculateWhereAndWhatToRender(cursorMovedToInitialPos, ref renderData, ref currentLineInfo, ref previousLineInfo); + + RenderedLineData[] previousRenderLines = _previousRender.lines; + int previousLogicalLine = previousLineInfo.LogicalLineIndex; + int previousPhysicalLine = previousLineInfo.PhysicalLineCount; + + RenderedLineData[] renderLines = renderData.lines; + int logicalLine = currentLineInfo.LogicalLineIndex; + int physicalLine = currentLineInfo.PhysicalLineCount; + int pseudoPhysicalLineOffset = currentLineInfo.PseudoPhysicalLineOffset; + int lenPrevLastLine = 0; int logicalLineStartIndex = logicalLine; int physicalLineStartCount = physicalLine; @@ -599,7 +663,7 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi 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. @@ -649,6 +713,7 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi UpdateColorsIfNecessary(defaultColor); + // The last logical line is shorter than our previous render? Clear them. for (int currentLines = physicalLine; currentLines < previousPhysicalLine;) { _console.SetCursorPosition(0, _initialY + currentLines); @@ -661,7 +726,7 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi } } - // Fewer lines than our last render? Clear them. + // Fewer logical lines than our previous render? Clear them. for (; previousLogicalLine < previousRenderLines.Length; previousLogicalLine++) { // No need to write new line if all we need is to clear the extra previous render. @@ -672,7 +737,8 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi // Preserve the current render data. _previousRender = renderData; - // If we counted pseudo physical lines, deduct them before updating '_initialY'. + // 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. @@ -689,10 +755,13 @@ int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenLastPhysi // 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, ""); + 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) From 4c33c668fb33e24bde97549e3377bcb37ac6af92 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 1 Aug 2019 14:37:51 -0700 Subject: [PATCH 3/6] Address the rendering issue that happens when shift-selecting text that has been scrolled up-off the buffer --- PSReadLine/Render.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index fdba95aec..aeec30c81 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -549,10 +549,17 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, ref Ren var newTop = _initialY + physicalLine; if (newTop == bufferHeight) { - // 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"); + 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 { From 249bce1265ed7872354d1fd9bd87bc8b49c4464d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 30 Aug 2019 12:03:39 -0700 Subject: [PATCH 4/6] Address the easy part of Jason's comments --- PSReadLine/Render.cs | 54 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index aeec30c81..602fff934 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -28,10 +28,12 @@ public override string ToString() public partial class PSConsoleReadLine { - struct RenderedLineInfo + struct LineInfoForRendering { - public int LogicalLineIndex; - public int PhysicalLineCount; + public int CurrentLogicalLineIndex; + public int CurrentPhysicalLineCount; + public int PreviousLogicalLineIndex; + public int PreviousPhysicalLineCount; public int PseudoPhysicalLineOffset; } @@ -467,7 +469,7 @@ private int PhysicalLineCount(int columns, bool isFirstLogicalLine, out int lenL /// 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, ref RenderData renderData, ref RenderedLineInfo current, ref RenderedLineInfo previous) + private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, RenderData renderData, out LineInfoForRendering lineInfoForRendering) { int bufferWidth = _console.BufferWidth; int bufferHeight = _console.BufferHeight; @@ -485,13 +487,12 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, ref Ren bool hasToWriteAll = true; - if (cursorY > _initialY && renderLines.Length > 1) + if (renderLines.Length > 1) { - // The current 'cursorTop' is below the initial 'cursorTop' and there are multiple logical lines. - // This indicates the user is editing in the middle or end of the existing text. - // In this case, it's possible that we can skip rendering until reaching the first changed logical line. + // 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 minLineLength = previousRenderLines.Length; + int minLinesLength = previousRenderLines.Length; int linesToCheck = -1; if (renderLines.Length < previousRenderLines.Length) @@ -510,8 +511,8 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, ref Ren // If the current logical lines are less than the previous, and the cursor is at the beginning of the first line in buffer, // then it's possible we are facing this special case and thus would need to do additional checks later. - minLineLength = renderLines.Length; - if (cursorX == Options.ContinuationPrompt.Length && cursorY == 0) + minLinesLength = renderLines.Length; + if (_initialY < 0 && cursorX == Options.ContinuationPrompt.Length && cursorY == 0) { // Number of physical lines before counting the first line in buffer. linesToCheck = 0 - _initialY; @@ -519,7 +520,7 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, ref Ren } // Find the first logical line that was changed. - for (; logicalLine < minLineLength; logicalLine++) + for (; logicalLine < minLinesLength; logicalLine++) { // Found the first different logical line? Break out the loop. if (renderLines[logicalLine].line != previousRenderLines[logicalLine].line) { break; } @@ -611,13 +612,12 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, ref Ren } } - current.LogicalLineIndex = logicalLine; - current.PhysicalLineCount = physicalLine; - current.PseudoPhysicalLineOffset = pseudoPhysicalLineOffset; - - previous.LogicalLineIndex = previousLogicalLine; - previous.PhysicalLineCount = previousPhysicalLine; - previous.PseudoPhysicalLineOffset = 0; + lineInfoForRendering = default; + lineInfoForRendering.CurrentLogicalLineIndex = logicalLine; + lineInfoForRendering.CurrentPhysicalLineCount = physicalLine; + lineInfoForRendering.PreviousLogicalLineIndex = previousLogicalLine; + lineInfoForRendering.PreviousPhysicalLineCount = previousPhysicalLine; + lineInfoForRendering.PseudoPhysicalLineOffset = pseudoPhysicalLineOffset; } private void ReallyRender(RenderData renderData, string defaultColor) @@ -640,24 +640,24 @@ void UpdateColorsIfNecessary(string newColor) renderData.bufferWidth = bufferWidth; renderData.bufferHeight = bufferHeight; - // Move the cursor to where we started, but make cursor invisible while we're rendering. + // Make cursor invisible while we're rendering. _console.CursorVisible = false; // 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. - RenderedLineInfo currentLineInfo = default, previousLineInfo = default; - CalculateWhereAndWhatToRender(cursorMovedToInitialPos, ref renderData, ref currentLineInfo, ref previousLineInfo); + LineInfoForRendering lineInfoForRendering; + CalculateWhereAndWhatToRender(cursorMovedToInitialPos, renderData, out lineInfoForRendering); RenderedLineData[] previousRenderLines = _previousRender.lines; - int previousLogicalLine = previousLineInfo.LogicalLineIndex; - int previousPhysicalLine = previousLineInfo.PhysicalLineCount; + int previousLogicalLine = lineInfoForRendering.PreviousLogicalLineIndex; + int previousPhysicalLine = lineInfoForRendering.PreviousPhysicalLineCount; RenderedLineData[] renderLines = renderData.lines; - int logicalLine = currentLineInfo.LogicalLineIndex; - int physicalLine = currentLineInfo.PhysicalLineCount; - int pseudoPhysicalLineOffset = currentLineInfo.PseudoPhysicalLineOffset; + int logicalLine = lineInfoForRendering.CurrentLogicalLineIndex; + int physicalLine = lineInfoForRendering.CurrentPhysicalLineCount; + int pseudoPhysicalLineOffset = lineInfoForRendering.PseudoPhysicalLineOffset; int lenPrevLastLine = 0; int logicalLineStartIndex = logicalLine; From ef6712022ded37f4d66c0bfafc0a95a33611fc08 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 30 Aug 2019 14:54:45 -0700 Subject: [PATCH 5/6] Address the hard part of Jason's comment --- PSReadLine/Render.cs | 46 +++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index 602fff934..1237a518c 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -473,8 +473,6 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, RenderD { int bufferWidth = _console.BufferWidth; int bufferHeight = _console.BufferHeight; - int cursorX = _console.CursorLeft; - int cursorY = _console.CursorTop; RenderedLineData[] previousRenderLines = _previousRender.lines; int previousLogicalLine = 0; @@ -497,25 +495,21 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, RenderD if (renderLines.Length < previousRenderLines.Length) { - // Handle a special case: - // - top part of the text has been scrolled up-off the buffer; - // - the cursor is at the beginning of the first line in buffer; - // - the first line contains nothing but a new-line character; - // - the edit operation was backward delete a character. - // - // The editing was essentially removing the current empty line and move the cursor to the end of the previous line. - // In this case, what we want is to start rendering from the previous logical line, where the cursor is supposed - // to be moved to. However, if we only compare to find the first changed logical line, we will miss the right one - // here, because that logical line is the same as before since the next logical line wrapped to it is empty. - // - // If the current logical lines are less than the previous, and the cursor is at the beginning of the first line in buffer, - // then it's possible we are facing this special case and thus would need to do additional checks later. - minLinesLength = renderLines.Length; - if (_initialY < 0 && cursorX == Options.ContinuationPrompt.Length && cursorY == 0) + + // 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 (_initialY < 0) { - // Number of physical lines before counting the first line in buffer. - linesToCheck = 0 - _initialY; + int y = ConvertOffsetToPoint(_current).Y; + if (y < 0) + { + // Number of physical lines from the initial row to the row where the cursor is supposed to be set at. + linesToCheck = y - _initialY + 1; + } } } @@ -528,14 +522,14 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, RenderD int count = PhysicalLineCount(renderLines[logicalLine].columns, logicalLine == 0, out _); physicalLine += count; - if (physicalLine == linesToCheck && previousRenderLines[logicalLine + 1].columns == Options.ContinuationPrompt.Length) + if (linesToCheck < 0) { - // Additional check for the special case mentioned above: the cursor is supposed to be moved to the previous line. - if (ConvertOffsetToPoint(_current).Y == -1) - { - physicalLine -= count; - break; - } + continue; + } + else if (physicalLine >= linesToCheck) + { + physicalLine -= count; + break; } } From 8e6be7d1a744421622fa8ec59ddb611a696203d2 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 11 Sep 2019 10:48:29 -0700 Subject: [PATCH 6/6] Address Rob's comments --- PSReadLine/Render.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index 1237a518c..a0f4757ad 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -360,8 +360,8 @@ private bool RenderErrorPrompt(RenderData renderData, string defaultColor) { // We may need to flip the color on the prompt if the error state changed. - var bufferWidth = _console.BufferWidth; - var promptText = _options.PromptText; + int bufferWidth = _console.BufferWidth; + string promptText = _options.PromptText; if (string.IsNullOrEmpty(promptText) || _initialY < 0) { @@ -373,7 +373,7 @@ private bool RenderErrorPrompt(RenderData renderData, string defaultColor) 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 changed. + // No need to flip the prompt color if the error state didn't change. return false; } @@ -413,7 +413,7 @@ private bool RenderErrorPrompt(RenderData renderData, string defaultColor) if (renderErrorPrompt) { - var color = renderData.errorPrompt ? _options._errorColor : defaultColor; + string color = renderData.errorPrompt ? _options._errorColor : defaultColor; if (renderData.errorPrompt && promptBufferCells != promptText.Length) { promptText = promptText.Substring(promptText.Length - promptBufferCells);