Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 82 additions & 28 deletions PSReadLine/Prediction.Views.cs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ private class PredictionInlineView : PredictionViewBase
private uint? _predictorSession;
private string _suggestionText;
private string _lastInputText;
private int _renderedLength;
private bool _alreadyAccepted;

internal string SuggestionText => _suggestionText;
Expand Down Expand Up @@ -671,11 +672,59 @@ internal override void RenderSuggestion(List<StringBuilder> consoleBufferLines,
}

int inputLength = _inputText.Length;
int totalLength = _suggestionText.Length;

// Get the maximum buffer cells that could be available to the current command line.
int maxBufferCells = _singleton._console.BufferHeight * _singleton._console.BufferWidth - _singleton._initialX;
bool skipRendering = false;

// Assuming the suggestion text contains wide characters only (1 character takes up 2 buffer cells),
// if it still can fit in the console buffer, then we are all good; otherwise, it is possible that
// it could not fit, and thus more calculation is needed to check if that's really the case.
if (totalLength * 2 > maxBufferCells)
{
int length = SubstringLengthByCells(_suggestionText, maxBufferCells);
if (length <= inputLength)
{
// Even the user input cannot fit in the console buffer without having part of it scrolled up-off the buffer.
// We don't attempt to render the suggestion text in this case.
skipRendering = true;
}
else if (length < totalLength)
{
// The whole suggestion text cannot fit in the console buffer without having part of it scrolled up off the buffer.
// We truncate the end part and append ellipsis.

// We need to truncate 4 buffer cells ealier (just to be safe), so we have enough room to add the ellipsis.
int lenFromEnd = SubstringLengthByCellsFromEnd(_suggestionText, length - 1, countOfCells: 4);
totalLength = length - lenFromEnd;
if (totalLength <= inputLength)
{
// No suggestion left after truncation, so no need to render.
skipRendering = true;
}
}
}

if (skipRendering)
{
_renderedLength = 0;
return;
}

_renderedLength = totalLength;
StringBuilder currentLineBuffer = consoleBufferLines[currentLogicalLine];

currentLineBuffer.Append(_singleton._options._inlinePredictionColor)
.Append(_suggestionText, inputLength, _suggestionText.Length - inputLength)
.Append(VTColorUtils.AnsiReset);
currentLineBuffer
.Append(_singleton._options._inlinePredictionColor)
.Append(_suggestionText, inputLength, _renderedLength - inputLength);

if (_renderedLength < _suggestionText.Length)
{
currentLineBuffer.Append("...");
}

currentLineBuffer.Append(VTColorUtils.AnsiReset);
}

internal override void OnSuggestionAccepted()
Expand All @@ -702,34 +751,38 @@ internal override void Clear(bool cursorAtEol)
return;
}

int left, top;
int inputLen = _inputText.Length;
IConsole console = _singleton._console;

if (cursorAtEol)
if (_renderedLength > 0)
{
left = console.CursorLeft;
top = console.CursorTop;
console.BlankRestOfLine();
}
else
{
Point bufferEndPoint = _singleton.ConvertOffsetToPoint(inputLen);
left = bufferEndPoint.X;
top = bufferEndPoint.Y;
_singleton.WriteBlankRestOfLine(left, top);
}
// Clear the suggestion only if we actually rendered it.
int left, top;
int inputLen = _inputText.Length;
IConsole console = _singleton._console;

int bufferWidth = console.BufferWidth;
int columns = LengthInBufferCells(_suggestionText, inputLen, _suggestionText.Length);
if (cursorAtEol)
{
left = console.CursorLeft;
top = console.CursorTop;
console.BlankRestOfLine();
}
else
{
Point bufferEndPoint = _singleton.ConvertOffsetToPoint(inputLen);
left = bufferEndPoint.X;
top = bufferEndPoint.Y;
_singleton.WriteBlankRestOfLine(left, top);
}

int remainingLenInCells = bufferWidth - left;
columns -= remainingLenInCells;
if (columns > 0)
{
int extra = columns % bufferWidth > 0 ? 1 : 0;
int count = columns / bufferWidth + extra;
_singleton.WriteBlankLines(top + 1, count);
int bufferWidth = console.BufferWidth;
int columns = LengthInBufferCells(_suggestionText, inputLen, _renderedLength);

int remainingLenInCells = bufferWidth - left;
columns -= remainingLenInCells;
if (columns > 0)
{
int extra = columns % bufferWidth > 0 ? 1 : 0;
int count = columns / bufferWidth + extra;
_singleton.WriteBlankLines(top + 1, count);
}
}

Reset();
Expand All @@ -742,6 +795,7 @@ internal override void Reset()
_predictorId = Guid.Empty;
_predictorSession = null;
_alreadyAccepted = false;
_renderedLength = 0;
}

