diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs index 6dcdc9f90..a59bdf4ef 100644 --- a/PSReadLine/BasicEditing.cs +++ b/PSReadLine/BasicEditing.cs @@ -267,10 +267,6 @@ private bool AcceptLineImpl(bool validate) _emphasisStart = -1; _emphasisLength = 0; - var insertionPoint = _current; - // Make sure cursor is at the end before writing the line - _current = _buffer.Length; - if (renderNeeded) { ForceRender(); @@ -281,6 +277,7 @@ private bool AcceptLineImpl(bool validate) // can report an error as it normally does. if (validate && !_statusIsErrorMessage) { + var insertionPoint = _current; var errorMessage = Validate(_ast); if (!string.IsNullOrWhiteSpace(errorMessage)) { @@ -308,8 +305,13 @@ private bool AcceptLineImpl(bool validate) ClearStatusMessage(render: true); } - // Let public API set cursor to end of line incase end of line is end of buffer - SetCursorPosition(_current); + // Make sure cursor is at the end before writing the line. + if (_current != _buffer.Length) + { + // Let public API set cursor to end of line incase end of line is end of buffer. + _current = _buffer.Length; + SetCursorPosition(_current); + } if (_prediction.ActiveView is PredictionListView listView) { diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index bef39e858..d107632a6 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2191,5 +2191,16 @@ internal static string SelectCommandArgumentDescription return ResourceManager.GetString("SelectCommandArgumentDescription", resourceCulture); } } + + /// + /// Looks up a localized string similar to: Cannot locate the offset in the rendered text that was pointed by the original cursor. Initial Coord: ({0}, {1}) Buffer: ({2}, {3}) Cursor: ({4}, {5}). + /// + internal static string FailedToConvertPointToRenderDataOffset + { + get + { + return ResourceManager.GetString("FailedToConvertPointToRenderDataOffset", resourceCulture); + } + } } } diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 61ca0ff41..f57835029 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -855,4 +855,7 @@ Or not saving history with: Make visual selection of the command arguments. + + Cannot locate the offset in the rendered text that was pointed by the original cursor. Initial Coord: ({0}, {1}) Buffer: ({2}, {3}) Cursor: ({4}, {5}) + diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 7fd302394..ba1a49247 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -509,6 +509,10 @@ private string InputLoop() var moveToLineCommandCount = _moveToLineCommandCount; var moveToEndOfLineCommandCount = _moveToEndOfLineCommandCount; + // We attempt to handle window resizing only once per a keybinding processing, because we assume the + // window resizing cannot and shouldn't happen within the processing of a given keybinding. + _handlePotentialResizing = true; + var key = ReadKey(); ProcessOneKey(key, _dispatchTable, ignoreIfNoAction: false, arg: null); if (_inputAccepted) @@ -709,10 +713,6 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) _delayedOneTimeInitCompleted = true; } - _previousRender = _initialPrevRender; - _previousRender.bufferWidth = _console.BufferWidth; - _previousRender.bufferHeight = _console.BufferHeight; - _previousRender.errorPrompt = false; _buffer.Clear(); _edits = new List(); _undoEditIndex = 0; @@ -728,6 +728,9 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) _initialY = _console.CursorTop; _initialForeground = _console.ForegroundColor; _initialBackground = _console.BackgroundColor; + _previousRender = _initialPrevRender; + _previousRender.UpdateConsoleInfo(_console); + _previousRender.initialY = _initialY; _statusIsErrorMessage = false; _initialOutputEncoding = _console.OutputEncoding; @@ -1033,6 +1036,8 @@ public static void InvokePrompt(ConsoleKeyInfo? key = null, object arg = null) _singleton._initialX = console.CursorLeft; _singleton._initialY = console.CursorTop; _singleton._previousRender = _initialPrevRender; + _singleton._previousRender.UpdateConsoleInfo(console); + _singleton._previousRender.initialY = _singleton._initialY; _singleton.Render(); console.CursorVisible = true; diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index d99313621..9fd67159d 100644 --- a/PSReadLine/Render.Helper.cs +++ b/PSReadLine/Render.Helper.cs @@ -45,12 +45,12 @@ private static string Spaces(int cnt) : new string(' ', cnt); } - private static int LengthInBufferCells(string str) + internal static int LengthInBufferCells(string str) { return LengthInBufferCells(str, 0, str.Length); } - private static int LengthInBufferCells(string str, int start, int end) + internal static int LengthInBufferCells(string str, int start, int end) { var sum = 0; for (var i = start; i < end; i++) @@ -70,7 +70,7 @@ private static int LengthInBufferCells(string str, int start, int end) return sum; } - private static int LengthInBufferCells(char c) + internal static int LengthInBufferCells(char c) { if (c < 256) { diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index 354cd8149..94df96316 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -12,6 +12,7 @@ using System.Runtime.InteropServices; using System.Text; using Microsoft.PowerShell.Internal; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { @@ -26,6 +27,115 @@ public override string ToString() } } + internal class RenderedLineData + { + public readonly string Line; + private readonly bool _isFirstLogicalLine; + + private int _physicalLineCount, _lengthOfLastPhsicalLine; + private int _bufferWidth, _initialX; + + public RenderedLineData(string line, bool isFirstLogicalLine) + { + Line = line; + _isFirstLogicalLine = isFirstLogicalLine; + } + + public int PhysicalLineCount(int bufferWidth, int initialX, out int lenLastPhysicalLine) + { + bool useCachedValues = bufferWidth == _bufferWidth && (!_isFirstLogicalLine || initialX == _initialX); + if (useCachedValues) + { + lenLastPhysicalLine = _lengthOfLastPhsicalLine; + return _physicalLineCount; + } + + _bufferWidth = bufferWidth; + _initialX = initialX; + + // The first logical line has the user prompt. + int x = _isFirstLogicalLine ? initialX : 0; + int y = 1; + lenLastPhysicalLine = 0; + + for (int i = 0; i < Line.Length; i++) + { + var c = Line[i]; + + // Simple escape sequence skipping. + if (c == 0x1b && (i + 1) < Line.Length && Line[i + 1] == '[') + { + i += 2; + while (i < Line.Length && Line[i] != 'm') + { + i++; + } + + continue; + } + + int size = PSConsoleReadLine.LengthInBufferCells(c); + if (x == 0 && lenLastPhysicalLine > 0) + { + y++; + lenLastPhysicalLine = 0; + } + + x += size; + lenLastPhysicalLine += size; + + if (x == bufferWidth) + { + x = 0; + } + else if (x > bufferWidth) + { + // It could wrap to the next line in case of a multi-cell character. + // If character didn't fit on current line, it will move entirely to the next line. + x = size; + y++; + lenLastPhysicalLine = size; + } + } + + _lengthOfLastPhsicalLine = lenLastPhysicalLine; + _physicalLineCount = y; + + return y; + } + } + + internal class RenderData + { + public int bufferWidth; + public int bufferHeight; + public int cursorLeft; + public int cursorTop; + public int initialY; + public bool errorPrompt; + public RenderedLineData[] lines; + + public void UpdateConsoleInfo(IConsole console) + { + bufferWidth = console.BufferWidth; + bufferHeight = console.BufferHeight; + cursorLeft = console.CursorLeft; + cursorTop = console.CursorTop; + } + } + + internal readonly struct RenderDataOffset + { + public RenderDataOffset(int logicalLineIndex, int visibleCharIndex) + { + LogicalLineIndex = logicalLineIndex; + VisibleCharIndex = visibleCharIndex; + } + + public readonly int LogicalLineIndex; + public readonly int VisibleCharIndex; + } + public partial class PSConsoleReadLine { struct LineInfoForRendering @@ -37,31 +147,19 @@ struct LineInfoForRendering public int PseudoPhysicalLineOffset; } - struct RenderedLineData - { - public string line; - public int columns; - } - - class RenderData - { - public int bufferWidth; - public int bufferHeight; - public bool errorPrompt; - public RenderedLineData[] lines; - } - private const int COMMON_WIDEST_CONSOLE_WIDTH = 160; - private readonly List _consoleBufferLines = new List(1) {new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)}; + private readonly List _consoleBufferLines = new(1) {new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)}; private static readonly string[] _spaces = new string[80]; private RenderData _previousRender; - private static readonly RenderData _initialPrevRender = new RenderData + private static readonly RenderData _initialPrevRender = new() { - lines = new[] { new RenderedLineData{ columns = 0, line = ""}} + lines = new[] { new RenderedLineData(line: "", isFirstLogicalLine: true) }, + errorPrompt = false }; private int _initialX; private int _initialY; private bool _waitingToRender; + private bool _handlePotentialResizing; private ConsoleColor _initialForeground; private ConsoleColor _initialBackground; @@ -145,8 +243,7 @@ private void ForceRender() for (var i = 0; i < logicalLineCount; i++) { var line = _consoleBufferLines[i].ToString(); - renderLines[i].line = line; - renderLines[i].columns = LengthInBufferCells(line); + renderLines[i] = new RenderedLineData(line, isFirstLogicalLine: i == 0); } // And then do the real work of writing to the screen. @@ -467,52 +564,6 @@ private bool RenderErrorPrompt(RenderData renderData, string defaultColor) return true; } - /// - /// 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) - { - if (columns == 0) - { - // This could happen for a new logical line with an empty-string continuation prompt. - lenLastPhysicalLine = 0; - return 1; - } - - int cnt = 1; - int bufferWidth = _console.BufferWidth; - - 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) - { - // 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. @@ -565,9 +616,9 @@ private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, RenderD for (; logicalLine < minLinesLength; logicalLine++) { // Found the first different logical line? Break out the loop. - if (renderLines[logicalLine].line != previousRenderLines[logicalLine].line) { break; } + if (renderLines[logicalLine].Line != previousRenderLines[logicalLine].Line) { break; } - int count = PhysicalLineCount(renderLines[logicalLine].columns, logicalLine == 0, out _); + int count = renderLines[logicalLine].PhysicalLineCount(bufferWidth, _initialX, out _); physicalLine += count; if (linesToCheck < 0) @@ -678,9 +729,7 @@ void UpdateColorsIfNecessary(string newColor) } // In case the buffer was resized - RecomputeInitialCoords(); - renderData.bufferWidth = bufferWidth; - renderData.bufferHeight = bufferHeight; + RecomputeInitialCoords(isTextBufferUnchanged: false); // Make cursor invisible while we're rendering. _console.CursorVisible = false; @@ -689,8 +738,7 @@ void UpdateColorsIfNecessary(string newColor) bool cursorMovedToInitialPos = RenderErrorPrompt(renderData, defaultColor); // Calculate what to render and where to start the rendering. - LineInfoForRendering lineInfoForRendering; - CalculateWhereAndWhatToRender(cursorMovedToInitialPos, renderData, out lineInfoForRendering); + CalculateWhereAndWhatToRender(cursorMovedToInitialPos, renderData, out LineInfoForRendering lineInfoForRendering); RenderedLineData[] previousRenderLines = _previousRender.lines; int previousLogicalLine = lineInfoForRendering.PreviousLogicalLineIndex; @@ -710,9 +758,9 @@ void UpdateColorsIfNecessary(string newColor) if (logicalLine != logicalLineStartIndex) _console.Write("\n"); var lineData = renderLines[logicalLine]; - _console.Write(lineData.line); + _console.Write(lineData.Line); - physicalLine += PhysicalLineCount(lineData.columns, logicalLine == 0, out int lenLastLine); + physicalLine += lineData.PhysicalLineCount(bufferWidth, _initialX, 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. @@ -722,9 +770,7 @@ void UpdateColorsIfNecessary(string newColor) while (physicalLine > previousPhysicalLine && previousLogicalLine < previousRenderLines.Length) { - previousPhysicalLine += PhysicalLineCount(previousRenderLines[previousLogicalLine].columns, - previousLogicalLine == 0, - out lenPrevLastLine); + previousPhysicalLine += previousRenderLines[previousLogicalLine].PhysicalLineCount(bufferWidth, _initialX, out lenPrevLastLine); previousLogicalLine += 1; } @@ -768,10 +814,13 @@ void UpdateColorsIfNecessary(string newColor) _console.SetCursorPosition(0, _initialY + currentLines); currentLines++; - var lenToClear = currentLines == previousPhysicalLine ? lenPrevLastLine : bufferWidth; - if (lenToClear > 0) + if (currentLines == previousPhysicalLine) { - _console.Write(Spaces(lenToClear)); + _console.Write(Spaces(lenPrevLastLine)); + } + else + { + _console.BlankRestOfLine(); } } @@ -791,7 +840,8 @@ void UpdateColorsIfNecessary(string newColor) } // No need to write new line if all we need is to clear the extra previous render. - _console.Write(Spaces(previousRenderLines[line].columns)); + int lineCount = previousRenderLines[line].PhysicalLineCount(bufferWidth, _initialX, out _); + WriteBlankLines(lineCount); } // Preserve the current render data. @@ -872,6 +922,9 @@ void UpdateColorsIfNecessary(string newColor) _console.SetCursorPosition(point.X, point.Y); _console.CursorVisible = true; + _previousRender.UpdateConsoleInfo(_console); + _previousRender.initialY = _initialY; + // TODO: set WindowTop if necessary _lastRenderTime.Restart(); @@ -945,19 +998,127 @@ private void GetRegion(out int start, out int length) } } - private void RecomputeInitialCoords() + private void RecomputeInitialCoords(bool isTextBufferUnchanged) { - if ((_previousRender.bufferWidth != _console.BufferWidth) - || (_previousRender.bufferHeight != _console.BufferHeight)) + if (!_handlePotentialResizing) + { + return; + } + + // We attempt to handle window resizing only once per a keybinding processing, because we assume the + // window resizing cannot and shouldn't happen within the processing of a given keybinding. + // This is, in particular, to avoid unneeded checks while we are in the 'MenuComplete' or a similar + // function that handles some keystroke inputs directly within the function, does rendering multiple + // times, and changes cursor position directly by '_console.SetCursorPosition'. + // For 'MenuComplete', we will not attempt to handle resizing while the menu is displayed, because + // that's simply a wrong thing to do. + _handlePotentialResizing = false; + + // Operations like menu completion and inline dynamic help may cause the screen buffer to scroll up, + // and '_initialY' would have been adjusted accordingly. + // In that case, we need to adjust the old cursor position accordingly too. + int preInitialY = _previousRender.initialY; + if (preInitialY != _initialY) + { + _previousRender.cursorTop -= preInitialY - _initialY; + _previousRender.initialY = _initialY; + } + + if (_previousRender.bufferWidth == _console.BufferWidth && + _previousRender.bufferHeight == _console.BufferHeight) + { + int left = _console.CursorLeft; + int top = _console.CursorTop; + + int preLeft = _previousRender.cursorLeft; + int preTop = _previousRender.cursorTop; + + if (preLeft == left && preTop > top) + { + // Try to handle a special scenario: the max-size terminal windows gets restored to + // the normal size, and then is immediately changed to max size again. + _initialY -= preTop - top; + } + else if (left == 0 && top == 0 && preLeft != 0 && preTop != 0) + { + // Try to handle a special scenario: a Terminal User Interface (TUI) utility is used + // with a custom key-binding to get rich editing experience, for example: + // Set-PSReadlineKeyHandler -Chord "Shift+Tab" -ScriptBlock { + // $s = fzf.exe + // [Microsoft.PowerShell.PSConsoleReadLine]::Insert($s) + // } + // The TUI utility will likely erase the screen buffer, so we try writing out prompt + // and start afresh in this case. + string newPrompt = GetPrompt(); + if (!string.IsNullOrEmpty(newPrompt)) + { + _console.Write(newPrompt); + } + + _initialX = _console.CursorLeft; + _initialY = _console.CursorTop; + _previousRender = _initialPrevRender; + } + + return; + } + + // If the console buffer width or height changed, our initial coordinates may have as well. + if (isTextBufferUnchanged) { - // If the buffer width changed, our initial coordinates - // may have as well. + // 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 = _initialX % _console.BufferWidth; + _initialX %= _console.BufferWidth; // Recompute Y from the cursor _initialY = 0; + // Calculate the new cursor position when assuming '_initialY' is at line 0. var pt = ConvertOffsetToPoint(_current); + // Update '_initialY' based on the difference from the actual current cursor position after the resize. + _initialY = _console.CursorTop - pt.Y; + } + else + { + // The '_buffer' and '_current' have changed since the last rendering, so we cannot rely on them + // for the re-calculation. A typical example would be the user clears the input with `Escape` after + // a resize. That will cause the '_buffer' to be empty and '_current' to be 0 when we reach here. + // + // Instead, we will use the saved previous cursor position to re-calculate the initial coordinates, + // based on the previous rendering data. + // First, calculate the offset in the previous rendering data based on the old initial coordinates, + // old buffer width, and the old cursor position. + RenderDataOffset offset = ConvertPointToRenderDataOffset(_initialX, _initialY, _previousRender); + if (offset.LogicalLineIndex == -1) + { + // This should never happen unless it's a bug in 'ConvertPointToRenderDataOffset'. + string message = string.Format( + CultureInfo.CurrentCulture, + PSReadLineResources.FailedToConvertPointToRenderDataOffset, + _initialX, + _initialY, + _previousRender.bufferWidth, + _previousRender.bufferHeight, + _previousRender.cursorLeft, + _previousRender.cursorTop); + throw new InvalidOperationException(message); + } + + // Recompute X from the buffer width: + _initialX %= _console.BufferWidth; + + // Recompute Y from the cursor + _initialY = 0; + // Now, use the new initial coordinates, new buffer width, and the rendering data offset to calculate + // the new cursor position when assuming '_initialY' is at line 0. + Point pt = ConvertRenderDataOffsetToPoint(_initialX, _initialY, _console.BufferWidth, _previousRender, offset); + // Update '_initialY' based on the difference from the actual current cursor position after the resize. + // This is based on the assumption that the cursor is still pointing to the same character after resizing, + // or at least pointing to the physical line where the same character is located after resizing. + // However, that assumption is not always guaranteed in Windows Terminal, see the issue: + // https://github.com/microsoft/terminal/issues/10848, and + // https://github.com/microsoft/terminal/issues/10868 _initialY = _console.CursorTop - pt.Y; } } @@ -968,9 +1129,7 @@ private void MoveCursor(int newCursor) if (!_waitingToRender) { // In case the buffer was resized - RecomputeInitialCoords(); - _previousRender.bufferWidth = _console.BufferWidth; - _previousRender.bufferHeight = _console.BufferHeight; + RecomputeInitialCoords(isTextBufferUnchanged: true); var point = ConvertOffsetToPoint(newCursor); if (point.Y < 0) @@ -981,7 +1140,8 @@ private void MoveCursor(int newCursor) if (point.Y == _console.BufferHeight) { - // The cursor top exceeds the buffer height, so adjust the initial cursor + // The cursor top exceeds the buffer height. This may happen when moving cursor to the end of line, + // while the end of line is actually the end of buffer. In this case, we adjust the initial cursor // position and the to-be-set cursor position for scrolling up the buffer. _initialY -= 1; point.Y -= 1; @@ -997,6 +1157,9 @@ private void MoveCursor(int newCursor) { _console.SetCursorPosition(point.X, point.Y); } + + _previousRender.UpdateConsoleInfo(_console); + _previousRender.initialY = _initialY; } // While waiting to render, and a keybinding has occured that is moving the cursor, @@ -1109,6 +1272,296 @@ private int ConvertLineAndColumnToOffset(Point point) return (point.Y == y) ? offset : -1; } + internal Point ConvertRenderDataOffsetToPoint(int initialX, int initialY, int bufferWidth, RenderData renderData, RenderDataOffset offset) + { + if (offset.LogicalLineIndex == 0 && offset.VisibleCharIndex == -1) + { + // (0, -1) means the cursor should be right at the initial coordinate. + return new Point { X = initialX, Y = initialY }; + } + + int x = initialX; + int y = initialY; + + int lengthOfLastPhysicalLine = -1; + int limit = offset.LogicalLineIndex; + if (offset.VisibleCharIndex == int.MaxValue) + { + limit++; + } + + // Make sure 'x' and 'y' are pointing to the end of the last processed logical line after this loop ends. + for (int i = 0; i < limit; i++) + { + if (i > 0) + { + // Move 'x' and 'y' to the start position where the next logical line would be rendered from. + // If 'x == 0 && lengthOfLastPhysicalLine == 0', it's a special case we need to handle: + // * the last logical line starts from the beginning of a physical line (x == 0), AND + // * the last logical line contains no visible character. + if (x > 0 || lengthOfLastPhysicalLine == 0) + { + x = 0; + y++; + } + } + + RenderedLineData lineData = renderData.lines[i]; + int physicalLineCount = lineData.PhysicalLineCount(bufferWidth, initialX, out lengthOfLastPhysicalLine); + y += physicalLineCount - 1; + + if (y == initialY) + { + x += lengthOfLastPhysicalLine; + } + else + { + x = lengthOfLastPhysicalLine; + } + + if (x == bufferWidth) + { + // In the case that the length of last physical line takes the whole buffer width, + // the cursor would be pushed to the start of the next line. + x = 0; + y++; + } + } + + if (offset.VisibleCharIndex == int.MaxValue) + { + // The cursor is right at the end of the logical line. + return new Point { X = x, Y = y }; + } + + if (limit > 0) + { + // The logical line we are going to scan character by character is not the first logical line, + // so we need to move 'x' and 'y' to the start of the next physical line. + if (x > 0 || lengthOfLastPhysicalLine == 0) + { + x = 0; + y++; + } + } + + int visibleCharIndex = -1, size = 0; + string line = renderData.lines[limit].Line; + for (int i = 0; i < line.Length; i++) + { + var c = line[i]; + + // Simple escape sequence skipping. + if (c == 0x1b && (i + 1) < line.Length && line[i + 1] == '[') + { + i += 2; + while (i < line.Length && line[i] != 'm') + { + i++; + } + + continue; + } + + visibleCharIndex++; + size = LengthInBufferCells(c); + + if (visibleCharIndex == offset.VisibleCharIndex) + { + break; + } + + x += size; + if (x == bufferWidth) + { + x = 0; + y++; + } + else if (x > bufferWidth) + { + // It could wrap to the next line in case of a multi-cell character. + // If character didn't fit on current line, it will move entirely to the next line. + x = size; + y++; + } + } + + // If the offset is pointing to a double-cell character that happens to be wrapped to the next physical line, + // then we move 'x' and 'y' to the start of the next physical line, so the new cursor continues to point to + // that specific character. + if (x + size > bufferWidth) + { + x = 0; + y++; + } + + return new Point { X = x, Y = y }; + } + + internal RenderDataOffset ConvertPointToRenderDataOffset(int initialX, int initialY, RenderData renderData) + { + int x = initialX; + int y = initialY; + var point = new Point { X = renderData.cursorLeft, Y = renderData.cursorTop }; + + if (point.Y == y && point.X == x) + { + // The given cursor is the same as the initial coordinate, return (0, -1) in this case. + return new RenderDataOffset(logicalLineIndex: 0, visibleCharIndex: -1); + } + + if (point.Y < y || (point.Y == y && point.X < x)) + { + // The given cursor is out of range, return (-1, -1). + return new RenderDataOffset(-1, -1); + } + + int prevX = 0, prevY = 0; + int logicalLineIndex = 0; + int bufferWidth = renderData.bufferWidth; + + for (; logicalLineIndex < renderData.lines.Length; logicalLineIndex++) + { + // Make 'prevX' and 'prevY' point to the start position where the current logical line would be rendered from. + prevX = x; + prevY = y; + + RenderedLineData lineData = renderData.lines[logicalLineIndex]; + int physicalLineCount = lineData.PhysicalLineCount(bufferWidth, initialX, out int lengthOfLastPhysicalLine); + y += physicalLineCount - 1; + + if (y == initialY) + { + x += lengthOfLastPhysicalLine; + } + else + { + x = lengthOfLastPhysicalLine; + } + + if (x == bufferWidth) + { + // In the case that the length of last physical line takes the whole buffer width, + // the cursor would be pushed to the start of the next line. + x = 0; + y++; + } + + if (point.Y == y && point.X == x) + { + // The cursor is right at the end of the logical line. + // We use 'int.MaxValue' as the character index to indicate that the whole logical line is included. + return new RenderDataOffset(logicalLineIndex, visibleCharIndex: int.MaxValue); + } + + if (point.Y < y || (point.Y == y && point.X < x)) + { + // The current logical line covers where the cursor is pointing at, so we will look for the character + // index within the logical line next. + break; + } + + // Now move 'x' and 'y' to the start position where the next logical line would be rendered from. + // - when 'x > 0', move to the start of the next line; + // - when 'x == 0', we either already did this from above, or it's a special case: + // * the current logical line starts from the beginning of a physical line (x == 0), AND + // * the current logical line contains no visible character. + if (x > 0 || lengthOfLastPhysicalLine == 0) + { + x = 0; + y++; + } + } + + // If we didn't find the given cursor within the range of the rendered lines, return (-1, -1). + if (logicalLineIndex == renderData.lines.Length || point.Y < prevY) + { + // - logicalLineIndex == renderData.lines.Length + // This could happen when the cursor was somehow pointing to a screen buffer position + // beyond the ending coordinate of the last logical line. + // + // - point.Y < prevY + // This could happen when the cursor was somehow pointing to a screen buffer position + // on the same last physical line of a logical line, but was beyond the ending X of + // the logical line. For example, the end of the logical line is at (x:3, y:2), and the + // cursor was pointing to (x:7, y:2). + // + // Both should never happen practically because we should never move cursor to an invalid + // position like that. But we need to handle the extreme situation where either of them + // just happened due to a bug in our code. + return new RenderDataOffset(-1, -1); + } + + // Now we have found the logical line that contains the visible character that the cursor was pointing at. + // Move 'x' and 'y' back to the start point where that logical line would be rendered from. + x = prevX; + y = prevY; + + // If it's right at where the cursor was pointing to, then we are done. + if (point.Y == y && point.X == x) + { + return new RenderDataOffset(logicalLineIndex, 0); + } + + // Now we will scan the current logical line to find which character the cursor was pointing at. + int visibleCharIndex = 0; + string line = renderData.lines[logicalLineIndex].Line; + + for (int i = 0; i < line.Length; i++) + { + var c = line[i]; + + // Simple escape sequence skipping. + if (c == 0x1b && (i + 1) < line.Length && line[i + 1] == '[') + { + i += 2; + while (i < line.Length && line[i] != 'm') + { + i++; + } + + continue; + } + + int size = LengthInBufferCells(c); + x += size; + + if (x == bufferWidth) + { + x = 0; + y++; + } + else if (x > bufferWidth) + { + // It could wrap to the next line in case of a multi-cell character. + // If character didn't fit on current line, it will move entirely to the next line. + x = size; + y++; + } + + if (point.Y == y) + { + if (point.X < x) + { + // This could happen when the cursor was pointing to a double-cell character + // that was wrapped to the next physical line -- because there was only one + // cell space left at the end of the previous physical line. + return new RenderDataOffset(logicalLineIndex, visibleCharIndex); + } + else if (point.X == x) + { + // 'x' is pointing to where the next visible character would be rendered. + return new RenderDataOffset(logicalLineIndex, visibleCharIndex + 1); + } + } + + visibleCharIndex++; + } + + // We should never reach here in theory. + return new RenderDataOffset(-1, -1); + } + /// /// Returns the logical line number under the cursor in a multi-line buffer. /// When rendering, a logical line may span multiple physical lines. diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index 65accd066..891df2f9d 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -46,18 +46,19 @@ - + PreserveNewest - - + PreserveNewest + + PreserveNewest - - + PreserveNewest + + + assets\%(RecursiveDir)\%(FileName)%(Extension) PreserveNewest - - - PreserveNewest - + PreserveNewest + diff --git a/test/ResizingTest.cs b/test/ResizingTest.cs new file mode 100644 index 000000000..a02ed783a --- /dev/null +++ b/test/ResizingTest.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Microsoft.PowerShell; +using Newtonsoft.Json; +using Xunit; + +namespace Test.Resizing +{ +#pragma warning disable 0649 + + /// + /// This class is initialized by JSON deserialization. + /// + internal sealed class LogicalToPhysicalLineTestData + { + public string Name; + public string Line; + public bool IsFirstLogicalLine; + public List Context; + } + + /// + /// This class is initialized by JSON deserialization. + /// + internal sealed class LogicalToPhysicalLineTestContext + { + public int BufferWidth; + public int InitialX; + public int LineCount; + public int LastLineLen; + } + + /// + /// This class is initialized by JSON deserialization. + /// + internal sealed class ResizingTestData + { + public string Name; + public List Lines; + public int OldBufferWidth; + public int NewBufferWidth; + public List Context; + } + + /// + /// This class is initialized by JSON deserialization. + /// + internal sealed class ResizingTestContext + { + public Point OldInitial; + public Point OldCursor; + public Point NewInitial; + public Point NewCursor; + public RenderOffset Offset; + + internal sealed class RenderOffset + { + public int LineIndex; + public int CharIndex; + } + } + +#pragma warning restore 0649 +} + +namespace Test +{ + using Test.Resizing; + + public partial class ReadLine + { + private static List s_resizingTestData; + + private void InitializeTestData() + { + if (s_resizingTestData is null) + { + string path = Path.Combine("assets", "resizing", "renderdata-to-cursor-point.json"); + string text = File.ReadAllText(path); + s_resizingTestData = JsonConvert.DeserializeObject>(text); + } + } + + private PSConsoleReadLine GetPSConsoleReadLineSingleton() + { + return (PSConsoleReadLine)typeof(PSConsoleReadLine) + .GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); + } + + [Fact] + public void ConvertPointToRenderDataOffset_ShouldWork() + { + InitializeTestData(); + PSConsoleReadLine instance = GetPSConsoleReadLineSingleton(); + + foreach (ResizingTestData test in s_resizingTestData) + { + RenderData renderData = new() + { + lines = new RenderedLineData[test.Lines.Count], + bufferWidth = test.OldBufferWidth + }; + + for (int i = 0; i < test.Lines.Count; i++) + { + renderData.lines[i] = new RenderedLineData(test.Lines[i], isFirstLogicalLine: i == 0); + } + + for (int j = 0; j < test.Context.Count; j++) + { + ResizingTestContext context = test.Context[j]; + renderData.cursorLeft = context.OldCursor.X; + renderData.cursorTop = context.OldCursor.Y; + + RenderDataOffset offset = instance.ConvertPointToRenderDataOffset(context.OldInitial.X, context.OldInitial.Y, renderData); + Assert.True( + context.Offset.LineIndex == offset.LogicalLineIndex && + context.Offset.CharIndex == offset.VisibleCharIndex, + $"{test.Name}-context_{j}: calculated offset is not what's expected [line: {offset.LogicalLineIndex}, char: {offset.VisibleCharIndex}]"); + } + } + } + + [Fact] + public void ConvertRenderDataOffsetToPoint_ShouldWork() + { + InitializeTestData(); + PSConsoleReadLine instance = GetPSConsoleReadLineSingleton(); + + foreach (ResizingTestData test in s_resizingTestData) + { + RenderData renderData = new() + { + lines = new RenderedLineData[test.Lines.Count], + bufferWidth = test.OldBufferWidth + }; + + for (int i = 0; i < test.Lines.Count; i++) + { + renderData.lines[i] = new RenderedLineData(test.Lines[i], isFirstLogicalLine: i == 0); + } + + for (int j = 0; j < test.Context.Count; j++) + { + ResizingTestContext context = test.Context[j]; + if (context.Offset.LineIndex != -1) + { + renderData.cursorLeft = context.OldCursor.X; + renderData.cursorTop = context.OldCursor.Y; + + var offset = new RenderDataOffset(context.Offset.LineIndex, context.Offset.CharIndex); + Point newCursor = instance.ConvertRenderDataOffsetToPoint(context.NewInitial.X, context.NewInitial.Y, test.NewBufferWidth, renderData, offset); + Assert.True( + context.NewCursor.X == newCursor.X && + context.NewCursor.Y == newCursor.Y, + $"{test.Name}-context_{j}: calculated new cursor is not what's expected [X: {newCursor.X}, Y: {newCursor.Y}]"); + } + } + } + } + + [Fact] + public void PhysicalLineCountMethod_ShouldWork() + { + var path = Path.Combine("assets", "resizing", "physical-line-count.json"); + var text = File.ReadAllText(path); + var testDataList = JsonConvert.DeserializeObject>(text); + + foreach (LogicalToPhysicalLineTestData test in testDataList) + { + RenderedLineData lineData = new(test.Line, test.IsFirstLogicalLine); + for (int i = 0; i < test.Context.Count; i++) + { + LogicalToPhysicalLineTestContext context = test.Context[i]; + int lineCount = lineData.PhysicalLineCount(context.BufferWidth, context.InitialX, out int lastLinelen); + Assert.True( + context.LineCount == lineCount && + context.LastLineLen == lastLinelen, + $"{test.Name}-context_{i}: calculated physical line count or length of last physical line is not what's expected [count: {lineCount}, lastLen: {lastLinelen}]"); + } + } + } + } +} diff --git a/test/assets/resizing/physical-line-count.json b/test/assets/resizing/physical-line-count.json new file mode 100644 index 000000000..9daf51929 --- /dev/null +++ b/test/assets/resizing/physical-line-count.json @@ -0,0 +1,225 @@ +[ + { + "Name": "BasicTest_1", + // The prompt and input used are actually: + // PS:1> new-ilNugetPackage -PackagePath C:\arena\tmp\NugetPackages -PackageVersion 7.2.0-preview.1 -WinFxdBinPath C:\Users\rocky\Downloads\Win\ -LinuxFxdBinPath C:\Users\rocky\Downloads\Linux\ -GenAPIToolPath C:\Users\rocky\Tools\genapi + "Line": "\u001b[0m\u001b[93mnew-ILNugetPackage\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackagePath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\arena\\tmp\\NugetPackages\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackageVersion\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m7.2.0-preview.1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-WinFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Win\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-LinuxFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Linux\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-GenAPIToolPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Tools\\genapi", + "IsFirstLogicalLine": true, + "Context": [ + { + "BufferWidth": 150, + "InitialX": 6, + "LineCount": 2, + "LastLineLen": 84 + }, + { + "BufferWidth": 102, + "InitialX": 6, + "LineCount": 3, + "LastLineLen": 30 + }, + { + "BufferWidth": 59, + "InitialX": 6, + "LineCount": 4, + "LastLineLen": 57 + }, + { + "BufferWidth": 58, + "InitialX": 6, + "LineCount": 5, + "LastLineLen": 2 + }, + { + "BufferWidth": 117, + "InitialX": 6, + "LineCount": 2, + "LastLineLen": 117 + }, + { + "BufferWidth": 234, + "InitialX": 6, + "LineCount": 1, + "LastLineLen": 228 + }, + { + "BufferWidth": 236, + "InitialX": 6, + "LineCount": 1, + "LastLineLen": 228 + } + ] + }, + + { + "Name": "BasicTest_2", + // The continuation prompt and input used are actually: + // >> new-ilNugetPackage -PackagePath C:\arena\tmp\NugetPackages -PackageVersion 7.2.0-preview.1 -WinFxdBinPath C:\Users\rocky\Downloads\Win\ -LinuxFxdBinPath C:\Users\rocky\Downloads\Linux\ -GenAPIToolPath C:\Users\rocky\Tools\genapi + "Line": "\u001b[0m\u001b[37m>> \u001b[0m\u001b[93mnew-ILNugetPackage\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackagePath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\arena\\tmp\\NugetPackages\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackageVersion\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m7.2.0-preview.1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-WinFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Win\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-LinuxFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Linux\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-GenAPIToolPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Tools\\genapi", + "IsFirstLogicalLine": false, + "Context": [ + { + "BufferWidth": 150, + "InitialX": 6, + "LineCount": 2, + "LastLineLen": 81 + }, + { + "BufferWidth": 102, + "InitialX": 6, + "LineCount": 3, + "LastLineLen": 27 + }, + { + "BufferWidth": 77, + "InitialX": 6, + "LineCount": 3, + "LastLineLen": 77 + }, + { + "BufferWidth": 54, + "InitialX": 6, + "LineCount": 5, + "LastLineLen": 15 + }, + { + "BufferWidth": 234, + "InitialX": 6, + "LineCount": 1, + "LastLineLen": 231 + }, + { + "BufferWidth": 236, + "InitialX": 6, + "LineCount": 1, + "LastLineLen": 231 + } + ] + }, + + { + "Name": "BasicTest_3", + // Empty prompt, and the input contains no visible character. + "Line": "\u001b[0m\u001b[93m", + "IsFirstLogicalLine": true, + "Context": [ + { + "BufferWidth": 58, + "InitialX": 6, + "LineCount": 1, + "LastLineLen": 0 + }, + { + "BufferWidth": 54, + "InitialX": 0, + "LineCount": 1, + "LastLineLen": 0 + } + ] + }, + + { + "Name": "BasicTest_4", + // Empty prompt, and the input contains no visible character. + "Line": "\u001b[0m\u001b[93m", + "IsFirstLogicalLine": false, + "Context": [ + { + "BufferWidth": 58, + "InitialX": 6, + "LineCount": 1, + "LastLineLen": 0 + }, + { + "BufferWidth": 54, + "InitialX": 0, + "LineCount": 1, + "LastLineLen": 0 + } + ] + }, + + { + "Name": "BasicTest_5", + // Continuation prompt followed by an empty line. + "Line": "\u001b[0m\u001b[93m>> ", + "IsFirstLogicalLine": false, + "Context": [ + { + "BufferWidth": 58, + "InitialX": 6, + "LineCount": 1, + "LastLineLen": 3 + }, + { + "BufferWidth": 54, + "InitialX": 0, + "LineCount": 1, + "LastLineLen": 3 + } + ] + }, + + { + "Name": "BasicTest_6", + // Input is: dir *.psd1 -Recurse | sls -SimpleMatch Desktop-ab + "Line": "\u001b[0m\u001b[93mdir\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m*.psd1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-Recurse\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m|\u001b[0m\u001b[39;49m \u001b[0m\u001b[93msls\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-SimpleMatch\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mDesktop-ab", + "IsFirstLogicalLine": true, + "Context": [ + { + "BufferWidth": 55, + "InitialX": 6, + "LineCount": 1, + "LastLineLen": 49 + }, + { + "BufferWidth": 54, + "InitialX": 6, + "LineCount": 2, + "LastLineLen": 1 + } + ] + }, + + { + "Name": "BasicTest_7", + // Input is: dir *.psd1 -Recurse | sls -SimpleMatch Desktop-有 + "Line": "\u001b[0m\u001b[93mdir\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m*.psd1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-Recurse\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m|\u001b[0m\u001b[39;49m \u001b[0m\u001b[93msls\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-SimpleMatch\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mDesktop-有", + "IsFirstLogicalLine": true, + "Context": [ + { + "BufferWidth": 54, + "InitialX": 6, + "LineCount": 2, + "LastLineLen": 2 + }, + { + "BufferWidth": 54, + "InitialX": 7, + "LineCount": 2, + "LastLineLen": 2 + } + ] + }, + + { + "Name": "BasicTest_8", + // dir *.psd1 -Recurse | sls -SimpleMatch Desktop-有人 + "Line": "\u001b[0m\u001b[93mdir\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m*.psd1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-Recurse\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m|\u001b[0m\u001b[39;49m \u001b[0m\u001b[93msls\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-SimpleMatch\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mDesktop-有人", + "IsFirstLogicalLine": true, + "Context": [ + { + "BufferWidth": 54, + "InitialX": 6, + "LineCount": 2, + "LastLineLen": 4 + }, + { + "BufferWidth": 54, + "InitialX": 7, + "LineCount": 2, + "LastLineLen": 4 + } + ] + } +] diff --git a/test/assets/resizing/renderdata-to-cursor-point.json b/test/assets/resizing/renderdata-to-cursor-point.json new file mode 100644 index 000000000..d481ad25b --- /dev/null +++ b/test/assets/resizing/renderdata-to-cursor-point.json @@ -0,0 +1,1008 @@ +[ + { + "Name": "BasicTest_1", + "Lines": [ + "\u001b[0m\u001b[93mnew-ilNugetPackage\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackagePath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\arena\\tmp\\NugetPackages\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackageVersion\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m7.2.0-preview.1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-WinFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Win\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-LinuxFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Linux\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-GenAPIToolPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Tools\\genapi", + "\u001b[0m\u001b[37m>> ", + "\u001b[0m\u001b[37m>> \u001b[0m\u001b[93mnew-ilNugetPackage\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackagePath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\arena\\tmp\\NugetPackages\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackageVersion\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m7.2.0-preview.1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-WinFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Win\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-LinuxFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Linux\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-GenAPIToolPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Tools\\genapi" + ], + "OldBufferWidth": 90, + "NewBufferWidth": 124, + "Context": [ + { + "OldInitial": { + "X": 7, + "Y": 9 + }, + // Pointing to the end of the third logical line. + "OldCursor": { + "X": 51, + "Y": 15 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 2147483647 + }, + + "NewInitial": { + "X": 7, + "Y": 9 + }, + "NewCursor": { + "X": 107, + "Y": 13 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 9 + }, + // Pointing to '\' at "Downloads\Linux" of the third logical line. + "OldCursor": { + "X": 0, + "Y": 15 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 180 + }, + + "NewInitial": { + "X": 7, + "Y": 9 + }, + "NewCursor": { + "X": 56, + "Y": 13 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 9 + }, + // Pointing to the space right after "Downloads\Linux" of the third logical line. + "OldCursor": { + "X": 7, + "Y": 15 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 187 + }, + + "NewInitial": { + "X": 7, + "Y": 9 + }, + "NewCursor": { + "X": 63, + "Y": 13 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 9 + }, + // Pointing to the start of the third logical line. + "OldCursor": { + "X": 3, + "Y": 13 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 3 + }, + + "NewInitial": { + "X": 7, + "Y": 10 + }, + "NewCursor": { + "X": 3, + "Y": 13 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 14 + }, + // Pointing to the end of the first physical line -- 'r' from "7.2.0-preview.1" of the first logical line. + "OldCursor": { + "X": 89, + "Y": 14 + }, + + "Offset": { + "LineIndex": 0, + "CharIndex": 82 + }, + + "NewInitial": { + "X": 7, + "Y": 14 + }, + "NewCursor": { + "X": 89, + "Y": 14 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 14 + }, + // Pointing to the start of the second physical line -- first 'e' from "7.2.0-preview.1" of the first logical line. + "OldCursor": { + "X": 0, + "Y": 15 + }, + + "Offset": { + "LineIndex": 0, + "CharIndex": 83 + }, + + "NewInitial": { + "X": 7, + "Y": 14 + }, + "NewCursor": { + "X": 90, + "Y": 14 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 14 + }, + // Pointing at the start of the second logical line. + "OldCursor": { + "X": 3, + "Y": 17 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 2147483647 + }, + + "NewInitial": { + "X": 7, + "Y": 14 + }, + "NewCursor": { + "X": 3, + "Y": 16 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 14 + }, + // Pointing to the initial coordinates. + "OldCursor": { + "X": 7, + "Y": 14 + }, + + "Offset": { + "LineIndex": 0, + "CharIndex": -1 + }, + + "NewInitial": { + "X": 7, + "Y": 14 + }, + "NewCursor": { + "X": 7, + "Y": 14 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 1 + }, + // Pointing to a position that is beyond the end of third logical line. + "OldCursor": { + "X": 7, + "Y": 8 + }, + + "Offset": { + "LineIndex": -1, + "CharIndex": -1 + }, + + "NewInitial": { + "X": -1, + "Y": -1 + }, + "NewCursor": { + "X": -1, + "Y": -1 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 1 + }, + // Pointing to a position that is beyond the end of first logical line at its last physical line. + "OldCursor": { + "X": 70, + "Y": 3 + }, + + "Offset": { + "LineIndex": -1, + "CharIndex": -1 + }, + + "NewInitial": { + "X": -1, + "Y": -1 + }, + "NewCursor": { + "X": -1, + "Y": -1 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 1 + }, + // Pointing to a position that is beyond the end of the second logical line, at its last physical line. + "OldCursor": { + "X": 4, + "Y": 4 + }, + + "Offset": { + "LineIndex": -1, + "CharIndex": -1 + }, + + "NewInitial": { + "X": -1, + "Y": -1 + }, + "NewCursor": { + "X": -1, + "Y": -1 + } + } + ] + }, + + { + "Name": "BasicTest_2", + "Lines": [ + "\u001b[0m\u001b[93mnew-ilNugetPackage\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackagePath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\arena\\tmp\\NugetPackages\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackageVersion\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m7.2.0-preview.1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-WinFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Win\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-LinuxFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Linux\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-GenAPIToolPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Tools\\genapi", + "\u001b[0m\u001b[37m>> ", + "\u001b[0m\u001b[37m>> \u001b[0m\u001b[93mnew-ilNugetPackage\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackagePath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\arena\\tmp\\NugetPackages\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackageVersion\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m7.2.0-preview.1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-WinFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Win\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-LinuxFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Linux\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-GenAPIToolPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Tools\\genapi" + ], + "OldBufferWidth": 124, + "NewBufferWidth": 90, + "Context": [ + { + "OldInitial": { + "X": 7, + "Y": 11 + }, + // Pointing to the end of the third logical line. + "OldCursor": { + "X": 107, + "Y": 15 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 2147483647 + }, + + "NewInitial": { + "X": 7, + "Y": 9 + }, + "NewCursor": { + "X": 51, + "Y": 15 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 11 + }, + // Pointing to '\' at "Downloads\Linux" of the third logical line. + "OldCursor": { + "X": 56, + "Y": 15 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 180 + }, + + "NewInitial": { + "X": 7, + "Y": 8 + }, + "NewCursor": { + "X": 0, + "Y": 14 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 11 + }, + // Pointing to the space right after "Downloads\Linux" of the third logical line. + "OldCursor": { + "X": 63, + "Y": 15 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 187 + }, + + "NewInitial": { + "X": 7, + "Y": 8 + }, + "NewCursor": { + "X": 7, + "Y": 14 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 11 + }, + // Pointing to the start of the third logical line. + "OldCursor": { + "X": 3, + "Y": 14 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 3 + }, + + "NewInitial": { + "X": 7, + "Y": 9 + }, + "NewCursor": { + "X": 3, + "Y": 13 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 11 + }, + // Pointing to the 'D' at "Downloads\Win" of the third logical line. + "OldCursor": { + "X": 0, + "Y": 15 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 124 + }, + + "NewInitial": { + "X": 7, + "Y": 9 + }, + "NewCursor": { + "X": 34, + "Y": 14 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 11 + }, + // Pointing to the end of the first physical line -- 'o' at "-WinFxdBinPath C:\Users\ro" of the first logical line. + "OldCursor": { + "X": 123, + "Y": 11 + }, + + "Offset": { + "LineIndex": 0, + "CharIndex": 116 + }, + + "NewInitial": { + "X": 7, + "Y": 9 + }, + "NewCursor": { + "X": 33, + "Y": 10 + } + }, + { + "OldInitial": { + "X": 7, + "Y": 11 + }, + // Pointing to the start of the second logical line. + "OldCursor": { + "X": 3, + "Y": 13 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 2147483647 + }, + + "NewInitial": { + "X": 7, + "Y": 9 + }, + "NewCursor": { + "X": 3, + "Y": 12 + } + } + ] + }, + + { + "Name": "BasicTest_3", + // Use empty string as the continuation prompt. + "Lines": [ + "\u001b[0m\u001b[93mnew-ilNugetPackage\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackagePath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\arena\\tmp\\NugetPackages\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackageVersion\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m7.2.0-preview.1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-WinFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Win\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-LinuxFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Linux\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-GenAPIToolPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Tools\\genapi", + "", + "\u001b[0m\u001b[93mnew-ilNugetPackage\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackagePath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\arena\\tmp\\NugetPackages\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackageVersion\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m7.2.0-preview.1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-WinFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Win\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-LinuxFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Linux\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-GenAPIToolPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Tools\\genapi", + "\u001b[0m\u001b[93mhello" + ], + "OldBufferWidth": 90, + "NewBufferWidth": 124, + "Context": [ + { + "OldInitial": { + "X": 6, + "Y": 9 + }, + // Pointing to the start of the third logical line. + "OldCursor": { + "X": 0, + "Y": 13 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 0 + }, + + "NewInitial": { + "X": 6, + "Y": 9 + }, + "NewCursor": { + "X": 0, + "Y": 12 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 9 + }, + // Pointing to the start of the second logical line. + "OldCursor": { + "X": 0, + "Y": 12 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 2147483647 + }, + + "NewInitial": { + "X": 6, + "Y": 9 + }, + "NewCursor": { + "X": 0, + "Y": 11 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 9 + }, + // Pointing to the start of the fourth logical line. + "OldCursor": { + "X": 0, + "Y": 16 + }, + + "Offset": { + "LineIndex": 3, + "CharIndex": 0 + }, + + "NewInitial": { + "X": 6, + "Y": 9 + }, + "NewCursor": { + "X": 0, + "Y": 14 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 9 + }, + // Pointing to 'e' of the fourth logical line. + "OldCursor": { + "X": 1, + "Y": 16 + }, + + "Offset": { + "LineIndex": 3, + "CharIndex": 1 + }, + + "NewInitial": { + "X": 6, + "Y": 9 + }, + "NewCursor": { + "X": 1, + "Y": 14 + } + } + ] + }, + + { + "Name": "BasicTest_4", + // Use empty string as the continuation prompt. + "Lines": [ + "\u001b[0m\u001b[93mhello", + "\u001b[0m\u001b[93mworld", + "\u001b[0m\u001b[93mnew-ilNugetPackage\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackagePath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\arena\\tmp\\NugetPackages\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-PackageVersion\u001b[0m\u001b[39;49m \u001b[0m\u001b[37m7.2.0-preview.1\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-WinFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Win\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-LinuxFxdBinPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Downloads\\Linux\\\u001b[0m\u001b[39;49m \u001b[0m\u001b[90m-GenAPIToolPath\u001b[0m\u001b[39;49m \u001b[0m\u001b[37mC:\\Users\\rocky\\Tools\\genapi" + ], + "OldBufferWidth": 90, + "NewBufferWidth": 124, + "Context": [ + { + "OldInitial": { + "X": 6, + "Y": 11 + }, + // Pointing to the position right after 'o' in the first logical line. + "OldCursor": { + "X": 11, + "Y": 11 + }, + + "Offset": { + "LineIndex": 0, + "CharIndex": 2147483647 + }, + + "NewInitial": { + "X": 6, + "Y": 11 + }, + "NewCursor": { + "X": 11, + "Y": 11 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 11 + }, + // Pointing to the 'w' in the second logical line. + "OldCursor": { + "X": 0, + "Y": 12 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 0 + }, + + "NewInitial": { + "X": 6, + "Y": 11 + }, + "NewCursor": { + "X": 0, + "Y": 12 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 11 + }, + // Pointing to 'r' in the second logical line. + "OldCursor": { + "X": 2, + "Y": 12 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 2 + }, + + "NewInitial": { + "X": 6, + "Y": 11 + }, + "NewCursor": { + "X": 2, + "Y": 12 + } + } + ] + }, + + { + "Name": "BasicTest_5", + // Use empty string as the continuation prompt. + "Lines": [ + "\u001b[0m\u001b[93m12345678901234567890123456789012345678901234567890123456789012345678901234567890123有4567890", + "\u001b[0m\u001b[93m12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789有准备0" + ], + "OldBufferWidth": 90, + "NewBufferWidth": 124, + "Context": [ + { + "OldInitial": { + "X": 6, + "Y": 12 + }, + // Pointing to '有' in the first logical line. + "OldCursor": { + "X": 0, + "Y": 13 + }, + + "Offset": { + "LineIndex": 0, + "CharIndex": 83 + }, + + "NewInitial": { + "X": 6, + "Y": 12 + }, + "NewCursor": { + "X": 89, + "Y": 12 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 12 + }, + // Pointing to '4' after the '有' in the first logical line. + "OldCursor": { + "X": 2, + "Y": 13 + }, + + "Offset": { + "LineIndex": 0, + "CharIndex": 84 + }, + + "NewInitial": { + "X": 6, + "Y": 12 + }, + "NewCursor": { + "X": 91, + "Y": 12 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 12 + }, + // Pointing to '有' in the second logical line. + "OldCursor": { + "X": 0, + "Y": 15 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 89 + }, + + "NewInitial": { + "X": 6, + "Y": 12 + }, + "NewCursor": { + "X": 89, + "Y": 13 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 12 + }, + // Pointing to '准' in the second logical line. + "OldCursor": { + "X": 2, + "Y": 15 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 90 + }, + + "NewInitial": { + "X": 6, + "Y": 12 + }, + "NewCursor": { + "X": 91, + "Y": 13 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 12 + }, + // Pointing to '备' in the second logical line. + "OldCursor": { + "X": 4, + "Y": 15 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 91 + }, + + "NewInitial": { + "X": 6, + "Y": 12 + }, + "NewCursor": { + "X": 93, + "Y": 13 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 12 + }, + // Pointing to the '0' right after '备' in the second logical line. + "OldCursor": { + "X": 6, + "Y": 15 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 92 + }, + + "NewInitial": { + "X": 6, + "Y": 11 + }, + "NewCursor": { + "X": 95, + "Y": 12 + } + } + ] + }, + + { + // Testing scenarios where the logical lines happen to fit in the whole buffer width. + "Name": "BasicTest_6", + "Lines": [ + "\u001b[0m\u001b[97m123456789012345678901234567890123456789012345678901234567890123456789012345678901234", + "\u001b[0m\u001b[37m>> \u001b[0m\u001b[97m123456789012345678901234567890123456789012345678901234567890123456789012345678901234567", + "\u001b[0m\u001b[37m>> \u001b[0m\u001b[97m123456" + ], + "OldBufferWidth": 90, + "NewBufferWidth": 124, + "Context": [ + { + "OldInitial": { + "X": 6, + "Y": 3 + }, + // Pointing to the end of the the third logical line. + "OldCursor": { + "X": 9, + "Y": 5 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 2147483647 + }, + + "NewInitial": { + "X": 6, + "Y": 3 + }, + "NewCursor": { + "X": 9, + "Y": 5 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 3 + }, + // Pointing to '3' of the third logical line. + "OldCursor": { + "X": 5, + "Y": 5 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 5 + }, + + "NewInitial": { + "X": 6, + "Y": 3 + }, + "NewCursor": { + "X": 5, + "Y": 5 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 3 + }, + // Pointing to '1' of the third logical line. + "OldCursor": { + "X": 3, + "Y": 5 + }, + + "Offset": { + "LineIndex": 2, + "CharIndex": 3 + }, + + "NewInitial": { + "X": 6, + "Y": 3 + }, + "NewCursor": { + "X": 3, + "Y": 5 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 3 + }, + // Pointing to the end of the second logical line. + "OldCursor": { + "X": 0, + "Y": 5 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 2147483647 + }, + + "NewInitial": { + "X": 6, + "Y": 3 + }, + "NewCursor": { + "X": 90, + "Y": 4 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 3 + }, + // Pointing to the end of the second logical line. + "OldCursor": { + "X": 89, + "Y": 4 + }, + + "Offset": { + "LineIndex": 1, + "CharIndex": 89 + }, + + "NewInitial": { + "X": 6, + "Y": 3 + }, + "NewCursor": { + "X": 89, + "Y": 4 + } + }, + { + "OldInitial": { + "X": 6, + "Y": 3 + }, + // Pointing to the end of the first logical line. + "OldCursor": { + "X": 0, + "Y": 4 + }, + + "Offset": { + "LineIndex": 0, + "CharIndex": 2147483647 + }, + + "NewInitial": { + "X": 6, + "Y": 3 + }, + "NewCursor": { + "X": 90, + "Y": 3 + } + } + ] + } +] diff --git a/tools/helper.psm1 b/tools/helper.psm1 index c5e4e0d9e..29ba9f06f 100644 --- a/tools/helper.psm1 +++ b/tools/helper.psm1 @@ -1,5 +1,5 @@ -$MinimalSDKVersion = '6.0.100-preview.1.21103.13' +$MinimalSDKVersion = '6.0.100' $IsWindowsEnv = [System.Environment]::OSVersion.Platform -eq "Win32NT" $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path $LocalDotnetDirPath = if ($IsWindowsEnv) { "$env:LocalAppData\Microsoft\dotnet" } else { "$env:HOME/.dotnet" } @@ -39,7 +39,7 @@ function Find-Dotnet $env:PATH = $LocalDotnetDirPath + [IO.Path]::PathSeparator + $env:PATH } else { - throw "Cannot find the dotnet SDK for .NET 5. Please specify '-Bootstrap' to install build dependencies." + throw "Cannot find the dotnet SDK with the version $MinimalSDKVersion or higher. Please specify '-Bootstrap' to install build dependencies." } } }