Files

1776 lines
72 KiB
C#

/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Management.Automation.Language;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.PowerShell.Internal;
using Microsoft.PowerShell.PSReadLine;
namespace Microsoft.PowerShell
{
internal struct Point
{
public int X;
public int Y;
public override string ToString()
{
return String.Format(CultureInfo.InvariantCulture, "{0},{1}", X, Y);
}
}
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
{
public int CurrentLogicalLineIndex;
public int CurrentPhysicalLineCount;
public int PreviousLogicalLineIndex;
public int PreviousPhysicalLineCount;
public int PseudoPhysicalLineOffset;
}
private const int COMMON_WIDEST_CONSOLE_WIDTH = 160;
private readonly List<StringBuilder> _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()
{
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;
private int _current;
private int _emphasisStart;
private int _emphasisLength;
private class SavedTokenState
{
internal Token[] Tokens { get; set; }
internal int Index { get; set; }
internal string Color { get; set; }
}
private void MaybeParseInput()
{
if (_tokens == null)
{
ParseInput();
}
}
private string ParseInput()
{
var text = _buffer.ToString();
_ast = Parser.ParseInput(text, out _tokens, out _parseErrors);
return text;
}
private void ClearStatusMessage(bool render)
{
_statusBuffer.Clear();
_statusLinePrompt = null;
_statusIsErrorMessage = false;
if (render)
{
RenderWithPredictionQueryPaused();
}
}
private void RenderWithPredictionQueryPaused()
{
// Sometimes we need to re-render the buffer to show status line, or to clear
// the visual selection, or to clear the visual emphasis.
// In those cases, the buffer text is unchanged, and thus we can skip querying
// for prediction during the rendering, but instead, use the existing results.
using var _ = _prediction.PauseQuery();
Render();
}
private void Render()
{
// If there are a bunch of keys queued up, skip rendering if we've rendered
// recently.
if (_queuedKeys.Count > 10 && (_lastRenderTime.ElapsedMilliseconds < 50))
{
// We won't render, but most likely the tokens will be different, so make
// sure we don't use old tokens, also allow garbage to get collected.
_tokens = null;
_ast = null;
_parseErrors = null;
_waitingToRender = true;
return;
}
ForceRender();
}
private void ForceRender()
{
var defaultColor = VTColorUtils.DefaultColor;
// Geneate a sequence of logical lines with escape sequences for coloring.
int logicalLineCount = GenerateRender(defaultColor);
// Now write that out (and remember what we did so we can clear previous renders
// and minimize writing more than necessary on the next render.)
var renderLines = new RenderedLineData[logicalLineCount];
var renderData = new RenderData {lines = renderLines};
for (var i = 0; i < logicalLineCount; i++)
{
var line = _consoleBufferLines[i].ToString();
renderLines[i] = new RenderedLineData(line, isFirstLogicalLine: i == 0);
}
// And then do the real work of writing to the screen.
// Rendering data is in reused
ReallyRender(renderData, defaultColor);
// Cleanup some excess buffers, saving a few because we know we'll use them.
var bufferCount = _consoleBufferLines.Count;
var excessBuffers = bufferCount - renderLines.Length;
if (excessBuffers > 5)
{
_consoleBufferLines.RemoveRange(renderLines.Length, excessBuffers);
}
}
private int GenerateRender(string defaultColor)
{
var text = ParseInput();
_prediction.QueryForSuggestion(text);
string color = defaultColor;
string activeColor = string.Empty;
bool afterLastToken = false;
int currentLogicalLine = 0;
bool inSelectedRegion = false;
void UpdateColorsIfNecessary(string newColor)
{
if (!object.ReferenceEquals(newColor, activeColor))
{
if (!inSelectedRegion)
{
_consoleBufferLines[currentLogicalLine]
.Append(VTColorUtils.AnsiReset)
.Append(newColor);
}
activeColor = newColor;
}
}
void RenderOneChar(char charToRender, bool toEmphasize)
{
if (charToRender == '\n')
{
if (inSelectedRegion)
{
// Turn off inverse before end of line, turn on after continuation prompt
_consoleBufferLines[currentLogicalLine].Append(VTColorUtils.AnsiReset);
}
currentLogicalLine += 1;
if (currentLogicalLine == _consoleBufferLines.Count)
{
_consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH));
}
// Reset the color for continuation prompt so the color sequence will always be explicitly
// specified for continuation prompt in the generated render strings.
// This is necessary because we will likely not rewrite all texts during rendering, and thus
// we cannot assume the continuation prompt can continue to use the active color setting from
// the previous rendering string.
activeColor = string.Empty;
if (Options.ContinuationPrompt.Length > 0)
{
UpdateColorsIfNecessary(Options._continuationPromptColor);
_consoleBufferLines[currentLogicalLine].Append(Options.ContinuationPrompt);
}
if (inSelectedRegion)
{
// Turn off inverse before end of line, turn on after continuation prompt
_consoleBufferLines[currentLogicalLine].Append(Options.SelectionColor);
}
return;
}
UpdateColorsIfNecessary(toEmphasize ? _options._emphasisColor : color);
if (char.IsControl(charToRender))
{
_consoleBufferLines[currentLogicalLine].Append('^');
_consoleBufferLines[currentLogicalLine].Append((char)('@' + charToRender));
}
else
{
_consoleBufferLines[currentLogicalLine].Append(charToRender);
}
}
foreach (var buf in _consoleBufferLines)
{
buf.Clear();
}
var tokenStack = new Stack<SavedTokenState>();
tokenStack.Push(new SavedTokenState
{
Tokens = _tokens,
Index = 0,
Color = defaultColor
});
int selectionStart = -1;
int selectionEnd = -1;
if (_visualSelectionCommandCount > 0)
{
GetRegion(out int regionStart, out int regionLength);
if (regionLength > 0)
{
selectionStart = regionStart;
selectionEnd = selectionStart + regionLength;
}
}
for (int i = 0; i < text.Length; i++)
{
if (i == selectionStart)
{
_consoleBufferLines[currentLogicalLine].Append(Options.SelectionColor);
inSelectedRegion = true;
}
else if (i == selectionEnd)
{
_consoleBufferLines[currentLogicalLine].Append(VTColorUtils.AnsiReset);
_consoleBufferLines[currentLogicalLine].Append(activeColor);
inSelectedRegion = false;
}
if (!afterLastToken)
{
// Figure out the color of the character - if it's in a token,
// use the tokens color otherwise use the initial color.
var state = tokenStack.Peek();
var token = state.Tokens[state.Index];
while (i == token.Extent.EndOffset)
{
if (state.Index == state.Tokens.Length - 1)
{
tokenStack.Pop();
if (tokenStack.Count == 0)
{
afterLastToken = true;
token = null;
color = defaultColor;
break;
}
else
{
state = tokenStack.Peek();
// It's possible that a 'StringExpandableToken' is the last available token, for example:
// 'begin $a\abc def', 'process $a\abc | blah' and 'end $a\abc; hello'
// due to the special handling of the keywords 'begin', 'process' and 'end', all the above 3 script inputs
// generate only 2 tokens by the parser -- A KeywordToken, and a StringExpandableToken '$a\abc'. Text after
// '$a\abc' is not tokenized at all.
// We repeat the test to see if we fall into this case ('token' is the final one in the stack).
continue;
}
}
color = state.Color;
token = state.Tokens[++state.Index];
}
if (!afterLastToken && i == token.Extent.StartOffset)
{
color = GetTokenColor(token);
if (token is StringExpandableToken stringToken)
{
// We might have nested tokens.
if (stringToken.NestedTokens != null && stringToken.NestedTokens.Any())
{
var tokens = new Token[stringToken.NestedTokens.Count + 1];
stringToken.NestedTokens.CopyTo(tokens, 0);
// NestedTokens doesn't have an "EOS" token, so we use
// the string literal token for that purpose.
tokens[tokens.Length - 1] = stringToken;
tokenStack.Push(new SavedTokenState
{
Tokens = tokens,
Index = 0,
Color = color
});
if (i == tokens[0].Extent.StartOffset)
{
color = GetTokenColor(tokens[0]);
}
}
}
}
}
var charToRender = text[i];
var toEmphasize = i >= _emphasisStart && i < (_emphasisStart + _emphasisLength);
RenderOneChar(charToRender, toEmphasize);
}
if (inSelectedRegion)
{
_consoleBufferLines[currentLogicalLine].Append(VTColorUtils.AnsiReset);
inSelectedRegion = false;
}
_prediction.ActiveView.RenderSuggestion(_consoleBufferLines, ref currentLogicalLine);
activeColor = string.Empty;
if (_statusLinePrompt != null)
{
currentLogicalLine += 1;
if (currentLogicalLine > _consoleBufferLines.Count - 1)
{
_consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH));
}
color = _statusIsErrorMessage ? Options._errorColor : defaultColor;
UpdateColorsIfNecessary(color);
foreach (char c in _statusLinePrompt)
{
_consoleBufferLines[currentLogicalLine].Append(c);
}
_consoleBufferLines[currentLogicalLine].Append(_statusBuffer);
}
return currentLogicalLine + 1;
}
/// <summary>
/// Flip the color on the prompt if the error state changed.
/// </summary>
/// <returns>
/// A bool value indicating whether we need to flip the color,
/// namely whether we moved cursor to the initial position.
/// </returns>
private bool RenderErrorPrompt(RenderData renderData, string defaultColor)
{
if (_initialY < 0
|| _options.PromptText == null
|| _options.PromptText.Length == 0
|| String.IsNullOrEmpty(_options.PromptText[0]))
{
// 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;
}
// We may need to flip the color on the prompt if the error state changed.
renderData.errorPrompt = (_parseErrors != null && _parseErrors.Length > 0);
if (renderData.errorPrompt == _previousRender.errorPrompt)
{
// No need to flip the prompt color if the error state didn't change.
return false;
}
// We need to update the prompt
_console.SetCursorPosition(_initialX, _initialY);
string promptText =
(renderData.errorPrompt && _options.PromptText.Length == 2)
? _options.PromptText[1]
: _options.PromptText[0];
// promptBufferCells is the number of visible characters in the prompt
int promptBufferCells = LengthInBufferCells(promptText);
bool renderErrorPrompt = false;
int bufferWidth = _console.BufferWidth;
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);
}
}
if (renderErrorPrompt)
{
if (!promptText.Contains('\x1b'))
{
string color = renderData.errorPrompt ? _options._errorColor : defaultColor;
_console.Write(color);
_console.Write(promptText);
_console.Write(VTColorUtils.AnsiReset);
}
else
{
_console.Write(promptText);
}
}
return true;
}
/// <summary>
/// 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.
/// </summary>
private void CalculateWhereAndWhatToRender(bool cursorMovedToInitialPos, RenderData renderData, out LineInfoForRendering lineInfoForRendering)
{
int bufferWidth = _console.BufferWidth;
int bufferHeight = _console.BufferHeight;
RenderedLineData[] previousRenderLines = _previousRender.lines;
int previousLogicalLine = 0;
int previousPhysicalLine = 0;
RenderedLineData[] renderLines = renderData.lines;
int logicalLine = 0;
int physicalLine = 0;
int pseudoPhysicalLineOffset = 0;
bool hasToWriteAll = true;
if (renderLines.Length > 1)
{
// There are multiple logical lines, so it's possible the first N logical lines are not affected by the user's editing,
// in which case, we can skip rendering until reaching the first changed logical line.
int minLinesLength = previousRenderLines.Length;
int linesToCheck = -1;
if (renderLines.Length < previousRenderLines.Length)
{
minLinesLength = renderLines.Length;
// 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)
{
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;
}
}
}
// Find the first logical line that was changed.
for (; logicalLine < minLinesLength; logicalLine++)
{
// Found the first different logical line? Break out the loop.
if (renderLines[logicalLine].Line != previousRenderLines[logicalLine].Line) { break; }
int count = renderLines[logicalLine].PhysicalLineCount(bufferWidth, _initialX, out _);
physicalLine += count;
if (linesToCheck < 0)
{
continue;
}
else if (physicalLine >= linesToCheck)
{
physicalLine -= count;
break;
}
}
if (logicalLine > 0)
{
// Some logical lines at the top were not affected by the editing.
// We only need to write starting from the first changed logical line.
hasToWriteAll = false;
previousLogicalLine = logicalLine;
previousPhysicalLine = physicalLine;
var newTop = _initialY + physicalLine;
if (newTop == bufferHeight)
{
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
{
// For the logical line that we will start to re-render from, it's possible that
// 1. the whole logical line had already been scrolled up-off the buffer. This could happen when you backward delete characters
// on the first line in buffer and cause the current line to be folded to the previous line.
// 2. the logical line spans on multiple physical lines and the top a few physical lines had already been scrolled off the buffer.
// This could happen when you edit on the top a few physical lines in the buffer, which belong to a longer logical line.
// Either of them will cause 'newTop' to be less than 0.
if (newTop < 0)
{
// In this case, we will render the whole logical line starting from the upper-left-most point of the window.
// By doing this, we are essentially adding a few pseudo physical lines (the physical lines that belong to the logical line but
// had been scrolled off the buffer would be re-rendered). So, update 'physicalLine'.
pseudoPhysicalLineOffset = 0 - newTop;
physicalLine += pseudoPhysicalLineOffset;
newTop = 0;
}
_console.SetCursorPosition(left: 0, top: newTop);
}
}
}
if (hasToWriteAll && !cursorMovedToInitialPos)
{
// The editing was in the first logical line. We have to write everything in this case.
// Move the cursor to the initial position if we haven't done so.
if (_initialY < 0)
{
// The prompt had been scrolled up-off the buffer. Now we are about to render from the very
// beginning, so we clear the screen and invoke/print the prompt line.
_console.Write("\x1b[2J");
_console.SetCursorPosition(0, _console.WindowTop);
string newPrompt = GetPrompt();
if (!string.IsNullOrEmpty(newPrompt))
{
_console.Write(newPrompt);
}
_initialX = _console.CursorLeft;
_initialY = _console.CursorTop;
_previousRender = _initialPrevRender;
}
else
{
_console.SetCursorPosition(_initialX, _initialY);
}
}
lineInfoForRendering = default;
lineInfoForRendering.CurrentLogicalLineIndex = logicalLine;
lineInfoForRendering.CurrentPhysicalLineCount = physicalLine;
lineInfoForRendering.PreviousLogicalLineIndex = previousLogicalLine;
lineInfoForRendering.PreviousPhysicalLineCount = previousPhysicalLine;
lineInfoForRendering.PseudoPhysicalLineOffset = pseudoPhysicalLineOffset;
}
private void ReallyRender(RenderData renderData, string defaultColor)
{
string activeColor = "";
int bufferWidth = _console.BufferWidth;
int bufferHeight = _console.BufferHeight;
void UpdateColorsIfNecessary(string newColor)
{
if (!object.ReferenceEquals(newColor, activeColor))
{
_console.Write(newColor);
activeColor = newColor;
}
}
// In case the buffer was resized
RecomputeInitialCoords(isTextBufferUnchanged: false);
// 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.
CalculateWhereAndWhatToRender(cursorMovedToInitialPos, renderData, out LineInfoForRendering lineInfoForRendering);
RenderedLineData[] previousRenderLines = _previousRender.lines;
int previousLogicalLine = lineInfoForRendering.PreviousLogicalLineIndex;
int previousPhysicalLine = lineInfoForRendering.PreviousPhysicalLineCount;
RenderedLineData[] renderLines = renderData.lines;
int logicalLine = lineInfoForRendering.CurrentLogicalLineIndex;
int physicalLine = lineInfoForRendering.CurrentPhysicalLineCount;
int pseudoPhysicalLineOffset = lineInfoForRendering.PseudoPhysicalLineOffset;
int lenPrevLastLine = 0;
int logicalLineStartIndex = logicalLine;
int physicalLineStartCount = physicalLine;
for (; logicalLine < renderLines.Length; logicalLine++)
{
if (logicalLine != logicalLineStartIndex) _console.Write("\n");
var lineData = renderLines[logicalLine];
_console.Write(lineData.Line);
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.
// We don't clear it unconditionally to allow things like a prompt
// on the right side of the line.
while (physicalLine > previousPhysicalLine
&& previousLogicalLine < previousRenderLines.Length)
{
previousPhysicalLine += previousRenderLines[previousLogicalLine].PhysicalLineCount(bufferWidth, _initialX, out lenPrevLastLine);
previousLogicalLine += 1;
}
// Our current physical line might be in the middle of the
// previous logical line, in which case we need to blank
// the rest of the line, otherwise we blank just the end
// of what was written.
int lenToClear = 0;
if (physicalLine == previousPhysicalLine)
{
// We're on the end of the previous logical line, so we
// only need to clear any extra.
if (lenPrevLastLine > lenLastLine)
lenToClear = lenPrevLastLine - lenLastLine;
}
else if (physicalLine < previousPhysicalLine)
{
// We're in the middle of a previous logical line, we
// need to clear to the end of the line.
if (lenLastLine < bufferWidth)
{
lenToClear = bufferWidth - lenLastLine;
if (physicalLine == 1)
lenToClear -= _initialX;
}
}
if (lenToClear > 0)
{
UpdateColorsIfNecessary(defaultColor);
_console.Write(Spaces(lenToClear));
}
}
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);
currentLines++;
if (currentLines == previousPhysicalLine)
{
_console.Write(Spaces(lenPrevLastLine));
}
else
{
_console.BlankRestOfLine();
}
}
// Fewer logical lines than our previous render? Clear them.
for (int line = previousLogicalLine; line < previousRenderLines.Length; line++)
{
if (line > previousLogicalLine || logicalLineStartIndex < renderLines.Length)
{
// For the first of the remaining previous logical lines, if we didn't actually
// render anything for the current logical lines, then the cursor is already at
// the beginning of the right physical line that should be cleared, and thus no
// need to write a new line in such case.
// In other cases, we need to write a new line to get the cursor to the correct
// physical line.
_console.Write("\n");
}
// No need to write new line if all we need is to clear the extra previous render.
int lineCount = previousRenderLines[line].PhysicalLineCount(bufferWidth, _initialX, out _);
WriteBlankLines(lineCount);
}
// Preserve the current render data.
_previousRender = renderData;
// If we counted pseudo physical lines, deduct them to get the real physical line counts
// before updating '_initialY'.
physicalLine -= pseudoPhysicalLineOffset;
// Reset the colors after we've finished all our rendering.
_console.Write(VTColorUtils.AnsiReset);
if (_initialY + physicalLine > bufferHeight)
{
// We had to scroll to render everything, update _initialY
_initialY = bufferHeight - physicalLine;
}
else if (pseudoPhysicalLineOffset > 0)
{
// When we rewrote a logical line (or part of a logical line) that had previously been scrolled up-off
// the buffer (fully or partially), we need to adjust '_initialY' if the changes to that logical line
// don't result in the same number of physical lines to be scrolled up-off the buffer.
// Calculate the total number of physical lines starting from the logical line we re-wrote.
int physicalLinesStartingFromTheRewrittenLogicalLine =
physicalLine - (physicalLineStartCount - pseudoPhysicalLineOffset);
Debug.Assert(
bufferHeight + pseudoPhysicalLineOffset >= physicalLinesStartingFromTheRewrittenLogicalLine,
"number of physical lines starting from the first changed logical line should be no more than the buffer height plus the pseudo lines we added.");
int offset = physicalLinesStartingFromTheRewrittenLogicalLine > bufferHeight
? pseudoPhysicalLineOffset - (physicalLinesStartingFromTheRewrittenLogicalLine - bufferHeight)
: pseudoPhysicalLineOffset;
_initialY += offset;
}
// Calculate the coord to place the cursor for the next input.
var point = ConvertOffsetToPoint(_current);
if (point.Y == bufferHeight)
{
// The cursor top exceeds the buffer height, so we need to
// scroll up the buffer by 1 line.
_console.Write("\n");
// Adjust the initial cursor position and the to-be-set cursor position
// after scrolling up the buffer.
_initialY -= 1;
point.Y -= 1;
}
else if (point.Y < 0)
{
// This could happen in at least 3 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 (Backspace) 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).
// 3. Both 'bck-i-search' and 'fwd-i-search' may find a history command with multi-line text, and the
// matching string in the text, where the cursor is supposed to be moved to, will be scrolled up-off
// the buffer after rendering.
//
// In these case, we move the cursor to the left-most position of the first line, where it's closest to
// the real position it should be in the ideal world.
// First update '_current' to the index of the first character that appears on the line 0,
// then we call 'ConvertOffsetToPoint' again to get the right cursor position to use.
point.X = point.Y = 0;
_current = ConvertLineAndColumnToOffset(point);
point = ConvertOffsetToPoint(_current);
}
_console.SetCursorPosition(point.X, point.Y);
_console.CursorVisible = true;
_previousRender.UpdateConsoleInfo(_console);
_previousRender.initialY = _initialY;
// TODO: set WindowTop if necessary
_lastRenderTime.Restart();
_waitingToRender = false;
}
private string GetTokenColor(Token token)
{
if ((token.TokenFlags & TokenFlags.CommandName) != 0)
{
return _options._commandColor;
}
switch (token.Kind)
{
case TokenKind.Comment:
return _options._commentColor;
case TokenKind.Parameter:
case TokenKind.Generic when token is StringLiteralToken slt && slt.Text.StartsWith("--"):
return _options._parameterColor;
case TokenKind.Variable:
case TokenKind.SplattedVariable:
return _options._variableColor;
case TokenKind.StringExpandable:
case TokenKind.StringLiteral:
case TokenKind.HereStringExpandable:
case TokenKind.HereStringLiteral:
return _options._stringColor;
case TokenKind.Number:
return _options._numberColor;
}
if ((token.TokenFlags & TokenFlags.Keyword) != 0)
{
return _options._keywordColor;
}
if (token.Kind != TokenKind.Generic && (token.TokenFlags & (TokenFlags.BinaryOperator | TokenFlags.UnaryOperator | TokenFlags.AssignmentOperator)) != 0)
{
return _options._operatorColor;
}
if ((token.TokenFlags & TokenFlags.TypeName) != 0)
{
return _options._typeColor;
}
if ((token.TokenFlags & TokenFlags.MemberName) != 0)
{
return _options._memberColor;
}
return _options._defaultTokenColor;
}
private void GetRegion(out int start, out int length)
{
if (_mark < _current)
{
start = _mark;
length = _current - start;
}
else
{
start = _current;
length = _mark - start;
}
}
private void RecomputeInitialCoords(bool isTextBufferUnchanged)
{
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)
{
// The '_buffer' and '_current' still reflects what has been rendered on the screen,
// so we can use them to re-calculate the initial coordinates in this case.
// Recompute X from the buffer width:
_initialX %= _console.BufferWidth;
// Recompute 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;
}
}
private void MoveCursor(int newCursor)
{
// Only update screen cursor if the buffer is fully rendered.
if (!_waitingToRender)
{
// In case the buffer was resized
RecomputeInitialCoords(isTextBufferUnchanged: true);
var point = ConvertOffsetToPoint(newCursor);
if (point.Y < 0)
{
Ding();
return;
}
if (point.Y == _console.BufferHeight)
{
// 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;
// Insure the cursor is on the last line of the buffer prior
// to issuing a newline to scroll the buffer.
_console.SetCursorPosition(point.X, point.Y);
// Scroll up the buffer by 1 line.
_console.Write("\n");
}
else
{
_console.SetCursorPosition(point.X, point.Y);
}
_previousRender.UpdateConsoleInfo(_console);
_previousRender.initialY = _initialY;
}
// While waiting to render, and a keybinding has occurred that is moving the cursor,
// converting offset to point could potentially result in an invalid screen position,
// but the insertion point should reflect the move.
_current = newCursor;
}
internal Point ConvertOffsetToPoint(int offset)
{
int x = _initialX;
int y = _initialY;
int bufferWidth = _console.BufferWidth;
var continuationPromptLength = LengthInBufferCells(Options.ContinuationPrompt);
for (int i = 0; i < offset; i++)
{
char c = _buffer[i];
if (c == '\n')
{
y += 1;
x = continuationPromptLength;
}
else
{
int size = LengthInBufferCells(c);
x += size;
// Wrap? No prompt when wrapping
if (x >= bufferWidth)
{
// If character didn't fit on current line, it will move entirely to the next line.
x = ((x == bufferWidth) ? 0 : size);
// If cursor is at column 0 and the next character is newline, let the next loop
// iteration increment y.
if (x != 0 || !(i + 1 < offset && _buffer[i + 1] == '\n'))
{
y += 1;
}
}
}
}
// If next character actually exists, and isn't newline, check if wider than the space left on the current line.
if (_buffer.Length > offset && _buffer[offset] != '\n')
{
int size = LengthInBufferCells(_buffer[offset]);
if (x + size > bufferWidth)
{
// Character was wider than remaining space, so character, and cursor, appear on next line.
x = 0;
y++;
}
}
return new Point {X = x, Y = y};
}
private int ConvertLineAndColumnToOffset(Point point)
{
int offset;
int x = _initialX;
int y = _initialY;
int bufferWidth = _console.BufferWidth;
var continuationPromptLength = LengthInBufferCells(Options.ContinuationPrompt);
for (offset = 0; offset < _buffer.Length; offset++)
{
// If we are on the correct line, return when we find
// the correct column
if (point.Y == y && point.X <= x)
{
return offset;
}
char c = _buffer[offset];
if (c == '\n')
{
// If we are about to move off of the correct line,
// the line was shorter than the column we wanted so return.
if (point.Y == y)
{
return offset;
}
y += 1;
x = continuationPromptLength;
}
else
{
int size = LengthInBufferCells(c);
x += size;
// Wrap? No prompt when wrapping
if (x >= bufferWidth)
{
// If character didn't fit on current line, it will move entirely to the next line.
x = ((x == bufferWidth) ? 0 : size);
// If cursor is at column 0 and the next character is newline, let the next loop
// iteration increment y.
if (x != 0 || !(offset + 1 < _buffer.Length && _buffer[offset + 1] == '\n'))
{
y += 1;
}
}
}
}
// Return -1 if y is out of range, otherwise the last line was shorter
// than we wanted, but still in range so just return the last offset.
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);
}
/// <summary>
/// Returns the logical line number under the cursor in a multi-line buffer.
/// When rendering, a logical line may span multiple physical lines.
/// </summary>
private int GetLogicalLineNumber()
{
var current = _current;
var lineNumber = 1;
for (int i = 0; i < current; i++)
{
if (_buffer[i] == '\n')
{
lineNumber++;
}
}
return lineNumber;
}
/// <summary>
/// Returns the number of logical lines in a multi-line buffer.
/// When rendering, a logical line may span multiple physical lines.
/// </summary>
private int GetLogicalLineCount()
{
var count = 1;
for (int i = 0; i < _buffer.Length; i++)
{
if (_buffer[i] == '\n')
{
count++;
}
}
return count;
}
private bool LineIsMultiLine()
{
for (int i = 0; i < _buffer.Length; i++)
{
if (_buffer[i] == '\n')
return true;
}
return false;
}
private int GetStatusLineCount()
{
if (_statusLinePrompt == null)
return 0;
return (_statusLinePrompt.Length + _statusBuffer.Length) / _console.BufferWidth + 1;
}
[ExcludeFromCodeCoverage]
void IPSConsoleReadLineMockableMethods.Ding()
{
switch (Options.BellStyle)
{
case BellStyle.None:
break;
case BellStyle.Audible:
if (Options.DingDuration > 0)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Console.Beep(Options.DingTone, Options.DingDuration);
}
else
{
Console.Beep();
}
}
break;
case BellStyle.Visual:
// TODO: flash prompt? command line?
break;
}
}
/// <summary>
/// Notify the user based on their preference for notification.
/// </summary>
public static void Ding()
{
_singleton._mockableMethods.Ding();
}
private bool PromptYesOrNo(string s)
{
_statusLinePrompt = s;
Render();
var key = ReadKey();
_statusLinePrompt = null;
Render();
return key.KeyStr.Equals("y", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Scroll the display up one screen.
/// </summary>
public static void ScrollDisplayUp(ConsoleKeyInfo? key = null, object arg = null)
{
TryGetArgAsInt(arg, out var numericArg, +1);
var console = _singleton._console;
var newTop = console.WindowTop - (numericArg * console.WindowHeight);
if (newTop < 0)
{
newTop = 0;
}
console.SetWindowPosition(0, newTop);
}
/// <summary>
/// Scroll the display up one line.
/// </summary>
public static void ScrollDisplayUpLine(ConsoleKeyInfo? key = null, object arg = null)
{
TryGetArgAsInt(arg, out var numericArg, +1);
var console = _singleton._console;
var newTop = console.WindowTop - numericArg;
if (newTop < 0)
{
newTop = 0;
}
console.SetWindowPosition(0, newTop);
}
/// <summary>
/// Scroll the display down one screen.
/// </summary>
public static void ScrollDisplayDown(ConsoleKeyInfo? key = null, object arg = null)
{
TryGetArgAsInt(arg, out var numericArg, +1);
var console = _singleton._console;
var newTop = console.WindowTop + (numericArg * console.WindowHeight);
if (newTop > (console.BufferHeight - console.WindowHeight))
{
newTop = (console.BufferHeight - console.WindowHeight);
}
console.SetWindowPosition(0, newTop);
}
/// <summary>
/// Scroll the display down one line.
/// </summary>
public static void ScrollDisplayDownLine(ConsoleKeyInfo? key = null, object arg = null)
{
TryGetArgAsInt(arg, out var numericArg, +1);
var console = _singleton._console;
var newTop = console.WindowTop + numericArg;
if (newTop > (console.BufferHeight - console.WindowHeight))
{
newTop = (console.BufferHeight - console.WindowHeight);
}
console.SetWindowPosition(0, newTop);
}
/// <summary>
/// Scroll the display to the top.
/// </summary>
public static void ScrollDisplayTop(ConsoleKeyInfo? key = null, object arg = null)
{
_singleton._console.SetWindowPosition(0, 0);
}
/// <summary>
/// Scroll the display to the cursor.
/// </summary>
public static void ScrollDisplayToCursor(ConsoleKeyInfo? key = null, object arg = null)
{
// Ideally, we'll put the last input line at the bottom of the window
var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length);
var console = _singleton._console;
var newTop = point.Y - console.WindowHeight + 1;
// If the cursor is already visible, and we're on the first
// page-worth of the buffer, then just scroll to the top (we can't
// scroll to before the beginning of the buffer).
//
// Note that we don't want to just return, because the window may
// have been scrolled way past the end of the content, so we really
// do need to set the new window top to 0 to bring it back into
// view.
if (newTop < 0)
{
newTop = 0;
}
// But if the cursor won't be visible, make sure it is.
if (newTop > console.CursorTop)
{
// Add 10 for some extra context instead of putting the
// cursor on the bottom line.
newTop = console.CursorTop - console.WindowHeight + 10;
}
// But we can't go past the end of the buffer.
if (newTop > (console.BufferHeight - console.WindowHeight))
{
newTop = (console.BufferHeight - console.WindowHeight);
}
console.SetWindowPosition(0, newTop);
}
}
}