/// <summary>
Expand Down
24 changes: 18 additions & 6 deletions PSReadLine/Render.Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,22 @@ private static int SubstringLengthByCells(string text, int start, int countOfCel

for (int i = start; i < text.Length; i++)
{
if (cellLength >= countOfCells)
cellLength += LengthInBufferCells(text[i]);

if (cellLength > countOfCells)
{
return charLength;
}

cellLength += LengthInBufferCells(text[i]);
charLength++;

if (cellLength == countOfCells)
{
return charLength;
}
}

return 0;
return charLength;
}

private static int SubstringLengthByCellsFromEnd(string text, int countOfCells)
Expand All @@ -146,16 +152,22 @@ private static int SubstringLengthByCellsFromEnd(string text, int start, int cou

for (int i = start; i >= 0; i--)
{
if (cellLength >= countOfCells)
cellLength += LengthInBufferCells(text[i]);

if (cellLength > countOfCells)
{
return charLength;
}

cellLength += LengthInBufferCells(text[i]);
charLength++;

if (cellLength == countOfCells)
{
return charLength;
}
}

return 0;
return charLength;
}
}
}
2 changes: 1 addition & 1 deletion PSReadLine/UndoRedo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ private void RemoveEditsAfterUndo()
_edits.RemoveRange(_undoEditIndex, removeCount);
if (_edits.Count < _editGroupStart)
{
// Reset the group start index if any edits before setting the start mark were undone.
// Reset the group start index if any edits after setting the start mark were undone.
_editGroupStart = -1;
}
}
Expand Down
2 changes: 1 addition & 1 deletion PSReadLine/YankPaste.vi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private void SaveLinesToClipboard(int lineIndex, int lineCount)
}

/// <summary>
/// Remove a portion of text from the buffer, save it to the vi register
/// Remove a portion of text from the buffer, save it to the vi register
/// and also save it to the edit list to support undo.
/// </summary>
/// <param name="start"></param>
Expand Down
23 changes: 23 additions & 0 deletions test/InlinePredictionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -727,5 +727,28 @@ public void Inline_HistoryAndPluginSource_ExecutionStatus()

Assert.Null(_mockedMethods.lastCommandRunStatus);
}

[SkippableFact]
public void Inline_TruncateVeryLongSuggestion()
{
TestSetup(new TestConsole(width: 10, height: 2, keyboardLayout: _), KeyMode.Cmd);
using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView);

// Truncate long suggestion to make sure the user input is not scrolled up-off the console buffer.
SetHistory(new string('v', 25));
Test("vv", Keys(
'v', CheckThat(() => AssertScreenIs(2,
TokenClassification.Command, 'v',
TokenClassification.InlinePrediction, new string('v', 9),
TokenClassification.InlinePrediction, new string('v', 6) + "...")),
'v', CheckThat(() => AssertScreenIs(2,
TokenClassification.Command, "vv",
TokenClassification.InlinePrediction, new string('v', 8),
TokenClassification.InlinePrediction, new string('v', 6) + "...")),
// Once accepted, the suggestion text should be blanked out.
_.Enter, CheckThat(() => AssertScreenIs(1,
TokenClassification.Command, "vv"))
));
}
}
}
13 changes: 13 additions & 0 deletions test/MockConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ internal TestConsole(dynamic keyboardLayout)
ClearBuffer();
}

internal TestConsole(int width, int height, dynamic keyboardLayout)
{
_keyboardLayout = keyboardLayout;
BackgroundColor = ReadLine.BackgroundColors[0];
ForegroundColor = ReadLine.Colors[0];
CursorLeft = 0;
CursorTop = 0;
_bufferWidth = _windowWidth = width;
_bufferHeight = _windowHeight = height; // big enough to avoid the need to implement scrolling
buffer = new CHAR_INFO[BufferWidth * BufferHeight];
ClearBuffer();
}

internal void Init(object[] items)
{
this.index = 0;
Expand Down
7 changes: 6 additions & 1 deletion test/UnitTestReadLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -537,11 +537,16 @@ private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg)
=> VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true);

private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers)
{
TestSetup(console: null, keyMode, keyHandlers);
}

private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] keyHandlers)
{
Skip.If(WindowsConsoleFixtureHelper.GetKeyboardLayout() != this.Fixture.Lang,
$"Keyboard layout must be set to {this.Fixture.Lang}");

_console = new TestConsole(_);
_console = console ?? new TestConsole(_);
_mockedMethods = new MockedMethods();
var instance = (PSConsoleReadLine)typeof(PSConsoleReadLine)
.GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
Expand Down