mirror of
https://github.com/PowerShell/PSReadLine
synced 2026-06-08 12:12:13 +00:00
1323 lines
53 KiB
C#
1323 lines
53 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;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
public partial class PSConsoleReadLine
|
|
{
|
|
struct LineInfoForRendering
|
|
{
|
|
public int CurrentLogicalLineIndex;
|
|
public int CurrentPhysicalLineCount;
|
|
public int PreviousLogicalLineIndex;
|
|
public int PreviousPhysicalLineCount;
|
|
public int PseudoPhysicalLineOffset;
|
|
}
|
|
|
|
struct RenderedLineData
|
|
{
|
|
public string line;
|
|
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<StringBuilder> _consoleBufferLines = new List<StringBuilder>(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
|
|
{
|
|
lines = new[] { new RenderedLineData{ columns = 0, line = ""}}
|
|
};
|
|
private int _initialX;
|
|
private int _initialY;
|
|
private bool _waitingToRender;
|
|
|
|
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].line = line;
|
|
renderLines[i].columns = LengthInBufferCells(line);
|
|
}
|
|
|
|
// 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>
|
|
/// Given the length of a logical line, calculate the number of physical lines it takes to render
|
|
/// the logical line on the console.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <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 = PhysicalLineCount(renderLines[logicalLine].columns, logicalLine == 0, 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();
|
|
renderData.bufferWidth = bufferWidth;
|
|
renderData.bufferHeight = bufferHeight;
|
|
|
|
// 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.
|
|
LineInfoForRendering lineInfoForRendering;
|
|
CalculateWhereAndWhatToRender(cursorMovedToInitialPos, renderData, out lineInfoForRendering);
|
|
|
|
RenderedLineData[] previousRenderLines = _previousRender.lines;
|
|
int previousLogicalLine = lineInfoForRendering.PreviousLogicalLineIndex;
|
|
int previousPhysicalLine = lineInfoForRendering.PreviousPhysicalLineCount;
|
|
|
|
RenderedLineData[] renderLines = renderData.lines;
|
|
int logicalLine = lineInfoForRendering.CurrentLogicalLineIndex;
|
|
int physicalLine = lineInfoForRendering.CurrentPhysicalLineCount;
|
|
int pseudoPhysicalLineOffset = lineInfoForRendering.PseudoPhysicalLineOffset;
|
|
|
|
int lenPrevLastLine = 0;
|
|
int logicalLineStartIndex = logicalLine;
|
|
int physicalLineStartCount = physicalLine;
|
|
|
|
for (; logicalLine < renderLines.Length; logicalLine++)
|
|
{
|
|
if (logicalLine != logicalLineStartIndex) _console.Write("\n");
|
|
|
|
var lineData = renderLines[logicalLine];
|
|
_console.Write(lineData.line);
|
|
|
|
physicalLine += PhysicalLineCount(lineData.columns, logicalLine == 0, out int lenLastLine);
|
|
|
|
// Find the previous logical line (if any) that would have rendered
|
|
// the current physical line because we may need to clear it.
|
|
// 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 += PhysicalLineCount(previousRenderLines[previousLogicalLine].columns,
|
|
previousLogicalLine == 0,
|
|
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++;
|
|
var lenToClear = currentLines == previousPhysicalLine ? lenPrevLastLine : bufferWidth;
|
|
if (lenToClear > 0)
|
|
{
|
|
_console.Write(Spaces(lenToClear));
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
_console.Write(Spaces(previousRenderLines[line].columns));
|
|
}
|
|
|
|
// Preserve the current render data.
|
|
_previousRender = renderData;
|
|
|
|
// If we counted pseudo physical lines, deduct them to get the real physical line counts
|
|
// before updating '_initialY'.
|
|
physicalLine -= pseudoPhysicalLineOffset;
|
|
|
|
// Reset the colors after we've finished all our rendering.
|
|
_console.Write(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;
|
|
|
|
// 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()
|
|
{
|
|
if ((_previousRender.bufferWidth != _console.BufferWidth)
|
|
|| (_previousRender.bufferHeight != _console.BufferHeight))
|
|
{
|
|
// If the buffer width changed, our initial coordinates
|
|
// may have as well.
|
|
// Recompute X from the buffer width:
|
|
_initialX = _initialX % _console.BufferWidth;
|
|
|
|
// Recompute Y from the cursor
|
|
_initialY = 0;
|
|
var pt = ConvertOffsetToPoint(_current);
|
|
_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();
|
|
_previousRender.bufferWidth = _console.BufferWidth;
|
|
_previousRender.bufferHeight = _console.BufferHeight;
|
|
|
|
var point = ConvertOffsetToPoint(newCursor);
|
|
if (point.Y < 0)
|
|
{
|
|
Ding();
|
|
return;
|
|
}
|
|
|
|
if (point.Y == _console.BufferHeight)
|
|
{
|
|
// The cursor top exceeds the buffer height, so 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);
|
|
}
|
|
}
|
|
|
|
// While waiting to render, and a keybinding has occured 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;
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
}
|